From 92d701b17ef8b25ee755e762c0c6e99feddac13f Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:39:24 +0200 Subject: [PATCH 1/5] docs: define hierarchical pty contract --- DEVELOPMENT.md | 33 +++- README.md | 5 +- docs/client.md | 10 +- docs/disk-layout.md | 2 +- .../01-launch-context/requirements.md | 32 ++++ .../01-launch-context/spec.md | 47 ++++++ .../02-lifecycle/requirements.md | 27 +++ .../01-session-runtime/02-lifecycle/spec.md | 55 ++++++ docs/vrs/01-session-runtime/intuition.md | 24 +++ docs/vrs/01-session-runtime/ontology.md | 32 ++++ docs/vrs/01-session-runtime/requirements.md | 55 ++++++ docs/vrs/01-session-runtime/spec.md | 56 +++++++ .../01-synchronization/requirements.md | 28 ++++ .../01-synchronization/spec.md | 40 +++++ .../02-geometry/requirements.md | 27 +++ .../vrs/02-session-stream/02-geometry/spec.md | 45 +++++ docs/vrs/02-session-stream/intuition.md | 25 +++ docs/vrs/02-session-stream/ontology.md | 36 ++++ docs/vrs/02-session-stream/requirements.md | 63 +++++++ docs/vrs/02-session-stream/spec.md | 90 ++++++++++ docs/vrs/03-registry/intuition.md | 24 +++ docs/vrs/03-registry/ontology.md | 32 ++++ docs/vrs/03-registry/requirements.md | 52 ++++++ docs/vrs/03-registry/spec.md | 85 ++++++++++ .../01-cli-package/requirements.md | 30 ++++ docs/vrs/04-surfaces/01-cli-package/spec.md | 47 ++++++ .../04-surfaces/02-libraries/requirements.md | 27 +++ docs/vrs/04-surfaces/02-libraries/spec.md | 51 ++++++ docs/vrs/04-surfaces/intuition.md | 22 +++ docs/vrs/04-surfaces/ontology.md | 29 ++++ docs/vrs/04-surfaces/requirements.md | 52 ++++++ docs/vrs/04-surfaces/spec.md | 54 ++++++ docs/vrs/intuition.md | 30 ++++ docs/vrs/ontology.md | 45 +++++ docs/vrs/requirements.md | 84 ++++++++++ docs/vrs/spec.md | 58 +++++++ scripts/verify-docs.ts | 156 ++++++++++++++++++ src/server.ts | 18 +- src/spawn.ts | 9 +- 39 files changed, 1611 insertions(+), 26 deletions(-) create mode 100644 docs/vrs/01-session-runtime/01-launch-context/requirements.md create mode 100644 docs/vrs/01-session-runtime/01-launch-context/spec.md create mode 100644 docs/vrs/01-session-runtime/02-lifecycle/requirements.md create mode 100644 docs/vrs/01-session-runtime/02-lifecycle/spec.md create mode 100644 docs/vrs/01-session-runtime/intuition.md create mode 100644 docs/vrs/01-session-runtime/ontology.md create mode 100644 docs/vrs/01-session-runtime/requirements.md create mode 100644 docs/vrs/01-session-runtime/spec.md create mode 100644 docs/vrs/02-session-stream/01-synchronization/requirements.md create mode 100644 docs/vrs/02-session-stream/01-synchronization/spec.md create mode 100644 docs/vrs/02-session-stream/02-geometry/requirements.md create mode 100644 docs/vrs/02-session-stream/02-geometry/spec.md create mode 100644 docs/vrs/02-session-stream/intuition.md create mode 100644 docs/vrs/02-session-stream/ontology.md create mode 100644 docs/vrs/02-session-stream/requirements.md create mode 100644 docs/vrs/02-session-stream/spec.md create mode 100644 docs/vrs/03-registry/intuition.md create mode 100644 docs/vrs/03-registry/ontology.md create mode 100644 docs/vrs/03-registry/requirements.md create mode 100644 docs/vrs/03-registry/spec.md create mode 100644 docs/vrs/04-surfaces/01-cli-package/requirements.md create mode 100644 docs/vrs/04-surfaces/01-cli-package/spec.md create mode 100644 docs/vrs/04-surfaces/02-libraries/requirements.md create mode 100644 docs/vrs/04-surfaces/02-libraries/spec.md create mode 100644 docs/vrs/04-surfaces/intuition.md create mode 100644 docs/vrs/04-surfaces/ontology.md create mode 100644 docs/vrs/04-surfaces/requirements.md create mode 100644 docs/vrs/04-surfaces/spec.md create mode 100644 docs/vrs/intuition.md create mode 100644 docs/vrs/ontology.md create mode 100644 docs/vrs/requirements.md create mode 100644 docs/vrs/spec.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f8c2ea7..2cc9892 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -25,6 +25,7 @@ npm run typecheck # typecheck with tsc (no emit) npm test # run all tests once npm run test:watch # run tests in watch mode npm run verify-docs # run executable examples in docs/testing.md +node scripts/verify-docs.ts --vrs-only # validate the VRS hierarchy # Usage (during development — run the TS source directly with Node) node --experimental-strip-types src/cli.ts run -- [args...] @@ -47,6 +48,10 @@ Source is written in TypeScript with `.ts` import extensions. `npm run build` co ## Architecture +The hierarchical durable contract is maintained in +[`docs/vrs`](docs/vrs/spec.md); this guide explains the implementation and +development workflow. + ``` ┌─────────────────────────────────────────────┐ │ Daemon (one per session) │ @@ -80,13 +85,21 @@ Binary packets over Unix sockets: `[type: uint8][length: uint32BE][payload]` |------|----|-----------|---------| | DATA | 0 | Both | Raw terminal bytes | | ATTACH | 1 | Client → Server | `[rows: uint16BE, cols: uint16BE]` (4 bytes) | -| DETACH | 2 | Client → Server | Empty | +| DETACH | 2 | Client → Server; machine adapter → consumer | Empty | | RESIZE | 3 | Client → Server | `[rows: uint16BE, cols: uint16BE]` (4 bytes) | | EXIT | 4 | Server → Client | `[exitCode: int32BE]` (4 bytes) | | SCREEN | 5 | Server → Client | ANSI escape sequences (string) | -| PEEK | 6 | Client → Server | Empty | - -`PacketReader` handles streaming reassembly of partial reads. Decoders gracefully handle truncated payloads (defaults for size, -1 for exit code). Unknown message types are silently ignored by the server. +| PEEK | 6 | Client → Server | Flags: plain bit 0, full-scrollback bit 1 | +| STATUS | 7 | Both | Empty request or JSON response | +| GEOMETRY | 10 | Server → Client | `[rows: uint16BE, cols: uint16BE]` (4 bytes) | + +`PacketReader` handles streaming reassembly of partial reads and rejects a +declared payload above 32 MiB. Decoders retain legacy fallbacks for truncated +size and exit payloads; command-specific surfaces validate stronger contracts. +Unknown bounded message types are ignored by the server. Each valid `ATTACH` or +recognized `PEEK` that emits terminal state starts with `GEOMETRY`, then an +ordered `SCREEN` baseline, then live `DATA` or `EXIT`. A local machine detach +may instead emit `DETACH` before the baseline. ## Key Design Decisions @@ -102,9 +115,13 @@ We avoid TS enums because they emit runtime code that can't be type-stripped. In The PTY can only be one size. If a peek client's terminal size were used, it could reflow the session — imagine vim at 120x40 suddenly becoming 40x20 because someone peeked from their phone. Readonly clients are excluded from size negotiation entirely. They see whatever fits; the active user's layout is never disrupted. -### Last attached client wins for size +### Smallest writable client wins for size -When multiple interactive (non-peek) clients are connected, the most recently attached client's terminal size is used for the PTY. This is simple and predictable. An alternative would be minimum dimensions across all clients, but that punishes the primary user when a smaller client connects. +When multiple interactive (non-peek) clients are connected, the PTY uses the +minimum requested row count and minimum requested column count independently. +This guarantees that every writable client can represent the complete shared +grid. Readonly clients receive effective-geometry updates but never constrain +the size. ### xterm-headless as the screen buffer @@ -168,6 +185,7 @@ npm test # run once (or: pty test) npm run test:watch # watch mode (or: pty test watch) npx vitest run -t "peek" # run tests matching "peek" npm run verify-docs # run executable examples in docs/testing.md +node scripts/verify-docs.ts --vrs-only # validate the VRS hierarchy ``` ### node-pty on macOS @@ -204,8 +222,9 @@ tests/ tui.test.ts docs/ testing.md Testing library documentation (with executable examples) + vrs/ Hierarchical system requirements and specification scripts/ - verify-docs.ts Extracts and runs doc examples via vitest + verify-docs.ts Validates VRS structure and runs doc examples via vitest completions/ pty.bash Bash tab completion pty.zsh Zsh tab completion diff --git a/README.md b/README.md index f439188..9c8bdfb 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Persistent terminal sessions. Run a process, detach, reconnect later. From anywh Uses [@xterm/headless](https://github.com/xtermjs/xterm.js/tree/master/headless) internally. +The durable system contract and subsystem map live in +[docs/vrs](docs/vrs/spec.md). + ## Install ```sh @@ -321,7 +324,7 @@ The values are overlaid on the session child's inherited environment before its Direct launches can also persist removals from the inherited environment with repeatable `pty run --unset-env KEY`. Removals are applied before `--env` overlays, so an explicit assignment wins when both mention the same key, regardless of flag order. Both policies survive manual and permanent restart. Metadata created before `unsetEnv` was introduced retains the historical ambient-inheritance behavior. -Two child invariants are applied after that policy: `PTY_SESSION` is always set to the session's stable id, and an absent `TERM` receives the existing `xterm-256color` default. Consequently, `--unset-env PTY_SESSION` cannot remove the session marker, and `--unset-env TERM` selects the default rather than leaving `TERM` absent. An explicit `--env TERM=...` assignment is preserved. +Two child invariants are applied after that policy: `PTY_SESSION` is always set to the session's stable id, and node-pty treats `TERM` as terminal capability metadata. An absent or empty `TERM` selects the runtime's `xterm-256color` terminal name; a nonempty value is preserved. Consequently, `--unset-env PTY_SESSION` cannot remove the session marker, and `--unset-env TERM` or `--env TERM=` selects the default rather than leaving `TERM` absent. Ordinary environment assignments, including empty values such as `NO_COLOR=`, remain exact. ### Permanent sessions diff --git a/docs/client.md b/docs/client.md index beacc56..b203bf2 100644 --- a/docs/client.md +++ b/docs/client.md @@ -187,14 +187,16 @@ interface SpawnDaemonOptions { isolateEnv?: boolean; // inherit only the safe allow-list extraEnv?: Record; // explicit assignments applied last unsetEnv?: string[]; // inherited keys removed before assignments - env?: Record; // exact child env; mutually exclusive with the above + env?: Record; // replacement base; PTY_SESSION/TERM invariants still apply } ``` `unsetEnv` removals run before `extraEnv` assignments. The server then forces -`PTY_SESSION` to the stable session id and fills an absent `TERM` with -`xterm-256color`; naming either key in `unsetEnv` does not suppress those -invariants. An explicit `extraEnv.TERM` value is preserved. +`PTY_SESSION` to the stable session id. Node-pty treats `TERM` as terminal +capability metadata: an absent or empty value selects the runtime's +`xterm-256color` terminal name, while a nonempty value is preserved. Naming +either key in `unsetEnv` does not suppress those invariants. Ordinary +assignments retain exact values, including an empty `NO_COLOR`. ### `resolveCommand(cmd: string): string` diff --git a/docs/disk-layout.md b/docs/disk-layout.md index bab59ef..19b49d9 100644 --- a/docs/disk-layout.md +++ b/docs/disk-layout.md @@ -58,7 +58,7 @@ Pretty-printed JSON. Source of truth: `SessionMetadata` in `src/sessions.ts`. isolateEnv?: boolean; extraEnv?: { [k: string]: string }; // explicit inherited-env overlay (`--env`) unsetEnv?: string[]; // inherited env keys removed before `extraEnv` - env?: { [k: string]: string }; // exact child env for programmatic callers + env?: { [k: string]: string }; // replacement base; runtime invariants still apply createdAt: string; // ISO 8601 exitCode?: number; // present after clean exit exitedAt?: string; diff --git a/docs/vrs/01-session-runtime/01-launch-context/requirements.md b/docs/vrs/01-session-runtime/01-launch-context/requirements.md new file mode 100644 index 0000000..2cefe9b --- /dev/null +++ b/docs/vrs/01-session-runtime/01-launch-context/requirements.md @@ -0,0 +1,32 @@ +# Launch context requirements + +> **Role.** Preserve the complete child launch across runtime creation and +> restart. These requirements refine the parent session-runtime contract. + +## Requirements + +- **PTY.RUN.ENV-R01 — Complete launch record.** Persisted launches retain + command, args, display command, cwd, initial rows and columns, lifetime flags, + tags, display name, and the chosen environment mode. _refines: PTY.RUN-R04._ +- **PTY.RUN.ENV-R02 — Exclusive environment modes.** A caller chooses either an + explicit replacement environment base or + inherited/isolate-plus-removals-and-assignments; combining them is rejected + before spawn. Exactness applies to ordinary caller-owned keys, subject to the + runtime-owned invariants below. _refines: PTY.RUN-R05._ +- **PTY.RUN.ENV-R03 — Ordered inherited policy.** In inherited and isolated + modes, named removals are applied before explicit assignments, so assignment + wins independently of CLI flag order. _refines: PTY.RUN-R05._ +- **PTY.RUN.ENV-R04 — Runtime invariants.** `PTY_SESSION` is always the stable + id. `TERM` is terminal capability metadata: an absent or empty value + selects the runtime's `xterm-256color` terminal name, while a nonempty value + is preserved through node-pty's public terminal-name contract. Ordinary keys + retain exact assigned values, including an empty `NO_COLOR`. _refines: + PTY.RUN-R05._ +- **PTY.RUN.ENV-R05 — Restart durability.** Explicit restart and metadata-based + permanent respawn preserve environment removals and assignments. A + manifest-backed permanent respawn re-reads its manifest declaration, while a + missing or unreadable manifest falls back to persisted metadata. _refines: + PTY.RUN-R04._ +- **PTY.RUN.ENV-R06 — Historical compatibility.** Metadata without an + `unsetEnv` field retains historical ambient inheritance rather than gaining + an implicit removal policy. _refines: PTY.RUN-R04._ diff --git a/docs/vrs/01-session-runtime/01-launch-context/spec.md b/docs/vrs/01-session-runtime/01-launch-context/spec.md new file mode 100644 index 0000000..1393bc5 --- /dev/null +++ b/docs/vrs/01-session-runtime/01-launch-context/spec.md @@ -0,0 +1,47 @@ +# Launch context specification + +This document specifies launch-definition assembly and persistence. It builds +on [requirements.md](./requirements.md) and the parent +[runtime specification](../spec.md). + +## Status + +Active. + +## Environment modes + +```text +exact mode: copy env -> PTY_SESSION -> normalize TERM if absent or empty + +inherited mode: process.env + -> remove PTY_SERVER_CONFIG + -> unsetEnv[] + -> extraEnv{} + -> PTY_SESSION + -> normalize TERM if absent or empty + +isolated mode: allowlisted process.env + LC_* + -> unsetEnv[] -> extraEnv{} + -> PTY_SESSION -> normalize TERM if absent or empty +``` + +Passing `env` together with `isolateEnv`, `extraEnv`, or a non-empty `unsetEnv` +is invalid in both `SpawnDaemonOptions` and `ServerOptions` +(PTY.RUN.ENV-R02–R04). + +The environment map is exact for ordinary caller-owned keys: removal removes, +assignment wins, and empty values such as `NO_COLOR=` remain empty. Two keys are +runtime-owned instead. `PTY_SESSION` is forced to the stable session id, and +node-pty interprets `TERM` as its terminal-name capability. The runtime selects +`xterm-256color` when it is absent or empty; a nonempty value is preserved. + +`SessionMetadata` stores `rows`, `cols`, `ephemeral`, `isolateEnv`, `extraEnv`, +`unsetEnv`, and exact `env` alongside the command fields. Operator restart and +metadata fallback pass them back to `spawnDaemon`. Manifest-backed permanent +respawn re-reads command, cwd, tags, and assignments from `pty.toml`; unreadable +or absent definitions use the recorded values (PTY.RUN.ENV-R01, R05–R06). + +The operator restart path may scrub explicitly named variables from the +daemon's own inherited environment before spawn. This prevents the restarter's +ambient identity from becoming undeclared child state; it does not alter the +persisted child policy. diff --git a/docs/vrs/01-session-runtime/02-lifecycle/requirements.md b/docs/vrs/01-session-runtime/02-lifecycle/requirements.md new file mode 100644 index 0000000..ef67096 --- /dev/null +++ b/docs/vrs/01-session-runtime/02-lifecycle/requirements.md @@ -0,0 +1,27 @@ +# Lifecycle requirements + +> **Role.** Define exit, preservation, cleanup, and restart ownership for one +> session. These requirements refine the parent session-runtime contract. + +## Requirements + +- **PTY.RUN.LIFE-R01 — Derived lifecycle state.** `running`, `exited`, and + `vanished` are derived from process/socket evidence and exit metadata, not a + mutable stored status field. _refines: PTY.RUN-R06._ +- **PTY.RUN.LIFE-R02 — Ordered finalization.** The runtime drains child output, + records one exit result and final screen lines, emits one exit event, then + applies preservation or reap policy. _refines: PTY.RUN-R06._ +- **PTY.RUN.LIFE-R03 — Explicit policy precedence.** Explicit removal wins; + `keep=true` forces preservation; ephemeral forces reap except when keep wins; + permanent and explicit kill preserve state for restart or inspection. + _refines: PTY.RUN-R06, PTY.RUN-R07._ +- **PTY.RUN.LIFE-R04 — Generation-safe mutation.** Cleanup, replacement, and + permanent respawn compare the observed generation while holding the per-id + creation lock. A changed generation remains untouched. _refines: + PTY.RUN-R07._ +- **PTY.RUN.LIFE-R05 — Stateless permanent reconciliation.** `gc` can respawn a + dead permanent session, reap parent-orphans or abandoned sessions, and hold a + repeatedly fast-failing session as flapping. A dry run reports the same plan + without lifecycle writes. _refines: PTY.RUN-R07._ +- **PTY.RUN.LIFE-R06 — Observational purity.** Listing and state reads do not + trigger cleanup, respawn, or metadata repair. _refines: PTY.RUN-R07._ diff --git a/docs/vrs/01-session-runtime/02-lifecycle/spec.md b/docs/vrs/01-session-runtime/02-lifecycle/spec.md new file mode 100644 index 0000000..aeab774 --- /dev/null +++ b/docs/vrs/01-session-runtime/02-lifecycle/spec.md @@ -0,0 +1,55 @@ +# Lifecycle specification + +This document specifies session lifecycle transitions. It builds on +[requirements.md](./requirements.md) and the parent +[runtime specification](../spec.md). + +## Status + +Active. + +## State and actions + +```text + child exits cleanly +running ------------------------> exited + | | + | daemon disappears | restart / permanent gc + v v +vanished ----------------------> running (new generation) + | ^ + +------- explicit cleanup -------+ +``` + +`exited` requires recorded `exitCode`/`exitedAt`; a dead daemon without that +record is `vanished`. Both are non-live and may be inspected, removed, swept, or +used as restart input. `listSessions` derives the state and remains +observational (PTY.RUN.LIFE-R01, R06). + +## Exit policy + +| Condition | Result | +| --- | --- | +| explicit `rm` | remove after generation-safe daemon shutdown | +| `keep=true` | preserve until explicit `rm` | +| `--ephemeral` | reap on shutdown unless keep is set | +| `strategy=permanent` | preserve for reconciliation unless ephemeral | +| explicit `kill` | stop and preserve unless ephemeral | +| ordinary exit | configured `PTY_REAP_ON_EXIT`, default reap | + +Before applying the table, the runtime's public exit callback is ordered after +the PTY read stream, terminal metadata is snapshotted, and `session_exit` is +emitted (PTY.RUN.LIFE-R02–R03). + +## Ownership and reconciliation + +A stable id has an exclusive creation lock. Cleanup checks the opaque metadata +generation it observed before deleting socket or all artifacts. Permanent +respawn holds that lock across compare, stale cleanup, and replacement socket +publication. A replacement generation therefore cannot be deleted by an older +daemon or stale `gc` plan (PTY.RUN.LIFE-R04). + +`gc` applies parent-orphan removal before permanent respawn, optionally detects +cwd disappearance or idle abandonment, tracks bounded fast-failure state, and +then sweeps eligible finished records. `--dry-run` plans without mutation +(PTY.RUN.LIFE-R05). diff --git a/docs/vrs/01-session-runtime/intuition.md b/docs/vrs/01-session-runtime/intuition.md new file mode 100644 index 0000000..f69d1c0 --- /dev/null +++ b/docs/vrs/01-session-runtime/intuition.md @@ -0,0 +1,24 @@ +# Session runtime intuition + +*For: runtime maintainers · Assumes: the root pty model · Covers: why one daemon +owns one session* + +A session survives because its daemon is detached from the command that asked +for it. The daemon is deliberately small in responsibility: own one child PTY, +maintain one terminal model, serve clients, and finalize one lifecycle. + +```text +launcher exits daemon exits + | | + v v +session continues child result + policy finalize +``` + +Restart is not “run the same-looking command.” It replays a complete launch +definition. Environment removals matter as much as assignments: if `NO_COLOR` +was deliberately absent, inheriting it from a later operator shell would change +the program even though the command line stayed identical. + +Cleanup is likewise about ownership, not filenames. A stale daemon may see the +same stable id after a replacement has started. The opaque generation turns +“delete this name” into “delete this name only if it is still mine.” diff --git a/docs/vrs/01-session-runtime/ontology.md b/docs/vrs/01-session-runtime/ontology.md new file mode 100644 index 0000000..de35ab8 --- /dev/null +++ b/docs/vrs/01-session-runtime/ontology.md @@ -0,0 +1,32 @@ +# Session runtime ontology + +Root terms are inherited from [../ontology.md](../ontology.md). + +## Language + +**Launch definition**: +The complete persisted input needed to start the child equivalently: command, +arguments, display command, working directory, initial geometry, lifetime +flags, tags, display name, and environment policy. + +**Inherited environment policy**: +The ordered transformation `base -> removals -> assignments -> runtime +invariants`. It is distinct from an exact environment map. + +**Ordinary environment key**: +A caller-owned child variable whose assigned value, including an empty value, +is preserved. `PTY_SESSION` is runtime identity and `TERM` is terminal +capability metadata, so neither is ordinary launch data. + +**Permanent session**: +A preserved session tagged `strategy=permanent` whose absent or dead runtime is +eligible for explicit `gc` reconciliation. +_Avoid_: immortal session; abandonment and flapping policy can still stop it. + +**Reap**: +Removal of a finished session's registry artifacts according to exit policy. +_Avoid_: kill (termination and removal are distinct lifecycle actions). + +**Generation-owned cleanup**: +Deletion that proceeds only while the target metadata still belongs to the +daemon generation or observation used to authorize it. diff --git a/docs/vrs/01-session-runtime/requirements.md b/docs/vrs/01-session-runtime/requirements.md new file mode 100644 index 0000000..1f0975b --- /dev/null +++ b/docs/vrs/01-session-runtime/requirements.md @@ -0,0 +1,55 @@ +# Session runtime requirements + +> **Role.** The execution realization of the root +> [session contract](../requirements.md): one independently surviving daemon, +> child PTY, and terminal model per session. Every requirement refines a +> `PTY-R*` requirement. + +## Assumptions + +- **PTY.RUN-A01 Child semantics:** The spawned command obeys ordinary Unix PTY, + signal, and exit-status semantics. +- **PTY.RUN-A02 Recoverable declaration:** A permanent session backed by a + readable `pty.toml` may take its next launch declaration from that manifest; + otherwise persisted metadata is the last-known-good declaration. + +## Acceptable tradeoffs + +- **PTY.RUN-T01 Bounded startup wait:** Daemon startup has a finite socket-ready + wait. Immediate process failure is surfaced earlier, while a living but slow + daemon may consume the full bound. +- **PTY.RUN-T02 Stateless permanent reconciliation:** Permanent respawn has no + resident supervisor. `pty gc` invocations provide cadence and rate limiting. + +## Requirements + +- **PTY.RUN-R01 — Client-independent lifetime.** The daemon and child continue + after the creating or attached client exits. Binding daemon lifetime to a + spawner is explicit opt-in. _refines: PTY-R01._ +- **PTY.RUN-R02 — Terminal authority.** The runtime feeds every child output byte + through one headless terminal model and uses that model for screen replay, + terminal modes, cursor, scrollback, and exit snapshots. _refines: PTY-R01, + PTY-R03._ +- **PTY.RUN-R03 — Per-session failure boundary.** Each session has its own + daemon, child, terminal, and socket. Closing one runtime does not control + another. _refines: PTY-R02._ +- **PTY.RUN-R04 — Restart-equivalent launch.** Initial launch, explicit restart, + and permanent respawn preserve the applicable command, args, cwd, initial + geometry, lifetime flags, tags, display name, and child-environment policy. + _refines: PTY-R05._ +- **PTY.RUN-R05 — Explicit environment policy.** Exact environment and inherited + environment policy are mutually exclusive; removals precede assignments; + ordinary keys retain explicit empty values, while `PTY_SESSION` and terminal + capability metadata remain runtime-owned invariants. + _refines: PTY-R05, PTY-R09._ +- **PTY.RUN-R06 — Observable exit.** Child data is drained before one terminal + exit result is finalized, with signal exits represented by shell-compatible + status. Final screen metadata and lifecycle policy are applied before daemon + shutdown completes. _refines: PTY-R05, PTY-R06._ +- **PTY.RUN-R07 — Explicit reconciliation.** Restart, permanent respawn, + abandonment, parent-orphan handling, and cleanup are explicit lifecycle + actions with generation checks; observation alone never performs them. + _refines: PTY-R02, PTY-R05._ + +See [launch context](./01-launch-context/requirements.md) and +[lifecycle](./02-lifecycle/requirements.md) for the concrete refinements. diff --git a/docs/vrs/01-session-runtime/spec.md b/docs/vrs/01-session-runtime/spec.md new file mode 100644 index 0000000..4b590ad --- /dev/null +++ b/docs/vrs/01-session-runtime/spec.md @@ -0,0 +1,56 @@ +# Session runtime specification + +This document specifies the per-session execution engine. It builds on +[requirements.md](./requirements.md). + +## Status + +Active. + +## Runtime structure + +```text +detached Node daemon + +-- node-pty child process + +-- xterm-headless + SerializeAddon + +-- Unix socket server + +-- event writer + `-- generation-owned registry artifacts +``` + +`spawnDaemon` serializes `ServerOptions` into the daemon launch, starts a +detached Node process, and waits for its socket with a bounded failure path. +`PtyServer` spawns the command under a real PTY, writes child output to the +headless terminal and stream clients, records terminal events, and owns cleanup +for its generation (PTY.RUN-R01–R03). + +The child is launched through `/bin/sh -c 'exec "$@"'` so scripts, symlinks, +and shebangs follow shell execution semantics without leaving an intermediate +shell process. + +## Terminal pipeline + +```text +child output + |---> xterm parser ---> serializable terminal state + `---> ordered client broadcast + +client DATA ---> child PTY input +effective size ---> xterm resize ---> child PTY resize +``` + +The runtime tracks mode sequences that a screen serialization alone cannot +re-establish reliably and prefixes them when reconstructing a client. It also +derives bell, title, notification, focus, and cursor events from terminal +output (PTY.RUN-R02). + +## Launch and lifecycle refinements + +- [launch context](./01-launch-context/spec.md) specifies command, geometry, + environment, and restart preservation (PTY.RUN-R04–R05). +- [lifecycle](./02-lifecycle/spec.md) specifies exit, reap, permanent respawn, + generation ownership, and explicit cleanup (PTY.RUN-R06–R07). + +The runtime does not decide how users discover a session, how a client renders +it, or which product should supervise it. Those belong to the registry, stream, +and surface subsystems. diff --git a/docs/vrs/02-session-stream/01-synchronization/requirements.md b/docs/vrs/02-session-stream/01-synchronization/requirements.md new file mode 100644 index 0000000..4bc010c --- /dev/null +++ b/docs/vrs/02-session-stream/01-synchronization/requirements.md @@ -0,0 +1,28 @@ +# Synchronization requirements + +> **Role.** Define the causally exact initial and reconnect baseline. These +> requirements refine the parent session-stream contract. + +## Requirements + +- **PTY.STREAM.SYNC-R01 — Geometry first.** An attach or peek generation emits + effective geometry before screen, data, or session exit for that generation. + A local machine detach outcome may terminate before terminal state begins. + _refines: PTY.STREAM-R01._ +- **PTY.STREAM.SYNC-R02 — Exact parser cut.** Screen serialization runs only + after an ordered terminal-parser marker; post-marker data and exit are queued + until after that screen. _refines: PTY.STREAM-R02._ +- **PTY.STREAM.SYNC-R03 — No lost settling data.** Data accepted while waiting + for resize settling remains represented in the later screen baseline rather + than being emitted ahead of it. _refines: PTY.STREAM-R02._ +- **PTY.STREAM.SYNC-R04 — Source-ordered exit.** Post-cut data is flushed before + its queued exit. A child that exited before the cut receives one synthesized + exit only after the baseline and any final post-cut data. _refines: + PTY.STREAM-R01, PTY.STREAM-R02._ +- **PTY.STREAM.SYNC-R05 — Supersession.** A later attach or peek request on the + same socket atomically replaces its client role and invalidates the prior + delayed cut and queue. A malformed attach changes neither. _refines: + PTY.STREAM-R02._ +- **PTY.STREAM.SYNC-R06 — Reconnect equivalence.** Every reconnect begins a new + generation satisfying the same geometry-screen-live order. _refines: + PTY.STREAM-R06._ diff --git a/docs/vrs/02-session-stream/01-synchronization/spec.md b/docs/vrs/02-session-stream/01-synchronization/spec.md new file mode 100644 index 0000000..66f2315 --- /dev/null +++ b/docs/vrs/02-session-stream/01-synchronization/spec.md @@ -0,0 +1,40 @@ +# Synchronization specification + +This document specifies the initial terminal parser cut. It builds on +[requirements.md](./requirements.md) and the parent +[stream specification](../spec.md). + +## Status + +Active. + +## Per-client state machine + +```text +ATTACH / PEEK + | + v + settling -- parser marker enqueued --> cutting -- marker callback --> live + | | | + data is represented in SCREEN queue DATA/EXIT write directly + +complete ATTACH / recognized PEEK: replace role and synchronization generation +malformed ATTACH: preserve role and generation +``` + +The runtime emits current or newly negotiated `GEOMETRY` on admission. An +attach affected by recent resize may remain `settling` for the redraw bound. +The eventual call to `terminal.write("", callback)` establishes the exact cut +(PTY.STREAM.SYNC-R01–R03). + +In the callback, the current generation writes `SCREEN`, transitions to live, +flushes `postCutPackets` in source order, and synthesizes `EXIT` only when the +runtime already exited and no queued exit exists. `node-pty` exposes its public +exit after draining PTY data, so queued `DATA` precedes queued `EXIT` +(PTY.STREAM.SYNC-R04). + +Each complete attach or recognized peek replaces readonly/writable state and +increments `initialScreenGeneration`. Delayed callbacks compare that token +before writing, so stale baselines and queues cannot leak across role changes or +reconnect generations. Attach geometry validation precedes both mutations; +peek preserves its optional flag compatibility (PTY.STREAM.SYNC-R05–R06). diff --git a/docs/vrs/02-session-stream/02-geometry/requirements.md b/docs/vrs/02-session-stream/02-geometry/requirements.md new file mode 100644 index 0000000..d9f7348 --- /dev/null +++ b/docs/vrs/02-session-stream/02-geometry/requirements.md @@ -0,0 +1,27 @@ +# Geometry requirements + +> **Role.** Define the single grid shared by writable clients. These +> requirements refine the parent session-stream contract. + +## Requirements + +- **PTY.STREAM.GEO-R01 — Independent dimensions.** Effective rows are the + minimum requested rows and effective columns are the minimum requested + columns across connected writable clients. _refines: PTY.STREAM-R03._ +- **PTY.STREAM.GEO-R02 — Writable membership.** A writable client joins on + attach, updates its request on resize, and leaves on detach, error, or close. + _refines: PTY.STREAM-R03._ +- **PTY.STREAM.GEO-R03 — Readonly neutrality.** Peek and status clients do not + constrain geometry. Peek clients still receive every effective-geometry + update needed to parse the state they observe. _refines: PTY.STREAM-R03, + PTY.STREAM-R05._ +- **PTY.STREAM.GEO-R04 — Ordered application.** On change, the runtime resizes + its terminal model, broadcasts geometry, then resizes the child PTY before + child redraw output can be delivered. _refines: PTY.STREAM-R03._ +- **PTY.STREAM.GEO-R05 — Explicit requested/effective split.** Client APIs retain + their requested size separately from the effective size reported by the + runtime and resize their local terminal grid before parsing affected bytes. + _refines: PTY.STREAM-R03._ +- **PTY.STREAM.GEO-R06 — Zero-writable stability.** Removing the last writable + client does not invent a new size; the runtime retains its last effective + grid until a later writable request changes it. _refines: PTY.STREAM-R03._ diff --git a/docs/vrs/02-session-stream/02-geometry/spec.md b/docs/vrs/02-session-stream/02-geometry/spec.md new file mode 100644 index 0000000..de2f699 --- /dev/null +++ b/docs/vrs/02-session-stream/02-geometry/spec.md @@ -0,0 +1,45 @@ +# Geometry specification + +This document specifies shared terminal-size negotiation. It builds on +[requirements.md](./requirements.md) and the parent +[stream specification](../spec.md). + +## Status + +Active. + +## Negotiation + +For writable client set `W`: + +```text +effectiveRows = min(client.rows for client in W) +effectiveCols = min(client.cols for client in W) +``` + +Rows and columns are minimized independently. A client requesting `60x80` and +one requesting `30x200` therefore produce `30x80`. Readonly clients are absent +from `W` (PTY.STREAM.GEO-R01–R03). + +## Change order + +```text +writable membership/request change + -> resize headless terminal + -> broadcast GEOMETRY to attached and readonly clients + -> resize child PTY (SIGWINCH/redraw may follow) + -> deliver resulting terminal DATA +``` + +The headless terminal is resized before the child so it is ready to parse the +redraw. Geometry is enqueued before that redraw can become stream data +(PTY.STREAM.GEO-R04). + +`SessionConnection`, server-mode `Session`, and `attachPty` consume geometry as +an event, update effective rows/columns, and resize their emulator before later +screen/data. Calling `resize` changes the local requested size; a smaller peer +can keep effective size below it (PTY.STREAM.GEO-R05). + +When `W` becomes empty, negotiation performs no resize. The last effective +terminal and PTY grid remains the baseline for readonly observation and the +next attach (PTY.STREAM.GEO-R06). diff --git a/docs/vrs/02-session-stream/intuition.md b/docs/vrs/02-session-stream/intuition.md new file mode 100644 index 0000000..1b17a8b --- /dev/null +++ b/docs/vrs/02-session-stream/intuition.md @@ -0,0 +1,25 @@ +# Session stream intuition + +*For: protocol implementers and terminal embedders · Assumes: the session +runtime model · Covers: reconstructing one shared terminal correctly* + +A screen baseline and live bytes are two halves of one ordered history. If the +boundary between them is approximate, bytes can be missing from both halves or +appear in both. The runtime therefore places an ordered marker in xterm's parser +queue, serializes after the marker, and holds later packets until that baseline +has been sent. + +```text +parser input: A B C | D E exit + ^ exact cut +client stream: GEOMETRY, SCREEN(A B C), DATA(D E), EXIT +``` + +Geometry is part of this history. A byte stream generated for 80 columns cannot +be parsed correctly into a 120-column grid and repaired later. Geometry changes +therefore occupy the same ordered stream before affected screen or data. + +The machine attach surface deliberately does not invent a second protocol. It +forwards the existing frames on a descriptor that cannot be contaminated by +human terminal output or stderr. That keeps one ordering authority across local, +remote, interactive, and embedded clients. diff --git a/docs/vrs/02-session-stream/ontology.md b/docs/vrs/02-session-stream/ontology.md new file mode 100644 index 0000000..211da13 --- /dev/null +++ b/docs/vrs/02-session-stream/ontology.md @@ -0,0 +1,36 @@ +# Session stream ontology + +Root terms are inherited from [../ontology.md](../ontology.md). + +## Language + +**Requested geometry**: +The rows and columns most recently advertised by one writable client. It is an +input to negotiation, not necessarily the terminal's current grid. + +**Effective geometry**: +The shared rows and columns selected across all connected writable clients and +applied to both the child PTY and headless terminal. + +**Readonly client**: +A `PEEK` connection that receives terminal state but cannot send child input or +participate in geometry negotiation. +_Avoid_: unauthorized client; readonly is not an access-control claim. + +**Synchronization generation**: +One `ATTACH` or `PEEK` request's ordered transfer of geometry, baseline, and +post-cut events. A later request on the socket invalidates it. + +**Screen baseline**: +A serialized terminal state representing all parser input before one exact cut. +It is the starting state for later data, not a periodically sampled screenshot. + +**Machine attach stream v1**: +The versioned CLI contract that reframes terminal events unchanged onto a +caller-owned inherited file descriptor while retaining the invoking terminal +for input and requested geometry. + +**Machine attach outcome**: +Exactly one terminal frame before clean EOF. `EXIT` means the session process +ended; `DETACH` means this attach client intentionally detached. EOF without an +outcome is truncation or transport loss. diff --git a/docs/vrs/02-session-stream/requirements.md b/docs/vrs/02-session-stream/requirements.md new file mode 100644 index 0000000..8497fc2 --- /dev/null +++ b/docs/vrs/02-session-stream/requirements.md @@ -0,0 +1,63 @@ +# Session stream requirements + +> **Role.** The ordered transport realization of the root terminal contract. +> It projects one runtime's terminal state to ephemeral clients. Every +> requirement refines a `PTY-R*` requirement. + +## Assumptions + +- **PTY.STREAM-A01 Ordered byte transport:** A local Unix socket and a routed + remote byte stream preserve byte order for one connection. +- **PTY.STREAM-A02 Parser cut:** The headless terminal's write callback is an + ordered marker after all earlier parser writes. + +## Acceptable tradeoffs + +- **PTY.STREAM-T01 Redraw settling:** After an effective resize, initial screen + capture may wait for a bounded redraw-settle interval so the baseline is not + a known transient mid-redraw frame. +- **PTY.STREAM-T02 Unversioned base framing:** The base packet header has no + protocol-version field. Bounded unknown packet types are ignored for additive + compatibility; capability-specific surfaces fail closed on missing required + packets. + +## Requirements + +- **PTY.STREAM-R01 — Ordered reconstruction.** Every `ATTACH` and `PEEK` + generation that emits terminal state starts with `GEOMETRY`, then exactly one + `SCREEN` baseline, then post-cut `DATA` and at most one `EXIT` in source order. + A local machine `DETACH` may terminate its client before the baseline. + _refines: PTY-R03._ +- **PTY.STREAM-R02 — Exact parser boundary.** Output accepted before the screen + cut is represented by `SCREEN`; output and exit after the cut are queued and + cannot overtake the baseline. A newer mode request on the same connection + invalidates the unfinished generation. _refines: PTY-R03._ +- **PTY.STREAM-R03 — Causal effective geometry.** Writable attach/resize/disconnect + recomputes the minimum requested rows and columns. Changed `GEOMETRY` is + broadcast before terminal bytes produced for that size; readonly clients + receive updates but never constrain the grid. _refines: PTY-R04._ +- **PTY.STREAM-R04 — Bounded framing.** Every packet is length-delimited, partial + reads are reassembled, and a declared payload above 32 MiB drops the + connection without unbounded buffering. _refines: PTY-R09._ +- **PTY.STREAM-R05 — Replaceable client roles.** Every attach frame with complete + geometry replaces the socket's role with writable, installs its requested + geometry, and authorizes input and resize. Every recognized peek frame + replaces the role with readonly and removes its geometry constraint. A + malformed attach changes neither role nor synchronization generation. Status + observes without joining geometry, and detach closes the client without + ending the session. _refines: PTY-R01, PTY-R06._ +- **PTY.STREAM-R06 — Reconnect is a new baseline.** Reconnecting to a living + session creates a fresh ordering generation and resets protocol parsing; + observed session `EXIT` is terminal and is never reconnected past. _refines: + PTY-R03, PTY-R05._ +- **PTY.STREAM-R07 — Machine stream fidelity.** Machine attach reframes original + `GEOMETRY`, `SCREEN`, `DATA`, and `EXIT` packets on a caller-owned inherited + descriptor and uses the existing empty `DETACH` frame for the local-detach + outcome. It keeps stdin/stdout as the controlling terminal, honors + backpressure, and ends with exactly one framed outcome: `EXIT` when the + session process ended or `DETACH` when this client intentionally detached. + EOF after either is clean; EOF without either is truncation or transport + loss. _refines: PTY-R08, PTY-R09._ + +See [synchronization](./01-synchronization/requirements.md) and +[geometry](./02-geometry/requirements.md) for concrete refinements. diff --git a/docs/vrs/02-session-stream/spec.md b/docs/vrs/02-session-stream/spec.md new file mode 100644 index 0000000..5d7a951 --- /dev/null +++ b/docs/vrs/02-session-stream/spec.md @@ -0,0 +1,90 @@ +# Session stream specification + +This document specifies the binary session protocol and its client modes. It +builds on [requirements.md](./requirements.md). + +## Status + +Active. + +## Packet framing + +```text ++------------+----------------+--------------------+ +| type: u8 | length: u32 BE | payload: N bytes | ++------------+----------------+--------------------+ +``` + +`PacketReader` buffers partial frames and emits complete packets in order. A +length greater than `32 * 1024 * 1024` poisons the reader and requires the +connection to be destroyed (PTY.STREAM-R04). + +| Type | Id | Direction | Payload | +| --- | ---: | --- | --- | +| `DATA` | 0 | both | terminal bytes | +| `ATTACH` | 1 | client to runtime | rows `u16BE`, cols `u16BE` | +| `DETACH` | 2 | client to runtime; machine adapter to consumer | empty | +| `RESIZE` | 3 | client to runtime | rows `u16BE`, cols `u16BE` | +| `EXIT` | 4 | runtime to client | signed exit code `i32BE` | +| `SCREEN` | 5 | runtime to client | serialized ANSI terminal state or requested plain text | +| `PEEK` | 6 | client to runtime | flags: plain bit 0, full-scrollback bit 1 | +| `STATUS` | 7 | both | empty request or JSON response | +| 8–9 | — | — | reserved | +| `GEOMETRY` | 10 | runtime to client | effective rows `u16BE`, cols `u16BE` | + +Bounded unknown types are ignored by the runtime. Legacy size/exit payload +decoders retain historical fallback values; commands with stronger contracts +validate their required shape before acting (PTY.STREAM-T02). + +`GEOMETRY` is an additive type at id 10. Clients predating it ignore the +bounded unknown packet and continue with their historical raw `SCREEN`/`DATA` +behavior. Machine attach v1 instead requires geometry and fails explicitly +against an older daemon that cannot establish its reconstruction contract. + +## Connection roles + +```text +new connection + +-- ATTACH -> replace role with writable, contribute geometry, accept input + +-- PEEK -> replace role with readonly, remove geometry constraint + `-- STATUS -> observation only, does not join geometry +``` + +An `ATTACH` carrying complete geometry or a recognized `PEEK` replaces rather +than accumulates role state and starts a fresh synchronization generation. A +malformed `ATTACH` leaves both role and generation unchanged. Peek retains its +historical optional/extensible flag handling rather than imposing a new strict +payload shape. `DETACH` ends only the connection. Socket close removes its +writable geometry constraint and may change the effective grid +(PTY.STREAM-R05). + +## Ordered state transfer + +The [synchronization specification](./01-synchronization/spec.md) owns initial +and reconnect ordering. The [geometry specification](./02-geometry/spec.md) +owns requested/effective size negotiation. Together they establish: + +```text +GEOMETRY -> SCREEN -> DATA* -> EXIT? +``` + +## Human and machine attach + +Ordinary attach clears the user's terminal before `SCREEN`, writes subsequent +`DATA` to stdout, forwards stdin and stdout resize events, and sanitizes modes +on detach/exit. Machine attach receives the same socket packets and reframes +the four server event types to `--attach-stream-fd-v1`; on local detach it emits +the existing empty `DETACH` frame as the terminal outcome. Stdout remains the +controlling TTY and receives no screen bytes (PTY.STREAM-R07). + +The machine state machine requires `GEOMETRY`, permits further geometry updates +while awaiting `SCREEN`, then permits live events. Each reconnect resets it to +the initial state. A local detach may emit its terminal `DETACH` outcome while +the initial baseline is still pending. Descriptor backpressure pauses the +source socket. The caller retains descriptor ownership; the attach adapter ends +its stream view, not the underlying fd. Exactly one outcome terminates a clean +stream: framed `EXIT` means the session ended, while framed `DETACH` means this +client intentionally detached. EOF without either outcome, including transport +loss, reconnect give-up, or abrupt administrative session destruction before a +process `EXIT` was observed, is truncation. Administrative destruction does not +introduce a third clean outcome (PTY.STREAM-R06–R07). diff --git a/docs/vrs/03-registry/intuition.md b/docs/vrs/03-registry/intuition.md new file mode 100644 index 0000000..a41fab2 --- /dev/null +++ b/docs/vrs/03-registry/intuition.md @@ -0,0 +1,24 @@ +# Registry intuition + +*For: integrators and fast-path readers · Assumes: the root session model · +Covers: durable identity without turning files into the live protocol* + +The registry answers “what sessions exist, how were they launched, and what +happened?” The socket answers “what is happening in this terminal now?” Keeping +those questions separate makes both boundaries simpler. + +```text +registry JSON/events live socket +identity + history ordered terminal state +cheap external reads attach/input/geometry +``` + +Atomic rename means an external reader never sees half a metadata document, but +it does not turn independent writers into a transaction. Generation checks +solve a different race: they prevent an old owner from deleting a new session +that reused the same stable id. + +The stable id is deliberately boring because it reaches filesystem and kernel +socket paths. Display names and tags carry richer presentation and grouping, +while explicit ambiguity and `PTY_ROOT` keep them from silently becoming the +wrong kind of identity boundary. diff --git a/docs/vrs/03-registry/ontology.md b/docs/vrs/03-registry/ontology.md new file mode 100644 index 0000000..9bb83fc --- /dev/null +++ b/docs/vrs/03-registry/ontology.md @@ -0,0 +1,32 @@ +# Registry ontology + +Root terms are inherited from [../ontology.md](../ontology.md). + +## Language + +**Tier 1 artifact**: +A documented external-readable storage surface whose changes are called out for +version-pinned consumers: session metadata or events. + +**Tier 2 artifact**: +An implementation-owned registry artifact that may move without storage-format +compatibility: socket, pid, lock, theme, or gc log. + +**Running**: +A session whose daemon process is alive and whose socket is reachable. + +**Exited**: +A non-live session with recorded exit details. + +**Vanished**: +A non-live session without recorded exit details because the daemon could not +finalize them. +_Avoid_: exited; the cause and code are unknown. + +**Presentation reference**: +A display-name lookup accepted only when exactly one session matches. It is a +convenience selector, not durable identity. + +**Hard isolation**: +Selection of a distinct registry root. Tag filtering within a registry is soft +scoping, not isolation. diff --git a/docs/vrs/03-registry/requirements.md b/docs/vrs/03-registry/requirements.md new file mode 100644 index 0000000..c92d288 --- /dev/null +++ b/docs/vrs/03-registry/requirements.md @@ -0,0 +1,52 @@ +# Registry requirements + +> **Role.** The durable identity and observation realization of the root +> contract. Every requirement refines a `PTY-R*` requirement. + +## Assumptions + +- **PTY.REG-A01 Single writer authority:** The `pty` implementation is the + canonical writer. External tools may read tier-1 files directly. +- **PTY.REG-A02 Same-filesystem publication:** Temporary and target metadata + files share a filesystem, so rename publishes atomically. + +## Acceptable tradeoffs + +- **PTY.REG-T01 Last-write-wins metadata:** Individual file replacement is + atomic, but concurrent read-modify-write metadata updates are not + transactionally merged. +- **PTY.REG-T02 Bounded events:** Event history truncates from 1,000 to the most + recent 500 lines rather than growing without bound. + +## Requirements + +- **PTY.REG-R01 — Root isolation.** Every command resolves one registry from + explicit `PTY_ROOT` or the documented default; distinct roots share no + sessions, sockets, events, or cleanup. _refines: PTY-R02, PTY-R07._ +- **PTY.REG-R02 — Durable launch metadata.** Tier-1 metadata records stable + identity-adjacent launch, generation, presentation, exit, and restart state + and is published atomically. _refines: PTY-R05._ +- **PTY.REG-R03 — Pure inventory.** Listing derives `running`, `exited`, or + `vanished` from bounded process/socket evidence and never cleans, restarts, + migrates, or repairs state. _refines: PTY-R06._ +- **PTY.REG-R04 — Observable live state.** Status exposes the effective terminal + geometry, cursor/scrollback, process and daemon resources, terminal modes, + and anonymous client counts plus requested geometry and constraints without + attaching. Events expose timestamped lifecycle, terminal, presentation, tag, and + `user.*` changes. _refines: PTY-R06._ +- **PTY.REG-R05 — Stable identity.** Filename-safe stable ids are unique in a + registry. Display names are mutable and non-unique; exact id wins resolution, + and an ambiguous display name refuses with candidates. Tags support + composable filtering but do not create hard isolation. _refines: PTY-R07._ +- **PTY.REG-R06 — Documented compatibility tiers.** Tier-1 metadata and events + are documented and storage changes are called out; temporary files are + ignored; tier-2 socket, pid, lock, and UI files remain internal. Older + metadata fields retain documented defaults. _refines: PTY-R09._ +- **PTY.REG-R07 — Generation-safe ownership.** A daemon or lifecycle operation + removes registry artifacts only when generation evidence still matches its + observation. Creation uses an exclusive, dead-owner-recoverable id lock. + _refines: PTY-R02, PTY-R05._ +- **PTY.REG-R08 — External-readable events.** Each event is one JSONL + envelope with session, type, timestamp, and typed payload; external followers + can reopen after truncation and subscribe instead of polling metadata. + _refines: PTY-R06, PTY-R09._ diff --git a/docs/vrs/03-registry/spec.md b/docs/vrs/03-registry/spec.md new file mode 100644 index 0000000..824b402 --- /dev/null +++ b/docs/vrs/03-registry/spec.md @@ -0,0 +1,85 @@ +# Registry specification + +This document specifies registry identity, storage, observation, and ownership. +It builds on [requirements.md](./requirements.md). + +## Status + +Active. The complete public field and event schemas are maintained in +[the disk-layout reference](../../disk-layout.md). + +## Root and artifacts + +```text +$PTY_ROOT/ mode 0700 + .json tier 1 metadata + .events.jsonl tier 1 events + .sock tier 2 live socket + .pid tier 2 daemon pid + .lock tier 2 creation lock + .tmp.. ignored publication temporary + theme, gc.log tier 2 operator/UI state +``` + +`PTY_ROOT` is canonical. Deprecated `PTY_SESSION_DIR` is accepted only when the +canonical variable is absent and emits a one-time warning unless explicitly +silenced. When both exist, `PTY_ROOT` wins visibly. Every operation uses the +same resolution (PTY.REG-R01, R06). + +## Identity and resolution + +Stable ids use `[a-zA-Z0-9._-]+`, fit both the filename and the smallest +supported Unix socket path, and are protected by a per-id lock. Display names +allow printable presentation text but are not path material or identity. + +```text +reference + -> exact stable id match + -> exactly one displayName match + -> otherwise absent or explicit ambiguity +``` + +Repeated `--filter-tag key=value` predicates all must match. A reserved tag can +drive lifecycle or outer-tool bookkeeping, but tags remain metadata within one +registry (PTY.REG-R05). + +## Metadata publication and observation + +Metadata is pretty-printed JSON written to a randomized sibling temporary and +renamed into place. Readers see the old or new complete file. The public schema +includes opaque generation and daemon pid, launch definition, timestamps, exit +and last-screen state, tags/display name, and last writable attach time +(PTY.REG-R02, R06). + +Inventory combines metadata with bounded pid/socket probes: + +| State | Evidence | +| --- | --- | +| `running` | daemon alive and socket reachable | +| `exited` | no live daemon and recorded exit details | +| `vanished` | no live daemon and no recorded exit details | + +The inventory path performs no cleanup. `STATUS` queries a live daemon and +reports terminal, process, daemon, modes, uptime, and client connections. Client +rows contain no user identity; they expose readonly/writable role, requested +geometry, request sequence, and whether each dimension constrains the effective +grid (PTY.REG-R03–R04). + +## Events + +Each event line has `{ session, type, ts, ...payload }`. System types cover +terminal signals, lifecycle, exec, respawn/abandon/flapping, and metadata +changes. User types are namespaced `user.` and reject empty, +whitespace, control characters, and system-name collisions. + +Appends are serialized within a runtime. Periodic bounded truncation atomically +replaces the file with the latest 500 lines when it reaches 1,000, so followers +detect inode change and reopen. Session lifecycle cleanup removes the event file +with the session (PTY.REG-R08). + +## Ownership + +Metadata generation is opaque to readers. Daemon self-cleanup, explicit remove, +and reconciliation compare their observed generation before deleting. Creation +locks are exclusive and may be stolen only when the recorded owner process is +dead (PTY.REG-R07). diff --git a/docs/vrs/04-surfaces/01-cli-package/requirements.md b/docs/vrs/04-surfaces/01-cli-package/requirements.md new file mode 100644 index 0000000..e8faef0 --- /dev/null +++ b/docs/vrs/04-surfaces/01-cli-package/requirements.md @@ -0,0 +1,30 @@ +# CLI and package requirements + +> **Role.** Define the executable and distribution boundary. These requirements +> refine the parent surface contract. + +## Requirements + +- **PTY.SURF.CLI-R01 — Single CLI process.** `bin/pty` validates compiled output + and imports `dist/cli.js` in-process without spawning a forwarding child. + _refines: PTY.SURF-R04, PTY.SURF-R07._ +- **PTY.SURF.CLI-R02 — Inherited descriptor fidelity.** Every descriptor + inherited by the package entrypoint remains available to CLI features; the + launcher does not assume only stdin/stdout/stderr. _refines: PTY.SURF-R04._ +- **PTY.SURF.CLI-R03 — Direct signal ownership.** The OS-visible CLI process is + the process running command handlers, so termination signals do not depend on + a wrapper's forwarding or orphan a handler child. _refines: PTY.SURF-R04._ +- **PTY.SURF.CLI-R04 — Machine attach separation.** In machine mode, stdin and + stdout remain the controlling terminal, terminal events use only the selected + descriptor, diagnostics use stderr, and exactly one `EXIT` or `DETACH` outcome + is flushed before clean CLI completion. _refines: PTY.SURF-R03, + PTY.SURF-R04._ +- **PTY.SURF.CLI-R05 — Fail-closed v1 admission.** The descriptor is a valid + writable inherited fd greater than or equal to 3. Any emitted terminal + baseline begins with geometry then screen; a local detach may instead emit + its terminal outcome before the baseline. Unsupported server-event order or + EOF without a terminal outcome exits non-zero without silently degrading to + raw output. _refines: PTY.SURF-R03._ +- **PTY.SURF.CLI-R06 — Completion parity.** Required values such as the machine + stream fd are emitted as value-taking options in bash, fish, and zsh rather + than boolean switches. _refines: PTY.SURF-R05._ diff --git a/docs/vrs/04-surfaces/01-cli-package/spec.md b/docs/vrs/04-surfaces/01-cli-package/spec.md new file mode 100644 index 0000000..6edf7f9 --- /dev/null +++ b/docs/vrs/04-surfaces/01-cli-package/spec.md @@ -0,0 +1,47 @@ +# CLI and package specification + +This document specifies the executable distribution boundary. It builds on +[requirements.md](./requirements.md) and the parent +[surface specification](../spec.md). + +## Status + +Active. + +## Entrypoint + +```text +OS exec bin/pty + -> set process title + -> verify ../dist/cli.js exists + -> dynamic import in the same process + -> CLI main reads original argv and inherited fds +``` + +There is no wrapper child. Consequently a caller's fd 3, controlling terminal, +process id, and signals are properties of the process actually executing the +CLI (PTY.SURF.CLI-R01–R03). + +## Machine attach + +`attach --attach-stream-fd-v1 ` requires an inherited fd greater than +or equal to 3 and validates it with a zero-byte write before session resolution. +It retains stdin/stdout for raw mode, requested size, input, and resize events. +It writes reframed terminal-event packets only to the fd, diagnostics only to +stderr, and no terminal bytes to stdout (PTY.SURF.CLI-R04–R05). + +The adapter owns its stream view with `autoClose: false`: session exit writes +framed `EXIT`; Ctrl-\\ writes the existing empty `DETACH` frame and sends the +server-side detach request. Either outcome is flushed before clean completion, +while the caller still owns the descriptor. Descriptor errors, unsupported +initial order, transport loss, reconnect give-up, and EOF without either +terminal outcome are non-zero failures. An administrative session destruction +is such an outcome-less failure unless the adapter first observed process +`EXIT`; it does not synthesize a third outcome. + +## Schema and completions + +The command schema distinguishes free-valued and choice-valued options. +Completion generation uses this arity to make `--attach-stream-fd-v1` consume a +following value in bash, fish, and zsh. Tests execute generated bash completion +behavior in addition to checking text output (PTY.SURF.CLI-R06). diff --git a/docs/vrs/04-surfaces/02-libraries/requirements.md b/docs/vrs/04-surfaces/02-libraries/requirements.md new file mode 100644 index 0000000..cce0d4b --- /dev/null +++ b/docs/vrs/04-surfaces/02-libraries/requirements.md @@ -0,0 +1,27 @@ +# Library requirements + +> **Role.** Define the embedding and real-terminal testing surfaces. These +> requirements refine the parent surface contract. + +## Requirements + +- **PTY.SURF.LIB-R01 — Explicit module boundaries.** The package exports client, + server, protocol, keys, testing, and TUI modules from compiled JavaScript with + matching declarations. _refines: PTY.SURF-R07._ +- **PTY.SURF.LIB-R02 — Shared client semantics.** `SessionConnection`, direct + client helpers, and `attachPty` use the shared packet codec, stable-id + registry paths, and effective-geometry events. _refines: PTY.SURF-R01._ +- **PTY.SURF.LIB-R03 — Real-process tests.** `Session.spawn` starts a real PTY; + `Session.server` starts a real persistent runtime; neither substitutes a mock + terminal transport. _refines: PTY.SURF-R02._ +- **PTY.SURF.LIB-R04 — Reconstructable assertions.** Testing screenshots expose + trimmed lines, joined text, and ANSI serialization; bounded waits report the + current screen when their predicate fails. _refines: PTY.SURF-R02._ +- **PTY.SURF.LIB-R05 — Multi-client geometry fidelity.** Server-mode test + sessions and TUI panes distinguish requested and effective geometry, resize + their emulator before later data, and update across peer attach, resize, and + disconnect. _refines: PTY.SURF-R02._ +- **PTY.SURF.LIB-R06 — Daemon strategy equivalence.** Direct server-module + launch, explicit server-module override, and installed-CLI fallback carry the + same launch definition or reject unsupported options explicitly. _refines: + PTY.SURF-R01, PTY.SURF-R03._ diff --git a/docs/vrs/04-surfaces/02-libraries/spec.md b/docs/vrs/04-surfaces/02-libraries/spec.md new file mode 100644 index 0000000..3be8cee --- /dev/null +++ b/docs/vrs/04-surfaces/02-libraries/spec.md @@ -0,0 +1,51 @@ +# Library specification + +This document specifies exported embedding surfaces. It builds on +[requirements.md](./requirements.md) and the parent +[surface specification](../spec.md). + +## Status + +Active. The TUI module remains alpha. + +## Public modules + +| Export | Contract | +| --- | --- | +| `/client` | session discovery, lifecycle, connection, attach/peek/send/stats, events, ptyfile helpers | +| `/server` | embeddable `PtyServer` runtime | +| `/protocol` | packet constants, codecs, bounded streaming reader | +| `/keys` | named key and sequence parsing | +| `/testing` | real-PTY `Session` and screenshot types | +| `/tui` | alpha terminal rendering/input/widgets and PTY panes | + +`tsc -p tsconfig.build.json` produces `dist` JavaScript and declarations while +rewriting source `.ts` imports to `.js`. Development source remains runnable +with Node type stripping (PTY.SURF.LIB-R01). + +## Testing backends + +```text +Session.spawn -> direct node-pty child -> local headless terminal +Session.server -> PtyServer -> SessionConnection -> local headless terminal +``` + +Both backends send real key bytes and parse real terminal output. Screenshots +project the active terminal as lines/text/ANSI. `waitForText`, `waitForAbsent`, +and general `waitFor` repeatedly inspect that state until success or a bounded +diagnostic failure (PTY.SURF.LIB-R03–R04). + +Server mode supports attach, reconnect, peer clients, and resize. A `GEOMETRY` +event resizes the receiving terminal before affected screen/data; public +`rows`/`cols` report effective rather than merely requested dimensions. +`SessionConnection` and `attachPty` preserve the same rule +(PTY.SURF.LIB-R02, R05). + +## Daemon launch strategies + +An explicit server-module override wins, then an installed sibling +`dist/server.js`, then delegation to the installed `pty` CLI for bundled +consumers. Direct strategies carry serialized launch options. CLI fallback is +limited to the options its command surface can express and rejects an exact +environment map instead of silently changing its meaning +(PTY.SURF.LIB-R06). diff --git a/docs/vrs/04-surfaces/intuition.md b/docs/vrs/04-surfaces/intuition.md new file mode 100644 index 0000000..248d5dd --- /dev/null +++ b/docs/vrs/04-surfaces/intuition.md @@ -0,0 +1,22 @@ +# Surface intuition + +*For: CLI and package maintainers · Assumes: runtime, stream, and registry +contracts · Covers: adding an interface without creating another system* + +The safest surface is a thin adapter. A remote attach is still an attach. A +testing server is still a `PtyServer`. A TUI pane still consumes geometry before +screen bytes. This keeps difficult terminal ordering in one place. + +```text +human CLI --------+ +machine CLI ------+--> shared client/runtime modules --> one protocol +testing library --+ +TUI pane ---------+ +remote route -----+ +``` + +Process boundaries are part of an interface. A wrapper that spawns a child and +inherits only fds 0–2 silently breaks a caller-owned fd 3 even if every protocol +unit test passes. Loading the compiled CLI in the package-entrypoint process is +both simpler and more faithful: argv, signals, controlling terminal, and all +inherited descriptors arrive at the actual CLI. diff --git a/docs/vrs/04-surfaces/ontology.md b/docs/vrs/04-surfaces/ontology.md new file mode 100644 index 0000000..b373061 --- /dev/null +++ b/docs/vrs/04-surfaces/ontology.md @@ -0,0 +1,29 @@ +# Surface ontology + +Root terms are inherited from [../ontology.md](../ontology.md). + +## Language + +**Package entrypoint**: +The shipped `bin/pty` executable that selects compiled CLI code without adding +a second process or alternate argument contract. + +**Controlling terminal**: +The invoking terminal retained on stdin/stdout for raw input, size, and resize +events even when screen events are emitted on a machine descriptor. + +**Remote route**: +A fabric-provided ordered stream bridged to one ordinary local session socket. +It is transport composition, not a second session protocol. + +**Testing session**: +A real PTY-backed test handle using either a direct child backend or the +persistent server backend. + +**Terminal screenshot**: +A point-in-time projection of a testing terminal as trimmed lines, joined plain +text, and ANSI serialization. It is not the live protocol's screen baseline. + +**TUI toolkit**: +The alpha package surface for terminal layout, rendering, input, widgets, and +attaching a session as a pane. diff --git a/docs/vrs/04-surfaces/requirements.md b/docs/vrs/04-surfaces/requirements.md new file mode 100644 index 0000000..fdbb65f --- /dev/null +++ b/docs/vrs/04-surfaces/requirements.md @@ -0,0 +1,52 @@ +# Surface requirements + +> **Role.** Compose the runtime, stream, and registry through supported user and +> embedding boundaries. Every requirement refines a root `PTY-R*` requirement. + +## Assumptions + +- **PTY.SURF-A01 Node package:** The distributed CLI and libraries run on a + supported Node.js runtime with the package's native `node-pty` dependency. +- **PTY.SURF-A02 Trusted fabric peer:** Remote routing delegates peer transport + and authorization to `fabric`; `pty` receives an ordered local stream. + +## Acceptable tradeoffs + +- **PTY.SURF-T01 Alpha TUI toolkit:** `@compoundingtech/pty/tui` is a shipped but + alpha surface and may evolve under the pre-1.0 compatibility policy. +- **PTY.SURF-T02 CLI fallback for bundled embedders:** If an embedded client + cannot locate the sibling server module, daemon creation may delegate to the + installed `pty` CLI rather than materialize bundled source. + +## Requirements + +- **PTY.SURF-R01 — One behavioral core.** CLI and library operations call the + same runtime, protocol, registry, event, and lifecycle primitives rather than + defining parallel semantics. _refines: PTY-R08._ +- **PTY.SURF-R02 — Real terminal testing.** The testing library drives real PTY + processes and the same server protocol, exposes reconstructable text/ANSI + screenshots, and applies effective geometry before affected terminal bytes. + _refines: PTY-R03, PTY-R04, PTY-R08._ +- **PTY.SURF-R03 — Explicit capability failure.** A surface validates required + descriptors, refs, roots, and protocol order before mutation or output; an + older daemon that lacks a required machine-stream contract fails clearly + rather than silently degrading. _refines: PTY-R09._ +- **PTY.SURF-R04 — Descriptor-preserving package entrypoint.** The shipped + `bin/pty` runs the compiled CLI in the invoking process, preserving inherited + descriptors above stderr and delivering signals to the actual CLI process. + _refines: PTY-R08._ +- **PTY.SURF-R05 — Schema-consistent CLI.** Command help, parsing, and generated + bash/fish/zsh completions agree about flag arity and choices; stable ids and + ambiguous display references follow registry resolution. _refines: PTY-R07, + PTY-R08, PTY-R09._ +- **PTY.SURF-R06 — Protocol-preserving remote route.** Remote list returns a + structured control response; routed attach/peek/send hands the ordinary + per-session protocol through unchanged, including reconnect baselines and + machine-stream order. _refines: PTY-R03, PTY-R08._ +- **PTY.SURF-R07 — Buildable published surface.** Package exports resolve to + compiled `dist` modules with matching declarations, while TypeScript sources + remain directly runnable for development. Missing compiled CLI output fails + with an actionable error. _refines: PTY-R08, PTY-R09._ + +See [CLI and package](./01-cli-package/requirements.md) and +[libraries](./02-libraries/requirements.md) for concrete refinements. diff --git a/docs/vrs/04-surfaces/spec.md b/docs/vrs/04-surfaces/spec.md new file mode 100644 index 0000000..d349389 --- /dev/null +++ b/docs/vrs/04-surfaces/spec.md @@ -0,0 +1,54 @@ +# Surface specification + +This document specifies how the product and package surfaces compose lower +layers. It builds on [requirements.md](./requirements.md). + +## Status + +Active. The TUI toolkit is alpha; the package as a whole is pre-1.0. + +## Surface map + +```text +@compoundingtech/pty + +-- bin/pty + dist/cli.js operator CLI + +-- /client session and registry client API + +-- /server embeddable runtime + +-- /protocol packet codec + +-- /testing real-PTY test sessions + +-- /tui alpha terminal UI toolkit + `-- /keys key-name codec +``` + +The CLI imports the same modules exported to embedders. `Session.server`, +`SessionConnection`, and `attachPty` consume the same ordered geometry and +terminal frames as the CLI. No surface owns a second session state machine +(PTY.SURF-R01–R02). + +## Command routing + +The CLI schema defines commands, positionals, flags, repeatability, value mode, +and choices. Parsing, help, and generated completions consume that shape. +Before machine attach resolves a session, it validates that the requested +descriptor is an open writable inherited fd greater than or equal to 3. +Reference-taking commands use stable-id first, unambiguous-display-name second +resolution (PTY.SURF-R03, R05). + +## Remote composition + +`remote-serve --stdio` receives a newline-delimited JSON control request from a +trusted fabric route. `list` returns JSON. A routed command resolves the remote +reference, replies with an acknowledgment, then bridges residual and subsequent +bytes bidirectionally to the local session socket. The session protocol remains +unchanged across the bridge. Interactive remote attach may reconnect by dialing +a new route; a resolved session absence ends that loop (PTY.SURF-R06). + +The listening-socket form is transitional. The on-demand stdio form leaves +persistence and roaming to fabric and does not introduce a central pty daemon. + +## Child specifications + +- [CLI and package](./01-cli-package/spec.md) owns the executable artifact, + process boundary, descriptors, signals, help, and completions. +- [libraries](./02-libraries/spec.md) owns public module boundaries and the + testing/TUI embedding contracts. diff --git a/docs/vrs/intuition.md b/docs/vrs/intuition.md new file mode 100644 index 0000000..f77fb59 --- /dev/null +++ b/docs/vrs/intuition.md @@ -0,0 +1,30 @@ +# pty intuition + +*For: maintainers and embedders · Assumes: Unix PTYs and terminal escape +sequences · Covers: the system-wide mental model* + +The durable thing is a **session**, not the terminal window connected to it. +One daemon owns one child PTY and continuously parses the child's bytes into a +headless terminal. Clients come and go. The daemon keeps the process and the +screen model alive between them. + +```text +child <-> PTY <-> per-session runtime <-> zero or more clients + | + +-> atomic metadata + ordered events +``` + +Reattachment is state transfer, not log replay. A client first learns the +effective grid, then receives one screen image representing everything before +an exact parser cut, then receives bytes produced after that cut. This is why a +client can reconstruct colors, cursor position, alternate-screen applications, +and concurrent output without guessing. + +The filesystem registry is a separate projection. It gives sessions durable +identity, restart inputs, lifecycle guards, and cheap observation. It does not +replace the live stream, and the live stream does not become identity. + +The CLI and libraries are adapters around those same two boundaries. A useful +test of any new surface is therefore simple: does it preserve session lifetime, +ordered terminal state, effective geometry, and generation-safe lifecycle—or +does it explicitly decline the capability? diff --git a/docs/vrs/ontology.md b/docs/vrs/ontology.md new file mode 100644 index 0000000..162ff20 --- /dev/null +++ b/docs/vrs/ontology.md @@ -0,0 +1,45 @@ +# pty ontology + +Terms defined here are inherited by every descendant. Child ontologies add +only local terms. + +## Language + +**Session**: +A stable-id execution record consisting of a child process, its PTY and +terminal state, and its registry artifacts. A session can outlive every client. + +**Session runtime**: +The per-session daemon, child PTY, headless terminal emulator, and lifecycle +logic that maintain a session independently of clients. +_Avoid_: server (ambiguous with the child command), central daemon. + +**Client**: +One socket connection observing or interacting with a session. A client is +ephemeral and is never session identity. + +**Terminal state**: +The ordered result of parsing the child's terminal byte stream: grid, style, +cursor, terminal modes, alternate screen, and scrollback. +_Avoid_: log (a log cannot reconstruct this state). + +**Registry**: +One directory tree selected by `PTY_ROOT`, containing the identities and +artifacts for a set of sessions. Distinct registries are hard isolation; +filtered tags are not. + +**Stable id**: +The immutable, path-safe session name used for socket and registry filenames. +_Avoid_: display name, label. + +**Display name**: +Mutable, non-unique presentation metadata. It resolves as a convenience +reference only when exactly one session matches. + +**Generation**: +An opaque token identifying one daemon's ownership of a session's mutable +registry artifacts. It prevents stale cleanup from deleting a replacement. + +**Surface**: +A supported way to invoke or embed the system: CLI, package entrypoint, exported +library, testing API, or remote route. diff --git a/docs/vrs/requirements.md b/docs/vrs/requirements.md new file mode 100644 index 0000000..cb76ec6 --- /dev/null +++ b/docs/vrs/requirements.md @@ -0,0 +1,84 @@ +# pty requirements + +## Context + +These requirements define the durable contract of the `pty` project described +in the [README](../../README.md): persistent terminal sessions and the reusable +libraries built on the same terminal model. The README remains the product +purpose and user guide; this tree owns the testable system constraints. There +is intentionally no `vision.md` in this tree. + +Child requirements refine this contract by scoped ID: + +- [session runtime](./01-session-runtime/requirements.md) +- [session stream](./02-session-stream/requirements.md) +- [registry](./03-registry/requirements.md) +- [surfaces](./04-surfaces/requirements.md) + +## Assumptions + +- **PTY-A01 Unix host:** Supported hosts provide Unix PTYs, Unix-domain sockets, + process signals, and atomic same-filesystem rename. The supported products are + macOS and Linux. +- **PTY-A02 Trusted user boundary:** A registry belongs to one trusted OS user. + Socket and filesystem permissions are the access boundary; readonly clients + are a behavior mode, not an authorization mechanism. +- **PTY-A03 Terminal byte stream:** A child process expresses terminal state as + ordered bytes and terminal control sequences. Reconstructing that state + requires a terminal emulator, not line-oriented logging. + +## Acceptable tradeoffs + +- **PTY-T01 Per-session daemon:** Each persistent session pays the resource cost + of an independent daemon in exchange for failure isolation and no central + lifetime owner. +- **PTY-T02 Shared grid:** All writable clients share one effective PTY grid. + The smallest requested row and column dimensions win, preserving a complete + view for every writable client at the cost of reducing larger clients. +- **PTY-T03 Pre-1.0 evolution:** Public storage and package APIs may change + before 1.0 when the documented compatibility tier permits it. Changes remain + explicit and version-pinned consumers can retain the old contract. + +## Requirements + +### Must preserve sessions independently of observers + +- **PTY-R01 — Persistent execution.** A session's child process and terminal state + continue independently of the client that created, attached to, or detached + from it. +- **PTY-R02 — Isolated failure domains.** Failure or replacement of one session, + client, or control-plane invocation does not implicitly terminate unrelated + sessions. + +### Must preserve terminal meaning + +- **PTY-R03 — Reconstructable terminal.** A newly attached client can reconstruct + a causally valid terminal state, including style, cursor, modes, alternate + screen, scrollback, and subsequent output, without a byte-loss or reordering + window. +- **PTY-R04 — Deterministic shared geometry.** Concurrent writable clients produce + one explicit effective geometry, and every reconstructing client learns that + geometry in causal order with the terminal state it describes. Readonly + observation does not alter it. + +### Must make lifecycle and state inspectable + +- **PTY-R05 — Durable launch and lifecycle.** A preserved session retains enough + launch definition for an equivalent explicit or policy-driven restart, while + explicit removal and cleanup cannot delete a replacement generation. +- **PTY-R06 — Observational state.** Callers can inspect session identity, + lifecycle, metadata, events, clients, resources, and effective geometry + without attaching or mutating the session. +- **PTY-R07 — Explicit isolation and identity.** Registry selection and stable + session identity are explicit. Presentation labels and tags do not silently + become durable identity or hard isolation boundaries. + +### Must compose through supported surfaces + +- **PTY-R08 — Contract-equivalent surfaces.** The CLI, exported libraries, + testing API, local socket transport, machine attach stream, and remote route + preserve the same session, stream, geometry, and lifecycle contracts where + they expose those capabilities. +- **PTY-R09 — Bounded compatibility.** Protocol and storage readers reject + unbounded or structurally invalid input, while documented extension and + compatibility paths preserve older clients and metadata where safe. diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md new file mode 100644 index 0000000..023b305 --- /dev/null +++ b/docs/vrs/spec.md @@ -0,0 +1,58 @@ +# pty specification + +This document specifies the composition of the current `pty` system. It builds +on [requirements.md](./requirements.md). + +## Status + +Active. The implementation and executable tests are the behavioral evidence; +this tree is their durable contract map. + +## Scope + +This tree defines persistent session execution, terminal-state transport, +registry state, and the supported package surfaces. It does not define a window +manager, shell, multi-tenant security boundary, or orchestration policy for +products that use `pty`. + +## System composition + +```text +04 surfaces (CLI, package APIs, testing, remote) + | + +-----------------------+ + v v +02 ordered session stream 03 durable registry + | | + +-----------+-----------+ + v + 01 session runtime + | + v + child process + PTY +``` + +The dependency direction follows the numeric tree. The runtime owns the child +and terminal emulator. The stream projects ordered terminal state to clients. +The registry projects durable identity and observations. Surfaces compose these +contracts without redefining them. + +## Core invariants + +1. A session is identified by its stable registry id, not by a client, process + label, display name, or socket connection (PTY-R01, PTY-R07). +2. Terminal reconstruction is an ordered state transfer: effective geometry, + then one screen baseline, then post-baseline data or exit (PTY-R03, PTY-R04). +3. Durable metadata describes a launch and an observed generation; live status + is derived from metadata plus process/socket evidence (PTY-R05, PTY-R06). +4. Every surface either preserves the relevant underlying contract or rejects + the unsupported capability explicitly (PTY-R08, PTY-R09). + +## Subsystem ownership + +| Subsystem | Owns | Does not own | +| --- | --- | --- | +| [session runtime](./01-session-runtime/spec.md) | child, PTY, emulator, launch context, exit and restart primitives | client presentation, registry discovery | +| [session stream](./02-session-stream/spec.md) | framing, connection roles, synchronization, geometry and exit order | session identity, restart policy | +| [registry](./03-registry/spec.md) | roots, stable ids, metadata, events, inventory and cleanup ownership | terminal parsing, CLI rendering | +| [surfaces](./04-surfaces/spec.md) | CLI/package/API composition, packaging, remote routing | alternate semantics for the three lower layers | diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index b3e7564..521c4aa 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -7,6 +7,162 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, ".."); const docsPath = path.join(projectRoot, "docs", "testing.md"); +const vrsRoot = path.join(projectRoot, "docs", "vrs"); + +function collectMarkdown(dir: string): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const target = path.join(dir, entry.name); + return entry.isDirectory() + ? collectMarkdown(target) + : entry.isFile() && entry.name.endsWith(".md") + ? [target] + : []; + }); +} + +function collectVrsNodeDirectories(dir: string, companions: Set): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + if (!entry.isDirectory() || companions.has(entry.name)) return []; + const target = path.join(dir, entry.name); + return [target, ...collectVrsNodeDirectories(target, companions)]; + }); +} + +function verifyVrs(): void { + if (!fs.existsSync(vrsRoot)) return; + + const files = collectMarkdown(vrsRoot); + const errors: string[] = []; + const contents = new Map(files.map((file) => [file, fs.readFileSync(file, "utf-8")])); + const requirementDefinitions = new Map(); + const requirementIdsByFile = new Map(); + const companionDirectories = new Set([ + ".decisions", + ".experiments", + ".reference", + ".delta", + ".proposed", + ]); + + for (const file of files.filter((candidate) => path.basename(candidate) === "requirements.md")) { + const content = contents.get(file)!; + const definitionPattern = /^- \*\*(PTY(?:\.[A-Z]+)*-R\d+)\s+[^*]+\*\*/gm; + let definition: RegExpExecArray | null; + const ids: string[] = []; + while ((definition = definitionPattern.exec(content)) !== null) { + const id = definition[1]; + ids.push(id); + const prior = requirementDefinitions.get(id); + if (prior) errors.push(`duplicate requirement ${id}: ${prior} and ${file}`); + requirementDefinitions.set(id, file); + } + requirementIdsByFile.set(file, ids); + if (ids.length === 0) errors.push(`${file} defines no scoped PTY requirement IDs`); + const namespaces = new Set(ids.map((id) => id.slice(0, id.lastIndexOf("-R")))); + if (namespaces.size > 1) errors.push(`${file} mixes requirement namespaces`); + const sequence = ids.map((id) => Number(id.match(/-R(\d+)$/)?.[1])); + if (!sequence.every((number, index) => number === index + 1)) { + errors.push(`${file} requirement IDs must be sequential in document order`); + } + } + + for (const file of files) { + const content = contents.get(file)!; + const relative = path.relative(projectRoot, file); + + for (const entry of fs.readdirSync(path.dirname(file), { withFileTypes: true })) { + if ( + entry.isDirectory() && + !companionDirectories.has(entry.name) && + !/^\d{2}-[a-z0-9-]+$/.test(entry.name) + ) { + errors.push(`${relative}: subsystem directory ${entry.name} needs a numeric prefix`); + } + } + + const linkPattern = /\]\(([^)]+\.md)(?:#[^)]+)?\)/g; + let link: RegExpExecArray | null; + while ((link = linkPattern.exec(content)) !== null) { + if (/^[a-z]+:/i.test(link[1])) continue; + const target = path.resolve(path.dirname(file), link[1]); + if (!fs.existsSync(target)) errors.push(`${relative}: broken link ${link[1]}`); + } + + if (path.basename(file) === "spec.md") { + if (!content.includes("[requirements.md](./requirements.md)")) { + errors.push(`${relative}: spec must build on its sibling requirements.md`); + } + if (!content.includes("## Status")) errors.push(`${relative}: spec must declare Status`); + } + if (path.basename(file) === "ontology.md" && !content.includes("## Language")) { + errors.push(`${relative}: ontology must define a Language section`); + } + if (path.basename(file) === "intuition.md" && !/\*For:[^*]+ ·\s*Assumes:[^*]+ ·\s*Covers:[^*]+\*/.test(content)) { + errors.push(`${relative}: intuition must declare For, Assumes, and Covers`); + } + } + + const requirementsFiles = files.filter((file) => path.basename(file) === "requirements.md"); + for (const file of requirementsFiles) { + if (file === path.join(vrsRoot, "requirements.md")) continue; + const content = contents.get(file)!; + const parentFile = path.join(path.dirname(path.dirname(file)), "requirements.md"); + const parentContent = contents.get(parentFile); + if (!parentContent) { + errors.push(`${path.relative(projectRoot, file)}: missing direct parent requirements.md`); + continue; + } + const parentIds = new Set(requirementIdsByFile.get(parentFile) ?? []); + const namespace = requirementIdsByFile.get(file)?.[0]?.replace(/-R\d+$/, ""); + const parentNamespace = requirementIdsByFile.get(parentFile)?.[0]?.replace(/-R\d+$/, ""); + if (namespace && parentNamespace && !new RegExp(`^${parentNamespace.replaceAll(".", "\\.")}\\.[A-Z]+$`).test(namespace)) { + errors.push(`${path.relative(projectRoot, file)}: namespace ${namespace} must extend direct parent ${parentNamespace}`); + } + const requirementBlocks = content.split(/(?=^- \*\*PTY(?:\.[A-Z]+)*-R\d+\s+)/m).slice(1); + for (const block of requirementBlocks) { + const id = block.match(/^- \*\*(PTY(?:\.[A-Z]+)*-R\d+)\s+/)?.[1]; + const refinementClause = block.match(/_refines:\s*([^_]+?)\._/s)?.[1]; + const refinementIds = refinementClause + ? [...refinementClause.matchAll(/PTY(?:\.[A-Z]+)*-R\d+/g)].map((match) => match[0]) + : []; + if (id && refinementIds.length === 0) { + errors.push(`${path.relative(projectRoot, file)}: ${id} must declare _refines:_`); + } else if (id && !refinementIds.every((refinement) => parentIds.has(refinement))) { + errors.push(`${path.relative(projectRoot, file)}: ${id} may refine only direct-parent requirements`); + } + } + } + + const nodeDirs = new Set([ + vrsRoot, + ...collectVrsNodeDirectories(vrsRoot, companionDirectories), + ]); + for (const dir of nodeDirs) { + for (const required of ["requirements.md", "spec.md"]) { + if (!fs.existsSync(path.join(dir, required))) { + errors.push(`${path.relative(projectRoot, dir)}: VRS node is missing ${required}`); + } + } + } + + for (const [file, content] of contents) { + for (const match of content.matchAll(/PTY(?:\.[A-Z]+)*-R\d+/g)) { + if (!requirementDefinitions.has(match[0])) { + errors.push(`${path.relative(projectRoot, file)}: unknown requirement reference ${match[0]}`); + } + } + } + + if (errors.length > 0) { + console.error(`VRS verification failed:\n${errors.map((error) => `- ${error}`).join("\n")}`); + process.exit(1); + } + console.log(`Verified structural shape of ${files.length} VRS documents and ${requirementDefinitions.size} requirement IDs`); +} + +verifyVrs(); + +if (process.argv.includes("--vrs-only")) process.exit(0); const content = fs.readFileSync(docsPath, "utf-8"); diff --git a/src/server.ts b/src/server.ts index 1daf96f..4254a0b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -116,7 +116,7 @@ export interface ServerOptions { * * Mutually exclusive with `isolateEnv` / `extraEnv` / `unsetEnv` — passing * `env` together with inherited-environment policy throws. Use this when - * the caller wants total control of the child environment (e.g., a + * the caller wants exact ordinary environment keys (e.g., a * launcher shell that injects a shim tmux on `PATH`). */ env?: Record; } @@ -142,21 +142,21 @@ const ISOLATED_ENV_ALLOWLIST = new Set([ * Shift+Enter is indistinguishable from Enter. */ const DEFAULT_CHILD_TERM = "xterm-256color"; -/** Apply the TERM default in-place after the env has been assembled. Never - * overrides an explicit value — only fills in when it's absent. */ +/** Apply the TERM default in-place after the env has been assembled. A + * nonempty terminal name is preserved; absence and empty both select the + * runtime default. */ function ensureChildTerm(env: Record): void { if (!env.TERM) env.TERM = DEFAULT_CHILD_TERM; } function buildChildEnv(options: ServerOptions): Record { - // Mutual exclusion: `env` (explicit, verbatim) can't be combined with the - // inherited-environment policy path. If you want total control you pass - // `env`; otherwise isolation/removals/assignments compose explicitly. Picking - // one implicitly would hide intent. + // Mutual exclusion: the replacement-base `env` can't be combined with the + // inherited-environment policy path. Otherwise isolation, removals, and + // assignments compose explicitly. Picking one implicitly would hide intent. if (options.env && (options.isolateEnv || options.extraEnv || options.unsetEnv?.length)) { throw new Error( "ServerOptions.env is mutually exclusive with isolateEnv/extraEnv/unsetEnv. " + - "Use env for verbatim control, or inherited environment policy options — not both." + "Use env as a replacement base, or inherited environment policy options — not both." ); } @@ -526,7 +526,7 @@ export class PtyServer { // NOTE: intentionally no `name:` option here — node-pty's `name` // unconditionally clobbers env.TERM, which would hide any TERM the // caller inherited or set explicitly. `buildChildEnv` guarantees - // childEnv.TERM is populated (defaulting to xterm-256color if absent), + // childEnv.TERM is populated (defaulting if absent or empty), // so node-pty will pick it up naturally. Was `name: "xterm-256color"` // before; removing it lets inherited values like `xterm-kitty` flow // through and lets TUIs negotiate the richer capabilities they allow. diff --git a/src/spawn.ts b/src/spawn.ts index 0f77ac9..ff2788e 100644 --- a/src/spawn.ts +++ b/src/spawn.ts @@ -41,9 +41,10 @@ export interface SpawnDaemonOptions { extraEnv?: Record; /** Environment keys removed from inheritance before `extraEnv` is applied. */ unsetEnv?: string[]; - /** Use this env dict verbatim for the spawned child — no inheritance from - * the daemon's `process.env`, no allow-list. `PTY_SESSION` is always - * injected on top so nesting detection and `pty exec` keep working. + /** Use this env dict as the child environment's replacement base — no + * inheritance from the daemon's `process.env`, no allow-list. Runtime + * invariants still force `PTY_SESSION` and normalize an absent or empty + * `TERM`. * * Mutually exclusive with `isolateEnv` / `extraEnv` / `unsetEnv` — passing * `env` together with inherited-environment policy will throw at startup. */ @@ -140,7 +141,7 @@ export async function spawnDaemon(options: SpawnDaemonOptions): Promise { if (options.env && (options.isolateEnv || options.extraEnv || options.unsetEnv?.length)) { throw new Error( "SpawnDaemonOptions.env is mutually exclusive with isolateEnv/extraEnv/unsetEnv. " + - "Use env for verbatim control, or inherited environment policy options — not both.", + "Use env as a replacement base, or inherited environment policy options — not both.", ); } const strategy = resolveSpawnStrategy(); From 35c589996d0d1b607ba35bdae01c5b36759a4956 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:50:58 +0200 Subject: [PATCH 2/5] docs: reduce VRS to core contract --- DEVELOPMENT.md | 33 +-- README.md | 4 +- docs/client.md | 10 +- docs/disk-layout.md | 2 +- .../01-launch-context/requirements.md | 32 --- .../01-launch-context/spec.md | 47 ---- .../02-lifecycle/requirements.md | 27 --- .../01-session-runtime/02-lifecycle/spec.md | 55 ----- docs/vrs/01-session-runtime/intuition.md | 24 -- docs/vrs/01-session-runtime/ontology.md | 32 --- docs/vrs/01-session-runtime/requirements.md | 55 ----- docs/vrs/01-session-runtime/spec.md | 56 ----- .../01-synchronization/requirements.md | 28 --- .../01-synchronization/spec.md | 40 ---- .../02-geometry/requirements.md | 27 --- .../vrs/02-session-stream/02-geometry/spec.md | 45 ---- docs/vrs/02-session-stream/intuition.md | 25 --- docs/vrs/02-session-stream/ontology.md | 36 --- docs/vrs/02-session-stream/requirements.md | 63 ------ docs/vrs/02-session-stream/spec.md | 90 -------- docs/vrs/03-registry/intuition.md | 24 -- docs/vrs/03-registry/ontology.md | 32 --- docs/vrs/03-registry/requirements.md | 52 ----- docs/vrs/03-registry/spec.md | 85 ------- .../01-cli-package/requirements.md | 30 --- docs/vrs/04-surfaces/01-cli-package/spec.md | 47 ---- .../04-surfaces/02-libraries/requirements.md | 27 --- docs/vrs/04-surfaces/02-libraries/spec.md | 51 ----- docs/vrs/04-surfaces/intuition.md | 22 -- docs/vrs/04-surfaces/ontology.md | 29 --- docs/vrs/04-surfaces/requirements.md | 52 ----- docs/vrs/04-surfaces/spec.md | 54 ----- docs/vrs/intuition.md | 30 --- docs/vrs/ontology.md | 45 ---- docs/vrs/requirements.md | 144 ++++++------ docs/vrs/spec.md | 209 ++++++++++++++---- scripts/verify-docs.ts | 165 +++----------- src/server.ts | 18 +- src/spawn.ts | 9 +- 39 files changed, 307 insertions(+), 1549 deletions(-) delete mode 100644 docs/vrs/01-session-runtime/01-launch-context/requirements.md delete mode 100644 docs/vrs/01-session-runtime/01-launch-context/spec.md delete mode 100644 docs/vrs/01-session-runtime/02-lifecycle/requirements.md delete mode 100644 docs/vrs/01-session-runtime/02-lifecycle/spec.md delete mode 100644 docs/vrs/01-session-runtime/intuition.md delete mode 100644 docs/vrs/01-session-runtime/ontology.md delete mode 100644 docs/vrs/01-session-runtime/requirements.md delete mode 100644 docs/vrs/01-session-runtime/spec.md delete mode 100644 docs/vrs/02-session-stream/01-synchronization/requirements.md delete mode 100644 docs/vrs/02-session-stream/01-synchronization/spec.md delete mode 100644 docs/vrs/02-session-stream/02-geometry/requirements.md delete mode 100644 docs/vrs/02-session-stream/02-geometry/spec.md delete mode 100644 docs/vrs/02-session-stream/intuition.md delete mode 100644 docs/vrs/02-session-stream/ontology.md delete mode 100644 docs/vrs/02-session-stream/requirements.md delete mode 100644 docs/vrs/02-session-stream/spec.md delete mode 100644 docs/vrs/03-registry/intuition.md delete mode 100644 docs/vrs/03-registry/ontology.md delete mode 100644 docs/vrs/03-registry/requirements.md delete mode 100644 docs/vrs/03-registry/spec.md delete mode 100644 docs/vrs/04-surfaces/01-cli-package/requirements.md delete mode 100644 docs/vrs/04-surfaces/01-cli-package/spec.md delete mode 100644 docs/vrs/04-surfaces/02-libraries/requirements.md delete mode 100644 docs/vrs/04-surfaces/02-libraries/spec.md delete mode 100644 docs/vrs/04-surfaces/intuition.md delete mode 100644 docs/vrs/04-surfaces/ontology.md delete mode 100644 docs/vrs/04-surfaces/requirements.md delete mode 100644 docs/vrs/04-surfaces/spec.md delete mode 100644 docs/vrs/intuition.md delete mode 100644 docs/vrs/ontology.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2cc9892..f8c2ea7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -25,7 +25,6 @@ npm run typecheck # typecheck with tsc (no emit) npm test # run all tests once npm run test:watch # run tests in watch mode npm run verify-docs # run executable examples in docs/testing.md -node scripts/verify-docs.ts --vrs-only # validate the VRS hierarchy # Usage (during development — run the TS source directly with Node) node --experimental-strip-types src/cli.ts run -- [args...] @@ -48,10 +47,6 @@ Source is written in TypeScript with `.ts` import extensions. `npm run build` co ## Architecture -The hierarchical durable contract is maintained in -[`docs/vrs`](docs/vrs/spec.md); this guide explains the implementation and -development workflow. - ``` ┌─────────────────────────────────────────────┐ │ Daemon (one per session) │ @@ -85,21 +80,13 @@ Binary packets over Unix sockets: `[type: uint8][length: uint32BE][payload]` |------|----|-----------|---------| | DATA | 0 | Both | Raw terminal bytes | | ATTACH | 1 | Client → Server | `[rows: uint16BE, cols: uint16BE]` (4 bytes) | -| DETACH | 2 | Client → Server; machine adapter → consumer | Empty | +| DETACH | 2 | Client → Server | Empty | | RESIZE | 3 | Client → Server | `[rows: uint16BE, cols: uint16BE]` (4 bytes) | | EXIT | 4 | Server → Client | `[exitCode: int32BE]` (4 bytes) | | SCREEN | 5 | Server → Client | ANSI escape sequences (string) | -| PEEK | 6 | Client → Server | Flags: plain bit 0, full-scrollback bit 1 | -| STATUS | 7 | Both | Empty request or JSON response | -| GEOMETRY | 10 | Server → Client | `[rows: uint16BE, cols: uint16BE]` (4 bytes) | - -`PacketReader` handles streaming reassembly of partial reads and rejects a -declared payload above 32 MiB. Decoders retain legacy fallbacks for truncated -size and exit payloads; command-specific surfaces validate stronger contracts. -Unknown bounded message types are ignored by the server. Each valid `ATTACH` or -recognized `PEEK` that emits terminal state starts with `GEOMETRY`, then an -ordered `SCREEN` baseline, then live `DATA` or `EXIT`. A local machine detach -may instead emit `DETACH` before the baseline. +| PEEK | 6 | Client → Server | Empty | + +`PacketReader` handles streaming reassembly of partial reads. Decoders gracefully handle truncated payloads (defaults for size, -1 for exit code). Unknown message types are silently ignored by the server. ## Key Design Decisions @@ -115,13 +102,9 @@ We avoid TS enums because they emit runtime code that can't be type-stripped. In The PTY can only be one size. If a peek client's terminal size were used, it could reflow the session — imagine vim at 120x40 suddenly becoming 40x20 because someone peeked from their phone. Readonly clients are excluded from size negotiation entirely. They see whatever fits; the active user's layout is never disrupted. -### Smallest writable client wins for size +### Last attached client wins for size -When multiple interactive (non-peek) clients are connected, the PTY uses the -minimum requested row count and minimum requested column count independently. -This guarantees that every writable client can represent the complete shared -grid. Readonly clients receive effective-geometry updates but never constrain -the size. +When multiple interactive (non-peek) clients are connected, the most recently attached client's terminal size is used for the PTY. This is simple and predictable. An alternative would be minimum dimensions across all clients, but that punishes the primary user when a smaller client connects. ### xterm-headless as the screen buffer @@ -185,7 +168,6 @@ npm test # run once (or: pty test) npm run test:watch # watch mode (or: pty test watch) npx vitest run -t "peek" # run tests matching "peek" npm run verify-docs # run executable examples in docs/testing.md -node scripts/verify-docs.ts --vrs-only # validate the VRS hierarchy ``` ### node-pty on macOS @@ -222,9 +204,8 @@ tests/ tui.test.ts docs/ testing.md Testing library documentation (with executable examples) - vrs/ Hierarchical system requirements and specification scripts/ - verify-docs.ts Validates VRS structure and runs doc examples via vitest + verify-docs.ts Extracts and runs doc examples via vitest completions/ pty.bash Bash tab completion pty.zsh Zsh tab completion diff --git a/README.md b/README.md index 9c8bdfb..4205e36 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Persistent terminal sessions. Run a process, detach, reconnect later. From anywh Uses [@xterm/headless](https://github.com/xtermjs/xterm.js/tree/master/headless) internally. -The durable system contract and subsystem map live in +The durable system contract lives in [docs/vrs](docs/vrs/spec.md). ## Install @@ -324,7 +324,7 @@ The values are overlaid on the session child's inherited environment before its Direct launches can also persist removals from the inherited environment with repeatable `pty run --unset-env KEY`. Removals are applied before `--env` overlays, so an explicit assignment wins when both mention the same key, regardless of flag order. Both policies survive manual and permanent restart. Metadata created before `unsetEnv` was introduced retains the historical ambient-inheritance behavior. -Two child invariants are applied after that policy: `PTY_SESSION` is always set to the session's stable id, and node-pty treats `TERM` as terminal capability metadata. An absent or empty `TERM` selects the runtime's `xterm-256color` terminal name; a nonempty value is preserved. Consequently, `--unset-env PTY_SESSION` cannot remove the session marker, and `--unset-env TERM` or `--env TERM=` selects the default rather than leaving `TERM` absent. Ordinary environment assignments, including empty values such as `NO_COLOR=`, remain exact. +Two child invariants are applied after that policy: `PTY_SESSION` is always set to the session's stable id, and an absent `TERM` receives the existing `xterm-256color` default. Consequently, `--unset-env PTY_SESSION` cannot remove the session marker, and `--unset-env TERM` selects the default rather than leaving `TERM` absent. An explicit `--env TERM=...` assignment is preserved. ### Permanent sessions diff --git a/docs/client.md b/docs/client.md index b203bf2..beacc56 100644 --- a/docs/client.md +++ b/docs/client.md @@ -187,16 +187,14 @@ interface SpawnDaemonOptions { isolateEnv?: boolean; // inherit only the safe allow-list extraEnv?: Record; // explicit assignments applied last unsetEnv?: string[]; // inherited keys removed before assignments - env?: Record; // replacement base; PTY_SESSION/TERM invariants still apply + env?: Record; // exact child env; mutually exclusive with the above } ``` `unsetEnv` removals run before `extraEnv` assignments. The server then forces -`PTY_SESSION` to the stable session id. Node-pty treats `TERM` as terminal -capability metadata: an absent or empty value selects the runtime's -`xterm-256color` terminal name, while a nonempty value is preserved. Naming -either key in `unsetEnv` does not suppress those invariants. Ordinary -assignments retain exact values, including an empty `NO_COLOR`. +`PTY_SESSION` to the stable session id and fills an absent `TERM` with +`xterm-256color`; naming either key in `unsetEnv` does not suppress those +invariants. An explicit `extraEnv.TERM` value is preserved. ### `resolveCommand(cmd: string): string` diff --git a/docs/disk-layout.md b/docs/disk-layout.md index 19b49d9..bab59ef 100644 --- a/docs/disk-layout.md +++ b/docs/disk-layout.md @@ -58,7 +58,7 @@ Pretty-printed JSON. Source of truth: `SessionMetadata` in `src/sessions.ts`. isolateEnv?: boolean; extraEnv?: { [k: string]: string }; // explicit inherited-env overlay (`--env`) unsetEnv?: string[]; // inherited env keys removed before `extraEnv` - env?: { [k: string]: string }; // replacement base; runtime invariants still apply + env?: { [k: string]: string }; // exact child env for programmatic callers createdAt: string; // ISO 8601 exitCode?: number; // present after clean exit exitedAt?: string; diff --git a/docs/vrs/01-session-runtime/01-launch-context/requirements.md b/docs/vrs/01-session-runtime/01-launch-context/requirements.md deleted file mode 100644 index 2cefe9b..0000000 --- a/docs/vrs/01-session-runtime/01-launch-context/requirements.md +++ /dev/null @@ -1,32 +0,0 @@ -# Launch context requirements - -> **Role.** Preserve the complete child launch across runtime creation and -> restart. These requirements refine the parent session-runtime contract. - -## Requirements - -- **PTY.RUN.ENV-R01 — Complete launch record.** Persisted launches retain - command, args, display command, cwd, initial rows and columns, lifetime flags, - tags, display name, and the chosen environment mode. _refines: PTY.RUN-R04._ -- **PTY.RUN.ENV-R02 — Exclusive environment modes.** A caller chooses either an - explicit replacement environment base or - inherited/isolate-plus-removals-and-assignments; combining them is rejected - before spawn. Exactness applies to ordinary caller-owned keys, subject to the - runtime-owned invariants below. _refines: PTY.RUN-R05._ -- **PTY.RUN.ENV-R03 — Ordered inherited policy.** In inherited and isolated - modes, named removals are applied before explicit assignments, so assignment - wins independently of CLI flag order. _refines: PTY.RUN-R05._ -- **PTY.RUN.ENV-R04 — Runtime invariants.** `PTY_SESSION` is always the stable - id. `TERM` is terminal capability metadata: an absent or empty value - selects the runtime's `xterm-256color` terminal name, while a nonempty value - is preserved through node-pty's public terminal-name contract. Ordinary keys - retain exact assigned values, including an empty `NO_COLOR`. _refines: - PTY.RUN-R05._ -- **PTY.RUN.ENV-R05 — Restart durability.** Explicit restart and metadata-based - permanent respawn preserve environment removals and assignments. A - manifest-backed permanent respawn re-reads its manifest declaration, while a - missing or unreadable manifest falls back to persisted metadata. _refines: - PTY.RUN-R04._ -- **PTY.RUN.ENV-R06 — Historical compatibility.** Metadata without an - `unsetEnv` field retains historical ambient inheritance rather than gaining - an implicit removal policy. _refines: PTY.RUN-R04._ diff --git a/docs/vrs/01-session-runtime/01-launch-context/spec.md b/docs/vrs/01-session-runtime/01-launch-context/spec.md deleted file mode 100644 index 1393bc5..0000000 --- a/docs/vrs/01-session-runtime/01-launch-context/spec.md +++ /dev/null @@ -1,47 +0,0 @@ -# Launch context specification - -This document specifies launch-definition assembly and persistence. It builds -on [requirements.md](./requirements.md) and the parent -[runtime specification](../spec.md). - -## Status - -Active. - -## Environment modes - -```text -exact mode: copy env -> PTY_SESSION -> normalize TERM if absent or empty - -inherited mode: process.env - -> remove PTY_SERVER_CONFIG - -> unsetEnv[] - -> extraEnv{} - -> PTY_SESSION - -> normalize TERM if absent or empty - -isolated mode: allowlisted process.env + LC_* - -> unsetEnv[] -> extraEnv{} - -> PTY_SESSION -> normalize TERM if absent or empty -``` - -Passing `env` together with `isolateEnv`, `extraEnv`, or a non-empty `unsetEnv` -is invalid in both `SpawnDaemonOptions` and `ServerOptions` -(PTY.RUN.ENV-R02–R04). - -The environment map is exact for ordinary caller-owned keys: removal removes, -assignment wins, and empty values such as `NO_COLOR=` remain empty. Two keys are -runtime-owned instead. `PTY_SESSION` is forced to the stable session id, and -node-pty interprets `TERM` as its terminal-name capability. The runtime selects -`xterm-256color` when it is absent or empty; a nonempty value is preserved. - -`SessionMetadata` stores `rows`, `cols`, `ephemeral`, `isolateEnv`, `extraEnv`, -`unsetEnv`, and exact `env` alongside the command fields. Operator restart and -metadata fallback pass them back to `spawnDaemon`. Manifest-backed permanent -respawn re-reads command, cwd, tags, and assignments from `pty.toml`; unreadable -or absent definitions use the recorded values (PTY.RUN.ENV-R01, R05–R06). - -The operator restart path may scrub explicitly named variables from the -daemon's own inherited environment before spawn. This prevents the restarter's -ambient identity from becoming undeclared child state; it does not alter the -persisted child policy. diff --git a/docs/vrs/01-session-runtime/02-lifecycle/requirements.md b/docs/vrs/01-session-runtime/02-lifecycle/requirements.md deleted file mode 100644 index ef67096..0000000 --- a/docs/vrs/01-session-runtime/02-lifecycle/requirements.md +++ /dev/null @@ -1,27 +0,0 @@ -# Lifecycle requirements - -> **Role.** Define exit, preservation, cleanup, and restart ownership for one -> session. These requirements refine the parent session-runtime contract. - -## Requirements - -- **PTY.RUN.LIFE-R01 — Derived lifecycle state.** `running`, `exited`, and - `vanished` are derived from process/socket evidence and exit metadata, not a - mutable stored status field. _refines: PTY.RUN-R06._ -- **PTY.RUN.LIFE-R02 — Ordered finalization.** The runtime drains child output, - records one exit result and final screen lines, emits one exit event, then - applies preservation or reap policy. _refines: PTY.RUN-R06._ -- **PTY.RUN.LIFE-R03 — Explicit policy precedence.** Explicit removal wins; - `keep=true` forces preservation; ephemeral forces reap except when keep wins; - permanent and explicit kill preserve state for restart or inspection. - _refines: PTY.RUN-R06, PTY.RUN-R07._ -- **PTY.RUN.LIFE-R04 — Generation-safe mutation.** Cleanup, replacement, and - permanent respawn compare the observed generation while holding the per-id - creation lock. A changed generation remains untouched. _refines: - PTY.RUN-R07._ -- **PTY.RUN.LIFE-R05 — Stateless permanent reconciliation.** `gc` can respawn a - dead permanent session, reap parent-orphans or abandoned sessions, and hold a - repeatedly fast-failing session as flapping. A dry run reports the same plan - without lifecycle writes. _refines: PTY.RUN-R07._ -- **PTY.RUN.LIFE-R06 — Observational purity.** Listing and state reads do not - trigger cleanup, respawn, or metadata repair. _refines: PTY.RUN-R07._ diff --git a/docs/vrs/01-session-runtime/02-lifecycle/spec.md b/docs/vrs/01-session-runtime/02-lifecycle/spec.md deleted file mode 100644 index aeab774..0000000 --- a/docs/vrs/01-session-runtime/02-lifecycle/spec.md +++ /dev/null @@ -1,55 +0,0 @@ -# Lifecycle specification - -This document specifies session lifecycle transitions. It builds on -[requirements.md](./requirements.md) and the parent -[runtime specification](../spec.md). - -## Status - -Active. - -## State and actions - -```text - child exits cleanly -running ------------------------> exited - | | - | daemon disappears | restart / permanent gc - v v -vanished ----------------------> running (new generation) - | ^ - +------- explicit cleanup -------+ -``` - -`exited` requires recorded `exitCode`/`exitedAt`; a dead daemon without that -record is `vanished`. Both are non-live and may be inspected, removed, swept, or -used as restart input. `listSessions` derives the state and remains -observational (PTY.RUN.LIFE-R01, R06). - -## Exit policy - -| Condition | Result | -| --- | --- | -| explicit `rm` | remove after generation-safe daemon shutdown | -| `keep=true` | preserve until explicit `rm` | -| `--ephemeral` | reap on shutdown unless keep is set | -| `strategy=permanent` | preserve for reconciliation unless ephemeral | -| explicit `kill` | stop and preserve unless ephemeral | -| ordinary exit | configured `PTY_REAP_ON_EXIT`, default reap | - -Before applying the table, the runtime's public exit callback is ordered after -the PTY read stream, terminal metadata is snapshotted, and `session_exit` is -emitted (PTY.RUN.LIFE-R02–R03). - -## Ownership and reconciliation - -A stable id has an exclusive creation lock. Cleanup checks the opaque metadata -generation it observed before deleting socket or all artifacts. Permanent -respawn holds that lock across compare, stale cleanup, and replacement socket -publication. A replacement generation therefore cannot be deleted by an older -daemon or stale `gc` plan (PTY.RUN.LIFE-R04). - -`gc` applies parent-orphan removal before permanent respawn, optionally detects -cwd disappearance or idle abandonment, tracks bounded fast-failure state, and -then sweeps eligible finished records. `--dry-run` plans without mutation -(PTY.RUN.LIFE-R05). diff --git a/docs/vrs/01-session-runtime/intuition.md b/docs/vrs/01-session-runtime/intuition.md deleted file mode 100644 index f69d1c0..0000000 --- a/docs/vrs/01-session-runtime/intuition.md +++ /dev/null @@ -1,24 +0,0 @@ -# Session runtime intuition - -*For: runtime maintainers · Assumes: the root pty model · Covers: why one daemon -owns one session* - -A session survives because its daemon is detached from the command that asked -for it. The daemon is deliberately small in responsibility: own one child PTY, -maintain one terminal model, serve clients, and finalize one lifecycle. - -```text -launcher exits daemon exits - | | - v v -session continues child result + policy finalize -``` - -Restart is not “run the same-looking command.” It replays a complete launch -definition. Environment removals matter as much as assignments: if `NO_COLOR` -was deliberately absent, inheriting it from a later operator shell would change -the program even though the command line stayed identical. - -Cleanup is likewise about ownership, not filenames. A stale daemon may see the -same stable id after a replacement has started. The opaque generation turns -“delete this name” into “delete this name only if it is still mine.” diff --git a/docs/vrs/01-session-runtime/ontology.md b/docs/vrs/01-session-runtime/ontology.md deleted file mode 100644 index de35ab8..0000000 --- a/docs/vrs/01-session-runtime/ontology.md +++ /dev/null @@ -1,32 +0,0 @@ -# Session runtime ontology - -Root terms are inherited from [../ontology.md](../ontology.md). - -## Language - -**Launch definition**: -The complete persisted input needed to start the child equivalently: command, -arguments, display command, working directory, initial geometry, lifetime -flags, tags, display name, and environment policy. - -**Inherited environment policy**: -The ordered transformation `base -> removals -> assignments -> runtime -invariants`. It is distinct from an exact environment map. - -**Ordinary environment key**: -A caller-owned child variable whose assigned value, including an empty value, -is preserved. `PTY_SESSION` is runtime identity and `TERM` is terminal -capability metadata, so neither is ordinary launch data. - -**Permanent session**: -A preserved session tagged `strategy=permanent` whose absent or dead runtime is -eligible for explicit `gc` reconciliation. -_Avoid_: immortal session; abandonment and flapping policy can still stop it. - -**Reap**: -Removal of a finished session's registry artifacts according to exit policy. -_Avoid_: kill (termination and removal are distinct lifecycle actions). - -**Generation-owned cleanup**: -Deletion that proceeds only while the target metadata still belongs to the -daemon generation or observation used to authorize it. diff --git a/docs/vrs/01-session-runtime/requirements.md b/docs/vrs/01-session-runtime/requirements.md deleted file mode 100644 index 1f0975b..0000000 --- a/docs/vrs/01-session-runtime/requirements.md +++ /dev/null @@ -1,55 +0,0 @@ -# Session runtime requirements - -> **Role.** The execution realization of the root -> [session contract](../requirements.md): one independently surviving daemon, -> child PTY, and terminal model per session. Every requirement refines a -> `PTY-R*` requirement. - -## Assumptions - -- **PTY.RUN-A01 Child semantics:** The spawned command obeys ordinary Unix PTY, - signal, and exit-status semantics. -- **PTY.RUN-A02 Recoverable declaration:** A permanent session backed by a - readable `pty.toml` may take its next launch declaration from that manifest; - otherwise persisted metadata is the last-known-good declaration. - -## Acceptable tradeoffs - -- **PTY.RUN-T01 Bounded startup wait:** Daemon startup has a finite socket-ready - wait. Immediate process failure is surfaced earlier, while a living but slow - daemon may consume the full bound. -- **PTY.RUN-T02 Stateless permanent reconciliation:** Permanent respawn has no - resident supervisor. `pty gc` invocations provide cadence and rate limiting. - -## Requirements - -- **PTY.RUN-R01 — Client-independent lifetime.** The daemon and child continue - after the creating or attached client exits. Binding daemon lifetime to a - spawner is explicit opt-in. _refines: PTY-R01._ -- **PTY.RUN-R02 — Terminal authority.** The runtime feeds every child output byte - through one headless terminal model and uses that model for screen replay, - terminal modes, cursor, scrollback, and exit snapshots. _refines: PTY-R01, - PTY-R03._ -- **PTY.RUN-R03 — Per-session failure boundary.** Each session has its own - daemon, child, terminal, and socket. Closing one runtime does not control - another. _refines: PTY-R02._ -- **PTY.RUN-R04 — Restart-equivalent launch.** Initial launch, explicit restart, - and permanent respawn preserve the applicable command, args, cwd, initial - geometry, lifetime flags, tags, display name, and child-environment policy. - _refines: PTY-R05._ -- **PTY.RUN-R05 — Explicit environment policy.** Exact environment and inherited - environment policy are mutually exclusive; removals precede assignments; - ordinary keys retain explicit empty values, while `PTY_SESSION` and terminal - capability metadata remain runtime-owned invariants. - _refines: PTY-R05, PTY-R09._ -- **PTY.RUN-R06 — Observable exit.** Child data is drained before one terminal - exit result is finalized, with signal exits represented by shell-compatible - status. Final screen metadata and lifecycle policy are applied before daemon - shutdown completes. _refines: PTY-R05, PTY-R06._ -- **PTY.RUN-R07 — Explicit reconciliation.** Restart, permanent respawn, - abandonment, parent-orphan handling, and cleanup are explicit lifecycle - actions with generation checks; observation alone never performs them. - _refines: PTY-R02, PTY-R05._ - -See [launch context](./01-launch-context/requirements.md) and -[lifecycle](./02-lifecycle/requirements.md) for the concrete refinements. diff --git a/docs/vrs/01-session-runtime/spec.md b/docs/vrs/01-session-runtime/spec.md deleted file mode 100644 index 4b590ad..0000000 --- a/docs/vrs/01-session-runtime/spec.md +++ /dev/null @@ -1,56 +0,0 @@ -# Session runtime specification - -This document specifies the per-session execution engine. It builds on -[requirements.md](./requirements.md). - -## Status - -Active. - -## Runtime structure - -```text -detached Node daemon - +-- node-pty child process - +-- xterm-headless + SerializeAddon - +-- Unix socket server - +-- event writer - `-- generation-owned registry artifacts -``` - -`spawnDaemon` serializes `ServerOptions` into the daemon launch, starts a -detached Node process, and waits for its socket with a bounded failure path. -`PtyServer` spawns the command under a real PTY, writes child output to the -headless terminal and stream clients, records terminal events, and owns cleanup -for its generation (PTY.RUN-R01–R03). - -The child is launched through `/bin/sh -c 'exec "$@"'` so scripts, symlinks, -and shebangs follow shell execution semantics without leaving an intermediate -shell process. - -## Terminal pipeline - -```text -child output - |---> xterm parser ---> serializable terminal state - `---> ordered client broadcast - -client DATA ---> child PTY input -effective size ---> xterm resize ---> child PTY resize -``` - -The runtime tracks mode sequences that a screen serialization alone cannot -re-establish reliably and prefixes them when reconstructing a client. It also -derives bell, title, notification, focus, and cursor events from terminal -output (PTY.RUN-R02). - -## Launch and lifecycle refinements - -- [launch context](./01-launch-context/spec.md) specifies command, geometry, - environment, and restart preservation (PTY.RUN-R04–R05). -- [lifecycle](./02-lifecycle/spec.md) specifies exit, reap, permanent respawn, - generation ownership, and explicit cleanup (PTY.RUN-R06–R07). - -The runtime does not decide how users discover a session, how a client renders -it, or which product should supervise it. Those belong to the registry, stream, -and surface subsystems. diff --git a/docs/vrs/02-session-stream/01-synchronization/requirements.md b/docs/vrs/02-session-stream/01-synchronization/requirements.md deleted file mode 100644 index 4bc010c..0000000 --- a/docs/vrs/02-session-stream/01-synchronization/requirements.md +++ /dev/null @@ -1,28 +0,0 @@ -# Synchronization requirements - -> **Role.** Define the causally exact initial and reconnect baseline. These -> requirements refine the parent session-stream contract. - -## Requirements - -- **PTY.STREAM.SYNC-R01 — Geometry first.** An attach or peek generation emits - effective geometry before screen, data, or session exit for that generation. - A local machine detach outcome may terminate before terminal state begins. - _refines: PTY.STREAM-R01._ -- **PTY.STREAM.SYNC-R02 — Exact parser cut.** Screen serialization runs only - after an ordered terminal-parser marker; post-marker data and exit are queued - until after that screen. _refines: PTY.STREAM-R02._ -- **PTY.STREAM.SYNC-R03 — No lost settling data.** Data accepted while waiting - for resize settling remains represented in the later screen baseline rather - than being emitted ahead of it. _refines: PTY.STREAM-R02._ -- **PTY.STREAM.SYNC-R04 — Source-ordered exit.** Post-cut data is flushed before - its queued exit. A child that exited before the cut receives one synthesized - exit only after the baseline and any final post-cut data. _refines: - PTY.STREAM-R01, PTY.STREAM-R02._ -- **PTY.STREAM.SYNC-R05 — Supersession.** A later attach or peek request on the - same socket atomically replaces its client role and invalidates the prior - delayed cut and queue. A malformed attach changes neither. _refines: - PTY.STREAM-R02._ -- **PTY.STREAM.SYNC-R06 — Reconnect equivalence.** Every reconnect begins a new - generation satisfying the same geometry-screen-live order. _refines: - PTY.STREAM-R06._ diff --git a/docs/vrs/02-session-stream/01-synchronization/spec.md b/docs/vrs/02-session-stream/01-synchronization/spec.md deleted file mode 100644 index 66f2315..0000000 --- a/docs/vrs/02-session-stream/01-synchronization/spec.md +++ /dev/null @@ -1,40 +0,0 @@ -# Synchronization specification - -This document specifies the initial terminal parser cut. It builds on -[requirements.md](./requirements.md) and the parent -[stream specification](../spec.md). - -## Status - -Active. - -## Per-client state machine - -```text -ATTACH / PEEK - | - v - settling -- parser marker enqueued --> cutting -- marker callback --> live - | | | - data is represented in SCREEN queue DATA/EXIT write directly - -complete ATTACH / recognized PEEK: replace role and synchronization generation -malformed ATTACH: preserve role and generation -``` - -The runtime emits current or newly negotiated `GEOMETRY` on admission. An -attach affected by recent resize may remain `settling` for the redraw bound. -The eventual call to `terminal.write("", callback)` establishes the exact cut -(PTY.STREAM.SYNC-R01–R03). - -In the callback, the current generation writes `SCREEN`, transitions to live, -flushes `postCutPackets` in source order, and synthesizes `EXIT` only when the -runtime already exited and no queued exit exists. `node-pty` exposes its public -exit after draining PTY data, so queued `DATA` precedes queued `EXIT` -(PTY.STREAM.SYNC-R04). - -Each complete attach or recognized peek replaces readonly/writable state and -increments `initialScreenGeneration`. Delayed callbacks compare that token -before writing, so stale baselines and queues cannot leak across role changes or -reconnect generations. Attach geometry validation precedes both mutations; -peek preserves its optional flag compatibility (PTY.STREAM.SYNC-R05–R06). diff --git a/docs/vrs/02-session-stream/02-geometry/requirements.md b/docs/vrs/02-session-stream/02-geometry/requirements.md deleted file mode 100644 index d9f7348..0000000 --- a/docs/vrs/02-session-stream/02-geometry/requirements.md +++ /dev/null @@ -1,27 +0,0 @@ -# Geometry requirements - -> **Role.** Define the single grid shared by writable clients. These -> requirements refine the parent session-stream contract. - -## Requirements - -- **PTY.STREAM.GEO-R01 — Independent dimensions.** Effective rows are the - minimum requested rows and effective columns are the minimum requested - columns across connected writable clients. _refines: PTY.STREAM-R03._ -- **PTY.STREAM.GEO-R02 — Writable membership.** A writable client joins on - attach, updates its request on resize, and leaves on detach, error, or close. - _refines: PTY.STREAM-R03._ -- **PTY.STREAM.GEO-R03 — Readonly neutrality.** Peek and status clients do not - constrain geometry. Peek clients still receive every effective-geometry - update needed to parse the state they observe. _refines: PTY.STREAM-R03, - PTY.STREAM-R05._ -- **PTY.STREAM.GEO-R04 — Ordered application.** On change, the runtime resizes - its terminal model, broadcasts geometry, then resizes the child PTY before - child redraw output can be delivered. _refines: PTY.STREAM-R03._ -- **PTY.STREAM.GEO-R05 — Explicit requested/effective split.** Client APIs retain - their requested size separately from the effective size reported by the - runtime and resize their local terminal grid before parsing affected bytes. - _refines: PTY.STREAM-R03._ -- **PTY.STREAM.GEO-R06 — Zero-writable stability.** Removing the last writable - client does not invent a new size; the runtime retains its last effective - grid until a later writable request changes it. _refines: PTY.STREAM-R03._ diff --git a/docs/vrs/02-session-stream/02-geometry/spec.md b/docs/vrs/02-session-stream/02-geometry/spec.md deleted file mode 100644 index de2f699..0000000 --- a/docs/vrs/02-session-stream/02-geometry/spec.md +++ /dev/null @@ -1,45 +0,0 @@ -# Geometry specification - -This document specifies shared terminal-size negotiation. It builds on -[requirements.md](./requirements.md) and the parent -[stream specification](../spec.md). - -## Status - -Active. - -## Negotiation - -For writable client set `W`: - -```text -effectiveRows = min(client.rows for client in W) -effectiveCols = min(client.cols for client in W) -``` - -Rows and columns are minimized independently. A client requesting `60x80` and -one requesting `30x200` therefore produce `30x80`. Readonly clients are absent -from `W` (PTY.STREAM.GEO-R01–R03). - -## Change order - -```text -writable membership/request change - -> resize headless terminal - -> broadcast GEOMETRY to attached and readonly clients - -> resize child PTY (SIGWINCH/redraw may follow) - -> deliver resulting terminal DATA -``` - -The headless terminal is resized before the child so it is ready to parse the -redraw. Geometry is enqueued before that redraw can become stream data -(PTY.STREAM.GEO-R04). - -`SessionConnection`, server-mode `Session`, and `attachPty` consume geometry as -an event, update effective rows/columns, and resize their emulator before later -screen/data. Calling `resize` changes the local requested size; a smaller peer -can keep effective size below it (PTY.STREAM.GEO-R05). - -When `W` becomes empty, negotiation performs no resize. The last effective -terminal and PTY grid remains the baseline for readonly observation and the -next attach (PTY.STREAM.GEO-R06). diff --git a/docs/vrs/02-session-stream/intuition.md b/docs/vrs/02-session-stream/intuition.md deleted file mode 100644 index 1b17a8b..0000000 --- a/docs/vrs/02-session-stream/intuition.md +++ /dev/null @@ -1,25 +0,0 @@ -# Session stream intuition - -*For: protocol implementers and terminal embedders · Assumes: the session -runtime model · Covers: reconstructing one shared terminal correctly* - -A screen baseline and live bytes are two halves of one ordered history. If the -boundary between them is approximate, bytes can be missing from both halves or -appear in both. The runtime therefore places an ordered marker in xterm's parser -queue, serializes after the marker, and holds later packets until that baseline -has been sent. - -```text -parser input: A B C | D E exit - ^ exact cut -client stream: GEOMETRY, SCREEN(A B C), DATA(D E), EXIT -``` - -Geometry is part of this history. A byte stream generated for 80 columns cannot -be parsed correctly into a 120-column grid and repaired later. Geometry changes -therefore occupy the same ordered stream before affected screen or data. - -The machine attach surface deliberately does not invent a second protocol. It -forwards the existing frames on a descriptor that cannot be contaminated by -human terminal output or stderr. That keeps one ordering authority across local, -remote, interactive, and embedded clients. diff --git a/docs/vrs/02-session-stream/ontology.md b/docs/vrs/02-session-stream/ontology.md deleted file mode 100644 index 211da13..0000000 --- a/docs/vrs/02-session-stream/ontology.md +++ /dev/null @@ -1,36 +0,0 @@ -# Session stream ontology - -Root terms are inherited from [../ontology.md](../ontology.md). - -## Language - -**Requested geometry**: -The rows and columns most recently advertised by one writable client. It is an -input to negotiation, not necessarily the terminal's current grid. - -**Effective geometry**: -The shared rows and columns selected across all connected writable clients and -applied to both the child PTY and headless terminal. - -**Readonly client**: -A `PEEK` connection that receives terminal state but cannot send child input or -participate in geometry negotiation. -_Avoid_: unauthorized client; readonly is not an access-control claim. - -**Synchronization generation**: -One `ATTACH` or `PEEK` request's ordered transfer of geometry, baseline, and -post-cut events. A later request on the socket invalidates it. - -**Screen baseline**: -A serialized terminal state representing all parser input before one exact cut. -It is the starting state for later data, not a periodically sampled screenshot. - -**Machine attach stream v1**: -The versioned CLI contract that reframes terminal events unchanged onto a -caller-owned inherited file descriptor while retaining the invoking terminal -for input and requested geometry. - -**Machine attach outcome**: -Exactly one terminal frame before clean EOF. `EXIT` means the session process -ended; `DETACH` means this attach client intentionally detached. EOF without an -outcome is truncation or transport loss. diff --git a/docs/vrs/02-session-stream/requirements.md b/docs/vrs/02-session-stream/requirements.md deleted file mode 100644 index 8497fc2..0000000 --- a/docs/vrs/02-session-stream/requirements.md +++ /dev/null @@ -1,63 +0,0 @@ -# Session stream requirements - -> **Role.** The ordered transport realization of the root terminal contract. -> It projects one runtime's terminal state to ephemeral clients. Every -> requirement refines a `PTY-R*` requirement. - -## Assumptions - -- **PTY.STREAM-A01 Ordered byte transport:** A local Unix socket and a routed - remote byte stream preserve byte order for one connection. -- **PTY.STREAM-A02 Parser cut:** The headless terminal's write callback is an - ordered marker after all earlier parser writes. - -## Acceptable tradeoffs - -- **PTY.STREAM-T01 Redraw settling:** After an effective resize, initial screen - capture may wait for a bounded redraw-settle interval so the baseline is not - a known transient mid-redraw frame. -- **PTY.STREAM-T02 Unversioned base framing:** The base packet header has no - protocol-version field. Bounded unknown packet types are ignored for additive - compatibility; capability-specific surfaces fail closed on missing required - packets. - -## Requirements - -- **PTY.STREAM-R01 — Ordered reconstruction.** Every `ATTACH` and `PEEK` - generation that emits terminal state starts with `GEOMETRY`, then exactly one - `SCREEN` baseline, then post-cut `DATA` and at most one `EXIT` in source order. - A local machine `DETACH` may terminate its client before the baseline. - _refines: PTY-R03._ -- **PTY.STREAM-R02 — Exact parser boundary.** Output accepted before the screen - cut is represented by `SCREEN`; output and exit after the cut are queued and - cannot overtake the baseline. A newer mode request on the same connection - invalidates the unfinished generation. _refines: PTY-R03._ -- **PTY.STREAM-R03 — Causal effective geometry.** Writable attach/resize/disconnect - recomputes the minimum requested rows and columns. Changed `GEOMETRY` is - broadcast before terminal bytes produced for that size; readonly clients - receive updates but never constrain the grid. _refines: PTY-R04._ -- **PTY.STREAM-R04 — Bounded framing.** Every packet is length-delimited, partial - reads are reassembled, and a declared payload above 32 MiB drops the - connection without unbounded buffering. _refines: PTY-R09._ -- **PTY.STREAM-R05 — Replaceable client roles.** Every attach frame with complete - geometry replaces the socket's role with writable, installs its requested - geometry, and authorizes input and resize. Every recognized peek frame - replaces the role with readonly and removes its geometry constraint. A - malformed attach changes neither role nor synchronization generation. Status - observes without joining geometry, and detach closes the client without - ending the session. _refines: PTY-R01, PTY-R06._ -- **PTY.STREAM-R06 — Reconnect is a new baseline.** Reconnecting to a living - session creates a fresh ordering generation and resets protocol parsing; - observed session `EXIT` is terminal and is never reconnected past. _refines: - PTY-R03, PTY-R05._ -- **PTY.STREAM-R07 — Machine stream fidelity.** Machine attach reframes original - `GEOMETRY`, `SCREEN`, `DATA`, and `EXIT` packets on a caller-owned inherited - descriptor and uses the existing empty `DETACH` frame for the local-detach - outcome. It keeps stdin/stdout as the controlling terminal, honors - backpressure, and ends with exactly one framed outcome: `EXIT` when the - session process ended or `DETACH` when this client intentionally detached. - EOF after either is clean; EOF without either is truncation or transport - loss. _refines: PTY-R08, PTY-R09._ - -See [synchronization](./01-synchronization/requirements.md) and -[geometry](./02-geometry/requirements.md) for concrete refinements. diff --git a/docs/vrs/02-session-stream/spec.md b/docs/vrs/02-session-stream/spec.md deleted file mode 100644 index 5d7a951..0000000 --- a/docs/vrs/02-session-stream/spec.md +++ /dev/null @@ -1,90 +0,0 @@ -# Session stream specification - -This document specifies the binary session protocol and its client modes. It -builds on [requirements.md](./requirements.md). - -## Status - -Active. - -## Packet framing - -```text -+------------+----------------+--------------------+ -| type: u8 | length: u32 BE | payload: N bytes | -+------------+----------------+--------------------+ -``` - -`PacketReader` buffers partial frames and emits complete packets in order. A -length greater than `32 * 1024 * 1024` poisons the reader and requires the -connection to be destroyed (PTY.STREAM-R04). - -| Type | Id | Direction | Payload | -| --- | ---: | --- | --- | -| `DATA` | 0 | both | terminal bytes | -| `ATTACH` | 1 | client to runtime | rows `u16BE`, cols `u16BE` | -| `DETACH` | 2 | client to runtime; machine adapter to consumer | empty | -| `RESIZE` | 3 | client to runtime | rows `u16BE`, cols `u16BE` | -| `EXIT` | 4 | runtime to client | signed exit code `i32BE` | -| `SCREEN` | 5 | runtime to client | serialized ANSI terminal state or requested plain text | -| `PEEK` | 6 | client to runtime | flags: plain bit 0, full-scrollback bit 1 | -| `STATUS` | 7 | both | empty request or JSON response | -| 8–9 | — | — | reserved | -| `GEOMETRY` | 10 | runtime to client | effective rows `u16BE`, cols `u16BE` | - -Bounded unknown types are ignored by the runtime. Legacy size/exit payload -decoders retain historical fallback values; commands with stronger contracts -validate their required shape before acting (PTY.STREAM-T02). - -`GEOMETRY` is an additive type at id 10. Clients predating it ignore the -bounded unknown packet and continue with their historical raw `SCREEN`/`DATA` -behavior. Machine attach v1 instead requires geometry and fails explicitly -against an older daemon that cannot establish its reconstruction contract. - -## Connection roles - -```text -new connection - +-- ATTACH -> replace role with writable, contribute geometry, accept input - +-- PEEK -> replace role with readonly, remove geometry constraint - `-- STATUS -> observation only, does not join geometry -``` - -An `ATTACH` carrying complete geometry or a recognized `PEEK` replaces rather -than accumulates role state and starts a fresh synchronization generation. A -malformed `ATTACH` leaves both role and generation unchanged. Peek retains its -historical optional/extensible flag handling rather than imposing a new strict -payload shape. `DETACH` ends only the connection. Socket close removes its -writable geometry constraint and may change the effective grid -(PTY.STREAM-R05). - -## Ordered state transfer - -The [synchronization specification](./01-synchronization/spec.md) owns initial -and reconnect ordering. The [geometry specification](./02-geometry/spec.md) -owns requested/effective size negotiation. Together they establish: - -```text -GEOMETRY -> SCREEN -> DATA* -> EXIT? -``` - -## Human and machine attach - -Ordinary attach clears the user's terminal before `SCREEN`, writes subsequent -`DATA` to stdout, forwards stdin and stdout resize events, and sanitizes modes -on detach/exit. Machine attach receives the same socket packets and reframes -the four server event types to `--attach-stream-fd-v1`; on local detach it emits -the existing empty `DETACH` frame as the terminal outcome. Stdout remains the -controlling TTY and receives no screen bytes (PTY.STREAM-R07). - -The machine state machine requires `GEOMETRY`, permits further geometry updates -while awaiting `SCREEN`, then permits live events. Each reconnect resets it to -the initial state. A local detach may emit its terminal `DETACH` outcome while -the initial baseline is still pending. Descriptor backpressure pauses the -source socket. The caller retains descriptor ownership; the attach adapter ends -its stream view, not the underlying fd. Exactly one outcome terminates a clean -stream: framed `EXIT` means the session ended, while framed `DETACH` means this -client intentionally detached. EOF without either outcome, including transport -loss, reconnect give-up, or abrupt administrative session destruction before a -process `EXIT` was observed, is truncation. Administrative destruction does not -introduce a third clean outcome (PTY.STREAM-R06–R07). diff --git a/docs/vrs/03-registry/intuition.md b/docs/vrs/03-registry/intuition.md deleted file mode 100644 index a41fab2..0000000 --- a/docs/vrs/03-registry/intuition.md +++ /dev/null @@ -1,24 +0,0 @@ -# Registry intuition - -*For: integrators and fast-path readers · Assumes: the root session model · -Covers: durable identity without turning files into the live protocol* - -The registry answers “what sessions exist, how were they launched, and what -happened?” The socket answers “what is happening in this terminal now?” Keeping -those questions separate makes both boundaries simpler. - -```text -registry JSON/events live socket -identity + history ordered terminal state -cheap external reads attach/input/geometry -``` - -Atomic rename means an external reader never sees half a metadata document, but -it does not turn independent writers into a transaction. Generation checks -solve a different race: they prevent an old owner from deleting a new session -that reused the same stable id. - -The stable id is deliberately boring because it reaches filesystem and kernel -socket paths. Display names and tags carry richer presentation and grouping, -while explicit ambiguity and `PTY_ROOT` keep them from silently becoming the -wrong kind of identity boundary. diff --git a/docs/vrs/03-registry/ontology.md b/docs/vrs/03-registry/ontology.md deleted file mode 100644 index 9bb83fc..0000000 --- a/docs/vrs/03-registry/ontology.md +++ /dev/null @@ -1,32 +0,0 @@ -# Registry ontology - -Root terms are inherited from [../ontology.md](../ontology.md). - -## Language - -**Tier 1 artifact**: -A documented external-readable storage surface whose changes are called out for -version-pinned consumers: session metadata or events. - -**Tier 2 artifact**: -An implementation-owned registry artifact that may move without storage-format -compatibility: socket, pid, lock, theme, or gc log. - -**Running**: -A session whose daemon process is alive and whose socket is reachable. - -**Exited**: -A non-live session with recorded exit details. - -**Vanished**: -A non-live session without recorded exit details because the daemon could not -finalize them. -_Avoid_: exited; the cause and code are unknown. - -**Presentation reference**: -A display-name lookup accepted only when exactly one session matches. It is a -convenience selector, not durable identity. - -**Hard isolation**: -Selection of a distinct registry root. Tag filtering within a registry is soft -scoping, not isolation. diff --git a/docs/vrs/03-registry/requirements.md b/docs/vrs/03-registry/requirements.md deleted file mode 100644 index c92d288..0000000 --- a/docs/vrs/03-registry/requirements.md +++ /dev/null @@ -1,52 +0,0 @@ -# Registry requirements - -> **Role.** The durable identity and observation realization of the root -> contract. Every requirement refines a `PTY-R*` requirement. - -## Assumptions - -- **PTY.REG-A01 Single writer authority:** The `pty` implementation is the - canonical writer. External tools may read tier-1 files directly. -- **PTY.REG-A02 Same-filesystem publication:** Temporary and target metadata - files share a filesystem, so rename publishes atomically. - -## Acceptable tradeoffs - -- **PTY.REG-T01 Last-write-wins metadata:** Individual file replacement is - atomic, but concurrent read-modify-write metadata updates are not - transactionally merged. -- **PTY.REG-T02 Bounded events:** Event history truncates from 1,000 to the most - recent 500 lines rather than growing without bound. - -## Requirements - -- **PTY.REG-R01 — Root isolation.** Every command resolves one registry from - explicit `PTY_ROOT` or the documented default; distinct roots share no - sessions, sockets, events, or cleanup. _refines: PTY-R02, PTY-R07._ -- **PTY.REG-R02 — Durable launch metadata.** Tier-1 metadata records stable - identity-adjacent launch, generation, presentation, exit, and restart state - and is published atomically. _refines: PTY-R05._ -- **PTY.REG-R03 — Pure inventory.** Listing derives `running`, `exited`, or - `vanished` from bounded process/socket evidence and never cleans, restarts, - migrates, or repairs state. _refines: PTY-R06._ -- **PTY.REG-R04 — Observable live state.** Status exposes the effective terminal - geometry, cursor/scrollback, process and daemon resources, terminal modes, - and anonymous client counts plus requested geometry and constraints without - attaching. Events expose timestamped lifecycle, terminal, presentation, tag, and - `user.*` changes. _refines: PTY-R06._ -- **PTY.REG-R05 — Stable identity.** Filename-safe stable ids are unique in a - registry. Display names are mutable and non-unique; exact id wins resolution, - and an ambiguous display name refuses with candidates. Tags support - composable filtering but do not create hard isolation. _refines: PTY-R07._ -- **PTY.REG-R06 — Documented compatibility tiers.** Tier-1 metadata and events - are documented and storage changes are called out; temporary files are - ignored; tier-2 socket, pid, lock, and UI files remain internal. Older - metadata fields retain documented defaults. _refines: PTY-R09._ -- **PTY.REG-R07 — Generation-safe ownership.** A daemon or lifecycle operation - removes registry artifacts only when generation evidence still matches its - observation. Creation uses an exclusive, dead-owner-recoverable id lock. - _refines: PTY-R02, PTY-R05._ -- **PTY.REG-R08 — External-readable events.** Each event is one JSONL - envelope with session, type, timestamp, and typed payload; external followers - can reopen after truncation and subscribe instead of polling metadata. - _refines: PTY-R06, PTY-R09._ diff --git a/docs/vrs/03-registry/spec.md b/docs/vrs/03-registry/spec.md deleted file mode 100644 index 824b402..0000000 --- a/docs/vrs/03-registry/spec.md +++ /dev/null @@ -1,85 +0,0 @@ -# Registry specification - -This document specifies registry identity, storage, observation, and ownership. -It builds on [requirements.md](./requirements.md). - -## Status - -Active. The complete public field and event schemas are maintained in -[the disk-layout reference](../../disk-layout.md). - -## Root and artifacts - -```text -$PTY_ROOT/ mode 0700 - .json tier 1 metadata - .events.jsonl tier 1 events - .sock tier 2 live socket - .pid tier 2 daemon pid - .lock tier 2 creation lock - .tmp.. ignored publication temporary - theme, gc.log tier 2 operator/UI state -``` - -`PTY_ROOT` is canonical. Deprecated `PTY_SESSION_DIR` is accepted only when the -canonical variable is absent and emits a one-time warning unless explicitly -silenced. When both exist, `PTY_ROOT` wins visibly. Every operation uses the -same resolution (PTY.REG-R01, R06). - -## Identity and resolution - -Stable ids use `[a-zA-Z0-9._-]+`, fit both the filename and the smallest -supported Unix socket path, and are protected by a per-id lock. Display names -allow printable presentation text but are not path material or identity. - -```text -reference - -> exact stable id match - -> exactly one displayName match - -> otherwise absent or explicit ambiguity -``` - -Repeated `--filter-tag key=value` predicates all must match. A reserved tag can -drive lifecycle or outer-tool bookkeeping, but tags remain metadata within one -registry (PTY.REG-R05). - -## Metadata publication and observation - -Metadata is pretty-printed JSON written to a randomized sibling temporary and -renamed into place. Readers see the old or new complete file. The public schema -includes opaque generation and daemon pid, launch definition, timestamps, exit -and last-screen state, tags/display name, and last writable attach time -(PTY.REG-R02, R06). - -Inventory combines metadata with bounded pid/socket probes: - -| State | Evidence | -| --- | --- | -| `running` | daemon alive and socket reachable | -| `exited` | no live daemon and recorded exit details | -| `vanished` | no live daemon and no recorded exit details | - -The inventory path performs no cleanup. `STATUS` queries a live daemon and -reports terminal, process, daemon, modes, uptime, and client connections. Client -rows contain no user identity; they expose readonly/writable role, requested -geometry, request sequence, and whether each dimension constrains the effective -grid (PTY.REG-R03–R04). - -## Events - -Each event line has `{ session, type, ts, ...payload }`. System types cover -terminal signals, lifecycle, exec, respawn/abandon/flapping, and metadata -changes. User types are namespaced `user.` and reject empty, -whitespace, control characters, and system-name collisions. - -Appends are serialized within a runtime. Periodic bounded truncation atomically -replaces the file with the latest 500 lines when it reaches 1,000, so followers -detect inode change and reopen. Session lifecycle cleanup removes the event file -with the session (PTY.REG-R08). - -## Ownership - -Metadata generation is opaque to readers. Daemon self-cleanup, explicit remove, -and reconciliation compare their observed generation before deleting. Creation -locks are exclusive and may be stolen only when the recorded owner process is -dead (PTY.REG-R07). diff --git a/docs/vrs/04-surfaces/01-cli-package/requirements.md b/docs/vrs/04-surfaces/01-cli-package/requirements.md deleted file mode 100644 index e8faef0..0000000 --- a/docs/vrs/04-surfaces/01-cli-package/requirements.md +++ /dev/null @@ -1,30 +0,0 @@ -# CLI and package requirements - -> **Role.** Define the executable and distribution boundary. These requirements -> refine the parent surface contract. - -## Requirements - -- **PTY.SURF.CLI-R01 — Single CLI process.** `bin/pty` validates compiled output - and imports `dist/cli.js` in-process without spawning a forwarding child. - _refines: PTY.SURF-R04, PTY.SURF-R07._ -- **PTY.SURF.CLI-R02 — Inherited descriptor fidelity.** Every descriptor - inherited by the package entrypoint remains available to CLI features; the - launcher does not assume only stdin/stdout/stderr. _refines: PTY.SURF-R04._ -- **PTY.SURF.CLI-R03 — Direct signal ownership.** The OS-visible CLI process is - the process running command handlers, so termination signals do not depend on - a wrapper's forwarding or orphan a handler child. _refines: PTY.SURF-R04._ -- **PTY.SURF.CLI-R04 — Machine attach separation.** In machine mode, stdin and - stdout remain the controlling terminal, terminal events use only the selected - descriptor, diagnostics use stderr, and exactly one `EXIT` or `DETACH` outcome - is flushed before clean CLI completion. _refines: PTY.SURF-R03, - PTY.SURF-R04._ -- **PTY.SURF.CLI-R05 — Fail-closed v1 admission.** The descriptor is a valid - writable inherited fd greater than or equal to 3. Any emitted terminal - baseline begins with geometry then screen; a local detach may instead emit - its terminal outcome before the baseline. Unsupported server-event order or - EOF without a terminal outcome exits non-zero without silently degrading to - raw output. _refines: PTY.SURF-R03._ -- **PTY.SURF.CLI-R06 — Completion parity.** Required values such as the machine - stream fd are emitted as value-taking options in bash, fish, and zsh rather - than boolean switches. _refines: PTY.SURF-R05._ diff --git a/docs/vrs/04-surfaces/01-cli-package/spec.md b/docs/vrs/04-surfaces/01-cli-package/spec.md deleted file mode 100644 index 6edf7f9..0000000 --- a/docs/vrs/04-surfaces/01-cli-package/spec.md +++ /dev/null @@ -1,47 +0,0 @@ -# CLI and package specification - -This document specifies the executable distribution boundary. It builds on -[requirements.md](./requirements.md) and the parent -[surface specification](../spec.md). - -## Status - -Active. - -## Entrypoint - -```text -OS exec bin/pty - -> set process title - -> verify ../dist/cli.js exists - -> dynamic import in the same process - -> CLI main reads original argv and inherited fds -``` - -There is no wrapper child. Consequently a caller's fd 3, controlling terminal, -process id, and signals are properties of the process actually executing the -CLI (PTY.SURF.CLI-R01–R03). - -## Machine attach - -`attach --attach-stream-fd-v1 ` requires an inherited fd greater than -or equal to 3 and validates it with a zero-byte write before session resolution. -It retains stdin/stdout for raw mode, requested size, input, and resize events. -It writes reframed terminal-event packets only to the fd, diagnostics only to -stderr, and no terminal bytes to stdout (PTY.SURF.CLI-R04–R05). - -The adapter owns its stream view with `autoClose: false`: session exit writes -framed `EXIT`; Ctrl-\\ writes the existing empty `DETACH` frame and sends the -server-side detach request. Either outcome is flushed before clean completion, -while the caller still owns the descriptor. Descriptor errors, unsupported -initial order, transport loss, reconnect give-up, and EOF without either -terminal outcome are non-zero failures. An administrative session destruction -is such an outcome-less failure unless the adapter first observed process -`EXIT`; it does not synthesize a third outcome. - -## Schema and completions - -The command schema distinguishes free-valued and choice-valued options. -Completion generation uses this arity to make `--attach-stream-fd-v1` consume a -following value in bash, fish, and zsh. Tests execute generated bash completion -behavior in addition to checking text output (PTY.SURF.CLI-R06). diff --git a/docs/vrs/04-surfaces/02-libraries/requirements.md b/docs/vrs/04-surfaces/02-libraries/requirements.md deleted file mode 100644 index cce0d4b..0000000 --- a/docs/vrs/04-surfaces/02-libraries/requirements.md +++ /dev/null @@ -1,27 +0,0 @@ -# Library requirements - -> **Role.** Define the embedding and real-terminal testing surfaces. These -> requirements refine the parent surface contract. - -## Requirements - -- **PTY.SURF.LIB-R01 — Explicit module boundaries.** The package exports client, - server, protocol, keys, testing, and TUI modules from compiled JavaScript with - matching declarations. _refines: PTY.SURF-R07._ -- **PTY.SURF.LIB-R02 — Shared client semantics.** `SessionConnection`, direct - client helpers, and `attachPty` use the shared packet codec, stable-id - registry paths, and effective-geometry events. _refines: PTY.SURF-R01._ -- **PTY.SURF.LIB-R03 — Real-process tests.** `Session.spawn` starts a real PTY; - `Session.server` starts a real persistent runtime; neither substitutes a mock - terminal transport. _refines: PTY.SURF-R02._ -- **PTY.SURF.LIB-R04 — Reconstructable assertions.** Testing screenshots expose - trimmed lines, joined text, and ANSI serialization; bounded waits report the - current screen when their predicate fails. _refines: PTY.SURF-R02._ -- **PTY.SURF.LIB-R05 — Multi-client geometry fidelity.** Server-mode test - sessions and TUI panes distinguish requested and effective geometry, resize - their emulator before later data, and update across peer attach, resize, and - disconnect. _refines: PTY.SURF-R02._ -- **PTY.SURF.LIB-R06 — Daemon strategy equivalence.** Direct server-module - launch, explicit server-module override, and installed-CLI fallback carry the - same launch definition or reject unsupported options explicitly. _refines: - PTY.SURF-R01, PTY.SURF-R03._ diff --git a/docs/vrs/04-surfaces/02-libraries/spec.md b/docs/vrs/04-surfaces/02-libraries/spec.md deleted file mode 100644 index 3be8cee..0000000 --- a/docs/vrs/04-surfaces/02-libraries/spec.md +++ /dev/null @@ -1,51 +0,0 @@ -# Library specification - -This document specifies exported embedding surfaces. It builds on -[requirements.md](./requirements.md) and the parent -[surface specification](../spec.md). - -## Status - -Active. The TUI module remains alpha. - -## Public modules - -| Export | Contract | -| --- | --- | -| `/client` | session discovery, lifecycle, connection, attach/peek/send/stats, events, ptyfile helpers | -| `/server` | embeddable `PtyServer` runtime | -| `/protocol` | packet constants, codecs, bounded streaming reader | -| `/keys` | named key and sequence parsing | -| `/testing` | real-PTY `Session` and screenshot types | -| `/tui` | alpha terminal rendering/input/widgets and PTY panes | - -`tsc -p tsconfig.build.json` produces `dist` JavaScript and declarations while -rewriting source `.ts` imports to `.js`. Development source remains runnable -with Node type stripping (PTY.SURF.LIB-R01). - -## Testing backends - -```text -Session.spawn -> direct node-pty child -> local headless terminal -Session.server -> PtyServer -> SessionConnection -> local headless terminal -``` - -Both backends send real key bytes and parse real terminal output. Screenshots -project the active terminal as lines/text/ANSI. `waitForText`, `waitForAbsent`, -and general `waitFor` repeatedly inspect that state until success or a bounded -diagnostic failure (PTY.SURF.LIB-R03–R04). - -Server mode supports attach, reconnect, peer clients, and resize. A `GEOMETRY` -event resizes the receiving terminal before affected screen/data; public -`rows`/`cols` report effective rather than merely requested dimensions. -`SessionConnection` and `attachPty` preserve the same rule -(PTY.SURF.LIB-R02, R05). - -## Daemon launch strategies - -An explicit server-module override wins, then an installed sibling -`dist/server.js`, then delegation to the installed `pty` CLI for bundled -consumers. Direct strategies carry serialized launch options. CLI fallback is -limited to the options its command surface can express and rejects an exact -environment map instead of silently changing its meaning -(PTY.SURF.LIB-R06). diff --git a/docs/vrs/04-surfaces/intuition.md b/docs/vrs/04-surfaces/intuition.md deleted file mode 100644 index 248d5dd..0000000 --- a/docs/vrs/04-surfaces/intuition.md +++ /dev/null @@ -1,22 +0,0 @@ -# Surface intuition - -*For: CLI and package maintainers · Assumes: runtime, stream, and registry -contracts · Covers: adding an interface without creating another system* - -The safest surface is a thin adapter. A remote attach is still an attach. A -testing server is still a `PtyServer`. A TUI pane still consumes geometry before -screen bytes. This keeps difficult terminal ordering in one place. - -```text -human CLI --------+ -machine CLI ------+--> shared client/runtime modules --> one protocol -testing library --+ -TUI pane ---------+ -remote route -----+ -``` - -Process boundaries are part of an interface. A wrapper that spawns a child and -inherits only fds 0–2 silently breaks a caller-owned fd 3 even if every protocol -unit test passes. Loading the compiled CLI in the package-entrypoint process is -both simpler and more faithful: argv, signals, controlling terminal, and all -inherited descriptors arrive at the actual CLI. diff --git a/docs/vrs/04-surfaces/ontology.md b/docs/vrs/04-surfaces/ontology.md deleted file mode 100644 index b373061..0000000 --- a/docs/vrs/04-surfaces/ontology.md +++ /dev/null @@ -1,29 +0,0 @@ -# Surface ontology - -Root terms are inherited from [../ontology.md](../ontology.md). - -## Language - -**Package entrypoint**: -The shipped `bin/pty` executable that selects compiled CLI code without adding -a second process or alternate argument contract. - -**Controlling terminal**: -The invoking terminal retained on stdin/stdout for raw input, size, and resize -events even when screen events are emitted on a machine descriptor. - -**Remote route**: -A fabric-provided ordered stream bridged to one ordinary local session socket. -It is transport composition, not a second session protocol. - -**Testing session**: -A real PTY-backed test handle using either a direct child backend or the -persistent server backend. - -**Terminal screenshot**: -A point-in-time projection of a testing terminal as trimmed lines, joined plain -text, and ANSI serialization. It is not the live protocol's screen baseline. - -**TUI toolkit**: -The alpha package surface for terminal layout, rendering, input, widgets, and -attaching a session as a pane. diff --git a/docs/vrs/04-surfaces/requirements.md b/docs/vrs/04-surfaces/requirements.md deleted file mode 100644 index fdbb65f..0000000 --- a/docs/vrs/04-surfaces/requirements.md +++ /dev/null @@ -1,52 +0,0 @@ -# Surface requirements - -> **Role.** Compose the runtime, stream, and registry through supported user and -> embedding boundaries. Every requirement refines a root `PTY-R*` requirement. - -## Assumptions - -- **PTY.SURF-A01 Node package:** The distributed CLI and libraries run on a - supported Node.js runtime with the package's native `node-pty` dependency. -- **PTY.SURF-A02 Trusted fabric peer:** Remote routing delegates peer transport - and authorization to `fabric`; `pty` receives an ordered local stream. - -## Acceptable tradeoffs - -- **PTY.SURF-T01 Alpha TUI toolkit:** `@compoundingtech/pty/tui` is a shipped but - alpha surface and may evolve under the pre-1.0 compatibility policy. -- **PTY.SURF-T02 CLI fallback for bundled embedders:** If an embedded client - cannot locate the sibling server module, daemon creation may delegate to the - installed `pty` CLI rather than materialize bundled source. - -## Requirements - -- **PTY.SURF-R01 — One behavioral core.** CLI and library operations call the - same runtime, protocol, registry, event, and lifecycle primitives rather than - defining parallel semantics. _refines: PTY-R08._ -- **PTY.SURF-R02 — Real terminal testing.** The testing library drives real PTY - processes and the same server protocol, exposes reconstructable text/ANSI - screenshots, and applies effective geometry before affected terminal bytes. - _refines: PTY-R03, PTY-R04, PTY-R08._ -- **PTY.SURF-R03 — Explicit capability failure.** A surface validates required - descriptors, refs, roots, and protocol order before mutation or output; an - older daemon that lacks a required machine-stream contract fails clearly - rather than silently degrading. _refines: PTY-R09._ -- **PTY.SURF-R04 — Descriptor-preserving package entrypoint.** The shipped - `bin/pty` runs the compiled CLI in the invoking process, preserving inherited - descriptors above stderr and delivering signals to the actual CLI process. - _refines: PTY-R08._ -- **PTY.SURF-R05 — Schema-consistent CLI.** Command help, parsing, and generated - bash/fish/zsh completions agree about flag arity and choices; stable ids and - ambiguous display references follow registry resolution. _refines: PTY-R07, - PTY-R08, PTY-R09._ -- **PTY.SURF-R06 — Protocol-preserving remote route.** Remote list returns a - structured control response; routed attach/peek/send hands the ordinary - per-session protocol through unchanged, including reconnect baselines and - machine-stream order. _refines: PTY-R03, PTY-R08._ -- **PTY.SURF-R07 — Buildable published surface.** Package exports resolve to - compiled `dist` modules with matching declarations, while TypeScript sources - remain directly runnable for development. Missing compiled CLI output fails - with an actionable error. _refines: PTY-R08, PTY-R09._ - -See [CLI and package](./01-cli-package/requirements.md) and -[libraries](./02-libraries/requirements.md) for concrete refinements. diff --git a/docs/vrs/04-surfaces/spec.md b/docs/vrs/04-surfaces/spec.md deleted file mode 100644 index d349389..0000000 --- a/docs/vrs/04-surfaces/spec.md +++ /dev/null @@ -1,54 +0,0 @@ -# Surface specification - -This document specifies how the product and package surfaces compose lower -layers. It builds on [requirements.md](./requirements.md). - -## Status - -Active. The TUI toolkit is alpha; the package as a whole is pre-1.0. - -## Surface map - -```text -@compoundingtech/pty - +-- bin/pty + dist/cli.js operator CLI - +-- /client session and registry client API - +-- /server embeddable runtime - +-- /protocol packet codec - +-- /testing real-PTY test sessions - +-- /tui alpha terminal UI toolkit - `-- /keys key-name codec -``` - -The CLI imports the same modules exported to embedders. `Session.server`, -`SessionConnection`, and `attachPty` consume the same ordered geometry and -terminal frames as the CLI. No surface owns a second session state machine -(PTY.SURF-R01–R02). - -## Command routing - -The CLI schema defines commands, positionals, flags, repeatability, value mode, -and choices. Parsing, help, and generated completions consume that shape. -Before machine attach resolves a session, it validates that the requested -descriptor is an open writable inherited fd greater than or equal to 3. -Reference-taking commands use stable-id first, unambiguous-display-name second -resolution (PTY.SURF-R03, R05). - -## Remote composition - -`remote-serve --stdio` receives a newline-delimited JSON control request from a -trusted fabric route. `list` returns JSON. A routed command resolves the remote -reference, replies with an acknowledgment, then bridges residual and subsequent -bytes bidirectionally to the local session socket. The session protocol remains -unchanged across the bridge. Interactive remote attach may reconnect by dialing -a new route; a resolved session absence ends that loop (PTY.SURF-R06). - -The listening-socket form is transitional. The on-demand stdio form leaves -persistence and roaming to fabric and does not introduce a central pty daemon. - -## Child specifications - -- [CLI and package](./01-cli-package/spec.md) owns the executable artifact, - process boundary, descriptors, signals, help, and completions. -- [libraries](./02-libraries/spec.md) owns public module boundaries and the - testing/TUI embedding contracts. diff --git a/docs/vrs/intuition.md b/docs/vrs/intuition.md deleted file mode 100644 index f77fb59..0000000 --- a/docs/vrs/intuition.md +++ /dev/null @@ -1,30 +0,0 @@ -# pty intuition - -*For: maintainers and embedders · Assumes: Unix PTYs and terminal escape -sequences · Covers: the system-wide mental model* - -The durable thing is a **session**, not the terminal window connected to it. -One daemon owns one child PTY and continuously parses the child's bytes into a -headless terminal. Clients come and go. The daemon keeps the process and the -screen model alive between them. - -```text -child <-> PTY <-> per-session runtime <-> zero or more clients - | - +-> atomic metadata + ordered events -``` - -Reattachment is state transfer, not log replay. A client first learns the -effective grid, then receives one screen image representing everything before -an exact parser cut, then receives bytes produced after that cut. This is why a -client can reconstruct colors, cursor position, alternate-screen applications, -and concurrent output without guessing. - -The filesystem registry is a separate projection. It gives sessions durable -identity, restart inputs, lifecycle guards, and cheap observation. It does not -replace the live stream, and the live stream does not become identity. - -The CLI and libraries are adapters around those same two boundaries. A useful -test of any new surface is therefore simple: does it preserve session lifetime, -ordered terminal state, effective geometry, and generation-safe lifecycle—or -does it explicitly decline the capability? diff --git a/docs/vrs/ontology.md b/docs/vrs/ontology.md deleted file mode 100644 index 162ff20..0000000 --- a/docs/vrs/ontology.md +++ /dev/null @@ -1,45 +0,0 @@ -# pty ontology - -Terms defined here are inherited by every descendant. Child ontologies add -only local terms. - -## Language - -**Session**: -A stable-id execution record consisting of a child process, its PTY and -terminal state, and its registry artifacts. A session can outlive every client. - -**Session runtime**: -The per-session daemon, child PTY, headless terminal emulator, and lifecycle -logic that maintain a session independently of clients. -_Avoid_: server (ambiguous with the child command), central daemon. - -**Client**: -One socket connection observing or interacting with a session. A client is -ephemeral and is never session identity. - -**Terminal state**: -The ordered result of parsing the child's terminal byte stream: grid, style, -cursor, terminal modes, alternate screen, and scrollback. -_Avoid_: log (a log cannot reconstruct this state). - -**Registry**: -One directory tree selected by `PTY_ROOT`, containing the identities and -artifacts for a set of sessions. Distinct registries are hard isolation; -filtered tags are not. - -**Stable id**: -The immutable, path-safe session name used for socket and registry filenames. -_Avoid_: display name, label. - -**Display name**: -Mutable, non-unique presentation metadata. It resolves as a convenience -reference only when exactly one session matches. - -**Generation**: -An opaque token identifying one daemon's ownership of a session's mutable -registry artifacts. It prevents stale cleanup from deleting a replacement. - -**Surface**: -A supported way to invoke or embed the system: CLI, package entrypoint, exported -library, testing API, or remote route. diff --git a/docs/vrs/requirements.md b/docs/vrs/requirements.md index cb76ec6..676b231 100644 --- a/docs/vrs/requirements.md +++ b/docs/vrs/requirements.md @@ -2,83 +2,89 @@ ## Context -These requirements define the durable contract of the `pty` project described -in the [README](../../README.md): persistent terminal sessions and the reusable -libraries built on the same terminal model. The README remains the product -purpose and user guide; this tree owns the testable system constraints. There -is intentionally no `vision.md` in this tree. - -Child requirements refine this contract by scoped ID: - -- [session runtime](./01-session-runtime/requirements.md) -- [session stream](./02-session-stream/requirements.md) -- [registry](./03-registry/requirements.md) -- [surfaces](./04-surfaces/requirements.md) +The [README](../../README.md) is the concise purpose and user guide for `pty`. +These requirements define its durable, testable system constraints. The +implementation contract and validation map live in [spec.md](./spec.md). ## Assumptions -- **PTY-A01 Unix host:** Supported hosts provide Unix PTYs, Unix-domain sockets, +- **A01 Unix host:** Supported hosts provide Unix PTYs, Unix-domain sockets, process signals, and atomic same-filesystem rename. The supported products are macOS and Linux. -- **PTY-A02 Trusted user boundary:** A registry belongs to one trusted OS user. - Socket and filesystem permissions are the access boundary; readonly clients - are a behavior mode, not an authorization mechanism. -- **PTY-A03 Terminal byte stream:** A child process expresses terminal state as - ordered bytes and terminal control sequences. Reconstructing that state - requires a terminal emulator, not line-oriented logging. +- **A02 Trusted user boundary:** One registry belongs to one trusted OS user. + Filesystem permissions are the access boundary; readonly mode is behavior, + not authorization. +- **A03 Terminal semantics:** Child output is an ordered terminal byte stream. + Reconstructing it requires a terminal emulator rather than line-oriented logs. ## Acceptable tradeoffs -- **PTY-T01 Per-session daemon:** Each persistent session pays the resource cost - of an independent daemon in exchange for failure isolation and no central - lifetime owner. -- **PTY-T02 Shared grid:** All writable clients share one effective PTY grid. - The smallest requested row and column dimensions win, preserving a complete - view for every writable client at the cost of reducing larger clients. -- **PTY-T03 Pre-1.0 evolution:** Public storage and package APIs may change - before 1.0 when the documented compatibility tier permits it. Changes remain - explicit and version-pinned consumers can retain the old contract. +- **T01 Per-session daemon:** Each session pays for an independent daemon in + exchange for client-independent lifetime and failure isolation. +- **T02 Shared grid:** All writable clients share the minimum requested rows and + minimum requested columns so every writer can represent the complete grid. +- **T03 Pre-1.0 compatibility:** Public storage and package APIs may evolve + before 1.0, but readers remain bounded and documented compatibility tiers are + preserved deliberately. ## Requirements -### Must preserve sessions independently of observers - -- **PTY-R01 — Persistent execution.** A session's child process and terminal state - continue independently of the client that created, attached to, or detached - from it. -- **PTY-R02 — Isolated failure domains.** Failure or replacement of one session, - client, or control-plane invocation does not implicitly terminate unrelated - sessions. - -### Must preserve terminal meaning - -- **PTY-R03 — Reconstructable terminal.** A newly attached client can reconstruct - a causally valid terminal state, including style, cursor, modes, alternate - screen, scrollback, and subsequent output, without a byte-loss or reordering - window. -- **PTY-R04 — Deterministic shared geometry.** Concurrent writable clients produce - one explicit effective geometry, and every reconstructing client learns that - geometry in causal order with the terminal state it describes. Readonly - observation does not alter it. - -### Must make lifecycle and state inspectable - -- **PTY-R05 — Durable launch and lifecycle.** A preserved session retains enough - launch definition for an equivalent explicit or policy-driven restart, while - explicit removal and cleanup cannot delete a replacement generation. -- **PTY-R06 — Observational state.** Callers can inspect session identity, - lifecycle, metadata, events, clients, resources, and effective geometry - without attaching or mutating the session. -- **PTY-R07 — Explicit isolation and identity.** Registry selection and stable - session identity are explicit. Presentation labels and tags do not silently - become durable identity or hard isolation boundaries. - -### Must compose through supported surfaces - -- **PTY-R08 — Contract-equivalent surfaces.** The CLI, exported libraries, - testing API, local socket transport, machine attach stream, and remote route - preserve the same session, stream, geometry, and lifecycle contracts where - they expose those capabilities. -- **PTY-R09 — Bounded compatibility.** Protocol and storage readers reject - unbounded or structurally invalid input, while documented extension and - compatibility paths preserve older clients and metadata where safe. +### Must preserve runtime meaning + +- **R01 Independent session lifetime:** A session child and terminal state + continue independently of creating, attached, detached, or observing clients; + failure of one session or client does not implicitly terminate another. +- **R02 Durable launch context:** Initial launch, explicit restart, and + policy-driven respawn preserve command, arguments, working directory, initial + geometry, lifetime policy, labels, tags, and child-environment policy. + Exact-environment mode is exclusive with inherited/isolate policy; inherited + removals precede assignments. Ordinary assigned values, including an empty + `NO_COLOR`, remain exact. `PTY_SESSION` and its generation token are + runtime-owned; absent or empty `TERM` selects `xterm-256color`, while a + nonempty terminal name is preserved. Historical metadata without removals + retains ambient-inheritance behavior. +- **R03 Ordered, generation-safe lifecycle:** Child output is drained before + exit is finalized. Restart, permanent reconciliation, abandonment, explicit + removal, and cleanup follow explicit policy and cannot mutate or delete a + replacement generation. + +### Must reconstruct one shared terminal + +- **R04 Ordered reconstruction:** Every valid attach or recognized peek that + emits terminal state sends effective geometry, exactly one screen baseline, + then post-cut data and at most one process exit in source order. A later mode + request supersedes an unfinished generation; reconnect starts a new one. +- **R05 Replaceable client roles:** A complete `ATTACH` makes its socket + writable, installs requested geometry, and enables input and resize. A + recognized `PEEK` makes it readonly and removes its geometry constraint. A + malformed attach changes neither role nor synchronization generation. +- **R06 Deterministic geometry:** Effective rows and columns are the independent + minima requested by writable clients. Attach, resize, and disconnect + recompute them; readonly observation never constrains them. Geometry changes + are visible before terminal bytes produced for the new size. +- **R07 Bounded stream protocol:** Packets are length-delimited, fragmented + input is reassembled, oversized input is rejected without unbounded + buffering, and reconnect or unsupported capability failure is explicit. +- **R08 Machine attach outcomes:** Machine attach preserves the framed + `GEOMETRY`, `SCREEN`, `DATA`, and `EXIT` stream on a caller-owned inherited + descriptor while stdin/stdout remain the controlling terminal. A clean stream + ends with exactly one `EXIT` when the session process ended or empty `DETACH` + when this client intentionally detached; EOF without either is truncation. + Administrative destruction is truncation unless process `EXIT` was observed. + +### Must expose durable state through one behavioral core + +- **R09 Stable, inspectable registry:** Registry root and filename-safe stable + id are explicit. Display names and tags are presentation metadata. Inventory + and status expose lifecycle, clients, requested/effective geometry, resources, + and metadata without attaching or mutating the session. +- **R10 Durable, compatible records:** Metadata retains the launch and lifecycle + fields needed for inspection and restart, preserves unknown fields on update, + and uses generation-aware atomic replacement. Events are external-readable + JSONL records with bounded retention. Readers reject structurally invalid or + unbounded input while retaining documented legacy fallbacks. +- **R11 Equivalent supported surfaces:** CLI commands, exported client/server/ + protocol/testing APIs, the shipped package entrypoint, local transport, and + remote routing preserve the applicable runtime, stream, geometry, registry, + and lifecycle contracts. A surface rejects unsupported capabilities instead + of silently weakening them; tests use real PTYs and processes. diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md index 023b305..be74628 100644 --- a/docs/vrs/spec.md +++ b/docs/vrs/spec.md @@ -1,58 +1,187 @@ # pty specification -This document specifies the composition of the current `pty` system. It builds -on [requirements.md](./requirements.md). +This document specifies the current `pty` system. It builds on +[requirements.md](./requirements.md). ## Status -Active. The implementation and executable tests are the behavioral evidence; -this tree is their durable contract map. +Draft until every mapped contract is present on the default branch. The test +matrix below is the executable validation boundary. ## Scope -This tree defines persistent session execution, terminal-state transport, -registry state, and the supported package surfaces. It does not define a window -manager, shell, multi-tenant security boundary, or orchestration policy for -products that use `pty`. +This specification defines persistent session execution, ordered terminal +transport, registry state, and supported CLI/package surfaces. It does not +define a shell, window manager, multi-tenant authorization boundary, or product +orchestration policy. -## System composition +## Composition ```text -04 surfaces (CLI, package APIs, testing, remote) - | - +-----------------------+ - v v -02 ordered session stream 03 durable registry - | | - +-----------+-----------+ - v - 01 session runtime - | - v - child process + PTY +CLI / package / testing / remote surfaces + | + +---------+---------+ + | | + ordered client stream durable registry + | | + +---------+---------+ + | + per-session runtime + | + child process + PTY ``` -The dependency direction follows the numeric tree. The runtime owns the child -and terminal emulator. The stream projects ordered terminal state to clients. -The registry projects durable identity and observations. Surfaces compose these -contracts without redefining them. +The runtime owns the child process and headless terminal model. The stream +projects one ordered terminal to ephemeral clients. The registry owns stable +identity and durable observations. Every public surface composes these three +sources rather than defining alternate semantics (R01, R09, R11). -## Core invariants +## Runtime and launch -1. A session is identified by its stable registry id, not by a client, process - label, display name, or socket connection (PTY-R01, PTY-R07). -2. Terminal reconstruction is an ordered state transfer: effective geometry, - then one screen baseline, then post-baseline data or exit (PTY-R03, PTY-R04). -3. Durable metadata describes a launch and an observed generation; live status - is derived from metadata plus process/socket evidence (PTY-R05, PTY-R06). -4. Every surface either preserves the relevant underlying contract or rejects - the unsupported capability explicitly (PTY-R08, PTY-R09). +A session is a detached daemon containing one child PTY and one headless +terminal emulator. The daemon owns the socket and survives client disconnects +(R01). -## Subsystem ownership +Launch environment assembly is ordered (R02): -| Subsystem | Owns | Does not own | +```text +replacement mode: copy env + +inherited mode: process.env -> remove internal server config +isolated mode: allowlisted process.env + LC_* + +policy modes only: base -> unsetEnv[] -> extraEnv{} +all modes: -> force PTY_SESSION + generation token + -> TERM absent/empty ? xterm-256color : preserve value +``` + +Replacement mode and the inherited/isolate policy options are mutually +exclusive. Metadata persists the selected mode and its removals/assignments. +Explicit restart reuses it. Permanent reconciliation re-reads a current +manifest declaration when available and otherwise uses persisted metadata. +Metadata predating `unsetEnv` retains historical ambient inheritance. + +On child exit, the runtime drains accepted output, records final screen and exit +state, emits the lifecycle event, and applies cleanup policy. Every mutating +cleanup/restart path compares stable id plus generation so stale work cannot +change a replacement session (R03). + +## Ordered client stream + +Packets use a five-byte header followed by a bounded payload: + +```text +[type: uint8][length: uint32BE][payload: length bytes] +``` + +The reader reassembles partial input and rejects declared payloads above 32 MiB +(R07). Unknown bounded message types are ignored for additive compatibility; +capability-specific surfaces fail closed when required packets are absent. + +### Synchronization + +For each admitted attach/peek generation (R04): + +```text +parser bytes before cut | parser bytes after cut | process exit + | | | + v v v +GEOMETRY -> SCREEN -----------> queued DATA -------> EXIT +``` + +The screen callback is the causal cut: `SCREEN` represents all earlier parser +writes; later data and exit queue behind it. A newer valid mode request +invalidates the unfinished generation. Reconnect creates a fresh generation. +A local machine detach may end with `DETACH` before a baseline is emitted. + +### Roles and geometry + +Role frames replace, rather than accumulate, socket state (R05): + +| Frame | Resulting role | Geometry membership | Input/resize | +| --- | --- | --- | --- | +| complete `ATTACH(rows, cols)` | writable | requested rows/cols | enabled | +| recognized `PEEK(flags)` | readonly | none | disabled | +| malformed `ATTACH` | unchanged | unchanged | unchanged | +| `STATUS` | unchanged | unchanged | unchanged | + +For writable request set `W`, shared geometry is (R06): + +```text +rows = min(client.rows for client in W) +cols = min(client.cols for client in W) +``` + +The dimensions are minimized independently. A changed `GEOMETRY` notification +precedes terminal output produced after the corresponding PTY resize. Removing +the last writable client leaves the last effective geometry stable. + +### Machine attach + +`attach --attach-stream-fd-v1 ` requires an inherited writable +descriptor `fd >= 3`. The packaged CLI runs without a wrapper child so the +descriptor, controlling terminal, signals, and process identity reach the +adapter unchanged (R08, R11). + +The adapter reframes only `GEOMETRY`, `SCREEN`, `DATA`, and terminal outcomes to +the descriptor; terminal interaction stays on stdin/stdout and diagnostics use +stderr. It flushes exactly one clean outcome before EOF: + +| Outcome | Meaning | +| --- | --- | +| `EXIT(code)` | the session process ended | +| empty `DETACH` | this local client intentionally detached | +| EOF without either | transport loss, reconnect give-up, descriptor failure, or abrupt administrative destruction | + +The last row is a non-zero truncation, not a third clean outcome (R07, R08). + +## Registry and lifecycle state + +`PTY_ROOT` selects one registry. A stable id owns socket, metadata, events, and +generation locks; display names and tags remain mutable lookup/presentation +fields (R09). + +Inventory is observational: it derives running/exited/stale state and enriches +it with live status when available, but does not restart, reap, or attach. +Status reports client roles, requested/effective geometry, process resources, +and terminal modes. + +Metadata and events form two compatibility tiers (R10): + +| Record | Contract | +| --- | --- | +| metadata JSON | durable launch/lifecycle source; atomic generation-aware updates preserve unknown fields | +| event JSONL | externally readable observation stream; serialized append and bounded retention | +| socket packets | internal bounded protocol with documented legacy decoding fallbacks | + +Explicit lifecycle commands and `gc` own mutation. Cleanup is authorized by the +observed generation; removal wins over late daemon finalization, and permanent +respawn cannot overwrite a replacement (R03, R10). + +## Surfaces + +The CLI, package entrypoint, exported client/server/protocol modules, testing +library, and remote route call the same behavioral core (R11). Completion +schemas preserve required option values. Remote streaming preserves local +packet order and fails explicitly when the peer lacks a capability. The testing +library drives real processes and PTYs and exposes screen, cursor, scrollback, +input, resize, and multi-client geometry without mocks. + +## Ownership and validation matrix + +| Requirement | Owning source | Primary executable evidence | | --- | --- | --- | -| [session runtime](./01-session-runtime/spec.md) | child, PTY, emulator, launch context, exit and restart primitives | client presentation, registry discovery | -| [session stream](./02-session-stream/spec.md) | framing, connection roles, synchronization, geometry and exit order | session identity, restart policy | -| [registry](./03-registry/spec.md) | roots, stable ids, metadata, events, inventory and cleanup ownership | terminal parsing, CLI rendering | -| [surfaces](./04-surfaces/spec.md) | CLI/package/API composition, packaging, remote routing | alternate semantics for the three lower layers | +| R01 | [server](../../src/server.ts), [spawn](../../src/spawn.ts) | [integration](../../tests/integration.test.ts), [exit reap](../../tests/exit-reap.test.ts), [shutdown](../../tests/shutdown-backstop.test.ts) | +| R02 | [server](../../src/server.ts), [spawn](../../src/spawn.ts), [sessions](../../src/sessions.ts), [ptyfile](../../src/ptyfile.ts) | [spawn options](../../tests/spawn-options.test.ts), [restart parity](../../tests/restart-launch-parity.test.ts), [restart scrub](../../tests/restart-env-scrub.test.ts), [ptyfile](../../tests/ptyfile.test.ts) | +| R03 | [server](../../src/server.ts), [sessions](../../src/sessions.ts) | [kill](../../tests/kill-wait.test.ts), [immediate reuse](../../tests/rm-immediate-reuse.test.ts), [generation guard](../../tests/gc-generation-guard.test.ts), [exit signal](../../tests/exit-signal.test.ts) | +| R04 | [server](../../src/server.ts), [connection](../../src/connection.ts) | [integration](../../tests/integration.test.ts), [alternate screen](../../tests/screen-replay-altscreen.test.ts), [scrollback](../../tests/scrollback-fidelity.test.ts) | +| R05 | [server](../../src/server.ts) | [integration](../../tests/integration.test.ts) | +| R06 | [server](../../src/server.ts), [protocol](../../src/protocol.ts) | [effective geometry](../../tests/effective-geometry.test.ts), [resize](../../tests/resize-tui.test.ts), [status](../../tests/stats-cli.test.ts) | +| R07 | [protocol](../../src/protocol.ts), [connection](../../src/connection.ts), [remote](../../src/remote.ts) | [protocol](../../tests/protocol.test.ts), [connection](../../tests/connection.test.ts), [remote reconnect](../../tests/remote-reconnect.test.ts) | +| R08 | [client](../../src/client.ts), [CLI](../../src/cli.ts), [entrypoint](../../bin/pty) | [attach stream](../../tests/attach-stream.test.ts), [signals](../../tests/wrapper-signal-forwarding.test.ts) | +| R09 | [sessions](../../src/sessions.ts), [server](../../src/server.ts), [CLI](../../src/cli.ts) | [root](../../tests/pty-root.test.ts), [display name](../../tests/display-name.test.ts), [status](../../tests/stats-cli.test.ts), [list purity](../../tests/list-purity.test.ts) | +| R10 | [sessions](../../src/sessions.ts), [events](../../src/events.ts), [protocol](../../src/protocol.ts) | [atomic writes](../../tests/atomic-writes.test.ts), [metadata events](../../tests/metadata-events.test.ts), [events](../../tests/events.test.ts), [disk layout](../../tests/disk-layout-docs.test.ts) | +| R11 | [CLI](../../src/cli.ts), [client API](../../src/client-api.ts), [remote](../../src/remote.ts), [testing API](../../src/testing/index.ts) | [help](../../tests/help.test.ts), [completions](../../tests/completions.test.ts), [remote](../../tests/remote-fabric.test.ts), [screenshots](../../tests/screenshot.test.ts), [keys](../../tests/keys.test.ts) | + +`node scripts/verify-docs.ts --vrs-only` validates this two-document shape, +sequential requirement IDs, links, and complete requirement references. diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index 521c4aa..dac7e01 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -9,146 +9,53 @@ const projectRoot = path.join(__dirname, ".."); const docsPath = path.join(projectRoot, "docs", "testing.md"); const vrsRoot = path.join(projectRoot, "docs", "vrs"); -function collectMarkdown(dir: string): string[] { - return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - const target = path.join(dir, entry.name); - return entry.isDirectory() - ? collectMarkdown(target) - : entry.isFile() && entry.name.endsWith(".md") - ? [target] - : []; - }); -} - -function collectVrsNodeDirectories(dir: string, companions: Set): string[] { - return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - if (!entry.isDirectory() || companions.has(entry.name)) return []; - const target = path.join(dir, entry.name); - return [target, ...collectVrsNodeDirectories(target, companions)]; - }); -} - function verifyVrs(): void { if (!fs.existsSync(vrsRoot)) return; - const files = collectMarkdown(vrsRoot); + const expected = ["requirements.md", "spec.md"]; + const actual = fs.readdirSync(vrsRoot).sort(); + const requirementsPath = path.join(vrsRoot, "requirements.md"); + const specPath = path.join(vrsRoot, "spec.md"); const errors: string[] = []; - const contents = new Map(files.map((file) => [file, fs.readFileSync(file, "utf-8")])); - const requirementDefinitions = new Map(); - const requirementIdsByFile = new Map(); - const companionDirectories = new Set([ - ".decisions", - ".experiments", - ".reference", - ".delta", - ".proposed", - ]); - - for (const file of files.filter((candidate) => path.basename(candidate) === "requirements.md")) { - const content = contents.get(file)!; - const definitionPattern = /^- \*\*(PTY(?:\.[A-Z]+)*-R\d+)\s+[^*]+\*\*/gm; - let definition: RegExpExecArray | null; - const ids: string[] = []; - while ((definition = definitionPattern.exec(content)) !== null) { - const id = definition[1]; - ids.push(id); - const prior = requirementDefinitions.get(id); - if (prior) errors.push(`duplicate requirement ${id}: ${prior} and ${file}`); - requirementDefinitions.set(id, file); - } - requirementIdsByFile.set(file, ids); - if (ids.length === 0) errors.push(`${file} defines no scoped PTY requirement IDs`); - const namespaces = new Set(ids.map((id) => id.slice(0, id.lastIndexOf("-R")))); - if (namespaces.size > 1) errors.push(`${file} mixes requirement namespaces`); - const sequence = ids.map((id) => Number(id.match(/-R(\d+)$/)?.[1])); - if (!sequence.every((number, index) => number === index + 1)) { - errors.push(`${file} requirement IDs must be sequential in document order`); - } - } - - for (const file of files) { - const content = contents.get(file)!; - const relative = path.relative(projectRoot, file); - - for (const entry of fs.readdirSync(path.dirname(file), { withFileTypes: true })) { - if ( - entry.isDirectory() && - !companionDirectories.has(entry.name) && - !/^\d{2}-[a-z0-9-]+$/.test(entry.name) - ) { - errors.push(`${relative}: subsystem directory ${entry.name} needs a numeric prefix`); - } - } - - const linkPattern = /\]\(([^)]+\.md)(?:#[^)]+)?\)/g; - let link: RegExpExecArray | null; - while ((link = linkPattern.exec(content)) !== null) { - if (/^[a-z]+:/i.test(link[1])) continue; - const target = path.resolve(path.dirname(file), link[1]); - if (!fs.existsSync(target)) errors.push(`${relative}: broken link ${link[1]}`); - } - if (path.basename(file) === "spec.md") { - if (!content.includes("[requirements.md](./requirements.md)")) { - errors.push(`${relative}: spec must build on its sibling requirements.md`); - } - if (!content.includes("## Status")) errors.push(`${relative}: spec must declare Status`); - } - if (path.basename(file) === "ontology.md" && !content.includes("## Language")) { - errors.push(`${relative}: ontology must define a Language section`); - } - if (path.basename(file) === "intuition.md" && !/\*For:[^*]+ ·\s*Assumes:[^*]+ ·\s*Covers:[^*]+\*/.test(content)) { - errors.push(`${relative}: intuition must declare For, Assumes, and Covers`); - } + if (actual.join("\n") !== expected.join("\n")) { + errors.push(`docs/vrs must contain only ${expected.join(" and ")}`); } + if (!fs.existsSync(requirementsPath) || !fs.existsSync(specPath)) { + errors.push("docs/vrs requires requirements.md and spec.md"); + } else { + const requirements = fs.readFileSync(requirementsPath, "utf-8"); + const spec = fs.readFileSync(specPath, "utf-8"); + const ids = [...requirements.matchAll(/^- \*\*(R\d{2}) [^*]+:\*\*/gm)].map( + (match) => match[1], + ); - const requirementsFiles = files.filter((file) => path.basename(file) === "requirements.md"); - for (const file of requirementsFiles) { - if (file === path.join(vrsRoot, "requirements.md")) continue; - const content = contents.get(file)!; - const parentFile = path.join(path.dirname(path.dirname(file)), "requirements.md"); - const parentContent = contents.get(parentFile); - if (!parentContent) { - errors.push(`${path.relative(projectRoot, file)}: missing direct parent requirements.md`); - continue; + if (ids.length === 0) errors.push("requirements.md defines no requirement IDs"); + if (!ids.every((id, index) => id === `R${String(index + 1).padStart(2, "0")}`)) { + errors.push("requirement IDs must be sequential in document order"); } - const parentIds = new Set(requirementIdsByFile.get(parentFile) ?? []); - const namespace = requirementIdsByFile.get(file)?.[0]?.replace(/-R\d+$/, ""); - const parentNamespace = requirementIdsByFile.get(parentFile)?.[0]?.replace(/-R\d+$/, ""); - if (namespace && parentNamespace && !new RegExp(`^${parentNamespace.replaceAll(".", "\\.")}\\.[A-Z]+$`).test(namespace)) { - errors.push(`${path.relative(projectRoot, file)}: namespace ${namespace} must extend direct parent ${parentNamespace}`); + if (!spec.includes("[requirements.md](./requirements.md)")) { + errors.push("spec.md must link its requirements.md"); } - const requirementBlocks = content.split(/(?=^- \*\*PTY(?:\.[A-Z]+)*-R\d+\s+)/m).slice(1); - for (const block of requirementBlocks) { - const id = block.match(/^- \*\*(PTY(?:\.[A-Z]+)*-R\d+)\s+/)?.[1]; - const refinementClause = block.match(/_refines:\s*([^_]+?)\._/s)?.[1]; - const refinementIds = refinementClause - ? [...refinementClause.matchAll(/PTY(?:\.[A-Z]+)*-R\d+/g)].map((match) => match[0]) - : []; - if (id && refinementIds.length === 0) { - errors.push(`${path.relative(projectRoot, file)}: ${id} must declare _refines:_`); - } else if (id && !refinementIds.every((refinement) => parentIds.has(refinement))) { - errors.push(`${path.relative(projectRoot, file)}: ${id} may refine only direct-parent requirements`); - } - } - } + if (!spec.includes("## Status")) errors.push("spec.md must declare Status"); - const nodeDirs = new Set([ - vrsRoot, - ...collectVrsNodeDirectories(vrsRoot, companionDirectories), - ]); - for (const dir of nodeDirs) { - for (const required of ["requirements.md", "spec.md"]) { - if (!fs.existsSync(path.join(dir, required))) { - errors.push(`${path.relative(projectRoot, dir)}: VRS node is missing ${required}`); - } + const references = new Set([...spec.matchAll(/\bR\d{2}\b/g)].map((match) => match[0])); + for (const id of ids) { + if (!references.has(id)) errors.push(`spec.md does not reference ${id}`); + } + for (const id of references) { + if (!ids.includes(id)) errors.push(`spec.md references unknown requirement ${id}`); } - } - for (const [file, content] of contents) { - for (const match of content.matchAll(/PTY(?:\.[A-Z]+)*-R\d+/g)) { - if (!requirementDefinitions.has(match[0])) { - errors.push(`${path.relative(projectRoot, file)}: unknown requirement reference ${match[0]}`); + for (const [file, content] of [ + [requirementsPath, requirements], + [specPath, spec], + ] as const) { + for (const match of content.matchAll(/\]\(([^)#]+)(?:#[^)]+)?\)/g)) { + if (/^[a-z]+:/i.test(match[1])) continue; + if (!fs.existsSync(path.resolve(path.dirname(file), match[1]))) { + errors.push(`${path.relative(projectRoot, file)} has broken link ${match[1]}`); + } } } } @@ -157,7 +64,7 @@ function verifyVrs(): void { console.error(`VRS verification failed:\n${errors.map((error) => `- ${error}`).join("\n")}`); process.exit(1); } - console.log(`Verified structural shape of ${files.length} VRS documents and ${requirementDefinitions.size} requirement IDs`); + console.log("Verified 2 VRS documents and 11 requirement IDs"); } verifyVrs(); diff --git a/src/server.ts b/src/server.ts index 4254a0b..1daf96f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -116,7 +116,7 @@ export interface ServerOptions { * * Mutually exclusive with `isolateEnv` / `extraEnv` / `unsetEnv` — passing * `env` together with inherited-environment policy throws. Use this when - * the caller wants exact ordinary environment keys (e.g., a + * the caller wants total control of the child environment (e.g., a * launcher shell that injects a shim tmux on `PATH`). */ env?: Record; } @@ -142,21 +142,21 @@ const ISOLATED_ENV_ALLOWLIST = new Set([ * Shift+Enter is indistinguishable from Enter. */ const DEFAULT_CHILD_TERM = "xterm-256color"; -/** Apply the TERM default in-place after the env has been assembled. A - * nonempty terminal name is preserved; absence and empty both select the - * runtime default. */ +/** Apply the TERM default in-place after the env has been assembled. Never + * overrides an explicit value — only fills in when it's absent. */ function ensureChildTerm(env: Record): void { if (!env.TERM) env.TERM = DEFAULT_CHILD_TERM; } function buildChildEnv(options: ServerOptions): Record { - // Mutual exclusion: the replacement-base `env` can't be combined with the - // inherited-environment policy path. Otherwise isolation, removals, and - // assignments compose explicitly. Picking one implicitly would hide intent. + // Mutual exclusion: `env` (explicit, verbatim) can't be combined with the + // inherited-environment policy path. If you want total control you pass + // `env`; otherwise isolation/removals/assignments compose explicitly. Picking + // one implicitly would hide intent. if (options.env && (options.isolateEnv || options.extraEnv || options.unsetEnv?.length)) { throw new Error( "ServerOptions.env is mutually exclusive with isolateEnv/extraEnv/unsetEnv. " + - "Use env as a replacement base, or inherited environment policy options — not both." + "Use env for verbatim control, or inherited environment policy options — not both." ); } @@ -526,7 +526,7 @@ export class PtyServer { // NOTE: intentionally no `name:` option here — node-pty's `name` // unconditionally clobbers env.TERM, which would hide any TERM the // caller inherited or set explicitly. `buildChildEnv` guarantees - // childEnv.TERM is populated (defaulting if absent or empty), + // childEnv.TERM is populated (defaulting to xterm-256color if absent), // so node-pty will pick it up naturally. Was `name: "xterm-256color"` // before; removing it lets inherited values like `xterm-kitty` flow // through and lets TUIs negotiate the richer capabilities they allow. diff --git a/src/spawn.ts b/src/spawn.ts index ff2788e..0f77ac9 100644 --- a/src/spawn.ts +++ b/src/spawn.ts @@ -41,10 +41,9 @@ export interface SpawnDaemonOptions { extraEnv?: Record; /** Environment keys removed from inheritance before `extraEnv` is applied. */ unsetEnv?: string[]; - /** Use this env dict as the child environment's replacement base — no - * inheritance from the daemon's `process.env`, no allow-list. Runtime - * invariants still force `PTY_SESSION` and normalize an absent or empty - * `TERM`. + /** Use this env dict verbatim for the spawned child — no inheritance from + * the daemon's `process.env`, no allow-list. `PTY_SESSION` is always + * injected on top so nesting detection and `pty exec` keep working. * * Mutually exclusive with `isolateEnv` / `extraEnv` / `unsetEnv` — passing * `env` together with inherited-environment policy will throw at startup. */ @@ -141,7 +140,7 @@ export async function spawnDaemon(options: SpawnDaemonOptions): Promise { if (options.env && (options.isolateEnv || options.extraEnv || options.unsetEnv?.length)) { throw new Error( "SpawnDaemonOptions.env is mutually exclusive with isolateEnv/extraEnv/unsetEnv. " + - "Use env as a replacement base, or inherited environment policy options — not both.", + "Use env for verbatim control, or inherited environment policy options — not both.", ); } const strategy = resolveSpawnStrategy(); From 96ba602d7683a71fe818f69125328e3a80be93d0 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:27:04 +0200 Subject: [PATCH 3/5] docs: enforce pty VRS vocabulary --- docs/vrs/spec.md | 2 +- scripts/verify-docs.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md index be74628..8f06c63 100644 --- a/docs/vrs/spec.md +++ b/docs/vrs/spec.md @@ -141,7 +141,7 @@ The last row is a non-zero truncation, not a third clean outcome (R07, R08). generation locks; display names and tags remain mutable lookup/presentation fields (R09). -Inventory is observational: it derives running/exited/stale state and enriches +Inventory is observational: it derives running/exited/vanished state and enriches it with live status when available, but does not restart, reap, or attach. Status reports client roles, requested/effective geometry, process resources, and terminal modes. diff --git a/scripts/verify-docs.ts b/scripts/verify-docs.ts index dac7e01..7bc0213 100644 --- a/scripts/verify-docs.ts +++ b/scripts/verify-docs.ts @@ -10,7 +10,10 @@ const docsPath = path.join(projectRoot, "docs", "testing.md"); const vrsRoot = path.join(projectRoot, "docs", "vrs"); function verifyVrs(): void { - if (!fs.existsSync(vrsRoot)) return; + if (!fs.existsSync(vrsRoot)) { + console.error("VRS verification failed:\n- docs/vrs is missing"); + process.exit(1); + } const expected = ["requirements.md", "spec.md"]; const actual = fs.readdirSync(vrsRoot).sort(); From 118d5ce3100c9d3e2fb3a74475f37d86327fa8bb Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:33:26 +0200 Subject: [PATCH 4/5] docs: specify live daemon recovery --- docs/vrs/requirements.md | 14 +++++++++++--- docs/vrs/spec.md | 27 ++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/vrs/requirements.md b/docs/vrs/requirements.md index 676b231..783b45b 100644 --- a/docs/vrs/requirements.md +++ b/docs/vrs/requirements.md @@ -46,7 +46,9 @@ implementation contract and validation map live in [spec.md](./spec.md). - **R03 Ordered, generation-safe lifecycle:** Child output is drained before exit is finalized. Restart, permanent reconciliation, abandonment, explicit removal, and cleanup follow explicit policy and cannot mutate or delete a - replacement generation. + replacement generation. Recovery after external registry unlink rebinds the + same supporting daemon and child without signaling, restarting, relaunching, + duplicating the provider, or disconnecting existing clients. ### Must reconstruct one shared terminal @@ -77,12 +79,18 @@ implementation contract and validation map live in [spec.md](./spec.md). - **R09 Stable, inspectable registry:** Registry root and filename-safe stable id are explicit. Display names and tags are presentation metadata. Inventory and status expose lifecycle, clients, requested/effective geometry, resources, - and metadata without attaching or mutating the session. + metadata, and any live-recovery capability without attaching or mutating the + session. - **R10 Durable, compatible records:** Metadata retains the launch and lifecycle fields needed for inspection and restart, preserves unknown fields on update, and uses generation-aware atomic replacement. Events are external-readable JSONL records with bounded retention. Readers reject structurally invalid or - unbounded input while retaining documented legacy fallbacks. + unbounded input while retaining documented legacy fallbacks. Live recovery + authenticates the current private registry root, recovery directory, + generation, daemon process, launch identity, and metadata revision; it fails + closed on stale, replayed, interrupted-publication, tampered, wrong-root, or + path-replacement attempts while allowing an authenticated interrupted lock to + resume. - **R11 Equivalent supported surfaces:** CLI commands, exported client/server/ protocol/testing APIs, the shipped package entrypoint, local transport, and remote routing preserve the applicable runtime, stream, geometry, registry, diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md index 8f06c63..e652bd9 100644 --- a/docs/vrs/spec.md +++ b/docs/vrs/spec.md @@ -158,6 +158,27 @@ Explicit lifecycle commands and `gc` own mutation. Cleanup is authorized by the observed generation; removal wins over late daemon finalization, and permanent respawn cannot overwrite a replacement (R03, R10). +### Live registry recovery + +A supporting daemon may publish an opaque recovery capability only when it can +prove its process-start identity and both `PTY_ROOT` and `.recovery` are private +directories owned by the daemon user. If an external cleanup unlinks that live +session's socket, pid, and metadata paths, `recover --snapshot` authenticates a +complete retained metadata snapshot and asks the original daemon to rebind its +listener. It preserves the daemon generation, child process, provider launch, +terminal state, and attached clients; it does not probe by signal, restart, +relaunch, or replace an occupied pathname (R03, R09). + +The request/result exchange binds stable id, daemon pid and process-start token, +generation, launch identity, root and recovery-directory device/inode identity, +and the daemon's signed metadata revision. Metadata mutation advances the signed +revision before publishing the replacement record. Recovery therefore fails +closed after a partial publication and rejects missing, legacy, stale, replayed, +tampered, wrong-root, permission-downgraded, or path-replacement state. Success +atomically republishes the registry state and rotates the recovery secret. An +authenticated lock left by an interrupted recoverer may resume; other creation +locks remain authoritative and are never displaced (R10). + ## Surfaces The CLI, package entrypoint, exported client/server/protocol modules, testing @@ -173,14 +194,14 @@ input, resize, and multi-client geometry without mocks. | --- | --- | --- | | R01 | [server](../../src/server.ts), [spawn](../../src/spawn.ts) | [integration](../../tests/integration.test.ts), [exit reap](../../tests/exit-reap.test.ts), [shutdown](../../tests/shutdown-backstop.test.ts) | | R02 | [server](../../src/server.ts), [spawn](../../src/spawn.ts), [sessions](../../src/sessions.ts), [ptyfile](../../src/ptyfile.ts) | [spawn options](../../tests/spawn-options.test.ts), [restart parity](../../tests/restart-launch-parity.test.ts), [restart scrub](../../tests/restart-env-scrub.test.ts), [ptyfile](../../tests/ptyfile.test.ts) | -| R03 | [server](../../src/server.ts), [sessions](../../src/sessions.ts) | [kill](../../tests/kill-wait.test.ts), [immediate reuse](../../tests/rm-immediate-reuse.test.ts), [generation guard](../../tests/gc-generation-guard.test.ts), [exit signal](../../tests/exit-signal.test.ts) | +| R03 | [server](../../src/server.ts), [sessions](../../src/sessions.ts), [recovery](../../src/recovery.ts) | [kill](../../tests/kill-wait.test.ts), [immediate reuse](../../tests/rm-immediate-reuse.test.ts), [generation guard](../../tests/gc-generation-guard.test.ts), [exit signal](../../tests/exit-signal.test.ts), [recovery](../../tests/recovery.test.ts) | | R04 | [server](../../src/server.ts), [connection](../../src/connection.ts) | [integration](../../tests/integration.test.ts), [alternate screen](../../tests/screen-replay-altscreen.test.ts), [scrollback](../../tests/scrollback-fidelity.test.ts) | | R05 | [server](../../src/server.ts) | [integration](../../tests/integration.test.ts) | | R06 | [server](../../src/server.ts), [protocol](../../src/protocol.ts) | [effective geometry](../../tests/effective-geometry.test.ts), [resize](../../tests/resize-tui.test.ts), [status](../../tests/stats-cli.test.ts) | | R07 | [protocol](../../src/protocol.ts), [connection](../../src/connection.ts), [remote](../../src/remote.ts) | [protocol](../../tests/protocol.test.ts), [connection](../../tests/connection.test.ts), [remote reconnect](../../tests/remote-reconnect.test.ts) | | R08 | [client](../../src/client.ts), [CLI](../../src/cli.ts), [entrypoint](../../bin/pty) | [attach stream](../../tests/attach-stream.test.ts), [signals](../../tests/wrapper-signal-forwarding.test.ts) | -| R09 | [sessions](../../src/sessions.ts), [server](../../src/server.ts), [CLI](../../src/cli.ts) | [root](../../tests/pty-root.test.ts), [display name](../../tests/display-name.test.ts), [status](../../tests/stats-cli.test.ts), [list purity](../../tests/list-purity.test.ts) | -| R10 | [sessions](../../src/sessions.ts), [events](../../src/events.ts), [protocol](../../src/protocol.ts) | [atomic writes](../../tests/atomic-writes.test.ts), [metadata events](../../tests/metadata-events.test.ts), [events](../../tests/events.test.ts), [disk layout](../../tests/disk-layout-docs.test.ts) | +| R09 | [sessions](../../src/sessions.ts), [server](../../src/server.ts), [recovery](../../src/recovery.ts), [CLI](../../src/cli.ts) | [root](../../tests/pty-root.test.ts), [display name](../../tests/display-name.test.ts), [status](../../tests/stats-cli.test.ts), [list purity](../../tests/list-purity.test.ts), [recovery](../../tests/recovery.test.ts) | +| R10 | [sessions](../../src/sessions.ts), [events](../../src/events.ts), [recovery](../../src/recovery.ts), [protocol](../../src/protocol.ts) | [atomic writes](../../tests/atomic-writes.test.ts), [metadata events](../../tests/metadata-events.test.ts), [events](../../tests/events.test.ts), [recovery](../../tests/recovery.test.ts), [disk layout](../../tests/disk-layout-docs.test.ts) | | R11 | [CLI](../../src/cli.ts), [client API](../../src/client-api.ts), [remote](../../src/remote.ts), [testing API](../../src/testing/index.ts) | [help](../../tests/help.test.ts), [completions](../../tests/completions.test.ts), [remote](../../tests/remote-fabric.test.ts), [screenshots](../../tests/screenshot.test.ts), [keys](../../tests/keys.test.ts) | `node scripts/verify-docs.ts --vrs-only` validates this two-document shape, From 550109f7d06a65c8db46ad1e935552d1cc025139 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:37:54 +0200 Subject: [PATCH 5/5] docs: state recovery publication semantics --- docs/vrs/spec.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md index e652bd9..3354ec9 100644 --- a/docs/vrs/spec.md +++ b/docs/vrs/spec.md @@ -175,7 +175,8 @@ and the daemon's signed metadata revision. Metadata mutation advances the signed revision before publishing the replacement record. Recovery therefore fails closed after a partial publication and rejects missing, legacy, stale, replayed, tampered, wrong-root, permission-downgraded, or path-replacement state. Success -atomically republishes the registry state and rotates the recovery secret. An +republishes the socket, pid, and metadata with no-replace and owned-rollback +semantics and rotates the recovery secret. An authenticated lock left by an interrupted recoverer may resume; other creation locks remain authoritative and are never displaced (R10).