Build the Markdown-spec-to-HTML plan renderer

High completed
2026-07-12 agentics feature High effort

Ship a deterministic renderer that turns a small Markdown plan spec into today's full styled HTML plan — proven by round-trip tests showing the rendered output re-extracts to the identical spec.

At a glance

Every plan today is typed out by the model as roughly 85 KB of HTML, nearly half of it boilerplate that never changes between plans. This work moves that boilerplate into a script: authors write a small Markdown spec and the renderer stamps out the styled, interactive HTML page. We'll know it worked when specs extracted from existing plans render back to HTML that re-extracts identically, with every downstream tool contract intact.

Implement Read and implement all steps in the plan at docs/plans/build-plan-html-renderer.md — Build the Markdown-spec-to-HTML plan renderer. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/build-plan-html-renderer.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: Build the Markdown-spec-to-HTML plan renderer. The plan at docs/plans/build-plan-html-renderer.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/build-plan-html-renderer.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/build-plan-html-renderer.md — Build the Markdown-spec-to-HTML plan renderer. Brief subagents with the plan file at docs/plans/build-plan-html-renderer.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/build-plan-html-renderer.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 build-plan-html-renderer.html
Path docs/plans/build-plan-html-renderer.html
Spec docs/plans/build-plan-html-renderer.md
Definition of done 7 / 7 done

Context

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

This is Phase 1 of the guideline-driven plan generation proposal ( docs/proposals/plan-generation-from-markdown-guidelines.md , commit ad5ea81 ). That investigation measured the current pipeline: the skill file costs ~19k tokens to load, the 2,015-line SKELETON.html ~22k tokens to read, and the average 84 KB plan ~21k output tokens to write — 40–48% of every plan being identical CSS, JavaScript, and SVG. The repository already ships the read side of the fix: extractSections() and buildDigest() in scripts/lib/plan-spec.mjs derive a Markdown spec from any plan's HTML. Phase 1 builds the write side — a renderer that produces the exact DOM contract that finalize-plan , the plans gallery, and the smoke tests depend on — plus round-trip tests and a regeneration hook. Later phases rewrite the skill around planning guidelines; nothing in this phase changes how plans are authored yet.

Files that change

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

agentics/
  • kit/plugins/plan-agent/
    • CHANGELOG.md modified 2.18.0 entry for renderer, tests, hook
    • hooks.json modified register the render-plan-html hook
  • scripts/lib/
    • plan-shell.mjs new style and layout shell extracted from SKELETON.html
    • plan-spec.mjs modified add parseSpecMarkdown(), inverse of buildDigest()
  • .claude-plugin/marketplace.json modified bump plan-agent to 2.18.0
  • kit/plugins/plan-agent/hooks/render-plan-html.py new PostToolUse hook re-rendering sibling HTML
  • scripts/build-plan-html.mjs new spec-to-HTML renderer CLI
  • tests/plugins/test-build-plan-html.mjs new unit, CLI, and round-trip property tests

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 Add parseSpecMarkdown() to scripts/lib/plan-spec.mjs — the inverse of buildDigest() : parse a Markdown plan spec (title, Objective, Context, Files, Steps with Why/Verify, Tests, Acceptance Criteria, Verification, plus YAML frontmatter for metadata) into the same sections object extractSections() returns.
Why
Keeping both directions in the one shared library means the renderer consumes exactly what the extractor emits, so the round-trip property is testable and the two sides cannot drift apart.
Verify
Round trip in one command: feed buildDigest(extractSections(html)) from a committed plan into parseSpecMarkdown() and assert the result deep-equals the original sections object.
2
done Extract the presentation shell into scripts/lib/plan-shell.mjs — move the SKELETON.html CSS, icon sprite, JavaScript behaviours, and frozen strings into exported template functions that hold style and layout only, never plan content.
Why
The proposal's core requirement is that the template is used only for style and layout; isolating it in one versioned module lets the renderer stamp the ~55 KB of boilerplate without the model ever emitting it.
Verify
Grep the module for the three frozen strings — the step-chip markup, the "No items to report" sentence, and the "Pursue as goal" label — all present byte-for-byte, and confirm it exports no content of its own.
3
done Build the scripts/build-plan-html.mjs CLI — node scripts/build-plan-html.mjs <spec.md> [-o <plan.html>] renders a spec through the shell into a single self-contained HTML plan reproducing today's DOM contract: the plan-* meta tags, #objective and the at-a-glance block, the implement row and more-ways drawer, #steps step cards, #tests , #criteria-list , #verification , the completion checklist, and HTML-escaping of all spec text.
Why
This is the deterministic renderer at the heart of Phase 1 — once it exists, plan boilerplate stops flowing through the model's output channel entirely.
Verify
Render a hand-written sample spec and open the result in a browser: badges, progress bar, and copy buttons all work, and node scripts/extract-plan-spec.mjs on the output exits 0.
4
done Compute derived fields inside the renderer — the implement, goal, and workflow prompts from the objective and output path; the effort level from step and file counts (the same Low/Medium/High thresholds the skill uses); the auto-generated file-tree from the Files list; the criteria count in the progress header; and a sidebar nav filtered to the sections actually present.
Why
Values that can be derived should never be authored — computing them removes a whole class of drift between meta tags, badges, and body content.
Verify
A 3-step, 2-file sample spec renders with data-effort="low" and a 7-step spec with data-effort="high" ; the plan-implement and plan-goal meta tags quote the spec's objective and relative path verbatim.
5
done Write tests/plugins/test-build-plan-html.mjs — unit cases for parseSpecMarkdown() , CLI integration cases, and the round-trip property: for a sample of committed plans in docs/plans/ , extract the spec, render it, re-extract, and assert deep equality; also assert the frozen strings and zero unfilled skeleton placeholder tokens in rendered output.
Why
The round trip is the proof that the renderer preserves the machine contract every downstream consumer — finalize-plan , the gallery hooks, the extractor — depends on.
Verify
node tests/plugins/test-build-plan-html.mjs exits 0 and reports at least 10 committed plans round-tripped cleanly.
6
done Add the regeneration hook kit/plugins/plan-agent/hooks/render-plan-html.py and register it in kit/plugins/plan-agent/hooks.json (PostToolUse on Write|Edit|MultiEdit, matching the plugin's existing index hooks) — when a spec .md inside the resolved plans directory is written, re-render its sibling .html via build-plan-html.mjs . Resolve the directory with the full plansDirectory settings precedence the skill mandates — project .claude/settings.local.json , then project .claude/settings.json , then global ~/.claude/settings.json , falling back to docs/plans/ — so other projects that configure a custom plans path re-render in the right place (note: rebuild-plans-index.py and validate-plan-filename.py currently skip the settings.local.json layer; the new hook follows the skill's full precedence).
Why
The hook keeps the Markdown/HTML pair fresh in normal operation — the same pattern the plugin already uses to rebuild the plans gallery index. It is best-effort (PostToolUse is non-blocking), so it must exit non-zero with the error on stderr when the renderer fails, and the round-trip test suite doubles as the parity check that catches stale pairs.
Verify
Pipe a simulated PostToolUse JSON payload for a spec write into the hook and confirm the sibling HTML is regenerated; a markdown write outside the plans directory leaves everything untouched; with plansDirectory set to a custom path in settings, a spec write under that path re-renders there while a write under docs/plans/ is ignored.
7
done Bump plan-agent from 2.17.0 to 2.18.0 in .claude-plugin/marketplace.json and add a matching entry to kit/plugins/plan-agent/CHANGELOG.md describing the renderer, tests, and hook.
Why
Repo convention — a new capability is a minor version bump set manually in the PR; the marketplace value is what ships, with no CI guard behind it.
Verify
The settings hook validates marketplace.json syntax after the edit, and the version is strictly higher than 2.17.0 on main.

Tests

The tests that prove the change does what it promises.

Tier 1 — This plan changes application code
Objective Rendered plan HTML re-extracts to an identical spec File: tests/plugins/test-build-plan-html.mjs Type: smoke Asserts: For a sample of committed plans in docs/plans/ , extract → render → re-extract produces a deep-equal spec; rendered output carries the three frozen strings, every required plan-* meta tag, and zero unfilled skeleton placeholder tokens. Run: node tests/plugins/test-build-plan-html.mjs
Unit parseSpecMarkdown() parses every spec section File: tests/plugins/test-build-plan-html.mjs Targets: parseSpecMarkdown() in scripts/lib/plan-spec.mjs Key cases: YAML frontmatter maps to metadata fields; numbered steps split into action, why, and verify; the Files list parses to path, badge, and note; a missing Objective, Steps, Criteria, or Verification section raises ParseError .
Integration build-plan-html.mjs CLI renders a self-contained plan File: tests/plugins/test-build-plan-html.mjs Targets: the renderer CLI plus scripts/lib/plan-shell.mjs Key cases: writes the -o output file; exits 1 with a helpful message on an unparseable spec; derived effort, implement, and goal metadata present; the workflow meta tag is omitted when the spec defines no workflow prompt.
Integration render-plan-html.py hook regenerates sibling HTML File: tests/plugins/test-build-plan-html.mjs Targets: kit/plugins/plan-agent/hooks/render-plan-html.py Key cases: a spec write under the resolved plans directory re-renders the sibling .html ; a custom plansDirectory in settings (local → project → global precedence) is honoured, with docs/plans/ only as the fallback; markdown outside the plans directory is a no-op; a malformed hook payload exits cleanly without touching files.

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.

Write a fresh sample spec, render it with node scripts/build-plan-html.mjs , and open the HTML in a browser: header badges, progress bar, step chips, and copy buttons all render, with no unfilled skeleton placeholder tokens. Run node scripts/extract-plan-spec.mjs against the rendered file and confirm the printed spec matches the source. Then run node tests/plugins/test-build-plan-html.mjs (the round-trip suite over committed plans) plus the existing smoke tests tests/plugins/test-humanized-skeleton.sh and tests/plugins/test-goal-prompt.sh — all must exit 0. Finally, simulate the hook: pipe a PostToolUse JSON payload for a spec write into kit/plugins/plan-agent/hooks/render-plan-html.py and confirm the sibling HTML is regenerated, then repeat with a non-plans markdown path and confirm nothing changes.

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.

Phase 2 — author the guidelines library and rewrite the implementation-plan skill around Markdown authoring

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

Implement Phase 2 of docs/proposals/plan-generation-from-markdown-guidelines.md: create the guidelines library under kit/plugins/plan-agent/skills/implementation-plan/guidelines/ (planning-principles, section-catalog, right-sizing, writing-style) and rewrite SKILL.md so the agent authors a Markdown spec and renders it with scripts/build-plan-html.mjs
Phase 3 — point finalize-plan and the status gates at the Markdown spec

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

Implement Phase 3 of docs/proposals/plan-generation-from-markdown-guidelines.md: update finalize-plan and the plan status/checkbox flows to edit the Markdown spec (checkbox flips plus frontmatter) and re-render the HTML via scripts/build-plan-html.mjs, then retire the byte-for-byte frozen-string contracts once nothing reads them
Backfill Markdown spec sources for every legacy plan 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:

Run a workflow to backfill Markdown spec sources for every plan in docs/plans/ using scripts/extract-plan-spec.mjs, verifying each rendered HTML re-extracts identically — follow the guarded-batch pattern from scripts/backfill-plan-digests.mjs