From plan-html
Interactive decision-deck planning. Same research/design thinking as /plan, but the plan is an interactive HTML deck the user works through in the browser — intent cards and decision cards, answered with buttons OR free text, with per-card questions to the agent — iterated in rounds until finalized. Every answer autosaves, so interruptions lose nothing. Use whenever the user wants to plan, review, or iterate on a plan interactively instead of reading markdown in the terminal.
How this skill is triggered — by the user, by Claude, or both
Slash command
/plan-html:plan-htmlThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
<!-- experiment-banner:start -->
🧪 This is an experiment — part of AI Will Replace You.
Stop wasting time on AI. I run practical experiments — real lessons you can use tomorrow, biweekly.
▶ Watch the demo on YouTube · 📖 Read the write-up · ✉️ Subscribe to the newsletter
More on @aiwillreplaceyou
Same goal as /plan — research a codebase and design an implementation plan
interactively — but the review surface is the browser deck, not the
terminal. The plan is delivered as cards the user answers in place:
intent cards (what they want, in their words, editable) and decision
cards (forks in the design). Free text counts as a full answer; every card
can carry a question to the agent; rounds repeat until the user finalizes.
The user can also reshape the plan directly: add their own intents,
boundaries, decisions, or steps with a "+ Add" button per section, and strike
out any card (✕) to remove it. Added cards come back tagged userAdded;
struck cards simply vanish from the round.
plan.json and is rendered by the deck.plan.json to
<repo>/design/plans/<slug>/plan.json (gitignored — verify; add
design/plans/ to .gitignore if missing). Never /tmp: the sibling
answers.json autosave must survive reboots and be findable after an
interrupted session.answers.json next to the plan — if present, the user has prior answers;
the server preloads them automatically. On a timeout result, the
autosaved answers are in the printed JSON (saved) — treat them as a
partial round, don't discard them.Ask only what you need to scope the exploration — a couple of questions max. The deep interrogation does NOT happen in chat: it happens in the deck (Stage 3), where answers persist and the user can think. Do not proceed until the user confirms the task framing.
Spawn explore agents in parallel (single message, multiple Agent calls),
sized to scope (1 small → 3–4 large), each returning structured markdown with
file paths and line numbers. Synthesize briefly (a few lines, not a
report), confirm with the user, then move on. Detailed findings belong in the
deck's card bodies, not the terminal.
Draft the plan directly as plan.json (schema below).
Every plan MUST have a goal, intents[] with a verify per intent, a
plan-level diagramSvg, and grill cards — no exceptions. boundaries[] is
optional: include it when the task has real scope risk (things that must not
be touched, cost ceilings, "don't refactor X") and skip it for small
contained tasks. The user can drop any boundary with its "Not needed" button.
goal — one sentence that shouts what we're building and why. Rendered
as a highlighted banner at the top of the deck. If you can't write it in
one sentence, the plan isn't understood yet.
GRILL THE USER — hard, but proportionate. The round-1 deck must
interrogate the user: turn every real assumption, ambiguity, and unstated
requirement into a needs-you decision card. Cover (where they genuinely
apply): who/what uses this, edge cases, failure modes, cost/limits,
security, what NOT to build, and what "done" looks like. Challenge the
user's framing when you disagree — present the conflict as a card with your
counter-position in building/tradeoffs. If an answer comes back vague,
re-ask it sharper in the next round; don't build on a vague answer.
Scale the grilling to scope and risk: a contained task earns a handful
of sharp cards, not an inquisition — never manufacture questions to fill
the deck.
The loop must converge — balance looping against finishing. Every round should shrink the open set, not grow it sideways. New cards in round N+1 are justified only by the user's answers or by re-exploration findings — not by second-guessing settled cards. When nothing real is left open, say so in the round-status line and let the user finalize; aim to be finalize-ready by round 2–3 for most plans. One round straight to Finalize is a success, not a shortcut.
intents[] — the spine of the plan. An intent is one outcome the
user wants + why + how we prove it's done — written in the user's own
words (first person works well: "I want my answers to survive an
interruption"). It is NOT a feature list and NOT an implementation step;
if you can't attach a binary verify (method + exact command + expected
result) to it, it's not an intent yet — sharpen it. Intents replace
acceptance criteria; they are user-editable in the deck, so write them to
be edited.
agentQuestions[]). When an
intent is fuzzy, attach pointed clarifying questions directly to it — who
uses it, what "done"/"revert"/"approve" concretely mean, edge cases — and
the deck renders them as an in-card checklist the user answers inline.
Prefer this over spinning up a separate needs-you decision when the
ambiguity is about the intent itself; use decision cards for genuine
forks in the design. Answers come back in the round under the card's
grill[] (each {id, q, answer}) — read them and tighten the intent (and
its verify) next round. Soft-gated: unanswered grill questions are
surfaced and counted but do NOT block Finalize.boundaries[] — the fence (optional, shown after the intents). What's in
scope, what's out of scope, and what must not be touched. One boundary per
card, written as a testable statement. When present, boundaries gate
Finalize — the user must agree to each, rewrite it, or drop it.
Diagrams are REQUIRED. Every plan ships a plan-level diagramSvg
(architecture/flow of what's being built). Additionally, any intent or
decision that describes structure, flow, or sequence gets its own per-card
diagramSvg — if prose is explaining a shape, draw the shape instead.
Diagram authoring rules (the deck styles these classes — use them):
class="node" boxes (bright blue). Color-code stages with the variants
node alt1 (teal), node alt2 (amber), node alt3 (purple),
node alt4 (rose) — e.g. inputs teal, processing blue, outputs amber.class="node-label" (white, bold); secondary lines use
class="node-sub" (light blue) — never invent your own fills/colors.class="edge" with marker-end='url(#arrow)'; label them with
class="edge-label" where the flow isn't obvious.UI work gets mocks — always, no exceptions. Any decision about a
screen, page, layout, component, or visual flow MUST present 2–3
alternative mocks as its options — the user picks a design by clicking a
mock, not by imagining one from prose. Use option objects (see schema):
{"label", "caption", "html", "width", "height"} for hi-fi HTML mocks
(default — prefer these), or {"label", "caption", "viewBox", "svg"} for
lo-fi SVG wireframes (fallback only). The deck renders html inline in a
sandboxed, auto-scaled, zoomable iframe.
Inline only — NEVER as files. Do not write mock .html/.png files to
disk, do not send mocks as attachments, do not link out. If a visual is not
inside the deck card, it does not count as delivered. Mock decisions
automatically render in their own prominent "🎨 Design mocks" section
near the top, expanded by default — you don't need to do anything beyond
using option objects.
<style> block and reuses the
real class names and component shapes, so the mock is visually
indistinguishable from a real screen of the app. If the app is dark, the
mock is dark; if cards have a 1px --border and 8px radius, so does the
mock. Populate with realistic sample data, not lorem/placeholders.<!doctype html> with an inline <style> — no external
CSS, fonts, or scripts (the iframe is sandboxed and offline).:root{--…} tokens verbatim and build with them; reuse
the real component class names so the mock reads as the real UI.width/height to a realistic screen size (e.g. 900×600); the
deck scales it down to fit. Author at full size — don't pre-shrink.width/height across the 2–3 alternatives so they compare
like-for-like; vary only the layout being decided.wf-frame for the screen/panel outline, wf-box for outlined regions,
wf-fill/wf-accent for the regions being decided, wf-bar for
text-line placeholders, wf-text for region labels, wf-note for small
annotations. Only these classes — no custom colors. Sizing discipline is
mandatory or text overflows the frame (the common failure): keep ALL
shapes and text strictly inside the viewBox; leave wf-frame; keep wf-text strings short (truncate, don't let them run
past the box edge) and never start text near the left/right margin where
it will clip. Same viewBox across the alternatives (0 0 200 140 for a
screen, wider for dashboards).caption states the tradeoff in one line
("denser, scales to 100 items" vs "calmer, 10 items max").decisions[] — the forks. Any place the design could go more than one
way: approach, library, boundary, what to defer, how much freedom the agent
gets. Mark needs-you vs agent-call vs fyi. Use dependsOn to link a
decision to the intents/decisions it hangs off — the deck shows the user
their own answers on linked cards.
steps[] — numbered implementation steps, each tagged with the intent
it serves. Steps are derived: when a round changes an intent or decision,
update the steps to match — they reshape every round and only lock at
finalize. Steps are now editable and deletable in the deck like any
card: the user can rewrite a step's fields in place, strike it out (✕) to
delete it, or add their own. The user's step edits are authoritative —
an edited step comes back with userEdited: true and its full draft;
keep the user's wording, don't silently regenerate or override it. A step
struck out is gone (absent from the round). You may still re-derive other
steps when an intent/decision changes, but leave the user's edited/added
steps as they set them unless their own answers force a change.
finalVerify[] — the plan-level gate: for each intent, how the whole
feature is proven end-to-end once built. Editable in the deck — the user
can edit any row (intent / method / command / expected), add rows, or delete
them. The edited rows ride back in the round as finalVerify[]; treat them
as authoritative (keep the user's commands/expectations) and reconcile the
plan's finalVerify to match. A new round reseeds the table from your
updated plan.
Default to live mode. The deck stays open and you stay in a loop beside it: the user can ask a question and get the answer streamed back into the same open page — no close, no re-serve. The plan file is the single source of truth; the page is a live view of it (SSE), and you keep it current.
Serve in live mode — ALWAYS in the background:
# ${CLAUDE_PLUGIN_ROOT} resolves to this skill's install dir (plugin install).
# If you cloned manually instead, use that path, e.g. ~/.claude/skills/plan-html
python3 "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/skills/plan-html}/serve_plan.py" \
--plan <abs-path>/design/plans/<slug>/plan.json --live
Run it with run_in_background: true — never foreground. A
foreground-blocking serve owns your turn, so the user can't type to you in
the terminal until it yields. Background-serving keeps both channels open at
once: the user types into the deck (autosaved + streamed to you) AND can
still message you in the terminal. Poll the background server's output file
for events between turns; don't block on it.
Stable port for tunnels (--port). By default the server picks a random
free port, so a cloudflared URL dies whenever the deck restarts. If the
user is on a tunnel and you'll be restarting the deck (e.g. iterating on the
template), pass --port <fixed> so the same tunnel URL keeps working across
restarts. Otherwise omit it.
Don't miss the final event. On Finalize/Send to agent the deck POSTs the round and the browser shows a "handed back to the agent — you can close this tab" page (it can't reliably close a user-opened tab, so the page IS the signal, not an auto-close). Because the server runs in the background, that round lands in the output file — actively pick it up. If a user says "I finalized but nothing happened," the round is in the server's stdout/log; read it and proceed.
The deck opens; every user change autosaves to answers.json. The server
prints a JSON line per event to stdout (it does NOT exit on the first
one):
{"action":"ask","id","cardId","text"} — a live question (also queued in
questions.json).{"action":"timeout",...} if the idle timeout hits.Grill request. The intents section has an "Interview me to sharpen
these intents" button. Clicking it sends an ask with
cardId:"__intents__" and a text starting [GRILL REQUEST]. When you see
it: write pointed agentQuestions[] onto the fuzzy/ambiguous intents (not
every intent — only ones that genuinely need clarifying), bump rev, and
write plan.json. The questions stream into the cards over SSE; the user
answers inline and the answers come back as grill[] (see agentQuestions
under intents). This is grilling on demand — the user pulls it when they
want it, instead of you front-loading questions on every intent.
Optional public tunnel (on demand). The deck binds 127.0.0.1 by
default. If the user asks to share it / open it on their phone / "open a
tunnel" while a deck is already running, start a cloudflared quick
tunnel as a SEPARATE background process pointed at the live port — do NOT
restart the server (that would not lose answers, but it's needless churn and
changes the port):
cloudflared tunnel --url http://localhost:<live-port> --no-autoupdate
Grep its log for the https://<random>.trycloudflare.com URL and paste it
to the user. The tunnel is ephemeral (URL changes each run, no account
needed) and stays up until you stop the cloudflared process — stop it when
the plan is done. (ngrok http <port> is an alternative if cloudflared
isn't installed.)
Arm a persistent Monitor the instant you serve — this is MANDATORY, not a poll. Every deck event (ask, clarification, finalize, send-round, timeout) arrives on the server's stdout the microsecond it happens. Do NOT poll the output file between turns — that is what makes a question sit "queued — no agent is watching." Instead, the moment the background server is up, attach a persistent Monitor to its output file so each event wakes you instantly, with zero polling — the same instant path finalize already rides:
Monitor(persistent: true, command:
tail -n0 -f <server-output-file> |
grep -E --line-buffered '"action": ?"(ask|finalize|send-round|timeout)"')
With the Monitor armed, asks, clarifications and finalize are all delivered one uniform way, instantly. The deck shows an "agent is replying…" spinner on Send; if no reply lands in ~25s it flips to "no agent is watching this deck right now" — with the Monitor running, that state should essentially never appear. If it does, your Monitor died: re-arm it.
For each ask event — answer DURABLY (write the plan), then optionally
paint fast (SSE push):
cardId and append to its thread:
{"role":"user","text":<their question>} then your
{"role":"agent","text":<answer>}.rev and write plan.json. This is the source of truth and the
ONLY reliable delivery. The server pushes the new plan over SSE and the
page reconciles in place; crucially, the server also re-sends the full plan
on every SSE (re)connect — so an answer written to the plan survives a
dropped mobile connection and reappears on reconnect. An answer that is
not in the plan is lost if the user's stream blipped (common on phones/
tunnels). Never rely on the push alone.POST /answer {cardId, text} to
stream the reply into the card a beat sooner. This is a nicety on top of the
plan write, never a substitute — if you skip the plan write, a reconnect
shows "still queued" forever even though you "answered."send-round/finalize/timeout event.The user can keep working on other cards while you answer one — only the asked card shows a "thinking…" state. Don't block the whole plan on one question.
If an ask lands while you're busy elsewhere: nothing is lost — it's
queued in <plan-stem>.questions.json and the Monitor still fires. Pick it
up; writing the plan delivers the answer (and heals any "queued" state) the
moment the page next has the plan.
On send-round / finalize / timeout — proceed exactly as the
round rules below describe. (Live answers and rounds coexist: live handles
"answer my question now"; a round handles "I've answered a batch, process
them.")
Read the printed round
({"action", "round", "agentActions", "cards":[...], "finalVerify":[...]}).
finalVerify carries the user's edited finish-line rows — reconcile your
plan's finalVerify to match them.
First check agentActions — the user's global requests for the next round:
reexplore: true — re-run the exploration agents before answering
anything, then update affected cards (and say in one status line what
changed in the code since last look).note — free-text instruction for the next round; honor it.Cards arrive in the user's priority order (they drag-reorder by the ⠿
grip): cards[] is sorted top-first and each carries priority (0 = most
important within its section). Build in that order; let it set what you
tackle first.
Struck-out cards are GONE — absent means removed. When the user strikes
a card (the ✕) it is dropped from the round entirely: it will NOT appear in
cards[]. So a card you sent last round that is missing this round was
removed by the user — drop it from the plan, don't re-add it, don't re-ask
it. (You lose the "why" — accept the user's call.)
User-added cards arrive with userAdded: true and a draft. The user
created this card in the browser and may have filled it in fully or
partially (draft holds whatever they typed: title, intent/boundary text,
verify, options, status, step fields). Promote it into the plan as a
real card next round: keep the user's content, complete what's missing
(write a proper verify for an added intent, draw a diagram if it has
structure, normalize options for an added decision), and give it a stable
id. Treat the user's draft as authoritative intent — fill gaps, don't
override.
Then, for each card:
choice — a button pick (approve / reject / agent / a custom
option label / a mock label). null is fine if answer has text:
free text is a full answer, treat it with the same weight as a
button. (The user can pick any text choice straight from the collapsed
card via inline quick-answer buttons — same choice either way.)answer — the user's own words: detail, redirect, or a standalone
answer. Always read it; it overrides/refines the button.question — the user needs an answer before deciding. Answer it in
the card's thread (see below), don't make them dig in the terminal.edit — the user rewrote an intent or a boundary in place. Replace the
card's text with their version (and adjust dependent decisions/steps/
verify if the rewrite changes scope). An edited boundary is the user
re-drawing the fence — re-check every step still fits inside it.Keep the title in sync with the content. A card's title is just a
short handle — when discussion, an edit, a question, or your own rework
changes what a card is actually about, rewrite its title (and summary)
to match. Don't leave a stale title describing the old meaning. This
applies to intents, boundaries, decisions, and steps alike: if the content
moved, the title moves with it. (The deck reconciles the new title live over
SSE, so the user sees it update in place.)
If action is "send-round" — iterate:
question, append to that card's thread:
{"role":"user","text":<their question>} then
{"role":"agent","text":<your answer>}."round" by 1, write the updated plan.json, print one line
saying what changed, and re-serve (step 1). The user's prior answers
reload automatically; their delivered questions are cleared.If action is "finalize" — briefly restate (2–4 lines max) what was
decided where it diverged from your defaults, then proceed to build (or
hand to /dev-cycle). Do not re-dump the plan.
If action is "timeout" (exit code 1) — the user stepped away.
Their partial answers are in saved and on disk. Say so in one line and
stop; on the next session, re-serve the same plan — nothing is lost.
More questions after finalize? Re-open the deck — never ask in the
terminal. Finalize locks the plan, but building often surfaces forks the
plan didn't cover. When that happens: set "followUp": true, bump
round, add ONLY the new needs-you cards (keep the locked sections for
reference — don't re-open settled decisions), and re-serve. The deck shows
a follow-up banner and the Finalize button becomes "Resume build" —
in follow-up rounds only the new needs-you cards gate it; settled
intents/boundaries stay locked and don't re-gate. (A follow-up intent or
boundary can be marked "status": "needs-you" to gate again.)
When the answers come back, continue building. Repeat as often as the
build demands — a follow-up round is cheap; building on a guess isn't.
{
"title": "Feature title",
"slug": "feature-slug", // stable id; also the dir name
"task": "the original request", // optional
"date": "YYYY-MM-DD", // optional
"round": 1, // bump on every re-serve
"rev": 0, // bump on every LIVE write so the open deck reconciles
"followUp": false, // true = post-finalize round: new questions from the build
"summary": "1–2 sentence what-and-why (markdown ok)",
"goal": "REQUIRED — one sentence: what we're building and why, shown as a banner",
"boundaries": [ // optional — the fence, shown first when present
{
"id": "boundary-scope",
"title": "Only the skill files change",
"boundary": "In scope: the three skill files. Out of scope: everything under `src/`. Do **not** add new dependencies. (markdown ok)",
"thread": [] // Q&A history, optional
}
],
"intents": [ // REQUIRED — the spine, replaces AC
{
"id": "intent-persist",
"title": "Answers survive interruptions", // short card title
"intent": "I want every answer I give to survive a closed tab, a timeout, or a reboot. (markdown ok)",
"verify": { "method": "e2e", "command": "…exact command…", "expected": "…observable result…" },
"agentQuestions": [ // optional — grill the user to sharpen this intent
{ "id": "scope", "q": "Does *every* answer include drag-order, or just card picks? (markdown ok)" }
], // answers come back as grill:[{id,q,answer}] on the round
"thread": [ {"role":"user","text":"…"}, {"role":"agent","text":"…"} ] // Q&A history, optional
}
],
"decisions": [
{
"id": "round-trip",
"title": "How decisions return to the agent",
"status": "needs-you", // needs-you | agent-call | fyi
"summary": "one line shown on the collapsed card",
"building": "what we're building for this decision (markdown ok)",
"tradeoffs": ["**option A**: …", "**option B**: …"], // optional, markdown ok
"verify": "how we'll prove this was right", // optional, string or {method,command,expected}
"options": ["Blocking server", "Watched file"], // optional; replaces Approve/Reject/Your-call
// UI decisions: options are mock objects instead — the user clicks a mock to pick it.
// These cards auto-render in the dedicated "Design mocks" section, expanded, zoomable.
// PREFER hi-fi HTML that reuses the real app's theme tokens + component classes,
// so the mock looks like the product. Same width/height across alternatives.
// "options": [
// {"label":"Sidebar", "caption":"nav left, content right",
// "html":"<!doctype html><html><head><style>:root{/* paste app tokens */}…</style></head><body>…full mock reusing real classes…</body></html>",
// "width":900, "height":600}, // rendered inline, sandboxed, scaled, zoomable
// {"label":"Top tabs", "caption":"full-width content", "html":"…", "width":900, "height":600},
// // SVG wireframe is the lo-fi FALLBACK (only when there's no app look to match):
// {"label":"Lo-fi", "caption":"blocky wireframe", "viewBox":"0 0 200 140",
// "svg":"<rect class='wf-frame' x='4' y='4' width='192' height='132'/>…keep all text inside the box…"}
// ],
"dependsOn": ["intent-persist"], // optional; linked cards shown with the user's answers
"thread": [], // Q&A history, optional
"diagramSvg": "<rect class='node' …/>…", // optional per-card diagram (intents too)
"diagramViewBox": "0 0 720 240"
}
],
"steps": [ // read-only reference
{ "title": "Step title", "description": "what it does (markdown ok)",
"files": "path/to/file", "test": "e2e", "intent": "intent-persist" }
],
"finalVerify": [ // plan-level gate, one row per intent
{ "intent": "intent-persist", "method": "e2e",
"command": "…exact command…", "expected": "…observable result…" }
],
"diagramSvg": "<rect class='node' …/>…", // REQUIRED — plan-level architecture/flow diagram
"diagramViewBox": "0 0 720 240" // optional
}
The deck also sends a global agentActions object with every round:
{"reexplore": bool, "note": "free text"} — the user's standing panel for
"re-explore the code before answering" and any other next-round instruction.
Notes:
code, fenced blocks,
bullet lists, links) — format the plan properly; don't write flat text.needs-you decisions
gate Finalize; a card is satisfied by a button pick OR free text OR an
in-place edit. Open questions also block Finalize (they must be answered in
a round first). A boundary answered "Not needed — drop it" is removed from
the next round's plan.options reads better than Approve/Reject when the decision is a concrete
pick between named approaches.DO:
/plan; only the
output surface differs.intents[] (one outcome + why + verify each) and a
plan-level diagram; add boundaries[] whenever scope needs a fence.agentActions every round — reexplore means re-run the explore
agents before answering anything.dependsOn.thread on the next round.title/summary in sync with its content — when the
meaning changes, rewrite the title; never leave a stale handle.run_in_background: true) so the user can type to
you in the terminal AND into the deck at the same time.cloudflared tunnel on demand (against the live port, no restart)
when the user wants the deck public / on their phone.uv run --directory to launch serve_plan.py (per CLAUDE.md).DON'T:
agent-call just to avoid asking — if it changes what you
build and you have no clear default, it's needs-you.answers.json — it's the user's work.Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub doryzi/plan-html --plugin plan-html