2026-10-25 05:02:18 +01:00
# shell: processes, boxes, live output, speech
Scope: `src/shell/` (namespace `detz::shell` ). Source of truth is `../tai.py` ; this file states what the C++ port
does today, how it is verified, and the gaps. Wiring for other modules: [wiring/shell.md ](wiring/shell.md ).
## Command Output Rule (repository AGENTS.md)
Every command an agent runs for the user streams live and is returned to the agent. The window is a fixed-height
message (`LiveStream` , or the chat TUI's `live` block through the `StreamSink` protocol). A command that stops on a
prompt (sudo, ssh, y/n) runs on a pty and asks through `ask(text, secret)` ; the answer is typed into the command, a
decline (`nullopt` ) sends end-of-file, and time spent waiting for the user never counts against the timeout.
Anything that runs a process goes through `RunLive` with `ShellIo::Stream` and `ShellIo::Ask` . No bare
`subprocess` -style calls for agent commands.
## Caps (constants, all from tai.py)
| constant | value | where |
|---|---|---|
| `kShellTimeout` (SHELL_TIMEOUT) | 120 s | runlive.h |
| `kRunOutputCap` (RUN_OUTPUT_CAP) | 4,000,000 chars, pty path only | runlive.h |
| `kRunPromptIdle` (RUN_PROMPT_IDLE) | 1.0 s of silence before a prompt is asked | runlive.h |
| `kRunMaxAsks` (RUN_MAX_ASKS) | 20 questions per command | runlive.h |
| `kRunAnswerCap` (RUN_ANSWER_CAP) | 1000 chars typed per answer | runlive.h |
| `kRunRows` x `kRunCols` | 24 x 120 pty window | pty.h |
| `kStreamHeight` (STREAM_HEIGHT) | 4 lines | livestream.h |
| `kShellSnapshotMaxFiles` | 25 files | targets.h |
| ShellSession caps | SID_MAX 64, CHAIN_CAP 64, ROWS_CAP 1000, TREE_DEPTH 8, TREE_KIDS 10, TREE_LINES 40 | session.h |
## Parity table (tai.py symbol -> C++)
| tai.py | C++ | notes |
|---|---|---|
2026-10-28 13:11:45 +01:00
| `run_live` , `run_live_pty` | `RunLive::Run` , `RunLive::RunPty` | pipe path with reader thread, pty path with prompt detection; `RunInterrupted` on Ctrl-C when `allow_interrupt` (foreground only), child group killed |
| `KeyboardInterrupt` out of `run_live` | `RunInterrupted` (runlive.h) | converted to `TurnInterrupted` by `AgentReplHost::run_turn` , so the REPL prints `interrupted` and checkpoints |
2026-10-25 05:02:18 +01:00
| `RUN_CTRL_RE` , `RUN_PROMPT_RE` | `RunLive::StripControl` , `RunLive::LooksLikePrompt` | hand-written scanners, same alternation |
| `PtyProc` | `PtyProc` | posix_openpt, TIOCSCTTY, TIOCSWINSZ, poll reader, echo probe |
| `subprocess.Popen/run` , `shutil.which` | `Popen` , `Subprocess::Run` , `Subprocess::Which` (proc.h) | errno reporting through a CLOEXEC pipe, Python `OSError` /`TimeoutExpired` texts |
| `LiveStream` , `stream_status` , `human_size` | `LiveStream` , `StreamStatus` , `HumanSize` | |
| `Spinner` | `Spinner` | stderr tty only, 80 ms frames, shimmer + clock when color is on |
| `LaneBoard` | `LaneBoard` | `list_agents` injected as `AgentLister` |
| `Glow` (every static method) | `Glow` (glow.h) | `describe_call` over `JsonValue` , `agent_row` /`lanes` over `AgentRow` |
| `Sandbox` , `container_engine` , `box_state` , `update_box_caps` , `ensure_box` , `box_exec` , `box_python` , `box_transcribe` , `box_speak` , `box_opencode_binary` , `box_opencode_run` | `Sandbox` (sandbox.h) | `BoxOpencodeExec` is the exec half; the caller runs `opencode_parse_events` |
| `BOX_CONTAINERFILE` , `BOX_STT` , `BOX_TTS` , `BOX_OPENCODE_INSTALL_SCRIPT` | `BoxData` (boxdata.cpp, generated from tai.py) | byte-identical, checked by the box parity case |
| `curl_hint` , `CURL_HINT` | `CurlHint` , `kCurlHint` | |
| `ShellSession` (current, register, tree_lines, report, ...) | `ShellSession` (session.h) | `SessionDb` interface replaces `store.db` ; pid/boot/chain logic identical |
| `shell_target_paths` , `segment_targets` | `ShellTargets::Paths` , `ShellTargets::Segment` | shlex, glob and os.walk ported (`Shlex` , `PyGlob` ) |
| `Tools.shell_stream` , `Tools.shell_ask` | `ShellIo::Stream` , `ShellIo::Ask` over `ShellApp` | |
| `edge_token` , `edge_timestamp` , `edge_ssml` , `edge_chunks` , `ws_send` , `ws_recv_exact` , `ws_recv` , `edge_skew` , `edge_connect` , `edge_synthesize` | `EdgeSpeech` (edge.h) | TLS bytes via libcurl connect-only, `EdgeSocket` seam for tests |
| `play_audio` , `listen_audio` , `RECORDERS` , `TRANSCRIBERS` | `Audio` (audio.h) | |
Old invented API (`Sandbox::run(allow-list)` , `ShellSession::exec` , `PtyProc::spawn/read_all` , `Spinner::tick/done` ,
`LiveStream::push/drain/joined` , `LaneBoard::set/render` , `Glow::highlight` , `speak_text` ) is gone; tai.py has none of
it. Replacements are listed in wiring/shell.md.
## How it is verified
- `tests/shell_parity.py` builds `tests/shell_parity_driver.cpp` and runs every case through the driver and through
the same function of `../tai.py` , comparing bytes. Real processes, a real pty (including the real `su` password
prompt), real files, a scripted fake `podman` , a fake `/proc` , a fake websocket server. Fuzzed inputs for markdown,
wrap, truncate, diff, json. `python3 tests/shell_parity.py [--fuzz N] [--filter NAME] [--live] [-v]` .
- `tests/shell_test.cpp` : unit tests, build line in its header.
- Both build with `-Wall -Wextra` and must be warning-free; the parity run fails on any compiler warning.
## Gaps and deviations (stated, not hidden)
- Unicode classes: `\w` , `\d` , `str.isalnum` , `str.lower` use hand-written ranges (exact for ASCII, Latin-1, Greek,
Cyrillic basics; approximate beyond). Affects markdown italics boundaries, number tokens in `highlight_python` ,
`complete_files` case folding for exotic scripts.
- The pipe path of `run_live` keeps all output in memory (no cap), exactly like tai.py; only the pty path is capped.
- After a timeout only the shell is killed (`Popen.kill` ), as in tai.py. A grandchild that keeps the pipe open makes
`run_live` wait up to 10 s for the reader and leaves the grandchild running.
- `Spinner` and `LaneBoard` are time based; the parity run compares the first frames, the final clear and the frame
count within 2, not every frame.
- Edge speech: wire handling is verified offline against tai.py with a scripted server. The live service answered
`403 Forbidden` to tai.py and to the port alike when last tried (2026-10-25, this machine), so end-to-end audio
was not verified.
- `Sandbox::EnsureBox` image build and the opencode install were exercised only against a scripted fake engine, not
a real multi-minute podman build.
2026-10-28 13:11:45 +01:00
- `ShellIo::Ask` REPL fallback: Ctrl-C/EOF during the question maps to a decline through the shared fd-0 reader,
like tai.py maps `KeyboardInterrupt` /`EOFError` (decline sends end-of-file to the command).
2026-10-25 05:02:18 +01:00
- `BackendError` here is `detz::shell::BackendError` ; the agent core converts it at the call site.