From 4886131dabe88b6774a5c38015d2c146f65f13f1 Mon Sep 17 00:00:00 2001 From: Ned Date: Thu, 1 Oct 2026 19:09:04 -0700 Subject: [PATCH 1/2] Audit terminal and activity specs; fix C1 recovery, launch state and push preview --- docs/compatible-agents.md | 15 ++-------- docs/compatible-agents.rationale.md | 2 +- docs/specs/alert.md | 33 +++++---------------- docs/specs/security-local.md | 2 +- docs/specs/terminal-context.md | 2 +- docs/specs/terminal-escapes.md | 14 +++++---- docs/specs/terminal-escapes.rationale.md | 2 +- docs/specs/terminal-state.md | 6 ++-- lib/src/lib/resume-patterns.test.ts | 9 ++++++ lib/src/lib/terminal-controls.test.ts | 24 +++++++++++++++ lib/src/lib/terminal-controls.ts | 16 +++++----- lib/src/lib/terminal-lifecycle.ts | 6 +++- lib/src/lib/terminal-protocol.ts | 6 ++-- lib/src/lib/terminal-registry.alert.test.ts | 25 ++++++++++++++++ lib/src/lib/terminal-state.ts | 20 ++++++++++--- lib/src/remote/burrow/push-delivery.test.ts | 26 +++++++++++++++- lib/src/remote/burrow/push-delivery.ts | 22 +++++++------- scripts/spec-word-budgets.json | 6 ++-- 18 files changed, 159 insertions(+), 77 deletions(-) diff --git a/docs/compatible-agents.md b/docs/compatible-agents.md index b10e8f555..fd17dbdd0 100644 --- a/docs/compatible-agents.md +++ b/docs/compatible-agents.md @@ -45,16 +45,7 @@ An agent integration normally needs one registry entry, an exit fixture, and a r ### Add the definition and fixture -1. Edit [the coding agent registry](../lib/src/lib/coding-agents.ts). For example, Copilot's definition is: - - ```ts - { - name: 'GitHub Copilot', - commands: ['copilot'], - resume: '--resume', - watchByDefault: true, - } - ``` +1. Add an entry following `CodingAgent` in [the coding agent registry](../lib/src/lib/coding-agents.ts). 2. Declare the executable names the agent actually installs and the resume option or subcommand it supports. The shared parser handles space/equals separators, terminal escapes, and command reconstruction. IDs must fit its alphanumeric, hyphen, and underscore grammar. If an agent cannot identify the exact conversation on exit, discuss its capture mechanism in an issue first. Do not substitute a “latest conversation” command. 3. Add a sanitized exit excerpt to [the fixtures](../lib/src/lib/__fixtures__/coding-agents.ts), with the expected rebuilt command, agent version, and operating system. Replace personal paths, account information, and session IDs; preserve relevant wording and terminal escapes. Record real exit output rather than reconstructing a hint from documentation. @@ -90,7 +81,7 @@ Open a **draft pull request** with the entry, fixture, documentation, and verifi - **Must capture before killing the target PTYs**, within the host's bounded teardown. Each host owns the target scope; interrupt every live PTY within it, regardless of recognized command, and exclude exited PTYs. (rationale) - **Must write one `^C` into each target PTY per interrupt call, never signal it.** The shared machine alone decides the second press. Host interrupts must settle within their timeout. (rationale) - **Never finish early on quiet or replace the retry gates with a blanket second press.** Poll until every target yields or the capture budget expires; retry timing and ask detection live in the module's comments. (rationale) -- **Must scan only bytes received after the mark taken before the first interrupt.** Never widen the scan into earlier output; buffer eviction may discard fresh bytes but must not promote stale bytes into the scan. (rationale) +- **Must scan only output received after the mark taken before the first interrupt.** Never widen the scan into earlier output; buffer eviction may discard fresh output but must not promote stale output into the scan. (rationale) - **Must report each detected command immediately**, retaining earlier detections if a later target times out. Source of truth: `captureAgentRecovery` / `RecoveryHost` in `lib/src/host/recovery-capture.ts`; pinned by `lib/src/host/recovery-capture.test.ts`. @@ -102,7 +93,7 @@ Source of truth: `captureAgentRecovery` / `RecoveryHost` in `lib/src/host/recove - **Must rebuild only a known invocation plus an opaque id.** The command is *rebuilt* as label, space, captured id, never sliced from the buffer, keeping the hint's executable alias; a long option's id may follow a space or `=`, and only Claude's legacy `claude --continue` omits it. The id must begin with an ASCII alphanumeric and contain only ASCII alphanumerics, hyphens, and underscores. The invocation must end on a word break but nothing stronger (rationale). - **Must observe a separator after the newest invocation before capturing it.** Buffer end is insufficient, even at the capture deadline; never fall back to an older hint while the newest is unterminated. Pinned by `waits through every ID split` in `lib/src/host/recovery-capture.test.ts` (rationale). - **Must strip the scan window as a whole, in one pass, with an unterminated control swallowing the rest of it** — the string controls (OSC, DCS, SOS, PM, APC) **in either introducer form, `ESC` or bare C1**, and equally a CSI the window was cut off *inside* (rationale). **Must match every escape by its full ECMA-48 shape**, never by the Fe range (rationale). **Must share one implementation**: `stripTerminalControls` removes string controls by running `TerminalControlStreamFilter`, so the batch and streaming readers cannot disagree. -- **Must strip in boundary mode**: *every complete* control becomes a newline rather than vanishing, except SGR and charset designators, the two classes that neither move the cursor nor erase. **Must discard incomplete trailing presentation controls without creating a boundary** (rationale). +- **Must strip in boundary mode**: complete non-string ESC/CSI sequences and standalone C1 controls become newlines, except SGR and charset designators. ESC and C1 counterparts must produce the same boundary. String controls and their payloads vanish; LF, CR, and TAB remain text boundaries; other C0 controls that do not move or erase text vanish. **Must discard incomplete trailing presentation controls without creating a boundary** (rationale). - **Must select the rightmost match in the last 50 lines**, newest *by position* and never by pattern order (rationale). Source of truth: `CODING_AGENTS` in `lib/src/lib/coding-agents.ts`; `detectResumeCommand` / `normalizeResumeCommand` in `lib/src/lib/resume-patterns.ts`; `stripTerminalControls` in `lib/src/lib/terminal-controls.ts`; pinned by `lib/src/lib/coding-agents.test.ts`, `lib/src/lib/resume-patterns.test.ts`, and `lib/src/lib/terminal-controls.test.ts`. diff --git a/docs/compatible-agents.rationale.md b/docs/compatible-agents.rationale.md index 58d6ba110..f246dba24 100644 --- a/docs/compatible-agents.rationale.md +++ b/docs/compatible-agents.rationale.md @@ -50,7 +50,7 @@ Rows 1–2 are why a blanket second press is wrong; `Press Ctrl-C again` was abs **Why the Fe range is not enough to match an escape.** `ESC 7` / `ESC 8` and `ESC c` have final bytes outside it, so a matcher keyed on the introducer alone strips the ESC and leaks the final byte into the text. -**Why boundary-mode stripping inverts the rule.** Observed in the wild: a stored `claude --resume codex`. Deleting controls instead of replacing them with a newline welded two fragments never adjacent on screen into one id-shaped token, which then passed the id grammar. Erasures count too — `\x1b[2K` means the text before it on that line is gone — while SGR and charset designators are the only classes where the text either side really is contiguous. +**Why boundary-mode stripping inverts the rule.** Observed in the wild: a stored `claude --resume codex`. Deleting controls instead of replacing them with a newline welded two fragments never adjacent on screen into one id-shaped token, which then passed the id grammar. Erasures count too — `\x1b[2K` means the text before it on that line is gone. Among the non-string ESC/CSI presentation sequences, SGR and charset designators leave the surrounding text contiguous. String payloads are removed by the earlier framing pass; this policy does not imply that every string protocol leaves the cursor unchanged. ## Recovery record diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 4b6b75172..85a67cd49 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -155,7 +155,7 @@ An **await** parks on one Session until it finishes what it is doing, then repor **An await crosses from the renderer to the host process that holds the manager**, and the wait itself never leaves the host: -- The renderer asks to park (`await`, under an `awaitId` its client mints) and, if it gives up, to cancel (`awaitCancel`); **the host answers exactly one `alert:awaitResult {awaitId, outcome}` per await, to the realm that parked it**, a cancel included. **A realm's repeated `awaitId` is ignored, never answered twice**; a malformed `id` or `until` is answered `cancelled`. +- The renderer asks to park (`await`, under an `awaitId` its client mints) and, if it gives up, to cancel (`awaitCancel`); **the host answers exactly one `alert:awaitResult {awaitId, outcome}` per await, to the realm that parked it**, a cancel included. **An `awaitId` already parked in that realm is ignored**; a malformed `id` or `until` is answered `cancelled`. - **A realm that ends — a disposed or recreated webview, a reloaded or closed window — has everything it parked cancelled and answered by the host, *synchronously*** (rationale); a disposing adapter settles its own. - `cancelled` has no wire outcome of its own: the renderer reports it to `dor` as an error, which is also what forgets the in-flight control request. - The fake adapter runs the same host in process. The Pocket phone adapter has no `dor` and protocol-v1 carries no await, so it settles every request `cancelled` at once. @@ -175,7 +175,7 @@ Source of truth: `awaitCompletion` in `lib/src/lib/alert-manager.ts`; `AlertComm Rules: -- **The key is `commandWatchKey(rawCommandLine)`**: the last command of a list (`&&`, `||`, `;`, `&`, newline) and the first stage of its pipeline, grouping dropped, leading `VAR=value` words and transparent wrappers (`sudo`, `npx`, …) skipped, then argv[0]'s basename minus any launcher suffix (`docs/specs/terminal-state.md`). **Reserved words are grammar, fish's included**: a leading `do`, `then`, `if`, `!`, `and`, `not`, `begin`, … is stripped, a segment a closer (`done`, `fi`, `esac`, `end`) leads is skipped with its redirection or pipe, and a `for` header, a case pattern, and fish's `case` line are dropped, so `for f in *; do make; done` keys on `make` (rationale). **A redirection is never the program or a runner's script**, a separate target skipped with it (rationale). **An unknown wrapper flag stops the skip**, keying the wrapper itself. **A script runner keys as `