Give every plan a back-link to its prototype, persist the prototype's derived data model in both files, and add a PostToolUse hook that flags when a prototype has drifted from its own model or from its plan's copy of it.
A prototype already knows which plan it came from, but the plan has no idea a prototype exists, and neither file records the data model they supposedly share. This puts a link on both ends and a durable model block in both files, so a hook can say "these two have drifted apart" instead of everyone finding out at implementation time.
Read and implement all steps in the plan at docs/plans/add-prototype-plan-linking.md — Make a plan and its prototype aware of each other. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-prototype-plan-linking.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: Make a plan and its prototype aware of each other. The plan at docs/plans/add-prototype-plan-linking.md describes one approach — use it as reference, but optimize for the outcome. Fan out across parallel subagents where that serves the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-prototype-plan-linking.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.
Run a workflow to implement the plan at docs/plans/add-prototype-plan-linking.md — Make a plan and its prototype aware of each other. Brief subagents with the plan file at docs/plans/add-prototype-plan-linking.md. Reserve a final verification phase for the lead agent, not a subagent. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-prototype-plan-linking.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-prototype-plan-linking.html
docs/plans/add-prototype-plan-linking.html
docs/plans/add-prototype-plan-linking.md
Context
The story behind this plan — what prompted the work and why it matters now.
/plan-agent:prototype already stamps <meta name="proto-source"> into every
prototype it generates, and build-prototypes-index.sh renders it on the
gallery card as "from <plan>". The link is one-directional: open a plan and
nothing tells you a prototype exists.
The deeper gap is that the two artifacts have no shared, machine-readable
overlap. Step 3 of the prototype skill derives a data model — entity, fields
with inferred types, primary action, success signal — and then discards it once
the skeleton is filled. Nothing durable survives to compare against later.
That asymmetry is why full bidirectional sync was rejected. Plans are
markdown-source plus rendered HTML; prototypes have no spec file, so the HTML
is the source. Propagating a prototype edit back into plan prose would need an
HTML-to-model parser over generated files, which CLAUDE.md forbids. The design
here is narrower and holds: plan to prototype is regeneration (re-run/plan-agent:prototype, which Step 3 guarantees is deterministic), prototype
to plan is detection (compare two JSON blobs and report).
Three limitations, all accepted:
The drift hook compares structure, not intent. A hand-edit that changes the
prototype's rendered columns is caught, because the check compares the model
block against the prototype's own <th> headers and form field names. A
hand-edit that only changes copy, styling, or seed values is not caught, and
should not be.
Detection runs one way only — prototype HTML against plan frontmatter. A plan
whose proto-model: is hand-edited directly, bypassing the skill's write-back,
desyncs with no signal. Plans are user-owned prose rather than generated output,
so this is tolerated rather than guarded.
The frontmatter write-back has no transaction semantics. Two concurrent
sessions touching the same plan spec could drop one writer's update. Unlikely in
single-session use and not worth locking for.
docs/prototypes/ currently holds only index.html — no real prototype exists
yet. Everything here is verified against fixtures rather than live artifacts.
Two duplication facts shape several steps below. The renderer exists twice —scripts/build-plan-html.mjs and scripts/lib/plan-shell.mjs are the sources,
re-copied byte-for-byte under kit/plugins/plan-agent/scripts/, withtests/plugins/test-build-plan-html.mjs:557 asserting the parity. The plans-index
builder exists three times, all byte-identical. Every change here must land in
every copy, or the suite fails and gallery regeneration silently drops the chip.
Files that change
Every file this plan touches, and what happens to each one.
kit/plugins/plan-agent/skills/prototype/reference/PROTOTYPE-SKELETON.htmlmodified add the#proto-modelJSON blockkit/plugins/plan-agent/skills/prototype/SKILL.mdmodified pin theproto-sourceformat, serialize the model, write the link back into the source plan specscripts/lib/plan-shell.mjsmodified optionalplan-prototypemeta tag and the header link (repo-root source)kit/plugins/plan-agent/scripts/lib/plan-shell.mjsmodified byte-identical re-copy of the abovescripts/build-plan-html.mjsmodified thread theprototypefrontmatter key into bothmetaTags()andheader()(repo-root source)kit/plugins/plan-agent/scripts/build-plan-html.mjsmodified byte-identical re-copy of the abovekit/plugins/plan-agent/hooks/build-index.shmodified prototype chip on the plans gallery cardscripts/build-plans-index.shmodified byte-identical re-copy of the abovedocs/plans/build-index.shmodified byte-identical re-copy of the above- kit/plugins/plan-agent/hooks/
check-prototype-drift.pynew the drift comparisondispatch.pymodified fan out to the drift check on prototype writes
- kit/plugins/plan-agent/
README.mdmodified document the new hook and theprototype:/plan-prototypekeysCHANGELOG.mdmodified 4.4.0 entry
.claude-plugin/marketplace.jsonmodified bump plan-agent 4.3.1 to 4.4.0- tests/plugins/
test-prototype-plan-link.mjsnew objective testtest-prototype-drift.shnew drift-hook casestest-build-plan-html.mjsmodified back-compat case for specs with noprototype:key
Steps
The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.
<script type="application/json" id="proto-model">{{PROTO_MODEL}}</script> to PROTOTYPE-SKELETON.html immediately after the existing #seed block at line 63.
#seed keeps both machine-readable blocks together.grep -c 'id="proto-model"' kit/plugins/plan-agent/skills/prototype/reference/PROTOTYPE-SKELETON.html prints 1.{{SOURCE_PLAN}} contract in skills/prototype/SKILL.md: on the plan path it is the repo-relative path of the plan's markdown spec (docs/plans/<slug>.md), not a title or free text; on the idea, image, and Figma paths it stays empty.
proto-source, and today that token is undefined free text that only ever fed the gallery card's display string — without a format contract the whole comparison is unimplementable.build-prototypes-index.sh still renders the value as its "from <plan>" card text.skills/prototype/SKILL.md to substitute {{PROTO_MODEL}} with the Step 3 model serialized as compact single-line JSON — keys entity, fields (each {name, type}), action, successSignal — using the same script-breakout escaping rule already documented for {{SEED_JSON}}, never HTML escaping.
JSON.parse on an HTML-escaped block fails the same way it would for the seed, and the existing rule is already written down one paragraph above.{{PROTO_MODEL}} in its placeholder list and states the script-breakout rule applies to it.skills/prototype/SKILL.md that runs before the prototype HTML is written in Step 6, resolving the plan-path input's .html to its sibling .md spec by extension swap and writing prototype: docs/prototypes/<slug>.html plus proto-model: <compact single-line JSON> into that spec's frontmatter — plan path only, skipped for idea, image, and Figma inputs. When the sibling .md does not exist, skip the write-back entirely, still generate the prototype, and print one line telling the user to run node scripts/extract-plan-spec.mjs <plan>.html > <plan>.md first if they want the back-link. The JSON must be single-line: never pretty-printed, never containing a raw newline or a bare ---.
docs/plans/ are legacy HTML with no spec sibling, so a blind extension swap would fail generation or write an empty spec for the majority of real inputs — and materializing a spec as a side effect of prototyping would silently rewrite a plan the user never asked us to touch. The frontmatter parser is also a naive line scanner, so an embedded newline or --- silently truncates the block and corrupts status and created for all three consumers that re-scan it.node scripts/extract-plan-spec.mjs still parses the plan; run it against a legacy HTML plan with no sibling and confirm the prototype is still written, no spec is created, and the notice is printed.prototype through the renderer in three places: read parsed.metadata.prototype in build-plan-html.mjs near the existing parsed.metadata.created handling; pass it to shell.metaTags(), which emits <meta name="plan-prototype"> conditionally; and extend shell.header()'s parameter list and its call site in build-plan-html.mjs to render an anchor inside .plan-header-actions beside the effort badge, with visible text View prototype and aria-label="View the interactive prototype for this plan". Compute its href with path.relative() from the rendered plan's output directory to the repo-relative prototype: target — never a hard-coded ../prototypes/. Apply every change to scripts/build-plan-html.mjs and scripts/lib/plan-shell.mjs, then re-copy both byte-for-byte to kit/plugins/plan-agent/scripts/.
plansDirectory is configurable, so a hard-coded ../prototypes/ resolves to custom/prototypes/ for a plan rendered under custom/plans/ while the prototype is written to docs/prototypes/ — and nested plan directories break the same way; and tests/plugins/test-build-plan-html.mjs:557 asserts the bundled renderer is byte-identical to the repo-root source, so editing only the bundled copy fails the suite and leaves callers of the root renderer without prototype: support.prototype: from both docs/plans/ and a custom plans directory and confirm the meta tag, a header anchor with non-empty accessible text, and an href that resolves in each; render one without it and confirm none do; diff scripts/build-plan-html.mjs kit/plugins/plan-agent/scripts/build-plan-html.mjs is empty.card-date span, as a text-bearing span matching the existing status-chip / type-chip / effort-chip pattern, carrying a title explaining that the prototype opens from inside the plan. It must not be an anchor — the whole card is already wrapped in <a class="gallery-card"> at line 149, and a nested <a> is invalid HTML that browsers silently unnest. Apply the identical change to all three byte-identical copies of the builder: kit/plugins/plan-agent/hooks/build-index.sh, scripts/build-plans-index.sh, and docs/plans/build-index.sh.
<a nested inside another <a; diff between all three builder copies is empty.hooks/check-prototype-drift.py, reading the PostToolUse JSON payload on stdin and running two comparisons: (A) the prototype's #proto-model field names against the <th> headers and form field name/id attributes present in that same file, and (B) the prototype's #proto-model against the proto-model: frontmatter of the plan its proto-source names. Resolve that plan path under the plans directory only, and read its frontmatter with a single-line regex (^proto-model:\s*(.*)$ then json.loads) in the style of validate-plan-filename.py's _is_completed — not a general YAML parser. Stay silent when the plan is missing, when either proto-model is absent, or when either fails to parse. Each warning names the two files, the diverging field, and what to re-run. Always sys.exit(0) — never exit 2, even though dispatch.py would propagate it as actionable feedback.
plan-spec.mjs over time; and exiting 0 matches every other hook in this plugin, keeping a drift report about some other plan from interrupting whatever the user is actually doing.proto-model (silent).dispatch.py by appending it to the is_prototype branch after the existing build-prototypes-index.sh call, sharing the same deadline.
dispatch.py already gates on _PROTOTYPES_MARKER and fans out, so no hooks.json change is needed — but the children share one 55s budget with a 5s per-child floor, so the cheaper check goes last and must stay cheap or it gets skipped by the fail-open path and drift silently stops being detected.grep -n check-prototype-drift kit/plugins/plan-agent/hooks/dispatch.py shows it inside the is_prototype block, after the index rebuild.prototype: / plan-prototype keys in the plugin README.md, add the 4.4.0 CHANGELOG entry, and bump plan-agent from 4.3.1 to 4.4.0 in .claude-plugin/marketplace.json.
kit/plugins/<name>/ requires a marketplace version bump, this is a feature so it is a minor one, and the plugin README documents every other hook individually.BASE_REF=main node scripts/check-plugin-versions.mjs exits 0.Tests
The tests that prove the change does what it promises.
/plan-agent:prototype's own behaviour — the frontmatter write-back and the generated #proto-model block — are verified manually via steps 2 through 4's verify lines. A SKILL.md is agent instructions, not executable code, so no committed test can assert the skill performed them; the tests below cover the renderer, the gallery, and the hook only.prototype: emits the plan-prototype meta tag plus a header anchor with non-empty accessible text and a resolving relative href, rendering one without it emits neither, the plans gallery card gains a text-bearing chip with no nested anchor, and check-prototype-drift.py stays silent on a matched fixture pair while reporting on a diverged one; Run: node tests/plugins/test-prototype-plan-link.mjs<th> headers, model diverges from plan frontmatter, plan exists but carries no proto-model yet (silent), proto-source names a plan that does not exist, proto-source resolving outside the plans directory, prototype has no #proto-model block, malformed JSON in either block — every case exits 0dispatch.py runs both build-prototypes-index.sh and check-prototype-drift.py; a payload for an unrelated path spawns neitherprototype: key renders byte-identical to its pre-change output; a spec rendered from a custom plansDirectory produces a resolving href; the existing byte-identical-copies assertion at line 557 still passes for both renderer filesDefinition 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.
Build a fixture pair under a temp directory: a plan spec with single-lineprototype: and proto-model: keys, and a prototype HTML whose #proto-model
matches it and whose proto-source names the spec.
Render the spec with node kit/plugins/plan-agent/scripts/build-plan-html.mjs and confirm the output contains
<spec>.md -o <spec>.html<meta and a header anchor with visible text and an
name="plan-prototype"aria-label.
Open the rendered plan in a browser and click the link — it must resolve to the
prototype file, confirming the ../prototypes/ href computation.
Pipe a synthetic PostToolUse payload naming the prototype intopython3 kit/plugins/plan-agent/hooks/check-prototype-drift.py and confirm it
prints nothing and exits 0. Then edit one field name in the prototype's#proto-model block, re-run, and confirm it prints two warnings — one for the
DOM mismatch, one for the plan mismatch, each naming both files and the field —
and still exits 0. Remove the proto-model: line from the plan spec and confirm
the run goes silent again rather than warning.
Finally run node tests/plugins/test-prototype-plan-link.mjs, bash,
tests/plugins/test-prototype-drift.shnode, and
tests/plugins/test-build-plan-html.mjsbash; all four must pass, the last
tests/plugins/test-build-prototypes-index.sh
confirming the existing gallery builder still works against a prototype carrying
the new block.
Wrapping up
Three gates that must all pass before this plan is marked completed.
Completion Report
No items to report — all requirements met.