What does design-doc do?
gemini-4-argon writes and revises all prose, including summaries, plain
twins, handles, revision notes, and publication text. The session hands it the
facts and the rules (the writing contract, the slop-cop rules, the length caps)
through gemini -m gemini-4-argon --skip-trust -p, lands the reply verbatim, and collects
evidence and publishes the result itself. Prose helpers call the same model
through gemini --skip-trust -p, with no fallback model.
An architecture design is a stack of decisions on top of a stack of assumptions. This skill runs the design as a conversation — the user decides everything — and renders the result as a document where every claim traces back to an assumption, a decision, and the question round that produced it.
One command drives the mechanical parts:
TOOL="python3 ${CLAUDE_PLUGIN_ROOT}/skills/design-doc/scripts/design.py"
$TOOL scaffold --title <name> # fresh ./<slug>/ directory for this one design doc
$TOOL scaffold --example # ./tinyq/, a small filled-in worked example
$TOOL check <dir> # lint the registers, the plain twins, the diagram source and the summary deck; errors exit non-zero, --strict makes the publish-blocking warnings errors too
$TOOL summary-text <dir> # the deck as Markdown, one ## section per panel; the gates' input
$TOOL plainify <dir> [--only DQ3,A2] [--dry-run]
$TOOL glossary <dir> # terms the prose uses and the glossary lacks, as candidate entries
$TOOL build <dir> # compile components/*.tsx into components.js; only for a doc with a components/ directory
$TOOL render-check <dir> # render every Mermaid block in headless Chrome; fail on a parse error or a page exception
$TOOL pdf <dir> # print the doc through the template's print stylesheet into <dir>/design-doc.pdf
$TOOL snapshot <dir> --note "…" --item "…" # stamp a revision the changes-since view diffs against; note is the reader-facing headline, each --item one change
$TOOL links <dir> [--fetch] [--missing] # every decision and open item with its pull-request links and status; --fetch asks GitHub whether each link merged and reports drift, --missing lists the entries still unlinked
Read reference/method.md before Phase 1, reference/writing.md before Phase 5, and reference/publish.md before Phase 6 — the method file is the round/register protocol, the writing file is the voice contract, the publish file is the hosting flow. reference/schema.md is the field-by-field contract for the JSON files; scaffold the tinyq example when you want a filled register next to the schema.
Terminology
- Register — a structured list in
registers.jsonwhose entries have stable IDs:A#assumptions,DQ#decisions,Q#open questions,V#spikes,c-<slug>architecture cards. - Round — one board put to the user, logged verbatim in
qa-log.json. A round asks every open fork at once, each as adesign-doc.forkblock, and asks nothing else. Answers that open new forks start the next round, again all at once;qa-log.jsonrecords the rounds in order, so a later board can depend on an earlier answer. - Supersession — a changed decision gets a new entry and the old one gets
s: "superseded"plus abypointer, which the doc renders as "Replaced by" and the successor's title. History stays legible because nothing is edited in place. - Star (
★) — marks the load-bearing assumption, the one whose failure invalidates the document. - Spike — a named, time-boxed experiment (
V#) that turns an estimate marked(E)into a measured number. - Executive summary —
summary.html, a hand-written HTML fragment besideregisters.json, for the reader who reads nothing else: a deck of full-screen panels by default, one poster when the design is one idea and one figure. Never parsed as data;checklints its shape, its word budgets, and its citations. - Plain twin — the
pfield beside an entry's precise wording, one or two short sentences saying what the entry means to someone who will not read it. The doc shows twins by default and the exact wording one toggle away. - Key and theme —
key: truemarks the 3–8 decisions and assumptions the doc opens on as cards;themefiles every entry under one of the design's 3–5 themes, named in Phase 0 and defined in thethemesmap. - Diagram — the system diagram's Mermaid source, in the
diagramregister key; the doc renders it, the reader pans and hovers it, and the request paths step through it.diagram.overviewis the ten-node subset the Overview card draws, with the full graph one click away. A hand-drawnsysd.svgstays valid asdiagram.kind: "svg". - Handle — the
hfield on every decision, assumption, open item, arch card, and numbers table: what a reader would call it, two to five words. Citations and chip rows show the handle; the ID lives in the hover card. - Component — an interactive figure declared in the
componentsmap (dd.tabs,dd.before-after,dd.whatif,dd.steps,dd.timeline,dd.matrix, the two compare-panel figuresdd.flowanddd.lanes, the hand-drawn system mapdd.system, anddd.arch, which embeds a published architecture doc) and placed by id in the deck or on an entry; a TSX file undercomponents/is the escape hatchbuildcompiles. - Figure brief and critique — the two NOTES.md sections the deck is drawn from and read against. A brief is five lines per figure, written before the figure is drawn: the point it makes, the decision it serves by ID, the smallest view that makes the point, what it leaves out, and the real labels it uses. Its first two lines become the
<figcaption>. The critique is one row per panel, figure, table, and stat, written before the gates: the decision it serves, the claim it makes, the question a reader is left with, and fixed or cut.checkwarns when either heading is missing. - Link — an entry of
links[]on a decision, an open item, or a finding row: the pull request, issue, commit, or doc that lands it.closes: trueon an open item's link marks the one that retires it. The doc shows each link as a chip with its GitHub state and heads every open group with how many of its items are closed. - Statuses: decisions are
resolved | superseded | open, which the doc renders as Decided, Replaced, and Still open; assumptions areworking | validate, rendered Assumed and Needs someone to confirm (someone outside the document has to say yes). Open items areopen | closed, rendered Still open and Closed; a mergedcloseslink closes an item beforessays so. Prose uses the rendered words, JSON the values.
Scope
This skill stops at the design. Its outputs are a decision record and a document; implementation code is a different task that starts after the proposal survives review. Four gates keep the record honest:
- Design forks are the user's to decide. Every fork goes through a round, even when one option looks obviously right: the record of why is worth more than the saved exchange.
qa-log.jsonis verbatim and append-only. Clean up spelling in answers, change nothing of substance, and skip explain-only exchanges; it is a decision log, not a transcript.- The adversarial review artifact stays out of the rendered doc. Only the decisions it drove ship; a findings section reads as self-congratulation and tells the reader nothing.
- The doc carries no vanity counts ("27 entries", "9 spikes pending"). Numbers appear when they describe the system, not the effort.
Phase 0 — Scaffold and diagnosis
Run $TOOL scaffold --title <name>. Every design doc lives in its own fresh directory — scaffold creates ./<slug>/ (pass an explicit path as a positional argument to put it elsewhere) and refuses a non-empty target, so one design never mixes into another's files or an existing project's. Interview the user about the current system before proposing anything: what exists, what hurts, and why. Write the diagnosis into NOTES.md as root causes, not symptoms ("durability latency is S3 latency", not "writes are slow"). The diagnosis never goes on a board: write it straight into NOTES.md, the user reviews it in the rendered doc, and every correction they make is folded back in their own words. Close the diagnosis with the one classification the executive summary hangs on: a change to a system that exists today, or net-new, with nothing behind it but requirements. Name the design's three to five themes in the same breath ("storage", "dispatch", "tenancy"): they become the themes map the decisions and assumptions file under, and a theme the diagnosis cannot name is a theme the design does not have. Designing against a wrong diagnosis wastes every later phase.
Exit criteria: the project directory exists; the user has read the diagnosis in the rendered doc, change or net-new and the themes included, and agrees with it.
Phase 1 — Assumptions
Before the first round, check the available-skills list for cc-present:present (the /cc-present board skill) and invoke it with the Skill tool when listed; the interview runs as a live board from the very first question, with AskUserQuestion only as the no-cc-present fallback. This plugin ships its own block pack — run cc-present pack list and read design-doc's reference fragment; its design-doc.fork block is the interview surface, and the only block an interview board carries.
Write the assumptions straight into registers.json; no board confirms or challenges them. Never put a design-doc.claims block, a confirm/challenge sweep, or a register dump in front of the user: they review the assumptions in the rendered doc, and a correction they make is folded back in their own words. An assumption the design cannot proceed without choosing between is a fork, and goes on the Phase 2 board as a design-doc.fork block. Collect ground truths from the diagnosis interview, the code, and what the user has already said: constraints the design must satisfy, facts about scale and workload, things the user believes but hasn't verified. Each becomes an A# entry with status working or validate; star the load-bearing one, and record who has to confirm each validate entry as an open item. When the user flags an assumption as shaky, that flag goes in the entry; resolving it on their behalf would defeat the point of the register.
Exit criteria: the user has reviewed the assumptions in the rendered doc and they cover what the user knows; $TOOL check is clean.
Phase 2 — Design rounds
Design by question rounds. The interview is one cc-present board that asks every open fork at once, each as a design-doc.fork block with rich options: a consequence on every option, pros and cons, exactly one recommended, and the open-list escape. The board carries fork blocks and nothing else. Anything beside the forks is a defect: a design-doc.claims block, a confirm/challenge sweep, a design-doc.registers dump, or a context block. A fork's own question, consequences, and pros and cons carry the context it needs. Answers that open new forks go on the next board, built the same way.
Rounds run on a live cc-present board: before the first round (in Phase 1 and again here), check the available-skills list for cc-present:present (the /cc-present board skill) and invoke it with the Skill tool — its instructions govern composing and pushing the board, and every round from then on goes through a board. One fork is one design-doc.fork block, whose options carry their consequence inline and whose escape hatch returns {defer: {title, why}}: the human names the open question, so the Q# lands without spending another round asking what to call it. Where the pack is missing, a plain choice block carries the same shape by hand — consequence descriptions on every option, exactly one marked "(Recommended)", and a last option "Add to open list" that actually enqueues a Q# — and AskUserQuestion is the surface of last resort, used only when cc-present:present is absent from the skill list. While the board skill exists, design forks never go through AskUserQuestion. Ask each fork as a plain question a person can answer cold ("How should workers receive jobs?"). The register ID it settles goes in the block's decides field, or trails the AskUserQuestion header, never in the question text; an option's consequence says what happens, not which entry it supersedes. The rounds are an interview, not a survey: point out flaws in the current draft, suggest alternatives the user didn't name, push back where the evidence disagrees, and decide nothing yourself. Each answer becomes a DQ# with the resolution, the rejected alternatives, and the round number. When a later round changes an earlier decision, supersede: a new DQ#, a by pointer on the old one.
Exit criteria: no undecided fork remains outside the open list; every DQ# traces to a round in qa-log.json.
Phase 3 — Adversarial review
Attack the middle draft, before polish makes flaws harder to see. Use the codex plugin skill when it's available; otherwise spawn a fresh-context subagent with no stake in the design and a brief to attack it as a skeptical senior engineer: correctness bugs, missing failure modes, unjustified numbers. Save the output verbatim as <reviewer>-review-<date>.md, index each finding in the findings register (data only — never rendered), and disposition every one: a new decision, an open item, or a recorded rejection with a reason. A finding fixed by code carries the pull request that fixed it as the row's fifth element, in the same shape as a links[] entry. A finding that needs the user's call becomes a round like any other, whose question says what the reviewer found, in words; the finding number stays in the register. Then run the reviewer again on the updated registers: dispositions change the design, and a changed design grows new flaws. One pass is the floor, not the norm.
The register stops moving in this phase, so this is where every rendered entry gets its plain twin and its handle. p sits beside the precise wording of each tl;dr line, ground rule, decision, assumption, and open item, written by the hand that wrote the entry while the reasoning is fresh; h is the two-to-five-word name a citation shows for it. The twin says what the entry means to someone who will not read it, the handle what they would call it; both contracts are in reference/writing.md. $TOOL plainify <dir> drafts missing twins and handles with gemini-4-argon. Run it in the foreground with timeout: 600000. Never background it or watch it with a Monitor: Claude Code never wakes an idle in-process teammate for either notification. --provider none lists the gaps without generating prose. Read each draft against its original and edit it before moving on; a twin that drifts from its entry is a second claim the registers do not back. In the same pass, rewrite every decision title that is still a question as the answer, a noun phrase of at most ten words; the question stays in rounds[].q.
Exit criteria: every finding has a disposition, and the latest pass produced nothing that changes a decision; every rendered entry carries a twin and a handle you have read, no decision title ends in a question mark, and $TOOL check is clean.
Phase 4 — The quantitative story
Latency is one axis a design can be measured on, not the default. The doc carries a small library of quantitative components; pick the ones that describe this system, skip the rest (the doc hides empty sections), and skip the phase entirely when the design has no quantitative story:
- Request paths (
paths, plus thescaleMarksstrip) — for designs whose story is latency: p50/p95 segments per step, summed and traceable. - Load ceilings (
ceilings) — for designs whose story is load: each resource gets a ceiling, its first observable symptom, and the guard in front of it. - Number tables (
numbers) — any other axis, each table with its own columns: a throughput budget, storage growth, a cost model, freshness windows.
Two rules hold whichever components are in play. Every unmeasured number is marked (E) and gated on a named V# spike, because an estimate nobody plans to measure is a guess wearing a costume. And under load the system backpressures, rejects, or goes stale; a design whose overload mode corrupts data is off-design.
Exit criteria: no (E) without a spike; every ceiling row has a guard.
Phase 5 — The document
Read reference/writing.md first; the deck contract, the twin contract, and the voice contract live there, and its Figures section sends you to reference/gallery/ before you draft. Fill meta in registers.json (title, date, slug, banner for the starred assumption). Write summary.html before anything else: a deck of full-screen panels, thesis first, built from the diagnosis (what exists today, or what is required), the resolved decisions (what this proposes), and the numbers, with one figure drawn for this design on every panel. When the design is one idea with one figure, write a single poster panel instead and say so in NOTES.md. The panel kinds, the kit, and the word budgets are in reference/schema.md; check enforces the budgets. Put the system diagram's Mermaid source in the diagram register key, its nodes named after the arch cards they belong to, so the doc can pan it, highlight it, and step the request paths through it. Write diagram.overview beside it: the same graph cut to ten nodes, ids drawn from the full source, which is what the Overview card draws. Where a number or a sequence earns it, declare one or two components in the components map and place them, in a deck figure or on an entry; the kinds and the escape hatch are in reference/writing.md. The compare panel's figure is a kit component too. Two architecture designs set side by side, today beside proposed or one option beside another, are drawn with the architecture diagram component, dd.arch, never a picture: one arch/v1 doc giving each design a top-level node with its parts nested, a part only one design has marked planned or retired, and a note on each part that differs. dd.flow draws stages in columns with what each one leaks or fixes hanging off them, and dd.lanes two stacks of boxes for steps or ownership compared side by side. Both measure their labels and lay themselves out, so nothing overlaps at any width; a hand-drawn SVG is for a shape the kit lacks, and check says so when it meets one with eight or more boxes. Every label, in the diagrams, the deck, the chips, and the table heads, follows one capitalisation rule: acronyms upper-case (Tenant API, DPoP), product names as their owners write them (Postgres), identifiers in backticks, everything else sentence case. check lints labels against the acronym list in design.py, extended by meta.acronyms. Then set key: true on the 3–8 decisions and assumptions the doc should open on and file every one under a theme from Phase 0; write the tl;dr as three to five plain twins of at most 20 words each. Both files sit beside registers.json and the doc loads them at runtime, so the HTML itself is never edited. Then write the doc content in two passes: structure and de-jargoning first, then a separate tone pass whose test for every sentence is "does this solicit feedback, or make a claim?"; the doc exists to be corrected, not admired.
The deck is one argument, and every part of it names the decision it serves; the contract is in reference/writing.md under One argument. No figure is drawn before its brief, the thesis diagram included: five lines under "Figure briefs" in NOTES.md, whose first two lines become the figure's <figcaption>, and a figure whose brief cannot name a decision is not drawn. The system diagram draws the proposed system by role: one node per role, the one thing the design decides about it on a second line, and every edge labelled by what flows on it. Today's state lives in the compare figure, beside the proposal, and nowhere else. Every panel headline is a claim with a verb in it, every figure and table carries a caption citing its decision, every numbers table opens on its claim, and every stat's label cites the decision it changes or the risk it sizes.
Before the gates, run the critique pass: one row per panel, figure, table, and stat under "The critique" in NOTES.md, with the decision it serves, the claim it makes, the question a reader is left with, and whether it was fixed or cut. A row with no decision is cut, not argued for. Then read the headlines alone; they have to carry the argument on their own.
Five gates run over the summary, in this order, and each is a required step, not an available upgrade:
- Load the
writing-docsskill by path,~/.claude/plugins/cache/skills/writing-docs/0.7.0/skills/writing-docs/SKILL.md, and run its edit passes over the deck: completeness, then accuracy, then structure, then clarity, then brevity. - Run the voice gate:
wlm profile listfirst, always; with a profile, read the style card before drafting, then runwlm -p <profile> adversary critiqueover the output of$TOOL summary-text <dir>and over the exported Markdown, and fold in or explicitly reject every flag. The critique needs machine negatives and exits without a verdict when it has none; then, or without a profile, the fallback contract applies, and NOTES.md records which one graded the doc. - Run
slop-cop check summary.html --lang=html --llm-effort=off, then$TOOL summary-text <dir> > summary.mdandslop-cop check summary.md --lang=markdown --llm-effort=off. $TOOL check --strict <dir>: handles on every cited entry, no question titles, capitalisation, the word caps, component props, overview ids.$TOOL render-check <dir>: every Mermaid block and every component draws in headless Chrome.
Triage every finding in NOTES.md: fixed, or rejected with a reason. The numbered checklist with exact invocations is in reference/writing.md. Run slop-cop check over the doc's Markdown after each pass as well. Then $TOOL pdf <dir> and look at the pages with pdftoppm: a structural check tells you the PDF exists; only your eyes tell you it renders.
Exit criteria: the doc renders over python3 -m http.server 8641 and opens on the deck with nothing else visible; the PDF is built and visually inspected. Every figure has a brief and a caption naming its decision; every numbers table opens on its claim; the critique has a row for every panel, figure, table, and stat, and none lacks a decision. All five gates ran, in order, and every finding they raised is triaged in NOTES.md.
Phase 6 — Publish and handoff
Stage a clean deploy folder holding only the files meant to ship:
$TOOL snapshot . --note "<headline>" --item "<one change, for the reader>"
mkdir -p dist
cp design-doc.html dist/index.html
cp registers.json qa-log.json NOTES.md summary.html dist/
cp -R history dist/
A doc with a components/ directory runs $TOOL build . first and copies components.js too. The site config is an ai.json beside index.html, or one directory up for a collection, and it switches on two things: the assistant, from {endpoint, model, key, reasoning?}, and the GitHub state on every link chip, from github: {token}. Either half stands alone, {"disabled": true} turns the assistant off, and both halves are written at deploy from secrets and never committed; the token's scopes are in reference/publish.md.
Nothing else ships. The diagram source is in the registers, the PDF button prints the page on the fly through the template's print stylesheet, and the diagram libraries load from a CDN at pinned versions. So dist/ carries no sysd.svg, no PDF, and no vendored code. A doc that kept a hand-drawn diagram (diagram.kind: "svg") copies sysd.svg too. The snapshot stamps this publish as a revision; it hashes summary.html with the registers, so an edit to the deck is a revision on its own. From the second one onward, a returning reviewer lands directly in the diff against the revision they last read, unchanged content tucked behind a toggle. The note and --item bullets are the first thing that reader sees: write them in plain language for someone coming back after days — what changed and what it means for them, never round numbers or register IDs (the diff panel already lists those). The contract with a worked example is in reference/publish.md, and revision prose passes the same voice gate as the doc — wlm profile list, style card, adversary critique over the drafted note — before the snapshot is stamped.
Ask the user where it goes, then follow reference/publish.md: local serving is python3 -m http.server 8641; public hosting is wrangler deploy when authenticated, or wrangler deploy --temporary, which returns a claim URL that expires in 60 minutes; hand that to the user immediately. After deploying, one lightweight check (the page loads with the right title) is enough; exhaustive per-asset probing after a confirmed deploy is noise. Add a changelog entry to NOTES.md naming the deploy name and live URL — later register edits redeploy through the same name to the same URL, with the exact sequence in reference/publish.md.
After it ships
The register keeps recording once the proposal is accepted. Every pull request or issue that carries a decision or retires an open item goes on that entry's links[] the moment it opens, with closes: true on the open item it retires. A finding fixed by code takes the same link as its row's fifth element. When the pull request merges, set s: "closed" on the item, run $TOOL links <dir> --fetch so the states GitHub reports are checked against the register, and snapshot, so a returning reader sees what landed. $TOOL links <dir> --missing prints the entries still unlinked; it is the to-do list for a session that shipped the code and skipped the link. One link per entry, on the pull request that lands it, and the handle and twin stay as they were: the contract is in reference/writing.md, the field shapes in reference/schema.md.
Exit criteria: the user has the URL or serve command; the changelog records what shipped; the revision snapshot is recorded, its note and items having passed the voice gate first; $TOOL links <dir> --missing lists nothing the user did not knowingly leave unlinked.
Common issues
- PDF step fails with "no Chrome found" — install Chrome/Chromium or set
CHROME=/path/to/chrome.$TOOL pdfand$TOOL render-checkexit 2 with instructions. - Diagram host says "Diagrams need a network connection" — the doc imports Mermaid from jsdelivr at a pinned version and the import failed. The rest of the doc works;
$TOOL pdfand$TOOL render-checkneed the same network and say so when it is missing. - Doc shows a "Data not loaded" screen — it was opened as
file://; browsers block local-file fetch. Serve the folder over HTTP as the screen says. - Doc opens on the tl;dr with no Summary section —
summary.htmlis missing besideregisters.json, or wasn't copied intodist/.$TOOL checkwarns about the first. checkwarns "hand-drawn diagram" — the project still carries asysd.svgand no Mermaid source. Move the diagram intodiagram.source, or setdiagram.kind: "svg"to keep the file; the migration sequence is inreference/publish.md.- wlm voice profile absent — the fallback voice contract in
reference/writing.mdapplies. A missing profile excuses the style card, not thewlm profile listcheck that discovered it. - No Ask bar at the bottom right, no Cmd-K AI row — the page found no
ai.jsonbesideindex.htmlor one directory up, or it holds{"disabled": true}. Local work sets thedesign-doc-aikey inlocalStorage; a deploy writes the file from a secret, perreference/publish.md. - Link chips show no state dot, no hover card, no progress bar — the site config carries no
github.token, or the token cannot see the repository, which renders as unknown. Local work sets thedesign-doc-githubkey inlocalStorageto{"token": "…"}; a deploy writes that half from its own secret.$TOOL links <dir> --fetchreads the same states from the command line. checkerrors on adata-component— the host names an id thecomponentsmap lacks, or the entry's props fail its kind's schema underreference/components/. Declare the entry, then place it.