diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml
index 3023d414abe..784172d699e 100644
--- a/.github/workflows/computer-e2e.yml
+++ b/.github/workflows/computer-e2e.yml
@@ -117,6 +117,7 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
# Why: boot the BUILT daemon-entry under plain Node the way production
# forks it. v1.4.129-rc.1 shipped a daemon that exited at module load
# (leaked electron require) while every other check passed; this fails
@@ -158,6 +159,7 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-mac.e2e.ts tests/e2e/computer-mac-safari.e2e.ts
linux:
@@ -185,6 +187,7 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
- run: xvfb-run --auto-servernum dbus-run-session -- pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-linux.e2e.ts
windows:
@@ -204,4 +207,5 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts tests/e2e/computer-windows-store.e2e.ts
diff --git a/.github/workflows/mobile-ios-release.yml b/.github/workflows/mobile-ios-release.yml
index b38a79b9267..f759bd3efee 100644
--- a/.github/workflows/mobile-ios-release.yml
+++ b/.github/workflows/mobile-ios-release.yml
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
inputs:
bump_patch_version:
- description: 'Bump the iOS marketing version patch number before release. Tick this after a version has shipped to the App Store (the release fails fast if the current version''s train is already closed).'
+ description: 'Use the first open iOS patch version after the checked-in version, skipping versions already closed on the App Store.'
required: false
default: false
type: boolean
diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml
index 2998d1dcce7..e9683904be2 100644
--- a/.github/workflows/mobile.yml
+++ b/.github/workflows/mobile.yml
@@ -30,6 +30,11 @@ jobs:
with:
node-version-file: package.json
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.3'
+
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -56,6 +61,9 @@ jobs:
- name: Test
run: pnpm test
+ - name: Test iOS release version resolution
+ run: ruby fastlane/ios_release_version_test.rb
+
- name: Lint
run: pnpm lint
diff --git a/README.md b/README.md
index aa4ee905281..0c8fb045410 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.31](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.31/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.32](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.32/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
@@ -230,7 +230,7 @@ yay -S stably-orca-bin
Pair with your desktop app to monitor and steer your agents from your phone.
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) or [join TestFlight](https://testflight.apple.com/join/YjeGMQBA)
-- **Android:** [Download APK 0.0.31](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.31/app-release.apk)
+- **Android:** [Download APK 0.0.32](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.32/app-release.apk)
---
diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc
index fad227b81fd..77b41ee22ff 100644
--- a/config/reliability-gates.jsonc
+++ b/config/reliability-gates.jsonc
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
- "updatedAt": "2026-07-20",
+ "updatedAt": "2026-07-22",
"policy": {
"maturityLevels": [
"experimental",
@@ -1390,6 +1390,7 @@
"headed desktop remote-server pairing",
"headless remote-server parity",
"daemon and relay reconnect",
+ "remote completion classification across disconnect and reconnect",
"terminal exit retirement and restart restore",
"mixed-version fallback"
],
@@ -1415,21 +1416,36 @@
"wsl",
"remote-runtime"
],
- "coverageNotes": "Deterministic macOS tests cover controller claims, daemon and SSH/relay operation replay, mixed-version selection, runtime ownership, exact provisional handoff, durable terminal retirement, two independent viewer mirrors, and guarded adoption of legacy live PTYs. The adoption harness models v1.4.150 agent/setup/shell tabs, current-generation restart and reconnect, exact handle/incarnation/worktree/host checks, topology CAS, competing clients, split-pane/group restoration, WSL ownership, and SSH owner rejection. The secondary parity repro runs independent clients against one headless remote Orca runtime over encrypted pairing and a real daemon-backed PTY. A headed Orca desktop server paired to a separate client is the primary live user topology and remains uncollected for this issue. SSH coverage is provider/relay contract and fault-injection coverage only; it does not substitute for paired-server coverage. Live Windows, WSL, and SSH hosts remain gaps.",
+ "coverageNotes": "Deterministic macOS tests cover controller claims, daemon and SSH/relay operation replay, mixed-version selection, runtime ownership, exact provisional handoff, durable terminal retirement, two independent viewer mirrors, guarded adoption of legacy live PTYs, and completion classification when either the outer remote transport or authoritative host/provider process inspection becomes unreachable. The adoption harness models v1.4.150 agent/setup/shell tabs, current-generation restart and reconnect, exact handle/incarnation/worktree/host checks, topology CAS, competing clients, split-pane/group restoration, WSL ownership, and SSH owner rejection. The secondary parity repro runs independent clients against one headless remote Orca runtime over encrypted pairing and a real daemon-backed PTY. The primary headed topology was exercised with an isolated macOS Orca desktop server, a separate paired Edge client, a real Codex process, injected WebSocket loss, reconnect, and explicit stop; host inspection and visible client state agreed throughout. SSH coverage is provider/relay contract and fault-injection coverage only; it does not substitute for paired-server coverage. Live Windows, Linux, WSL, and SSH hosts remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/8878",
+ "https://github.com/stablyai/orca/issues/9151",
"https://github.com/stablyai/orca/issues/9352",
"https://github.com/stablyai/orca/pull/9687"
],
- "invariant": "For every claim-capable execution route, one provider-session identity has at most one live PTY owner and one canonical host surface across concurrent clients, retries, reconnects, and stale publications. A live orphan may be adopted only when the controller proves its exact handle and incarnation, its worktree and host owner match, no competing visual owner exists, and a host topology CAS wins. A physical exit retires that exact incarnation durably so stale client state and host restart cannot recreate it. Mixed-version routes select the unchanged legacy request before any authority side effect or execution-owner-local filesystem access.",
- "oracle": "Race independent clients and repeated operation IDs, then assert one physical spawn and one canonical PTY/surface; inject exit-before-reply, provider disconnect, conflicting claim scope, old daemon/relay capabilities, reused handles, stale incarnations, owner mismatch, and topology revision conflict; assert safe adoption or explicit failure without a second spawn or wrong-process attachment. Restore legacy split panes and groups beside a newer host-owned tab, preserving output, input, resize, titles, tab/leaf identity, active group, and multi-client convergence. After exact exit, assert terminal and tab listings omit the surface, a stale publication cannot restore it, restart cannot resurrect it, and an exact provisional handoff is consumed even when exit wins before the next snapshot.",
+ "invariant": "For every claim-capable execution route, one provider-session identity has at most one live PTY owner and one canonical host surface across concurrent clients, retries, reconnects, and stale publications. A live orphan may be adopted only when the controller proves its exact handle and incarnation, its worktree and host owner match, no competing visual owner exists, and a host topology CAS wins. A viewer may classify completion only from successful host/provider inspection or explicit lifecycle evidence; transport, handle, or provider unavailability remains unknown and breaks any consecutive-idle proof. A physical exit retires that exact incarnation durably so stale client state and host restart cannot recreate it. Mixed-version routes select the unchanged legacy request before any authority side effect or execution-owner-local filesystem access.",
+ "oracle": "Race independent clients and repeated operation IDs, then assert one physical spawn and one canonical PTY/surface; inject exit-before-reply, provider disconnect, conflicting claim scope, old daemon/relay capabilities, reused handles, stale incarnations, owner mismatch, and topology revision conflict; assert safe adoption or explicit failure without a second spawn or wrong-process attachment. Restore legacy split panes and groups beside a newer host-owned tab, preserving output, input, resize, titles, tab/leaf identity, active group, and multi-client convergence. For completion, drive a known running agent through outer transport loss, authoritative provider rejection, reconnect, explicit stop, real exit status, and successful hook completion; assert unavailable evidence never dispatches completion and two fresh authoritative idle samples are required after the gap. After exact exit, assert terminal and tab listings omit the surface, a stale publication cannot restore it, restart cannot resurrect it, and an exact provisional handoff is consumed even when exit wins before the next snapshot.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/shared/claimed-agent-pty-owner.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/providers/ssh-pty-provider-agent-session-create-operation.test.ts src/main/runtime/orca-runtime-agent-session-operation.test.ts src/main/runtime/remote-agent-session-host-authority.integration.test.ts src/main/runtime/orca-runtime-terminal-retirement.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
"pnpm test:repro:remote-agent-session",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-orphan-owner.test.ts src/main/runtime/terminal-orphan-topology.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts --maxWorkers=1"
+ "Manual headed paired-server journey: isolated Orca desktop host + separate paired web client + real Codex process + 20-second WebSocket fault + reconnect + explicit stop",
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-orphan-owner.test.ts src/main/runtime/terminal-orphan-topology.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts --maxWorkers=1",
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/runtime/orca-runtime.test.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts",
+ "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts src/renderer/src/lib/codex-session-restart.test.ts"
],
"testFiles": [
+ "src/main/providers/pty-process-inspection.test.ts",
+ "src/main/daemon/terminal-host.test.ts",
+ "src/main/daemon/daemon-pty-router.test.ts",
+ "src/main/daemon/degraded-daemon-pty-provider.test.ts",
+ "src/relay/pty-handler.test.ts",
+ "src/main/runtime/orca-runtime.test.ts",
+ "tests/e2e/remote-agent-completion-authority.unit.test.ts",
+ "src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
+ "src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts",
+ "src/renderer/src/components/terminal-pane/pty-connection.test.ts",
+ "src/renderer/src/lib/codex-session-restart.test.ts",
"src/shared/claimed-agent-pty-owner.test.ts",
"src/main/daemon/daemon-pty-adapter.test.ts",
"src/main/providers/ssh-pty-provider-agent-session-create-operation.test.ts",
@@ -1448,6 +1464,62 @@
"src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts"
],
"assertionRefs": [
+ {
+ "file": "src/main/runtime/orca-runtime.test.ts",
+ "assertions": [
+ "completion-sensitive process inspection preserves authoritative host/provider failures"
+ ]
+ },
+ {
+ "file": "src/main/providers/pty-process-inspection.test.ts",
+ "assertions": [
+ "dedicated provider inspection preserves failures and rejects missing PTYs instead of returning idle evidence"
+ ]
+ },
+ {
+ "file": "src/main/daemon/daemon-pty-router.test.ts",
+ "assertions": [
+ "completion inspection rejects an unmapped session instead of borrowing the current daemon"
+ ]
+ },
+ {
+ "file": "src/main/daemon/degraded-daemon-pty-provider.test.ts",
+ "assertions": [
+ "completion inspection rejects an unmapped session instead of borrowing the local fallback"
+ ]
+ },
+ {
+ "file": "src/relay/pty-handler.test.ts",
+ "assertions": [
+ "strict relay inspection rejects a missing PTY"
+ ]
+ },
+ {
+ "file": "tests/e2e/remote-agent-completion-authority.unit.test.ts",
+ "assertions": [
+ "transport loss remains unknown through reconnect and cannot dispatch completion",
+ "returned unavailability or a thrown transport failure interrupts consecutive-idle proof and requires two fresh authoritative idle samples",
+ "explicit stop, real exit status, and genuine successful completion remain distinct"
+ ]
+ },
+ {
+ "file": "src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
+ "assertions": [
+ "direct SSH terminals use strict main-process inspection rather than lax split IPC evidence"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/terminal-pane/pty-connection.test.ts",
+ "assertions": [
+ "completion polling uses the atomic process-inspection boundary without regressing established lifecycle behavior"
+ ]
+ },
+ {
+ "file": "src/renderer/src/lib/codex-session-restart.test.ts",
+ "assertions": [
+ "one unreachable pane cannot suppress restart notices for another authoritatively confirmed Codex pane"
+ ]
+ },
{
"file": "src/shared/claimed-agent-pty-owner.test.ts",
"assertions": [
@@ -1532,6 +1604,24 @@
}
],
"evidenceRuns": [
+ {
+ "date": "2026-07-22",
+ "runner": "manual",
+ "platform": "macos",
+ "command": "Manual headed paired-server journey: isolated Orca desktop host + separate paired web client + real Codex process + 20-second WebSocket fault + reconnect + explicit stop",
+ "result": "passed",
+ "durationSeconds": 549,
+ "summary": "The primary user topology used an isolated headed Orca desktop as the owning server and a separate paired Edge client. Host inspection reported Codex alive before, during, and after a page-scoped WebSocket fault; the client showed no completion toast, reconnected to the same live Codex TUI, and explicit stop restored the shell prompt with no child process."
+ },
+ {
+ "date": "2026-07-23",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts src/renderer/src/lib/codex-session-restart.test.ts",
+ "result": "passed",
+ "durationSeconds": 11.96,
+ "summary": "Eleven focused files and 870 tests passed on the current-main candidate. The cross-boundary harness fails with the implementation reverted by dispatching process-exit from unavailable remote evidence. Direct SSH uses strict main-process inspection, daemon and relay inspection reject missing or unmapped sessions, the terminal lifecycle suite uses the atomic inspection boundary, and one stale pane cannot suppress restart notices for a separately confirmed Codex pane."
+ },
{
"date": "2026-07-21",
"runner": "local",
@@ -1542,13 +1632,13 @@
"summary": "Ten focused files and 325 tests passed after the final review fixes, covering claim scope, mixed-version Pi/SSH fallback ordering, operation replay, terminal retirement, causal inventory fencing, exact concurrent handoff confirmation, daemon-generation integration, transport behavior, and remote host integration."
},
{
- "date": "2026-07-21",
+ "date": "2026-07-22",
"runner": "local",
"platform": "macos",
"command": "pnpm test:repro:remote-agent-session",
"result": "passed",
- "durationSeconds": 48.47,
- "summary": "The secondary build-backed headless parity harness passed over encrypted WebSocket pairing with independent clients, proving one spawn, retry adoption, durable exit retirement, stale-publication rejection, and no restart resurrection."
+ "durationSeconds": 48.63,
+ "summary": "The secondary build-backed headless parity harness passed post-rebase on main@72a2d7bc7 over encrypted WebSocket pairing with independent clients, proving one spawn, retry adoption, durable exit retirement, stale-publication rejection, and no restart resurrection."
},
{
"date": "2026-07-22",
@@ -1588,22 +1678,23 @@
},
"redGreenEvidence": {
"status": "partial",
- "evidence": "The exact cross-boundary retirement oracle is red on pre-#9687 commit 2a32c5c9a and PR #9053 head d3a1d3047 because the pinned persisted surface remains in the host publication, and green on main@4fce2de49. Duplicate-resume red evidence remains encoded in lower-layer tests; saved CI artifacts are still needed."
+ "evidence": "Issue #9151 has local red/green evidence: current main dispatches process-exit from a stale remote handle, while the fix preserves unknown liveness and requires fresh consecutive idle evidence after reconnect. The exact retirement oracle is red on pre-#9687 commit 2a32c5c9a and PR #9053 head d3a1d3047 because the pinned persisted surface remains in the host publication, and green on main@4fce2de49. Duplicate-resume red evidence remains encoded in lower-layer tests; saved CI artifacts are still needed."
},
"performanceBudget": {
"required": true,
- "evidence": "Agent-session reconciliation runs only at explicit claim admission, dedupes concurrent provider listing, and adds no polling or renderer output work. Create-operation ledgers are capped globally and per client, expire after 24 hours, and reject rather than evict live replay fences. Capability caches are bounded or connection-scoped, and exact handoffs are consumed by the next authoritative snapshot."
+ "evidence": "Agent-session reconciliation runs only at explicit claim admission, dedupes concurrent provider listing, and adds no polling or renderer output work. Completion inspection reuses the coordinator's per-pane in-flight guard, global concurrency/rate queue, and existing error backoff; the strict daemon path reduces two foreground RPCs to one. Create-operation ledgers are capped globally and per client, expire after 24 hours, and reject rather than evict live replay fences. Capability caches are bounded or connection-scoped, and exact handoffs are consumed by the next authoritative snapshot."
},
"promotionCriteria": [
"Run the focused gate and remote-server repro for at least 100 consecutive passes or 14 days across required CI platforms.",
"Attach saved red/green evidence for duplicate remote resume and exit-before-snapshot retirement.",
- "Add the primary live journey with a headed Orca desktop server and a separate paired client; use a physical host when OS, ConPTY, update, sleep, firewall, or window lifecycle is causal.",
+ "Automate the headed Orca desktop-server journey with a separate paired client; add a physical host only when OS, ConPTY, update, sleep, firewall, or window lifecycle is causal.",
"Add live SSH/WSL provider evidence before claiming full provider coverage; Docker SSH proves only the SSH provider/relay path."
],
"knownGaps": [
- "The primary live topology—a headed Orca desktop server with an isolated profile paired to a separate persistent client—has not been run for this issue. The causal boundary is host membership rather than OS/window/ConPTY/update/sleep behavior, so a physical headed host was not required for the current disposition.",
+ "The primary headed macOS desktop-server journey is collected but not automated in CI; Windows and Linux window, ConPTY, update, sleep/wake, and firewall behavior remain uncollected.",
+ "Mixed-version pairings remain conservative only when the completion-aware client and strict-inspection host changes are both present; older peers retain their legacy classification behavior.",
"The secondary headless parity harness runs on macOS with a local daemon-backed execution owner and independent short-lived encrypted RPC clients; two persistent viewer-store mirrors and reconnect ordering are joined deterministically in the cross-boundary unit test rather than mounted live.",
- "SSH and relay failure ordering is deterministic provider-contract coverage, not a live SSH-host journey or paired-Orca-server proof; WSL has no provider-specific run.",
+ "SSH and relay failure ordering is deterministic provider-contract coverage, not a live SSH-host journey or paired-Orca-server proof; WSL has no provider-specific run, and Linux and Windows runs remain uncollected.",
"Fresh-launch operation replay is memory-backed and intentionally does not survive runtime restart; a durable operation journal is a documented future extension.",
"Automatic sleep checkpoints, verified nested-SSH execution namespaces, and multi-process profile coordination remain outside v1."
],
@@ -2461,11 +2552,107 @@
],
"demotionRule": "Cannot promote while Windows E2E is flaky, silently skipped, or screenshot-only."
},
+ {
+ "id": "terminal-performance.cold-restore-replay-budget",
+ "title": "Daemon cold restore keeps replay work and retained payloads bounded",
+ "maturity": "experimental",
+ "protection": "partial",
+ "owner": "terminal-runtime",
+ "layer": "main-daemon-unit",
+ "surfaces": [
+ "startup restore",
+ "daemon history replay",
+ "sleep and hibernation restore",
+ "main-process memory"
+ ],
+ "platforms": ["macos", "linux", "windows"],
+ "providers": ["daemon", "wsl"],
+ "coveredPlatforms": ["macos"],
+ "coveredProviders": ["daemon"],
+ "coverageNotes": "Deterministic main-process tests cover byte-bounded cache eviction and ACK release, one-at-a-time replay admission, a fixed per-turn replay budget within one large output record, UTF-16 boundary preservation, and checkpoint-only restore bypass while another replay is paused. The same HistoryReader path carries WSL context, but live WSL and cross-platform startup-scale runs remain gaps.",
+ "motivatingLinks": [
+ "https://github.com/stablyai/orca/issues/9971",
+ "https://github.com/stablyai/orca/pull/9990",
+ "https://github.com/stablyai/orca/issues/9441"
+ ],
+ "invariant": "Cold restore must reproduce persisted terminal output while admitting at most one scratch-emulator replay, yielding after at most 64 Ki UTF-16 code units or 1,024 replay operations, keeping sticky restore payloads within 16 MiB, and allowing header-only checkpoint restores to bypass the replay queue.",
+ "oracle": "Pause setImmediate during two single-batch restores larger than one replay slice and require exactly one admitted yield at a time, preserved text across a surrogate-pair slice boundary, and a concurrent header-only checkpoint restore to finish without consuming a replay slot. Cache tests require least-recently-used eviction, rejection of one oversized payload, and zero retained cache bytes after renderer ACK.",
+ "commands": [
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/cold-restore-payload-cache.test.ts src/main/daemon/history-reader.test.ts src/main/daemon/terminal-history-incremental-restore.test.ts src/main/daemon/hibernation-cold-restore-repro.test.ts src/main/daemon/daemon-pty-adapter.test.ts"
+ ],
+ "testFiles": [
+ "src/main/daemon/cold-restore-payload-cache.test.ts",
+ "src/main/daemon/history-reader.test.ts",
+ "src/main/daemon/terminal-history-incremental-restore.test.ts",
+ "src/main/daemon/hibernation-cold-restore-repro.test.ts",
+ "src/main/daemon/daemon-pty-adapter.test.ts"
+ ],
+ "assertionRefs": [
+ {
+ "file": "src/main/daemon/terminal-history-incremental-restore.test.ts",
+ "assertions": [
+ "large single-batch replays yield within the record, preserve a surrogate pair at the slice boundary, and admit only one scratch replay at a time",
+ "a header-only checkpoint restore completes while an unrelated incremental replay is paused"
+ ]
+ },
+ {
+ "file": "src/main/daemon/cold-restore-payload-cache.test.ts",
+ "assertions": [
+ "least-recently-used payloads are evicted to the aggregate byte budget",
+ "one payload larger than the entire budget is not retained"
+ ]
+ },
+ {
+ "file": "src/main/daemon/daemon-pty-adapter.test.ts",
+ "assertions": [
+ "StrictMode remount receives sticky cold-restore data until renderer ACK clears its retained bytes"
+ ]
+ }
+ ],
+ "evidenceRuns": [
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/cold-restore-payload-cache.test.ts src/main/daemon/history-reader.test.ts src/main/daemon/terminal-history-incremental-restore.test.ts src/main/daemon/hibernation-cold-restore-repro.test.ts src/main/daemon/daemon-pty-adapter.test.ts",
+ "result": "passed",
+ "durationSeconds": 5.68,
+ "summary": "Five focused files passed 150 tests, including deterministic single-record replay slicing, one-at-a-time admission, UTF-16 boundary preservation, header-only queue bypass, byte-bounded LRU eviction, and ACK cleanup."
+ }
+ ],
+ "runtimeBudget": {
+ "p95Seconds": 15,
+ "scope": "focused main-process cold-restore unit contract"
+ },
+ "flakeHistory": {
+ "status": "unknown",
+ "evidence": "The focused deterministic slice is new and has no CI or soak history yet."
+ },
+ "redGreenEvidence": {
+ "status": "partial",
+ "evidence": "The prior implementation had no yield inside one large batch and queued header-only restores behind the shared semaphore by inspection; an intentional pre-fix test run was not recorded."
+ },
+ "performanceBudget": {
+ "required": true,
+ "evidence": "Production admits one emulator replay globally, yields after a deterministic 64 Ki character or 1,024-operation budget even within one record, bypasses the semaphore for the common header-only final-checkpoint path, and caps sticky payloads at 16 MiB. No polling, subprocess, session inventory, or renderer wake loop is added."
+ },
+ "promotionCriteria": [
+ "Record an intentional-break red run for both the within-record yield and header-only bypass assertions.",
+ "Collect startup event-loop-delay evidence with dozens of near-cap histories on representative macOS, Windows, and Linux hardware.",
+ "Add live WSL restore evidence before claiming WSL coverage."
+ ],
+ "knownGaps": [
+ "The log decoder and final headless snapshot serialization remain synchronous inside the one-at-a-time replay slot; the gate bounds replay writes, not every CPU phase.",
+ "No live Electron startup-scale run currently proves first-pane paint order or end-to-end restore latency with dozens of histories.",
+ "SSH, remote-runtime, relay, and mobile do not use this local daemon history reader and are unaffected."
+ ],
+ "demotionRule": "Keep experimental or demote to protection none if output differs across replay slices, header-only restores consume a replay slot, retained payload bytes exceed the cap, or the focused gate flakes."
+ },
{
"id": "terminal-performance.no-hot-list-sessions",
"title": "Hot terminal interactions do not call global PTY session listing",
"maturity": "experimental",
- "protection": "none",
+ "protection": "partial",
"owner": "terminal-performance",
"layer": "ipc-count-contract",
"surfaces": [
@@ -2489,19 +2676,74 @@
"wsl",
"remote-runtime"
],
- "coveredPlatforms": [],
+ "coveredPlatforms": [
+ "macos"
+ ],
"coveredProviders": [],
- "coverageNotes": "Registered gap on main. The targeted-hasPty product hardening and no-hot count assertions exist only on the pending reliability stack. It registers here with its owning split PR.",
+ "coverageNotes": "Platform-neutral unit coverage proves the Resource Manager closed badge performs one readiness seed, coalesces unknown spawn signals, skips known-session reattach signals, and installs no interval. Broader terminal interaction coverage remains on the pending reliability stack.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/7002",
- "https://github.com/stablyai/orca/pull/6858"
+ "https://github.com/stablyai/orca/pull/6858",
+ "https://github.com/stablyai/orca/issues/9386",
+ "https://github.com/stablyai/orca/pull/9387"
],
"invariant": "Typing, focus, terminal switch, workspace switch, visibility resume, resize, render, and per-pane liveness paths must not call global pty:listSessions; they must use targeted per-PTY APIs or cached provider-owned state.",
- "oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and closed Resource Manager status badges avoid pty:listSessions; targeted hasPty/getSize calls are allowed for liveness/resize slices and forbidden for light tab/active-state resume. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session local/daemon/SSH fixtures.",
- "commands": [],
- "testFiles": [],
- "assertionRefs": [],
- "evidenceRuns": [],
+ "oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and a closed Resource Manager budget of one readiness seed plus one coalesced inventory read only for unknown spawn IDs; known-session reattach signals and steady closed time perform zero reads. Targeted hasPty/getSize calls are allowed for liveness/resize slices and forbidden for light tab/active-state resume. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session local/daemon/SSH fixtures.",
+ "commands": [
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty.test.ts src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx src/renderer/src/components/status-bar/resource-session-inventory.test.ts src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts"
+ ],
+ "testFiles": [
+ "src/main/ipc/pty.test.ts",
+ "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx",
+ "src/renderer/src/components/status-bar/resource-session-inventory.test.ts",
+ "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts"
+ ],
+ "assertionRefs": [
+ {
+ "file": "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx",
+ "assertions": [
+ "the false-to-true workspace readiness transition performs one daemon inventory seed",
+ "a failed readiness seed surfaces an error and a later inventory refresh recovers",
+ "known-session reattach signals perform zero additional inventory reads",
+ "multiple unknown background spawn signals coalesce to one inventory read",
+ "spawn signals during a slow inventory read never overlap provider-wide scans and cause at most one required follow-up",
+ "unknown sessions that exit before reconciliation cancel their queued inventory read",
+ "unmount during a slow inventory read cannot schedule follow-up work",
+ "exit and out-of-order refresh races cannot resurrect stale sessions"
+ ]
+ },
+ {
+ "file": "src/main/ipc/pty.test.ts",
+ "assertions": [
+ "global inventory starts local and SSH provider listings concurrently"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/status-bar/resource-session-inventory.test.ts",
+ "assertions": [
+ "daemon inventory construction copies its source and preserves count parity",
+ "single and batch removals preserve unrelated sessions and no-op references"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts",
+ "assertions": [
+ "the closed inventory hook installs no interval",
+ "the badge count comes from cached daemon inventory rather than wake-hint bindings"
+ ]
+ }
+ ],
+ "evidenceRuns": [
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty.test.ts src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx src/renderer/src/components/status-bar/resource-session-inventory.test.ts src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts",
+ "result": "passed",
+ "durationSeconds": 4.3,
+ "summary": "4 files and 358 tests passed, covering readiness seed/recovery, zero interval polling, bounded unknown-spawn reconciliation, concurrent provider starts, exit fencing, cleanup, and out-of-order refresh fencing."
+ }
+ ],
"runtimeBudget": {
"p95Seconds": 20,
"scope": "unit or focused Electron count gate"
@@ -2512,7 +2754,7 @@
},
"redGreenEvidence": {
"status": "partial",
- "evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout while still allowing the active PTY scheduler hint, SSH/remote broad listing is skipped, Resource Manager broad session inventory polling is scoped to the open popover rather than its closed badge, and panes close only on authoritative false. Needs broader raw focus/workspace-switch/render/high-session count coverage before promotion."
+ "evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout while still allowing the active PTY scheduler hint, SSH/remote broad listing is skipped, and the closed Resource Manager performs one readiness seed while known reattach signals and steady time perform no additional reads. Needs broader raw focus/workspace-switch/render/high-session count coverage before promotion."
},
"performanceBudget": {
"required": true,
@@ -2524,8 +2766,8 @@
"Run with enough preserved sessions/providers to make a broad listing observable."
],
"knownGaps": [
- "No executable coverage on main yet; the slice lives on the pending fix-terminal-reliability stack.",
- "Current command covers targeted visibility/first-input liveness, resize re-assertion on visibility resume, light tab/active-state resume, SSH/remote skip behavior, and closed Resource Manager session-poll avoidance, but not every hot interaction listed in the invariant.",
+ "Current commands cover Resource Manager readiness/lifecycle inventory counts; the broader targeted-liveness slice still lives on the pending fix-terminal-reliability stack.",
+ "Current coverage includes the closed Resource Manager's no-interval and known-reattach budgets, but not every hot interaction listed in the invariant.",
"No Electron or IPC-level high-session counter gate yet proves raw focus, workspace switch, render, or high-session typing stay at zero global listSessions calls."
],
"demotionRule": "Cannot promote if the test allows broad listing in any hot interaction path."
diff --git a/config/scripts/claude-account-windows-spawn-repro.mjs b/config/scripts/claude-account-windows-spawn-repro.mjs
new file mode 100644
index 00000000000..eaf04762e5b
--- /dev/null
+++ b/config/scripts/claude-account-windows-spawn-repro.mjs
@@ -0,0 +1,342 @@
+import { spawn } from 'node:child_process'
+import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { buildWindowsCommandInvocation } from '../../src/main/claude-accounts/windows-command-invocation.ts'
+
+const strategy = process.argv[2]
+if (!['baseline', 'candidate', 'explicit-cmd'].includes(strategy)) {
+ throw new Error(
+ 'Usage: node config/scripts/claude-account-windows-spawn-repro.mjs '
+ )
+}
+if (process.platform !== 'win32') {
+ throw new Error('This reproduction requires a physical Windows host.')
+}
+
+const expectedArgs = [
+ '',
+ 'two words',
+ 'amp&ersand',
+ 'pipe|value',
+ 'lessvalue',
+ 'caret^value',
+ 'trailing\\',
+ 'two-trailing\\\\',
+ '(parentheses)',
+ '100%',
+ '%ORCA_ARG_TRAP%',
+ 'bang!value',
+ '한글-λ'
+]
+const tempRoot = await mkdtemp(join(tmpdir(), 'orca-claude-spawn-'))
+const reportedDir = join(tempRoot, 'Profile with spaces 한글')
+const reportedCapturePath = join(reportedDir, 'capture.json')
+const reportedPidPath = join(reportedDir, 'pids.json')
+const reportedShimPath = join(reportedDir, 'claude fixture.cmd')
+const reportedFixturePath = join(reportedDir, 'capture-child.cjs')
+const fixtureDir = join(tempRoot, 'Profile space & ^ (paren) %ORCA_PATH_TRAP% !bang! 한글')
+const capturePath = join(fixtureDir, 'capture.json')
+const pidPath = join(fixtureDir, 'pids.json')
+const shimPath = join(fixtureDir, 'claude fixture.cmd')
+const fixturePath = join(fixtureDir, 'capture-child.cjs')
+const fixtureEnv = {
+ ...process.env,
+ CLAUDE_CONFIG_DIR: join(fixtureDir, 'config space & ^ (paren) %ORCA_ENV_LITERAL% !bang! 한글'),
+ ORCA_ARG_TRAP: 'EXPANDED_ARG',
+ ORCA_PATH_TRAP: 'EXPANDED_PATH',
+ ORCA_FIXTURE_CAPTURE: capturePath,
+ ORCA_FIXTURE_PIDS: pidPath,
+ ORCA_FIXTURE_NODE: process.execPath
+}
+const reportedEnv = {
+ ...fixtureEnv,
+ CLAUDE_CONFIG_DIR: join(reportedDir, 'config with spaces 한글'),
+ ORCA_FIXTURE_CAPTURE: reportedCapturePath,
+ ORCA_FIXTURE_PIDS: reportedPidPath
+}
+
+function quoteForCandidate(value) {
+ return `"${value.replace(/"/g, '""')}"`
+}
+
+function launch(args, command = shimPath, env = fixtureEnv) {
+ if (strategy === 'baseline') {
+ return spawn(command, args, { cwd: tempRoot, env, shell: true, windowsHide: true })
+ }
+ if (strategy === 'candidate') {
+ return spawn(quoteForCandidate(command), args, {
+ cwd: tempRoot,
+ env,
+ shell: true,
+ windowsHide: true
+ })
+ }
+ const invocation = buildWindowsCommandInvocation(command, args)
+ return spawn(invocation.command, invocation.args, {
+ cwd: tempRoot,
+ env,
+ shell: false,
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
+ windowsHide: true
+ })
+}
+
+function collect(child) {
+ return new Promise((resolve) => {
+ let stdout = ''
+ let stderr = ''
+ child.stdout?.on('data', (chunk) => (stdout += chunk.toString()))
+ child.stderr?.on('data', (chunk) => (stderr += chunk.toString()))
+ child.on('error', (error) => resolve({ code: null, stdout, stderr, error: error.message }))
+ child.on('close', (code) => resolve({ code, stdout, stderr, error: null }))
+ })
+}
+
+async function waitForFile(path, timeoutMs = 5_000) {
+ const deadline = Date.now() + timeoutMs
+ while (Date.now() < deadline) {
+ try {
+ return JSON.parse(await readFile(path, 'utf8'))
+ } catch {
+ await new Promise((resolve) => setTimeout(resolve, 25))
+ }
+ }
+ throw new Error(`Timed out waiting for fixture output: ${path}`)
+}
+
+async function taskExists(pid) {
+ const result = await collect(
+ spawn('tasklist.exe', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {
+ windowsHide: true
+ })
+ )
+ if (result.error || result.code !== 0) {
+ throw new Error(`tasklist failed for PID ${pid}: ${result.error ?? result.stderr}`)
+ }
+ return result.stdout.includes(`"${pid}"`)
+}
+
+async function killTree(pid) {
+ const result = await collect(
+ spawn('taskkill.exe', ['/pid', String(pid), '/t', '/f'], { windowsHide: true })
+ )
+ if (result.error || result.code !== 0) {
+ throw new Error(`taskkill failed for PID ${pid}: ${result.error ?? result.stderr}`)
+ }
+}
+
+async function waitForTreeExit(pids, timeoutMs = 5_000) {
+ const deadline = Date.now() + timeoutMs
+ let alive = {}
+ do {
+ alive = Object.fromEntries(
+ await Promise.all(
+ Object.entries(pids).map(async ([name, pid]) => [name, await taskExists(pid)])
+ )
+ )
+ if (!Object.values(alive).some(Boolean)) {
+ return alive
+ }
+ await new Promise((resolve) => setTimeout(resolve, 50))
+ } while (Date.now() < deadline)
+ return alive
+}
+
+const results = {
+ strategy,
+ reportedPath: null,
+ pathMatrix: {},
+ argvMatrix: {},
+ hostilePathAndArgv: null,
+ error: null,
+ cancellation: null
+}
+const fixtureSource =
+ `const { spawn } = require('node:child_process')\n` +
+ `const { writeFileSync } = require('node:fs')\n` +
+ `if (process.argv[2] === '--exit-error') { process.stderr.write('fixture error: 한글 & ^ % !\\n'); process.exit(23) }\n` +
+ `if (process.argv[2] === '--linger') {\n` +
+ ` const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { windowsHide: true })\n` +
+ ` writeFileSync(process.env.ORCA_FIXTURE_PIDS, JSON.stringify({ child: process.pid, grandchild: grandchild.pid }))\n` +
+ ` setInterval(() => {}, 1000)\n` +
+ `} else {\n` +
+ ` writeFileSync(process.env.ORCA_FIXTURE_CAPTURE, JSON.stringify({ argv: process.argv.slice(2), configDir: process.env.CLAUDE_CONFIG_DIR }))\n` +
+ `}\n`
+const shimSource = '@echo off\r\n"%ORCA_FIXTURE_NODE%" "%~dp0capture-child.cjs" %*\r\n'
+let lingeringShellPid = null
+try {
+ await mkdir(fixtureDir, { recursive: true })
+ await mkdir(reportedDir, { recursive: true })
+ await writeFile(fixturePath, fixtureSource, 'utf8')
+ await writeFile(shimPath, shimSource, 'utf8')
+ await writeFile(reportedFixturePath, fixtureSource, 'utf8')
+ await writeFile(reportedShimPath, shimSource, 'utf8')
+
+ const reportedArgs = ['auth', 'status', '--json']
+ const reportedRun = await collect(launch(reportedArgs, reportedShimPath, reportedEnv))
+ let reportedCapture = null
+ try {
+ reportedCapture = await waitForFile(reportedCapturePath, 1_000)
+ } catch {}
+ results.reportedPath = {
+ ...reportedRun,
+ actual: reportedCapture,
+ expected: { argv: reportedArgs, configDir: reportedEnv.CLAUDE_CONFIG_DIR },
+ pass:
+ reportedRun.code === 0 &&
+ JSON.stringify(reportedCapture) ===
+ JSON.stringify({ argv: reportedArgs, configDir: reportedEnv.CLAUDE_CONFIG_DIR })
+ }
+
+ for (const [name, segment] of Object.entries({
+ spaces: 'profile space',
+ ampersand: 'profile&name',
+ caret: 'profile^name',
+ parentheses: 'profile(name)',
+ percent: 'profile%ORCA_PATH_TRAP%',
+ bang: 'profile!name',
+ unicode: 'profile-한글-λ'
+ })) {
+ const directory = join(tempRoot, segment)
+ const captureFile = join(directory, 'capture.json')
+ const command = join(directory, 'claude fixture.cmd')
+ await mkdir(directory, { recursive: true })
+ await writeFile(join(directory, 'capture-child.cjs'), fixtureSource, 'utf8')
+ await writeFile(command, shimSource, 'utf8')
+ const env = {
+ ...fixtureEnv,
+ CLAUDE_CONFIG_DIR: join(directory, 'config'),
+ ORCA_FIXTURE_CAPTURE: captureFile
+ }
+ const run = await collect(launch(reportedArgs, command, env))
+ let actual = null
+ try {
+ actual = await waitForFile(captureFile, 500)
+ } catch {}
+ results.pathMatrix[name] = {
+ code: run.code,
+ stderr: run.stderr.trim(),
+ actual: actual?.argv ?? null,
+ pass: run.code === 0 && JSON.stringify(actual?.argv) === JSON.stringify(reportedArgs)
+ }
+ }
+
+ for (const [name, value] of Object.entries({
+ empty: '',
+ spaces: 'two words',
+ ampersand: 'amp&ersand',
+ pipe: 'pipe|value',
+ lessThan: 'lessvalue',
+ caret: 'caret^value',
+ trailingBackslash: 'trailing\\',
+ twoTrailingBackslashes: 'two-trailing\\\\',
+ parentheses: '(parentheses)',
+ percent: '%ORCA_ARG_TRAP%',
+ bang: 'bang!value',
+ unicode: '한글-λ'
+ })) {
+ const args = ['prefix', value, 'suffix']
+ const captureFile = join(reportedDir, `capture-${name}.json`)
+ const env = { ...reportedEnv, ORCA_FIXTURE_CAPTURE: captureFile }
+ const run = await collect(launch(args, reportedShimPath, env))
+ let actual = null
+ try {
+ actual = await waitForFile(captureFile, 500)
+ } catch {}
+ results.argvMatrix[name] = {
+ code: run.code,
+ stderr: run.stderr.trim(),
+ actual: actual?.argv ?? null,
+ pass: run.code === 0 && JSON.stringify(actual?.argv) === JSON.stringify(args)
+ }
+ }
+
+ const argvRun = await collect(launch(expectedArgs))
+ let capture = null
+ try {
+ capture = await waitForFile(capturePath, 1_000)
+ } catch {}
+ results.hostilePathAndArgv = {
+ ...argvRun,
+ actual: capture,
+ expected: { argv: expectedArgs, configDir: fixtureEnv.CLAUDE_CONFIG_DIR },
+ pass:
+ argvRun.code === 0 &&
+ JSON.stringify(capture) ===
+ JSON.stringify({ argv: expectedArgs, configDir: fixtureEnv.CLAUDE_CONFIG_DIR })
+ }
+
+ results.error = await collect(launch(['--exit-error'], reportedShimPath, reportedEnv))
+ results.error.pass =
+ results.error.code === 23 && results.error.stderr.includes('fixture error: 한글 & ^ % !')
+
+ const lingering = launch(['--linger'], reportedShimPath, reportedEnv)
+ lingeringShellPid = lingering.pid
+ const lingeringResult = collect(lingering)
+ try {
+ const pids = await waitForFile(reportedPidPath)
+ await killTree(lingering.pid)
+ const alive = await waitForTreeExit({
+ shell: lingering.pid,
+ child: pids.child,
+ grandchild: pids.grandchild
+ })
+ results.cancellation = {
+ shell: lingering.pid,
+ ...pids,
+ alive,
+ pass: !Object.values(alive).some(Boolean)
+ }
+ } catch (error) {
+ if (await taskExists(lingering.pid)) {
+ await killTree(lingering.pid)
+ }
+ const launchResult = await Promise.race([
+ lingeringResult,
+ new Promise((resolve) =>
+ setTimeout(
+ () => resolve({ code: null, error: 'fixture did not exit after cleanup' }),
+ 5_000
+ )
+ )
+ ])
+ results.cancellation = {
+ shell: lingering.pid,
+ launchResult,
+ pass: false,
+ error: error instanceof Error ? error.message : String(error)
+ }
+ }
+} finally {
+ if (lingeringShellPid && (await taskExists(lingeringShellPid))) {
+ await killTree(lingeringShellPid)
+ }
+ try {
+ let pids
+ try {
+ pids = JSON.parse(await readFile(reportedPidPath, 'utf8'))
+ } catch {
+ pids = JSON.parse(await readFile(pidPath, 'utf8'))
+ }
+ for (const pid of [pids.child, pids.grandchild]) {
+ if (await taskExists(pid)) {
+ await killTree(pid)
+ }
+ }
+ } catch {}
+ await rm(tempRoot, { recursive: true, force: true })
+}
+
+console.log(JSON.stringify(results, null, 2))
+process.exitCode =
+ results.reportedPath?.pass &&
+ Object.values(results.pathMatrix).every((result) => result.pass) &&
+ Object.values(results.argvMatrix).every((result) => result.pass) &&
+ results.hostilePathAndArgv?.pass &&
+ results.error?.pass &&
+ results.cancellation?.pass
+ ? 0
+ : 1
diff --git a/config/scripts/computer-e2e-workflow.test.mjs b/config/scripts/computer-e2e-workflow.test.mjs
index 3ef41583217..560859c5264 100644
--- a/config/scripts/computer-e2e-workflow.test.mjs
+++ b/config/scripts/computer-e2e-workflow.test.mjs
@@ -177,7 +177,7 @@ describe('computer-use e2e workflow', () => {
)
})
- it('builds Electron main output before every computer-use e2e run', () => {
+ it('builds Electron main output and verifies the CLI before every computer-use e2e run', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8')
)
@@ -187,6 +187,7 @@ describe('computer-use e2e workflow', () => {
.map((step) => step.run)
.filter((run) => typeof run === 'string')
const buildIndex = runs.indexOf('pnpm build:electron-vite')
+ const cliVerifyIndex = runs.indexOf('pnpm verify:cli-runtime')
const e2eIndexes = runs
.map((run, index) => (run.includes('test:e2e:computer') ? index : -1))
.filter((index) => index >= 0)
@@ -195,10 +196,17 @@ describe('computer-use e2e workflow', () => {
buildIndex,
`${jobName} should build out/main before computer e2e`
).toBeGreaterThanOrEqual(0)
+ expect(
+ cliVerifyIndex,
+ `${jobName} should verify the CLI after the main build`
+ ).toBeGreaterThan(buildIndex)
for (const e2eIndex of e2eIndexes) {
expect(buildIndex, `${jobName} should build out/main before computer e2e`).toBeLessThan(
e2eIndex
)
+ expect(cliVerifyIndex, `${jobName} should verify the CLI before computer e2e`).toBeLessThan(
+ e2eIndex
+ )
}
}
})
diff --git a/config/scripts/verify-cli-bin.test.mjs b/config/scripts/verify-cli-bin.test.mjs
index da988064d05..d26a603a148 100644
--- a/config/scripts/verify-cli-bin.test.mjs
+++ b/config/scripts/verify-cli-bin.test.mjs
@@ -47,6 +47,14 @@ describe('verifyPackageCliBin', () => {
})
})
+ it('rejects a compiled CLI with a missing runtime dependency', () => {
+ const { projectDir } = makeProjectWithCli(
+ '#!/usr/bin/env node\nrequire("./missing-runtime-dependency")\n'
+ )
+
+ expect(() => verifyPackageCliBin({ projectDir, runHelp: true })).toThrow()
+ })
+
it('rejects an empty package bin target', () => {
const { projectDir } = makeProjectWithCli('')
diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json
index 0a79fb4d665..6b31708de90 100644
--- a/config/tsconfig.cli.json
+++ b/config/tsconfig.cli.json
@@ -3,6 +3,10 @@
"include": [
"../src/cli/**/*",
"../src/shared/**/*",
+ "../src/main/agent-state-file-reader.ts",
+ "../src/main/agent-hooks/agent-hook-file-comparison.ts",
+ "../src/main/agent-hooks/agent-hook-file-limits.ts",
+ "../src/main/agent-hooks/agent-hook-sftp-text-reader.ts",
"../src/main/agent-hooks/hook-stdin-contract.ts",
"../src/main/agent-hooks/hook-config-write-path.ts",
"../src/main/agent-hooks/hooks-json-read.ts",
@@ -19,10 +23,15 @@
"../src/main/codex/codex-app-server-client.ts",
"../src/main/codex/codex-app-server-grant-bridge.ts",
"../src/main/codex/codex-app-server-grant-envelope.ts",
+ "../src/main/codex/codex-app-server-grant-json.ts",
"../src/main/codex/codex-app-server-session.ts",
"../src/main/codex/codex-config-mirror.ts",
"../src/main/codex/codex-config-path-reference-rewrite.ts",
+ "../src/main/codex/codex-config-settings-preservation.ts",
+ "../src/main/codex/codex-config-settings-removal.ts",
+ "../src/main/codex/codex-config-settings-upsert.ts",
"../src/main/codex/codex-home-paths.ts",
+ "../src/main/codex/codex-host-retry-deadlines.ts",
"../src/main/codex/codex-hook-identity.ts",
"../src/main/codex/codex-hook-trust-grant.ts",
"../src/main/codex/codex-managed-trust-reconciliation.ts",
@@ -34,7 +43,11 @@
"../src/main/codex/codex-user-hook-trust-rebase-client.ts",
"../src/main/codex/codex-user-hook-trust-rebase.ts",
"../src/main/codex/codex-wsl-hook-install-plan.ts",
+ "../src/main/codex/codex-wsl-reconciliation-generations.ts",
+ "../src/main/codex/config-settings-baseline.ts",
+ "../src/main/codex/config-settings-conflict-resolution.ts",
"../src/main/codex/config-settings-promotion.ts",
+ "../src/main/codex/config-toml-key-path.ts",
"../src/main/codex/config-toml-line-scan.ts",
"../src/main/codex/config-toml-trust.ts",
"../src/main/codex/hook-service.ts",
@@ -42,12 +55,14 @@
"../src/main/codex-accounts/fs-utils.ts",
"../src/main/codex-accounts/wsl-codex-command.ts",
"../src/main/codex-cli/command.ts",
+ "../src/main/codex-cli/nvm-version-directory-discovery.ts",
"../src/main/command-code/command-code-managed-script.ts",
"../src/main/command-code/hook-service.ts",
"../src/main/copilot/hook-service.ts",
"../src/main/cursor/hook-service.ts",
"../src/main/droid/hook-service.ts",
"../src/main/gemini/hook-service.ts",
+ "../src/main/generated-node-bounded-file-reader.ts",
"../src/main/grok/hook-service.ts",
"../src/main/devin/hook-settings.ts",
"../src/main/devin/hook-service.ts",
diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg
index 1f82e4c1b53..4d25959e53c 100644
--- a/docs/assets/readme-downloads.svg
+++ b/docs/assets/readme-downloads.svg
@@ -1,5 +1,5 @@
- |