diff --git a/CHANGELOG.md b/CHANGELOG.md index c261c09..4ee3901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.1.0-canary.6 + +- Fix the Claude plugin icon being rejected as `ICON_INVALID`: ship the vllnt logo as `.claude-plugin/icon.png`, named by `icon` in `plugin.json`, instead of an SVG that embedded the PNG. + - Remove the previous marketplace name from the README; the migration note stays in this changelog. ## 0.1.0-canary.5 diff --git a/README.md b/README.md index ae20711..0195ae8 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/vllnt-logo.png` | vllnt logo; the generator wraps it as the Claude package's `.claude-plugin/icon.svg` | +| `assets/vllnt-logo.png` | vllnt logo; the generator ships it as the Claude package's `.claude-plugin/icon.png`, named by `icon` in `plugin.json` | | `plugins/claude/` | Generated Claude Code native plugin | | `plugins/codex/` | Generated portable Agent Plugin for Codex | | `plugins/cursor/` | Generated Cursor native plugin and principles rule | @@ -149,7 +149,7 @@ Automatic repository synchronization was independently verified on 2026-09-21. A Live testing caught a generated-help false hold and post-push PR-head propagation timing; fixes were reviewed and merged through PRs #5 and #6, with 37 regression tests passing. The fresh PR #7 case required neither manual metadata repair nor manual merge. Daily fallback is configured; its timer was not waited for. Push delivery, manual resends, automatic merging, and manual no-op execution were observed. -Claude Code 2.1.283 (2026-09-27): `claude plugin validate` passes for the catalog and plugin; a local-directory marketplace install into an isolated configuration installed the `vstack` plugin with all 22 skills, and a maintainer session with the local install listed every `vstack:*` skill. After `vllnt/stack` became public (merge `c3ad08c08fe460ed4f2e1a40630afe4001ddd23c`), an anonymous clone succeeded, and the CLI equivalents `claude plugin marketplace add vllnt/stack` plus the matching `claude plugin install` command in a clean, credential-free configuration installed the plugin from the GitHub source; a headless session there listed exactly the 22 `vstack:*` skills. The same commands after merge `915a4d0d519d726f84185ec698b3ed3f4c925cb8` installed it again with its icon, updated description, and 22 skills. The in-chat `/plugin` forms were not run separately. The marketplace was later renamed to `vllnt`; see the changelog for migrating earlier installs. Skill invocation behavior (including `plan-work` staying read-only), automatic skill selection, and the rest of the acceptance checklist remain unverified. +Claude Code 2.1.283 (2026-09-27): `claude plugin validate` passes for the catalog and plugin; a local-directory marketplace install into an isolated configuration installed the `vstack` plugin with all 22 skills, and a maintainer session with the local install listed every `vstack:*` skill. After `vllnt/stack` became public (merge `c3ad08c08fe460ed4f2e1a40630afe4001ddd23c`), an anonymous clone succeeded, and the CLI equivalents `claude plugin marketplace add vllnt/stack` plus the matching `claude plugin install` command in a clean, credential-free configuration installed the plugin from the GitHub source; a headless session there listed exactly the 22 `vstack:*` skills. The same commands after merge `915a4d0d519d726f84185ec698b3ed3f4c925cb8` installed it again with its icon, updated description, and 22 skills. The in-chat `/plugin` forms were not run separately. The marketplace listing later rejected that SVG icon (a PNG embedded in SVG) as `ICON_INVALID`; the replacement `.claude-plugin/icon.png` passes `claude plugin validate --strict`, but its marketplace acceptance and rendering remain unverified. The marketplace was later renamed to `vllnt`; see the changelog for migrating earlier installs. Skill invocation behavior (including `plan-work` staying read-only), automatic skill selection, and the rest of the acceptance checklist remain unverified. **Codex and Cursor installation, and startup/context-loss behavior in every host, remain unverified.** The repository is public and installable as a GitHub marketplace. A Claude plugin directory submission is in validation and is not listed yet. Canary builds are published as [GitHub prereleases](https://github.com/vllnt/stack/releases); releases are created manually. No automatic Claude/Codex principle loading exists. diff --git a/VERSION b/VERSION index 20eaaf3..8a85b8d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0-canary.5 +0.1.0-canary.6 diff --git a/plugins/claude/.claude-plugin/icon.png b/plugins/claude/.claude-plugin/icon.png new file mode 100644 index 0000000..10b7149 Binary files /dev/null and b/plugins/claude/.claude-plugin/icon.png differ diff --git a/plugins/claude/.claude-plugin/icon.svg b/plugins/claude/.claude-plugin/icon.svg deleted file mode 100644 index e112555..0000000 --- a/plugins/claude/.claude-plugin/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/plugins/claude/.claude-plugin/plugin.json b/plugins/claude/.claude-plugin/plugin.json index f7edd43..598afe1 100644 --- a/plugins/claude/.claude-plugin/plugin.json +++ b/plugins/claude/.claude-plugin/plugin.json @@ -4,6 +4,7 @@ }, "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).", "homepage": "https://vllnt.com", + "icon": "./.claude-plugin/icon.png", "keywords": [ "workflows", "engineering", @@ -13,5 +14,5 @@ "license": "MIT", "name": "vstack", "repository": "https://github.com/vllnt/stack", - "version": "0.1.0-canary.5" + "version": "0.1.0-canary.6" } diff --git a/plugins/claude/SOURCE.json b/plugins/claude/SOURCE.json index ba64368..9368076 100644 --- a/plugins/claude/SOURCE.json +++ b/plugins/claude/SOURCE.json @@ -1,7 +1,7 @@ { "files": { - ".claude-plugin/icon.svg": "0b4b639f6df841b4904ec0cd9f6d38131c6373ce4a52a1e7fe6452a37598d7f1", - ".claude-plugin/plugin.json": "075f3e05fe3733d2930d0aaaf378d02758436dfa4f4006c34eda3c505b48e1ef", + ".claude-plugin/icon.png": "71502ebbc531cc46866dffe33dae3207ad22cf529f801ec971bb69114c064608", + ".claude-plugin/plugin.json": "60311ca1334975967e4ae9ae7710b7b57a9a854f35b5fff3b8da26a008e8c63b", "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 4c34085..3513f3a 100644 --- a/plugins/codex/SOURCE.json +++ b/plugins/codex/SOURCE.json @@ -1,7 +1,7 @@ { "files": { "LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559", - "plugin.json": "c78e76ede21d79745dc7572f7ba3f563d9296d9d1c3404cedc217f96597f320b", + "plugin.json": "1ba701827e2e216e367635d59628b4e5e6d599090dc2ea73bd0a2afe996aff2b", "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 c7cc4fd..499ffaf 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.5" + "version": "0.1.0-canary.6" } diff --git a/plugins/cursor/.cursor-plugin/plugin.json b/plugins/cursor/.cursor-plugin/plugin.json index 3f06dc7..72b4293 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.5" + "version": "0.1.0-canary.6" } diff --git a/plugins/cursor/SOURCE.json b/plugins/cursor/SOURCE.json index c70f232..036df41 100644 --- a/plugins/cursor/SOURCE.json +++ b/plugins/cursor/SOURCE.json @@ -1,6 +1,6 @@ { "files": { - ".cursor-plugin/plugin.json": "63c0531e7ec6ff67f7fecd54ee886f3d10706dbab51820807b373378ce7ee4b3", + ".cursor-plugin/plugin.json": "b8908a118e691f11f901524b4f28b372cf35699b60392575c2a4a5dfaf3923a9", "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 6d52e6e..d098b54 100644 --- a/scripts/sync.py +++ b/scripts/sync.py @@ -3,7 +3,6 @@ from __future__ import annotations import argparse -import base64 import gzip import hashlib import io @@ -220,17 +219,16 @@ 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": +def icon_png(png: bytes) -> bytes: + """Check the PNG shipped as Claude's plugin icon; listings reject SVGs with embedded images.""" + if png[:8] != b"\x89PNG\r\n\x1a\n" or png[12:16] != b"IHDR": 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() + if len(png) > MAX_FILE: + raise Invalid("Claude icon must be at most 2 MiB") + return png def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes]: @@ -244,14 +242,15 @@ def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes] "author": {"name": "vllnt"}, "license": "MIT"} if host == "claude": manifest.update({"homepage": HOMEPAGE, "repository": REPOSITORY, - "keywords": ["workflows", "engineering", "principles", "skills"]}) + "keywords": ["workflows", "engineering", "principles", "skills"], + "icon": "./.claude-plugin/icon.png"}) if host == "codex": manifest["$schema"] = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" manifest_path = {"claude": ".claude-plugin/plugin.json", "codex": "plugin.json", "cursor": ".cursor-plugin/plugin.json"}[host] package = {**skills, "LICENSE": files["LICENSE"], manifest_path: encoded(manifest)} if host == "claude": - package[".claude-plugin/icon.svg"] = icon_svg(CLAUDE_ICON.read_bytes()) + package[".claude-plugin/icon.png"] = icon_png(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 4257f1c..8ad87f5 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -54,8 +54,9 @@ def test_deterministic_standalone_packages_and_provenance(self): self.assertEqual(package["skills/test-work/SKILL.md"], files["workflows/test-work/SKILL.md"]) 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"]) + self.assertEqual(".claude-plugin/icon.png" in package, host == "claude") + self.assertNotIn(".claude-plugin/icon.svg", package) + self.assertEqual(first["plugins/claude/.claude-plugin/icon.png"], sync.CLAUDE_ICON.read_bytes()) 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,15 +71,17 @@ def test_deterministic_standalone_packages_and_provenance(self): ("cursor", "plugins/cursor/.cursor-plugin/plugin.json"))} self.assertEqual(manifests["claude"]["repository"], "https://github.com/vllnt/stack") self.assertEqual(manifests["claude"]["homepage"], "https://vllnt.com") + self.assertEqual(manifests["claude"]["icon"], "./.claude-plugin/icon.png") + self.assertNotIn("icon", manifests["codex"]) self.assertNotIn("repository", manifests["codex"]) self.assertNotIn("repository", manifests["cursor"]) - def test_claude_icon_requires_square_png(self): + def test_claude_icon_requires_small_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)): + self.assertEqual(sync.icon_png(png(256, 256)), png(256, 256)) + for bad in (png(256, 128), png(64, 64), b"" + bytes(32), png(256, 256) + bytes(sync.MAX_FILE)): with self.assertRaises(sync.Invalid): - sync.icon_svg(bad) + sync.icon_png(bad) def test_added_and_removed_workflows(self): files = source()