diff --git a/design/war-room/README.md b/design/war-room/README.md new file mode 100644 index 0000000..7d2ecd5 --- /dev/null +++ b/design/war-room/README.md @@ -0,0 +1,53 @@ +# War Room TUI design + +Visual spec for `conduit watch` as a terminal UI (issue #89, PRD `prd/drafts/war-room.md`). +Exported from the Claude Design "War Room TUI" page on 2026-09-28, after four rounds on the +web layout and one round constraining it to a character grid. + +This is a design reference, not production code. The real TUI renders from the journal +projection with `@opentui/react`. + +## Files + +| Path | What it is | +|---|---| +| `frames/01-live-mid-mission.png` | Live run at T+47:00, WI-204 selected, 160x45 cell grid overlaid | +| `frames/02-card-journal-tool-selected.png` | Card drawer on WI-204, tool call #06 (Bash, exit 1) selected | +| `frames/03-approaching-the-wall.png` | T+1:52:20, budget line in view, 5h quota loud at 86%, WI-209 OVERDUE selected | +| `frames/04-color-tokens.png` | Every color token as a swatch | +| `golden/*.txt` | The same three frames as plain text, 160 columns by 45 rows | +| `golden/*.ans` | The same frames with 24-bit ANSI color and bold, one escape run per span | +| `tokens.json` | Color tokens as hex: stations (live, past, done), states, text and chrome, tool ticks | +| `tui-reference.js` | The cell-grid renderer the design page used, plus its mock scenario | + +`golden/` was generated by running `build()` from `tui-reference.js` under Bun at animation +phase 0. `cat golden/03-approaching-the-wall.ans` in a truecolor terminal shows the frame. + +## Rendering rules + +- A fixed grid, 160 columns by 45 rows minimum. Every element occupies whole cells. +- One font size. Emphasis is bold, normal, or dim. +- Borders and dividers are box-drawing characters (`─ │ ┌ ┐ └ ┘ ├ ┤ ┬ ┴`). +- Waterfall bars are full blocks with eighth blocks (`▏▎▍▌▋▊▉`) for partial ends. When two + segments meet inside one cell, the left color is the foreground of an eighth block and the + right color is its background (see `bar()`). +- Working segments animate by swapping `█` for `▓` every fourth cell. The overdue part of a + held bar is `▒` in the overdue color. The rework marker is `«`. The OVERDUE state label + blinks between an overdue fill and overdue text. +- Header meters are 8 cells of `█`, eighth blocks, and `░`. At 80% or more the whole meter + gets the loud fill. +- There is no hover. The selected row's details go in the status bar on row 42. The footer + carries key hints: `j/k` select, `enter` open drawer, `t` tool ticks, `r` replay, `esc` close. +- The drawer is 60 columns wide and its body scrolls, with an indicator in the last column. + +## Using the golden frames + +`tui-reference.js` draws from a hard-coded scenario (`CARDS`, `CALLS`, `FRAMES`). A journal +fixture that reproduces that scenario lets the real TUI's `captureCharFrame()` output be +compared against `golden/*.txt`. Expect small differences where the real projection has +data the mock did not; treat a difference as a question about the design or the projection, +and update the golden file only on purpose. + +The mock scenario shows some fields the journal may not record yet (for example, a +finding's severity). Where the journal lacks a field, the TUI renders "not recorded" and +logs the gap (PRD FR-13) instead of copying the mock. diff --git a/design/war-room/frames/01-live-mid-mission.png b/design/war-room/frames/01-live-mid-mission.png new file mode 100644 index 0000000..17b5db1 Binary files /dev/null and b/design/war-room/frames/01-live-mid-mission.png differ diff --git a/design/war-room/frames/02-card-journal-tool-selected.png b/design/war-room/frames/02-card-journal-tool-selected.png new file mode 100644 index 0000000..983835d Binary files /dev/null and b/design/war-room/frames/02-card-journal-tool-selected.png differ diff --git a/design/war-room/frames/03-approaching-the-wall.png b/design/war-room/frames/03-approaching-the-wall.png new file mode 100644 index 0000000..eac4ddc Binary files /dev/null and b/design/war-room/frames/03-approaching-the-wall.png differ diff --git a/design/war-room/frames/04-color-tokens.png b/design/war-room/frames/04-color-tokens.png new file mode 100644 index 0000000..053d6a7 Binary files /dev/null and b/design/war-room/frames/04-color-tokens.png differ diff --git a/design/war-room/golden/01-live-mid-mission.ans b/design/war-room/golden/01-live-mid-mission.ans new file mode 100644 index 0000000..a749f25 --- /dev/null +++ b/design/war-room/golden/01-live-mid-mission.ans @@ -0,0 +1,45 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONDUIT │ WALL ██▋░░░░░ 33% │ TOKENS █████░░░ 62% │ QUOTA 5h ███▎░░░░ 41% │ WATCHDOG █▊░░░░░░ 02:41 │ 4 working 2 waiting 1 held 3 done 1 scrap │ T+47:00 │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ decompose test implement review probe deliver │ +│ card 0 5m 10m 15m 20m 25m 30m 35m 40m T+47:00 state │ +│ ┌─ PRD-014 range-queries █████████████▊·fan-out ×11··················································································│ awaiting │ +│ ├─ WI-201 range-schema ▊█████████████████████████▏███████████▉█████▌ │ ✓ 25:36 │ +│ ├─ WI-202 cli-flags ▉██████████▎██████████████████████████▍█████████████▋ │ ✓ 29:04 │ +│ ├─ WI-203 date-parse ··▏██████████▋██████████████████████▌████████▊███████████▎██████▏ │ ✓ 34:09 │ +│>├─ WI-204 parse-range ··▏████████▎█████████████████▋██████▌«██████████████▋██████▌«█▓███▓███▓███▓███▓███▓███▓███▓███▌ 15:00 rw2/3 │ +│ ├─ WI-205 index-scan ·············▋███████████▏███████████████████████████████▎████████▍███▓███▓███▓███▓███▓███▓███▌ 12:00 │ +│ ├─ WI-206 query-planner ··················▏██████████▋███████████████████████████████████████████▍▓███▓███▓███▓███▓███▌ 09:00 │ +│ ├─ WI-207 legacy-shim ····▍████████▋████████████████████████████████████████▉█ SCRAP · no_progress │  SCRAP  │ +│ ├─ WI-208 docs-ranges ······▋████████▉█████████████████████████████▊████████▉█████████████▊··················▏██▓███▌ 03:00 │ +│ ├─ WI-209 migration-0042 ······▋███████████▏██████████████████████▏██████████▋human decision · approve migration 0042██│  HELD 06:00  │ +│ ├─ WI-210 perf-bench ······················▊███████████▎···························································│ │ +│ ├─ WI-211 e2e-suite ······························································································│ │ +│ └─ fan-in · quorum 10 ────────────────────────────────────────────▌────────▌───────────▌─────────────────────────────│ 3/11 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ WI-204 parse-range │ implement attempt #6 · 15:00 · rework 2/3 │ $6.20 · 639.8k tok │ last call Bash npx tsc --noEmit running │ +│  LIVE  46:12 WI-205 attempt #5 probe · Bash npm run probe:fuzz -- index j/k select enter open drawer t tool ticks r replay esc close │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/design/war-room/golden/01-live-mid-mission.txt b/design/war-room/golden/01-live-mid-mission.txt new file mode 100644 index 0000000..74f1006 --- /dev/null +++ b/design/war-room/golden/01-live-mid-mission.txt @@ -0,0 +1,45 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONDUIT │ WALL ██▋░░░░░ 33% │ TOKENS █████░░░ 62% │ QUOTA 5h ███▎░░░░ 41% │ WATCHDOG █▊░░░░░░ 02:41 │ 4 working 2 waiting 1 held 3 done 1 scrap │ T+47:00 │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ decompose test implement review probe deliver │ +│ card 0 5m 10m 15m 20m 25m 30m 35m 40m T+47:00 state │ +│ ┌─ PRD-014 range-queries █████████████▊·fan-out ×11··················································································│ awaiting │ +│ ├─ WI-201 range-schema ▊█████████████████████████▏███████████▉█████▌ │ ✓ 25:36 │ +│ ├─ WI-202 cli-flags ▉██████████▎██████████████████████████▍█████████████▋ │ ✓ 29:04 │ +│ ├─ WI-203 date-parse ··▏██████████▋██████████████████████▌████████▊███████████▎██████▏ │ ✓ 34:09 │ +│>├─ WI-204 parse-range ··▏████████▎█████████████████▋██████▌«██████████████▋██████▌«█▓███▓███▓███▓███▓███▓███▓███▓███▌ 15:00 rw2/3 │ +│ ├─ WI-205 index-scan ·············▋███████████▏███████████████████████████████▎████████▍███▓███▓███▓███▓███▓███▓███▌ 12:00 │ +│ ├─ WI-206 query-planner ··················▏██████████▋███████████████████████████████████████████▍▓███▓███▓███▓███▓███▌ 09:00 │ +│ ├─ WI-207 legacy-shim ····▍████████▋████████████████████████████████████████▉█ SCRAP · no_progress │ SCRAP │ +│ ├─ WI-208 docs-ranges ······▋████████▉█████████████████████████████▊████████▉█████████████▊··················▏██▓███▌ 03:00 │ +│ ├─ WI-209 migration-0042 ······▋███████████▏██████████████████████▏██████████▋human decision · approve migration 0042██│ HELD 06:00 │ +│ ├─ WI-210 perf-bench ······················▊███████████▎···························································│ │ +│ ├─ WI-211 e2e-suite ······························································································│ │ +│ └─ fan-in · quorum 10 ────────────────────────────────────────────▌────────▌───────────▌─────────────────────────────│ 3/11 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ WI-204 parse-range │ implement attempt #6 · 15:00 · rework 2/3 │ $6.20 · 639.8k tok │ last call Bash npx tsc --noEmit running │ +│ LIVE 46:12 WI-205 attempt #5 probe · Bash npm run probe:fuzz -- index j/k select enter open drawer t tool ticks r replay esc close │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/design/war-room/golden/02-card-journal-tool-selected.ans b/design/war-room/golden/02-card-journal-tool-selected.ans new file mode 100644 index 0000000..a781695 --- /dev/null +++ b/design/war-room/golden/02-card-journal-tool-selected.ans @@ -0,0 +1,45 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONDUIT │ WALL ██▋░░░░░ 33% │ TOKENS █████░░░ 62% │ QUOTA 5h ███▎░░░░ 41% │ WATCHDOG █▊░░░░░░ 02:41 │ 4 working 2 waiting 1 held 3 done 1 scrap │ T+47:00 │ +├──────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────┤ +│ decompose test implement review probe deliver │ WI-204 parse-range esc close │ +│ card 0 10m 20m 30m T+47:00 state │ working · rework 2 of 3 │ +│ ┌─ PRD-014 range-qu… ███████▍·fan-out ×11······································│ awaiting │ 6 attempts · $6.20 · 639.8k tok │ +│ ├─ WI-201 range-sc… ▍████▍███████████▋██▉██▉ │ ✓ 25:36 ├───────────────────────────────────────────────────────────┤ +│ ├─ WI-202 cli-flags █████▋██████████▋███▏████▍█▊ │ ✓ 29:04 │ 06:53 fan-out PRD-014 → test █│ +│ ├─ WI-203 date-par… ▋█████▊████████████▏████▏█████▎███ │ ✓ 34:09 │ 11:58 #1 test $0.42 38.1k tok 12 calls █│ +│>├─ WI-204 parse-ra… ▋████▋█████████▌███▏«██████▊███▌«███▓███▓███▓███▓█▌ 15:00 rw2/3 │ 19:54 #2 implement $2.10 212.0k tok 41 calls █│ +│ ├─ WI-205 index-sc… ······▊███████████████████████▎████▎▓███▓███▓███▓█▌ 12:00 │ 22:45 #3 review $0.61 64.3k tok 18 calls █│ +│ ├─ WI-206 query-pl… ·········▎█████▌██████████████████████▉█▓███▓███▓█▌ 09:00 │ 22:45 REJECT review → implement rw 1/3 █│ +│ ├─ WI-207 legacy-s… ·▉████▊███████████████████████ SCRAP · no_progress│  SCRAP  │ src/range.ts:88 missing null check on █│ +│ ├─ WI-208 docs-ran… ···▏████████████████████▏███████████▍·········▎█▓█▌ 03:00 │ open-ended range █│ +│ ├─ WI-209 migratio… ···▏█████▎███████████▋███▍█▊human decision████████│  HELD 06:00 │ test/range.test.ts no case for empty input █│ +│ ├─ WI-210 perf-ben… ···········▊█████▉································│ │ 29:04 #4 implement $1.64 171.0k tok 36 calls █│ +│ ├─ WI-211 e2e-suite ··················································│ │ 32:55 #5 review $0.55 58.0k tok 15 calls █│ +│ └─ fan-in · quorum 10 ───────────────────────▌────▌──────▌───────────────│ 3/11 │ 32:55 REJECT review → implement rw 2/3 █│ +│ │ src/range.ts:88 attempt #4 reverted the █│ +│ │ fix from attempt #2 █│ +│ │ test/range.test.ts:41 empty-input case █│ +│ │ asserts the wrong error █│ +│ │ 32:55 #6 implement $0.88 96.4k tok 23 calls █│ +│ │ running 15:00 █│ +│ │ █│ +│ │ tool calls · #6 █│ +│ │ R R G R E B R G E B R E B R E B G R E B R E B █│ +│ │ ^ █│ +│ │ R Read G Grep E Edit B exit 0 B nonzero B running █│ +│ │ █│ +│ │ #06 Bash npm test -- range  exit 1 0:41 █│ +│ │ ││ +│ │ #01 Read src/range.ts   0:03 ││ +│ │ #02 Read test/range.test.ts   0:02 ││ +│ │ #03 Grep "parseRange" src/   0:01 ││ +│ │ #04 Read src/cli/args.ts   0:04 ││ +│ │ #05 Edit src/range.ts   0:06 ││ +│ │ #06 Bash npm test -- range  exit 1 0:41 ││ +│ │ #07 Read src/range.ts   0:02 ││ +│ │ #08 Grep "Number.isNaN" src/   0:01 ││ +│ │ #09 Edit src/range.ts   0:05 ││ +├──────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────┤ +│ WI-204 parse-range │ implement attempt #6 · 15:00 · rework 2/3 │ $6.20 · 639.8k tok │ last call Bash npx tsc --noEmit running │ +│  LIVE  46:12 WI-205 attempt #5 probe · Bash npm run probe:fuzz -- index j/k select enter open drawer t tool ticks r replay esc close │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/design/war-room/golden/02-card-journal-tool-selected.txt b/design/war-room/golden/02-card-journal-tool-selected.txt new file mode 100644 index 0000000..260e2e4 --- /dev/null +++ b/design/war-room/golden/02-card-journal-tool-selected.txt @@ -0,0 +1,45 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONDUIT │ WALL ██▋░░░░░ 33% │ TOKENS █████░░░ 62% │ QUOTA 5h ███▎░░░░ 41% │ WATCHDOG █▊░░░░░░ 02:41 │ 4 working 2 waiting 1 held 3 done 1 scrap │ T+47:00 │ +├──────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────┤ +│ decompose test implement review probe deliver │ WI-204 parse-range esc close │ +│ card 0 10m 20m 30m T+47:00 state │ working · rework 2 of 3 │ +│ ┌─ PRD-014 range-qu… ███████▍·fan-out ×11······································│ awaiting │ 6 attempts · $6.20 · 639.8k tok │ +│ ├─ WI-201 range-sc… ▍████▍███████████▋██▉██▉ │ ✓ 25:36 ├───────────────────────────────────────────────────────────┤ +│ ├─ WI-202 cli-flags █████▋██████████▋███▏████▍█▊ │ ✓ 29:04 │ 06:53 fan-out PRD-014 → test █│ +│ ├─ WI-203 date-par… ▋█████▊████████████▏████▏█████▎███ │ ✓ 34:09 │ 11:58 #1 test $0.42 38.1k tok 12 calls █│ +│>├─ WI-204 parse-ra… ▋████▋█████████▌███▏«██████▊███▌«███▓███▓███▓███▓█▌ 15:00 rw2/3 │ 19:54 #2 implement $2.10 212.0k tok 41 calls █│ +│ ├─ WI-205 index-sc… ······▊███████████████████████▎████▎▓███▓███▓███▓█▌ 12:00 │ 22:45 #3 review $0.61 64.3k tok 18 calls █│ +│ ├─ WI-206 query-pl… ·········▎█████▌██████████████████████▉█▓███▓███▓█▌ 09:00 │ 22:45 REJECT review → implement rw 1/3 █│ +│ ├─ WI-207 legacy-s… ·▉████▊███████████████████████ SCRAP · no_progress│ SCRAP │ src/range.ts:88 missing null check on █│ +│ ├─ WI-208 docs-ran… ···▏████████████████████▏███████████▍·········▎█▓█▌ 03:00 │ open-ended range █│ +│ ├─ WI-209 migratio… ···▏█████▎███████████▋███▍█▊human decision████████│ HELD 06:00 │ test/range.test.ts no case for empty input █│ +│ ├─ WI-210 perf-ben… ···········▊█████▉································│ │ 29:04 #4 implement $1.64 171.0k tok 36 calls █│ +│ ├─ WI-211 e2e-suite ··················································│ │ 32:55 #5 review $0.55 58.0k tok 15 calls █│ +│ └─ fan-in · quorum 10 ───────────────────────▌────▌──────▌───────────────│ 3/11 │ 32:55 REJECT review → implement rw 2/3 █│ +│ │ src/range.ts:88 attempt #4 reverted the █│ +│ │ fix from attempt #2 █│ +│ │ test/range.test.ts:41 empty-input case █│ +│ │ asserts the wrong error █│ +│ │ 32:55 #6 implement $0.88 96.4k tok 23 calls █│ +│ │ running 15:00 █│ +│ │ █│ +│ │ tool calls · #6 █│ +│ │ R R G R E B R G E B R E B R E B G R E B R E B █│ +│ │ ^ █│ +│ │ R Read G Grep E Edit B exit 0 B nonzero B running █│ +│ │ █│ +│ │ #06 Bash npm test -- range exit 1 0:41 █│ +│ │ ││ +│ │ #01 Read src/range.ts 0:03 ││ +│ │ #02 Read test/range.test.ts 0:02 ││ +│ │ #03 Grep "parseRange" src/ 0:01 ││ +│ │ #04 Read src/cli/args.ts 0:04 ││ +│ │ #05 Edit src/range.ts 0:06 ││ +│ │ #06 Bash npm test -- range exit 1 0:41 ││ +│ │ #07 Read src/range.ts 0:02 ││ +│ │ #08 Grep "Number.isNaN" src/ 0:01 ││ +│ │ #09 Edit src/range.ts 0:05 ││ +├──────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────┤ +│ WI-204 parse-range │ implement attempt #6 · 15:00 · rework 2/3 │ $6.20 · 639.8k tok │ last call Bash npx tsc --noEmit running │ +│ LIVE 46:12 WI-205 attempt #5 probe · Bash npm run probe:fuzz -- index j/k select enter open drawer t tool ticks r replay esc close │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/design/war-room/golden/03-approaching-the-wall.ans b/design/war-room/golden/03-approaching-the-wall.ans new file mode 100644 index 0000000..0b7c07f --- /dev/null +++ b/design/war-room/golden/03-approaching-the-wall.ans @@ -0,0 +1,45 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONDUIT │ WALL ██████▎░ 78% │ TOKENS ██████▏░ 76% │ QUOTA 5h ██████▉░ 86% │ WATCHDOG █▉░░░░░░ 02:56 │ 2 working 0 waiting 1 held 7 done 1 scrap │ T+1:52:20 │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ decompose test implement review probe deliver │ +│ card 0 15m 30m 45m 1h 1h15 1h30 T+1:52:20 2h BUDGET 2:24:00│ state │ +│ ┌─ PRD-014 range-queries ████▋·fan-out ×11·····································································│ │ awaiting │ +│ ├─ WI-201 range-schema ▋██▋███████▎█▎█▏ │ │ ✓ 25:36 │ +│ ├─ WI-202 cli-flags ███▍██████▎█▌██▋█▎ │ │ ✓ 29:04 │ +│ ├─ WI-203 date-parse ▍███▏██████▉██▉███▊██ │ │ ✓ 34:09 │ +│ ├─ WI-204 parse-range ▍██▍█████▌█▉█████▎█▌██████████████▊██▏██▉███ │ │ ✓ 1:04:39 │ +│ ├─ WI-205 index-scan ····▏█████████████▊██▊██████████▌██▋ │ │ ✓ 53:52 │ +│ ├─ WI-206 query-planner ·····▊███▌██████████████▏████████▎████████████████▏███▊██▊ │ │ ✓ 1:22:45 │ +│ ├─ WI-207 legacy-shim ·▏██▏██████████████ SCRAP · no_progress │ │  SCRAP  │ +│ ├─ WI-208 docs-ranges ·▉████████████▉███████▌·····▋███▌ │ │ ✓ 49:24 │ +│>├─ WI-209 migration-0042 ·▉███▊███████▍█▋█▎human decision███▋▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│ │ OVERDUE +59:20 │ +│ ├─ WI-210 perf-bench ·······▎███▏···················▊████████████████▋██▋████▎«█████████▊████▍██▓███▓█▌ │ 11:20 rw1/3 │ +│ ├─ WI-211 e2e-suite ·································▎███▉█████████████████████▎██▍«██████████████▌▓█▌ │ 03:20 rw1/3 │ +│ └─ fan-in · quorum 10 ───────────────▌──▌───▌──────────▌──▌────────▌────────────▌───────────────────────│ │ 7/11 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ WI-209 migration-0042 │ HELD human decision · approve migration 0042 │ held since 29:00 · timeout 53:00 · on_timeout not recorded │ OVERDUE +59:20 │ +│  LIVE  1:51:58 WI-210 attempt #9 probe · Bash npm run bench -- --ranges j/k select enter open drawer t tool ticks r replay esc close │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/design/war-room/golden/03-approaching-the-wall.txt b/design/war-room/golden/03-approaching-the-wall.txt new file mode 100644 index 0000000..7e5ed37 --- /dev/null +++ b/design/war-room/golden/03-approaching-the-wall.txt @@ -0,0 +1,45 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONDUIT │ WALL ██████▎░ 78% │ TOKENS ██████▏░ 76% │ QUOTA 5h ██████▉░ 86% │ WATCHDOG █▉░░░░░░ 02:56 │ 2 working 0 waiting 1 held 7 done 1 scrap │ T+1:52:20 │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ decompose test implement review probe deliver │ +│ card 0 15m 30m 45m 1h 1h15 1h30 T+1:52:20 2h BUDGET 2:24:00│ state │ +│ ┌─ PRD-014 range-queries ████▋·fan-out ×11·····································································│ │ awaiting │ +│ ├─ WI-201 range-schema ▋██▋███████▎█▎█▏ │ │ ✓ 25:36 │ +│ ├─ WI-202 cli-flags ███▍██████▎█▌██▋█▎ │ │ ✓ 29:04 │ +│ ├─ WI-203 date-parse ▍███▏██████▉██▉███▊██ │ │ ✓ 34:09 │ +│ ├─ WI-204 parse-range ▍██▍█████▌█▉█████▎█▌██████████████▊██▏██▉███ │ │ ✓ 1:04:39 │ +│ ├─ WI-205 index-scan ····▏█████████████▊██▊██████████▌██▋ │ │ ✓ 53:52 │ +│ ├─ WI-206 query-planner ·····▊███▌██████████████▏████████▎████████████████▏███▊██▊ │ │ ✓ 1:22:45 │ +│ ├─ WI-207 legacy-shim ·▏██▏██████████████ SCRAP · no_progress │ │ SCRAP │ +│ ├─ WI-208 docs-ranges ·▉████████████▉███████▌·····▋███▌ │ │ ✓ 49:24 │ +│>├─ WI-209 migration-0042 ·▉███▊███████▍█▋█▎human decision███▋▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│ │ OVERDUE +59:20 │ +│ ├─ WI-210 perf-bench ·······▎███▏···················▊████████████████▋██▋████▎«█████████▊████▍██▓███▓█▌ │ 11:20 rw1/3 │ +│ ├─ WI-211 e2e-suite ·································▎███▉█████████████████████▎██▍«██████████████▌▓█▌ │ 03:20 rw1/3 │ +│ └─ fan-in · quorum 10 ───────────────▌──▌───▌──────────▌──▌────────▌────────────▌───────────────────────│ │ 7/11 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ WI-209 migration-0042 │ HELD human decision · approve migration 0042 │ held since 29:00 · timeout 53:00 · on_timeout not recorded │ OVERDUE +59:20 │ +│ LIVE 1:51:58 WI-210 attempt #9 probe · Bash npm run bench -- --ranges j/k select enter open drawer t tool ticks r replay esc close │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/design/war-room/tokens.json b/design/war-room/tokens.json new file mode 100644 index 0000000..d731fd5 --- /dev/null +++ b/design/war-room/tokens.json @@ -0,0 +1,56 @@ +{ + "STATIONS · live / past / done": { + "decompose live": "#E8967A", + "decompose past": "#744B3D", + "decompose done": "#462D25", + "test live": "#EC4F8E", + "test past": "#762847", + "test done": "#47182B", + "implement live": "#9D6CF5", + "implement past": "#4F367B", + "implement done": "#2F204A", + "review live": "#4C7BF4", + "review past": "#263E7A", + "review done": "#172549", + "probe live": "#00BFFF", + "probe past": "#006080", + "probe done": "#00394D", + "deliver live": "#2DD4BF", + "deliver past": "#176A60", + "deliver done": "#0E4039" + }, + "STATES": { + "working · now-line · LIVE": "#3BE37A", + "held fill (text #000000)": "#F5B940", + "overdue, blink A: fill": "#FF6B6B", + "overdue hatch ▒": "#FF6B6B", + "scrap fill · budget line": "#FF6B6B", + "rework marker «": "#FF6B6B", + "done text": "#5B6475", + "waiting dots ·": "#3A4150", + "not recorded fg": "#94A3B8", + "not recorded bg": "#1A1E27", + "replay playhead": "#00BFFF", + "meter loud fill (≥80%)": "#F5B940", + "meter fill █": "#7B8599", + "meter empty ░": "#2F3542" + }, + "TEXT & CHROME": { + "bold / primary text": "#E2E8F0", + "normal text": "#94A3B8", + "dim text": "#5B6475", + "borders · tree · faint": "#2F3542", + "background": "#000000", + "selected row bg": "#0F1018", + "status bar bg": "#141824", + "CONDUIT wordmark": "#00BFFF" + }, + "TOOL TICKS · bg / fg": { + "Read": "#343B4A / #E2E8F0", + "Grep": "#566078 / #E2E8F0", + "Edit": "#E2E8F0 / #000000", + "Bash, exit 0": "#8A94A7 / #000000", + "nonzero exit": "#FF6B6B / #000000", + "running (blinks to dim)": "#000000 / #3BE37A" + } +} diff --git a/design/war-room/tui-reference.js b/design/war-room/tui-reference.js new file mode 100644 index 0000000..fdde76d --- /dev/null +++ b/design/war-room/tui-reference.js @@ -0,0 +1,237 @@ +// War Room TUI cell-grid reference, exported from the Claude Design "War Room TUI" page (2026-09-28). +// Design reference only: this is the mock scenario and the drawing rules the design used, one glyph + +// one foreground + one background + bold flag per cell on a 160x45 grid. The real TUI renders from the +// journal projection with @opentui/react; use this for layout, colors, glyph choices, and golden frames. + +const W=160,H=45,CPX=7.2; +const BG='#000000',FG='#E2E8F0',MU='#94A3B8',DI='#5B6475',BD='#2F3542',DOT='#3A4150',SEL='#0F1018',BAR='#141824',NRB='#1A1E27'; +const G='#3BE37A',AM='#F5B940',CO='#FF6B6B',CY='#00BFFF',TRK='#7B8599',B=144,THR=12; +const ST=[['decompose','#E8967A','#744B3D','#462D25'],['test','#EC4F8E','#762847','#47182B'],['implement','#9D6CF5','#4F367B','#2F204A'],['review','#4C7BF4','#263E7A','#172549'],['probe','#00BFFF','#006080','#00394D'],['deliver','#2DD4BF','#176A60','#0E4039']]; +const TOOL={Read:['#343B4A',FG],Grep:['#566078',FG],Edit:[FG,'#000000'],Bash:['#8A94A7','#000000']}; +const LB=['','▏','▎','▍','▌','▋','▊','▉']; +const pad=n=>String(n).padStart(2,'0'); +const clk=m=>{const s=Math.round(m*60),h=Math.floor(s/3600),mm=Math.floor(s%3600/60),ss=s%60;return h?`${h}:${pad(mm)}:${pad(ss)}`:`${pad(mm)}:${pad(ss)}`;}; +const adj=m=>Number.isInteger(m)?m+((m*37+11)%60)/60:m; +const ev=m=>clk(adj(m)); +const PE=(s,n)=>s.length>=n?s:s+' '.repeat(n-s.length); +const CARDS=[ + {id:'PRD-014',t:'range-queries',v:[[0,0,6],[5,127,134]],done:134}, + {id:'WI-201',t:'range-schema',v:[[1,6,10],[2,10,17],[3,17,20],[4,20,22.6],[5,22.6,25]],done:25}, + {id:'WI-202',t:'cli-flags',v:[[1,6.5,11],[2,11,20],[3,20,22.8],[4,22.8,27],[5,27,29]],done:29}, + {id:'WI-203',t:'date-parse',v:[[1,7,12],[2,12,22],[3,22,26],[4,26,31],[5,31,34]],done:34}, + {id:'WI-204',t:'parse-range',v:[[1,7,11],[2,11,19],[3,19,22,'R'],[2,22,29],[3,29,32,'R'],[2,32,52],[3,52,55],[4,55,60],[5,60,64]],done:64}, + {id:'WI-205',t:'index-scan',v:[[1,12,17],[2,17,31],[3,31,35],[4,35,49],[5,49,53]],done:53}, + {id:'WI-206',t:'query-planner',v:[[1,14,19],[2,19,38],[3,38,50,'R'],[2,50,68],[3,68,72],[4,72,78],[5,78,82]],done:82}, + {id:'WI-207',t:'legacy-shim',v:[[1,8,12],[2,12,30]],scrap:30,reason:'no_progress'}, + {id:'WI-208',t:'docs-ranges',v:[[1,9,13],[2,13,26],[3,26,30],[4,30,36],[5,44,49]],done:49}, + {id:'WI-209',t:'migration-0042',v:[[1,9,14],[2,14,24],[3,24,27],[4,27,29],[5,121,126]],held:[29,121,53],hr:'human decision · approve migration 0042',hs:'human decision',done:126}, + {id:'WI-210',t:'perf-bench',v:[[1,16,21],[2,48,70],[3,70,74],[4,74,80,'R'],[2,80,95],[3,95,101],[4,101,118],[5,118,122]],done:122}, + {id:'WI-211',t:'e2e-suite',v:[[1,50,56],[2,56,84],[3,84,88,'R'],[2,88,109],[3,109,113],[4,113,120],[5,120,125]],done:125}, +]; +const CALLS=[['Read','src/range.ts'],['Read','test/range.test.ts'],['Grep','"parseRange" src/'],['Read','src/cli/args.ts'],['Edit','src/range.ts'],['Bash','npm test -- range',1],['Read','src/range.ts'],['Grep','"Number.isNaN" src/'],['Edit','src/range.ts'],['Bash','npm test -- range',0],['Read','test/fixtures/ranges.json'],['Edit','test/range.test.ts'],['Bash','npm test -- range',1],['Read','test/range.test.ts'],['Edit','test/range.test.ts'],['Bash','npm test -- range',0],['Grep','"parseRange(" src/'],['Read','src/cli/args.ts'],['Edit','src/cli/args.ts'],['Bash','npx tsc --noEmit',2],['Read','src/cli/args.ts'],['Edit','src/cli/args.ts'],['Bash','npx tsc --noEmit',null]]; +const DUR=[3,2,1,4,6,41,2,1,5,38,2,7,44,3,5,39,1,2,6,12,2,4,null]; +const dur=s=>Math.floor(s/60)+':'+pad(s%60); +function stateOf(c,t){ + if(c.scrap!=null&&c.scrap<=t)return{s:'scrap'}; + if(c.done!=null&&c.done<=t)return{s:'done'}; + if(c.held&&c.held[0]<=t&&c.held[1]>t)return{s:'held'}; + const cur=c.v.find(v=>v[1]<=t&&v[2]>t);if(cur)return{s:'work',cur}; + return{s:'wait'}; +} +class Grid{ + constructor(){this.c=[];for(let y=0;ythis.put(x+i,y,ch,fg,bg,b==null?0:b));return x+a.length;} + bgr(x0,x1,y,bg){for(let x=x0;x<=x1;x++){const c=this.get(x,y);if(c)c.bg=bg;}} + segs(x,y,list){list.forEach(p=>{x=this.t(x,y,p.s,p.fg||MU,p.bg,p.b);});return x;} + out(){return this.c.map(r=>{const runs=[];let cur=null;r.forEach(c=>{if(cur&&cur.fg===c.fg&&cur.bg===c.bg&&cur.b===c.b){cur.s+=c.ch;cur.n++;}else{cur={s:c.ch,fg:c.fg,bg:c.bg,b:c.b,n:1};runs.push(cur);}});return{runs:runs.map(u=>({s:u.s,fg:u.fg,bgc:u.bg,fw:u.b?700:400,w:(u.n*CPX).toFixed(1)+'px'}))};});} +} +const S=(s,fg,b,bg)=>({s,fg,b:b?1:0,bg}); +function bar(g,y,x0,e0,e1,col,base,chf){ + if(e1-e0<2)e1=e0+2; + const c0=Math.floor(e0/8),c1=Math.ceil(e1/8); + for(let c=c0;c0)g.put(x,y,cell.ch,cell.fg,col);else g.put(x,y,LB[a],base,col);} + else g.put(x,y,LB[Math.max(1,b-a)],col,base); + } + return [c0,c1]; +} +function meter(g,x,y,label,p,val){ + const loud=p>=80,len=1+label.length+1+8+1+val.length+1; + if(loud)g.bgr(x,x+len-1,y,AM); + const bg=loud?AM:BG,lf=loud?'#000000':DI,ff=loud?'#000000':TRK,ef=loud?'#000000':DOT,vf=loud?'#000000':FG; + g.t(x+1,y,label,lf,bg,loud?1:0); + const bx=x+2+label.length,n=Math.min(64,Math.round(p/100*64)); + for(let i=0;i<8;i++){const k=Math.max(0,Math.min(8,n-i*8));g.put(bx+i,y,k===8?'█':k===0?'░':LB[k],k===0?ef:ff,bg,0);} + g.t(bx+9,y,val,vf,bg,loud?1:0); + return x+len; +} +function wrap(parts,width,indent){ + const words=[];parts.forEach(p=>p.s.split(/(\s+)/).filter(Boolean).forEach(w=>words.push({...p,s:w}))); + const lines=[];let cur=[S(' '.repeat(indent))],len=indent; + words.forEach(w=>{if(len+w.s.length>width&&len>indent){lines.push(cur);cur=[S(' '.repeat(indent))];len=indent;if(/^\s+$/.test(w.s))return;}cur.push(w);len+=w.s.length;}); + lines.push(cur);return lines; +} +const FRAMES=[ + {n:'01',title:'Live, mid-mission',caption:'160×45 cell grid overlaid for alignment · j/k selection on WI-204',t:47,now:47,tok:[2.48,62],q:[41,18],sel:'WI-204',grid:true, + ev:['46:12','WI-205','attempt #5 probe · Bash npm run probe:fuzz -- index']}, + {n:'02',title:'Card journal: WI-204, tool tick #06 selected',caption:'drawer fixed at 60 columns · body scrolls, indicator in the right column',t:47,now:47,tok:[2.48,62],q:[41,18],sel:'WI-204',drawer:true,tool:5, + ev:['46:12','WI-205','attempt #5 probe · Bash npm run probe:fuzz -- index']}, + {n:'03',title:'Approaching the wall',caption:'budget column at 2:24:00 · 5h quota 86% · WI-209 OVERDUE selected',t:112+1/3,now:112+1/3,tok:[3.05,76],q:[86,27],sel:'WI-209', + ev:['1:51:58','WI-210','attempt #9 probe · Bash npm run bench -- --ranges']}, +]; +function build(cfg,ph){ + const g=new Grid(),{t,now}=cfg,dr=!!cfg.drawer,blink=Math.floor(ph/2)%2===0; + for(let x=1;xg.put(x,y,'─',BD)); + for(let y=1;y{g.put(0,y,'├',BD);g.put(W-1,y,'┤',BD);}); + let x=g.t(2,1,'CONDUIT',CY,null,1);x+=1;g.put(x,1,'│',BD);x+=1; + let lastLane=0;CARDS.forEach(c=>{c.v.forEach(v=>{const a=adj(v[1]);if(a<=t)lastLane=Math.max(lastLane,a);});[c.scrap,c.done,c.held&&c.held[0]].forEach(z=>{if(z!=null&&z!==false&&adj(z)<=t)lastLane=Math.max(lastLane,adj(z));});}); + const qk=cfg.q[0]>=cfg.q[1]?0:1,since=now-lastLane; + [['WALL',Math.round(now/B*100),Math.round(now/B*100)+'%'],['TOKENS',cfg.tok[1],cfg.tok[1]+'%'],['QUOTA '+(qk?'7d':'5h'),cfg.q[qk],cfg.q[qk]+'%'],['WATCHDOG',Math.round(since/THR*100),clk(since)]].forEach(m=>{x=meter(g,x,1,m[0],m[1],m[2]);g.put(x,1,'│',BD);x+=1;}); + const kids=CARDS.slice(1),tally={work:0,wait:0,held:0,done:0,scrap:0};kids.forEach(c=>tally[stateOf(c,t).s]++); + x+=1;[['working',tally.work,G],['waiting',tally.wait,MU],['held',tally.held,AM],['done',tally.done,MU],['scrap',tally.scrap,CO]].forEach(([k,v,c])=>{x=g.t(x,1,String(v),v?c:DI,null,1);x=g.t(x+1,1,k,DI);x+=1;}); + const el='T+'+clk(now);g.put(W-4-el.length,1,'│',BD);g.t(W-2-el.length,1,el,FG,null,1); + const CX0=dr?24:30,CX1=dr?86:142,CW=CX1-CX0,SX=dr?87:144,MR=dr?98:158,TMAX=CX0-2-13; + const nowX=now/B>=0.7?Math.min(CW-2,Math.round(now*(CW-2)/B)):CW-4,s=nowX/now,E=m=>Math.round(m*s*8),bcol=Math.floor(B*s),showB=bcol{lx=g.t(lx,3,n,c,null,1)+2;}); + const AY=4; + const nl=el;g.t(CX0+nowX-nl.length+1,AY,nl,G,null,1); + if(showB){const bl='BUDGET 2:24:00';g.t(CX0+bcol-bl.length,AY,bl,CO,null,1);g.put(CX0+bcol,AY,'│',CO,null,1);} + g.t(2,AY,'card',DI);g.t(SX,AY,'state',DI); + const step=[5,10,15,20,30,60].find(st=>st*s>=10); + for(let m=0;m*sc.done!=null&&c.done<=t).map(c=>c.done).sort((a,b)=>a-b); + rowsDef.forEach((c,i)=>{ + const y=5+i,sel=cfg.sel===c.id,base=sel?SEL:BG; + if(sel){g.bgr(1,MR,y,SEL);g.put(1,y,'>',FG,SEL,1);} + if(c.join){ + g.t(2,y,'└─',BD,base);g.t(5,y,'fan-in',MU,base);g.t(12,y,'· quorum 10',DI,base); + for(let cc=Math.floor(E(6)/8);ccg.put(CX0+Math.floor(E(d)/8),y,'▌',ST[5][1],base)); + g.t(SX,y,`${arrivals.length}/11`,MU,base); + g.put(CX0+nowX,y,'│',G,base,1);if(showB)g.put(CX0+bcol,y,'│',CO,base,1); + return; + } + const isP=i===0,st0=stateOf(c,t),st=isP&&st0.s==='wait'?{s:'awaiting_children'}:st0,done=st.s==='done'||st.s==='scrap'; + g.t(2,y,isP?'┌─':'├─',BD,base); + const idc=st.s==='work'?G:st.s==='held'?AM:st.s==='scrap'?CO:st.s==='done'?DI:MU; + g.t(5,y,c.id,sel&&st.s==='done'?FG:idc,base,st.s==='done'?0:1); + const title=c.t.length>TMAX?c.t.slice(0,TMAX-1)+'…':c.t;g.t(13,y,title,st.s==='done'?BD:DI,base); + let prev=isP?0:6,rw=0,lastR=false; + const dots=(a,b)=>{for(let cc=Math.ceil(E(a)/8);ccv[1]{ + const [si,a,b0]=v,b=Math.min(b0,t),act=st.s==='work'&&st.cur===v; + if(!(c.held&&c.held[0]>=prev-0.01&&c.held[0](cc+ph)%4===0?'▓':'█'):null); + if(lastR){rw++;if(!done)g.put(CX0+Math.ceil(E(a)/8),y,'«',CO,base,1);} + lastR=v[3]==='R'&&b0<=t;prev=b; + }); + if(isP){dots(6,t);g.t(CX0+Math.ceil(E(6)/8)+1,y,'fan-out ×11',DI,base);} + else if(st.s==='wait')dots(prev,t); + if(c.held&&c.held[0]dl){bar(g,y,CX0,E(dl),E(h1),CO,base,()=>'▒');} + } + if(st.s==='scrap'){const cx=CX0+Math.ceil(E(c.scrap)/8);g.put(cx,y,'█',CO,base);g.t(cx+2,y,'SCRAP · '+c.reason,CO,base,1);} + const nc=g.get(CX0+nowX,y);if(nc&&(nc.ch===' '||nc.ch==='·'))g.put(CX0+nowX,y,st.s==='work'?'▌':'│',G,base,1); + if(showB)g.put(CX0+bcol,y,'│',CO,base,1); + if(st.s==='work')g.t(SX,y,clk(t-st.cur[1])+(rw?` rw${rw}/3`:''),G,base,1); + else if(st.s==='held'){const dl=c.held[2]; + if(nowS(PE(ST[i][0],10),ST[i][1]); + const att=(ts,n,si,cost,tok,calls)=>[S(ev(ts)+' ',DI),S(PE('#'+n,8),FG,1),sc(si),S(PE(cost,7),FG),S(PE(tok+' tok',11),MU),S(calls+' calls',MU)]; + const rej=(ts,n)=>[S(ev(ts)+' ',DI),S('REJECT ',CO,1),S('review',ST[3][1]),S(' → ',DI),S('implement',ST[2][1]),S(` rw ${n}/3`,CO,1)]; + const fnd=(loc,txt)=>wrap([S(loc,FG),S(' '+txt,MU)],56,14); + const L=[]; + L.push([S(ev(6)+' ',DI),S('fan-out ',DI),S('PRD-014',MU),S(' → ',DI),S('test',ST[1][1])]); + L.push(att(11,1,1,'$0.42','38.1k',12),att(19,2,2,'$2.10','212.0k',41),att(22,3,3,'$0.61','64.3k',18),rej(22,1)); + L.push(...fnd('src/range.ts:88','missing null check on open-ended range'),...fnd('test/range.test.ts','no case for empty input')); + L.push(att(29,4,2,'$1.64','171.0k',36),att(32,5,3,'$0.55','58.0k',15),rej(32,2)); + L.push(...fnd('src/range.ts:88','attempt #4 reverted the fix from attempt #2'),...fnd('test/range.test.ts:41','empty-input case asserts the wrong error')); + L.push(att(32,6,2,'$0.88','96.4k',23),[S(' '.repeat(14)),S('running '+clk(t-32),G,1)],[]); + L.push([S('tool calls · #6',DI)]); + const sel=cfg.tool; + L.push(CALLS.flatMap(([tool,,x0],i)=>{const x=x0===undefined?0:x0,run=x0===null;const bg=run?BG:x?CO:TOOL[tool][0],fg=run?(blink?G:DI):x?'#000000':TOOL[tool][1];return[S(tool[0],fg,1,bg),S(' ')];})); + L.push([S(' '.repeat(sel*2)),S('^',FG,1)]); + L.push([S('R',FG,1,TOOL.Read[0]),S(' Read ',MU),S('G',FG,1,TOOL.Grep[0]),S(' Grep ',MU),S('E','#000000',1,TOOL.Edit[0]),S(' Edit ',MU),S('B','#000000',1,TOOL.Bash[0]),S(' exit 0 ',MU),S('B','#000000',1,CO),S(' nonzero ',MU),S('B',G,1),S(' running',MU)]); + L.push([]); + const row=(i,hl)=>{const [tool,path,x0]=CALLS[i],x=x0===undefined?0:x0,run=x0===null,bg=hl?BAR:undefined;const ex=run?'running':tool==='Bash'?'exit '+x:'';const tc=x?CO:tool==='Edit'?FG:MU; + return[S('#'+pad(i+1)+' ',DI,0,bg),S(PE(tool,6),tc,hl||x?1:0,bg),S(PE(path,30),x?CO:FG,0,bg),S(ex.padStart(8),run?G:x?CO:DI,x?1:0,bg),S(' '+PE(run?'…':dur(DUR[i]),6),MU,0,bg),S(' ',MU,0,bg)];}; + L.push(row(sel,true),[]); + CALLS.forEach((_,i)=>L.push(row(i,i===sel))); + const top=7,vis=34; + L.slice(0,vis).forEach((ln,i)=>g.segs(101,top+i,ln)); + const th=Math.max(1,Math.round(vis*vis/L.length)); + for(let i=0;i({...p,bg:BAR}))); + let fx=g.t(2,43,' LIVE ','#000000',G,1)+2; + fx=g.t(fx,43,cfg.ev[0],DI)+2;fx=g.t(fx,43,cfg.ev[1],FG,null,1)+2;g.t(fx,43,cfg.ev[2],MU); + const hints=[['j/k','select'],['enter','open drawer'],['t','tool ticks'],['r','replay'],['esc','close']]; + let hl=hints.reduce((a,[k,v])=>a+k.length+1+v.length+2,0)-2,hx=W-2-hl; + hints.forEach(([k,v])=>{hx=g.t(hx,43,k,FG,null,1);hx=g.t(hx+1,43,v,DI)+2;}); + return {n:cfg.n,label:cfg.n+' '+cfg.title,title:cfg.title,caption:cfg.caption,grid:!!cfg.grid,rows:g.out()}; +} +const TOK=[ + {name:'STATIONS · live / past / done',items:ST.flatMap(([k,a,p,d])=>[{k:k+' live',hex:a,a,b:a},{k:k+' past',hex:p,a:p,b:p},{k:k+' done',hex:d,a:d,b:d}])}, + {name:'STATES',items:[ + {k:'working · now-line · LIVE',hex:G,a:G,b:BG,g:'▌',bf:G}, + {k:'held fill (text #000000)',hex:AM,a:AM,b:AM}, + {k:'overdue, blink A: fill',hex:CO,a:CO,b:BG,g:'O',bf:CO}, + {k:'overdue hatch ▒',hex:CO,a:BG,b:BG,g:'▒',bf:CO}, + {k:'scrap fill · budget line',hex:CO,a:CO,b:CO}, + {k:'rework marker «',hex:CO,a:BG,b:BG,g:'«',bf:CO}, + {k:'done text',hex:DI,a:DI,b:DI}, + {k:'waiting dots ·',hex:DOT,a:BG,b:BG,g:'·',bf:DOT}, + {k:'not recorded fg',hex:MU,a:MU,b:MU}, + {k:'not recorded bg',hex:NRB,a:NRB,b:NRB}, + {k:'replay playhead',hex:CY,a:CY,b:CY}, + {k:'meter loud fill (≥80%)',hex:AM,a:AM,b:AM}, + {k:'meter fill █',hex:TRK,a:TRK,b:TRK}, + {k:'meter empty ░',hex:BD,a:BG,b:BG,g:'░',bf:BD}]}, + {name:'TEXT & CHROME',items:[ + {k:'bold / primary text',hex:FG,a:FG,b:FG}, + {k:'normal text',hex:MU,a:MU,b:MU}, + {k:'dim text',hex:DI,a:DI,b:DI}, + {k:'borders · tree · faint',hex:BD,a:BD,b:BD}, + {k:'background',hex:BG,a:BG,b:BG}, + {k:'selected row bg',hex:SEL,a:SEL,b:SEL}, + {k:'status bar bg',hex:BAR,a:BAR,b:BAR}, + {k:'CONDUIT wordmark',hex:CY,a:CY,b:CY}]}, + {name:'TOOL TICKS · bg / fg',items:[ + {k:'Read',hex:'#343B4A / '+FG,a:'#343B4A',b:'#343B4A',g:'R',bf:FG}, + {k:'Grep',hex:'#566078 / '+FG,a:'#566078',b:'#566078',g:'G',bf:FG}, + {k:'Edit',hex:FG+' / #000000',a:FG,b:FG,g:'E',bf:'#000'}, + {k:'Bash, exit 0',hex:'#8A94A7 / #000000',a:'#8A94A7',b:'#8A94A7',g:'B',bf:'#000'}, + {k:'nonzero exit',hex:CO+' / #000000',a:CO,b:CO,g:'B',bf:'#000'}, + {k:'running (blinks to dim)',hex:BG+' / '+G,a:BG,b:BG,g:'B',bf:G}]}, +]; +TOK.forEach(g=>g.items.forEach(it=>{it.g=it.g||'';it.bf=it.bf||'#000';})); + +export { W, H, ST, TOOL, CARDS, CALLS, DUR, FRAMES, TOK, Grid, build }; diff --git a/prd/drafts/war-room.md b/prd/drafts/war-room.md index 91b46bf..eeb7f79 100644 --- a/prd/drafts/war-room.md +++ b/prd/drafts/war-room.md @@ -4,7 +4,7 @@ missionId: ~ # Conduit — War Room -**Author:** Josh Owens **Date:** 2026-06-12 **Status:** Draft +**Author:** Josh Owens **Date:** 2026-06-12 **Revised:** 2026-09-28 (TUI first, #89) **Status:** Draft > Scope note: the War Room is named in the SPEC's lean glossary — *"Genba (the floor) → > the War Room — live stream of the journal"* — but has never been specified. This PRD @@ -133,7 +133,12 @@ projection; its whole value is that what you see *is* the journal. - **Multi-run / fleet dashboard** — comparing runs, run history lists across flows. Single run first; fleet view is a later PRD informed by this one. - **Authentication, multi-user hosting, remote access** — this renders local state for - the operator who owns the volume. Hosting is a product decision for another day. + the operator who owns the volume. The TUI reaches a remote box over SSH or + `docker exec -it`, so it opens no port and needs no auth decision. Hosting is a + product decision for another day. +- **The web view in the first delivery**: it follows the TUI as its own PRD and reuses + the shared core (§8). +- **A run picker**: v1 is one journal, one run. - **Kaizen analytics** — chronic-edge mining, cost trends, mutation proposals. The War Room *feeds* the schema for that work; it does not do it. - **Flow editing / run control** (pause, scrap, retry buttons) — write paths, all of @@ -170,6 +175,11 @@ projection; its whole value is that what you see *is* the journal. 12. Replay mode shall reconstruct a run from its journal alone, support play/pause/step/ scrub, and clearly label itself as replay (including runs that ended in a crash — the view shows last recorded state, not an invented terminal). +14. The kernel shall write a run-level heartbeat entry to the journal at a fixed interval + while a run is active, so the War Room can tell a crashed kernel (heartbeat stopped) + from a slow one (heartbeat current, no card activity). The heartbeat is the first + schema-gap item and is a kernel write; the War Room only reads it. The interval is + chosen at implementation. 13. Where the journal lacks a datum the UI is designed to show, the War Room shall render an explicit "not recorded" marker — never a guess or a blank — and shall log the gap (datum, journal location, run) to a schema-gap list. @@ -187,13 +197,16 @@ projection; its whole value is that what you see *is* the journal. displays what the journal stores and adds no new exposure surface (no raw env, no request bodies beyond what ingress already persisted). 6. The full lane graph — parent path, any child band, and the terminal rail — shall be - visible without horizontal scrolling at 1280px width for flows of up to 8 stations. + visible without horizontal scrolling at 160 terminal columns (TUI) or 1280px width + (web) for flows of up to 8 stations. ### Edge Cases & Error States - **Run crashed mid-flight**: journal ends without terminals. Render last known state - with an explicit staleness indicator ("no journal activity for N min; kernel not - observed") — do not infer completion or failure. + with an explicit staleness indicator ("no journal activity for N min") — do not infer + completion or failure. With the heartbeat (FR-14) the indicator says whether the + kernel is still beating. For a journal with no heartbeat entries it says "kernel not + observed". - **Kernel resumes a watched run**: checkpoint-skipped stations produce no new work events. The view must make skip-on-resume legible (a station satisfied by checkpoint, not re-executed) rather than appearing frozen — if the journal cannot distinguish @@ -236,27 +249,39 @@ projection; its whole value is that what you see *is* the journal. ## 8. Solution Approach -The War Room is a **local web view served by the kernel binary itself** — a -`conduit watch` style command alongside the existing CLI, not a separate service. It -ships in the same Docker image, reads the same mounted volume, and adds no new runtime -or infrastructure. - -The data model is **snapshot plus live tail**, built around a single shared projection -function — *the* War Room, in code form: +**Delivery order (revised 2026-09-28, #89).** The first surface is a terminal UI: +`conduit watch` renders in the terminal with `@opentui/react` and reads the journal +directly, with no server. The web view described below comes second and reuses the same +reader, projection function, and replay logic. It adds the rendering components and the +server and SSE layer. The TUI visual spec is `design/war-room/`: frames, 160x45 golden +text and ANSI frames, color tokens, and a cell-grid reference renderer. -- One pure function folds journal events into the rendered run state - (`events → reduce → view state`). It is the only place view state is derived. -- **Snapshot**: on load (and on any reconnect), the client fetches the current run state, - computed server-side by running that same function over the journal. -- **Live**: the watch process tails the journal and streams new events to the browser; - each event folds into the displayed state through the same function. -- **Replay**: the client fetches a raw event range and scrubs through it by re-folding - locally — same function, no streaming involved. - -Because snapshot, live, and replay are all the one function, the §7 principles (pure -projection, replay ≡ live, restart-identical rendering) are structural properties rather -than conventions. Reconnect gaps self-heal: a dropped connection triggers a fresh -snapshot fetch rather than requiring missed-event bookkeeping. +`conduit watch` is a command in the existing kernel binary, not a separate service. It +ships in the same Docker image, reads the same mounted volume, and adds no new runtime +or infrastructure. The web view, when it comes, is served by the same binary under a +distinct invocation, `conduit watch --web`, so that plain `conduit watch` never opens a +port. + +The data model is a **shared core**, built once around a single projection function, +*the* War Room in code form: + +- **Reader**: a read-only journal and state DB reader that loads the current state and + follows new rows by sequence number. +- **Projection**: one pure function folds journal events into the rendered run state + (`events → reduce → view state`). It is the only place view state is derived. The June + prototype's sketch at `design/warroom-prototype/lib/warroom-data.ts` is the starting + point, with its types reconciled against `src/types/kernel.ts`. +- **Live**: the reader follows the journal and each new row folds into the displayed + state through the projection. +- **Replay**: events up to a chosen point, re-folded through the same function. +- **Not recorded**: a gap in the journal renders as "not recorded" and is logged to the + schema-gap list (FR-13). + +Because live and replay are the one function, the §7 principles (pure projection, +replay ≡ live, restart-identical rendering) are structural properties rather than +conventions. The TUI has no server, so it has no snapshot or reconnect protocol: a +restart re-reads the journal and re-folds it. The web view adds a snapshot fetch and a +reconnect path on top of the same core. The primary view is a **trace waterfall**, validated through two prototype rounds (a kanban-style station board was prototyped first and failed the legibility test — even @@ -273,8 +298,9 @@ the system's author could not narrate it; the paradigm record is §10): compress, never scroll — with a minimum segment width so nothing vanishes. The wall-clock budget renders as a red boundary line that enters the viewport once the run passes ~70% of budget: approaching the wall is geometry, not a number. Detail - recovery is drag-to-zoom with a transient minimap (full run + viewport) and a - "fit all" snap-back; zoom chrome is invisible unless zoomed. + recovery is zoom on the time axis (drag in the web view) with a transient minimap + (full run + viewport) and a "fit all" snap-back; zoom chrome is invisible unless + zoomed. - **Live is the default; replay is a mode.** The footer is a slim strip — latest-event ticker plus a LIVE badge. Entering replay reveals the scrubber and flips the badge; the two are never visible together, and **every** header element (meters, elapsed, @@ -283,7 +309,11 @@ the system's author could not narrate it; the paradigm record is §10): - **Terminal aesthetic.** Monospace throughout, TUI-bordered panels, pure-black ground, saturated color slabs (engineers are the audience; the product is CLI-first). Both andons live in the header as bracketed meters; card drill-down is a - side panel showing the card's journal events as colored log lines. + side panel showing the card's journal events as colored log lines. In the TUI it is a + 60-column scrolling drawer. There is no hover: the selected row's details go in a + status line. Keys: j/k select, enter opens the drawer, t toggles tool-call ticks for + the current harness attempt, r enters replay, esc closes. Mouse clicks work on rows + and ticks. - The station board from the first prototype round survives as a candidate *secondary* view for high-WIP flows (a future PRD), not part of v1. @@ -291,14 +321,20 @@ the system's author could not narrate it; the paradigm record is §10): **Constraints:** -- **Bun-only toolchain** (per ADR-0002/0003): the UI is a static React SPA bundled by +- **TUI** renders with `@opentui/react` (Bun-native), pinned to an exact version while + it is pre-1.0. Tests use `@opentui/react/test-utils`: `captureCharFrame` for text + snapshots against `design/war-room/golden/`, `captureSpans` for colors, mock keys and + mouse for interaction, and `ManualClock` for countdowns and the watchdog. +- **Bun-only toolchain** (per ADR-0002/0003): the TUI is TypeScript run by Bun, with no + `Bun.serve` endpoints, no SSE, and no listening port. The web UI is a static React SPA bundled by Bun's native bundler and served by `Bun.serve()` from the kernel binary. No Node, Vite, or Next.js toolchain; no separate deployable. This decision (one binary, no second service) should be recorded as an ADR at implementation time. -- **Transport is one-directional SSE.** Chosen over WebSocket deliberately: the browser +- **Web view only: transport is one-directional SSE.** Chosen over WebSocket deliberately: the browser has no channel to send commands back, which makes the no-write-path rule (§7) an architectural property instead of a convention. The only client-initiated calls are - read-only snapshot/range fetches. + read-only snapshot/range fetches. The TUI has no transport and listens on nothing, so + the no-write-path rule holds there for the same reason. - **Visual language is terminal/TUI** (s-tui / btop energy): pure black `#000` ground, monospace everywhere, saturated color slabs. Color encodes **station identity** on waterfall segments (a healthy card walks the spectrum left→right; a broken hue order @@ -308,8 +344,8 @@ the system's author could not narrate it; the paradigm record is §10): **Dependencies:** -- **[reactiveSWR](https://github.com/queso/reactiveSWR)** (our own library) is the - SSE-to-client bridge: its server channel handles wire format, heartbeats, connection +- **[reactiveSWR](https://github.com/queso/reactiveSWR)** (our own library), for the web + view only. The TUI does not use it. It is the SSE-to-client bridge: its server channel handles wire format, heartbeats, connection tracking, and cleanup inside `Bun.serve`; its EventEmitter adapter bridges the journal tailer; SWR's revalidate-on-reconnect provides the snapshot self-healing in §8; its connection-status hook drives the LIVE/stale indicator. **Shape constraint:** the @@ -322,11 +358,12 @@ the system's author could not narrate it; the paradigm record is §10): **Integration points:** -- The existing CLI (new `watch` command), the Docker image (one exposed port), and the +- The existing CLI (new `watch` command), the Docker image (one exposed port for the web + view, none for the TUI), and the journal schema (every gap found per FR-13 feeds the kaizen-pipe PRD). -- The connection-status / kernel-liveness distinction: SSE heartbeats tell the browser - the *watch process* is alive; only a journal-level signal can say the *kernel* is - alive (see the heartbeat open question below). +- The connection-status / kernel-liveness distinction: in the web view, SSE heartbeats + tell the browser the *watch process* is alive. In either surface, only a journal-level + heartbeat (FR-14) can say the *kernel* is alive. ## 10. Risks & Open Questions @@ -336,29 +373,36 @@ the system's author could not narrate it; the paradigm record is §10): | Journal gaps are larger than expected — the UI is mostly "not recorded" | Medium | War Room ships hollow; kaizen schema work balloons | That discovery is a stated goal (§4); timebox the first pass, fix the top gaps in the journal, then finish the UI against the improved schema | | Read contention with a live kernel on one SQLite volume | Low | Lag or kernel slowdown | Kernel-writes-take-priority NFR; the journal/state split and WAL already exist for this reason — validate, don't redesign | | Scope creep toward run control (pause/retry/select buttons) | High | Write path sneaks in; second source of truth follows | The stop rule (§7) plus explicit out-of-scope listing; any control surface is its own PRD | -| Replay diverges from live rendering | Low | Trust in the projection collapses | Same-code-path principle (§7); single shared projection function (§8); replay-fidelity metric (§4) | -| Journal event bursts race the client cache update (a tick can append several rows back-to-back; folding must apply N sequential reducer steps, not lose updates) | Medium | Board renders a state the journal never contained | Add an interleaving/burst test to reactiveSWR before adoption; snapshot refetch is the recovery path either way | +| Replay diverges from live rendering | Low | Trust in the projection collapses | Same-code-path principle (§7); single shared projection function (§8); replay-fidelity metric (§4); core test that replay equals live | +| Journal event bursts fold incorrectly (a tick can append several rows back-to-back; the reader must hand the projection every row in sequence order and the projection must apply N sequential steps, not lose updates) | Medium | The screen shows a state the journal never contained | Core test: a burst of rows written in one tick folds to the same view as the same rows folded one at a time. Re-reading from sequence 0 is the recovery path. The web view adds the same test for its reactiveSWR cache before adoption | ### Open Questions -- [ ] Run identity: today one state DB ≈ one run lineage. Does the War Room need a - run-selection affordance now, or does single-journal = single-run hold until the - fleet-view PRD? -- [ ] Should replay be exportable (a shareable recording of a run) for launch material, - or is screen-capture enough for now? -- [ ] Does the journal need a run-level "kernel heartbeat" entry so the staleness - indicator (crashed vs. slow) can be precise? Candidate first schema-gap item — - SSE heartbeats only prove the watch process is alive, not the kernel (§9). +None open. Three were settled on 2026-09-29 (see Resolved). ### Resolved -- [x] **Delivery surface** → local web view (React SPA), not a TUI. It is the demo - artifact; the projection contract would have been identical either way (§8, §9). +- [x] **Run identity** (2026-09-29) → the simple choice: one journal is one run, with no + run picker in v1. Several flows may run at once; picking among them waits for the + fleet-view PRD. +- [x] **Replay export** (2026-09-29) → screen capture is enough. A vhs recording of a + fixture run is the launch demo. No export format. +- [x] **Kernel heartbeat** (2026-09-29) → yes. The kernel writes a run-level heartbeat + entry to the journal (FR-14). It is the first schema-gap item. +- [x] **Delivery surface** → TUI first, web view second (revised 2026-09-28, #89). This + was first resolved as web only, because the web view is the demo artifact. It was + reversed because flows run on a remote box, where a TUI works over SSH with no port + or auth decision; the TUI needs no server layer; and its tests assert on rendered + text. A vhs recording of a fixture run serves as the demo. The projection contract + is identical for both (§8, §9). - [x] **Primary paradigm** → trace waterfall, not a station board. The board prototype failed the narration test (the author could not read it); the waterfall passed immediately. The journal is a trace; render it as one. Board demoted to a possible future secondary view for high-WIP flows (§8). - [x] **Where the process lives** → a `conduit watch` command in the existing kernel - binary, serving the SPA and the event stream itself. No separate service (§9). -- [x] **Transport** → one-directional SSE via reactiveSWR, single-cache-key reducer - shape; replay re-folds locally without SSE (§9). + binary. No separate service (§9). Plain `conduit watch` is the TUI and serves nothing. + The web view is `conduit watch --web`, which also serves the SPA and the event + stream. +- [x] **Transport** (web view only, 2026-09-29) → one-directional SSE via reactiveSWR, + single-cache-key reducer shape; replay re-folds locally without SSE (§9). The TUI + reads the journal in-process and has no transport.