Add a static-HTML prototype generator to plan-agent

High completed
2026-06-29 agentics feature High effort

Ship a /plan-agent:prototype skill that turns any completed HTML plan — or a raw one-line idea — into a runnable, framework-free static-HTML prototype under docs/prototypes/ , so a developer can click through the real data shapes and core flow — and the idea's actual success signal — and judge the implementation's practicality before writing production code.

Implement Read and implement all steps in the plan at docs/plans/add-prototype-generator-skill.md — Add a static-HTML prototype generator to plan-agent. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-prototype-generator-skill.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
Pursue as goal — optimize for the outcome, in parallel
Achieve this goal: Add a static-HTML prototype generator to plan-agent. The plan at docs/plans/add-prototype-generator-skill.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-generator-skill.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 as workflow — launch parallel subagents
Run a workflow to implement the plan at docs/plans/add-prototype-generator-skill.md — Add a static-HTML prototype generator to plan-agent. Brief subagents with the plan file at docs/plans/add-prototype-generator-skill.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-generator-skill.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.
File add-prototype-generator-skill.html
Path docs/plans/add-prototype-generator-skill.html
Spec docs/plans/add-prototype-generator-skill.md
Definition of done 8 / 8 done

Context

The story behind this plan — what prompted the work and why it matters now.

plan-agent already produces detailed, self-contained HTML plans — but there is no cheap way to pressure-test whether a design is actually practical before building it for real. A throwaway static-HTML prototype (vanilla HTML/CSS/JS, a localStorage-backed "database", zero build) lets a developer validate the data model and the core user flow in minutes. The prototype must inherit plan-agent's portability framework — one self-contained file, no CDN, no framework, GitHub-Pages-deployable — so prototypes publish alongside the Plans gallery and Media library. The capability is a single new skill in the plan-agent plugin; it reuses the proven gallery generator ( hooks/build-index.sh ), the docs hub, and the self-contained HTML template style rather than inventing new infrastructure. Because the prototype skeleton is authored once and reused by every future prototype, this plan bakes the load-bearing concerns — output escaping (the prototype is published to a live *.github.io origin), accessibility, and per-prototype storage isolation — into the skeleton itself rather than leaving them to each generation. A reviewing panel (architecture, completeness, testability, risk, conventions, UX, accessibility) shaped this revision; see the Team Review at the foot of the plan.

Files that change

Every file this plan touches, and what happens to each one.

agentics/
  • kit/plugins/plan-agent/skills/prototype/SKILL.md new the prototype skill (command + ambient)
  • reference/PROTOTYPE-SKELETON.html new self-contained, a11y-baked template
  • kit/plugins/plan-agent/hooks/
    • build-prototypes-index.sh new gallery generator, forked from build-index.sh
    • hooks.json modified register auto-rebuild on docs/prototypes/
  • kit/plugins/plan-agent/templates/prototypes-gallery.html new gallery card template (mirrors plans-gallery.html)
  • .claude-plugin/plugin.json modified description only — never a version field
  • CHANGELOG.md modified 2.9.0 entry
  • README.md modified document the skill + usage
  • .claude-plugin/marketplace.json modified version 2.8.3 → 2.9.0 + desc
  • fixtures/plan-agent/sample-prototype.html new fixture the shell/DOM tests consume
  • plugins/
    • test-prototype-portability.sh new objective smoke test
    • test-build-prototypes-index.sh new gallery builder unit test
    • test-prototype-persistence.mjs new Node persistence test (no jsdom)
  • .github/workflows/publish-dist.yml modified run the three tests by explicit path
  • docs/index.html modified add Prototypes hub card
  • docs/prototypes/index.html new committed bootstrap gallery (then regenerated)
  • CLAUDE.md modified plan-agent plugin table row

Steps

The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.

1
done Scaffold the prototype skill and its SKILL.md (command + ambient). Create kit/plugins/plan-agent/skills/prototype/SKILL.md with frontmatter ( name: prototype , a three-part description ≤200 chars, and allowed-tools covering ToolSearch, ExitPlanMode, Bash, Read, Write, Glob, Grep, AskUserQuestion plus browser/preview tools) and a numbered workflow: (0) self-bootstrap out of plan mode; (1) resolve input — a first token ending in .html is a plan path, otherwise the args are a raw idea; (2) idea path runs a 3-question AskUserQuestion (core entity , primary action , observable success signal — with example phrasing and a sensible fallback when an answer is blank), plan path reads the file and extracts objective, steps, and domain nouns; (3) derive a deterministic data model : the primary domain noun → the entity; its attributes → columns/form fields with inferred types (string/number/date/bool); generate 2–3 seed rows; map the success signal to a visible summary; (4) echo the derived model back (entity, fields, action, success signal) for confirmation before writing; (5) fill the skeleton placeholders by {{token}} string replacement, HTML-escaping every interpolated value — except {{SEED_JSON}} , which is JSON-serialized with script-breakout escaping only (e.g. </script> ): HTML entities are not decoded when load() reads the JSON block as text, so HTML-escaping it would break JSON.parse ; (6) derive a verb-target slug and write to docs/prototypes/<slug>.html ( mkdir -p first) — note validate-plan-filename.py is plans-dir-scoped and does not enforce prototype names, so the skill owns the convention; (7) before writing a plan-derived prototype, run a quick secret/PII scrub on extracted seed values (it publishes to a public Pages origin); (8) trigger the gallery rebuild, preview + SendUserFile , and report what to validate.
Why
One SKILL.md gives both the /plan-agent:prototype command and ambient activation ("prototype this plan") with no separate command file, matching implementation-plan . Specifying the extraction heuristic and an echo-back makes two implementers produce the same prototype and lets the user catch a wrong interpretation before a file is written.
Verify
head -8 SKILL.md shows valid frontmatter with name , description (≤200 chars), and allowed-tools including ToolSearch ; the body names the .html -vs-idea branch, the data-model heuristic, the echo-back, fill-time escaping, the docs/prototypes/ output, and the scrub note.
2
done Build the self-contained, accessibility-baked prototype skeleton. Create kit/plugins/plan-agent/skills/prototype/reference/PROTOTYPE-SKELETON.html — one file, inline CSS + vanilla JS, reusing the plan skeleton's colour tokens. Data layer: an inline <script type="application/json" id="seed"> block holding JSON-serialized seed data with script-breakout escaping only (e.g. </script> ) — never HTML-escaped , or JSON.parse chokes on &quot; — plus a store keyed by a per-prototype {{STORE_KEY}} (derived from the file slug) — load() reads localStorage and falls back to the seed, save() writes, reset() is guarded by confirm() then clears and reseeds. Render records via textContent / createTextNode , never innerHTML . UI & a11y (required): a semantic <table> with <th scope="col"> (or a true <ul> ); an add/edit form whose every input has an associated <label for> , marks required fields, and blocks submit on empty/invalid with an inline error; a per-row delete control and an empty-state message; real <button> elements for the primary action, Reset, and delete; a visible :focus-visible ring; an aria-live="polite" status region announcing added/updated/deleted/reset; focus management (return focus to the form's first field after add, to the status/Reset after reset); and a summary badge bound to the success signal. Placeholders: {{TITLE}} , {{SOURCE_PLAN}} , {{STORE_KEY}} , {{SEED_JSON}} , {{COLUMNS}} (header cells + field keys), {{FORM_FIELDS}} (label + typed input per field), {{PRIMARY_ACTION}} , {{SUMMARY}} , plus <meta name="proto-source"> and <meta name="proto-created"> tags.
Why
Inline seed + localStorage is the only data layer that survives a double-click on file:// (a fetched ./data.json is blocked by file:// CORS). Because this one file is reused by every prototype and published to a live origin, escaping, a11y, and storage isolation must live in the skeleton — fixing them here fixes them everywhere.
Verify
Open the raw skeleton from file:// : the table renders from seed via textContent ; adding a row validates then persists across reload under the namespaced key; per-row delete works and an empty list shows the empty-state; Reset prompts a confirm then restores the seed; a value of "><script> in a field renders inert. Tab through: labels, real buttons, a visible focus ring, and an aria-live announcement on each action; token pairs meet AA contrast. DevTools Network tab is empty.
3
done Fork the gallery generator into build-prototypes-index.sh . Copy kit/plugins/plan-agent/hooks/build-index.sh to kit/plugins/plan-agent/hooks/build-prototypes-index.sh , retargeted to scan docs/prototypes/*.html (excluding index.html ), parse each file's <title> and <meta name="proto-*"> tags, and emit docs/prototypes/index.html sorted newest-first by proto-created . Preserve the html.escape() on every emitted card field so a hostile title cannot inject into the gallery. Render cards from a new kit/plugins/plan-agent/templates/prototypes-gallery.html that mirrors templates/plans-gallery.html (keep the externalised-template pattern rather than inlining markup).
Why
The real generator is hooks/build-index.sh (the plans gallery's card rendering, meta parsing, newest-first sort, and per-field html.escape() ); rebuild-plans-index.py is only the debounced hook dispatcher. Forking the correct file — rather than the dispatcher — preserves the proven escaping and keeps the prototypes gallery from re-deriving logic. A fork (not a shared parameterised builder) keeps the live plans gallery out of the blast radius.
Verify
Drop the test fixture into docs/prototypes/ , run bash hooks/build-prototypes-index.sh , and confirm it writes docs/prototypes/index.html with a card linking the prototype, newest-first; a fixture whose title contains < / " / & is escaped in the output; an empty dir produces the empty-state gallery and exits 0.
4
done Wire the auto-rebuild hook and the docs hub (with a committed bootstrap). Add a second PostToolUse ( Write|Edit|MultiEdit ) entry to kit/plugins/plan-agent/hooks.json that runs build-prototypes-index.sh , scoped so it only rebuilds for docs/prototypes/ writes (mirroring how the existing hooks gate on the plans dir). Add a third card to docs/index.html linking prototypes/index.html alongside Plans and Media ("Prototypes — clickable proofs of plans"). Generate and commit an initial docs/prototypes/index.html (empty-state gallery) so the hub link resolves immediately.
Why
The plans gallery auto-rebuilds via a PostToolUse hook; without a parallel entry the prototypes gallery goes stale on any write outside the skill. Committing an initial index.html stops the hub card 404-ing before the first prototype exists.
Verify
Open docs/index.html — a Prototypes card appears and its href="prototypes/index.html" resolves; writing a file under docs/prototypes/ rebuilds the gallery while leaving docs/plans/index.html untouched and raising no filename violation.
5
done Add the fixture, the three runnable tests, and CI wiring. Create tests/fixtures/plan-agent/sample-prototype.html (an inline #seed block plus proto-created / proto-source meta). Create three tests under tests/plugins/ following the repo's test-*.sh convention: test-prototype-portability.sh (objective smoke — asserts no external http(s):// resource refs, an inline #seed block, textContent -based rendering, that a hostile seed/title value is inert, and that running the builder yields an index.html containing the escaped fixture title, and — by static grep against the skeleton — that every input has a <label for> , Reset/primary are <button> , and an aria-live region exists); test-build-prototypes-index.sh (builder unit — parses <title> / proto-created , sorts newest-first, escapes < / " / & , ignores index.html , empty dir → empty-state + exit 0); and test-prototype-persistence.mjs ( plain Node with a tiny in-memory localStorage shim — no jsdom, no added dependency — seed parses+loads, adds persist under the namespaced key, two prototypes don't share state, reset restores). Register all three by explicit path in .github/workflows/publish-dist.yml — they need only bash and node builtins, so no install step is required.
Why
Playwright is not installed and CI runs tests by explicit path (no glob), so the originally-named tests would never run. A plain-Node check (in-memory localStorage shim) keeps the plan's zero-dependency ethos; an explicit CI entry makes the tests real; the fixture is what both shell tests consume.
Verify
Run all three locally — bash tests/plugins/test-prototype-portability.sh , bash tests/plugins/test-build-prototypes-index.sh , node tests/plugins/test-prototype-persistence.mjs — each exits 0; grep confirms the three paths are named in publish-dist.yml .
6
done Bump the version and document the new skill. In .claude-plugin/marketplace.json bump plan-agent 2.8.3 → 2.9.0 and extend its description ; mirror only the description (never a version field) in kit/plugins/plan-agent/.claude-plugin/plugin.json ; add a 2.9.0 entry to kit/plugins/plan-agent/CHANGELOG.md ; add a Features bullet + usage example to kit/plugins/plan-agent/README.md ; update the plan-agent row in root CLAUDE.md . Before committing, confirm 2.9.0 exceeds the version in git show origin/main:.claude-plugin/marketplace.json .
Why
A new skill is a MINOR bump per the marketplace rules; there is no CI version guard, so the bump must be checked against origin/main by hand. Keeping changelog/README/manifests/CLAUDE.md in sync keeps the catalog and contributor guide accurate; the settings hook validates the marketplace JSON on write.
Verify
python3 -c "import json;json.load(open('.claude-plugin/marketplace.json'))" exits 0 and the plan-agent version reads 2.9.0 and is > origin/main ; git grep -l prototype hits README, CHANGELOG, plugin.json, marketplace.json, and CLAUDE.md; plugin.json has no version field.

Tests

The tests that prove the change does what it promises.

Tier 1 — Code-touching plan
Objective A generated prototype is self-contained, escapes hostile input, persists locally, and lands in the gallery File: tests/plugins/test-prototype-portability.sh Type: smoke test Asserts: a generated prototype is self-contained, safe, and gallery-listed — greps the filled skeleton/fixture for zero external http(s):// resource references, an inline #seed block, and textContent -based rendering; confirms a hostile seed/title value renders inert; then runs build-prototypes-index.sh against a fixture dir and asserts index.html contains the escaped fixture title; and statically asserts the skeleton's a11y affordances are present (every input has a <label for> , Reset and the primary action are <button> , an aria-live region exists) — i.e. the plan's objective (a portable, safe, accessible, gallery-listed prototype) holds. Run: bash tests/plugins/test-prototype-portability.sh (wired into publish-dist.yml ). Note: the plan-path and idea-path skill invocations are non-deterministic and are verified manually (Verification), not by an automated test.
Unit Gallery builder parses, sorts, and escapes File: tests/plugins/test-build-prototypes-index.sh Targets: build-prototypes-index.sh meta parsing, ordering, escaping Key cases: parses <title> / proto-created from fixtures; sorts newest-first; escapes < / " / & in card output; ignores index.html ; empty dir → empty-state gallery, exit 0.
Integration Prototype store persistence (plain Node + localStorage shim — no jsdom) File: tests/plugins/test-prototype-persistence.mjs Targets: the skeleton's store logic ( load / save / reset ) driven with a tiny in-memory localStorage shim — no DOM runtime, no added dependency Key cases: seed JSON parses and loads on first call; adding a record persists under the namespaced {{STORE_KEY}} ; two prototypes (distinct keys) don't share state; reset restores the seed. Runs with node alone (a11y affordances are asserted statically by the portability smoke test).

Definition 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.

Load the plugins locally, then run /plan-agent:prototype docs/plans/<an-existing-plan>.html : confirm a prototype lands in docs/prototypes/ , opens standalone from file:// with working add/edit/delete, form validation that blocks an empty submit, a Reset guarded by a confirm() that reseeds from inline JSON, and a visible summary badge reflecting the idea's success signal. Tab through it: every input has a label, Reset and the primary action are real buttons, focus is visible and lands sensibly after each action, and an aria-live region announces add/update/delete/reset. Feed a hostile value (e.g. "><script> ) into a seed field and the title — confirm it renders inert in both the prototype and the gallery. Generate a second prototype and confirm the two do not share localStorage state (distinct {{STORE_KEY}} ). Confirm the write to docs/prototypes/foo.html rebuilds docs/prototypes/index.html via the new hooks.json entry, leaves docs/plans/index.html untouched, and raises no filename violation. Run /plan-agent:prototype "track gym workouts" and confirm the idea-path interview echoes back the derived model before writing. Run the three tests by their explicit CI paths — they pass. Finally confirm the bumped 2.9.0 exceeds origin/main and the marketplace JSON validates.

Wrapping up

Three gates that must all pass before this plan is marked completed.

Required

Completion Report

No items to report — all requirements met.

Next steps

Follow-up ideas that came up along the way — none of them are required to finish this plan.

Enforce verb-target filenames for prototypes via the validator hook

Paste this prompt into Claude to execute this follow-up:

Extend kit/plugins/plan-agent/hooks/validate-plan-filename.py (or add a parallel validator) to enforce the verb-target kebab-case convention on prototype files under docs/prototypes/, mirroring how it already gates docs/plans/. Reject placeholder/auto-generated slugs the instant a prototype is written. Bump the plan-agent patch version and add a CHANGELOG entry.
Prototype straight from a React/Vue component, with AI-synthesized mock data Wish List

Speculative / blue-sky idea — not on the critical path. Paste into Claude when ready to explore:

Paste this prompt into Claude to execute this follow-up:

Extend /plan-agent:prototype to accept a React or Vue component file and produce a static prototype that renders the component (inline compiled markup, no build) with an interactive props panel, plus AI-generated realistic seed data derived from the plan's domain entities. Keep the single-file, no-build, no-CDN, escaped-output, a11y-baked portability constraints from the base skeleton.