Design specification for Cadora's walk-away review: the parked run-state pill, the parked triage callout with its per-gate cards, the decided state, mobile behavior, the interaction contract, and the terminal moments on either side of the park.

Cadora · design spec

The parked run, by its surfaces

Everything needed to rebuild or extend the walk-away review surface — the exact tokens, the parked pill, the triage callout's anatomy, its states, the mobile rules, and the terminal lines on either side of the park. The rendered components are live HTML; edit them here or lift the specs into Figma.

Palette

Amber family, two voices — the flag asks, the pill waits


Parked amber#e8a13aPill text · on rgba(240,180,41,.15)
Flag amber--amber · #ffd08a⏸ Parked flag · callout border
Green--green · #16c784Approve · stored · decided ✓
Red--red · #fb7185Abort outline
Blue--blue · #3b82f6Document links
Ink--bg · #0f172aPage ground · comments field
Canvas--canvas · #111c2fGate card · reviewer input
Panel--panel-2 · #162237Callout surface
Line--line · #334158Borders
Muted--muted · #9aa8bcCost · kind chip · status msg

Three depths carry the hierarchy: the callout sits on --panel-2, each gate card insets to --canvas, and the comments field insets again to --bg — the darker the surface, the closer to the reviewer's own words.

Type & metrics

System sans for triage · monospace where the run speaks


RoleFamilySize / weightNotes
Node idsystem-uiinherit / 700<strong> in .park-head, left cell
Cost so farsystem-uiinherit$1.2874 so far — four decimals, right cell; omitted when unknown
Doc linksystem-ui13px--blue, word-break: break-all for long paths
Kind chipsystem-ui11px.park-kindnew / modified, muted, 6px left margin
Decided line · status msgsystem-ui13px.park-done green · .park-msg muted
Run-state pillsystem-ui12px / line-height 1Lowercase status word, e.g. parked
MetricValueMetricValue
Callout radius · padding12px · 14px 16pxGate card radius · padding10px · 10px 12px
Pill radius · padding999px · 5px 8pxComments padding6px 8px · 2 rows
Button padding7px 16pxButton padding ≤480px10px 18px · 14px
Decided opacity0.75Reviewer maxlength80
Poll interval5sRe-render after store900ms

The parked pill

One state where silence is healthy


running completed failed review_waiting stale? parked
PillFill / textMeaning
running · created#173765 / #3b82f6A conductor process is alive and working.
completed#c9ffd8 / #08783dFinalized, ok: true.
failed · error#4b1520 / #fb7185Finalized, something broke.
review_waiting#3b2f15 / #ffd08aLive gate — a process is blocked on a human right now.
stale?#3b2f15 / #ffd08aBadge on an "active" run whose status.json went quiet — the conductor may be dead.
parkedrgba(240,180,41,.15) / #e8a13aNo process, on purpose. Gates await review; cadora resume continues.

parked shares the amber family with review_waiting and stale? but is the only translucent fill — a run that is deliberately not running, not one that went quiet. The staleness check exempts it: parked is the one state where untouched-for-days is healthy. While parked, status.json reads "status": "parked" with the pending node list, and the manifest stays "ok": null — in flight is the truth.

Anatomy — the parked triage callout

One card per pending gate · decisions stored now, applied at resume


1
⏸ Parked2 gate(s) await review — the run is not running; decisions apply at cadora resume
2
Your name — recorded in the evidence with this decision
3
design$1.2874 so far
4 5
Comments — required to request changes
6
ApproveRequest changesAbort
1Header row — the .review-flag reads ⏸ Parked, then the count sentence: N gate(s) await review — the run is not running; decisions apply at cadora resume. The callout is the same amber-bordered .review-callout as the live panel, with the parked modifier.
2Reviewer name#park-reviewer, one input above all gates (max 80 chars). Prefilled from the browser's cadora-reviewer key; whatever is typed is recorded on every decision below it.
3Gate head.park-head: the node id in bold, and the node's spend to date on the right ($1.2874 so far) so the decision is priced.
4Documents.park-docs: each changed file as a link (opens the rendered doc in a new tab) with a .park-kind chip, new or modified. Empty gates say no changed documents.
5Comments.park-comments, two rows; required before Request changes, optional otherwise. Travels with the stored decision into the resumed run.
6Decision row — the same .review-btn classes as the live review panel: Approve (approve, green outline), Request changes (changes, neutral), Abort (abort, red outline), then the inline .park-msg status.

States

Every condition a gate card must render


Open · undecided

design$1.2874 so far
Comments — required to request changes
ApproveRequest changesAbort

Storing

design
ApproveRequest changesAbortstoring…

Stored · confirms, then re-renders

design
ApproveRequest changesAbort✓ stored: approve — applies at resume

Decided · reduced opacity

design✓ approve by Yves — applies at resume

Refused · the server's words

design
ApproveRequest changesnode 'design' already has a parked decision

No changed documents

ops-readiness$0.4310 so far
no changed documents
ApproveAbort

A decided gate collapses to its head line at opacity 0.75: ✓ <decision> by <name> — applies at resume (the by clause appears only when a reviewer was named). One decision per gate — a second attempt is refused with 409 and the refusal is printed in place. The callout renders as long as gates are pending; once every gate is decided the run simply waits, decided cards intact, for cadora resume.

Mobile — triage and decide

≤480px · the card stacks, the tap targets grow


design$1.2874 so far
Comments — required to request changes
ApproveRequest changesAbort
Rule at ≤480pxValue
Actions wrap.review-actions gains flex-wrap: wrap — three buttons never shrink to fit one line.
Tap targets grow.review-btn padding 10px 18px, font 14px.
Head stacks.park-head goes column, aligned to the start — node id above cost.
Workspace single-column.run-workspace drops its two-column grid to display: block.

The product rule: mobile is for triage and deciding — on a phone you decide gates you already understand. The full reading and annotation experience deliberately stays on desktop; the phone gets the verdicts, the desk gets the documents.

Interaction & behavior

What each control does, and the contract that protects the reviewer


ControlActionResult
ApprovePOST approveStored in the archive; the node passes when the run resumes.
Request changesPOST request_changes + commentsStored; at resume the stage re-runs with the comments in its prompt.
AbortPOST abortStored; the resumed run stops at this gate, recorded in the evidence.
Document linkOpen the rendered fileServes the doc's current bytes from the run workspace, new tab.
Reviewer inputSaved on submitName recorded on the decision and remembered as cadora-reviewer.
RuleWhy
Polling pauses while any gate is undecidedThe 5s auto-poll skips its refresh until every pending gate has a decision — a re-render must never wipe a half-typed comment. After a store, the panel re-renders once (900ms) to show the decided card.
cadora-reviewer persistsTyped once, the name prefills every later gate on every run from that browser — the live review panel reads the same key, so the identity follows the reviewer between surfaces. Each device's browser keeps its own copy.
JSON-only POSTPOST /api/runs/<id>/park/decision with Content-Type: application/json — body {node_id, decision, comments, reviewer}. Any other content type is refused (415).
Decisions bind to bytesThe store records the SHA-256 of each document's bytes at decision time — what the reviewer actually saw. A resume refuses to honor the decision if those bytes changed.
Same validity rules as every surfaceOnly approve / request_changes / abort; request_changes requires comments. The recorded method is dashboard.
RefusalServer responseThe panel shows
409 already decidednode 'design' already has a parked decisionThe server's error, verbatim, in .park-msg.
400 not pendingnode 'design' is not pending in this parkVerbatim.
400 invalid decisionrequest_changes requires reviewer comments · invalid review decision: '…'Verbatim — the gate stays open to retry.
404 not parkedrun is not parked - use the live review endpointNo JSON body, so the fallback: failed to store the decision.

Terminal moments

The park banner · the resume line — the surface's bookends


⏸ parked — 2 gate(s) awaiting review: requirements, design the agents' completed work is archived; nothing re-runs on resume. decide from the dashboard while parked (it applies at resume), or live at resume. continue with: cadora resume runs/pr1-park

The process then exits with code 75EX_TEMPFAIL, the sysexits convention for a temporary condition where the caller is invited to retry. Wrappers and schedulers must never read a waiting human as a failure. Parking is opted into per run with --on-review park; the default (wait) blocks in place.

▸ applying the decision made while parked: approve by Yves via dashboard ✗ stored decision for 'design' is invalid — the gate re-asks

At resume, each stored decision announces itself before it acts: <decision> by <reviewer> via <method> (an unnamed reviewer prints as (unattributed)). A stored decision that fails validation is discarded with the red line — one shot, then the gate falls back to live review. Nothing re-runs and nothing is re-paid: completed nodes reload verbatim from the archive.

Voice

Words are part of the design


DoNot
Name the state and the consequence — “the run is not running; decisions apply at cadora resume“Paused”
Name when it lands — “✓ stored: approve — applies at resume”“Saved!”
Relay the refusal verbatim — “node 'design' already has a parked decision”“Error 409”
Price the decision — $1.2874 so farHiding spend until the run ends
Sentence case — “Request changes”Title Case — “Request Changes”
Tokens and behavior are taken verbatim from cadora/dashboard/static/style.css, cadora/dashboard/static/app.js, and cadora/runner.py. Companion reading: the walk-away review manual, the user journey, and the live review panel's design spec — the parked surface reuses its decision row on purpose.