← back to Answer Cockpit
README.md
67 lines
# Answer Cockpit — one needs-Steve card at a time, one button to continue
Aggregates every needs-Steve iTerm session (dots 🔵 lightblue · 🟠 orange · 🟣 purple · 🟡 yellow)
into one urgency-ordered stream, shows **one card at a time** tinted to its dot, and one primary
button pushes the answer/continue **into that terminal's live pane** (osascript `write text` +
a second Enter) and advances. Ticket **TK-11793**. Cost **$0 (local)**.
## Run
```
bash start.sh # → http://127.0.0.1:9805 (admin / DW2024!)
bash start.sh --stop
```
Binds `127.0.0.1` only (hard-coded — it types into live agents). `start.sh` unsets
`CLAUDE_CODE_CHILD_SESSION`/`CLAUDE_PID` so the server never trips the colordots bridge rail.
Optional launchd: `com.steve.answer-cockpit.plist` (install is a Steve paste; never passes `--test`).
## Endpoints (Basic auth; every mutation is POST; body ≤ 16KB)
| Route | Does |
|---|---|
| `GET /api/queue?orphans=1` | scan (4s cache) → needs-Steve items, detail pre-resolved, memos merged by TK |
| `GET /api/item/:tty` | one item, fresh scan (post-answer confirm) |
| `POST /api/answer {tty,text,expectKey,force?,repaint?,dry?}` | type + submit into the pane |
| `POST /api/continue {tty,expectKey}` | types `continue` (orange, after Steve ran the paste himself) |
| `POST /api/focus {tty}` | select window/tab/session + activate |
| `POST /api/memo/decide {file,decision,note,tty?}` | approve→`_approved/` · block→`_rejected/` · revise→note; `_decisions.jsonl` + reversible ledger; types the decision into the linked pane |
| `POST /api/memo/undo {file,decision}` | mirror of approvals-viewer `:9795` undo |
| `GET /api/health` | heartbeat (PASS/WARN/FAIL) + last 20 audit lines |
## Rails (never loosened)
- Never executes shell from a card or memo — Copy is the only verb for `! ` pastes / ```` ```ungate-run ````.
- tty must match `^ttys\d{3}$` **and** be in a fresh live scan; refuses the cockpit's own tty;
refuses green/pink unless `force:true`; refuses codex REPLs unless `force`.
- `expectKey` (`tty|ticket|color`) must match the current card → 409 on a stale card.
- Text is an escaped AppleScript string literal fed on **stdin** (never `-e` concat); `\r`/control
chars rejected; `\n` space-joined. Single-flight `mkdir` lock + 1.2s pacing (no double-type).
- Every write-back **and every refusal** is audited to `data/audit.jsonl`. Unmeasured is never
green: an unavailable terminal API → `stale:true`, POSTs 503, heartbeat WARN; iTerm down → FAIL.
## Scale (51 live sessions, 45 panes — 2026-09-16)
The expensive reads never run on the request path: a **background async pane watcher** refreshes
the batched `contents of session` read every 12s (request paths read the cache; the POST guard
does ONE blocking fresh read right before typing), and `build()` is **stale-while-revalidate** —
one build at a time, concurrent polls get the last result instantly with `building:true`.
Measured: health 0.14s *during* a build (was starved past 60s), concurrent queue 0.025s.
## Detail sources (where the card text comes from)
1. `~/.claude/answer-cockpit/map/<tty>.json` (a future hook can make everything `exact`).
2. `~/.claude/sessions/<pid>.json` → sessionId → transcript (`exact`).
3. `lsof` cwd → project dir → newest transcript, validated (`likely` / `ambiguous`).
4. **Pane contents** (`confidence:"pane"`) — transcript saving is OFF for ~half the fleet
("inherited CLAUDE_CODE_CHILD_SESSION marker"), so the iTerm pane's own screen is read via
osascript `contents of session`; the prompt-box chrome is cut, a queued input is surfaced,
and an on-screen AskUserQuestion menu (`❯ 1. …`) is parsed into option buttons. A 1-char-per-
line scrollback (narrow-width collapse) is flagged, not shown as if real.
## Verified 2026-09-15
negative tests 401/400/403/409 + LAN refused · write-back typed **and executed** on a throwaway
plain-shell pane · live queue 6 == 6 vs `allcolordots` · headless-Chrome render · parser unit test
(positive / negative / degenerate) · **end-to-end on a real AskUserQuestion**: a throwaway
session asked Alpha/Bravo/Charlie, the cockpit ingested it (un-dotted, via pane detection), typed
the option's displayed NUMBER through `/api/answer`, and the session printed `ANSWER_RECEIVED=Bravo`.
**Hard-won fact:** a real menu selects by its **displayed number**, not by typed label — typing
"Bravo" + Enter fired the cursor default (Alpha). Options carry `n`; the client types `String(n)`
and sends `optionLabel` for the audit. Cody (contrarian) red-teamed the build 4–1 HOLD; all three
holes (prose-list false menus, superseded-menu buttons, digest-less expectKey) are fixed + tested.