@timurproko/a1 0.2.7-dev.792 → 0.2.7-dev.794
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell.d.ts +1 -0
- package/dist/app/session-shell/session-shell.js +10 -0
- package/dist/cli/tabs.js +1 -1
- package/dist/composition/owned-ui.d.ts +1 -0
- package/dist/composition/owned-ui.js +16 -3
- package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
- package/dist/features/resident-tab/shortcut-declarations.js +1 -0
- package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
- package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -1
- package/dist/foundation/launch-guardian/main.js +58 -6
- package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
- package/dist/foundation/process-containment/addon-containment.js +139 -0
- package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
- package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
- package/dist/foundation/process-containment/addon.d.ts +42 -0
- package/dist/foundation/process-containment/addon.js +57 -0
- package/dist/foundation/process-containment/artifact.d.ts +1 -1
- package/dist/foundation/process-containment/artifact.js +6 -5
- package/dist/foundation/process-containment/contracts.d.ts +2 -0
- package/dist/foundation/process-containment/index.d.ts +4 -4
- package/dist/foundation/process-containment/index.js +3 -3
- package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/native-process-inspector.js +10 -10
- package/dist/foundation/release/bootstrap.js +53 -16
- package/dist/foundation/release/index.d.ts +1 -1
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/release-store.d.ts +12 -0
- package/dist/foundation/release/release-store.js +124 -2
- package/dist/foundation/resident-tabs/bounded-host-command.d.ts +3 -1
- package/dist/foundation/resident-tabs/bounded-host-command.js +6 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +2 -0
- package/dist/foundation/resident-tabs/bridge-client.js +13 -0
- package/dist/foundation/resident-tabs/doctor.d.ts +0 -1
- package/dist/foundation/resident-tabs/doctor.js +4 -4
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +7 -2
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +15 -11
- package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
- package/dist/foundation/resident-tabs/host-entry.js +17 -0
- package/dist/foundation/resident-tabs/index.d.ts +2 -2
- package/dist/foundation/resident-tabs/index.js +1 -1
- package/dist/foundation/resident-tabs/maintenance.d.ts +5 -4
- package/dist/foundation/resident-tabs/maintenance.js +32 -17
- package/dist/foundation/resident-tabs/release-holds.d.ts +3 -2
- package/dist/foundation/resident-tabs/release-holds.js +16 -7
- package/dist/foundation/resident-tabs/settings.d.ts +0 -1
- package/dist/foundation/resident-tabs/settings.js +1 -7
- package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +11 -15
- package/dist/foundation/resident-tabs/terminal-host-artifact.js +57 -32
- package/dist/foundation/session-writer-lock/index.d.ts +1 -3
- package/dist/foundation/session-writer-lock/index.js +8 -6
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -0
- package/dist/native/darwin-arm64/manifest.json +8 -6
- package/dist/native/darwin-arm64/process-guardian.node +0 -0
- package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +14 -13
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +9 -7
- package/dist/native/darwin-x64/process-guardian.node +0 -0
- package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +15 -14
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +8 -6
- package/dist/native/linux-x64/process-guardian.node +0 -0
- package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +14 -13
- package/dist/native/linux-x64/{terminal-host → terminal-host.node} +0 -0
- package/dist/native/win32-x64/manifest.json +8 -6
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/win32-x64/terminal-host.manifest.json +14 -13
- package/dist/native/win32-x64/{terminal-host.exe → terminal-host.node} +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +3 -2
- package/dist/runtime-payload-inventory.json +1 -0
- package/dist/ui/settings/declarations.d.ts +0 -12
- package/dist/ui/settings/declarations.js +0 -10
- package/docs/architecture/boundaries.md +10 -9
- package/docs/architecture/process-guardian-provenance.md +17 -11
- package/docs/architecture/resident-tabs.md +36 -34
- package/docs/architecture/terminal-host-provenance.md +5 -3
- package/docs/architecture/toolchain.md +6 -6
- package/docs/openspec-archive-automation.md +3 -3
- package/docs/validation.md +5 -1
- package/package.json +1 -1
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
- package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
- package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
- package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
- package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/darwin-arm64/terminal-host +0 -0
- package/dist/native/darwin-x64/a1_session_lock.node +0 -0
- package/dist/native/darwin-x64/process-guardian +0 -0
- package/dist/native/darwin-x64/terminal-host +0 -0
- package/dist/native/linux-x64/a1_session_lock.node +0 -0
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/a1_session_lock.node +0 -0
- package/dist/native/win32-x64/process-guardian.exe +0 -0
|
@@ -4,7 +4,7 @@ Status: implemented. Milestones 1–6 deliver resident tabs as the bare-`a1` rou
|
|
|
4
4
|
|
|
5
5
|
## Context
|
|
6
6
|
|
|
7
|
-
Today bare `a1` runs as a chain: `bin/cli.js`, then a detached per-release **supervisor**, then the foreground **launch guardian**, then
|
|
7
|
+
Today bare `a1` runs as a chain: `bin/cli.js`, then a detached per-release **supervisor**, then the foreground Node **launch guardian**, then its in-process native **process-guardian addon**, then `bin/ui.js`. That last Node process hosts one in-process Pi `AgentSessionRuntime` behind the owned UI. The addon puts the UI in kill-on-close containment, so closing the terminal kills the agent; no process-guardian executable sits in the chain. `launch-instance-lifecycle` requires that behavior and reserves "a separately specified explicit resident capability" for anything that must survive. This design defines that capability.
|
|
8
8
|
|
|
9
9
|
The user set three product constraints:
|
|
10
10
|
|
|
@@ -14,12 +14,12 @@ The user set three product constraints:
|
|
|
14
14
|
|
|
15
15
|
A structured, message-based agent protocol cannot meet (2), because an extension's custom UI component is code running inside the agent process. It also forces a second, unrelated system for (3). Every tab is therefore a **terminal session**: a pseudoterminal running the complete A1 owned UI in "tab mode". Native extension components retain the declared text-terminal behavior; inline image protocols are explicitly outside the first version and use the existing text fallback.
|
|
16
16
|
|
|
17
|
-
The first version is deliberately narrower than the eventual product. It
|
|
17
|
+
The first version is deliberately narrower than the eventual product. It initially targeted Windows x64, Linux x64, and macOS arm64, with macOS x64 added by the later recorded decision; those are the targets A1 packages for the native addons. Resident tabs are the bare-`a1` route and argument-free `a1 solo` is the explicit direct-mode opt-out. Restoring tabs after reboot or logout, another architecture or platform, automatic resident-cohort upgrades, arbitrary CLI tabs, and split layouts each require a later change and their own evidence.
|
|
18
18
|
|
|
19
19
|
Existing assets:
|
|
20
20
|
|
|
21
21
|
- **`native/terminal-host`** is a working in-terminal host. It uses `libghostty-vt` pinned at `c5a21edf`, `portable-pty` 0.9.0 (ConPTY on Windows, Unix PTYs elsewhere), crossterm 0.29, and an A1-owned damage-aware frame composer. Its retained model and native encoders came from the fixed 2×2 proof; milestone 2 removed that interactive proof path and moved the model into the resident holder/attach roles. Three-platform certification remains separate from implementation.
|
|
22
|
-
- **The supervisor, process-guardian, and launch guardian** provide the patterns for detached spawn,
|
|
22
|
+
- **The supervisor, process-guardian addon, and launch guardian** provide the patterns for detached spawn, addon-backed process-start inspection, an endpoint probe with join-the-winner, and atomic metadata. The process-guardian addon has Windows, macOS, and Linux implementations.
|
|
23
23
|
- **The 2×2 spike evidence** in [`evidence/terminal-host-spike/`](evidence/terminal-host-spike/) is historical. Its verdicts stay pending and are not evidence for this design.
|
|
24
24
|
|
|
25
25
|
References:
|
|
@@ -53,8 +53,8 @@ The read-only review used A1 `develop` at `b42b3c9e`, PR #586 at `8fdb7361`, `E:
|
|
|
53
53
|
### Milestone 2 pinned launch contract (2026-10-11)
|
|
54
54
|
|
|
55
55
|
- Bare `a1` from a terminal always requests the resident tab route; with piped or redirected standard input or output it runs the direct UI without probing the host, because the attach client needs a terminal to size and own. Tab enablement is not persisted or configurable; host discovery or verification failure uses the bounded direct single-agent fallback.
|
|
56
|
-
-
|
|
57
|
-
- The host artifact resolves to `dist/native/<platform>-<arch>/
|
|
56
|
+
- The `tabsAutoName` setting lives in the flat `a1` object of the global agent `settings.json`. A stored legacy `tabsMax` value is preserved as unknown compatibility data but never limits creation or appears in Settings.
|
|
57
|
+
- The host artifact resolves to `dist/native/<platform>-<arch>/terminal-host.node`, with hermetic `A1_TERMINAL_HOST_PATH` only when `A1_DEV_INSTANCE_ID` is present; the fixed role entry is `dist/foundation/resident-tabs/host-entry.js`, and discovery may use `A1_TABS_ENDPOINT`.
|
|
58
58
|
- Tab children consume and erase `A1_TAB_ID`, `A1_TAB_TOKEN`, and `A1_TAB_ENDPOINT` before loading A1, Pi, extensions, or tools.
|
|
59
59
|
- Client-scoped bridge detach binds `clientId` and `controllerRevision` captured when the command is admitted. The additive generation-1 `attachment` and `bridge.request` fixtures freeze that shape.
|
|
60
60
|
- Launch fallback reasons are bounded to unsupported platform, no interactive terminal, missing artifact, failed probe, incompatible generation, unreadable registry, blocked recovery, unverified launch, and startup timeout. Fallback preserves resident records and presents exactly one direct-session notice.
|
|
@@ -66,10 +66,10 @@ The read-only review used A1 `develop` at `b42b3c9e`, PR #586 at `8fdb7361`, `E:
|
|
|
66
66
|
|
|
67
67
|
- The native `core/server.rs` and `core/holder.rs` state machines are deterministic and I/O-free; the production holder uses the holder core for bounded input admission and ordered writer effects, and the production server uses the server core for heartbeat supervision. Blocking identity/termination work runs through the bounded worker shell with deadlines and stale-incarnation filtering.
|
|
68
68
|
- Every attach, server, and holder role has an independent 15-second watchdog. Watchdog records are bounded, timestamped, owner-only metadata under the profile runtime directory; fault-only role freezes are refused by non-test release builds.
|
|
69
|
-
- Holders read PTY output continuously, use one bounded input authority, reserve complete pastes before delivery, and emit a typed uncertain-delivery result after an admitted write failure. Server-to-client control lanes are frame/byte bounded
|
|
69
|
+
- Holders read PTY output continuously, use one bounded input authority, reserve complete pastes before delivery, and emit a typed uncertain-delivery result after an admitted write failure. Each holder independently refreshes the server's latest complete retained surface even while hidden. Server-to-client control lanes are frame/byte bounded, render lanes retain one replaceable full current surface, and only the viewed tab is delivered to each attach client.
|
|
70
70
|
- Server replacement is serialized by `server-start.lock` in the profile runtime directory. Recovery verifies the recorded native owner before termination, waits boundedly for the registry-writer lease, increments the durable epoch, derives holder credentials, and re-admits only exact holder/child identities and incarnations. A durable three-start/sixty-second budget stops automatic replacement; explicit `--restart` clears it.
|
|
71
71
|
- Registry records now retain holder and child pid/start identities and holder incarnation. Replacement treats unverifiable records as `holder-lost`; it never starts a duplicate writer from uncertain state.
|
|
72
|
-
-
|
|
72
|
+
- Concurrent starts use an internal default of 2 and hard cap of 8. The server spaces starts by at least 250 ms and requests graceful tab shutdown before verified forced termination; open-tab count has no user-visible quota.
|
|
73
73
|
- All A1-owned Pi runtimes use one profile-neutral session-writer guard regardless of launch mode. A new target is reserved by canonical location, created as an empty owner-only placeholder, bound to stable file identity without a release gap, and only then exposed to Pi for session content. Runtime replacement acquires and binds the target before publishing it or releasing the prior writer; conflict paths preserve the source and may explicitly fork.
|
|
74
74
|
- The generation-1 holder transport continues to use the authenticated owner endpoint and replaceable full surfaces. Dedicated holder endpoints and patch deltas remain later optimizations and are not milestone-3 claims.
|
|
75
75
|
|
|
@@ -130,13 +130,13 @@ Alternatives considered:
|
|
|
130
130
|
- it duplicates a terminal core that already exists natively;
|
|
131
131
|
- v2 recorded stalls from exactly this design.
|
|
132
132
|
|
|
133
|
-
### 2. One native
|
|
133
|
+
### 2. One native addon, three Node-hosted roles, plus the A1 child
|
|
134
134
|
|
|
135
135
|
```text
|
|
136
136
|
terminal (detached, per OS user × profile)
|
|
137
137
|
┌──────────────────────────┠┌─────────────────────────────────────â”
|
|
138
|
-
│ a1 → guardian → │ │
|
|
139
|
-
│
|
|
138
|
+
│ a1 → guardian → │ │ node host-entry server │
|
|
139
|
+
│ node host-entry attach │◀───▶│ registry (atomic JSON, fsync) │
|
|
140
140
|
│ tab strip + active tab │ pipe│ topology, client fan-out, restore │
|
|
141
141
|
│ outer terminal I/O │ └───┬──────────────┬──────────────┬───┘
|
|
142
142
|
└──────────────────────────┘ │pipe │pipe │pipe
|
|
@@ -150,6 +150,8 @@ Alternatives considered:
|
|
|
150
150
|
└── tab bridge ──▶ server (status, session, name, detach)
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
Every attach, server, and holder role is `process.execPath` running the release's fixed `host-entry.js`; that entry loads the verified `terminal-host.node` addon and blocks in its native role dispatcher. Node carries no terminal bytes—the role remains native for its complete data path.
|
|
154
|
+
|
|
153
155
|
- **attach** is the foreground `a1` surface. It runs inside the bare-A1 launch instance and dies with the terminal, which is harmless.
|
|
154
156
|
- It owns raw mode, the alternate screen, outer terminal mode negotiation, the tab strip, composition of the active tab's retained surface, input arbitration, and shortcut interception.
|
|
155
157
|
- **server** is resident. It owns:
|
|
@@ -162,7 +164,7 @@ Alternatives considered:
|
|
|
162
164
|
- exactly one PTY (ConPTY on Windows) and its child process tree;
|
|
163
165
|
- one libghostty-vt retained model with bounded scrollback;
|
|
164
166
|
- input encoding and the per-tab damage state.
|
|
165
|
-
Because ConPTY belongs to its creating process, each tab has its own creator. A server crash therefore does not destroy a tab's ConPTY; holder failure affects only that tab. Actual child/tree exit and writer-lease release still require verification, not inference from the pseudoconsole closing.
|
|
167
|
+
Because ConPTY belongs to its creating process, each tab has its own creator. A server crash therefore does not destroy a tab's ConPTY; holder failure affects only that tab. Actual child/tree exit and writer-lease release still require verification, not inference from the pseudoconsole closing. The 2026-10-17 Windows x64 migration measurement put an idle Node-hosted holder at approximately 30–40 MiB of resident memory, versus a few MiB for the former native-executable holder; allocator and shared-page accounting make this a range rather than a per-tab budget. The separate holder process is retained for certified crash isolation, and latency budgets were not raised to absorb Node startup.
|
|
166
168
|
- **Why the server relays surfaces instead of clients connecting to holders directly.** It gives one authenticated endpoint per client, one ordering domain for focus and topology, and simpler multi-client arbitration. Measure the extra local pipe hop in the native hot-path evidence rather than assuming its latency. The server forwards encoded holder surfaces and does not re-render them.
|
|
167
169
|
|
|
168
170
|
### 3. Host scope, endpoints, writer authority, and credentials
|
|
@@ -182,9 +184,9 @@ Alternatives considered:
|
|
|
182
184
|
|
|
183
185
|
### 4. Detached start
|
|
184
186
|
|
|
185
|
-
- **Server start.** The server is started from the Node pre-guardian bootstrap (`ensureTerminalHost()` beside `ensureSupervisor()`), through `
|
|
187
|
+
- **Server start.** The server is started from the Node pre-guardian bootstrap (`ensureTerminalHost()` beside `ensureSupervisor()`), through `process.execPath host-entry.js server --detach` with the verified addon path supplied to the entry.
|
|
186
188
|
- **No general escape from containment.** On Windows, ordinary launch-instance jobs and holder-owned child jobs retain kill-on-close containment without `JOB_OBJECT_LIMIT_BREAKAWAY_OK` or silent breakaway; a job flag cannot allowlist an executable. On every platform, resident creation is a distinct authenticated fixed-role operation, not an arbitrary executable/argv spawn service.
|
|
187
|
-
- **Authorized launch and recovery.** Initial creation runs before the launch guardian. A contained attach client or surviving holder invokes the
|
|
189
|
+
- **Authorized launch and recovery.** Initial creation runs before the launch guardian. A contained attach client or surviving holder invokes the addon's verified `server --detach` recovery path; there is no arbitrary-spawn IPC method. When `runRole` first enters, it records one immutable host command: the current Node executable and the release's fixed role entry. The native routine accepts only a typed `server` or `holder` role and canonical profile, opens its own addon artifact, constructs the fixed role grammar, uses an allowlisted environment/cwd with handle inheritance disabled, and starts that recorded Node-and-entry pair. It verifies the resulting pid/start identity and containment before success. On Windows, a contained caller uses the fixed WMI escape, while the holder's fixed recovery invocation launches directly from its already-detached resident boundary; recursively invoking WMI from a detached service can fail in non-interactive sessions. Native tests may compile one feature-gated fixture role, but production accepts no caller-supplied executable or free-form argv. Owner-only credentials prevent cross-user access; this is not a sandbox against arbitrary malicious code already running as the same OS user.
|
|
188
190
|
- **Windows:**
|
|
189
191
|
- Select valid platform creation flags for detached, console-free startup; do not assume combining `DETACHED_PROCESS` and `CREATE_NO_WINDOW` adds a guarantee. Verify resulting process identity and job containment before reporting resident readiness.
|
|
190
192
|
- For a caller in a foreign kill-on-close job that denies breakaway (Windows OpenSSH, some IDE terminals), use WMI `Win32_Process.Create` with an explicit environment block and cwd, bounded startup, and post-launch verification (herdr reference).
|
|
@@ -192,7 +194,7 @@ Alternatives considered:
|
|
|
192
194
|
- **Every platform** records the observed detachment mode and any failed verification reason.
|
|
193
195
|
- **macOS and Linux:**
|
|
194
196
|
- Start resident roles in a new session (`setsid`) with no controlling terminal, standard streams on `/dev/null`, and an explicit environment and cwd, so terminal close and SSH hang-up (`SIGHUP`) do not reach them. Verify the resulting session, process group, and identity before reporting readiness.
|
|
195
|
-
- The process-guardian's kill-on-close tree tracking must not adopt resident roles, and ordinary descendants must not gain a way to leave their tree.
|
|
197
|
+
- The process-guardian addon's kill-on-close tree tracking must not adopt resident roles, and ordinary descendants must not gain a way to leave their tree.
|
|
196
198
|
- macOS: keep the resident roles in the user's per-user bootstrap namespace, so clipboard, keychain, and other per-user services keep working after the launching terminal closes (herdr #4100 is the reference). Linux: a session manager that kills user processes at logout (systemd `KillUserProcesses`) ends tabs at logout; that is the documented logout behavior, not a failure.
|
|
197
199
|
- Milestone 1 proves these with fixtures on each platform before any milestone relies on survival.
|
|
198
200
|
- **Holders** are spawned by the server through the same platform detach routine.
|
|
@@ -289,7 +291,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
289
291
|
- restarts: `restarts`, `restartWindowStart`, `lastExit`
|
|
290
292
|
- `registryRevision`, `epoch`, and `bootId`
|
|
291
293
|
- **No per-tab credential is persisted in the registry.** The owner-only profile secret is stored separately and derives per-tab/per-incarnation credentials from recorded non-secret identities. The environment sent with `tab.create` is kept in holder memory only; a tab restarted after a crash reuses its holder's environment.
|
|
292
|
-
- **Session-writer lease.** All A1-owned session writers (resident, solo/fallback, and Pi-comparison) on the supported platforms use one profile-neutral native lock primitive before opening a session for writing, independent of launch mode. It does not start a host, holder, or bridge for direct modes. The primitive is an OS lock held by the writer process itself (`LockFileEx` on Windows, an open-file-description `fcntl` lock on Linux, `flock` on macOS), so the kernel releases it only when the writer is gone. Node writers take it in-process through
|
|
294
|
+
- **Session-writer lease.** All A1-owned session writers (resident, solo/fallback, and Pi-comparison) on the supported platforms use one profile-neutral native lock primitive before opening a session for writing, independent of launch mode. It does not start a host, holder, or bridge for direct modes. The primitive is an OS lock held by the writer process itself (`LockFileEx` on Windows, an open-file-description `fcntl` lock on Linux, `flock` on macOS), so the kernel releases it only when the writer is gone. Node writers take it in-process through the same Node-API 9 `terminal-host.node` addon that exports the resident roles; there is no separate lock artifact, and a helper process cannot hold a lock on their behalf. Existing files are opened, identified by stable volume/device plus file identity, locked through that identity, and revalidated so case, junction/symlink, and hard-link aliases converge. A new file first holds a canonical-parent/name reservation, creates/opens without writing session content, acquires and revalidates file-identity authority while the reservation remains held, and only then admits writing; losing that race writes nothing. The writer routes include startup, `--session`, in-runtime switches, fork, import, new session, and the picker rename. The registry and holder coordinate admission, but a holder-only lock is insufficient: native lease custody must remain valid for the actual writer's lifetime, including any interval after holder death. Pin and prove the writer-bound lock/handle design on each platform in milestone 1; do not infer child death from pseudoterminal loss. Replacement requires lease acquisition and verified prior-writer/tree exit; uncertainty blocks it. Session switching acquires the target before relinquishing the old writer lease, and failed switches preserve the original session. Resident selection focuses an existing tab; direct-mode conflicts fail safely or offer an explicit fork, never silently attach or overwrite. Unmodified external Pi, older nonparticipating builds, and arbitrary file writers are outside this cooperative guarantee; document that limitation rather than claiming an OS sandbox.
|
|
293
295
|
- **Prompt journal.** `<dataDir>/tabs/<profile-token>/<tabId>/journal.jsonl` is an owner-only child-owned recovery journal, separate from asynchronous prompt history. Each submission has a stable ID and session/incarnation identity. Admission durably records bounded identity and recoverable editor text, never duplicate attachment payload bytes, before dispatch; timeout/failure leaves the prompt in the editor with a visible failure and dispatches nothing. The actual engine command retains every validated attachment. Correlate the record with the committed session entry, and retire it only after the corresponding session data is synchronized, including when command completion observes that the lifecycle already returned to ready; a settled turn's durable completion also records its submission ID so recovery never offers a completed prompt as unfinished. Recovery merges journal/session evidence by identity and never automatically resends, including when a crash occurs between dispatch, session append, sync, and journal retirement. It rehydrates image chips only from validated profile sidecars and removes unavailable tokens rather than presenting inert attachment labels. Preserve the first-turn journal and reserved session identity if Pi has not created its file yet; distinguish that from an unexpectedly missing existing transcript.
|
|
294
296
|
- **Draft checkpoint.** Use a periodic dirty checkpoint with a maximum one-second dirty interval while storage is healthy, including uninterrupted typing, rather than inactivity-only debounce. Expose failed/late checkpoints as degraded recovery; never continue claiming the one-second bound during a blocked disk or event-loop stall. Drafts and submitted prompts have different guarantees.
|
|
295
297
|
|
|
@@ -312,7 +314,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
312
314
|
|
|
313
315
|
### 10. Updates and version skew
|
|
314
316
|
|
|
315
|
-
- **
|
|
317
|
+
- **Artifacts and process images.** The server and holders are Node runtimes that load the addon and fixed entry from immutable release directories, never from the mutable npm prefix. Activation therefore never overwrites a loaded addon or entry, and release retention keeps every release used by a verified resident process.
|
|
316
318
|
- **Same generation, newer client.** The client attaches normally, and features the server lacks are disabled per method.
|
|
317
319
|
- **No automatic handoff in the first version.** A compatible resident cohort remains on its immutable release until all of its tabs stop. A newer same-generation client may attach with unsupported optional operations disabled.
|
|
318
320
|
- **Retention.** Release collection keeps the server's, every holder's, and every tab child's release while native identities verify. Package activation does not terminate or overwrite them.
|
|
@@ -322,8 +324,8 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
322
324
|
|
|
323
325
|
- **Strip.** Row 0, drawn by the attach client, reproduces the approved v2 default appearance:
|
|
324
326
|
- normal chips use RGB background `38,38,38` with foreground `170,170,170`, hover uses `48,48,48`, and the viewed chip uses `55,55,55` with white text, plus v2's palette fallback;
|
|
325
|
-
- the empty state
|
|
326
|
-
- chips are at most 20 columns, clipped with `…` by grapheme width, with no separators
|
|
327
|
+
- the empty state contains only the hoverable `+` control, with no clock, wallpaper, hint, or child cursor;
|
|
328
|
+
- chips are at most 20 columns, clipped with `…` by grapheme width, with no separators or close glyph;
|
|
327
329
|
- a `…` overflow menu holds tabs that do not fit, and `+` is always reserved;
|
|
328
330
|
- these presentation choices are fixed, not settings, while each working spinner uses that tab's reported A1 accent.
|
|
329
331
|
- **Keys.** These are intercepted by the attach client before input encoding. They are configurable through a keybindings file and conflict-checked against A1's registry at launch.
|
|
@@ -333,6 +335,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
333
335
|
| `Alt+A` | new tab |
|
|
334
336
|
| `Alt+W` | close |
|
|
335
337
|
| `F2` | rename |
|
|
338
|
+
| `Alt+.` | create or activate the global Settings tab |
|
|
336
339
|
| `Alt+1`…`Alt+9`, `Alt+0` | jump to a tab |
|
|
337
340
|
| unbound (`tabs.next` / `tabs.previous`) | next / previous, wrapping |
|
|
338
341
|
| unbound (`tabs.moveRight` / `tabs.moveLeft`) | move the tab right / left |
|
|
@@ -343,43 +346,43 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
343
346
|
- **`Ctrl+C` twice leaves, in every tab (user decision).** The client forwards the first `Ctrl+C` to the tab unchanged, so a single press still clears, copies, or interrupts. It consumes the second press within the existing clear/exit interval as the detach request and does not forward it. The tab therefore never receives a second `Ctrl+C` from the chord, which removes v2's bug where `Ctrl+C+C` killed the agent. The trade-off: a future CLI tab cannot receive two quick `Ctrl+C`s. Pressing them more slowly than the interval still reaches the program.
|
|
344
347
|
- **Mouse on row 0:**
|
|
345
348
|
- click activates a tab;
|
|
346
|
-
-
|
|
347
|
-
- right-click opens `Rename`/`Close`;
|
|
349
|
+
- right-click opens `Rename`/`Close` (`Close` only for Settings);
|
|
348
350
|
- dragging reorders, with a `│` drop marker, after 250 ms or once the pointer moves;
|
|
349
|
-
- `+`
|
|
351
|
+
- `+` opens a persistent dark v2 menu with `+ Agent`, a separator, and `Settings`; hover and click remain active through the opening release.
|
|
352
|
+
- **Settings.** The strip owns one profile-global `Settings` tab. The grouped launcher and `Alt+.` create or activate it; closing the screen or tab removes it. Resident Agent autocomplete omits `/settings`, and manually entering it does not open a per-Agent screen. `a1 solo` and `a1 pi` retain their existing Settings routes.
|
|
350
353
|
- **Commands inside A1 tabs:** existing `/name <name>` also renames the tab. Milestone 5 adds no tab-management slash commands: `/new-tab`, `/close`, `/tabs`, `/quit-all`, and `/restart-tab` are not exposed.
|
|
351
354
|
- **Naming:**
|
|
352
355
|
- Default names are `agent` or `agent N`.
|
|
353
356
|
- When `tabsAutoName` is on (the default, by user decision), the tab's model proposes a hyphenated lowercase name of at most 16 characters after the first settled turn. It uses a small budget and one retry, with a fallback derived from the first prompt, and it runs inside the child.
|
|
354
357
|
- A user rename sets `nameSource=user`, and auto-naming never overrides it.
|
|
355
358
|
- An automatic rename uses v2's title transition: 600 ms moving rainbow, typing at 45 ms per character, then a 300 ms settle. Manual renames do not animate.
|
|
356
|
-
- Rename is inline in the chip: `Enter` commits,
|
|
357
|
-
- **Close.** `Alt+W
|
|
359
|
+
- Rename is inline in the chip: `Enter` commits, `Esc` or a click elsewhere cancels, and `Ctrl+Left` / `Ctrl+Right` move by word. The name is trimmed, must not be empty, is at most 64 characters, and has control characters stripped.
|
|
360
|
+
- **Close.** `Alt+W` and the right-click menu close a tab directly, including while it is working. Closing removes the chip without a confirmation, stopping/read-only frame, or close glyph, then gracefully stops the child under the existing bounded termination policy. Its session stays resumable. Closing the last tab leaves only the hoverable `+` control.
|
|
358
361
|
- **Detach.**
|
|
359
362
|
- `Ctrl+C` twice detaches from any tab and is handled by the attach client, so it works even when the bridge is down. Inside an A1 tab, `/quit` and empty-editor `Ctrl+D` send a detach request carrying the immutable command-origin controller identity and revision; stale, ambiguous or unavailable attribution keeps the tab running and directs the user to `Ctrl+C` twice.
|
|
360
363
|
- The client restores the terminal. When agents are still running, it prints `1 agent still running run a1 to return.` or `N agents still running run a1 to return.`; otherwise it prints the resume hint.
|
|
361
364
|
- **Launch and reattach:**
|
|
362
365
|
- Bare `a1` ensures the server and attaches.
|
|
363
366
|
- With zero tabs, it creates one A1 tab in the launch cwd.
|
|
364
|
-
- Otherwise it views the last active tab from its retained surface without byte-history replay. Measure retained first paint and first input separately from bootstrap; status for the remaining tabs arrives without subscribing to their surfaces.
|
|
367
|
+
- Otherwise it views the last active tab from its retained surface without byte-history replay. Every holder keeps draining its PTY and updating its retained terminal independently while hidden; selecting a working tab requests its newest complete partial-response surface immediately and then continues live delivery. Measure retained first paint and first input separately from bootstrap; status for the remaining tabs arrives without subscribing to their surfaces.
|
|
365
368
|
- `a1 --session X` opens X in a new tab, or focuses the tab holding it.
|
|
366
369
|
- A new tab's cwd is the client's launch cwd.
|
|
367
|
-
- One internal prewarmed standby keeps the startup graph loaded in a hidden A1 process after a trusted tab has reported its session. Before promotion it owns no session or writer lease
|
|
370
|
+
- One internal prewarmed standby keeps the startup graph loaded in a hidden A1 process after a trusted tab has reported its session. Before promotion it owns no session or writer lease and has no registry/tab-strip entry. Creation promotes it only for an exact cwd, release, Node executable, and environment match; the server durably commits the new tab before exposing it to input. A mismatch discards it and normal creation retargets the next standby from the newly trusted tab. Standby launch or loss never changes visible tab state. It is not a setting. Measure A1 warm promotion directly; v2's tens-of-milliseconds result is reference evidence only.
|
|
368
371
|
- **Several terminals.** Each client has its own active tab, but each tab has one explicit input controller. Focus gain automatically claims the viewed tab through Decision 5's causal input barrier; if focus reporting is unavailable, first input starts the claim and is held until acknowledgement. There is no `Enter` takeover or persistent view-only banner. Only the current controller may send input or set PTY size. Delayed `/quit` and empty-editor `Ctrl+D` preserve their submission origin; stale or unprovable attribution affects neither client and falls back to attach-local double-`Ctrl+C`.
|
|
369
372
|
- **Background attention.** Only the chip icon changes. There is no bell, sound, or notification, and no setting for one.
|
|
370
373
|
|
|
371
374
|
### 12. Limits and resources
|
|
372
375
|
|
|
373
|
-
|
|
376
|
+
Internal resources remain bounded without imposing a user-visible tab-count quota.
|
|
374
377
|
|
|
375
|
-
-
|
|
378
|
+
- The only user-facing Tabs setting during resident launches is `tabsAutoName` (default on). Legacy `tabsMax` data is preserved but ignored. Tab enablement is not a setting. Direct `a1 solo` and `a1 pi` launches omit the Tabs section entirely.
|
|
376
379
|
- Unresponsive restart and stall-notice timing are internal fixed behavior at 120 and 300 seconds; concurrent starts use internal default 2/hard cap 8 and prewarm uses internal count/hard cap 1.
|
|
377
380
|
- Every open tab keeps its holder and A1 process until explicit close. There is no idle-suspension setting, timer, lifecycle, or glyph.
|
|
378
381
|
- Scrollback is capped at 10 MiB per holder, and the replay after a server restart is the retained model itself.
|
|
379
382
|
- The server exits after 10 minutes with no attached client and no live user tab; the prewarmed standby does not keep it alive and is stopped first.
|
|
380
383
|
- Structured diagnostic logs are `terminal-host-server.log` and `terminal-host-holder-<tab>.log`, rotated at 5 MiB with three kept. Use allowlisted fields/reason codes, not arbitrary exception strings, prompt text, terminal content, or credentials.
|
|
381
384
|
- Raw child stderr cannot be declared sanitized: when available separately from ConPTY's combined terminal stream, store it only under the owner-only per-tab recovery area, at 5 MiB with three generations and a seven-day maximum age, alongside bounded last-screen files. Do not scrape stderr back out of terminal cells. These private artifacts may contain conversation or sensitive content and are excluded from `a1 tabs doctor`; neither their contents nor arbitrary extension errors enter structured crash records.
|
|
382
|
-
- `a1 tabs host status` prints the server pid, build, release, generation, epoch, uptime, detachment mode, tab count by status, client count,
|
|
385
|
+
- `a1 tabs host status` prints the server pid, build, release, generation, epoch, uptime, detachment mode, tab count by status, client count, internal resource limits, volatile health counters since server start, and log paths.
|
|
383
386
|
|
|
384
387
|
#### Hard bounds (milestone 6 audit)
|
|
385
388
|
|
|
@@ -393,7 +396,6 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
|
|
|
393
396
|
| Per-client render slot | one pending full surface; a newer one replaces it | `roles/server.rs` `Peer::offer_surface` | `stalled_client_gets_one_full_current_surface` |
|
|
394
397
|
| Concurrent endpoint connections | 128 | `roles/server.rs` `admit_connection` | `connections_beyond_the_cap_are_refused_and_released_slots_are_reused` |
|
|
395
398
|
| Attached viewing clients | 16 | `roles/server.rs` `client_slot_available` | `attached_clients_are_capped` |
|
|
396
|
-
| Open tabs | `tabsMax` (default 10, hard 50), including session-selection starts | `roles/server.rs` `tab.create`, `attach.start` | `ten_concurrent_tab_starts_respect_the_server_limit` |
|
|
397
399
|
| Concurrent holder starts | default 2, hard 8 | `core/server.rs` | `limits_starts_and_drops_stale_completion` |
|
|
398
400
|
| Stopped tab records | 16, oldest dropped first | `roles/server.rs` `prune_stopped_records` | `stopped_records_are_bounded_oldest_first_and_live_tabs_are_kept` |
|
|
399
401
|
| Prior tab-set archives | bounded history | `roles/registry.rs` | `prior_tab_sets_are_bounded` |
|
|
@@ -410,7 +412,7 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
|
|
|
410
412
|
|
|
411
413
|
### 13. Packaging and certification
|
|
412
414
|
|
|
413
|
-
- `
|
|
415
|
+
- `terminal-host.node` and the fixed `dist/foundation/resident-tabs/host-entry.js` are built and packaged for Windows x64, Linux x64, macOS arm64, and macOS x64 (see the 2026-10-14 decision), extending the impact-selected Windows CI owner introduced by #588 to that exact target matrix, with Zig 0.15.2 for pinned libghostty-vt. One addon carries both every role and the session-writer lock; there is no terminal-host executable or separate lock addon. It ships in the immutable release with the v2 addon manifest, hash verification, provenance, licenses, and notices.
|
|
414
416
|
- Acceptance requires exact-package hermetic suites on each platform plus manual or isolated-worker evidence per platform for terminal close, SSH/session loss, reattach, input fidelity, extension text UI, failure recovery, rollback, and render smoothness. Evidence is never inferred from one platform to another.
|
|
415
417
|
- An authorized isolated-worker 24-hour soak remains required certification evidence on each platform; a miss blocks certification without silently changing the approved default or weakening fallback.
|
|
416
418
|
|
|
@@ -498,9 +500,9 @@ The v2 daemon was unreliable for structural reasons, and a forensic pass over it
|
|
|
498
500
|
2. With `TERMINAL_HOST_LOCAL_BUILD=1` and `TERMINAL_HOST_PREBUILT_VT` naming that directory, `build.rs` verifies `terminal-vt.manifest.json` against the vendored `VERSION`, the Ghostty commit, the Cargo and Zig targets, and each file's SHA-256, then links it and skips Zig. A mismatch fails the build in one line and never falls back. Builds with `CI` set ignore the variable.
|
|
499
501
|
3. Run one test: `cargo test --manifest-path native/terminal-host/Cargo.toml --features resident-fixtures --test role_integration <name>`.
|
|
500
502
|
|
|
501
|
-
Every chaos run prints its seed and records its executed action schedule in its report; `--seed <n>` fixes the seed, and `--replay <report.json>` re-executes a recorded schedule and reports actions it could not apply as divergence.
|
|
503
|
+
Every chaos run prints its seed and records its executed action schedule in its report; `--seed <n>` fixes the seed, and `--replay <report.json>` re-executes a recorded schedule and reports actions it could not apply as divergence. With the verified prebuilt terminal library selected, `--build` builds the release addon and the feature-gated `fake-tab` fixture; otherwise pass `--host <terminal-host.node>` and `--fake-tab <fixture>` explicitly. Product builds never emit a role executable; the `role-host` and `fake-tab` binaries are test fixtures only.
|
|
502
504
|
|
|
503
|
-
Known limit (2026-10-15): on a Windows desktop whose shells already run inside a job, a server started by a role test cannot leave that job, `verify_detached` rejects it, and tests that expect `started` fail locally while passing in CI; the release retention integration test, which runs whenever debug host
|
|
505
|
+
Known limit (2026-10-15): on a Windows desktop whose shells already run inside a job, a server started by a role test cannot leave that job, `verify_detached` rejects it, and tests that expect `started` fail locally while passing in CI; the release retention integration test, which runs whenever a debug host addon exists, fails the same way, and local chaos reports servers that holders did not replace. A separate change addresses this.
|
|
504
506
|
|
|
505
507
|
**Milestone 5 preview performance budgets (p95 on each exact platform build):** cold launch to first input is the direct startup budget plus 800 ms; matching-standby creation to first input is 250 ms; reattach first paint is 300 ms and first accepted input is 400 ms; input-to-process is 16 ms; output-to-present is 33 ms. Each result or miss is recorded independently for Windows x64, macOS arm64, and Linux x64. The platform-native validation lanes run `measure-resident-tabs.mjs` against their built host: its instrumented child reports process-admission timestamps through a real resident holder, while the outer test PTY records presentation, standby promotion, and retained-surface reattach timestamps.
|
|
506
508
|
|
|
@@ -508,8 +510,8 @@ Known limit (2026-10-15): on a Windows desktop whose shells already run inside a
|
|
|
508
510
|
|
|
509
511
|
- **[A server crash interrupts the display for everyone]** → Holders keep processes and screens alive. The server holds no VT, Pi, or extension code. It is auto-respawned by any client or holder, and reattach restores exact surfaces.
|
|
510
512
|
- **[Terminal fidelity in a composed surface]** (keyboard protocols, mouse, paste, IME, Unicode width, hyperlinks) → Reuse libghostty-vt encoding and the proof's input work, and add fidelity suites on each platform. Claims cover only the certified text-terminal contract; image protocols remain an explicit gap.
|
|
511
|
-
- **[Per-tab memory multiplies with holder and A1 processes;
|
|
512
|
-
- **[Detachment is platform-specific and fragile]** (Windows jobs and SSH, macOS bootstrap namespaces, Linux logout policies) → Detection and escape live in the native
|
|
513
|
+
- **[Per-tab memory multiplies with holder and A1 processes; measured working sets are observations, not A1 bounds]** → The 2026-10-17 Windows x64 measurement shows Node hosting raising an idle holder from a few MiB to approximately 30–40 MiB resident. One internal prewarmed standby, bounded concurrent starts, and throttled hidden delivery limit avoidable multiplication. Every open tab keeps its process until explicit close; there is no idle suspension or count quota. Merging holders into the server would discard the certified per-tab crash boundary and requires a separate change.
|
|
514
|
+
- **[Detachment is platform-specific and fragile]** (Windows jobs and SSH, macOS bootstrap namespaces, Linux logout policies) → Detection and escape live in the native addon per platform, while the process image is Node running the fixed role entry. The mode is reported, never assumed, and unverifiable survival falls back instead of claiming persistence.
|
|
513
515
|
- **[Three platforms multiply native and certification work]** → Platform code is confined to the thin I/O shell around one shared sans-IO core, and milestone 1 proves every platform primitive before later milestones build on it.
|
|
514
516
|
- **[The Zig and libghostty toolchain in CI and release]** → It is pinned already. Extend the #588 Windows owner and provenance gate to a macOS and Linux build matrix.
|
|
515
517
|
- **[No image protocol in tabs at first]** → Pi's text fallback applies and the gap is documented. Forwarding is a follow-up.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Terminal-host provenance
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The resident terminal host uses a small console-hosted stack. Its native implementation ships as the Node-API 9 addon `terminal-host.node`; the release's fixed `dist/foundation/resident-tabs/host-entry.js` loads that addon and dispatches the server, holder, attach, probe, maintenance, and internal roles inside the hosting Node runtime. It runs inside the user's existing terminal and does not create a separate desktop window. No terminal-host executable is built or shipped, and the same addon exports the in-process session-writer lock.
|
|
4
4
|
|
|
5
5
|
## Selected source components
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ The composed-terminal proof uses a small console-hosted stack. It runs inside th
|
|
|
14
14
|
|
|
15
15
|
## Release notices
|
|
16
16
|
|
|
17
|
-
`scripts/release/generate-terminal-host-notices.mjs` writes `terminal-host-THIRD-PARTY-NOTICES.txt` beside each release
|
|
17
|
+
`scripts/release/generate-terminal-host-notices.mjs` writes `terminal-host-THIRD-PARTY-NOTICES.txt` beside each release addon. It walks the locked Cargo graph for the build target (normal dependencies, including the Node-API glue; proc-macro crates and their subtrees never reach the addon), adds the Zig-linked components, and fails on a license expression outside MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Unicode-3.0, or Zlib (the `LLVM-exception` addendum is accepted), on a crate without license text, or on a pinned text whose digest changed.
|
|
18
18
|
|
|
19
19
|
A buffered A1-owned frame composer is the initial presentation layer. Ratatui is an optional evaluation candidate only if the narrow composer increases rendering risk; it is not selected by default.
|
|
20
20
|
|
|
@@ -76,4 +76,6 @@ Unverified, mismatched, or unsigned/unattested artifacts are not eligible for co
|
|
|
76
76
|
|
|
77
77
|
## Release artifact
|
|
78
78
|
|
|
79
|
-
`npm run build:terminal-host` (`scripts/development/build-terminal-host.mjs`) runs only on
|
|
79
|
+
`npm run build:terminal-host` (`scripts/development/build-terminal-host.mjs`) runs only on release and native validation runners. It builds the library once with `cargo build --release --locked --lib --features node-addon`, copies the cdylib as `terminal-host.node`, loads the addon in Node, and checks its `version()` report: protocol, generation, crate version, pinned `libghostty-vt` commit, and the absence of `fault-injection` and `resident-fixtures`. The macOS x64 cross build may use the recorded Mach-O architecture check when the arm64 runner cannot load the addon.
|
|
80
|
+
|
|
81
|
+
The generated `terminal-host.manifest.json` uses schema `a1-terminal-host-artifact-v2` and records `kind: "node-addon"`, Node-API 9, the addon's SHA-256 and size, the release-relative role-entry digest, the notices digest, target triple, Rust and Zig versions, and `Cargo.lock` digest. The role entry passes only the fixed role grammar and records `{ executable: process.execPath, entry, artifact: addonPath }` once in the addon; the explicit loaded artifact identity makes release-hold reporting independent of development environment propagation. Detached and WMI resident starts use the recorded Node-and-entry command and never resolve an A1-built executable. The publication `package` job refuses to pack unless every platform's process-guardian and terminal-host addons match their manifests, and A1 verifies the addon, entry, and notices before loading the host.
|
|
@@ -26,19 +26,19 @@ Physical desktop automation is absent from the active repository baseline. Any f
|
|
|
26
26
|
The published package contains:
|
|
27
27
|
|
|
28
28
|
- the sole public `a1` command;
|
|
29
|
-
- the internal supervisor
|
|
30
|
-
- immutable release/bootstrap, lifecycle, protocol, storage, update, owned-UI, and Pi-adapter modules;
|
|
29
|
+
- the internal supervisor, launch-guardian, exit-watcher, and fixed terminal-host role entries;
|
|
30
|
+
- immutable release/bootstrap, lifecycle, protocol, storage, update, owned-UI, and Pi-adapter modules, plus `process-guardian.node` and `terminal-host.node` for every supported target with manifests and terminal-host notices;
|
|
31
31
|
- current user and architecture documentation.
|
|
32
32
|
|
|
33
|
-
It contains no
|
|
33
|
+
It contains no executable built from A1 sources, separate session-lock addon, browser/desktop GUI, physical automation driver, or generated runtime data. PTY ownership, terminal emulation, composition, and input translation exist only inside the terminal-host addon; the JavaScript product path does not relay terminal bytes.
|
|
34
34
|
|
|
35
|
-
## Native
|
|
35
|
+
## Native addon layer
|
|
36
36
|
|
|
37
37
|
`npm run doctor` reports this contract against the current machine—Node range, npm major, git, the GitHub CLI, Rust/Cargo, and whether `node_modules` matches the lockfile—and names the install command for anything missing. `npm run build` runs the same check as its first step, so an unmet prerequisite fails by name rather than inside a module resolver or Cargo. Unmet tool prerequisites block; a Node or npm version outside the declared contract is reported as an advisory without failing the build.
|
|
38
38
|
|
|
39
|
-
Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
|
|
39
|
+
Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
`npm run build` compiles `native/process-guardian` as the host platform's Node-API 9 addon, places `process-guardian.node` under `dist/native/<platform>-<architecture>/`, loads it to observe its version identity, and writes the v2 integrity manifest. Preview and stable candidate workflows build the addon on isolated Windows, Linux, and macOS arm64 runners, cross-compile the macOS x64 addon on the arm64 runner with `--target x86_64-apple-darwin`, and assemble all targets before packing. The same release runners compile TypeScript and run `npm run build:terminal-host`, which builds one Node-API 9 `terminal-host.node` containing the fixed roles and the session-writer lock. Its v2 manifest binds the addon, `dist/foundation/resident-tabs/host-entry.js`, and notices. The local `npm run build` does not build that addon because the Zig fetch for `libghostty-vt` is quarantined by workstation antivirus on Windows; local native iteration uses a verified terminal library downloaded from a CI lane (see [`terminal-host-provenance.md`](terminal-host-provenance.md) and [`resident-tabs.md`](resident-tabs.md)). Cross-compiled artifacts record a Mach-O architecture check when they cannot be loaded on the producing runner. Supported native code is always loaded by Node; an absent, incompatible, or unsupported addon fails before runtime startup rather than falling back to PID-only cleanup. The process-guardian addon inherits terminal handles but transports no terminal bytes. Its separate Node-hosted exit watcher restores terminal state after guardian failure, subject to the documented node-wide-kill gap. See [`process-guardian-provenance.md`](process-guardian-provenance.md).
|
|
42
42
|
|
|
43
43
|
## Gates
|
|
44
44
|
|
|
@@ -8,7 +8,7 @@ Version-1 and version-2 deliveries and their existing comments, acceptance PRs,
|
|
|
8
8
|
|
|
9
9
|
## Version-3 lifecycle
|
|
10
10
|
|
|
11
|
-
1. **Draft plan:** create one normally named branch/PR such as `feature/...`, `fix/...`, or `chore/...`. Start the body with `## Proposal`, use it for one or two sentences of intent, and follow it with
|
|
11
|
+
1. **Draft plan:** create one normally named branch/PR such as `feature/...`, `fix/...`, or `chore/...`. Start the body with `## Proposal`, use it for one or two sentences of intent, and follow it with one or more unchecked, outcome-sized task items under `## Implementation`, with no fixed maximum. Each item names a delivered code outcome. Omit generic process tasks; run, verify, validate, evidence, or manual-test items; quoted phase lines; and routine validation command lists, and keep machine linkage under final collapsed `Automation`. Keep it draft and include only planning artifacts until the maintainer approves the plan and explicitly requests implementation.
|
|
12
12
|
2. **Same-PR implementation:** continue after explicit approval in the same worktree, branch, history, draft PR, and phase-free body. Check an Implementation item as soon as its associated code and tests are written and its detailed task bookkeeping is complete; an ordinary progress edit changes only `[ ]` to `[x]` and is not deferred until handoff. Adding, removing, reordering, or rewriting an item is a scope refinement requiring explicit approval and coherent planning updates, with affected scope left unchecked. Reconcile approved refinements before corresponding code edits.
|
|
13
13
|
3. **Complete implementation:** finish the code and its tests, substantive tasks, and explicit known-gap disposition. CI runs those tests after readiness; its success is objective evidence, not acceptance, and GitHub check counts are not Implementation task progress. Manual validation is the maintainer's optional choice before integrating and never blocks a checkbox, finalization, or merge.
|
|
14
14
|
4. **Plain acceptance list:** before checking the final item, keep the body phase-free and add final `## Acceptance` with one to ten concise implementation-specific behavior-and-result bullets. Do not use checkboxes, generic review/CI/approval/archive statements, angle-bracket placeholders or HTML, URLs, mentions, or automated-test inventory.
|
|
@@ -27,7 +27,7 @@ The implementation, synchronized canonical specs, conditional acceptance record,
|
|
|
27
27
|
|
|
28
28
|
## Draft PR body
|
|
29
29
|
|
|
30
|
-
The first screen should separate purpose from delivery detail, not foreground CI mechanics. Distill `Proposal` into one or two sentences answering why the PR exists, then put
|
|
30
|
+
The first screen should separate purpose from delivery detail, not foreground CI mechanics. Distill `Proposal` into one or two sentences answering why the PR exists, then put one or more unchecked, implementation-specific outcomes under `Implementation`, with no fixed maximum. They are a coarse progress summary, not a copy of the detailed OpenSpec task ledger:
|
|
31
31
|
|
|
32
32
|
```markdown
|
|
33
33
|
## Proposal
|
|
@@ -159,7 +159,7 @@ If the body list changes, candidate validation reruns and compares it with the c
|
|
|
159
159
|
|
|
160
160
|
## Automated finalization
|
|
161
161
|
|
|
162
|
-
The `OpenSpec finalization` workflow (`.github/workflows/openspec-finalization.yml`) runs on `pull_request_target` for `opened`, `synchronize`, `ready_for_review`, `reopened`, and `edited` events targeting `develop`, one event at a time per PR. It checks out default-branch policy, installs the pinned tooling without hooks, and runs `scripts/governance/publish-openspec-finalization.mjs --pr <n>`; the PR head enters that process only as the `openspec/` tree the pinned OpenSpec engine reads. An all-checked version-3 task-list draft with valid Acceptance and complete detailed pre-finalization inputs is re-read as one exact snapshot and marked ready; no content is changed, and incomplete, malformed, legacy, unassociated, forked, or raced drafts remain draft with a stable reason. A draft with any unchecked item is skipped quietly. A draft whose every task-list item is checked but whose promotion is refused for a body, task, evidence, or other content defect fails the run with an `OpenSpec finalization` error annotation naming the `draft-<code>` reason, so the stall shows as a red check instead of a green skip. Closed PRs are skipped. For a ready version-3 candidate the workflow reconciles the head to its finalized form:
|
|
162
|
+
The `OpenSpec finalization` workflow (`.github/workflows/openspec-finalization.yml`) runs on `pull_request_target` for `opened`, `synchronize`, `ready_for_review`, `converted_to_draft`, `reopened`, and `edited` events targeting `develop`, one event at a time per PR. It checks out default-branch policy, installs the pinned tooling without hooks, and runs `scripts/governance/publish-openspec-finalization.mjs --pr <n>`; the PR head enters that process only as the `openspec/` tree the pinned OpenSpec engine reads. An all-checked version-3 task-list draft with valid Acceptance and complete detailed pre-finalization inputs is re-read as one exact snapshot and marked ready; no content is changed, and incomplete, malformed, legacy, unassociated, forked, or raced drafts remain draft with a stable reason. A draft with any unchecked item is skipped quietly. A finalized PR returned to draft is promoted again when its body still has every item checked, its fence names the exact head as finalized, and the tree holds exactly that archive with no active change; otherwise it stays draft with `draft-finalization-redraft-form`. A draft whose every task-list item is checked but whose promotion is refused for a body, task, evidence, or other content defect fails the run with an `OpenSpec finalization` error annotation naming the `draft-<code>` reason, so the stall shows as a red check instead of a green skip. Closed PRs are skipped. For a ready version-3 candidate the workflow reconciles the head to its finalized form:
|
|
163
163
|
|
|
164
164
|
- **Active and current:** ordinary finalization with today's UTC date; one `docs(openspec): finalize <change>` commit.
|
|
165
165
|
- **Finalized, valid, and current:** nothing is pushed; the run reports `already-finalized`.
|
package/docs/validation.md
CHANGED
|
@@ -48,7 +48,11 @@ Conservative Development retains every pull-request owner but does not schedule
|
|
|
48
48
|
node scripts/release/generate-validation-ownership-ledger.mjs --output .artifacts/validation/ownership-ledger.json
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Containment-selected modular jobs build and test the debug `process-guardian.node` addon for their exact platform, export `A1_PROCESS_GUARDIAN_PATH` to that file, and run the launch-owner integration fixtures as Node processes. The tests exercise the real addon-held Windows job or Unix process group, addon-backed identity and terminal operations, and the Node exit watcher; no fixture invokes a process-guardian executable. The Windows startup lane keeps Defender real-time protection enabled while packaged startup loads the release addon.
|
|
52
|
+
|
|
53
|
+
The `terminal-host` owner is CI-only. A change under `native/terminal-host/` or to its run scripts selects, per platform (Windows x64, Linux x64, macOS arm64), one **Native terminal host** job and three **Native terminal host tests** partition jobs. The partition jobs each build the test profile and run one third of the serialized `role_integration` suite (`run-terminal-host-probe.mjs --partition <i>/3`, partitioned by sorted test name read from the suite's source); the feature-gated `role-host` binary stands in only for the fixed role grammar inside Rust integration tests and is never packaged. The build-and-probe job runs `npm run test:terminal-host` with the role suite skipped (`TERMINAL_HOST_PARTITION=none`), building `terminal-host.node` with Rust, Node-API 9, and Zig 0.15.2 for the lane's exact target and baseline CPU model. It loads the addon for its version and non-interactive probes, verifies the shared session-writer lock export, and uploads the debug addon and frozen protocol manifest; Linux also runs bounded decoder fuzzing.
|
|
54
|
+
|
|
55
|
+
The protected aggregate requires every partition of every selected platform, keyed by `partition` in the outcome authority and job envelope. Because the role suite no longer warms the resident launch paths in the build-and-probe job, its certification step runs one unmeasured `measure-resident-tabs.mjs` pass before the measured pass whose budgets are unchanged. Fidelity, latency, chaos, and certification start roles as `process.execPath dist/foundation/resident-tabs/host-entry.js` with `A1_TERMINAL_HOST_PATH` naming the addon. Windows cleanup identifies those Node-hosted residents by command line; it never kills every `node.exe` by image name. Locally, `npm run test:terminal-host` without a partition is unchanged and runs everything in one process. Local Windows builds fail in `build.rs` unless `TERMINAL_HOST_LOCAL_BUILD=1` is set, because Zig's package fetch unpacks test data that workstation antivirus quarantines. Native lanes also publish the terminal library they linked; a local build can link it instead of running Zig (see Local native iteration in [`architecture/resident-tabs.md`](architecture/resident-tabs.md)), which is development evidence only and never replaces selected CI. `TERMINAL_HOST_CONTRACTS_ONLY=1` is a library-only diagnostic and cannot substitute for selected CI. See [`native/terminal-host/README.md`](../native/terminal-host/README.md).
|
|
52
56
|
|
|
53
57
|
## Windows Full regression shards
|
|
54
58
|
|
package/package.json
CHANGED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { WindowsNativeProcessInspector, type InspectorCommandRunner } from "./windows-process-inspector.js";
|
|
2
|
-
/** Verifies Darwin process identity through the bounded native guardian inspection command. */
|
|
3
|
-
export declare class DarwinNativeProcessInspector extends WindowsNativeProcessInspector {
|
|
4
|
-
constructor(helperPath: string, runner?: InspectorCommandRunner);
|
|
5
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { WindowsNativeProcessInspector } from "./windows-process-inspector.js";
|
|
2
|
-
/** Verifies Darwin process identity through the bounded native guardian inspection command. */
|
|
3
|
-
export class DarwinNativeProcessInspector extends WindowsNativeProcessInspector {
|
|
4
|
-
constructor(helperPath, runner) {
|
|
5
|
-
super(helperPath, runner, "darwin-proc-start:");
|
|
6
|
-
}
|
|
7
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
|
-
/** Delegates one root runtime to the native guardian and exposes its verified containment identity. */
|
|
6
|
-
export class NativeGuardianContainment {
|
|
7
|
-
#identity;
|
|
8
|
-
#helper = null;
|
|
9
|
-
#rootIdentity = null;
|
|
10
|
-
#outcome = null;
|
|
11
|
-
instanceId;
|
|
12
|
-
helperPath;
|
|
13
|
-
statusPath;
|
|
14
|
-
exitNoticePath;
|
|
15
|
-
constructor(instanceId, helperPath, statusPath, exitNoticePath = null) {
|
|
16
|
-
this.instanceId = instanceId;
|
|
17
|
-
this.helperPath = helperPath;
|
|
18
|
-
this.statusPath = statusPath;
|
|
19
|
-
this.exitNoticePath = exitNoticePath;
|
|
20
|
-
this.#identity = { provider: "native-guardian-pending", token: instanceId };
|
|
21
|
-
}
|
|
22
|
-
get identity() {
|
|
23
|
-
return this.#identity;
|
|
24
|
-
}
|
|
25
|
-
async spawn(executable, arguments_, options) {
|
|
26
|
-
if (this.#helper)
|
|
27
|
-
throw new Error("process containment already has a root runtime");
|
|
28
|
-
await mkdir(dirname(this.statusPath), { recursive: true, mode: 0o700 });
|
|
29
|
-
await rm(this.statusPath, { force: true });
|
|
30
|
-
if (this.exitNoticePath)
|
|
31
|
-
await rm(this.exitNoticePath, { force: true });
|
|
32
|
-
const helper = spawn(this.helperPath, [
|
|
33
|
-
"--parent-pid", String(process.pid),
|
|
34
|
-
"--instance", this.instanceId,
|
|
35
|
-
"--status-file", this.statusPath,
|
|
36
|
-
...(this.exitNoticePath ? ["--exit-notice", this.exitNoticePath] : []),
|
|
37
|
-
"--", executable, ...arguments_,
|
|
38
|
-
], {
|
|
39
|
-
cwd: options.cwd,
|
|
40
|
-
env: {
|
|
41
|
-
...options.environment,
|
|
42
|
-
...(options.terminalType ? { TERM: options.terminalType } : {}),
|
|
43
|
-
// Protocol: the runtime arms this notice and the guardian prints it if the runtime dies unclean.
|
|
44
|
-
...(this.exitNoticePath ? { [PRODUCT_IDENTITY.environment.exitNoticePath]: this.exitNoticePath } : {}),
|
|
45
|
-
},
|
|
46
|
-
shell: false,
|
|
47
|
-
stdio: "inherit",
|
|
48
|
-
windowsHide: false,
|
|
49
|
-
});
|
|
50
|
-
this.#helper = helper;
|
|
51
|
-
this.#outcome = helperOutcome(helper);
|
|
52
|
-
const status = await waitForReadyStatus(this.statusPath, this.#outcome, 5_000);
|
|
53
|
-
this.#rootIdentity = { pid: status.pid, startIdentity: status.startIdentity };
|
|
54
|
-
this.#identity = { provider: status.containmentProvider, token: status.containmentToken };
|
|
55
|
-
return { identity: this.#rootIdentity, outcome: this.#outcome };
|
|
56
|
-
}
|
|
57
|
-
async contains(identity) {
|
|
58
|
-
return this.#rootIdentity !== null
|
|
59
|
-
&& identity.pid === this.#rootIdentity.pid
|
|
60
|
-
&& identity.startIdentity === this.#rootIdentity.startIdentity;
|
|
61
|
-
}
|
|
62
|
-
async stop(force) {
|
|
63
|
-
const root = this.#rootIdentity;
|
|
64
|
-
if (!root)
|
|
65
|
-
return;
|
|
66
|
-
try {
|
|
67
|
-
process.kill(root.pid, force ? "SIGKILL" : "SIGTERM");
|
|
68
|
-
}
|
|
69
|
-
catch { }
|
|
70
|
-
}
|
|
71
|
-
async waitForEmpty(timeoutMs) {
|
|
72
|
-
if (!this.#helper || this.#helper.exitCode !== null || this.#helper.signalCode !== null)
|
|
73
|
-
return true;
|
|
74
|
-
return await new Promise(resolvePromise => {
|
|
75
|
-
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
76
|
-
timer.unref();
|
|
77
|
-
const finish = (empty) => {
|
|
78
|
-
clearTimeout(timer);
|
|
79
|
-
this.#helper?.off("close", onClose);
|
|
80
|
-
resolvePromise(empty);
|
|
81
|
-
};
|
|
82
|
-
const onClose = () => finish(true);
|
|
83
|
-
this.#helper?.once("close", onClose);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
async close() {
|
|
87
|
-
const helper = this.#helper;
|
|
88
|
-
if (helper && helper.exitCode === null && helper.signalCode === null)
|
|
89
|
-
helper.kill("SIGKILL");
|
|
90
|
-
await this.waitForEmpty(1_500);
|
|
91
|
-
await rm(this.statusPath, { force: true });
|
|
92
|
-
if (this.exitNoticePath)
|
|
93
|
-
await rm(this.exitNoticePath, { force: true });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
export function resolveProcessGuardianPath(releaseRoot, environment = process.env, platform = process.platform, architecture = process.arch) {
|
|
97
|
-
const override = environment[PRODUCT_IDENTITY.environment.processGuardianPath]?.trim();
|
|
98
|
-
if (override)
|
|
99
|
-
return resolve(override);
|
|
100
|
-
const executable = platform === "win32"
|
|
101
|
-
? `${PRODUCT_IDENTITY.artifacts.processGuardianExecutable}.exe`
|
|
102
|
-
: PRODUCT_IDENTITY.artifacts.processGuardianExecutable;
|
|
103
|
-
return resolve(releaseRoot, "dist", "native", `${platform}-${architecture}`, executable);
|
|
104
|
-
}
|
|
105
|
-
function helperOutcome(helper) {
|
|
106
|
-
return new Promise(resolvePromise => {
|
|
107
|
-
let settled = false;
|
|
108
|
-
const settle = (outcome) => {
|
|
109
|
-
if (settled)
|
|
110
|
-
return;
|
|
111
|
-
settled = true;
|
|
112
|
-
resolvePromise(outcome);
|
|
113
|
-
};
|
|
114
|
-
helper.once("error", error => settle({ kind: "guardian-error", message: error.message, code: errorCode(error) }));
|
|
115
|
-
helper.once("close", (code, signal) => {
|
|
116
|
-
if (signal)
|
|
117
|
-
settle({ kind: "signaled", signal });
|
|
118
|
-
else
|
|
119
|
-
settle({ kind: "exited", exitCode: code ?? 1 });
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
async function waitForReadyStatus(path, outcome, timeoutMs) {
|
|
124
|
-
const deadline = Date.now() + timeoutMs;
|
|
125
|
-
let terminalOutcome = null;
|
|
126
|
-
void outcome.then(value => { terminalOutcome = value; });
|
|
127
|
-
while (Date.now() < deadline) {
|
|
128
|
-
try {
|
|
129
|
-
const value = JSON.parse(await readFile(path, "utf8"));
|
|
130
|
-
if (isReadyStatus(value))
|
|
131
|
-
return value;
|
|
132
|
-
throw new Error("process guardian published invalid ready status");
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
if (terminalOutcome)
|
|
139
|
-
throw new Error(`process guardian exited before readiness: ${JSON.stringify(terminalOutcome)}`);
|
|
140
|
-
await new Promise(resolvePromise => setTimeout(resolvePromise, 20));
|
|
141
|
-
}
|
|
142
|
-
throw new Error(`process guardian did not publish readiness within ${timeoutMs}ms`);
|
|
143
|
-
}
|
|
144
|
-
function isReadyStatus(value) {
|
|
145
|
-
return Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
146
|
-
&& typeof value.startIdentity === "string" && value.startIdentity.length > 0
|
|
147
|
-
&& typeof value.containmentProvider === "string" && value.containmentProvider.length > 0
|
|
148
|
-
&& typeof value.containmentToken === "string" && value.containmentToken.length > 0;
|
|
149
|
-
}
|
|
150
|
-
function errorCode(error) {
|
|
151
|
-
return "code" in error && typeof error.code === "string" ? error.code : null;
|
|
152
|
-
}
|