| name | shape-board |
| description | Serve a keyboard-driven data board that lets a human dispose of LLM output outside the console โ a Q&A board (multiple-choice over a doc) or a path-board (N candidate paths/plans/options as side-by-side columns, strike losers). Blocks until the human submits, returns their dispositions as JSON. Use instead of AskUserQuestion whenever there are โฅ3 items to compare or answer in parallel โ an interrogation/convergence loop, comparing candidate plans, sorting options to make a decision. Triggers on "shape-board", "path-board", "serve the board", "board these options/paths", "compare these side by side", "interrogation board". |
shape-board โ keyboard data boards for disposing of LLM output
The board is the non-isolated alternative to AskUserQuestion: it holds a whole
batch of items in spatial context beside the source, and the human drives it
entirely by keyboard. The calling loop owns convergence; this skill owns one
round โ present, collect, return.
One engine (serve.mjs), three board patterns. serve.mjs is payload+render-agnostic:
it injects the payload into a template, serves it, blocks on one POST, echoes the
result to stdout. WK_BOARD_TEMPLATE selects the template:
board.html (default) โ Q&A board: multiple-choice cards over a doc.
path-board.html โ path-board: N candidate paths/plans/options as side-by-side
columns; strike losers (whole path or per-milestone), customize, escalate. Use when
comparing โฅ3 whole routes the human narrows by ELIMINATION, not selection.
matrix-board.html โ comparison matrix: options (rows) ร criteria (columns) grid,
2D cell navigation. Strike an option (row), drop a criterion (column), flag a dealbreaker
cell, note a cell, elevate an option. Use for compare-across decisions over a shared
criteria set. Payload: {docText:"", head:{title,frontier,criteria:[...]}, batch:[{label, cells:[...]}]}
(batch = option-rows; criteria in head). Returns {dispositions:[{option,struck,struck_cells,notes,escalate}], dropped_criteria, invalidated, escalated}.
exec-board.html โ command-approval HITL: captured commands/code blocks; approve (a) the
ones to run, reject (x), note, escalate. Default PENDING โ nothing runs from the page; the AGENT
executes only approved[] out-of-band (offerโ commit). Payload: {docText:"", head:{title,frontier}, batch:[{cmd, lang?, note?, danger?}]}. Returns {dispositions, approved, rejected, escalated}.
quad-board.html โ 2ร2 quadrant: place items into four quadrants across two axes (1-4 assign,
0 unplace). Positioning is the gesture. Payload: {docText:"", head:{title,frontier,axes:{x,y},quadrants?:[4]}, batch:[{label,note?}]}. Returns {dispositions:[{label,quadrant,note}], by_quadrant, escalated}.
venn-board.html โ Venn (2 sets): assign each item to a region โ A-only (a), B-only (b),
both (i), neither (n). Payload: {docText:"", head:{title,frontier,sets:{a,b}}, batch:[{label,note?}]}. Returns {dispositions:[{label,region,note}], by_region, escalated}.
transcript-board.html โ transcript review: display turns (role+text); mark (space/m) the
keepers, note, escalate. Review-and-extract, not editing. Payload: {docText:"", head:{title,frontier}, batch:[{role, text}]}. Returns {dispositions, marked:[{i,role,text}], escalated}.
Q&A board โ contract
in { docText, batch:[ { q, choices:[ { label, what_changes } ], anchor? } ] }
out { dispositions:[ ... ], escalated:[q_text] }
docText โ the current (in-memory) shape doc, inlined. Never a path; the loop
holds the live doc and need not flush to disk.
batch[].anchor โ OPTIONAL. The doc element a question probes, e.g.
"inviolate:2". Display-only: it labels the card so the human sees which element
is under the knife. No cross-card behavior.
dispositions[] entries, one per card:
{ q, picks, pick_indices } choices picked โ arrays (multi-select; a single pick is a 1-element array)
{ q, custom } free-text answer
{ q, skip:true } skipped (also the default for untouched active cards)
{ q, escalate:true } human hit escalate โ route this q to a Workshop transform
Cards are multi-select: 1-5 toggle choices on/off, so a card can carry several
picks. picks[] holds the chosen labels, pick_indices[] the 0-based indices (sorted).
s/c/e are exclusive โ they clear any picks.
escalated[] โ the q of every escalate; the caller routes each to a Workshop
transform (fork into framings / adjudicate A vs B / lens), the board does not.
path-board โ contract
in { docText:"", head:{ title, frontier, forks:[{id,text}] },
batch:[ { id, archetype, grade, thesis, milestones:[{m,note}],
leans, risks, trades, forkFrom } ] }
out { dispositions:[ { id, archetype, struck, struck_milestones:[m], custom, escalate } ],
invalidated:[id], // whole paths struck
escalated:[id] } // paths sent to a Workshop transform
Polarity is elimination, not selection: the human strikes LOSERS with territory
knowledge; whatever survives un-struck carries forward. Do not ask it to pick a winner.
(docText must be "" โ the header context rides in head; the engine still validates
docText as a string.)
Invoke
Both boards run through the same engine; WK_BOARD_TEMPLATE picks the board. Paths
below use $SKILL = this skill's directory (where this SKILL.md lives, e.g.
~/.claude/skills/shape-board).
node "$SKILL/serve.mjs" /tmp/qa.json > /tmp/qa.result.json
WK_BOARD_TEMPLATE=path-board.html node "$SKILL/serve.mjs" /tmp/paths.json > /tmp/paths.result.json
Run serve.mjs in the background so the agent is not locked while the human works โ
you are notified when they submit. The server opens the browser (open; set
WK_BOARD_NO_OPEN=1 for headless) and blocks on a single POST /submit fired by
Enter; diagnostics go to stderr, the {dispositions, โฆ} object to stdout.
Port defaults to 3335 (override with WK_BOARD_PORT). node may be shadowed by a
broken nvm shell function โ use an absolute node path (e.g. /opt/homebrew/bin/node)
if it loops on _load_nvm.
Keyboard reference
Q&A board:
1-5 toggle choice N (multi-select) s skip
c custom (free-text) e escalate โ Workshop transform
โ โ move between cards โ submit the round
path-board:
โ โ move between paths (columns) space strike a milestone (invalidate step)
โ โ move between milestones x strike the whole path (invalidate route)
c customize (steer note) e escalate โ Workshop
โ submit
Async loop โ many one-shot boards, agent off the console (seed ฮฒ)
The engine is one-shot by design (serve โ block โ submit โ exit 0). "Async" is NOT a
persistent server โ it is the AGENT running async around a sequence of one-shot boards.
This keeps the engine host-agnostic (no standing state) and makes the offerโ commit invariant
hold for free: each board is an offer, and between boards the agent only composes the next
offer โ it never commits on the human's behalf.
The loop (the human never returns to the chat console between rounds):
1. write payload-N.json
2. serve it in the BACKGROUND: node "$SKILL/serve.mjs" payload-N.json > result-N.json
(the submit โ exit 0 IS the wake signal โ you are notified when the human hits Enter)
3. on notification, read result-N.json (the structured disposition)
4. COMPOSE payload-(N+1).json from it โ a fresh OFFER (never a commit)
5. go to 2, until the human's disposition ends the loop
Use the harness's background-run + wake-on-exit (or Monitor watching result-N.json). Hard
invariant: step 4 is re-offer only. An agent that acts on a disposition instead of
re-boarding it violates inviolate #1 (the human's Enter is the only commit).
Same-tab vs new-tab: reuse WK_BOARD_PORT across rounds so the browser refreshes in place
rather than opening a fresh tab each round โ new-tab churn can feel like "returning to a new
surface" even though the console is never touched.
Context-follow โ the doc pane tracks focus (seed ฮฑ, Q&A board)
On the Q&A board, batch[].anchor is no longer display-only: if the anchor text appears in
docText, focusing that card scrolls the left doc pane to the matching section and flashes it.
Author anchors as substrings of docText to light them up; anchors absent from the doc are a
graceful no-op. Keyboard-driven (rides the existing โโ focus), commits nothing.
Health
- Blank cards / "docText must be a string" โ payload malformed; the server
prints the reason to stderr and exits 1 before serving.
- Browser did not open โ visit the URL printed on stderr manually.
- Nothing returns โ the human has not hit Enter yet; the process blocks by
design until one submit arrives.