Self-contained resume doc: context, validated spike facts (S1/S2/S3/ T1/T3/T6), the critical gotchas (trust-vs-bypass dialog distinction, submission key-token recipe, dual-signal-no-quiescence completion, buffered-not-streaming), PI231 playbook (prod :4567 untouched), the remaining PRs (PR-1 reader → PR-2 driver → PR-3 wiring), working method (hybrid workflow + maintainer-supervised PI231), code anchors, conventions (jaekwon-park co-author, default-path-unchanged sacred), and first move. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
TUI-mode implementation — session handoff (resume at PR-1)
Written: 2026-05-30. Goal: a tested, working A-path TUI-mode before 2026-06-15 (the Anthropic billing split), so that even if 6/15 blocks the proxy, there is working code + data to adjust from.
0. What TUI-mode is (one paragraph)
OLP/OCP are OpenAI-compatible HTTP proxies that spawn the claude CLI. Anthropic's 2026-06-15 billing split routes traffic by the cc_entrypoint field the CLI stamps on every request: cli (genuine interactive terminal/IDE) → subscription pool; sdk-cli (-p, --output-format, any non-TTY invocation) → the separate Agent SDK credit pool (Pro plans get only ~$20 of credit there → useless). TUI-mode drives claude in a real interactive tmux session (no -p, no --output-format) so it stamps cc_entrypoint=cli, and reads the response from claude's native JSONL session transcript (not a hook file). This keeps the proxy usable for Pro subscribers post-6/15. It is OPT-IN (CLAUDE_TUI_MODE), default stays stream-json. It is an honest grey-area bridge (genuinely interactive, but automated) — see spec §10.
1. READ THESE FIRST (in order)
All on branch feat/tui-mode-pr0-isolation-seed (which is stacked on docs/tui-mode-design-spec, so it contains the spec + plan + PR-0):
docs/superpowers/specs/2026-05-30-tui-mode-production-design.md— the design spec (final; 3 independent reviews + 6 spikes folded in). Authoritative. Do not re-litigate its decisions.docs/superpowers/plans/2026-05-30-tui-mode-A-path-implementation-plan.md— the implementation plan (PR-0..PR-3) + the "Maintainer decisions" appendix at the bottom (resolves P1–P5 + open questions). Read the appendix carefully.- The committed PR-0 code (this branch's HEAD
1fd27e1):lib/sandbox/manager.mjs(thetuiparam +_seedTuiClaudeJson),test-features.mjsSuite 45,docs/adr/0002-plugin-architecture.md§ tuiSeed extension note.
2. WHERE WE ARE
✅ PR-0 TUI-only ISOLATION .claude.json seed (committed 1fd27e1, pushed, 816 tests pass)
⬜ PR-1 transcript reader lib/tui/transcript.mjs ← START HERE
⬜ PR-2 session driver lib/tui/session.mjs (needs PI231 integration testing)
⬜ PR-3 provider wiring CLAUDE_TUI_MODE branch + ADR 0016 + README
then: OCP single-tenant canary (Deployment A) post-2026-06-15 to measure REAL billing
Deployment B (family/team multi-tenant) is DEFERRED behind spikes T2 (body-capture proof the wire /v1/messages carries tools:[]) + T4 (concurrency) — these are verification, not open research (T6 already found the disable mechanism). Do NOT build B in this pass.
3. VALIDATED FACTS (do NOT re-spike — already proven on PI231, claude v2.1.158)
- S1 (PARTIAL):
--system-promptkeepscc_entrypoint=cli. BUT account-attached managed MCP servers (the owner's Gmail/Calendar/Drive) auto-attach over the network even with empty local config — the "no-tool" property is model restraint, not enforcement. (Gates B; mitigated by T6.) - S2 (PASS): read the native JSONL transcript at
<EHOME>/.claude/projects/<CWD_ENCODED>/<SESSION_ID>.jsonlwhereCWD_ENCODED= the spawn cwd with every/→-(including the leading slash), andSESSION_ID= the UUID you pass via--session-id(so YOU compute the path before spawn). The file is created lazily on first message — poll for it. Assistant text extracts escaping-clean (oneJSON.parseper line). Completion marker = a{"type":"system","subtype":"turn_duration"}line (last line of the turn by timestamp; carries messageCount + durationMs). - S3 (PASS): submission is 100% reliable IF Enter is a tmux key token, not a literal
\nin text (Ink bug anthropics/claude-code#15553). - T1 (PARTIAL):
turn_durationfires forend_turntext turns AND refusals, but is ABSENT on tool-use turns (which would hang a marker-only reader). - T3 (PASS): multiline/special-char prompts submit byte-for-byte via the file→send-keys recipe (§ below).
- T6 (PASS):
--strict-mcp-config(with NO--mcp-config) gives 0 managed-MCP attachment. Editing/seeding.claude.jsondoes NOT disable MCP (account/server-driven — negative control). Defense-in-depth: envCLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL=1+--disallowedTools "mcp__*"+ (for B)--tools "". Do NOT use--bare(breaks OAuth).
Billing caveat (honesty): S1 proved the cc_entrypoint=cli SIGNAL, NOT the actual billed pool — the split hasn't happened. The OCP canary post-6/15 is the first real billing measurement.
4. CRITICAL GOTCHAS (things that already bit us — a fresh session WILL miss these)
- TWO different startup dialogs — do not conflate them:
- bypass-permissions dialog ("you accept all responsibility… 1.No 2.Yes") → suppressed by
bypassPermissionsModeAccepted:truein the seeded.claude.json(PR-0 does this). Its default cursor is "1.No,exit" — a naive Enter EXITS. - trust-folder dialog ("Is this a project you trust? 1.Yes 2.No") → NOT suppressed by the seed. Still appears for a fresh ephemeral
$HOME(projects stripped). PR-2's session driver MUST answer it by sending "1". (A review-fix agent wrongly claimed bypassPermissionsModeAccepted suppresses the trust dialog — it does NOT. This was caught + corrected; do not reintroduce that claim.)
- bypass-permissions dialog ("you accept all responsibility… 1.No 2.Yes") → suppressed by
- Submission recipe (exact): write the prompt body to a FILE (never interpolate into a shell command line —
$()/backticks/;get mangled by the shell). Then:tmux send-keys -t <S> -- "$(cat promptfile)"(the leading--end-of-options guard is required), sleep ~1.5–2s to let Ink settle, then submit with a SEPARATE key token:tmux send-keys -t <S> Enter. Embedded\nin the body are delivered as soft line-breaks and do NOT submit. Verify submission via capture-pane (spinner / "esc to interrupt" / response bullet●); retry Enter (key token) up to ~4× as a defensive guard. - Completion = DUAL SIGNAL (no quiescence in v1): terminal when EITHER a
turn_durationline appears (happy path) ORstop_reason:"tool_use"is seen OR a wall-clock cap fires → return a clean error, never hang. Do NOT use "file size-stable for N seconds" as a v1 terminal signal — a long Opus extended-thinking turn legitimately stalls transcript growth and quiescence would falsely abort it. Quiescence is a T5 follow-up only. Read all assistanttextblocks since the matchinguserline; do NOT rely on file-tail byte ordering (write-order can differ from timestamp-order). Wall-clock cap = envCLAUDE_TUI_WALLCLOCK_MS(default 120000), config not constant. - No
--dangerously-skip-permissions— we read the transcript, no tool writes, so it's not needed (this is the whole point vs PR #101's hook approach). No--bare(breaks OAuth). - Streaming = BUFFERED. The transcript is read AFTER the turn completes, so there is NO true token-streaming. For
stream:true, replay the completed response as SSE chunks (one burst after the turn). The maintainer accepted this; document it in README. Do NOT try to parse the live terminal pane for token-streaming — that's the fragile path the maintainer explicitly rejected ("不做易坏的功能"). - max_tokens + sampling params (temperature/top_p/stop/etc.) — interactive claude has NO flags for these; they're parsed into IR (
lib/ir/openai-to-ir.mjs:182) and dropped at the CLI boundary today already. Graceful-drop + document; no regression.
5. PI231 (the test machine) — playbook
- SSH:
tlab@172.16.2.231(RPi4 arm64 Debian).claudev2.1.158 at~/.npm-global/bin/claude(export PATH="$HOME/.npm-global/bin:$PATH"). tmux 3.3a. uuidgen / python3 available. - ⚠️ PI231 runs PROD OLP on :4567 — DO NOT TOUCH IT. All TUI testing is
/tmpscratch + tmux sessions with unique names. Clean up: kill tmux sessions,rm -rfscratch dirs,shred -uany--debug-file(debug logs contain the OAuth bearer — nevercata full debug log; only grep specific tokens). - Ephemeral session recipe (what PR-0's seed enables): make an ephemeral
$HOME, symlink~/.claude/.credentials.jsoninto it, seed.claude.json(PR-0's_seedTuiClaudeJsondoes this: strip projects, set hasCompletedOnboarding + bypassPermissionsModeAccepted), runclaude --model <m> --session-id <uuid> [--system-prompt …] [--strict-mcp-config …]in a tmux session with cwd = ephemeral home. Answer the trust dialog with "1". cc_entrypoint capture: add--debug api --debug-file <path>, grepcc_entrypoint=, shred. - The maintainer (you, with the human) handles PI231 integration testing directly — subagents are unreliable at the delicate tmux/interactive timing (it bit us; the human's accumulated context catches subagent errors).
6. THE REMAINING PRs (concrete)
Use the plan (docs/superpowers/plans/...) PR sections + Maintainer-decisions appendix as the source of truth. Summary:
PR-1 — lib/tui/transcript.mjs (transcript reader) — START HERE
- Pure module, no existing-path touch. Exports a reader that, given
{ ephemeralRoot, sessionId, cwd }, computes the transcript path (§ CWD_ENCODED formula above), polls for lazy file creation, detects completion via the dual-signal guard (turn_duration OR tool_use OR wall-clock cap fromCLAUDE_TUI_WALLCLOCK_MS), extracts assistanttextblocks since the matchinguserline, and returns a resolved string (the adapter to IR[{delta},{stop}]chunks lives in PR-3's provider branch, not here — keep PR-1 transport-agnostic). - Needs real JSONL fixtures captured from PI231, especially a
tool-use-no-marker.jsonl(a turn withstop_reason:"tool_use"and NOturn_duration) + anout-of-order.jsonl(text block flushed after turn_duration by byte). The maintainer captures these on PI231. Unit-test the reader against the fixtures; assert the non-hang throw is distinguishable from the timeout.
PR-2 — lib/tui/session.mjs (session driver) — needs PI231 integration testing
- tmux transport behind a transport interface (so node-pty can slot later — but ship tmux only; node-pty stubbed).
runTuiTurn(isolationCtx, irRequest, opts)— takesisolationCtx(hasephemeralRoot+reqIdfrom PR-0), NOT raw keyId/reqId. - Spawn flags:
--session-id <uuid>+--system-prompt <OLP wrapper>+ (T6)--strict-mcp-config+--disallowedTools "mcp__*"+ envCLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL=1. (A also uses --strict-mcp-config per decision P3.) - Answer the trust-folder dialog (send "1") — the seed already handles bypass. (See gotcha #1.)
- Submit via the file→
send-keys -- "$(cat file)"→separate-Enter recipe (gotcha #2). - Call the PR-1 reader for the response; map to single-buffered + SSE-replay for stream:true.
- trap-guaranteed cleanup (
finally, not best-effort) + an orphan-tmux-session reaper on server startup wired into theisMainboot block atserver.mjs:2417(decision P2) — tmux sessions survive restart and hold the owner OAuth. - Preflight
/mcp-empty verification: a SEPARATE preflight session at startup (not in a serving turn — it would corrupt the reader's matching-user-line). For A: advisory (log warn). For B: hard gate (decision P3).
PR-3 — provider wiring
CLAUDE_TUI_MODEbranch inlib/providers/anthropic.mjsspawn()(~line 1164 →_spawnAndStream); default falls through to the untouched stream-json path. The string→IR-chunks adapter ([{type:'delta',role:'assistant',content},{type:'stop',finish_reason:'stop'}]) lives HERE — it fits BOTH cache consumers:getOrCompute(array,server.mjs:1445, buffered pathcollectAllChunks:1299) andgetOrComputeStreaming(:1587,sourceWithRelease:1558) with ZERO server edits.- New ADR 0016 (OLP) as authority of record (interactive-mode billing lane; supersedes ADR 0009 Amd 1's billing premise). Acknowledge PR #101 + jaekwon-park.
- README: new env var + Troubleshooting (onboarding/OAuth-login requirement, NO token-streaming = buffered, max_tokens/sampling not honored) + honest grey-area framing.
7. WORKING METHOD + CONVENTIONS
- Hybrid: run a small workflow per PR —
implement (sonnet) → review (opus, fresh-context) → fix (sonnet) → npm test must pass. THEN the maintainer commits + supervises PI231 integration. The maintainer's PI231 context catches subagent errors (it already caught a fabricated trust-dialog claim). - Branch stacking: PR-1 branches off
feat/tui-mode-pr0-isolation-seed(which has spec+plan+PR-0). Stack PR-2 off PR-1, etc. OR merge PR-0 to main first then branch — maintainer's call. No OLP TUI PRs are opened on GitHub yet (branches pushed only). - Co-author on every implementing commit:
Co-Authored-By: jaekwon-park <insainty21@gmail.com>(work traces to OCP PR #101) +Co-Authored-By: Claude <model> <noreply@anthropic.com>. - ALIGNMENT discipline: provider-plugin changes cite claude CLI version (v2.1.158) + spec/ADR. Independent reviewer per Iron Rule 10. Minimum reviewable unit per Iron Rule 11.
- Default-path-unchanged is sacred:
CLAUDE_TUI_MODEdefault off; stream-json path byte-for-byte unchanged until explicitly enabled. Reviewer must verify this for each PR. - Don't break
npm test(currently 816 pass). The__OLP_FORCE_ISOLATION_IN_TESTglobal is the seam to exercise real ISOLATION in tests (see Suite 43f / 45).
8. KEY CODE ANCHORS (verified)
lib/sandbox/manager.mjs:prepareIsolatedEnvironment(now hastui/tuiSeedSourceparams +reqIdin return +_seedTuiClaudeJsonhelper).lib/providers/anthropic.mjs:spawn:1164 →_spawnAndStream;ISOLATION:1667 (has tui doc comment);buildCliArgs:834 (passes only --model + --system-prompt today);extractSystemPrompt:123;irToAnthropic:601;OLP_SYSTEM_PROMPT_WRAPPER.server.mjs: spawn sites:1347(bufferedcollectAllChunks:1299→getOrCompute:1445) +:1564(streamingsourceWithRelease:1558→getOrComputeStreaming:1587);irChunkToOpenAISSE;isMainboot block:2417(reaper hook target).lib/ir/openai-to-ir.mjs:182(max_tokens parsed, dropped at CLI).lib/keys.mjs:validateKey:414;owner_tier ∈ {owner, guest}(:143), 'anonymous' is a runtime fallback identity (:439).
9. SUGGESTED FIRST MOVE in the new session
git checkout feat/tui-mode-pr0-isolation-seed && git pull(or confirm you're on it).- Read this handoff + the spec + the plan's Maintainer-decisions appendix.
- Capture the PR-1 JSONL fixtures on PI231 (esp.
tool-use-no-marker.jsonl) — drive a tool-inviting prompt under--system-promptand a normal prompt; save the transcripts as test fixtures. - Branch
feat/tui-mode-pr1-transcript-readeroff PR-0; run the PR-1 implement→review→fix workflow against the fixtures.
Deadline: working A-path + PI231 integration test before 2026-06-15. ~16 days.