diff --git a/.gitattributes b/.gitattributes
index d995215b26cf..be42f7cfd68d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -7,6 +7,12 @@
/skill-stubs/_shared/*.md text eol=lf
/skills/*/SKILL.md text eol=lf
/src/cli/bundled-skill-guides.ts text eol=lf
+# SVG bytes feed embedded data URLs and asset hashes on every build host.
+*.svg text eol=lf
+# Compiler input line breaks must not depend on the checkout OS.
+*.ts text eol=lf
+*.tsx text eol=lf
+*.html text eol=lf
# Bundled plugin trees are byte-hashed; CRLF checkout would break the pinned hash.
/resources/plugins/** text eol=lf
# Relay assets are copied verbatim into the bundle and hashed byte-for-byte into
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index ab70a4dc4e0f..389f25bd4dbe 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -573,6 +573,35 @@ jobs:
pnpm exec vitest run --config config/vitest.config.ts \
src/main/pty/codex-no-daemon-binary-contract.test.ts
+ # Why both pins and latest: Orca approves its managed-home hook entry with a hash
+ # Codex computes, so a release that changes it must redden here before users
+ # meet a review screen.
+ - name: Verify Codex hook file-entry contract (pinned)
+ env:
+ ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1'
+ run: |
+ set -euo pipefail
+ for install in codex-cli:"$CODEX_CLI_VERSION" codex-cli-no-daemon:"$CODEX_NO_DAEMON_CLI_VERSION"; do
+ ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/${install%%:*}/node_modules/.bin/codex" \
+ ORCA_CODEX_HOOK_CONTRACT_VERSION="${install#*:}" \
+ pnpm exec vitest run --config config/vitest.config.ts \
+ src/main/codex/codex-hook-file-entry-binary-contract.test.ts
+ done
+
+ - name: Install latest Codex CLI
+ run: |
+ set -euo pipefail
+ npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-latest" "@openai/codex@latest"
+
+ - name: Verify Codex hook file-entry contract (latest)
+ env:
+ ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1'
+ run: |
+ set -euo pipefail
+ ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-latest/node_modules/.bin/codex" \
+ pnpm exec vitest run --config config/vitest.config.ts \
+ src/main/codex/codex-hook-file-entry-binary-contract.test.ts
+
# Why the no-daemon install: it is the newest pinned Codex, and its project
# lookup decides whether a worktree launch stops at the trust prompt.
- name: Verify Codex project-trust contract
@@ -1032,6 +1061,7 @@ jobs:
src/main/browser/browser-route-webrtc-egress.electron.test.ts
src/main/browser/browser-route-h3-egress.electron.test.ts
src/main/browser/browser-route-dns-prefetch.electron.test.ts
+ src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
# Restore independent fixtures during builds; keep native lifecycle probes serial.
- uses: ./.github/actions/prepare-linux-package-fixture
@@ -1203,6 +1233,7 @@ jobs:
src/main/browser/browser-route-webrtc-egress.electron.test.ts
src/main/browser/browser-route-h3-egress.electron.test.ts
src/main/browser/browser-route-dns-prefetch.electron.test.ts
+ src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts
src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts
src/shared/child-process/windows-command-line.win32.test.ts
diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml
index 3073d160d11a..d6282a804a02 100644
--- a/.github/workflows/release-cut.yml
+++ b/.github/workflows/release-cut.yml
@@ -1212,12 +1212,25 @@ jobs:
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
+ # Compilation can overlap the release gates; signing remains behind release-preflight.
+ release-javascript:
+ needs: cut
+ if: needs.cut.outputs.should_release == 'true'
+ permissions:
+ contents: read
+ uses: ./.github/workflows/release-javascript.yml
+ with:
+ ref: refs/tags/${{ needs.cut.outputs.tag }}
+ secrets:
+ ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }}
+
build:
needs:
- cut
- create-release
- orcad-template
- release-preflight
+ - release-javascript
- relay-windows-process-tree
# Why not the implicit success(): a tag without the orcad template skips that job on purpose.
if: >-
@@ -1226,6 +1239,7 @@ jobs:
needs.cut.outputs.should_release == 'true' &&
needs.create-release.result == 'success' &&
needs.release-preflight.result == 'success' &&
+ needs.release-javascript.result == 'success' &&
needs.relay-windows-process-tree.result == 'success' &&
(needs.orcad-template.result == 'success' ||
(needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false'))
@@ -1460,26 +1474,30 @@ jobs:
}
cargo --version
- # Why ORCA_POSTHOG_WRITE_KEY here: this is the only build that
- # produces a published binary, so this is the only place the secret
- # needs to be in scope. The key is a PostHog *project* API key, not
- # a server secret — it ships in every official binary's app.asar
- # and is therefore extractable from any release. We still keep it
- # in GitHub Actions secrets so the literal stays out of the repo
- # (and out of fork CI runs / log scrapers / casual greps).
- # Why ORCA_BUILD_IDENTITY here (not in env at the job level): the
- # value comes from the per-tag classification above and electron-vite
- # reads it from `process.env` during `pnpm build:release` only.
- # Why ORCA_DIAGNOSTICS_TOKEN_URL here: official builds pin crash
- # diagnostic uploads to Orca's endpoint at compile time, matching the
- # telemetry gate's "official binary only" behavior.
+ # Consumers verify the same official build configuration before restoring the bundle.
+ - name: Download release JavaScript
+ if: needs.release-javascript.outputs.supported == 'true'
+ uses: actions/download-artifact@v8
+ with:
+ name: release-javascript
+ path: .build/release-javascript
+
- name: Build app
- run: pnpm build:release
+ shell: bash
+ run: |
+ if [ "$SHARED_JAVASCRIPT" = true ]; then
+ node config/scripts/release-javascript-artifact.mjs restore
+ pnpm run build:release:host
+ else
+ pnpm run build:release
+ fi
env:
# Why: Vite's web build crossed Node's default old-space ceiling on
# the macOS release runner, leaving v1.4.2-rc.8 as an incomplete draft.
NODE_OPTIONS: --max-old-space-size=4096
ORCA_BUILD_IDENTITY: ${{ steps.tag-classify.outputs.identity }}
+ SHARED_JAVASCRIPT: ${{ needs.release-javascript.outputs.supported }}
+ ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ needs.release-javascript.outputs.source_sha }}
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }}
# Fail the release rather than ship a relay that cannot launch outside
@@ -2330,6 +2348,7 @@ jobs:
- create-release
- orcad-template
- release-preflight
+ - release-javascript
# release-mac-build.yml downloads the relay addons from this run.
- relay-windows-process-tree
# Why not the implicit success(): a tag without the orcad template skips that job on purpose.
@@ -2339,6 +2358,7 @@ jobs:
needs.cut.outputs.should_release == 'true' &&
needs.create-release.result == 'success' &&
needs.release-preflight.result == 'success' &&
+ needs.release-javascript.result == 'success' &&
needs.relay-windows-process-tree.result == 'success' &&
(needs.orcad-template.result == 'success' ||
(needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false'))
@@ -2364,6 +2384,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_MAC_BUILD_REF: ${{ github.ref_name }}
RELEASE_MAC_BUILD_RELEASE_RUN_ID: ${{ github.run_id }}
+ RELEASE_MAC_BUILD_JAVASCRIPT_SOURCE_SHA: ${{ needs.release-javascript.outputs.supported == 'true' && needs.release-javascript.outputs.source_sha || '' }}
RELEASE_MAC_BUILD_TAG: ${{ needs.cut.outputs.tag }}
RELEASE_MAC_BUILD_WORKFLOW: release-mac-build.yml
diff --git a/.github/workflows/release-javascript-benchmark.yml b/.github/workflows/release-javascript-benchmark.yml
new file mode 100644
index 000000000000..6a5fc4efa31e
--- /dev/null
+++ b/.github/workflows/release-javascript-benchmark.yml
@@ -0,0 +1,120 @@
+name: Release JavaScript comparison
+
+on:
+ pull_request:
+ paths: ['.github/workflows/release-javascript-benchmark.yml']
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: release-javascript-comparison-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+env:
+ ORCA_BACKGROUND_LAUNCH: '1'
+ ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
+ ORCA_POSTHOG_WRITE_KEY: ci-build-comparison
+ NODE_OPTIONS: --max-old-space-size=4096
+
+jobs:
+ bundle:
+ uses: ./.github/workflows/release-javascript.yml
+ with:
+ ref: ${{ github.sha }}
+ secrets:
+ ORCA_POSTHOG_WRITE_KEY: ci-build-comparison
+
+ measure:
+ needs: bundle
+ name: ${{ matrix.mode }} ${{ matrix.host.platform }} ${{ matrix.host.arch }}
+ runs-on: ${{ matrix.host.os }}
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ max-parallel: 8
+ matrix:
+ mode: [baseline, shared]
+ host:
+ - os: ubuntu-latest
+ platform: linux
+ arch: x64
+ - os: ubuntu-24.04-arm
+ platform: linux
+ arch: arm64
+ - os: windows-2022
+ platform: win32
+ arch: x64
+ - os: macos-15
+ platform: darwin
+ arch: arm64
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ needs.bundle.outputs.source_sha }}
+ persist-credentials: false
+ - uses: ./.github/actions/install-node-dependencies
+ with:
+ native-runtime: node
+ persist-native-cache: 'false'
+ - uses: ./.github/actions/install-mobile-dependencies
+ - name: Install Linux provider dependencies
+ if: runner.os == 'Linux'
+ run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-atspi-2.0 at-spi2-core xclip xdotool
+ - name: Resolve build identity
+ shell: bash
+ run: |
+ RELEASE_TAG="v$(node -p 'require("./package.json").version')"
+ export RELEASE_TAG
+ echo "ORCA_BUILD_IDENTITY=$(node config/scripts/release-javascript-artifact.mjs identity)" >> "$GITHUB_ENV"
+ - name: Download shared JavaScript
+ if: matrix.mode == 'shared'
+ uses: actions/download-artifact@v8
+ with:
+ name: release-javascript
+ path: .build/release-javascript
+ - name: Measure release build
+ shell: bash
+ env:
+ MEASUREMENT_MODE: ${{ matrix.mode }}
+ ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ needs.bundle.outputs.source_sha }}
+ run: node config/scripts/release-javascript-benchmark.mjs "$MEASUREMENT_MODE"
+ - uses: actions/upload-artifact@v7
+ with:
+ name: release-javascript-measurement-${{ matrix.mode }}-${{ matrix.host.platform }}-${{ matrix.host.arch }}
+ path: .build/release-javascript-measurements/
+ retention-days: 7
+
+ comparison:
+ needs: [bundle, measure]
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ actions: read
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ needs.bundle.outputs.source_sha }}
+ persist-credentials: false
+ - uses: actions/setup-node@v6
+ with:
+ node-version-file: package.json
+ - uses: ./.github/actions/install-node-dependencies
+ - uses: actions/download-artifact@v8
+ with:
+ pattern: release-javascript-measurement-*
+ path: .build/release-javascript-measurements
+ - name: Compare build and transfer timings
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" > .build/release-javascript-measurements/jobs.json
+ node config/scripts/release-javascript-benchmark.mjs summary
+ cat .build/release-javascript-measurements/comparison.md >> "$GITHUB_STEP_SUMMARY"
+ - uses: actions/upload-artifact@v7
+ if: always()
+ with:
+ name: release-javascript-comparison
+ path: .build/release-javascript-measurements/comparison.md
+ retention-days: 30
diff --git a/.github/workflows/release-javascript.yml b/.github/workflows/release-javascript.yml
new file mode 100644
index 000000000000..2fc33c8d30c3
--- /dev/null
+++ b/.github/workflows/release-javascript.yml
@@ -0,0 +1,79 @@
+name: Release JavaScript bundle
+
+on:
+ workflow_call:
+ inputs:
+ ref:
+ required: true
+ type: string
+ outputs:
+ supported:
+ value: ${{ jobs.bundle.outputs.supported }}
+ source_sha:
+ value: ${{ jobs.bundle.outputs.source_sha }}
+ secrets:
+ ORCA_POSTHOG_WRITE_KEY:
+ required: true
+
+permissions:
+ contents: read
+
+env:
+ ORCA_BACKGROUND_LAUNCH: '1'
+
+jobs:
+ bundle:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ outputs:
+ supported: ${{ steps.source.outputs.supported }}
+ source_sha: ${{ steps.source.outputs.sha }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ inputs.ref }}
+ persist-credentials: false
+
+ # Older release refs retain their original per-platform build commands.
+ - name: Resolve bundle source and support
+ id: source
+ shell: bash
+ run: |
+ echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ node -e 'const p = require("./package.json"); console.log("supported=" + Boolean(p.scripts["build:release:javascript"] && p.scripts["build:release:host"]))' >> "$GITHUB_OUTPUT"
+
+ - uses: ./.github/actions/install-node-dependencies
+ if: steps.source.outputs.supported == 'true'
+ with:
+ native-runtime: node
+
+ - uses: ./.github/actions/install-mobile-dependencies
+ if: steps.source.outputs.supported == 'true'
+
+ - name: Resolve release build identity
+ id: identity
+ if: steps.source.outputs.supported == 'true'
+ run: |
+ RELEASE_TAG="v$(node -p 'require("./package.json").version')"
+ export RELEASE_TAG
+ echo "value=$(node config/scripts/release-javascript-artifact.mjs identity)" >> "$GITHUB_OUTPUT"
+
+ - name: Build and archive release JavaScript
+ if: steps.source.outputs.supported == 'true'
+ env:
+ NODE_OPTIONS: --max-old-space-size=4096
+ ORCA_BUILD_IDENTITY: ${{ steps.identity.outputs.value }}
+ ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
+ ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }}
+ run: |
+ pnpm run build:release:javascript
+ node config/scripts/release-javascript-artifact.mjs pack
+
+ - uses: actions/upload-artifact@v7
+ if: steps.source.outputs.supported == 'true'
+ with:
+ name: release-javascript
+ path: .build/release-javascript/
+ compression-level: 0
+ retention-days: 7
+ if-no-files-found: error
diff --git a/.github/workflows/release-mac-build.yml b/.github/workflows/release-mac-build.yml
index 80694364bae4..22558411c8c1 100644
--- a/.github/workflows/release-mac-build.yml
+++ b/.github/workflows/release-mac-build.yml
@@ -13,6 +13,11 @@ on:
description: release-cut workflow run that requested this build
required: true
type: string
+ javascript_source_sha:
+ description: Commit of the shared JavaScript artifact; empty keeps the legacy build
+ required: false
+ default: ''
+ type: string
permissions:
# actions: read downloads the orcad template the parent release-cut run built.
@@ -143,13 +148,29 @@ jobs:
run-id: ${{ inputs.release_run_id }}
github-token: ${{ github.token }}
+ - name: Download release JavaScript from the release run
+ if: inputs.javascript_source_sha != ''
+ uses: actions/download-artifact@v8
+ with:
+ name: release-javascript
+ path: .build/release-javascript
+ run-id: ${{ inputs.release_run_id }}
+ github-token: ${{ github.token }}
+
- name: Build app
- run: pnpm build:release
+ run: |
+ if [ -n "$ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA" ]; then
+ node config/scripts/release-javascript-artifact.mjs restore
+ pnpm run build:release:host
+ else
+ pnpm run build:release
+ fi
env:
# Why: Vite's web build crossed Node's default old-space ceiling on
# the macOS release runner, leaving v1.4.2-rc.8 as an incomplete draft.
NODE_OPTIONS: --max-old-space-size=4096
ORCA_BUILD_IDENTITY: ${{ steps.tag-classify.outputs.identity }}
+ ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ inputs.javascript_source_sha }}
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }}
# Fail the release rather than ship a relay that cannot launch outside
diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs
index b0666c42a3d6..60a842d0343a 100644
--- a/config/electron-builder.config.cjs
+++ b/config/electron-builder.config.cjs
@@ -206,6 +206,8 @@ module.exports = {
'!out/runtimes{,/**/*}',
'!out/node-runtime-cache{,/**/*}',
'!config{,/**/*}',
+ // Release archives and other build staging are not runtime resources.
+ '!.build{,/**/*}',
'!docs{,/**/*}',
'!mobile{,/**/*}',
'!native{,/**/*}',
diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc
index 6df235c0a0f4..346fe9ee7d1b 100644
--- a/config/reliability-gates.jsonc
+++ b/config/reliability-gates.jsonc
@@ -10,6 +10,309 @@
}
},
"gates": [
+ {
+ "id": "native-chat.transcript-consumed-boundary",
+ "title": "Transcript replacement remains visible when it races message publication",
+ "maturity": "experimental",
+ "protection": "partial",
+ "owner": "native-chat",
+ "layer": "host-file-reader-and-renderer",
+ "surfaces": ["native chat live transcript"],
+ "platforms": ["macos", "linux", "windows"],
+ "providers": ["local", "ssh", "wsl", "remote-runtime"],
+ "coveredPlatforms": ["macos"],
+ "coveredProviders": ["local"],
+ "coverageNotes": "Real local JSONL files exercise the shared host reader with Claude records; hidden Electron proof traverses actual host IPC and chat rendering. No physical remote or Windows/Linux deployment claim.",
+ "motivatingLinks": ["https://github.com/stablyai/orca/actions/runs/37524761640"],
+ "invariant": "A retained transcript cursor fingerprint contains only bytes actually consumed at that cursor. A larger in-place rewrite during publication must be delivered, and subsequent ordinary appends arrive once. Closing or failing a test leaves no active watcher.",
+ "oracle": "Synchronous larger rewrites during append, initial snapshot and replacement snapshot callbacks deliver new exactly once, followed by followup exactly once. A 4 MiB read retains an independent 64-byte backing allocation. Hidden Electron reproduces the same ordering at snapshot IPC publication and asserts the new rendered text replaces the old conversation.",
+ "commands": [
+ "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/transcript-watch-inplace-drain-race.test.ts src/main/native-chat/transcript-file-version.test.ts src/main/native-chat/transcript-watch.test.ts --maxWorkers=2",
+ "ORCA_BACKGROUND_LAUNCH=1 pnpm run test:e2e tests/e2e/native-chat-inplace-drain-race.spec.ts --workers=1"
+ ],
+ "testFiles": [
+ "src/main/native-chat/transcript-watch-inplace-drain-race.test.ts",
+ "src/main/native-chat/transcript-file-version.test.ts",
+ "src/main/native-chat/transcript-watch.test.ts",
+ "tests/e2e/native-chat-inplace-drain-race.spec.ts"
+ ],
+ "assertionRefs": [
+ {
+ "file": "src/main/native-chat/transcript-watch-inplace-drain-race.test.ts",
+ "assertions": ["delivers a larger rewrite while publishing a completed %s"]
+ },
+ {
+ "file": "src/main/native-chat/transcript-file-version.test.ts",
+ "assertions": ["owns only the final 64 bytes of a large read"]
+ },
+ {
+ "file": "tests/e2e/native-chat-inplace-drain-race.spec.ts",
+ "assertions": [
+ "renders a larger transcript rewrite during snapshot publication, then ordinary appends"
+ ]
+ }
+ ],
+ "evidenceRuns": [
+ {
+ "date": "2026-10-06",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/transcript-watch-inplace-drain-race.test.ts src/main/native-chat/transcript-file-version.test.ts src/main/native-chat/transcript-watch.test.ts --maxWorkers=2",
+ "result": "passed",
+ "durationSeconds": 2.85,
+ "summary": "32 tests in three files pass, including all three publication modes, replacement sizes, partial records and watcher teardown."
+ }
+ ],
+ "runtimeBudget": {
+ "p95Seconds": 30,
+ "scope": "Focused local unit command; CI/platform soak not established."
+ },
+ "flakeHistory": {
+ "status": "not-started",
+ "evidence": "Deterministic publication ordering replaces a timing-dependent filesystem race; no long-term soak claim."
+ },
+ "redGreenEvidence": {
+ "status": "complete",
+ "evidence": "Identical append callback regression fails on parent main with only old observed and passes with consumed-byte fingerprinting."
+ },
+ "performanceBudget": {
+ "required": true,
+ "evidence": "At most 64 owned, unpooled bytes per cursor. Fingerprint capture reuses already-read chunks and removes the post-publication filesystem read; no new timers, polls or subprocesses."
+ },
+ "knownGaps": [
+ "Physical SSH/WSL/runtime hosts and native Linux/Windows remain unverified.",
+ "This gate targets rewrites during publication, not atomic snapshots of arbitrary concurrent multi-chunk file overwrites."
+ ],
+ "promotionCriteria": [
+ "Collect policy soak and native/remote host evidence without weakening delivery or resource assertions."
+ ],
+ "demotionRule": "Remain experimental until soak and platform coverage; investigate missing replacements without retries or longer timeouts."
+ },
+ {
+ "id": "terminal-input.prompt-reply-occurrence-acknowledgment",
+ "title": "Terminal chat replies hide only the acknowledged prompt occurrence",
+ "maturity": "experimental",
+ "protection": "partial",
+ "owner": "terminal-runtime",
+ "layer": "renderer-mobile-provider-contract",
+ "surfaces": [
+ "desktop terminal chat prompt cards",
+ "mobile terminal chat composer",
+ "verified PTY input and paced answers"
+ ],
+ "platforms": ["macos", "linux", "windows"],
+ "providers": ["local", "daemon", "ssh", "wsl", "remote-runtime", "mobile-relay"],
+ "coveredPlatforms": ["macos"],
+ "coveredProviders": ["local", "daemon", "ssh", "remote-runtime", "mobile-relay"],
+ "coverageNotes": "Production mobile controller, parser, presentation, overlay, view, question, permission and composer run with native surfaces mocked on macOS. Desktop selector groups traverse the real observed-write queue, verified helper, IPC chunker, SSH provider and transport callbacks; paired sends traverse the real dispatcher and runtime writer into that provider. Local/daemon contracts and released-version terminal codecs are covered. Provider labels describe contracts, not physical deployments; physical WSL/Linux/Windows remain gaps.",
+ "motivatingLinks": ["https://github.com/stablyai/orca/pull/25761"],
+ "invariant": "A prompt occurrence leaves the input region only when an accepted reply answers it (a current host's provider acknowledgment, or an older host's whole-write acceptance as before) or the user collapses it to a strip above the composer, which writes nothing and keeps the card mounted with its partial answer; status-backed occurrences are told apart by the host wait's start, so view toggles and remounts preserve them while real clear/replacement supersedes them, and a transcript-only one is dropped when the view stops observing it; obsolete results cannot change replacements; selector groups wait for complete writes before pacing; refusal or unknown handoff never causes replay; a card retires an unsubmitted composer send without writing under it or losing its text, and the hidden composer cannot send or interrupt; Collapse or Escape folds a card without writing, never during an in-flight answer, and a new occurrence shows expanded again; the phone blocks only Send while a card is expanded and shows one card per occurrence; cancellation stops unsent groups and active dictation can stop.",
+ "oracle": "After acknowledged phone replies with stale host status, the exact card disappears and Send is enabled; unknown, refused or unverifiable-settlement replies retain enabled choices, while an older host's accepted reply dismisses like an acknowledged one; a same-occurrence view toggle retains pending acknowledgment and real replacements discard it. A slow 32 KiB SSH answer completes all chunks before Enter; cancellation preserves issued bytes but omits remaining groups. Paired settlement invokes the actual provider and lost callbacks remain unknown; older hosts deliver ordinary messages, launch and selector groups once and their accepted answers dismiss as before. An answered card stays hidden across a view remount; a collapsed card is a strip that frees the composer and expands back; an identical question in a new wait shows again; a composer send caught by a new card keeps its bubble without submitting or clearing the line under it.",
+ "commands": [
+ "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/runtime/runtime-terminal-verified-input.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty-write-ipc-validation.test.ts src/main/providers/ssh-pty-write.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
+ "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/mobile-native-chat-permission-send.test.ts mobile/src/session/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/use-mobile-native-chat-answer-send.test.ts mobile/src/session/use-mobile-native-chat-message-send.test.ts mobile/src/session/mobile-native-chat-send.test.ts mobile/src/session/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/MobileNativeChatPromptCard.test.tsx --maxWorkers=2",
+ "ORCA_BACKGROUND_LAUNCH=1 pnpm test tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts --maxWorkers=1",
+ "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty-write-ipc-validation.test.ts src/main/providers/ssh-pty-write.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts --maxWorkers=2",
+ "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.test.ts --maxWorkers=2"
+ ],
+ "testFiles": [
+ "src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx",
+ "src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx",
+ "src/renderer/src/components/native-chat/native-chat-observed-send.test.ts",
+ "src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts",
+ "src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts",
+ "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx",
+ "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx",
+ "src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts",
+ "src/renderer/src/runtime/runtime-terminal-verified-input.test.ts",
+ "src/renderer/src/components/native-chat/NativeChatComposer.test.tsx",
+ "src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx",
+ "src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx",
+ "src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts",
+ "src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
+ "src/renderer/src/lib/agent-paste-draft.test.ts",
+ "src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts",
+ "src/renderer/src/lib/agent-followup-delivery.test.ts",
+ "src/main/ipc/pty/ipc/write-input-acceptance.test.ts",
+ "src/main/ipc/pty/ipc/write-input-user-input.test.ts",
+ "src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts",
+ "src/main/ipc/pty-write-ipc-validation.test.ts",
+ "src/main/providers/ssh-pty-write.test.ts",
+ "src/main/daemon/daemon-pty-write-settlement-recovery.test.ts",
+ "src/main/runtime/runtime-terminal-writer.test.ts",
+ "src/main/runtime/rpc/terminal-send.test.ts",
+ "src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts",
+ "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts",
+ "src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts",
+ "tests/e2e/native-chat-ssh-delivery.unit.test.ts",
+ "mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts",
+ "mobile/src/session/use-mobile-native-chat-controller.test.ts",
+ "mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts",
+ "mobile/src/session/MobileNativeChatView.test.ts",
+ "mobile/src/session/mobile-prompt-controller.test.ts",
+ "mobile/src/session/mobile-native-chat-permission-send.test.ts",
+ "mobile/src/session/use-mobile-native-chat-cancel-ask.test.ts",
+ "mobile/src/session/use-mobile-native-chat-answer-send.test.ts",
+ "mobile/src/session/use-mobile-native-chat-message-send.test.ts",
+ "mobile/src/session/mobile-native-chat-send.test.ts",
+ "mobile/src/session/MobileNativeChatCardHeaderAction.test.tsx",
+ "mobile/src/session/MobileNativeChatPromptCard.test.tsx",
+ "tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts"
+ ],
+ "assertionRefs": [
+ {
+ "file": "src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx",
+ "assertions": [
+ "ignores approval A settling after answered replacement B",
+ "ignores a raw acknowledgment from a rebound PTY",
+ "sends only one cancellation, can cancel an answer, and permits retry after unknown"
+ ]
+ },
+ {
+ "file": "mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts",
+ "assertions": [
+ "accepted Deny hides exactly that occurrence and restores Send across reconnect",
+ "a late answer cannot reshow an answered replacement occurrence",
+ "a late answer cannot dismiss a replacement session"
+ ]
+ },
+ {
+ "file": "src/main/ipc/pty/ipc/write-input-acceptance.test.ts",
+ "assertions": [
+ "reports proven refusal and unknown acknowledgment separately",
+ "waits for each chunk and stops a paste after refusal",
+ "asks a pane awaiting daemon recovery to remount on either route"
+ ]
+ },
+ {
+ "file": "mobile/src/session/mobile-prompt-controller.test.ts",
+ "assertions": [
+ "keeps a question actionable after lost or unverifiable acknowledgment",
+ "acknowledges the same pending permission or Ask after a view-only toggle",
+ "keeps an acknowledged Deny hidden after the chat screen remounts on a lingering status",
+ "drops an accepted result after real prompt/session/PTY/tab/clear replacement",
+ "finishes an older-host selector once and dismisses it like an acknowledged answer",
+ "dismisses older-host approval choices like an acknowledged answer and restores Send",
+ "collapses a permission to a strip without writing, and expands it back",
+ "shows an identical heuristic question again after a new wait in terminal view",
+ "collapses an ask without writing and hides the heuristic card read from the same wait"
+ ]
+ },
+ {
+ "file": "tests/e2e/native-chat-ssh-delivery.unit.test.ts",
+ "assertions": [
+ "serializes every chunk of a healthy large SSH answer before Enter and dismissal",
+ "cancels remaining paced groups while an already-issued body still settles",
+ "paired terminal.send uses provider settlement for accepted and lost acknowledgment",
+ "older paired hosts keep their whole-write verdict for answers and ordinary sequences",
+ "older paired hosts confirm ordinary chat sends and pasted answers as before",
+ "finishes every older-host selector group once and settles the answer as delivered"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx",
+ "assertions": [
+ "keeps an in-flight message visible and never submits it into the approval",
+ "routes no typing to the hidden composer until the card is answered"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx",
+ "assertions": [
+ "keeps an answered approval hidden across a chat view remount and a PTY rebind",
+ "collapses to a strip that frees the composer, and expanding gives the card the input back",
+ "shows an identical question again in a new wait that arrived while the view was unmounted",
+ "keeps a collapsed transcript question collapsed while the transcript re-reads",
+ "keeps a partly answered question across collapse and expand, and Escape in its text field"
+ ]
+ },
+ {
+ "file": "src/main/runtime/runtime-terminal-writer.test.ts",
+ "assertions": [
+ "waits for complete body settlement before Enter",
+ "does not send the suffix after refusal or unknown settlement",
+ "returns unknown after an acknowledged prefix followed by refusal or bookkeeping failure",
+ "preserves legacy raw body and Enter writes"
+ ]
+ }
+ ],
+ "evidenceRuns": [
+ {
+ "date": "2026-10-05",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty-write-ipc-validation.test.ts src/main/providers/ssh-pty-write.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts --maxWorkers=2",
+ "result": "passed",
+ "durationSeconds": 232.36,
+ "summary": "257 tests passed across 19 explicit files, covering production cards, observed sends, existing launch/paste callers, IPC validation, SSH acknowledgment and daemon settlement recovery."
+ },
+ {
+ "date": "2026-10-05",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.test.ts --maxWorkers=2",
+ "result": "passed",
+ "durationSeconds": 10.32,
+ "summary": "73 tests passed across four explicit files after the controller extraction, covering the production view/card/composer, occurrence dismissal, controller session scoping, unknown/refused replies and stopping active dictation."
+ },
+ {
+ "date": "2026-10-06",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/runtime/runtime-terminal-verified-input.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty-write-ipc-validation.test.ts src/main/providers/ssh-pty-write.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
+ "result": "passed",
+ "durationSeconds": 14.53,
+ "summary": "391 tests passed across 29 explicit files after the round-4 review fix, including older-host acceptance, the inert hidden composer, collapse that keeps a partial answer and returns focus to the composer, pane-scoped card focus, wait-start occurrence identity and the pane remount signal."
+ },
+ {
+ "date": "2026-10-06",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/mobile-native-chat-permission-send.test.ts mobile/src/session/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/use-mobile-native-chat-answer-send.test.ts mobile/src/session/use-mobile-native-chat-message-send.test.ts mobile/src/session/mobile-native-chat-send.test.ts mobile/src/session/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/MobileNativeChatPromptCard.test.tsx --maxWorkers=2",
+ "result": "passed",
+ "durationSeconds": 1.19,
+ "summary": "186 tests passed across 12 explicit files after the round-3 review fixes, including older-host answers dismissing like acknowledged ones, collapse that keeps the card mounted, one card per occurrence, wait-start question identity and only Send blocked."
+ },
+ {
+ "date": "2026-10-06",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts --maxWorkers=1",
+ "result": "passed",
+ "durationSeconds": 25.68,
+ "summary": "10 terminal wire codec compatibility cases passed against current and two supported released source versions; this is a codec/RPC-reader contract, not a physical mixed-version paired deployment."
+ }
+ ],
+ "runtimeBudget": {
+ "p95Seconds": 300,
+ "scope": "Focused desktop and mobile unit/contract commands on the shared machine; CI p95 is not established."
+ },
+ "flakeHistory": {
+ "status": "not-started",
+ "evidence": "Local regression runs passed; cross-platform CI soak has not started."
+ },
+ "redGreenEvidence": {
+ "status": "partial",
+ "evidence": "Immutable 8b2538 baseline reproduced Enter overtaking the SSH body and paired raw acceptance without provider settlement. Desired-behavior ablations replacing only baseline mobile sender/occurrence modules fail five of fourteen cases; replacing selector scheduling fails one of six; replacing paired runtime writer fails two of six. The entire gate has not been reverted, and rendered/platform soak remains absent."
+ },
+ "performanceBudget": {
+ "required": true,
+ "evidence": "No polling, subprocess, startup await, new status producer or dismissal timer. Reuses the existing bounded per-tab committed observation and per-PTY queue; waits for each group before its existing pacing timer. Duplicate-cancel and yielding chunk tests bound writes. Optional paired proof reuses provider settlement only for requesting clients; legacy requests keep their raw path."
+ },
+ "knownGaps": [
+ "Provider acknowledgment proves handoff, not agent consumption or prompt closure; a host prompt gate remains deferred pending captured closing lifecycles.",
+ "Rendered Electron and physical mobile/microphone behavior require remote QA; native surfaces are mocked in these unit tests.",
+ "Physical WSL, Linux/Windows, paired mixed-version deployment and network fault campaigns have not been exercised.",
+ "Older hosts provide whole-write handoff only; their accepted answers dismiss the card as before, without provider acknowledgment.",
+ "A transcript-only question has no wait start: after the view stops observing it, a collapse or answer is dropped and the card shows again.",
+ "Cancelling a paced answer stops future groups; bytes already handed to the provider, including pending chunks, can still settle."
+ ],
+ "promotionCriteria": [
+ "Record change-reverted red/green evidence and repeatable remote rendered approval/question/dictation journeys.",
+ "Collect physical provider/platform and CI soak evidence before promotion."
+ ],
+ "demotionRule": "Keep experimental until the missing evidence is collected; investigate failures without weakening occurrence, acknowledgment, cancellation or refusal assertions."
+ },
{
"id": "agent-status.claude-task-wakeup-cycle",
"title": "Claude task wake-ups cannot finish an automation before its lead turn",
@@ -8720,7 +9023,7 @@
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts src/renderer/src/components/shared/useDaemonActions.test.tsx src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts src/main/ipc/pty-management.test.ts",
"result": "passed",
- "durationSeconds": 1.31,
+ "durationSeconds": 1.18,
"summary": "4 test files and 27 tests passed locally, including the existing daemon handler contract and a real 100-terminal Zustand cleanup fixture."
}
],
diff --git a/config/scripts/ci-shard-assignment.mjs b/config/scripts/ci-shard-assignment.mjs
index 1cb44969a637..2bdc7a5ad5d4 100644
--- a/config/scripts/ci-shard-assignment.mjs
+++ b/config/scripts/ci-shard-assignment.mjs
@@ -4,13 +4,7 @@ import { dirname } from 'node:path'
export const compareIds = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
-export function balanceFiles(files, count, timings, overheadMs = 0) {
- if (!Number.isInteger(count) || count < 1) {
- throw new Error('Invalid shard count')
- }
- if (new Set(files).size !== files.length) {
- throw new Error('Duplicate discovered file')
- }
+export function rankFilesByDuration(files, timings, overheadMs = 0) {
const known = Object.values(timings).filter((value) => Number.isFinite(value) && value > 0)
known.sort((a, b) => a - b)
const fallbackMs = known[Math.floor(known.length / 2)] ?? 1000
@@ -21,6 +15,17 @@ export function balanceFiles(files, count, timings, overheadMs = 0) {
overheadMs
}))
weighted.sort((a, b) => b.durationMs - a.durationMs || compareIds(a.file, b.file))
+ return { weighted, fallbackMs }
+}
+
+export function balanceFiles(files, count, timings, overheadMs = 0) {
+ if (!Number.isInteger(count) || count < 1) {
+ throw new Error('Invalid shard count')
+ }
+ if (new Set(files).size !== files.length) {
+ throw new Error('Duplicate discovered file')
+ }
+ const { weighted, fallbackMs } = rankFilesByDuration(files, timings, overheadMs)
const shards = Array.from({ length: count }, () => ({ files: [], durationMs: 0 }))
for (const entry of weighted) {
const target = shards.reduce((best, shard) =>
diff --git a/config/scripts/ci-shard-assignment.test.mjs b/config/scripts/ci-shard-assignment.test.mjs
index 607a73d392c4..323328fe8bc6 100644
--- a/config/scripts/ci-shard-assignment.test.mjs
+++ b/config/scripts/ci-shard-assignment.test.mjs
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { BaseSequencer } from 'vitest/node'
+import RuntimeSequencer from './vitest-runtime-sequencer.mjs'
import { balanceFiles } from './ci-shard-assignment.mjs'
import { discoverE2eFiles, planE2e } from './ci-e2e-shard-plan.mjs'
import { parseTimingLog } from './ci-shard-timing-import.mjs'
@@ -37,7 +37,7 @@ describe('timing-weighted shard selection', () => {
expect(() => balanceFiles(['a'], 0, {})).toThrow('count')
})
- it('uses the post-filter Vitest discovery unchanged across eight shards and retains default sort', async () => {
+ it('uses the post-filter Vitest discovery unchanged across eight shards', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-unit-shards-'))
directories.push(directory)
vi.stubEnv('ORCA_SHARD_MANIFEST', join(directory, 'assignment.json'))
@@ -49,7 +49,7 @@ describe('timing-weighted shard selection', () => {
const sequencer = new TimingSequencer({
config: { root: process.cwd(), shard: { index, count: 8 } }
})
- expect(sequencer.sort).toBe(BaseSequencer.prototype.sort)
+ expect(sequencer.sort).toBe(RuntimeSequencer.prototype.sort)
selected.push(...(await sequencer.shard(specs)))
const manifest = JSON.parse(readFileSync(join(directory, 'assignment.json'), 'utf8'))
expect(manifest.selectedShard).toBe(index)
diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json
index 9ba114966a87..0badaf6a7583 100644
--- a/config/scripts/ci-shard-timings.json
+++ b/config/scripts/ci-shard-timings.json
@@ -1237,7 +1237,6 @@
"src/main/codex/codex-persistent-command-retention.test.ts": 276,
"src/main/codex/codex-pinned-rollout-proof.test.ts": 53,
"src/main/codex/codex-prompt-registry-retention.test.ts": 376,
- "src/main/codex/codex-real-home-hook-install.test.ts": 339,
"src/main/codex/codex-real-home-path.test.ts": 57,
"src/main/codex/codex-requested-close-turn-timing.test.ts": 134,
"src/main/codex/codex-rollout-session-meta.test.ts": 81,
@@ -4604,7 +4603,6 @@
"src/main/window/terminal-tab-close-request-relay.test.ts": 55,
"src/main/window/updater-package-recovery-ipc.test.ts": 4430,
"src/main/window/window-close-decision.test.ts": 41,
- "src/main/windows-descendant-exit-verification.test.ts": 100,
"src/main/windows-live-tree-kill.win32.test.ts": 46,
"src/main/windows-process-tree-kill.test.ts": 61,
"src/main/windows-pty-root-identity.test.ts": 94,
@@ -5757,7 +5755,7 @@
"src/renderer/src/components/native-chat/native-chat-composer-containment.test.ts": 38,
"src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1320,
"src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 622,
- "src/renderer/src/components/native-chat/native-chat-composer-scope-cache.test.ts": 47,
+ "src/shared/native-chat-scope-cache.test.ts": 47,
"src/renderer/src/components/native-chat/native-chat-composer-state.test.ts": 98,
"src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 1742,
"src/renderer/src/components/native-chat/native-chat-context-usage-summary.test.ts": 54,
diff --git a/config/scripts/ci-unit-sequencer.mjs b/config/scripts/ci-unit-sequencer.mjs
index 94dcd933f45a..a609e03657e7 100644
--- a/config/scripts/ci-unit-sequencer.mjs
+++ b/config/scripts/ci-unit-sequencer.mjs
@@ -1,9 +1,9 @@
import { relative } from 'node:path'
import { readFileSync } from 'node:fs'
-import { BaseSequencer } from 'vitest/node'
+import RuntimeSequencer from './vitest-runtime-sequencer.mjs'
import { balanceFiles, readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
-export default class TimingSequencer extends BaseSequencer {
+export default class TimingSequencer extends RuntimeSequencer {
async shard(specs) {
const { index, count } = this.ctx.config.shard
const key = (spec) => relative(this.ctx.config.root, spec.moduleId).replaceAll('\\', '/')
diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs
index 454693e46d75..3daa56d1aa69 100644
--- a/config/scripts/electron-builder-config.test.mjs
+++ b/config/scripts/electron-builder-config.test.mjs
@@ -47,6 +47,27 @@ describe('electron-builder config', () => {
)
})
+ it('keeps release build staging out of app.asar while preserving runtime output', () => {
+ const matcher = new FileMatcher('/app', '/dest', (value) => value, electronBuilderConfig.files)
+ matcher.prependPattern('**/*')
+ const isPacked = matcher.createFilter()
+ expect(isPacked(join('/app', '.build'), { isDirectory: () => true })).toBe(false)
+ for (const stagingPath of [
+ '.build/release-javascript/release-javascript.tar.gz',
+ '.build/release-javascript/manifest.json',
+ '.build/release-javascript-123/out/main/index.js'
+ ]) {
+ expect(isPacked(join('/app', stagingPath), { isDirectory: () => false })).toBe(false)
+ }
+ for (const runtimePath of [
+ 'out/main/index.js',
+ 'out/cli/index.js',
+ 'out/renderer/index.html'
+ ]) {
+ expect(isPacked(join('/app', runtimePath), { isDirectory: () => false })).toBe(true)
+ }
+ })
+
it('keeps local agent tooling out of app.asar', () => {
const matcher = new FileMatcher('/app', '/dest', (value) => value, electronBuilderConfig.files)
matcher.prependPattern('**/*')
diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs
index d640244e1efe..8b7446c6be40 100644
--- a/config/scripts/pr-code-change-scope.mjs
+++ b/config/scripts/pr-code-change-scope.mjs
@@ -57,8 +57,30 @@ const GIT_COMPAT_PREFIXES = [
// Why narrow: the contract pins Codex's read-repair, so it runs when the heal that
// depends on it, its app-server transport, or the contract itself changes. The same
-// job pins --no-daemon for Orca's codex shell wrapper and the project-trust key.
+// job pins --no-daemon for Orca's codex shell wrapper, the project-trust key, and the
+// approval Orca writes for its hook entry in managed Codex homes and ~/.codex.
const CODEX_INDEX_HEAL_CONTRACT_PREFIXES = [
+ 'src/main/codex/codex-hook-file-entry-binary-contract',
+ 'src/main/codex/codex-hook-trust-',
+ 'src/main/codex/codex-hook-approval-first-write',
+ 'src/main/codex/codex-hook-hash-lookup',
+ 'src/main/codex/codex-hook-orca-approvals',
+ 'src/main/codex/codex-hook-reconcile',
+ 'src/main/codex/codex-hook-local-install',
+ 'src/main/codex/codex-hook-user-mirroring',
+ 'src/main/codex/codex-real-home-hook-',
+ 'src/main/codex/codex-real-home-hooks-json',
+ 'src/main/codex/codex-user-hook-trust-moves',
+ 'src/main/codex/codex-hook-definition',
+ 'src/main/codex/codex-hook-command-form',
+ 'src/main/codex/codex-hook-identity',
+ 'src/main/codex/codex-app-server-client',
+ 'src/main/codex/codex-trust-identity',
+ 'src/main/codex/config-toml-hook-trust-',
+ 'src/main/codex/config-toml-key-path',
+ 'src/main/agent-hooks/posix-hook-command',
+ 'src/main/agent-hooks/hook-post-command',
+ 'src/main/codex-cli/codex-read-only-app-server-args',
'src/main/agent-trust-presets',
'src/main/codex/config-toml-trust',
'src/main/pty/codex-no-daemon-binary-contract',
diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs
index 9420a44b11c9..27c1dab78f86 100644
--- a/config/scripts/pr-code-change-scope.test.mjs
+++ b/config/scripts/pr-code-change-scope.test.mjs
@@ -159,15 +159,16 @@ describe('per-job path classification', () => {
expectClassification(['src/main/codex/codex-index-heal-binary-contract.test.ts'], {
codex_index_heal_contract: true
})
- for (const file of ['src/main/agent-trust-presets.ts', 'src/main/codex/config-toml-trust.ts']) {
- expectClassification([file], {
- codex_index_heal_contract: true,
- package: true,
- package_windows: true
- })
- }
- // Keep the real-binary gate live when a transport or launch dependency changes.
+ // Keep the real-binary gate live when a trust, transport, launch or hook-approval dependency changes.
for (const file of [
+ 'src/main/agent-trust-presets.ts',
+ 'src/main/codex/config-toml-trust.ts',
+ 'src/main/codex/codex-hook-trust-derivation.ts',
+ 'src/main/codex/codex-hook-local-install.ts',
+ 'src/main/codex/codex-hook-orca-approvals.ts',
+ 'src/main/codex/codex-hook-reconcile.ts',
+ 'src/main/codex/config-toml-hook-trust-edit.ts',
+ 'src/main/codex-cli/codex-read-only-app-server-args.ts',
'src/main/codex/codex-app-server-capability-signal.ts',
'src/main/provider-process/provider-process-exit-deadline.ts',
'src/main/provider-process/provider-process-launch.ts',
diff --git a/config/scripts/project-renderer-web-client.mjs b/config/scripts/project-renderer-web-client.mjs
index c7999358cbf8..a3df2d9b1b56 100644
--- a/config/scripts/project-renderer-web-client.mjs
+++ b/config/scripts/project-renderer-web-client.mjs
@@ -154,7 +154,10 @@ async function minifyWebOutput() {
const result = await transform(readFileSync(targetPath, 'utf8'), {
legalComments: 'none',
loader,
- minify: true,
+ // Vite asset scripts already have short names; keep them stable across asset hash changes.
+ minifyIdentifiers: !outputPath.startsWith('assets/'),
+ minifySyntax: true,
+ minifyWhitespace: true,
target: 'es2020'
})
writeFileSync(targetPath, result.code)
diff --git a/config/scripts/project-renderer-web-client.test.mjs b/config/scripts/project-renderer-web-client.test.mjs
index 188da677924f..c31cd940e192 100644
--- a/config/scripts/project-renderer-web-client.test.mjs
+++ b/config/scripts/project-renderer-web-client.test.mjs
@@ -74,6 +74,24 @@ afterEach(() => {
})
describe('renderer web client projection', () => {
+ it('preserves existing minified bindings while producing runnable compact code', async () => {
+ const root = createRendererFixture()
+ writeFixtureFile(
+ root,
+ 'out/renderer/assets/web-shared.js',
+ 'const Zq = [1, 2, 3]; export function nM() { return Zq.length; }'
+ )
+ const result = await projectFixture(root)
+ expect(result.code, result.stderr).toBe(0)
+ const code = readFileSync(join(root, 'out/web/assets/web-shared.js'), 'utf8')
+ expect(code).toContain('function nM(')
+ expect(code).not.toContain('[1, 2, 3]')
+ const output = await import(
+ `data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
+ )
+ expect(output.nM()).toBe(3)
+ })
+
it('keeps the build-only manifest out of packaged apps', () => {
const builderConfig = readFileSync(resolve('config/electron-builder.config.cjs'), 'utf8')
diff --git a/config/scripts/release-cut-token-permissions.test.mjs b/config/scripts/release-cut-token-permissions.test.mjs
index 808e52194981..d4a6d36bc4ed 100644
--- a/config/scripts/release-cut-token-permissions.test.mjs
+++ b/config/scripts/release-cut-token-permissions.test.mjs
@@ -64,6 +64,10 @@ const EXPECTED_MATRIX = {
},
[`${RELEASE_WORKFLOW}#post-release-e2e`]: { actions: 'write' },
[`${RELEASE_WORKFLOW}#publish-release`]: { contents: 'write' },
+ [`${RELEASE_WORKFLOW}#release-javascript`]: { contents: 'read' },
+ [`${RELEASE_WORKFLOW}#release-javascript -> .github/workflows/release-javascript.yml#bundle`]: {
+ contents: 'read'
+ },
[`${RELEASE_WORKFLOW}#release-preflight`]: { contents: 'read' },
[`${RELEASE_WORKFLOW}#relay-windows-process-tree`]: { contents: 'read' },
[`${RELEASE_WORKFLOW}#relay-windows-process-tree -> .github/workflows/relay-windows-process-tree.yml#build`]:
diff --git a/config/scripts/release-javascript-artifact.mjs b/config/scripts/release-javascript-artifact.mjs
new file mode 100644
index 000000000000..13b4eb066049
--- /dev/null
+++ b/config/scripts/release-javascript-artifact.mjs
@@ -0,0 +1,176 @@
+import assert from 'node:assert/strict'
+import { createHash } from 'node:crypto'
+import {
+ chmodSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ readdirSync,
+ renameSync,
+ rmSync,
+ statSync,
+ writeFileSync
+} from 'node:fs'
+import { join, resolve } from 'node:path'
+import { runProcessSync, describeProcessFailure } from './script-child-process.mjs'
+import { isDirectInvocation } from './script-entry-detection.mjs'
+import { DESKTOP_RC_TAG, DESKTOP_STABLE_TAG } from './release-tag-patterns.mjs'
+import { getTarProgram } from './zip-extractor-command.mjs'
+
+const REQUIRED_FILES = [
+ 'cli/index.js',
+ 'main/index.js',
+ 'preload/index.js',
+ 'renderer/index.html',
+ 'renderer/.vite/manifest.json',
+ 'web/web-index.html',
+ 'mobile-web/manifest.json',
+ 'package.json'
+]
+const NATIVE_FILE = /\.(?:node|exe|dll|dylib|so(?:\.\d+)*|a)$/i
+const ARCHIVE_NAME = 'release-javascript.tar.gz'
+
+function sha256(bytes) {
+ return createHash('sha256').update(bytes).digest('hex')
+}
+
+function command(program, args, cwd) {
+ const result = runProcessSync({ program, args, cwd, timeoutMs: 120_000 })
+ assert.equal(result.code, 0, describeProcessFailure(result))
+ return result.stdout.trim()
+}
+
+export function releaseBuildIdentity(tag) {
+ if (DESKTOP_RC_TAG.test(tag)) {
+ return 'rc'
+ }
+ if (DESKTOP_STABLE_TAG.test(tag)) {
+ return 'stable'
+ }
+ throw new Error(`Invalid desktop release tag: ${tag}`)
+}
+
+export function releaseJavascriptConfiguration(root, env = process.env) {
+ const sourceSha = command('git', ['rev-parse', 'HEAD'], root)
+ assert.match(sourceSha, /^[a-f0-9]{40}$/)
+ if (env.ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA) {
+ assert.equal(
+ sourceSha,
+ env.ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA,
+ 'Release checkout differs from bundle source'
+ )
+ }
+ const { version, packageManager } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
+ const identity = releaseBuildIdentity(`v${version}`)
+ assert.equal(env.ORCA_BUILD_IDENTITY, identity, 'Release build identity differs from version')
+ assert(env.ORCA_DIAGNOSTICS_TOKEN_URL, 'Missing release diagnostics URL')
+ assert(env.ORCA_POSTHOG_WRITE_KEY, 'Missing release telemetry key')
+ return {
+ sourceSha,
+ version,
+ packageManager,
+ identity,
+ diagnosticsTokenUrl: env.ORCA_DIAGNOSTICS_TOKEN_URL,
+ telemetryKeySha256: sha256(env.ORCA_POSTHOG_WRITE_KEY)
+ }
+}
+
+export function javascriptInventory(directory, prefix = '', { excludeHostOutput = false } = {}) {
+ return readdirSync(directory, { withFileTypes: true })
+ .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
+ .flatMap((entry) => {
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name
+ const hostOutput = ['relay', 'orcad-template', 'orcad-prebuilds'].includes(name.split('/')[0])
+ if (excludeHostOutput && hostOutput) {
+ return []
+ }
+ assert(!entry.isSymbolicLink(), `JavaScript artifact contains a symlink: ${name}`)
+ assert(!NATIVE_FILE.test(name), `JavaScript artifact contains a native binary: ${name}`)
+ assert(!hostOutput, `JavaScript artifact contains host output: ${name}`)
+ const file = join(directory, entry.name)
+ if (entry.isDirectory()) {
+ return javascriptInventory(file, name, { excludeHostOutput })
+ }
+ assert(entry.isFile(), `JavaScript artifact contains a special file: ${name}`)
+ const bytes = readFileSync(file)
+ return [{ path: name, bytes: bytes.length, sha256: sha256(bytes) }]
+ })
+}
+
+function verifyRequiredFiles(files) {
+ const present = new Set(files.map((file) => file.path))
+ for (const file of REQUIRED_FILES) {
+ assert(present.has(file), `Missing JavaScript output: ${file}`)
+ }
+}
+
+export function packReleaseJavascript({ root = process.cwd(), artifactDir, configuration }) {
+ configuration ??= releaseJavascriptConfiguration(root)
+ const out = join(root, 'out')
+ const files = javascriptInventory(out)
+ verifyRequiredFiles(files)
+ mkdirSync(artifactDir, { recursive: true })
+ const archive = join(artifactDir, ARCHIVE_NAME)
+ command(getTarProgram(), ['-czf', archive, '-C', root, 'out'], root)
+ const manifest = {
+ schema: 1,
+ configuration,
+ archiveSha256: sha256(readFileSync(archive)),
+ archiveBytes: statSync(archive).size,
+ files
+ }
+ writeFileSync(join(artifactDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
+ return manifest
+}
+
+export function restoreReleaseJavascript({ root = process.cwd(), artifactDir, configuration }) {
+ configuration ??= releaseJavascriptConfiguration(root)
+ const manifest = JSON.parse(readFileSync(join(artifactDir, 'manifest.json'), 'utf8'))
+ assert.equal(manifest.schema, 1, 'Unsupported JavaScript artifact schema')
+ assert.deepEqual(manifest.configuration, configuration, 'JavaScript build configuration differs')
+ const archive = join(artifactDir, ARCHIVE_NAME)
+ assert.equal(sha256(readFileSync(archive)), manifest.archiveSha256, 'JavaScript archive differs')
+ const entries = command(getTarProgram(), ['-tzf', archive], root).split(/\r?\n/)
+ for (const entry of entries) {
+ assert(entry === 'out/' || entry.startsWith('out/'), `Unexpected archive entry: ${entry}`)
+ assert(
+ !entry.includes('\\') && !entry.split('/').includes('..'),
+ `Unsafe archive entry: ${entry}`
+ )
+ }
+ mkdirSync(join(root, '.build'), { recursive: true })
+ const temporary = mkdtempSync(join(root, '.build', 'release-javascript-'))
+ try {
+ command(getTarProgram(), ['-xzf', archive, '-C', temporary], root)
+ const staged = join(temporary, 'out')
+ const files = javascriptInventory(staged)
+ verifyRequiredFiles(files)
+ assert.deepEqual(files, manifest.files, 'JavaScript file inventory differs')
+ rmSync(join(root, 'out'), { recursive: true, force: true })
+ renameSync(staged, join(root, 'out'))
+ if (process.platform !== 'win32') {
+ chmodSync(join(root, 'out', 'cli', 'index.js'), 0o755)
+ }
+ } finally {
+ rmSync(temporary, { recursive: true, force: true })
+ }
+ return manifest
+}
+
+if (isDirectInvocation(import.meta.url, process.argv[1])) {
+ const [mode, artifact] = process.argv.slice(2)
+ if (mode === 'identity') {
+ console.log(releaseBuildIdentity(process.env.RELEASE_TAG))
+ } else {
+ const artifactDir = resolve(artifact ?? '.build/release-javascript')
+ const result =
+ mode === 'pack'
+ ? packReleaseJavascript({ artifactDir })
+ : mode === 'restore'
+ ? restoreReleaseJavascript({ artifactDir })
+ : assert.fail('Expected pack, restore, or identity')
+ console.log(
+ `[release-javascript] ${mode}: ${result.files.length} files, ${result.archiveBytes} archived bytes`
+ )
+ }
+}
diff --git a/config/scripts/release-javascript-artifact.test.mjs b/config/scripts/release-javascript-artifact.test.mjs
new file mode 100644
index 000000000000..420311b90f89
--- /dev/null
+++ b/config/scripts/release-javascript-artifact.test.mjs
@@ -0,0 +1,140 @@
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import {
+ javascriptInventory,
+ packReleaseJavascript,
+ releaseBuildIdentity,
+ restoreReleaseJavascript
+} from './release-javascript-artifact.mjs'
+
+const directories = []
+const configuration = {
+ sourceSha: 'a'.repeat(40),
+ version: '1.2.3-rc.4',
+ packageManager: 'pnpm@12.8.1',
+ identity: 'rc',
+ diagnosticsTokenUrl: 'https://example.test/diagnostics',
+ telemetryKeySha256: 'b'.repeat(64)
+}
+
+afterEach(() => {
+ for (const directory of directories.splice(0)) {
+ rmSync(directory, { recursive: true, force: true })
+ }
+})
+
+function fixture() {
+ const root = mkdtempSync(join(tmpdir(), 'orca-release-javascript-test-'))
+ directories.push(root)
+ for (const file of [
+ 'cli/index.js',
+ 'main/index.js',
+ 'preload/index.js',
+ 'renderer/index.html',
+ 'renderer/.vite/manifest.json',
+ 'web/web-index.html',
+ 'mobile-web/manifest.json',
+ 'package.json',
+ 'renderer/wasm/viewer.wasm',
+ 'main/index.js.map'
+ ]) {
+ const destination = join(root, 'out', file)
+ mkdirSync(dirname(destination), { recursive: true })
+ writeFileSync(destination, file === 'cli/index.js' ? '#!/usr/bin/env node\n' : file)
+ }
+ const artifactDir = join(root, '.build', 'artifact')
+ return { root, artifactDir, configuration }
+}
+
+describe('release JavaScript transfer', () => {
+ it('preserves hidden manifests, source maps and portable WASM while replacing stale outputs', () => {
+ const options = fixture()
+ const before = javascriptInventory(join(options.root, 'out'))
+ packReleaseJavascript(options)
+ writeFileSync(join(options.root, 'out', 'stale.js'), 'old')
+ restoreReleaseJavascript(options)
+ expect(javascriptInventory(join(options.root, 'out'))).toEqual(before)
+ })
+
+ it.each([
+ 'sourceSha',
+ 'version',
+ 'packageManager',
+ 'identity',
+ 'diagnosticsTokenUrl',
+ 'telemetryKeySha256'
+ ])('rejects a different %s before replacing the current build', (field) => {
+ const options = fixture()
+ packReleaseJavascript(options)
+ expect(() =>
+ restoreReleaseJavascript({
+ ...options,
+ configuration: { ...configuration, [field]: 'different' }
+ })
+ ).toThrow('JavaScript build configuration differs')
+ expect(readFileSync(join(options.root, 'out', 'main', 'index.js'), 'utf8')).toBe(
+ 'main/index.js'
+ )
+ })
+
+ it('rejects a damaged archive before replacing the current build', () => {
+ const options = fixture()
+ packReleaseJavascript(options)
+ writeFileSync(join(options.artifactDir, 'release-javascript.tar.gz'), 'damaged')
+ expect(() => restoreReleaseJavascript(options)).toThrow('JavaScript archive differs')
+ expect(readFileSync(join(options.root, 'out', 'main', 'index.js'), 'utf8')).toBe(
+ 'main/index.js'
+ )
+ })
+
+ it('rejects a file inventory that no longer describes the archive', () => {
+ const options = fixture()
+ const manifest = packReleaseJavascript(options)
+ manifest.files[0].sha256 = 'c'.repeat(64)
+ writeFileSync(join(options.artifactDir, 'manifest.json'), JSON.stringify(manifest))
+ expect(() => restoreReleaseJavascript(options)).toThrow('JavaScript file inventory differs')
+ })
+
+ it.each([
+ 'addon.node',
+ 'launcher.exe',
+ 'library.dll',
+ 'library.dylib',
+ 'library.so.1',
+ 'relay/relay.js'
+ ])('keeps %s out of the portable artifact', (file) => {
+ const options = fixture()
+ const destination = join(options.root, 'out', file)
+ mkdirSync(dirname(destination), { recursive: true })
+ writeFileSync(destination, 'host output')
+ expect(() => packReleaseJavascript(options)).toThrow(/native binary|host output/)
+ })
+
+ it.skipIf(process.platform === 'win32')(
+ 'rejects symlinks instead of following files outside the build',
+ () => {
+ const options = fixture()
+ symlinkSync(
+ join(options.root, 'out', 'main', 'index.js'),
+ join(options.root, 'out', 'linked.js')
+ )
+ expect(() => packReleaseJavascript(options)).toThrow('contains a symlink')
+ }
+ )
+
+ it('refuses incomplete build output', () => {
+ const options = fixture()
+ rmSync(join(options.root, 'out', 'preload', 'index.js'))
+ expect(() => packReleaseJavascript(options)).toThrow(
+ 'Missing JavaScript output: preload/index.js'
+ )
+ })
+
+ it('classifies stable and suffixed RC versions and rejects other tag families', () => {
+ expect(releaseBuildIdentity('v1.2.3')).toBe('stable')
+ expect(releaseBuildIdentity('v1.2.3-rc.4.perf')).toBe('rc')
+ expect(() => releaseBuildIdentity('mobile-v1.2.3')).toThrow('Invalid desktop release tag')
+ })
+})
diff --git a/config/scripts/release-javascript-benchmark.mjs b/config/scripts/release-javascript-benchmark.mjs
new file mode 100644
index 000000000000..6cbd6e1e63fd
--- /dev/null
+++ b/config/scripts/release-javascript-benchmark.mjs
@@ -0,0 +1,135 @@
+import assert from 'node:assert/strict'
+import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
+import { join, resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import {
+ javascriptInventory,
+ releaseJavascriptConfiguration,
+ restoreReleaseJavascript
+} from './release-javascript-artifact.mjs'
+import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
+import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
+import {
+ annotateJavascriptParityFiles,
+ compareJavascriptParityFiles
+} from './release-javascript-parity.mjs'
+
+const root = resolve(import.meta.dirname, '../..')
+const [mode, reportDir = '.build/release-javascript-measurements'] = process.argv.slice(2)
+
+function runBuild(script) {
+ const pnpm = resolvePnpmCliInvocation()
+ const started = performance.now()
+ const result = runProcessSync({
+ program: pnpm.command,
+ args: [...pnpm.prefixArgs, 'run', script],
+ cwd: root,
+ timeoutMs: 1_200_000,
+ maxOutputBytes: 64 * 1024 * 1024,
+ env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }
+ })
+ const milliseconds = performance.now() - started
+ process.stdout.write(result.stdout)
+ process.stderr.write(result.stderr)
+ assert.equal(result.code, 0, describeProcessFailure(result))
+ return milliseconds
+}
+
+function benchmark() {
+ assert(['baseline', 'shared'].includes(mode), 'Expected baseline, shared, or summary')
+ const configuration = releaseJavascriptConfiguration(root)
+ let restoreMs = 0
+ if (mode === 'shared') {
+ const started = performance.now()
+ restoreReleaseJavascript({
+ root,
+ artifactDir: join(root, '.build', 'release-javascript'),
+ configuration
+ })
+ restoreMs = performance.now() - started
+ }
+ const buildMs = runBuild(mode === 'baseline' ? 'build:release' : 'build:release:host')
+ const files = javascriptInventory(join(root, 'out'), '', { excludeHostOutput: true }).filter(
+ (file) => !file.path.endsWith('.map')
+ )
+ const report = {
+ mode,
+ platform: process.platform,
+ arch: process.arch,
+ configuration,
+ restoreMs,
+ buildMs,
+ files: annotateJavascriptParityFiles(join(root, 'out'), files)
+ }
+ mkdirSync(reportDir, { recursive: true })
+ writeFileSync(
+ join(reportDir, `${mode}-${process.platform}-${process.arch}.json`),
+ `${JSON.stringify(report)}\n`
+ )
+ console.log(
+ `[release-javascript] ${mode}: build ${(buildMs / 1000).toFixed(2)}s, restore ${(restoreMs / 1000).toFixed(2)}s`
+ )
+}
+
+function summarize() {
+ const reports = readdirSync(reportDir, { recursive: true })
+ .filter((file) => /(?:baseline|shared)-.*\.json$/.test(file))
+ .map((file) => JSON.parse(readFileSync(join(reportDir, file), 'utf8')))
+ const { jobs } = JSON.parse(readFileSync(join(reportDir, 'jobs.json'), 'utf8'))
+ const seconds = (step) => (Date.parse(step.completed_at) - Date.parse(step.started_at)) / 1000
+ const lines = [
+ '| Host | Baseline build | Shared host build | Restore | Download | Saved per host | Runtime file differences |',
+ '| --- | ---: | ---: | ---: | ---: | ---: | ---: |'
+ ]
+ let savedMs = 0
+ let differences = 0
+ for (const baseline of reports.filter((report) => report.mode === 'baseline')) {
+ const shared = reports.find(
+ (report) =>
+ report.mode === 'shared' &&
+ report.platform === baseline.platform &&
+ report.arch === baseline.arch
+ )
+ assert(shared, `Missing shared result for ${baseline.platform}/${baseline.arch}`)
+ assert.deepEqual(shared.configuration, baseline.configuration)
+ const job = jobs.find((job) => job.name === `shared ${baseline.platform} ${baseline.arch}`)
+ assert(job, 'Missing shared job timing')
+ const download = job.steps.find((step) => step.name === 'Download shared JavaScript')
+ assert(download?.conclusion === 'success', 'Missing successful artifact download')
+ const downloadMs = seconds(download) * 1000
+ const changed = compareJavascriptParityFiles(baseline.files, shared.files)
+ differences += changed.length
+ const saved = baseline.buildMs - shared.buildMs - shared.restoreMs - downloadMs
+ savedMs += saved
+ const format = (ms) => `${(ms / 1000).toFixed(1)}s`
+ lines.push(
+ `| ${baseline.platform}/${baseline.arch} | ${format(baseline.buildMs)} | ${format(shared.buildMs)} | ${format(shared.restoreMs)} | ${format(downloadMs)} | ${format(saved)} | ${changed.length} |`
+ )
+ if (changed.length) {
+ lines.push(
+ `\nDifferences for ${baseline.platform}/${baseline.arch}: ${changed.slice(0, 20).join(', ')}\n`
+ )
+ }
+ }
+ assert.equal(reports.length, 8, 'Expected both measurements on all four packaging hosts')
+ const producer = jobs.find((job) =>
+ job.steps.some((step) => step.name === 'Build and archive release JavaScript')
+ )
+ assert(producer, 'Missing producer job timing')
+ const producerSeconds = seconds(producer)
+ lines.push(
+ '',
+ `Shared producer job: ${producerSeconds.toFixed(1)}s including setup, compilation, archiving and upload.`,
+ `Net runner time saved across four hosts after charging the producer job: ${(savedMs / 1000 - producerSeconds).toFixed(1)}s.`,
+ 'Build measurements exclude consumer checkout/install and signing. Downloads and restoration are charged to shared builds. Runtime parity normalizes text line endings, asset references and manifest key order, excludes source maps, and permits native color rounding up to 0.00000101 in Display P3 or 0.00010001 in Lab. Other content must match. Artifact restoration always verifies exact producer hashes. The producer starts alongside release gates; any unfinished producer work still delays packaging.'
+ )
+ writeFileSync(join(reportDir, 'comparison.md'), `${lines.join('\n')}\n`)
+ console.log(lines.join('\n'))
+ assert.equal(differences, 0, 'Shared runtime output differs from per-host compilation')
+}
+
+if (mode === 'summary') {
+ summarize()
+} else {
+ benchmark()
+}
diff --git a/config/scripts/release-javascript-parity.mjs b/config/scripts/release-javascript-parity.mjs
new file mode 100644
index 000000000000..a3ff9577c3b0
--- /dev/null
+++ b/config/scripts/release-javascript-parity.mjs
@@ -0,0 +1,165 @@
+import assert from 'node:assert/strict'
+import { createHash } from 'node:crypto'
+import { readFileSync } from 'node:fs'
+import { basename, join } from 'node:path'
+
+const ASSET_PATH = /^(?:renderer|web)\/assets\/(.+)-[\w-]{8}\.(js|css|svg)$/
+const TEXT_FILE = /\.(?:js|css|html|json|svg)$/
+const NATIVE_COLOR = /color\(display-p3 ([^)]+)\)|(? name.replace(/[.*+?^$(){}|[\]\\]/g, '\\$&')).join('|') ||
+ '(?!)',
+ 'g'
+ )
+ return (content) => content.replace(references, (name) => replacements.get(name) ?? name)
+}
+
+export function normalizeManifestSourcePaths(value) {
+ if (typeof value === 'string') {
+ return value.replace(/node_modules\/\.pnpm\/[^/]+\/node_modules\//g, 'node_modules/')
+ }
+ if (Array.isArray(value)) {
+ return value.map(normalizeManifestSourcePaths)
+ }
+ if (!value || typeof value !== 'object') {
+ return value
+ }
+ const entries = Object.entries(value)
+ .map(([key, child]) => [normalizeManifestSourcePaths(key), normalizeManifestSourcePaths(child)])
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
+ assert.equal(
+ new Set(entries.map(([key]) => key)).size,
+ entries.length,
+ 'Ambiguous manifest source paths'
+ )
+ return Object.fromEntries(entries)
+}
+
+export function annotateJavascriptParityFiles(root, files) {
+ const names = new Map()
+ for (const file of files) {
+ const match = file.path.match(ASSET_PATH)
+ if (match) {
+ const stem = `${match[1]}.${match[2]}`
+ const entries = names.get(stem) ?? new Set()
+ entries.add(basename(file.path))
+ names.set(stem, entries)
+ }
+ }
+ const replacements = new Map(
+ [...names]
+ .filter(([, entries]) => entries.size === 1)
+ .map(([stem, entries]) => [[...entries][0], stem])
+ )
+ const contents = new Map(
+ files
+ .filter((file) => TEXT_FILE.test(file.path))
+ .map((file) => [
+ file.path,
+ readFileSync(join(root, file.path), 'utf8').replaceAll('\r\n', '\n')
+ ])
+ )
+ const normalizeIdentity = createAssetReferenceNormalizer(
+ new Map([...names].flatMap(([stem, entries]) => [...entries].map((name) => [name, stem])))
+ )
+ const ambiguous = []
+ for (const [stem, entries] of names) {
+ if (entries.size < 2) {
+ continue
+ }
+ for (const name of entries) {
+ const file = files.find((file) => ASSET_PATH.test(file.path) && basename(file.path) === name)
+ // Dependency hashes identify the module; final checks preserve every resolved reference.
+ const signature = createHash('sha256')
+ .update(normalizeIdentity(contents.get(file.path)))
+ .digest('hex')
+ const extension = stem.slice(stem.lastIndexOf('.'))
+ ambiguous.push([name, `${stem.slice(0, -extension.length)}-${signature}${extension}`])
+ }
+ }
+ const aliases = new Map([...replacements, ...ambiguous])
+ const normalizeReferences = createAssetReferenceNormalizer(aliases)
+ return files.map((file) => {
+ const name = basename(file.path)
+ const comparablePath = aliases.has(name)
+ ? file.path.slice(0, -name.length) + aliases.get(name)
+ : file.path
+ if (!contents.has(file.path)) {
+ return { ...file, comparablePath, comparableSha256: file.sha256 }
+ }
+ let content = normalizeReferences(contents.get(file.path))
+ const manifest = file.path === 'renderer/.vite/manifest.json'
+ if (manifest) {
+ content = JSON.stringify(normalizeManifestSourcePaths(JSON.parse(content)))
+ }
+ return {
+ ...file,
+ comparablePath,
+ comparableSha256: createHash('sha256').update(content).digest('hex'),
+ ...(file.path.endsWith('.css') ? { css: content } : {}),
+ ...(manifest ? { manifest: content } : {}),
+ ...(/(?:\/assets\/(?:App|Settings|ghostty|shell-icons)-.*\.js|\/[^/]+\.html)$/.test(file.path)
+ ? { text: content }
+ : {})
+ }
+ })
+}
+
+export function equivalentStylesheets(before, after) {
+ const beforeColors = [...before.matchAll(NATIVE_COLOR)]
+ const afterColors = [...after.matchAll(NATIVE_COLOR)]
+ const marker = (color) => (color.startsWith('lab(') ? 'NATIVE_LAB' : 'NATIVE_P3')
+ if (before.replace(NATIVE_COLOR, marker) !== after.replace(NATIVE_COLOR, marker)) {
+ return false
+ }
+ if (beforeColors.length !== afterColors.length) {
+ return false
+ }
+ return beforeColors.every((color, index) => {
+ const left = (color[1] ?? color[2]).split(/\s+/)
+ const right = (afterColors[index][1] ?? afterColors[index][2]).split(/\s+/)
+ const tolerance = color[1] ? 0.00000101 : 0.00010001
+ return (
+ left.length === right.length &&
+ left.every((value, channel) => {
+ if (value === right[channel]) {
+ return true
+ }
+ // Native CSS color conversion differs by one printed decimal unit across hosts.
+ return (
+ /^-?(?:\d*\.)?\d+%?$/.test(value) &&
+ /^-?(?:\d*\.)?\d+%?$/.test(right[channel]) &&
+ value.endsWith('%') === right[channel].endsWith('%') &&
+ Math.abs(Number.parseFloat(value) - Number.parseFloat(right[channel])) <= tolerance
+ )
+ })
+ )
+ })
+}
+
+export function compareJavascriptParityFiles(before, after) {
+ const reference = new Map(before.map((file) => [file.comparablePath, file]))
+ const candidate = new Map(after.map((file) => [file.comparablePath, file]))
+ assert.equal(reference.size, before.length, 'Ambiguous baseline output paths')
+ assert.equal(candidate.size, after.length, 'Ambiguous shared output paths')
+ const changed = [...new Set([...reference.keys(), ...candidate.keys()])].filter((path) => {
+ const left = reference.get(path)
+ const right = candidate.get(path)
+ if (!left || !right) {
+ return true
+ }
+ if (left.comparableSha256 === right.comparableSha256) {
+ return false
+ }
+ if (left.manifest && right.manifest) {
+ return (
+ JSON.stringify(normalizeManifestSourcePaths(JSON.parse(left.manifest))) !==
+ JSON.stringify(normalizeManifestSourcePaths(JSON.parse(right.manifest)))
+ )
+ }
+ return !left.css || !right.css || !equivalentStylesheets(left.css, right.css)
+ })
+ return changed
+}
diff --git a/config/scripts/release-javascript-parity.test.mjs b/config/scripts/release-javascript-parity.test.mjs
new file mode 100644
index 000000000000..5a4c62a4ac6a
--- /dev/null
+++ b/config/scripts/release-javascript-parity.test.mjs
@@ -0,0 +1,208 @@
+import { createHash } from 'node:crypto'
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { afterEach, expect, it } from 'vitest'
+import {
+ annotateJavascriptParityFiles,
+ compareJavascriptParityFiles,
+ equivalentStylesheets,
+ normalizeManifestSourcePaths
+} from './release-javascript-parity.mjs'
+
+const directories = []
+
+it('normalizes pnpm store path shortening without collapsing distinct source records', () => {
+ const shortened = '../../node_modules/.pnpm/package@1_hash/node_modules/package/index.js'
+ const full =
+ '../../node_modules/.pnpm/package@1.0.0_patch_hash=long/node_modules/package/index.js'
+ expect(
+ normalizeManifestSourcePaths({ [shortened]: { src: shortened, imports: [shortened] } })
+ ).toEqual(normalizeManifestSourcePaths({ [full]: { src: full, imports: [full] } }))
+ expect(() => normalizeManifestSourcePaths({ [shortened]: 1, [full]: 2 })).toThrow(
+ 'Ambiguous manifest source paths'
+ )
+})
+afterEach(() =>
+ directories.splice(0).forEach((directory) => rmSync(directory, { recursive: true, force: true }))
+)
+
+function inventory(entries) {
+ const root = mkdtempSync(join(tmpdir(), 'orca-release-parity-'))
+ directories.push(root)
+ const files = Object.entries(entries).map(([path, content]) => {
+ mkdirSync(dirname(join(root, path)), { recursive: true })
+ writeFileSync(join(root, path), content)
+ return { path, sha256: createHash('sha256').update(content).digest('hex') }
+ })
+ return annotateJavascriptParityFiles(root, files)
+}
+
+it('allows one decimal unit of native P3 rounding and its asset reference hashes', () => {
+ const before = inventory({
+ 'renderer/assets/theme-AAAAAAAA.css': '.x{color:color(display-p3 .134023 .230646 .695537)}',
+ 'renderer/assets/index-BBBBBBBB.js': 'import "./theme-AAAAAAAA.css";run()',
+ 'renderer/index.html': ''
+ })
+ const after = inventory({
+ 'renderer/assets/theme-CCCCCCCC.css': '.x{color:color(display-p3 .134023 .230647 .695537)}',
+ 'renderer/assets/index-DDDDDDDD.js': 'import "./theme-CCCCCCCC.css";run()',
+ 'renderer/index.html': ''
+ })
+ expect(compareJavascriptParityFiles(before, after)).toEqual([])
+})
+
+it('rejects meaningful color changes and every non-color stylesheet change', () => {
+ expect(
+ equivalentStylesheets(
+ 'a{color:color(display-p3 .1 .2 .3)}',
+ 'a{color:color(display-p3 .1 .20001 .3)}'
+ )
+ ).toBe(false)
+ expect(equivalentStylesheets('a{padding:1px}', 'a{padding:2px}')).toBe(false)
+ expect(equivalentStylesheets('a{color:red}', 'b{color:red}')).toBe(false)
+ expect(
+ equivalentStylesheets(
+ 'a{color:color(display-p3 .1 .2 .3)}',
+ 'a{color:color(display-p3 .1 .2 .3 / .5)}'
+ )
+ ).toBe(false)
+})
+
+it('rejects code changes, missing files and incorrect asset references', () => {
+ const before = inventory({ 'renderer/assets/index-AAAAAAAA.js': 'run()' })
+ const after = inventory({ 'renderer/assets/index-BBBBBBBB.js': 'other()' })
+ expect(compareJavascriptParityFiles(before, after)).toEqual(['renderer/assets/index.js'])
+ expect(compareJavascriptParityFiles(before, [])).toEqual(['renderer/assets/index.js'])
+ const reference = inventory({
+ 'renderer/assets/index-BBBBBBBB.js': 'run()',
+ 'renderer/index.html': ''
+ })
+ expect(
+ compareJavascriptParityFiles(
+ reference,
+ inventory({
+ 'renderer/assets/index-DDDDDDDD.js': 'run()',
+ 'renderer/index.html': ''
+ })
+ )
+ ).toEqual(['renderer/index.html'])
+})
+
+it('keeps ambiguous modules distinct and portable binary files exact', () => {
+ const before = inventory({
+ 'renderer/assets/App-AAAAAAAA.js': 'run()',
+ 'renderer/assets/App-BBBBBBBB.js': 'other()',
+ 'renderer/viewer.wasm': 'one'
+ })
+ expect(new Set(before.map((file) => file.comparablePath)).size).toBe(before.length)
+ expect(
+ compareJavascriptParityFiles(
+ before,
+ inventory({
+ 'renderer/assets/App-AAAAAAAA.js': 'run()',
+ 'renderer/assets/App-BBBBBBBB.js': 'other()',
+ 'renderer/viewer.wasm': 'two'
+ })
+ )
+ ).toEqual(['renderer/viewer.wasm'])
+})
+
+it('matches duplicate asset stems by content while rejecting a reference to the wrong module', () => {
+ const before = inventory({
+ 'renderer/assets/App-AAAAAAAA.js': 'desktop()',
+ 'renderer/assets/App-BBBBBBBB.js': 'web()',
+ 'renderer/index.html': ''
+ })
+ const after = inventory({
+ 'renderer/assets/App-CCCCCCCC.js': 'web()',
+ 'renderer/assets/App-DDDDDDDD.js': 'desktop()',
+ 'renderer/index.html': ''
+ })
+ expect(compareJavascriptParityFiles(before, after)).toEqual([])
+ const wrong = inventory({
+ 'renderer/assets/App-CCCCCCCC.js': 'web()',
+ 'renderer/assets/App-DDDDDDDD.js': 'desktop()',
+ 'renderer/index.html': ''
+ })
+ expect(compareJavascriptParityFiles(before, wrong)).toEqual(['renderer/index.html'])
+})
+
+it('preserves module identity across chained and cyclic dependencies with the same stem', () => {
+ const before = inventory({
+ 'renderer/assets/App-AAAAAAAA.js': 'desktop();import "./App-BBBBBBBB.js"',
+ 'renderer/assets/App-BBBBBBBB.js': 'web();import "./App-AAAAAAAA.js"',
+ 'renderer/index.html': ''
+ })
+ const after = inventory({
+ 'renderer/assets/App-CCCCCCCC.js': 'web();import "./App-DDDDDDDD.js"',
+ 'renderer/assets/App-DDDDDDDD.js': 'desktop();import "./App-CCCCCCCC.js"',
+ 'renderer/index.html': ''
+ })
+ expect(compareJavascriptParityFiles(before, after)).toEqual([])
+ const changed = inventory({
+ 'renderer/assets/App-CCCCCCCC.js': 'web();import "./App-DDDDDDDD.js"',
+ 'renderer/assets/App-DDDDDDDD.js': 'desktop();import "./App-DDDDDDDD.js"',
+ 'renderer/index.html': ''
+ })
+ expect(compareJavascriptParityFiles(before, changed)).not.toEqual([])
+})
+
+it('normalizes generated text and SVG line endings without changing escaped string values', () => {
+ const before = inventory({
+ 'renderer/assets/icon-AAAAAAAA.svg': '',
+ 'renderer/assets/index-BBBBBBBB.js': 'import "./icon-AAAAAAAA.svg";\r\nrun()'
+ })
+ const after = inventory({
+ 'renderer/assets/icon-CCCCCCCC.svg': '',
+ 'renderer/assets/index-DDDDDDDD.js': 'import "./icon-CCCCCCCC.svg";\nrun()'
+ })
+ expect(compareJavascriptParityFiles(before, after)).toEqual([])
+ expect(
+ compareJavascriptParityFiles(
+ inventory({ 'shared/template.js': 'const text = "\\r\\n"' }),
+ inventory({ 'shared/template.js': 'const text = "\\n"' })
+ )
+ ).toEqual(['shared/template.js'])
+})
+
+it('permits one printed decimal unit of native Lab rounding and rejects larger or unit changes', () => {
+ expect(
+ equivalentStylesheets(
+ 'a{color:lab(76.5514% 36.4219 15.5335)}',
+ 'a{color:lab(76.5514% 36.422 15.5335)}'
+ )
+ ).toBe(true)
+ expect(
+ equivalentStylesheets(
+ 'a{color:lab(76.5514% 36.4219 15.5335)}',
+ 'a{color:lab(76.5514% 36.4221 15.5335)}'
+ )
+ ).toBe(false)
+ expect(
+ equivalentStylesheets(
+ 'a{color:lab(76.5514% 36.4219 15.5335)}',
+ 'a{color:lab(76.5514 36.4219 15.5335)}'
+ )
+ ).toBe(false)
+})
+
+it('compares manifest keys without ordering differences while preserving import array order', () => {
+ const before = inventory({
+ 'renderer/assets/index-AAAAAAAA.js': 'run()',
+ 'renderer/.vite/manifest.json':
+ '{"_index-AAAAAAAA.js":{"imports":["a","b"],"file":"assets/index-AAAAAAAA.js"},"entry":1}'
+ })
+ const after = inventory({
+ 'renderer/assets/index-BBBBBBBB.js': 'run()',
+ 'renderer/.vite/manifest.json':
+ '{"entry":1,"_index-BBBBBBBB.js":{"file":"assets/index-BBBBBBBB.js","imports":["a","b"]}}'
+ })
+ expect(compareJavascriptParityFiles(before, after)).toEqual([])
+ const changed = inventory({
+ 'renderer/assets/index-BBBBBBBB.js': 'run()',
+ 'renderer/.vite/manifest.json':
+ '{"entry":1,"_index-BBBBBBBB.js":{"file":"assets/index-BBBBBBBB.js","imports":["b","a"]}}'
+ })
+ expect(compareJavascriptParityFiles(before, changed)).toEqual(['renderer/.vite/manifest.json'])
+})
diff --git a/config/scripts/release-javascript-workflow.test.mjs b/config/scripts/release-javascript-workflow.test.mjs
new file mode 100644
index 000000000000..720c8f3d7cd0
--- /dev/null
+++ b/config/scripts/release-javascript-workflow.test.mjs
@@ -0,0 +1,91 @@
+import { readFileSync } from 'node:fs'
+import { parse } from 'yaml'
+import { describe, expect, it } from 'vitest'
+
+const release = parse(readFileSync('.github/workflows/release-cut.yml', 'utf8'))
+const mac = parse(readFileSync('.github/workflows/release-mac-build.yml', 'utf8'))
+const javascript = parse(readFileSync('.github/workflows/release-javascript.yml', 'utf8'))
+const comparison = parse(readFileSync('.github/workflows/release-javascript-benchmark.yml', 'utf8'))
+
+describe('release JavaScript job boundaries', () => {
+ it('compiles the release tag while blocking gates run, with no publishing permission', () => {
+ const job = release.jobs['release-javascript']
+ expect(job.needs).toBe('cut')
+ expect(job.permissions).toEqual({ contents: 'read' })
+ expect(job.with.ref).toBe('refs/tags/${{ needs.cut.outputs.tag }}')
+ expect(javascript.permissions).toEqual({ contents: 'read' })
+ expect(javascript.jobs.bundle['runs-on']).toBe('ubuntu-latest')
+ expect(javascript.jobs.bundle.steps[0].with.ref).toBe('${{ inputs.ref }}')
+ expect(javascript.env.ORCA_BACKGROUND_LAUNCH).toBe('1')
+ })
+
+ it.each(['build', 'build-mac'])(
+ 'keeps %s behind signing gates and the bundle verdict',
+ (name) => {
+ const job = release.jobs[name]
+ expect(job.needs).toEqual(
+ expect.arrayContaining([
+ 'cut',
+ 'create-release',
+ 'release-preflight',
+ 'relay-windows-process-tree',
+ 'release-javascript'
+ ])
+ )
+ expect(job.if).toContain("needs.release-preflight.result == 'success'")
+ expect(job.if).toContain("needs.release-javascript.result == 'success'")
+ }
+ )
+
+ it('retains full compilation for old refs but fails rather than rebuilding a broken shared artifact', () => {
+ const job = javascript.jobs.bundle
+ const support = job.steps.find((step) => step.id === 'source')
+ expect(support.run).toContain('build:release:javascript')
+ expect(support.run).toContain('build:release:host')
+ const compile = job.steps.find((step) => step.name === 'Build and archive release JavaScript')
+ expect(compile.if).toBe("steps.source.outputs.supported == 'true'")
+ const download = release.jobs.build.steps.find(
+ (step) => step.name === 'Download release JavaScript'
+ )
+ expect(download.if).toBe("needs.release-javascript.outputs.supported == 'true'")
+ expect(download['continue-on-error']).toBeUndefined()
+ const build = release.jobs.build.steps.find((step) => step.name === 'Build app')
+ expect(build.run).toMatch(/artifact\.mjs restore\s+pnpm run build:release:host/)
+ expect(build.run).toMatch(/else\s+pnpm run build:release/)
+ expect(build['continue-on-error']).toBeUndefined()
+ })
+
+ it('restores the same parent-run bundle on macOS and retains native and runtime checks', () => {
+ const download = mac.jobs['build-mac'].steps.find(
+ (step) => step.name === 'Download release JavaScript from the release run'
+ )
+ expect(download.with['run-id']).toBe('${{ inputs.release_run_id }}')
+ expect(download.with.name).toBe('release-javascript')
+ const build = mac.jobs['build-mac'].steps.find((step) => step.name === 'Build app')
+ expect(build.env.ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA).toBe('${{ inputs.javascript_source_sha }}')
+ expect(build.run).toContain('pnpm run build:release:host')
+ for (const job of [release.jobs.build, mac.jobs['build-mac']]) {
+ expect(job.steps.map((step) => step.name)).toEqual(
+ expect.arrayContaining([
+ 'Gate runtime file-watcher process isolation',
+ 'Gate SSH relay watcher process isolation'
+ ])
+ )
+ }
+ })
+
+ it('compares both build paths on all packaging hosts without publishing credentials', () => {
+ expect(comparison.jobs.measure.strategy.matrix.mode).toEqual(['baseline', 'shared'])
+ expect(comparison.jobs.measure.strategy.matrix.host).toHaveLength(4)
+ expect(comparison.permissions).toEqual({ contents: 'read' })
+ expect(comparison.env.ORCA_POSTHOG_WRITE_KEY).toBe('ci-build-comparison')
+ expect(comparison.jobs.bundle.secrets.ORCA_POSTHOG_WRITE_KEY).toBe('ci-build-comparison')
+ const steps = comparison.jobs.measure.steps
+ expect(steps.find((step) => step.name === 'Download shared JavaScript').if).toBe(
+ "matrix.mode == 'shared'"
+ )
+ expect(steps.find((step) => step.name === 'Measure release build').run).toContain(
+ 'release-javascript-benchmark.mjs'
+ )
+ })
+})
diff --git a/config/scripts/release-mac-build-workflow-dispatch.test.mjs b/config/scripts/release-mac-build-workflow-dispatch.test.mjs
index 62e6b66c933c..00a8c21a592b 100644
--- a/config/scripts/release-mac-build-workflow-dispatch.test.mjs
+++ b/config/scripts/release-mac-build-workflow-dispatch.test.mjs
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import {
+ dispatchReleaseMacBuildWorkflow,
expectedReleaseMacBuildRunTitle,
readReleaseMacBuildWorkflowOptions,
runReleaseMacBuildWorkflow
@@ -19,6 +20,19 @@ const baseOptions = {
}
describe('release mac build workflow dispatch', () => {
+ it('passes the shared JavaScript commit only when the parent produced a bundle', async () => {
+ const request = vi.fn(async () => ({}))
+ const api = { owner: 'stablyai', repo: 'orca', request }
+ await dispatchReleaseMacBuildWorkflow(api, {
+ ...baseOptions,
+ javascriptSourceSha: 'a'.repeat(40)
+ })
+ expect(request.mock.calls[0][2].inputs).toEqual({
+ release_run_id: '777',
+ tag: 'v1.2.3-rc.4',
+ javascript_source_sha: 'a'.repeat(40)
+ })
+ })
it('dispatches the mac workflow and waits for the returned run id', async () => {
const { fetch, requests } = createGitHubFetch([
jsonResponse(200, {
diff --git a/config/scripts/run-node-server-tests.mjs b/config/scripts/run-node-server-tests.mjs
index 2927f77277a2..1a9356bc0f39 100644
--- a/config/scripts/run-node-server-tests.mjs
+++ b/config/scripts/run-node-server-tests.mjs
@@ -86,6 +86,9 @@ run(runtimePath, [
function defaultTestArgs() {
return [
...nodeServerTestPaths({ artifact, crossRuntime }),
+ // Electron probes run in desktop jobs; headless compatibility containers have no display.
+ '--exclude',
+ '**/*.electron.test.ts',
// A directory selector would otherwise pull them into lanes that lack their inputs.
...(crossRuntime ? [] : CROSS_RUNTIME_TEST_PATHS.flatMap((path) => ['--exclude', path]))
]
diff --git a/config/scripts/run-release-mac-build-workflow.mjs b/config/scripts/run-release-mac-build-workflow.mjs
index e74564965b3d..058647b1d930 100644
--- a/config/scripts/run-release-mac-build-workflow.mjs
+++ b/config/scripts/run-release-mac-build-workflow.mjs
@@ -8,6 +8,7 @@ export function readReleaseMacBuildWorkflowOptions(env = process.env) {
apiBaseUrl: env.GITHUB_API_URL ?? 'https://api.github.com',
pollSeconds: readPositiveInteger(env.RELEASE_MAC_BUILD_POLL_SECONDS, DEFAULT_POLL_SECONDS),
ref: requiredEnv(env.RELEASE_MAC_BUILD_REF, 'RELEASE_MAC_BUILD_REF'),
+ javascriptSourceSha: env.RELEASE_MAC_BUILD_JAVASCRIPT_SOURCE_SHA || undefined,
releaseRunId: requiredEnv(
env.RELEASE_MAC_BUILD_RELEASE_RUN_ID ?? env.GITHUB_RUN_ID,
'RELEASE_MAC_BUILD_RELEASE_RUN_ID'
@@ -66,7 +67,8 @@ export async function dispatchReleaseMacBuildWorkflow(api, options) {
const body = {
inputs: {
release_run_id: options.releaseRunId,
- tag: options.tag
+ tag: options.tag,
+ ...(options.javascriptSourceSha ? { javascript_source_sha: options.javascriptSourceSha } : {})
},
ref: options.ref
}
diff --git a/config/scripts/run-terminal-layout-parity.mjs b/config/scripts/run-terminal-layout-parity.mjs
new file mode 100644
index 000000000000..c1d5033d75df
--- /dev/null
+++ b/config/scripts/run-terminal-layout-parity.mjs
@@ -0,0 +1,229 @@
+#!/usr/bin/env node
+// Runs tests/e2e/terminal-layout-parity.spec.ts against a base and a head build and diffs the
+// captured layouts and saved sessions. Usage: tests/e2e/AGENTS.md.
+
+import { build } from 'esbuild'
+import {
+ existsSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ readdirSync,
+ rmSync,
+ writeFileSync
+} from 'node:fs'
+import { tmpdir } from 'node:os'
+import { basename, join, resolve } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { parseArgs } from 'node:util'
+import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
+const repoRoot = resolve(import.meta.dirname, '../..')
+const BUILD_STAMP = join('out', '.terminal-layout-parity-build')
+
+// Bundled like script-child-process.mjs: importing the .ts directly makes Node warn MODULE_TYPELESS_PACKAGE_JSON.
+async function importParityModules() {
+ const temporary = mkdtempSync(join(tmpdir(), 'orca-layout-parity-'))
+ try {
+ const outfile = join(temporary, 'parity.mjs')
+ await build({
+ stdin: {
+ contents: ['snapshot', 'declared-differences']
+ .map(
+ (name) =>
+ `export * from ${JSON.stringify(join(repoRoot, 'tests', 'e2e', `terminal-layout-parity-${name}.ts`))}`
+ )
+ .join('\n'),
+ resolveDir: repoRoot,
+ sourcefile: 'terminal-layout-parity-entry.ts'
+ },
+ bundle: true,
+ platform: 'node',
+ format: 'esm',
+ outfile,
+ logLevel: 'silent'
+ })
+ return await import(pathToFileURL(outfile).href)
+ } finally {
+ rmSync(temporary, { recursive: true, force: true })
+ }
+}
+
+const {
+ TERMINAL_LAYOUT_PARITY_OUT_ENV,
+ TERMINAL_LAYOUT_PARITY_DECLARED_DIFFERENCES,
+ TERMINAL_LAYOUT_PARITY_UNSTABLE_ON_MAIN,
+ compareParityCaptures,
+ isParityClean
+} = await importParityModules()
+
+// `pnpm run