diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e1b322..7207dd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 0.1.0-canary.4
+
+- Use the vllnt logo as the Claude plugin icon: `assets/vllnt-logo.png` is wrapped by the generator into `.claude-plugin/icon.svg`.
+
- Remove distribution version numbers from the README so routine version bumps need no README edits; `VERSION` and this changelog remain the version records.
- Record the `v0.1.0-canary.3` GitHub prerelease.
diff --git a/README.md b/README.md
index 81ea789..d89b144 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Then start a new chat and invoke a namespaced skill such as `/vstack:plan-work`.
| `upstream.lock.json` | Trusted upstream repository and immutable commit |
| `VERSION` | Shared distribution version |
| `scripts/sync.py` | Standard-library generator and read-only drift check |
-| `assets/claude-icon.svg` | Claude plugin icon, copied into the Claude package as `.claude-plugin/icon.svg` |
+| `assets/vllnt-logo.png` | vllnt logo; the generator wraps it as the Claude package's `.claude-plugin/icon.svg` |
| `plugins/claude/` | Generated Claude Code native plugin |
| `plugins/codex/` | Generated portable Agent Plugin for Codex |
| `plugins/cursor/` | Generated Cursor native plugin and principles rule |
diff --git a/VERSION b/VERSION
index 6cf7a46..cc9a41d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0-canary.3
+0.1.0-canary.4
diff --git a/assets/claude-icon.svg b/assets/claude-icon.svg
deleted file mode 100644
index 1f4882c..0000000
--- a/assets/claude-icon.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/assets/vllnt-logo.png b/assets/vllnt-logo.png
new file mode 100644
index 0000000..10b7149
Binary files /dev/null and b/assets/vllnt-logo.png differ
diff --git a/plugins/claude/.claude-plugin/icon.svg b/plugins/claude/.claude-plugin/icon.svg
index 1f4882c..e112555 100644
--- a/plugins/claude/.claude-plugin/icon.svg
+++ b/plugins/claude/.claude-plugin/icon.svg
@@ -1,10 +1 @@
-
+
diff --git a/plugins/claude/.claude-plugin/plugin.json b/plugins/claude/.claude-plugin/plugin.json
index 204a497..8564a6a 100644
--- a/plugins/claude/.claude-plugin/plugin.json
+++ b/plugins/claude/.claude-plugin/plugin.json
@@ -13,5 +13,5 @@
"license": "MIT",
"name": "vstack",
"repository": "https://github.com/vllnt/stack",
- "version": "0.1.0-canary.3"
+ "version": "0.1.0-canary.4"
}
diff --git a/plugins/claude/SOURCE.json b/plugins/claude/SOURCE.json
index 7cf958e..29fb7a8 100644
--- a/plugins/claude/SOURCE.json
+++ b/plugins/claude/SOURCE.json
@@ -1,7 +1,7 @@
{
"files": {
- ".claude-plugin/icon.svg": "dc3b420724330eed1296c63c5c6d3290fa031d0eceb39180a2258f6992c76108",
- ".claude-plugin/plugin.json": "ff44d6e276f70392c84dcfa3cf86f33ce5001ab864781dae8adbadbc90102689",
+ ".claude-plugin/icon.svg": "0b4b639f6df841b4904ec0cd9f6d38131c6373ce4a52a1e7fe6452a37598d7f1",
+ ".claude-plugin/plugin.json": "4808262e4a7a3e327124bfd2b6e23590f33502b981cfcc4e893be02674ab4a53",
"LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559",
"skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8",
"skills/build-landing-page/SKILL.md": "b08e4963d6149f3647b28a0d9dccdc8793812abab95a1d081d93699e142de0f0",
diff --git a/plugins/codex/SOURCE.json b/plugins/codex/SOURCE.json
index ce064ad..f10d7d2 100644
--- a/plugins/codex/SOURCE.json
+++ b/plugins/codex/SOURCE.json
@@ -1,7 +1,7 @@
{
"files": {
"LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559",
- "plugin.json": "aea0fdbea75ce7f02be70116bf560bcffc31258b6def32b79d302f2aee307dd4",
+ "plugin.json": "789b793f660535ffddadde5268e5d776ea9a76db6b4809014eb71537d877f235",
"skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8",
"skills/build-landing-page/SKILL.md": "b08e4963d6149f3647b28a0d9dccdc8793812abab95a1d081d93699e142de0f0",
"skills/build-landing-page/references/copywriting-formulas.md": "6d19f6166e1badcfdda0af4f8b303160e94ee6e6149321157e12849654cc013e",
diff --git a/plugins/codex/plugin.json b/plugins/codex/plugin.json
index 9569b5b..ab43f83 100644
--- a/plugins/codex/plugin.json
+++ b/plugins/codex/plugin.json
@@ -6,5 +6,5 @@
"description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).",
"license": "MIT",
"name": "vstack",
- "version": "0.1.0-canary.3"
+ "version": "0.1.0-canary.4"
}
diff --git a/plugins/cursor/.cursor-plugin/plugin.json b/plugins/cursor/.cursor-plugin/plugin.json
index d93bf60..8dcbac1 100644
--- a/plugins/cursor/.cursor-plugin/plugin.json
+++ b/plugins/cursor/.cursor-plugin/plugin.json
@@ -5,5 +5,5 @@
"description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).",
"license": "MIT",
"name": "vstack",
- "version": "0.1.0-canary.3"
+ "version": "0.1.0-canary.4"
}
diff --git a/plugins/cursor/SOURCE.json b/plugins/cursor/SOURCE.json
index b2c3d24..2c247bb 100644
--- a/plugins/cursor/SOURCE.json
+++ b/plugins/cursor/SOURCE.json
@@ -1,6 +1,6 @@
{
"files": {
- ".cursor-plugin/plugin.json": "3fbf8954a01a9f6a73fec13119d07eaa08e465fb3ac59c27cf711685cdd860f3",
+ ".cursor-plugin/plugin.json": "b4325fd2a33221ef271b6b92d621c9c62052b6bd6f468b567d47401fe35ee2ea",
"LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559",
"rules/vstack-principles.mdc": "5234bc0506e0a328d6163cd83785d2f37dd1fc23ef84287d71e03b92459e064a",
"skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8",
diff --git a/scripts/sync.py b/scripts/sync.py
index 6320550..dd6dac1 100644
--- a/scripts/sync.py
+++ b/scripts/sync.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
+import base64
import gzip
import hashlib
import io
@@ -12,6 +13,7 @@
import re
import selectors
import shutil
+import struct
import subprocess
import tarfile
import tempfile
@@ -24,7 +26,7 @@
HOMEPAGE = "https://vllnt.com"
DESCRIPTION = ("Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. "
"Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).")
-CLAUDE_ICON = ROOT / "assets" / "claude-icon.svg"
+CLAUDE_ICON = ROOT / "assets" / "vllnt-logo.png"
HOSTS = ("claude", "codex", "cursor")
PRINCIPLES = tuple(f"vllnt-{name}-principles" for name in ("thinking", "orchestration", "collaboration"))
MAX_ARCHIVE = 32 * 1024 * 1024
@@ -217,6 +219,19 @@ def payload(files: dict[str, bytes]) -> tuple[dict[str, bytes], bytes]:
return skills, rule.encode()
+def icon_svg(png: bytes) -> bytes:
+ """Wrap a square PNG in the SVG file Claude's plugin directory looks for."""
+ if png[:8] != b"\x89PNG\r\n\x1a\n":
+ raise Invalid("Claude icon must be a PNG")
+ width, height = struct.unpack(">II", png[16:24])
+ if width != height or width < 128:
+ raise Invalid("Claude icon must be square and at least 128px")
+ data = base64.b64encode(png).decode()
+ return (f'\n').encode()
+
+
def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes]:
if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?", version):
raise Invalid("VERSION must contain a semantic version")
@@ -235,7 +250,7 @@ def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes]
"cursor": ".cursor-plugin/plugin.json"}[host]
package = {**skills, "LICENSE": files["LICENSE"], manifest_path: encoded(manifest)}
if host == "claude":
- package[".claude-plugin/icon.svg"] = CLAUDE_ICON.read_bytes()
+ package[".claude-plugin/icon.svg"] = icon_svg(CLAUDE_ICON.read_bytes())
if host == "cursor":
package["rules/vstack-principles.mdc"] = rule
package["SOURCE.json"] = encoded({"upstream": lock, "files": {
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 081f8b3..0f36d2c 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -4,6 +4,7 @@
import io
import json
from pathlib import Path
+import struct
import subprocess
import tarfile
import tempfile
@@ -54,6 +55,7 @@ def test_deterministic_standalone_packages_and_provenance(self):
self.assertEqual("rules/vstack-principles.mdc" in package, host == "cursor")
self.assertEqual(package["LICENSE"], files["LICENSE"])
self.assertEqual(".claude-plugin/icon.svg" in package, host == "claude")
+ self.assertIn(b"data:image/png;base64,", first["plugins/claude/.claude-plugin/icon.svg"])
claude = json.loads(first[".claude-plugin/marketplace.json"])
codex = json.loads(first[".agents/plugins/marketplace.json"])
cursor = json.loads(first[".cursor-plugin/marketplace.json"])
@@ -70,6 +72,13 @@ def test_deterministic_standalone_packages_and_provenance(self):
self.assertNotIn("repository", manifests["codex"])
self.assertNotIn("repository", manifests["cursor"])
+ def test_claude_icon_requires_square_png(self):
+ png = lambda w, h: b"\x89PNG\r\n\x1a\n" + b"\0\0\0\rIHDR" + struct.pack(">II", w, h)
+ self.assertIn(b'viewBox="0 0 256 256"', sync.icon_svg(png(256, 256)))
+ for bad in (png(256, 128), png(64, 64), b"" + bytes(32)):
+ with self.assertRaises(sync.Invalid):
+ sync.icon_svg(bad)
+
def test_added_and_removed_workflows(self):
files = source()
files["workflows/another-task/SKILL.md"] = skill("another-task")