Make /plan-agent:implementation-plan deliver every new plan as a published claude.ai artifact by default, demote the local .html file to a --file opt-in, and teach the plans gallery to card artifact-only plans by linking their artifact URL — a sibling .html, when published, always wins the card.
New plans become shareable claude.ai links the moment they are delivered, with no HTML file landing in the repo unless the author opts in with --file. It worked when the gallery cards an artifact-only plan by its URL, flips to the file when one exists, and bash scripts/verify.sh is green with the new gallery, hook, gate, and merge tests.
Read and implement all steps in the plan at docs/plans/add-artifact-default-plan-publishing.md — Publish plans as artifacts by default. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-artifact-default-plan-publishing.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
More ways to run this plan — goal & workflow prompts, file path
Achieve this goal: Publish plans as artifacts by default. The plan at docs/plans/add-artifact-default-plan-publishing.md describes one approach — use it as reference, but optimize for the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-artifact-default-plan-publishing.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
add-artifact-default-plan-publishing.html
docs/plans/add-artifact-default-plan-publishing.html
docs/plans/add-artifact-default-plan-publishing.md
Context
The story behind this plan — what prompted the work and why it matters now.
Today the skill renders <stem>.html beside the spec and delivers it over a throwaway local HTTP server — a preview only the author's machine can see, plus a 60–120 KB generated file in every plan commit. Publishing to a claude.ai artifact gives each plan a stable, shareable URL, and the plugin already owns every piece of the mechanism: the build-feature skill's Step 9 publishes feature docs and records artifact-url: in frontmatter so later rounds republish to the same page, the renderer already http(s)-guards URL-bearing keys (issue:, design:), and the separate artifact-tools:plan-artifact skill reads and writes the same artifact-url: key.
One risk shapes the design: the Artifact tool is model-side, so no test or hook can exercise a real publish. Everything around the publish — the gallery rule, the render-hook guard, the republish instruction inside the rendered prompts — is testable, and the skill text carries a fallback to --file delivery when a publish fails, so a broken publish path degrades to exactly today's behaviour instead of losing the plan.
Decisions
Choices already settled — read these before re-opening any of them.
- Publishing is inline in plan-agent via the Artifact tool, not delegated to
artifact-tools:plan-artifact— the default output path must not depend on another plugin being installed; the sharedartifact-url:frontmatter key keeps the two compatible (user-confirmed). - The opt-in flag is
--fileand it is additive: the artifact always publishes,--filealso writes the.htmlbeside the spec, and the file wins the gallery card; a failed or unavailable Artifact tool falls back to file delivery with a one-line notice, and that fallback plan staying file-mode forever is accepted — anyone can still publish it later viaartifact-tools:plan-artifact(interview-confirmed). - Later spec changes republish on a bounded cadence:
buildrepublishes at### Phase:boundaries and on completion,finalize-planon completion, and the renderer's verification-gate tail carries the completion republish instruction for fresh-session agents (interview-confirmed). - The skill re-reads the spec frontmatter immediately before every publish and passes
url:wheneverartifact-url:is already present — theplan-artifactpattern that closes the two-sessions-two-URLs race (interview-confirmed). - The gallery guards URL schemes itself, mirroring the renderer: a non-http(s)
artifact-url:gets no card and a stderr warning, because the generator writes raw hrefs into a page people open (interview-confirmed). - Artifact cards open in a new tab with
rel="noopener"and carry a visually-hidden "opens on claude.ai" note beside the existing sr-only status text; file cards keep same-tab navigation (interview-confirmed). - "A file is published" means a sibling
.htmlexists — one signal shared by the gallery's link precedence and the render hook's re-render guard, with no new frontmatter mode key. - The gallery cards artifact-only plans from the
.mdspec directly; noplan-artifact-urlmeta tag is added because no consumer reads one — the spec is the source the gallery already trusts. - Gallery cards gain a
data-localstem attribute — identical whether the card came from the file or the spec — and the union merge driver keys on it with an href fallback, because href stops being a stable identity once publishing can flip it (interview follow-up, user-confirmed). - RED settled the test contracts: the chip is
class="artifact-chip", the gallery warning and the renderer warning both readignoring non-http(s) artifact-url, and gate fixtures must not carry "republish" in their titles — the prompts embed the title, which poisoned the first assertion run. - The renderer has byte-identical root copies (scripts/build-plan-html.mjs, scripts/lib/plan-shell.mjs) that the tests import — the GREEN renderer step syncs them alongside the kit originals.
- Steps follow red-green-verify because the change is mostly runnable shell, Python, and Node code with
scripts/verify.shbehind it;workflow: neverbecause the byte-identical copy sync and the tests-fail-first ordering do not fan out safely. - GREEN step 5: an
artifact-url:alone does not make a document a plan.docs/plans/also holdstype: session-exportnotes that carry the same key, and keying the card rule on the key alone promoted two of them into the gallery (111 → 113 cards on real data). The spec walk now additionally requires the four sectionsbuild-plan-html.mjsrefuses to render without — Objective, Steps, Acceptance Criteria, Verification — so the gallery and the renderer share one definition of "is a plan". - GREEN step 5: the "no plan files found — skipping" guard moved from the raw file walk to the parsed entry list. A plans directory holding only specs the gallery cannot link now leaves an existing index.html alone instead of blanking it.
- GREEN step 6: the href fallback strips a trailing
.htmlrather than keying on the raw href. A bare-href fallback is correct for cards that predatedata-localbut wrong for the first merge after this change — a legacy side keysadd-foo.htmlwhile a regenerated side keysadd-foo, doubling every card in an already-committed index. Stripping the extension lands both on the same stem. - GREEN step 7: the Plans-tab parity check the step asks for lives in tests/plugins/test-gallery-artifact-cards.mjs rather than in a new file. That test already builds the fixture the comparison needs, so the three sibling generators run against the same plans directory the gallery just carded and their topbar totals are asserted equal to its card count — the drift is caught on every run instead of once by hand.
Files that change
Every file this plan touches, and what happens to each one.
kit/plugins/plan-agent/skills/implementation-plan/SKILL.mdmodified --file flag, Artifact allowed-tool, scratchpad render + publish delivery, fallbackkit/plugins/plan-agent/templates/plans-gallery.htmlmodified .artifact-chip colour rulekit/plugins/plan-agent/hooks/build-index.shmodified card artifact-only specs, artifact chip, count rulescripts/build-plans-index.shmodified byte-identical copy of build-index.shdocs/plans/build-index.shmodified byte-identical copy of build-index.sh- kit/plugins/plan-agent/hooks/
build-artifacts-index.shmodified plans_count includes artifact-only specsbuild-designs-index.shmodified same plans_count rulebuild-prototypes-index.shmodified same plans_count rulerender-plan-html.pymodified skip sibling render when none exists, still rebuild index
kit/plugins/plan-agent/scripts/build-plan-html.mjsmodified artifact-url key, conditional republish clause in the gate tailscripts/build-plan-html.mjsmodified byte-identical root copy of the rendererkit/plugins/plan-agent/skills/build/SKILL.mdmodified republish after re-render, Artifact allowed-toolkit/plugins/plan-agent/skills/finalize-plan/SKILL.mdmodified republish after completion re-render, Artifact allowed-toolkit/plugins/plan-agent/skills/plans-library/SKILL.mdmodified collection wording and empty-state cover artifact-only specskit/plugins/plan-agent/skills/review-plan/SKILL.mdmodified sibling-.html assumption audit, render-to-scratchpad where neededkit/plugins/plan-agent/skills/plans-open/SKILL.mdmodified sibling-.html assumption audit- kit/plugins/plan-agent/
README.mdmodified flag table and workflow notesCHANGELOG.mdmodified 9.7.0 entry
.claude-plugin/marketplace.jsonmodified version 9.6.1 → 9.7.0scripts/merge-plans-index.mjsmodified key cards on the data-local stem, href fallback- tests/plugins/
test-merge-gallery-index.shmodified divergent-identity merge casetest-gallery-artifact-cards.mjsnew objective test: gallery card ruletest-render-hook-artifact-skip.shnew hook guard testtest-artifact-url-gate.mjsnew renderer gate republish test
Steps
The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.
RED
artifact-url: and no sibling .html, a spec with a sibling .html, a spec with neither, and a spec whose artifact-url: is javascript:alert(1), run hooks/build-index.sh against it, and assert the first gets exactly one card whose href is the claude.ai URL with target="_blank", rel="noopener", an artifact chip in its meta row, and a visually-hidden "opens on claude.ai" span, the second gets exactly one same-tab card linking the relative .html path, the third and fourth get no card, and the page's plan count equals the cards emitted.
javascript: href into a generated page.node tests/plugins/test-gallery-artifact-cards.mjs exits non-zero with the first fixture's missing-card assertion — paste the failure output..html none is created while the gallery index is still rebuilt, and that with an existing sibling the sibling is re-rendered as today.
bash tests/plugins/test-render-hook-artifact-skip.sh exits non-zero with the sibling-was-created assertion — paste the failure output.artifact-url: https://claude.ai/public/artifacts/test-123, one without the key, and one with artifact-url: javascript:alert(1), asserting the plan-implement, plan-goal, and plan-workflow prompts carry a republish instruction naming the URL only in the first case, the gate is unchanged when the key is absent, and the javascript: value is dropped with a warning.
node tests/plugins/test-artifact-url-gate.mjs exits non-zero because no republish clause exists yet — paste the failure output..html path on the other — and assert exactly one card survives.
bash tests/plugins/test-merge-gallery-index.sh exits non-zero on the new case — paste the failure output.GREEN
.html walk, walk the same tree for .md specs whose frontmatter carries an http(s) artifact-url: (any other scheme skipped with a stderr warning) and whose stem has no .html in the collection, parse title from the # Plan: heading, status, type, effort, and created from frontmatter, and step totals from numbered items and [x] markers under ## Steps, emit a card whose href is the artifact URL with target="_blank" rel="noopener", an artifact chip in the meta row, and an sr-only "opens on claude.ai" span, stamp every card — file and artifact alike — with a data-local attribute holding the plan's plans-dir-relative stem (path without extension, so both sides of a publish flip share one key), fold the rule into the script's own plans-collection count, then copy the file byte-identical over scripts/build-plans-index.sh and docs/plans/build-index.sh.
.html for the existing walk to find, so without a spec walk it vanishes from the gallery, and the parity test holds the three copies together.node tests/plugins/test-gallery-artifact-cards.mjs and node tests/plugins/test-build-index-parity.mjs both exit 0.data-local stem, falling back to href for cards that predate the attribute.
bash tests/plugins/test-merge-gallery-index.sh exits 0.plans_count() in kit/plugins/plan-agent/hooks/build-artifacts-index.sh, build-designs-index.sh, and build-prototypes-index.sh.
bash tests/plugins/test-build-designs-index.sh and bash tests/plugins/test-build-prototypes-index.sh exit 0, and on the step-1 temp fixture each updated plans_count() returns the same number as the gallery's emitted cards.<stem>.html exists beside the written spec, skip the sibling render but still trigger the gallery-index rebuild; when the sibling exists, re-render it exactly as today.
bash tests/plugins/test-render-hook-artifact-skip.sh exits 0.artifact-url: frontmatter key, http(s)-guarded exactly like design:, and append to the shared verification-gate tail — only when the key is present — an instruction to republish the plan artifact to that URL via the Artifact tool's url parameter after the re-render.
node tests/plugins/test-artifact-url-gate.mjs and node tests/plugins/test-build-plan-html.mjs both exit 0.--file to the Flags list and argument-hint, add Artifact to allowed-tools, update the description frontmatter, and replace Steps 5d and 7 so every plan renders <stem>.html into the session scratchpad and publishes it with the Artifact tool — re-reading the spec frontmatter immediately beforehand and passing url: whenever artifact-url: is already present, otherwise writing the returned URL back as artifact-url: — reports the URL, and sends the .md spec via SendUserFile; --file additionally writes the .html beside the spec and runs today's browser-preview flow, and a failed or unavailable publish falls back to file delivery with a one-line notice.
grep -q -- '--file' kit/plugins/plan-agent/skills/implementation-plan/SKILL.md passes, grep -q 'artifact-url' kit/plugins/plan-agent/skills/implementation-plan/SKILL.md passes, and the description frontmatter line stays within the 200-character budget by wc -c..html assumptions: in kit/plugins/plan-agent/skills/build/SKILL.md add "republish via the Artifact tool with url: at each ### Phase: boundary stop and on completion, when frontmatter carries artifact-url: and no sibling .html exists"; in skills/finalize-plan/SKILL.md add the same republish rule for the completion re-render; add Artifact to both allowed-tools lines; in skills/plans-library/SKILL.md update the collection description and the no-plans empty-state message to cover artifact-only specs; then audit skills/review-plan, plans-open, plan-status, documenting-plans, and markdown-to-html for paths that expect the .html to exist — fixing each (a reviewer or opener renders the spec to the scratchpad when no sibling exists) or recording it as confirmed sibling-free.
.html makes artifact-only plans second-class inside their own plugin.grep -l 'artifact-url' kit/plugins/plan-agent/skills/build/SKILL.md kit/plugins/plan-agent/skills/finalize-plan/SKILL.md kit/plugins/plan-agent/skills/plans-library/SKILL.md lists all three files, and the step's output names every audited skill as updated or confirmed sibling-free.--file and artifact-default delivery.
git fetch origin && BASE_REF=main node scripts/check-plugin-versions.mjs exits 0 and grep -q '9.7.0' kit/plugins/plan-agent/CHANGELOG.md passes.VERIFY
bash scripts/verify.sh.
SKIP (not configured) line counted as a pass.artifact-url:, simulate the hook event and confirm no sibling appears while the index rebuilds, run plan-agent-plans-index and confirm the card's href is the claude.ai URL, then add a sibling .html, re-run, and confirm the same plan's card now links the file.
.html path for the same stem.Tests
The tests that prove the change does what it promises.
.html wins the card. File: tests/plugins/test-gallery-artifact-cards.mjs; Type: integration; Asserts: spec with artifact-url: and no sibling gets one new-tab noopener card with the claude.ai href, artifact chip, and sr-only destination note, sibling-.html spec gets one same-tab card with the file href, keyless and javascript:-scheme specs get none, count matches cards, and the artifacts/designs/prototypes topbars print the same Plans total as the gallery emits cards; Run: node tests/plugins/test-gallery-artifact-cards.mjsartifact-url: present → all three prompts name the URL, absent → gate unchanged, javascript: scheme dropped with warning; Run: node tests/plugins/test-artifact-url-gate.mjsDefinition of done
The plan counts as done when every statement below is true — check each one off as you verify it.
Final check
One last pass to confirm the whole change works end to end.
Run bash scripts/verify.sh from the repo root and require exit 0, with tests/plugins/test-gallery-artifact-cards.mjs, test-render-hook-artifact-skip.sh, test-artifact-url-gate.mjs, the extended test-merge-gallery-index.sh, and the pre-existing test-build-index-parity.mjs all reported green — a SKIP (not configured) line is a skip, not a pass. Then run the step-14 walk: one temp spec, hook event, gallery build, sibling added, gallery rebuilt — the same plan's card href must read as the claude.ai artifact URL in the first index.html and as the relative .html path in the second. Finally, git fetch origin && BASE_REF=main node scripts/check-plugin-versions.mjs must exit 0 with plan-agent at 9.7.0.
The one piece no test can exercise is a real Artifact publish, because the Artifact tool is model-side. That path is verified on the feature's first live use; until then the skill text's fallback guarantees a failed publish degrades to exactly today's --file delivery rather than losing the plan.
Wrapping up
Three gates that must all pass before this plan is marked completed.
Completion Report
No items to report — all requirements met.