Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0-canary.5
0.1.0-canary.6
Binary file added plugins/claude/.claude-plugin/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion plugins/claude/.claude-plugin/icon.svg

This file was deleted.

3 changes: 2 additions & 1 deletion plugins/claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
4 changes: 2 additions & 2 deletions plugins/claude/SOURCE.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/SOURCE.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion plugins/cursor/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion plugins/cursor/SOURCE.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
19 changes: 9 additions & 10 deletions scripts/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import argparse
import base64
import gzip
import hashlib
import io
Expand Down Expand Up @@ -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'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
f'viewBox="0 0 {width} {height}"><image width="{width}" height="{height}" '
f'href="data:image/png;base64,{data}"/></svg>\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]:
Expand All @@ -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": {
Expand Down
15 changes: 9 additions & 6 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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"<svg/>" + bytes(32)):
self.assertEqual(sync.icon_png(png(256, 256)), png(256, 256))
for bad in (png(256, 128), png(64, 64), b"<svg/>" + 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()
Expand Down
Loading