Team recap · plan-agent 4.4.0

Plan and prototype linking with drift detection

We made a plan document and its clickable prototype point at each other, and added an automatic check that notices when the two have quietly fallen out of sync.

At a glance

6Changes shipped
21Files touched
6Decisions
4Open items
54New assertions
1Bug found late

Before this, a prototype knew which plan it came from, but the plan had no idea a prototype existed — and neither file wrote down the data model they supposedly shared, so nothing could tell you they had diverged.

The work was verified end to end (full test suite, a hand-built fixture pair, and a real browser click) before the plan was marked complete. One genuine bug surfaced during the manual walk that the automated tests had missed; it was fixed and regression-tested.

What changed

A plan now links to its prototype

Affects: everyone

A plan whose source carries a prototype: line now shows a View prototype link in its header, beside the effort and status badges. Clicking it opens the prototype.

To use: add prototype: docs/prototypes/<slug>.html to a plan’s Markdown source and re-render.

The gallery shows which plans have a prototype

Affects: everyone

Each card in the plans gallery gains a small prototype chip. It is a text label, not a second link — the whole card is already one big link, and nesting a link inside a link is invalid HTML that browsers silently pull apart.

Both files now record the shared data model

Affects: nobody yet

The prototype generator worked out a data model — the thing being tracked, its fields and their types, the main action, and the signal that proves it works — then threw it away once the page was built. That model is now written into both files as a single line of machine-readable data. This is the groundwork that makes the drift check possible at all.

A new check reports drift

Affects: teammates

Whenever a prototype is written, a check compares the recorded model against the prototype’s own visible columns and form fields, and against the plan’s copy. When they disagree it prints a warning naming both files, the field that differs, and what to re-run.

To use: nothing — it runs on its own after every prototype write.

The generator writes the link back

Affects: teammates

Running /plan-agent:prototype against a plan now writes the link and the model into that plan before writing the prototype. When the plan has no Markdown source — most older plans are HTML-only — it skips the write-back, still builds the prototype, and prints one line explaining how to create a source file.

The source-plan placeholder became a real contract

Affects: maintainers

{{SOURCE_PLAN}} was undefined free text that only ever fed a display string. It is now pinned: the repo-relative path of the plan’s Markdown source on the plan path, and empty for idea, image, and Figma inputs. The drift check resolves the owning plan from it, so without a format contract the whole comparison would be unimplementable.

How it works now

flowchart LR
  subgraph BEFORE["Before"]
    direction LR
    A1["Plan"]
    B1["Prototype"]
    B1 -->|"proto-source"| A1
  end
  subgraph AFTER["After"]
    direction LR
    A2["Plan"]
    B2["Prototype"]
    M2[["shared data model"]]
    A2 -->|"prototype:"| B2
    B2 -->|"proto-source"| A2
    A2 -.->|"proto-model"| M2
    B2 -.->|"proto-model"| M2
  end
The link used to run one way only. Now both files point at each other, and both carry a copy of the same data model — the part that makes drift detectable.
flowchart TD
  W["A prototype file is written"] --> D{"Path gate"}
  D -->|"unrelated path"| X["Exit, nothing spawned"]
  D -->|"under docs/prototypes/"| G["Rebuild the prototypes gallery"]
  G --> C["Run the drift check"]
  C --> A{"Model vs. the prototype's
own columns and fields"} C --> B{"Model vs. the plan's copy"} A -->|"differs"| W1["Warn, naming both files
and the field"] B -->|"differs"| W2["Warn, naming both files
and the field"] A -->|"matches"| S["Silent"] B -->|"matches"| S W1 --> Z["Always exit 0"] W2 --> Z S --> Z
The drift check runs last and always exits cleanly — a report about one plan must never block work on another.
flowchart TD
  S["The prototype generator runs"] --> Q{"Input type?"}
  Q -->|"idea, image, Figma"| N["No write-back"]
  Q -->|"plan path"| M{"Does the plan have
a Markdown source?"} M -->|"yes"| W["Write the link and the model
into the plan"] M -->|"no, most older plans"| K["Skip the write-back,
print how to create one"] W --> G["Write the prototype"] K --> G N --> G
The write-back is deliberately conditional: 69 of 84 committed plans are HTML-only, and creating a source file as a side effect would rewrite a file nobody asked us to touch.

Before and after

SituationBeforeAfter
Finding a plan’s prototypeNo way to know one existedA link in the plan header, and a chip on the gallery card
The shared data modelWorked out, then discardedRecorded in both files as one line
A hand-edited prototypeNobody noticesWarning naming both files and the diverging field
Where the link points—Worked out relative to the plan’s own folder, so a custom or nested plans folder still resolves
A plan with no prototype—Renders character-for-character identically to before
Running against an HTML-only plan—Prototype still built, no file created, one-line notice printed
When the check hits bad input—Exits cleanly every time, including missing files and malformed data

How we know it works

CheckResult
End-to-end test test-prototype-plan-link.mjs5 passed
Drift check test test-prototype-drift.sh14 passed
Renderer test test-build-plan-html.mjs35 passed
Prototypes gallery testpassed
Full plugin suite, 37 files0 failing
Publish and demo suites0 failing
Version guard4.4.0, clean
File-copy parityall identical
Pages smoke testfails, pre-existing

Also walked by hand: built a matching plan and prototype pair, rendered it, opened the plan in a browser and clicked the link (it reaches a working prototype, and the new data block does not break the prototype’s own behaviour), then ran a three-stage drift walk — a matching pair stays silent, one renamed field produces two warnings naming both files, and removing the plan’s copy silences the plan comparison.

Decisions

Detection, not two-way sync

Plan to prototype is regeneration — re-run the generator. Prototype to plan is detection — compare and report.

RejectedFull two-way sync. Prototypes have no separate source file, so the HTML is the source. Pushing a prototype edit back into plan prose would need an HTML-to-model parser over generated files, which the repo’s conventions forbid.

The header link ships no new styling

It uses the existing link colour and the header row’s spacing.

RejectedA styled button. The shared stylesheet is copied into every plan, so one new rule would change the bytes of all 84 existing plans and break the “a plan without a prototype renders identically” requirement. Flagged in the pull request as reversible if the team prefers the button.

The gallery chip is a text label, not a link

The card is already wrapped in a link, and a nested link is invalid HTML that browsers silently unnest.

RejectedAn icon-only chip — it gives screen-reader and voice-control users no signal that a prototype exists.

The link path is worked out, never hard-coded

A fixed ../prototypes/ would resolve to the wrong folder for any plan stored outside the default location, and nested folders break the same way.

RejectedThe hard-coded path, on the grounds that the plans folder is configurable.

The drift check always exits cleanly

Every other check in this plugin does, and the dispatcher would treat a failure exit as something the user must fix before continuing.

RejectedBlocking on drift. A report about some other plan interrupting whatever you are actually doing is worse than the drift itself.

The written data must stay on one line

The plan’s header reader is a simple line scanner. An embedded line break or stray separator would silently truncate the block and corrupt the plan’s status and creation date for every tool that reads it.

Learnings

A test fixture can be too clean to catch a bug

The drift check read field names straight out of the prototype’s HTML. The page template contains an authoring comment that includes a literal example attribute — data-field="key" — and that comment ships into every generated prototype. So the check reported a field called key that did not exist, on a file that had not drifted at all.

The automated test missed it entirely, because its fixture was a hand-written minimal page rather than the real template. The manual verification walk caught it on the first run.

Two fixes, both kept: strip comments before scanning, and point the test fixture at the real template so this class of bug cannot hide again.

Open items

Files touched

The linking itself — both copies must stay identical

  • scripts/build-plan-html.mjs and its bundled twin — read the prototype: key, work out the relative link
  • scripts/lib/plan-shell.mjs and its bundled twin — emit the marker tag and the header link

The gallery — three identical copies

  • scripts/build-plans-index.sh, kit/plugins/plan-agent/hooks/build-index.sh, docs/plans/build-index.sh — the prototype chip
  • kit/plugins/plan-agent/templates/plans-gallery.html — chip styling

The drift check

  • kit/plugins/plan-agent/hooks/check-prototype-drift.py — new; the whole comparison
  • kit/plugins/plan-agent/hooks/dispatch.py — run it after the gallery rebuild

The prototype generator

  • skills/prototype/SKILL.md — pinned the source-plan contract, added the model output and the write-back step
  • skills/prototype/reference/PROTOTYPE-SKELETON.html — the model block

Tests

  • tests/plugins/test-prototype-plan-link.mjs — new; the end-to-end check
  • tests/plugins/test-prototype-drift.sh — new; 10 check branches plus dispatcher fan-out
  • tests/plugins/test-build-plan-html.mjs — the “renders identically without a prototype” check, which renders through the previous version of the renderer and compares

Docs and metadata

  • README.md, CHANGELOG.md, .claude-plugin/marketplace.json — documentation and the 4.3.1 to 4.4.0 bump
  • docs/plans/add-prototype-plan-linking.md and its rendered page — plan marked complete

Glossary

Plan
A document describing work to be done. Written as Markdown — the editable source — and rendered to a web page people read.
Prototype
A single self-contained clickable page generated from a plan, so you can try the data shapes and the flow before building anything.
Spec
The Markdown source of a plan. Older plans have only the rendered page and no source.
Frontmatter
The block of key: value lines at the top of a Markdown file, between two --- markers.
Drift
When two files that are supposed to describe the same thing no longer agree.
Hook
A script the tooling runs automatically when a file is written.
Dispatcher
The single hook that checks a file’s path once and then decides which other hooks to run, instead of every hook waking up on every edit.
Skill
A set of instructions an AI agent follows. Instructions, not code — which is why some behaviour cannot be covered by an automated test.
Data model
The shape of the thing being tracked: its name, its fields and their types, the main action, and the signal that proves it works.
Mutation check
Deliberately breaking the code to confirm the test actually fails, proving the test is real.
Byte-identical
Two files matching exactly, character for character. Used here to prove existing plans were not disturbed.
Plans gallery
The browsable index page listing every plan as a card.