merge: land the reviewed Linux packaging stack onto main's split updater

This commit is contained in:
Neil
2026-09-02 02:20:22 -07:00
3145 changed files with 336799 additions and 220377 deletions
+4 -8
View File
@@ -1,11 +1,7 @@
/config/scripts/create-draft-release.mjs text eol=lf
/config/scripts/orca-dev.mjs text eol=lf
/config/scripts/latest-stable-release.mjs text eol=lf
/config/scripts/publish-complete-draft-releases.mjs text eol=lf
/config/scripts/release-rc-history.mjs text eol=lf
/config/scripts/run-internal-dev-setup.mjs text eol=lf
/config/scripts/verify-cli-bin.mjs text eol=lf
/config/scripts/verify-release-required-assets.mjs text eol=lf
# A shebang plus CRLF makes vite's SSR transform emit a literal `#!` mid-module,
# so any suite importing the script dies at load with a SyntaxError. Pin the whole
# directory rather than the scripts that happen to have a test today.
/config/scripts/**/*.mjs text eol=lf
/skill-guides/*.md text eol=lf
/skill-stubs/*.md text eol=lf
/skills/*/SKILL.md text eol=lf
+27 -3
View File
@@ -401,27 +401,51 @@ jobs:
echo "::warning::Could not discard draft $TAG; remove it manually."
- name: Prune expired adhoc releases
# Only after a live publish: $TAG is then a non-draft this step must not
# delete, and a run that failed before publishing has nothing to retire.
if: steps.publish_live.outcome == 'success'
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
# Protect the tag this run just shipped, so no filter mistake can delete
# a build minutes after the person who cut it was told it exists.
TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
# Why compute the cutoff in bash rather than with jq's `now`: this runs
# once per dispatch, and a fixed epoch makes the threshold visible in the
# log when someone asks where their build went.
cutoff=$(( $(date -u +%s) - ADHOC_RETAIN_DAYS * 86400 ))
echo "Pruning adhoc releases created before $(date -u -r "$cutoff" '+%Y-%m-%dT%H:%M:%SZ')"
echo "Pruning adhoc releases published before $(date -u -r "$cutoff" '+%Y-%m-%dT%H:%M:%SZ')"
# --cleanup-tag so pruning does not leave orphan tags with no release or
# assets attached. Drafts are excluded: a stale draft is the failure
# path's business, not the retention window's.
stale="$(gh release list --repo "$ADHOC_REPO" --limit 200 --json tagName,createdAt,isDraft \
--jq "map(select(.isDraft | not)) | map(select((.createdAt | fromdateiso8601) < $cutoff)) | .[].tagName")"
#
# Age comes from publishedAt, never createdAt. GitHub reports createdAt
# as the date of the *commit* a release's tag points at, and every tag
# here is cut from this repo's one seed commit — so all of them carry
# that same createdAt, and the day the window rolled past it the entire
# channel expired at once and a single run deleted it. A release with no
# publishedAt is kept rather than aged by guesswork.
jq_filter='map(select(.isDraft | not))'
if [[ -n "${TAG:-}" ]]; then
jq_filter+=" | map(select(.tagName != \"${TAG//\"/\\\"}\"))"
fi
jq_filter+=" | map(select((.publishedAt // \"\") != \"\"))"
jq_filter+=" | map(select((.publishedAt | fromdateiso8601) < $cutoff)) | .[].tagName"
stale="$(gh release list --repo "$ADHOC_REPO" --limit 200 --json tagName,publishedAt,isDraft \
--jq "$jq_filter")"
if [[ -z "$stale" ]]; then
echo "Nothing to prune."
exit 0
fi
while read -r tag; do
[[ -n "$tag" ]] || continue
# Belt-and-suspenders: the filter above should already exclude $TAG.
if [[ -n "${TAG:-}" && "$tag" == "$TAG" ]]; then
echo "::warning::Prune list still included just-published $tag after protect; skipping delete."
continue
fi
echo "Pruning $tag"
gh release delete "$tag" --repo "$ADHOC_REPO" --yes --cleanup-tag || \
echo "::warning::Could not prune $tag"
+17
View File
@@ -14,7 +14,24 @@ permissions:
contents: read
jobs:
# A cold cache would otherwise make all eight Node 26 shards compile the same native addons.
test_native_cache:
name: prepare test native cache node 26
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
node-version: '26'
test:
needs: [test_native_cache]
uses: ./.github/workflows/unit-tests.yml
with:
node_versions: '["26"]'
+3
View File
@@ -131,6 +131,9 @@ jobs:
- name: Enforce max-lines ratchet
run: pnpm run check:max-lines-ratchet
- name: Enforce ts-nocheck ratchet
run: pnpm run check:ts-nocheck-ratchet
- name: Enforce runtime Electron-import ratchet
run: pnpm run check:runtime-electron-ratchet
+71 -27
View File
@@ -922,9 +922,7 @@ jobs:
run: |
$env:SKIP_BUILD = '1'
$env:ORCA_E2E_FORWARD_APP_LOGS = '1'
pnpm run --if-present test:e2e:workspace-session-golden
pnpm run --if-present test:e2e:windows-fresh-startup-golden
pnpm run --if-present test:e2e:source-control-golden
- name: Upload Playwright traces
if: failure()
@@ -940,6 +938,9 @@ jobs:
if: needs.cut.outputs.should_release == 'true'
name: skill sharing release gate ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
# The full suite is release-blocking on macOS. Windows still produces the
# same evidence, but intermittent filesystem contention cannot block signing.
continue-on-error: ${{ matrix.platform == 'windows' }}
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1122,10 +1123,33 @@ jobs:
retention-days: 7
if-no-files-found: ignore
# Why: artifact jobs submit Windows binaries to SignPath. Keep every
# quota-consuming build behind all blocking release gates so a late test
# failure cannot create signing requests that can never be published.
release-preflight:
needs:
- cut
- terminal-rendering-golden
- skill-sharing-release-gate
- skill-sharing-linux-floor-release-gate
if: >-
always() &&
needs.cut.outputs.should_release == 'true' &&
needs.terminal-rendering-golden.result == 'success' &&
needs.skill-sharing-release-gate.result == 'success' &&
needs.skill-sharing-linux-floor-release-gate.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Confirm blocking release gates passed
run: echo "All blocking release gates passed; artifact builds may start."
build:
needs:
- cut
- create-release
- release-preflight
if: needs.cut.outputs.should_release == 'true'
strategy:
fail-fast: false
@@ -1170,12 +1194,22 @@ jobs:
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
# GitHub reruns also resume jobs skipped behind a failed gate. Never
# recreate Windows signing requests on a rerun; reuse the assets from the
# original attempt and require a fresh dispatch if they are missing.
- name: Skip Windows artifact rebuild on rerun
if: matrix.platform == 'win' && github.run_attempt != 1
shell: bash
run: |
echo "Windows artifact/signing steps are disabled on reruns (attempt $GITHUB_RUN_ATTEMPT)."
echo "Existing signed release assets must be reused; dispatch a fresh release only when a rebuild is required." >> "$GITHUB_STEP_SUMMARY"
# Why: `uses: ./…` resolves from the checked-out tag, not from the workflow
# ref, so cutting from an older/off-main ref whose tree predates a composite
# action would fail the step with "Can't find 'action.yml'". Restore the
# actions directory from the commit this workflow file itself came from.
- name: Restore composite actions from the workflow ref
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: bash
env:
WORKFLOW_SHA: ${{ github.workflow_sha }}
@@ -1321,6 +1355,7 @@ jobs:
# otherwise undecodable. The main bundle is platform-independent, so one
# leg publishes the maps for the whole release.
- name: Bundle main-process source maps
id: bundle-main-sourcemaps
if: matrix.platform == 'linux-x64'
shell: bash
env:
@@ -1328,9 +1363,17 @@ jobs:
run: |
set -euo pipefail
if [ -z "$(find out/main -name '*.js.map' -print -quit)" ]; then
echo "::error::No main-process source maps in out/main. Did build.sourcemap regress in electron.vite.config.ts?"
exit 1
# Older cut tags predate the hidden-source-map build setting. They
# are valid legacy releases, but have no map bundle to publish.
if grep -Eq "sourcemap:[[:space:]]*['\"]hidden['\"]" electron.vite.config.ts; then
echo "::error::No main-process source maps in out/main despite build.sourcemap='hidden'."
exit 1
fi
echo "has_maps=false" >>"$GITHUB_OUTPUT"
echo "::notice::Cut ref predates hidden main-process source maps; skipping map publication."
exit 0
fi
echo "has_maps=true" >>"$GITHUB_OUTPUT"
# Why: every entry in electron-builder's `files` is a negation, so
# app-builder prepends `**/*` and packs anything left in the workspace
# root into app.asar. Stage the bundle outside the checkout instead.
@@ -1338,7 +1381,7 @@ jobs:
ls -l "$RUNNER_TEMP/orca-sourcemaps-$TAG.zip"
- name: Publish main-process source maps
if: matrix.platform == 'linux-x64'
if: matrix.platform == 'linux-x64' && steps.bundle-main-sourcemaps.outputs.has_maps == 'true'
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
@@ -1373,7 +1416,7 @@ jobs:
# Why: SignPath signs GitHub workflow artifacts, so Windows builds must
# upload only after the production-signed installer has been returned.
- name: Build Windows release artifacts
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
uses: nick-fields/retry@v4
with:
timeout_minutes: 30
@@ -1384,7 +1427,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify Windows node-pty ConPTY runtime
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
run: |
$runtimeDir = 'dist/win-unpacked/resources/node_modules/node-pty/build/Release'
@@ -1401,7 +1444,7 @@ jobs:
}
- name: Install SignPath PowerShell module
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
uses: ./.github/actions/install-signpath-module
# ── Windows inner-binary signing (issue #7785) ─────────────────────
@@ -1418,7 +1461,7 @@ jobs:
# valid signature (Microsoft's OpenConsole.exe) must keep their signer.
- name: Stage unsigned inner PE files for signing
id: stage-inner
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
continue-on-error: true
shell: pwsh
run: |
@@ -1457,7 +1500,7 @@ jobs:
- name: Upload unsigned inner binaries for SignPath
id: upload-unsigned-inner
if: matrix.platform == 'win' && steps.stage-inner.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.stage-inner.outcome == 'success'
continue-on-error: true
uses: actions/upload-artifact@v7
with:
@@ -1467,7 +1510,7 @@ jobs:
- name: Submit inner binaries signing request
id: submit-inner-signing
if: matrix.platform == 'win' && steps.upload-unsigned-inner.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.upload-unsigned-inner.outcome == 'success'
continue-on-error: true
uses: signpath/github-action-submit-signing-request@v2
with:
@@ -1481,7 +1524,7 @@ jobs:
- name: Notify Slack that inner-binary signing is waiting for approval
id: notify-inner-signing
if: matrix.platform == 'win' && steps.submit-inner-signing.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.submit-inner-signing.outcome == 'success'
continue-on-error: true
shell: pwsh
env:
@@ -1549,7 +1592,7 @@ jobs:
# falls through to today's unsigned-inner flow rather than blocking.
- name: Download signed inner binaries from SignPath
id: download-signed-inner
if: matrix.platform == 'win' && steps.submit-inner-signing.outcome == 'success' && steps.notify-inner-signing.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.submit-inner-signing.outcome == 'success' && steps.notify-inner-signing.outcome == 'success'
continue-on-error: true
shell: pwsh
env:
@@ -1572,7 +1615,7 @@ jobs:
# shipping a mix of signed and unsigned binaries.
- name: Restore signed inner binaries into unpacked app
id: restore-signed-inner
if: matrix.platform == 'win' && steps.download-signed-inner.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.download-signed-inner.outcome == 'success'
continue-on-error: true
shell: pwsh
run: |
@@ -1613,7 +1656,7 @@ jobs:
# unsigned again, which the evidence gate will flag.
- name: Replace cached elevate.exe with the signed copy
id: sign-elevate-cache
if: matrix.platform == 'win' && steps.restore-signed-inner.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success'
continue-on-error: true
shell: pwsh
run: |
@@ -1640,7 +1683,7 @@ jobs:
- name: Rebuild NSIS installer from signed unpacked app
id: rebuild-nsis-signed
if: matrix.platform == 'win' && steps.restore-signed-inner.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success'
continue-on-error: true
shell: pwsh
run: |
@@ -1657,7 +1700,7 @@ jobs:
}
- name: Roll back to original installer after failed rebuild
if: matrix.platform == 'win' && steps.rebuild-nsis-signed.outcome == 'failure'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.rebuild-nsis-signed.outcome == 'failure'
shell: pwsh
run: |
if (Test-Path 'prepack-backup/orca-windows-setup.exe') {
@@ -1667,7 +1710,7 @@ jobs:
}
# ── End Windows inner-binary signing ───────────────────────────────
- name: Upload unsigned Windows installer for SignPath
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
id: upload-unsigned-windows-installer
uses: actions/upload-artifact@v7
with:
@@ -1679,7 +1722,7 @@ jobs:
# so the release job waits while the signing request is approved in UI.
- name: Submit Windows installer signing request
id: submit-signing-request
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
@@ -1691,7 +1734,7 @@ jobs:
wait-for-completion: false
- name: Notify Slack that Windows signing is waiting for approval
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -1755,7 +1798,7 @@ jobs:
Invoke-RestMethod -Method Post -Uri $env:SLACK_WEBHOOK_URL -ContentType 'application/json' -Body $payload
- name: Download signed Windows installer from SignPath
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
@@ -1773,7 +1816,7 @@ jobs:
Expand-Archive -Path signed-windows.zip -DestinationPath signed-windows -Force
- name: Stage signed Windows release assets
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
run: |
$signedInstaller = Get-ChildItem -Path signed-windows -Recurse -File -Filter 'orca-windows-setup.exe' | Select-Object -First 1
@@ -1810,7 +1853,7 @@ jobs:
Get-Item 'dist/orca-windows-setup.exe', 'dist/orca-windows-setup.exe.blockmap', 'dist/latest.yml'
- name: Verify signed Windows installer
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
run: |
$signature = Get-AuthenticodeSignature -FilePath 'dist/orca-windows-setup.exe'
@@ -1828,7 +1871,7 @@ jobs:
# proven on a real release, then flip ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED
# to 'true' so unsigned inner binaries block the release.
- name: Verify Windows inner binary signatures
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
shell: pwsh
env:
ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED: 'false'
@@ -1960,7 +2003,7 @@ jobs:
if ($policyFailure) { throw $policyFailure }
- name: Upload Windows inner signing evidence
if: always() && matrix.platform == 'win'
if: always() && matrix.platform == 'win' && github.run_attempt == 1
uses: actions/upload-artifact@v7
with:
name: orca-windows-inner-signing-evidence-${{ needs.cut.outputs.tag }}
@@ -1971,7 +2014,7 @@ jobs:
retention-days: 30
- name: Publish signed Windows release artifacts
if: matrix.platform == 'win'
if: matrix.platform == 'win' && github.run_attempt == 1
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
@@ -2026,6 +2069,7 @@ jobs:
needs:
- cut
- create-release
- release-preflight
if: needs.cut.outputs.should_release == 'true'
# Why: SignPath requires every job in this signing workflow to be
# GitHub-hosted. The actual mac build runs in release-mac-build.yml so
+1
View File
@@ -110,6 +110,7 @@ docs/**
!docs/reference/macos-press-and-hold.md
!docs/reference/orcad-operations.md
!docs/reference/relay-grace-time-reconfiguration.md
!docs/reference/windows-edr-posture.md
!docs/reference/windows-process-enumeration.md
!docs/reference/wsl-runner-verification.md
!docs/reference/remote-wire-compatibility.md
+7
View File
@@ -49,6 +49,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
- **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line — never derive that from the user's terminal-shell preference, and never launch a `.cmd` runner with a bare `cmd.exe /c` from a Git Bash pane (MSYS rewrites the `/c`). See [`docs/reference/windows-setup-shell.md`](./docs/reference/windows-setup-shell.md).
- **Windows child processes**: start them through `runProcess`/`spawnProcess` in `src/shared/child-process/` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. A ratchet test fails on any new direct import.
- **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`. See [`docs/reference/windows-process-enumeration.md`](./docs/reference/windows-process-enumeration.md).
- **Windows EDR signal**: don't add `-ExecutionPolicy Bypass`, `-EncodedCommand`, `cmd.exe /c` with escaped free text, per-operation interpreter spawning, or runtime `Add-Type` compilation without reading [`docs/reference/windows-edr-posture.md`](./docs/reference/windows-edr-posture.md) first — behavioural EDR scores each of those, and being signed does not clear them.
- **WSL commands**: build argv with `buildWslExecArgs` (always `--exec` — under `--`, `wsl.exe` expands `$name` in every argument and silently rewrites the script), and fence anything whose stdout you parse with `buildWslCapturedLoginShellCommand`, because the interactive login shell prints the distro banner to stdout. See [`docs/reference/wsl-command-execution.md`](./docs/reference/wsl-command-execution.md).
- **Linux native modules**: keep the glibc floor at Ubuntu 20.04 / glibc 2.31. A module compiled from source on a newer runner can reference symbol versions absent on the floor and crash the app on startup. See [`docs/reference/linux-glibc-compatibility.md`](./docs/reference/linux-glibc-compatibility.md); packaging fails if a bundled native binary needs newer glibc.
@@ -76,6 +77,12 @@ When adding or changing a Git command:
- Keep the real-binary compatibility contract in PR CI current. When adopting a newer Git feature, add its version boundary so the preferred command and fallback both run against representative Git releases.
- Preserve commands that begin with global Git options such as `-c` before the subcommand, including auto-maintenance suppression used by worktree-create fetches.
## Git Scan Safety
- Never enumerate every ref and then run `git ls-tree -r` or `git show` once per ref. That ref × tree fan-out can retain gigabytes of output before a downstream `sort -u` or search can make progress.
- Prefer `rg` over the checked-out files for source searches. For history or refs, use a named ref, an explicit namespace/path, `--max-count`, and a bounded output; do not use an unqualified `--all` scan as a first diagnostic.
- Keep repository-wide commands targeted to the current repository and worktree. If an unbounded scan is genuinely required, measure the ref count first, explain the cost, and get confirmation before running it.
## Git Provider Compatibility
Source-control and review changes must consider GitLab and other supported git providers, not only GitHub. Keep provider-specific behavior behind explicit checks, and avoid GitHub-only naming for generic review concepts.
+3 -5
View File
@@ -36,7 +36,7 @@
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.47](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.47/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -230,7 +230,7 @@ yay -S stably-orca-bin
Pair with your desktop app to monitor and steer your agents from your phone.
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) or [join TestFlight](https://testflight.apple.com/join/YjeGMQBA)
- **Android:** [Download APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
- **Android:** [Download APK 0.0.47](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.47/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
---
@@ -238,9 +238,8 @@ Pair with your desktop app to monitor and steer your agents from your phone.
- **Discord:** Join the community on **[Discord](https://discord.gg/fzjDKHxv8Q)**.
- **Twitter / X:** Follow **[@orca_build](https://x.com/orca_build)** for updates and announcements.
- **WeChat:** Scan to join the Orca community WeChat group 7. If it is full, use group 8.
- **WeChat:** Scan to join the Orca community WeChat group 8.
<img src="docs/assets/wechat-qr-group7.jpg" alt="WeChat group 7 QR code for the Orca community" width="160" />&nbsp;&nbsp;
<img src="docs/assets/wechat-qr-group8.jpg" alt="WeChat group 8 QR code for the Orca community" width="160" />
- **Feedback &amp; Ideas:** We ship fast. Missing something? [Request a new feature](https://github.com/stablyai/orca/issues).
@@ -262,7 +261,6 @@ Want to contribute or run locally? See our [CONTRIBUTING.md](.github/CONTRIBUTIN
</p>
## Signed Builds
Windows code signing sponored/provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org).
## License
+27 -4
View File
@@ -129,6 +129,11 @@ const rpmElectronRuntimeDependencies = [
'(libuuid or libuuid1)'
]
// Why mirrored, not imported: this config is CJS loaded by electron-builder outside the TS build.
// Keep in sync with isMarkdownDocumentName() in src/main/ipc/markdown-documents.ts and with
// config/nsis/orca-installer-hooks.nsh, which registers the same set on Windows.
const MARKDOWN_FILE_EXTENSIONS = ['md', 'markdown', 'mdx']
/** @type {import('electron-builder').Configuration} */
module.exports = {
appId,
@@ -415,12 +420,24 @@ module.exports = {
shortcutName: '${productName}',
uninstallDisplayName: '${productName}',
createDesktopShortcut: 'always',
// Why: on a real uninstall, stop and remove the relocated terminal daemon
// (which lives outside the install dir under LOCALAPPDATA by design). Guarded
// by ${isUpdated} inside so it never runs during an update's uninstallOldVersion.
include: resolve(__dirname, 'nsis', 'daemon-host-uninstall.nsh')
// Why: electron-builder allows one include, so both Windows installer hooks live in it -
// the relocated-daemon uninstall sweep (guarded by ${isUpdated} so it never runs during an
// update's uninstallOldVersion) and the additive markdown "Open with" registration.
// Windows markdown association is deliberately NOT done via `fileAssociations`; see the
// header comment in that file for why that would steal the user's default .md handler.
include: resolve(__dirname, 'nsis', 'orca-installer-hooks.nsh')
},
mac: {
// Why rank Alternate: Orca joins Finder's "Open With" list for Markdown without claiming
// LSHandlerRank ownership, so whichever editor the user already prefers stays the default.
// Why one entry per extension: app-builder-lib globs `*.${ext}`, which an array would break.
fileAssociations: MARKDOWN_FILE_EXTENSIONS.map((ext) => ({
ext,
name: 'Markdown Document',
description: 'Markdown Document',
role: 'Editor',
rank: 'Alternate'
})),
icon: 'resources/build/icon.icns',
entitlements: 'resources/build/entitlements.mac.plist',
entitlementsInherit: 'resources/build/entitlements.mac.plist',
@@ -507,6 +524,12 @@ module.exports = {
artifactName: 'orca-macos-${arch}.${ext}'
},
linux: {
// Why mimeTypes and not fileAssociations: shared-mime-info already maps *.md/*.markdown to
// text/markdown, so reusing that type puts Orca in the Open With list without shipping a glob
// override. A desktop entry's MimeType only adds a handler - mimeapps.list still owns the
// default. .mdx is deliberately absent: Ubuntu 24.04's mime database maps it to
// application/x-genesis-32x-rom, so claiming it here would need a glob override.
mimeTypes: ['text/markdown'],
// Why: Ubuntu desktop ships GNOME Orca as the `orca` package and /usr/bin/orca.
// The Linux installer should not claim those system package/file names.
executableName: 'orca-ide',
-36
View File
@@ -2,51 +2,15 @@
# This is a RATCHET: the list may only SHRINK. Do NOT add entries to get CI green —
# split the oversized file instead (AGENTS.md → "Do Not Disable Max Lines").
# Regenerate/prune: pnpm check:max-lines-ratchet --prune (removes stale entries only)
inline src/main/agent-hooks/server.ts
inline src/main/browser/agent-browser-bridge.ts
inline src/main/browser/browser-cookie-import.ts
inline src/main/browser/browser-manager.ts
inline src/main/codex-accounts/runtime-home-service.ts
inline src/main/index.ts
inline src/main/ipc/filesystem.ts
inline src/main/ipc/worktree-remote.ts
inline src/main/rate-limits/service.ts
inline src/main/runtime/orca-runtime-browser.ts
inline src/main/runtime/orca-runtime-files.ts
inline src/main/runtime/orca-runtime.test.ts
inline src/main/runtime/orca-runtime.ts
inline src/main/runtime/rpc/methods/orchestration.ts
inline src/main/ssh/ssh-channel-multiplexer.ts
inline src/main/ssh/ssh-connection.ts
inline src/main/ssh/ssh-relay-deploy.ts
inline src/main/ssh/ssh-relay-session.ts
inline src/main/updater.ts
inline src/preload/index.ts
inline src/relay/pty-handler.ts
inline src/renderer/src/components/TaskPage.tsx
inline src/renderer/src/components/Terminal.tsx
inline src/renderer/src/components/WorktreeJumpPalette.tsx
inline src/renderer/src/components/automations/AutomationsPage.tsx
inline src/renderer/src/components/editor/MarkdownPreview.tsx
inline src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
inline src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx
inline src/renderer/src/components/status-bar/StatusBar.tsx
inline src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx
inline src/renderer/src/components/terminal-pane/TerminalPane.tsx
inline src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
inline src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
inline src/renderer/src/runtime/sync-runtime-graph.ts
inline src/renderer/src/runtime/web-session-tabs-sync.ts
inline src/renderer/src/store/slices/agent-status.ts
inline src/renderer/src/store/slices/browser.ts
inline src/renderer/src/store/slices/linear.ts
inline src/renderer/src/store/slices/tabs.ts
inline src/renderer/src/store/slices/ui.ts
inline src/shared/keybindings.ts
mobile-config app/h/*/files/*.tsx
mobile-config app/h/*/session/*.tsx
mobile-config app/h/*/source-control/*.tsx
mobile-config app/index.tsx
mobile-config scripts/mock-server.ts
mobile-config src/terminal/terminal-webview-html.ts
mobile-config src/transport/rpc-client.ts
-23
View File
@@ -1,23 +0,0 @@
; Clean up the relocated terminal daemon on a REAL uninstall.
;
; Why: the daemon host is deliberately copied to a distinct image name
; (orca-terminal-daemon.exe) under %LOCALAPPDATA%\Orca\daemon-host so that app
; UPDATES cannot kill it — that relocation is what keeps terminals alive across
; updates. The same design means a normal uninstall's process sweep and file
; removal both miss it, leaving an orphaned daemon plus its runtime copy behind.
;
; The ${isUpdated} guard is essential: electron-builder runs this uninstaller as
; part of uninstallOldVersion on EVERY update, and killing the daemon there would
; defeat the whole feature. Only clean up on a genuine uninstall.
;
; The image name and the LOCALAPPDATA folder name must stay in sync with
; DAEMON_HOST_EXE_NAME and LOCAL_HOST_ROOT_NAME in
; src/main/daemon/daemon-host-relocation.ts.
!macro customUnInstall
${ifNot} ${isUpdated}
nsExec::Exec 'taskkill /F /IM orca-terminal-daemon.exe'
; Give the OS a moment to release the image lock before removing the tree.
Sleep 500
RMDir /r "$LOCALAPPDATA\Orca\daemon-host"
${endIf}
!macroend
+79
View File
@@ -0,0 +1,79 @@
; electron-builder NSIS hooks for the Orca Windows installer.
;
; electron-builder accepts exactly ONE `nsis.include` file, so every customInstall /
; customUnInstall hook Orca needs lives here.
; ---------------------------------------------------------------------------
; Markdown "Open with Orca" (issue #10138)
;
; Why hand-rolled instead of electron-builder's `fileAssociations` on Windows:
; app-builder-lib emits !insertmacro APP_ASSOCIATE, whose first line is
; WriteRegStr SHELL_CONTEXT "Software\Classes\.md" "" "<ProgID>"
; That overwrites whichever editor currently owns .md, with no backup, for every
; existing user on their next UPDATE - and APP_UNASSOCIATE never restores it, so
; uninstalling Orca would leave .md pointing at a deleted ProgID.
;
; These writes are additive only. Registering a ProgID plus an OpenWithProgids
; hint and an Applications\<exe>\SupportedTypes entry puts Orca in Explorer's
; "Open with" list and in "Choose another app", while the default handler stays
; exactly where the user left it. Never add a `Software\Classes\.<ext>` default
; value here.
;
; MARKDOWN_PROGID must stay in sync with the extension list handled by
; isMarkdownDocumentName() in src/main/ipc/markdown-documents.ts.
; ---------------------------------------------------------------------------
!define MARKDOWN_PROGID "Orca.Markdown"
!macro ORCA_REGISTER_MARKDOWN_OPEN_WITH EXT
WriteRegNone SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${MARKDOWN_PROGID}"
WriteRegStr SHELL_CONTEXT "Software\Classes\Applications\${APP_EXECUTABLE_FILENAME}\SupportedTypes" "${EXT}" ""
!macroend
!macro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH EXT
DeleteRegValue SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${MARKDOWN_PROGID}"
DeleteRegValue SHELL_CONTEXT "Software\Classes\Applications\${APP_EXECUTABLE_FILENAME}\SupportedTypes" "${EXT}"
!macroend
!macro customInstall
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}" "" "Markdown Document"
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\DefaultIcon" "" "$appExe,0"
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\shell\open" "" "Open with ${PRODUCT_NAME}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\shell\open\command" "" '"$appExe" "%1"'
!insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".md"
!insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".markdown"
!insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".mdx"
; Why: Explorer caches the association list until told otherwise.
System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)"
!macroend
; ---------------------------------------------------------------------------
; Clean up the relocated terminal daemon on a REAL uninstall.
;
; Why: the daemon host is deliberately copied to a distinct image name
; (orca-terminal-daemon.exe) under %LOCALAPPDATA%\Orca\daemon-host so that app
; UPDATES cannot kill it — that relocation is what keeps terminals alive across
; updates. The same design means a normal uninstall's process sweep and file
; removal both miss it, leaving an orphaned daemon plus its runtime copy behind.
;
; The ${isUpdated} guard is essential: electron-builder runs this uninstaller as
; part of uninstallOldVersion on EVERY update, and killing the daemon there would
; defeat the whole feature. Only clean up on a genuine uninstall.
;
; The image name and the LOCALAPPDATA folder name must stay in sync with
; DAEMON_HOST_EXE_NAME and LOCAL_HOST_ROOT_NAME in
; src/main/daemon/daemon-host-relocation.ts.
!macro customUnInstall
${ifNot} ${isUpdated}
nsExec::Exec 'taskkill /F /IM orca-terminal-daemon.exe'
; Give the OS a moment to release the image lock before removing the tree.
Sleep 500
RMDir /r "$LOCALAPPDATA\Orca\daemon-host"
${endIf}
; Why outside the ${isUpdated} guard: customInstall rewrites these on every update, so
; dropping them during uninstallOldVersion is correct and keeps the pair symmetric.
DeleteRegKey SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}"
!insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".md"
!insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".markdown"
!insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".mdx"
System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)"
!macroend
+197
View File
@@ -789,3 +789,200 @@ index 7b286d3d644c26141df516929703aa6e129df4b2..ec6bf3932c65b89c013ff133dc6bf46a
return exports;
};
diff --git a/lib/windowsPtyAgent.js b/lib/windowsPtyAgent.js
index a358ffb..fb3a96f 100644
--- a/lib/windowsPtyAgent.js
+++ b/lib/windowsPtyAgent.js
@@ -136,6 +136,9 @@ var WindowsPtyAgent = /** @class */ (function () {
if (this._useConpty) {
if (!this._useConptyDll) {
this._inSocket.readable = false;
+ // The non-DLL path previously only flipped `readable`, leaving the
+ // conin PipeWrap alive until the host exited (#947).
+ this._inSocket.destroy();
this._outSocket.readable = false;
this._getConsoleProcessList().then(function (consoleProcessList) {
consoleProcessList.forEach(function (pid) {
diff --git a/lib/windowsTerminal.js b/lib/windowsTerminal.js
index 3c38f89..e20b3e6 100644
--- a/lib/windowsTerminal.js
+++ b/lib/windowsTerminal.js
@@ -50,6 +50,27 @@ var WindowsTerminal = /** @class */ (function (_super) {
// Create new termal.
_this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
_this._socket = _this._agent.outSocket;
+ // Attach before readiness so a broken ConPTY output pipe cannot be unhandled.
+ _this._socket.on('error', function (err) {
+ var code = err && err.code;
+ // PTY output can report EPIPE before `_close()` wins the race.
+ _this._close();
+ if (code === 'EPIPE' || code === 'ERR_STREAM_PUSH_AFTER_EOF' || code === 'ERR_STREAM_DESTROYED') {
+ return;
+ }
+ // EIO, happens when someone closes our child process: the only process
+ // in the terminal.
+ // node < 0.6.14: errno 5
+ // node >= 0.6.14: read EIO
+ if (typeof code === 'string') {
+ if (~code.indexOf('errno 5') || ~code.indexOf('EIO'))
+ return;
+ }
+ // Throw anything else.
+ if (_this.listeners('error').length < 2) {
+ throw err;
+ }
+ });
// Not available until `ready` event emitted.
_this._pid = _this._agent.innerPid;
_this._fd = _this._agent.fd;
@@ -76,23 +99,6 @@ var WindowsTerminal = /** @class */ (function (_super) {
_this._deferreds = [];
}
});
- // Shutdown if `error` event is emitted.
- _this._socket.on('error', function (err) {
- // Close terminal session.
- _this._close();
- // EIO, happens when someone closes our child process: the only process
- // in the terminal.
- // node < 0.6.14: errno 5
- // node >= 0.6.14: read EIO
- if (err.code) {
- if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO'))
- return;
- }
- // Throw anything else.
- if (_this.listeners('error').length < 2) {
- throw err;
- }
- });
// Cleanup after the socket is closed.
_this._socket.on('close', function () {
_this.emit('exit', _this._agent.exitCode);
@@ -103,6 +109,20 @@ var WindowsTerminal = /** @class */ (function (_super) {
_this._name = name;
_this._readable = true;
_this._writable = true;
+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes
+ // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon.
+ _this._agent.inSocket.on('error', function () {
+ if (!_this._writable) {
+ return;
+ }
+ _this._close();
+ try {
+ _this._agent.kill();
+ }
+ catch (_a) {
+ // The failing pipe may have raced process exit; the terminal is already unwritable.
+ }
+ });
_this._forwardEvents();
return _this;
}
@@ -196,4 +216,4 @@ var WindowsTerminal = /** @class */ (function (_super) {
return WindowsTerminal;
}(terminal_1.Terminal));
exports.WindowsTerminal = WindowsTerminal;
-//# sourceMappingURL=windowsTerminal.js.map
\ No newline at end of file
+//# sourceMappingURL=windowsTerminal.js.map
diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts
index d705444..ce611b8 100644
--- a/src/windowsPtyAgent.ts
+++ b/src/windowsPtyAgent.ts
@@ -143,6 +143,9 @@ export class WindowsPtyAgent {
if (this._useConpty) {
if (!this._useConptyDll) {
this._inSocket.readable = false;
+ // The non-DLL path previously only flipped `readable`, leaving the
+ // conin PipeWrap alive until the host exited (#947).
+ this._inSocket.destroy();
this._outSocket.readable = false;
this._getConsoleProcessList().then(consoleProcessList => {
consoleProcessList.forEach((pid: number) => {
diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts
index 13f6c6d..eda63c8 100644
--- a/src/windowsTerminal.ts
+++ b/src/windowsTerminal.ts
@@ -51,6 +51,30 @@ export class WindowsTerminal extends Terminal {
this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
this._socket = this._agent.outSocket;
-
+
+ // Attach before readiness so a broken ConPTY output pipe cannot be unhandled.
+ this._socket.on('error', err => {
+ const code = (<any>err).code;
+
+ // PTY output can report EPIPE before `_close()` wins the race.
+ this._close();
+ if (code === 'EPIPE' || code === 'ERR_STREAM_PUSH_AFTER_EOF' || code === 'ERR_STREAM_DESTROYED') {
+ return;
+ }
+
+ // EIO, happens when someone closes our child process: the only process
+ // in the terminal.
+ // node < 0.6.14: errno 5
+ // node >= 0.6.14: read EIO
+ if (typeof code === 'string') {
+ if (~code.indexOf('errno 5') || ~code.indexOf('EIO')) return;
+ }
+
+ // Throw anything else.
+ if (this.listeners('error').length < 2) {
+ throw err;
+ }
+ });
+
// Not available until `ready` event emitted.
this._pid = this._agent.innerPid;
this._fd = this._agent.fd;
@@ -82,25 +108,6 @@ export class WindowsTerminal extends Terminal {
}
});
-
+
- // Shutdown if `error` event is emitted.
- this._socket.on('error', err => {
- // Close terminal session.
- this._close();
-
- // EIO, happens when someone closes our child process: the only process
- // in the terminal.
- // node < 0.6.14: errno 5
- // node >= 0.6.14: read EIO
- if ((<any>err).code) {
- if (~(<any>err).code.indexOf('errno 5') || ~(<any>err).code.indexOf('EIO')) return;
- }
-
- // Throw anything else.
- if (this.listeners('error').length < 2) {
- throw err;
- }
- });
-
// Cleanup after the socket is closed.
this._socket.on('close', () => {
this.emit('exit', this._agent.exitCode);
@@ -114,6 +121,19 @@ export class WindowsTerminal extends Terminal {
-
+
this._readable = true;
this._writable = true;
+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes
+ // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon.
+ this._agent.inSocket.on('error', () => {
+ if (!this._writable) {
+ return;
+ }
+ this._close();
+ try {
+ this._agent.kill();
+ } catch {
+ // The failing pipe may have raced process exit; the terminal is already unwritable.
+ }
+ });
-
+
this._forwardEvents();
}
+608 -65
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"updatedAt": "2026-08-23",
"updatedAt": "2026-08-31",
"policy": {
"maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"],
"blockingPromotion": {
@@ -998,7 +998,7 @@
},
{
"id": "ssh-relay.staged-upload-recovery",
"title": "SSH relay uploads remain retryable before the shared install lock",
"title": "SSH relay uploads and install locks remain retryable",
"maturity": "experimental",
"protection": "partial",
"owner": "ssh-relay-install",
@@ -1007,19 +1007,21 @@
"SSH relay first install",
"split shell and SFTP namespaces",
"system SSH transfer fallback",
"relay install retry after cancellation"
"relay install retry after cancellation",
"post-promotion retry after execution-host restart"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["ssh2", "system-ssh"],
"coveredPlatforms": ["macos", "linux"],
"coveredProviders": ["ssh2", "system-ssh"],
"coverageNotes": "Deterministic unit, exact POSIX shell, native ARM macOS PowerShell 7.6.4, and real ssh2 SFTP-wire tests cover lock ordering, concurrent-install loss, fixed-slot ownership identity, payload-only promotion, bounded stale-stage reclamation, installed-fast-path draining, joined cancellation teardown, cross-version isolation, split-SFTP redirection, and system-SSH bypass. A throwaway linux-arm64 Docker sshd reached through a non-loopback LAN address covers live bytes-in-flight SFTP cancellation, injected unconfirmed cancellation, immediate retry against a real Git repository, fixed-slot recovery behind unclaimable entries, and real version-GC filtering with 15,197 unrelated names.",
"coverageNotes": "Deterministic unit, exact POSIX shell, native ARM macOS PowerShell 7.6.4, and real ssh2 SFTP-wire tests cover lock ordering, concurrent-install loss, fixed-slot ownership identity, payload-only promotion, bounded stale-stage reclamation, boot-identity takeover, installed-fast-path draining, joined cancellation teardown, cross-version isolation, split-SFTP redirection, and system-SSH bypass. A throwaway linux-arm64 Docker sshd reached through a non-loopback LAN address covers live bytes-in-flight SFTP cancellation, injected unconfirmed cancellation, immediate retry against a real Git repository, fixed-slot recovery behind unclaimable entries, and real version-GC filtering with 15,197 unrelated names.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/9828",
"https://github.com/stablyai/orca/pull/10207"
"https://github.com/stablyai/orca/pull/10207",
"https://github.com/stablyai/orca/issues/17144"
],
"invariant": "A first-install relay transfer must complete in an attempt-owned fixed staging slot before acquiring the shared version install lock. Reservation, promotion, confirmed cleanup, and stale recovery must reject path replacement, persisted-identity mismatch, POSIX symlinks, and Windows reparse points. Recovery examines only eight fixed slot/claim/delete names and removes at most one stale valid stage per call; eight unclaimable states fail with an explicit manual-recovery message. Split-SFTP hosts must prove the stage identity on the exact transfer session, only payload contents may be promoted under the shared lock, and cancellation must boundedly join SFTP, stream, local file-handle, and transfer settlement.",
"oracle": "Pause a real ssh2 SFTP relay.js write after one remotely acknowledged chunk, prove the remote file is partial, abort the live transfer, and require no shared .install-lock, leaked local descriptor, or foreign-process termination. Separately inject two unconfirmed cancellations, require an independent deployment to install, launch, answer relay RPC, and read a real repository HEAD. Replace one retained fixed slot with an old-mtime same-owner directory while preserving the original, add a fixed-slot POSIX symlink, and require installed-path recovery to skip both while reclaiming a valid stale slot behind them. Add 15,197 unrelated relay-shaped names and run the real version GC, requiring bounded stdout and no removal. Unit and wire contracts cover exact POSIX and native PowerShell 0/1/7/8/9+ quota behavior, no-follow identity fencing, payload symlink/reparse rejection, one-item repeated draining, zero lock acquisition before upload settlement, joined transfer/channel teardown including never-settling failures, SFTP redirection, package.json namespace ownership, promotion only after the lock, cross-version isolation, and system-SSH behavior.",
"invariant": "A first-install relay transfer must complete in an attempt-owned fixed staging slot before acquiring the shared version install lock. Reservation, promotion, confirmed cleanup, and stale recovery must reject path replacement, persisted-identity mismatch, POSIX symlinks, and Windows reparse points. A newly acquired install lock atomically records the execution host's boot identity; only a verified identity change or the existing stale-age proof may replace it, while legacy, missing, malformed, and unreadable identity state must retain the conservative stale fallback. Recovery examines only eight fixed slot/claim/delete names and removes at most one stale valid stage per call; eight unclaimable states fail with an explicit manual-recovery message. Split-SFTP hosts must prove the stage identity on the exact transfer session, only payload contents may be promoted under the shared lock, and cancellation must boundedly join SFTP, stream, local file-handle, and transfer settlement.",
"oracle": "Pause a real ssh2 SFTP relay.js write after one remotely acknowledged chunk, prove the remote file is partial, abort the live transfer, and require no shared .install-lock, leaked local descriptor, or foreign-process termination. Separately inject two unconfirmed cancellations, require an independent deployment to install, launch, answer relay RPC, and read a real repository HEAD. Keep a fresh install lock on the current POSIX or Windows boot and require takeover to fail; replace its bounded identity with a prior-boot value and race concurrent recoverers, requiring exactly one atomic winner, a current successor identity, and no tombstone residue; omit the marker and require the legacy lock to remain fenced. Replace one retained fixed slot with an old-mtime same-owner directory while preserving the original, add a fixed-slot POSIX symlink, and require installed-path recovery to skip both while reclaiming a valid stale slot behind them. Add 15,197 unrelated relay-shaped names and run the real version GC, requiring bounded stdout and no removal. Unit and wire contracts cover exact POSIX and native PowerShell 0/1/7/8/9+ quota behavior, no-follow identity fencing, payload symlink/reparse rejection, one-item repeated draining, zero lock acquisition before upload settlement, joined transfer/channel teardown including never-settling failures, SFTP redirection, package.json namespace ownership, promotion only after the lock, cross-version isolation, and system-SSH behavior.",
"commands": [
"node config/scripts/run-ssh-staged-upload-reliability.mjs --powershell <PowerShell-7.6.4-executable> src/main/ssh/sftp-upload.test.ts src/main/ssh/ssh-file-transfer-abort.test.ts src/main/ssh/ssh-relay-deploy-staged-upload.test.ts src/main/ssh/ssh-relay-native-deps-install-staged-upload.test.ts src/main/ssh/ssh-relay-sftp-namespace-install.test.ts src/main/ssh/ssh-relay-install-namespace.test.ts src/main/ssh/ssh-relay-upload-stage-commands.test.ts src/main/ssh/sftp-namespace-resolution.test.ts src/main/ssh/ssh-connection-sftp-wire.test.ts src/main/ssh/ssh-remote-commands.test.ts src/main/ssh/ssh-relay-cross-version-isolation.test.ts",
"ORCA_REVIEW_SSH_UPLOAD_CANCEL=1 ORCA_REVIEW_SSH_TARGET_HOST=<non-loopback-host> ORCA_REVIEW_SSH_IMAGE=<throwaway-sshd-image> ORCA_REVIEW_EXPECT_RECOVERY=1 pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-relay-upload-cancel.docker.test.ts --maxWorkers=1 --reporter=verbose"
@@ -1074,7 +1076,8 @@
"assertions": [
"uses encoded PowerShell for Windows deploy commands",
"enumerates Windows staging children before copying",
"lets only one PowerShell caller acquire a legacy-visible lock"
"lets only one PowerShell caller acquire a legacy-visible lock",
"keeps current and legacy fresh locks fenced while one concurrent caller replaces a previous-boot lock"
]
},
{
@@ -1091,6 +1094,7 @@
{
"file": "src/main/ssh/ssh-relay-upload-cancel.docker.test.ts",
"assertions": [
"replaces a fresh previous-boot install lock over live ssh2 while preserving promoted payload and leaving no tombstone",
"aborts a live SFTP upload after remote bytes arrive without creating the shared lock",
"recovers cancellation with bounded safe reclamation and bounded real version GC"
]
@@ -1130,7 +1134,7 @@
},
"performanceBudget": {
"required": true,
"evidence": "Stage recovery examines only eight fixed slot/claim/delete paths and reclaims at most one stale valid stage per invocation; installed reconnects launch before asynchronous recovery. Full quota produces an explicit error instead of unbounded cleanup. Version GC still scans the relay base directory, but remote filtering caps stdout and local candidate work at 64. Cancellation adds one bounded five-second join of channel and transfer settlement."
"evidence": "Stage recovery examines only eight fixed slot/claim/delete paths and reclaims at most one stale valid stage per invocation; installed reconnects launch before asynchronous recovery. Install-lock identity is recorded once per acquisition and checked only during the existing at-most-once-per-minute recovery probe; marker reads are capped at 128 bytes. Full quota produces an explicit error instead of unbounded cleanup. Version GC still scans the relay base directory, but remote filtering caps stdout and local candidate work at 64. Cancellation adds one bounded five-second join of channel and transfer settlement."
},
"promotionCriteria": [
"Collect 100 consecutive CI passes or 14 days of soak history.",
@@ -1140,6 +1144,7 @@
"knownGaps": [
"The live Docker target is Linux ARM64 with a unified namespace; split-SFTP behavior is covered by real ssh2 wire and deterministic deploy fixtures.",
"Native PowerShell coverage runs on ARM macOS with POSIX filesystem paths; Windows OpenSSH, Windows PowerShell 5.1, and system-SSH behavior remain command and transfer-contract coverage rather than a live target.",
"No live VM or WSL reboot is injected during native-dependency installation; deterministic host-native command tests provide the previous-boot, current-boot, legacy-marker, and concurrent-takeover oracle.",
"The fixed pool retains up to eight relay bundles; eight foreign or otherwise unclaimable fixed states require manual inspection instead of automatic deletion.",
"Version GC remotely filters and caps output but still scans the base .orca-remote directory; it does not promise constant remote enumeration time.",
"The Docker oracle is opt-in because it requires a local image and a reachable non-loopback host address."
@@ -2488,30 +2493,31 @@
"https://github.com/stablyai/orca/issues/11298",
"https://github.com/stablyai/orca/pull/11300"
],
"invariant": "Every accepted runtime socket installs message, pong, close, and error ownership before any heartbeat probe. With uninterrupted timer delivery, the first socket that arms an idle heartbeat is probed immediately and an unresponsive socket is reaped within one interval. Later sockets join the existing shared cadence without another timer or immediate sweep and are reaped within two intervals. Responsive sockets survive, pause recovery grants a fresh probe, and close or error-to-close releases connection listeners and timers.",
"oracle": "With one fake clock and exact socket identities, accept the first socket at 0 ms and require an immediate owned probe plus reaping at 100 ms when unresponsive. Keep a responsive first socket, accept an unresponsive later socket at 50 ms, require the same shared timer, its first probe at 100 ms, no early reap, and termination at 200 ms. Inject synchronous message, pong, close, and error events, then require exact heartbeat membership and zero retained timers/listeners after final close. Production transport tests independently cover real socket round trips, pre-auth and capacity bounds, revocation, shutdown, and half-open cleanup.",
"invariant": "Every accepted runtime socket installs message, pong, close, and error ownership before any heartbeat probe. Unauthenticated sockets receive no heartbeat control frames during E2EE and are bounded by the pre-auth timeout; authenticated sockets share one periodic cadence, tolerate missed probes and event-loop pause, and release listeners and timers on close or error.",
"oracle": "With one fake clock and exact socket identities, accept an authenticated first socket at 0 ms and require its first probe on the 100 ms tick. Accept an unauthenticated later socket at 50 ms and require no probe at the 100 ms tick; authenticate it, then require its first probe on the next shared tick and termination only after the configured consecutive-miss budget. Inject synchronous message, pong, close, and error events, then require exact heartbeat membership and zero retained timers/listeners after final close. Production transport tests independently cover real socket round trips, pre-auth and capacity bounds, revocation, shutdown, and half-open cleanup.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/ws-transport-accept-order.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/rpc/ws-transport.test.ts"
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/ws-transport-accept-order.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/rpc/ws-transport.test.ts src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts"
],
"testFiles": [
"src/main/runtime/rpc/ws-transport-accept-order.test.ts",
"src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts",
"src/main/runtime/rpc/ws-transport.test.ts"
"src/main/runtime/rpc/ws-transport.test.ts",
"src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts"
],
"assertionRefs": [
{
"file": "src/main/runtime/rpc/ws-transport-accept-order.test.ts",
"assertions": [
"the first synchronous probe observes message, pong, close, and error ownership",
"the first unresponsive socket is reaped at one interval",
"a later socket keeps the original shared timer, is first probed on the shared tick, and is reaped within two intervals",
"the first periodic probe observes message, pong, close, and error ownership",
"an authenticated unresponsive socket is reaped on the configured consecutive-miss budget",
"an unauthenticated later socket is not probed before authentication, then joins the original shared timer",
"final close releases heartbeat membership, listeners, and timers"
]
},
{
"file": "src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts",
"assertions": [
"one missed probe reaps only the unresponsive client",
"a single missed probe does not reap the unresponsive client",
"event-loop resume grants clients a fresh probe"
]
},
@@ -2522,6 +2528,12 @@
"pre-auth, raw TCP, and accepted WebSocket resource bounds remain enforced",
"error and close races finalize membership once"
]
},
{
"file": "src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts",
"assertions": [
"an authenticated real WebSocket survives one swallowed pong and responds to later probes"
]
}
],
"evidenceRuns": [
@@ -2529,10 +2541,10 @@
"date": "2026-07-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/ws-transport-accept-order.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/rpc/ws-transport.test.ts",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/ws-transport-accept-order.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/rpc/ws-transport.test.ts src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts",
"result": "passed",
"durationSeconds": 0.91,
"summary": "Three runtime transport files and 35 tests passed with deterministic first- and later-socket cadence, exact listener/timer ownership, pause recovery, security bounds, and cleanup."
"summary": "Four runtime transport files and 42 tests passed with deterministic authenticated heartbeat cadence, handshake grace for later sockets, exact listener/timer ownership, pause recovery, transient packet-loss tolerance, security bounds, and cleanup."
}
],
"runtimeBudget": {
@@ -2545,7 +2557,7 @@
},
"redGreenEvidence": {
"status": "complete",
"evidence": "On latest main and with the listener-order fix disabled, the first synchronous probe observed no message, pong, close, or error owner. The structural listener-order fix passed those assertions. The published delayed-first-sweep alternative missed the first-socket one-interval cleanup bound. A later-socket oracle now separately pins the intended shared cadence at a 100 ms first probe and 200 ms reap after acceptance at 50 ms."
"evidence": "On the candidate before this review fix, an authenticated first socket kept the shared timer alive while an unauthenticated socket accepted at 50 ms was pinged at the 100 ms tick; the new oracle failed. After filtering heartbeat clients to the authenticated transport map, the same byte-identical oracle passes: no pre-auth ping, first post-auth probe on the next shared tick, and bounded cleanup."
},
"performanceBudget": {
"required": true,
@@ -5717,6 +5729,284 @@
],
"demotionRule": "Keep experimental or demote if ownership cardinality flakes, duplicate replay reaches a second renderer, active metadata or authority moves to the wrong leaf, deep normalization regresses, or a supported provider bypasses normalization."
},
{
"id": "terminal-session.split-activation-ordering",
"title": "Terminal splits activate before inherited-CWD lookup settles",
"maturity": "experimental",
"protection": "partial",
"owner": "terminal-renderer-lifecycle",
"layer": "renderer-unit-and-local-transport",
"surfaces": [
"terminal pane split",
"inherited working directory",
"pre-connect terminal input",
"split close cleanup",
"pre-bind pane detach"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "local-daemon", "ssh", "wsl", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "remote-runtime"],
"coverageNotes": "Deterministic renderer contracts hold CWD resolution behind an explicit promise, require the new pane to be created synchronously, and prove that close cancels the pending connection. A module-level stable-pane-key handoff preserves the exact CWD promise and bounded pre-connect input across whole-tab remounts, including a tab rehome between worktree buckets; stale owners are fenced, concrete PTY bind and definitive spawn failure clear the record, and explicit pane close discards it. Detach contracts reject cwd-pending and cwd-resolved deferred splits before PTY bind without mutation, carry resolved cwd for other unbound panes, and preserve persisted or live PTY handoff. Local IPC transport contracts exercise the real bounded pre-connect buffer and one live input FIFO across seeded and newly typed ordinary, acknowledged, and immediate writes, concurrent flushes, in-flight teardown, late spawn success or failure, attach failure, failed spawn, same-id reuse, stale-spawn retirement ownership, destroy, and mutable recovery metadata. The handoff registry is capped at 64 records for 15 seconds and shares the existing 1,024-entry/conservative UTF-16 input ceilings. A mocked direct-SSH authority-rotation contract proves a rejected stale spawn releases its deferred-CWD fence. The schema-v2 headful Electron benchmark records exact revision identity and attributes CWD request/settlement, PTY spawn request/result, bind, fixture unlock request/IPC write, fixture readiness, input, and first echo across 3 warmups and 20 measured cold-CWD cycles, requiring a distinct child PTY and observed child pty:exit before the next cycle. Remote-runtime coverage proves delegation remains host-owned; its host-delegated split path does not consume the local pre-connect input options, so remote-runtime input-remount replay and physical local-daemon, SSH, WSL, Linux, Windows, and folder-workspace latency journeys remain gaps.",
"motivatingLinks": ["https://github.com/stablyai/orca/commit/572ed1a8882"],
"invariant": "A terminal split creates and activates its renderer pane before an inherited-CWD lookup settles, starts its PTY only after the resolved directory is available, and cannot be externally detached while that deferred spawn remains unbound. A whole-tab remount or worktree rehome preserves the same stable pane's CWD promise and admitted local pre-connect bytes in order until a successor binds or the intent is definitively abandoned; stale owners cannot append or clear the successor's record. Other unbound panes preserve resolved cwd as startupCwd when detached. Bounded pre-connect input and later live local input share byte order, and teardown settles acknowledged writes without creating or rebinding a stale PTY. A disconnected or detached pending connect cannot bind its late fresh spawn, report its late failure through current callbacks, or ID-retire a newer same-ID owner; rejecting a stale direct-SSH spawn also releases the matching deferred-CWD fence. Natural exit cannot deliver queued work into a reused PTY id. Bound and remote-runtime splits remain owned by their execution host.",
"oracle": "Hold CWD resolution behind a controllable promise, invoke the production split path, and require manager.splitPane plus split telemetry before resolving it. Before PTY bind, require both cwd-pending and cwd-resolved deferred detach attempts to return null without layout, pane, tab, ownership, or focus mutation; separately require resolved cwd on an allowed unbound detach and unchanged persisted/live PTY adoption. Exercise the stable-pane handoff registry through repeated remounts and a worktree rehome, requiring the identical CWD promise, ordered ordinary/acknowledged/immediate seed replay, stale-owner fencing, 64-record/15-second bounds, and discard on bind, failure, or explicit close. Rotate a mocked direct-SSH authority while its delayed spawn is in flight, reject and disconnect the stale PTY claim, then require exactly one deferred-CWD cleanup when the delayed connect settles. At the local IPC PTY boundary, require zero connect calls while pending, the resolved CWD in spawn and local recovery metadata, one shared FIFO across seeded/new pre-connect and live ordinary/acknowledged/immediate input, prompt predecessor acknowledged-promise settlement on teardown, retirement of an unowned late fresh spawn, preservation of a newer same-ID owner, and zero stale delivery after failure, close, destroy, detach, natural exit, or same-id reuse. Capture callback exceptions must not change admission results. In visible Electron, press the real split shortcut for 3 warmup cycles, then after a cold inherited-CWD interval for each of 20 measured cycles, require an exact clean revision identity, complete focus/CWD/spawn/bind/fixture/input/echo attribution, distinct child PTYs, pane count, and child exits for every cycle; a timed-out, missing-event, or cleanup-aborted run must publish no headline latency and fail.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts tests/e2e/terminal-split-activation-latency-main-probe.ts tests/e2e/terminal-split-activation-latency-phases.ts tests/e2e/terminal-split-activation-latency-report.unit.test.ts --reporter=dot",
// Historical evidence record retained so its seven-file command remains auditable.
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts --reporter=dot",
// Historical evidence record retained so its nine-file command remains auditable.
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts --reporter=dot",
// Historical schema-v1 evidence records only; their labels do not verify the checkout or harness.
"ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=baseline-df14d1a2983d8339e788d0e521f1c4affd9c6d5f-headful-run1 ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-baseline-df14d1a2983d8339e788d0e521f1c4affd9c6d5f-headful-run1.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-d453ffcdb704764daced1b2917fddee7224389f0-headful-run2 ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-candidate-d453ffcdb704764daced1b2917fddee7224389f0-headful-run2.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
// Exact-HEAD schema-v1 evidence records use the committed harness but predate revision metadata.
"ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-962faacec8c-headful-current ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-962faacec8c-headful.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-962faacec8c-headful-run2 ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-962faacec8c-headful-run2.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
// Schema-v2 evidence embeds the exact checkout identity and clean/dirty state.
"ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-073e6c7b0eb-headful-clean ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-073e6c7b0eb-headful-clean.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1"
],
"testFiles": [
"src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts",
"src/renderer/src/lib/pane-manager/pane-split-close.test.ts",
"src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts",
"src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts",
"src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts",
"src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts",
"src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts",
"src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts",
"tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts",
"tests/e2e/terminal-split-activation-latency-main-probe.ts",
"tests/e2e/terminal-split-activation-latency-phases.ts",
"tests/e2e/terminal-split-activation-latency-report.unit.test.ts",
"tests/e2e/terminal-split-activation-latency.spec.ts"
],
"assertionRefs": [
{
"file": "src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts",
"assertions": [
"creates and records the split before pending CWD resolution",
"passes the resolved CWD promise without reviving a stale manager",
"rapid nested splits reuse one pending CWD lookup",
"keeps remote-runtime split ownership on its execution host"
]
},
{
"file": "src/renderer/src/lib/pane-manager/pane-split-close.test.ts",
"assertions": ["focuses the new pane before publishing an unresolved CWD spawn hint"]
},
{
"file": "src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts",
"assertions": [
"preserves a deferred split fence when OSC 7 updates cwd",
"clears a settled deferred entry only for matching promise identity",
"keeps a newer deferred lookup when an older cleanup callback arrives"
]
},
{
"file": "src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts",
"assertions": [
"starts no PTY connection before inherited CWD resolves",
"applies the resolved directory to transport options",
"disposing the split before resolution cancels the pending spawn",
"invokes deferred-CWD cleanup exactly once after an authority-rotated direct-SSH spawn is disconnected and its delayed connect settles"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts",
"assertions": [
"rejects a deferred split while inherited CWD is pending without mutation",
"continues rejecting after CWD resolves until PTY bind",
"carries resolved CWD as startupCwd for an allowed unbound detach",
"preserves persisted and live remote PTY detach handoff"
]
},
{
"file": "src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts",
"assertions": [
"concurrent flush calls share one worker and preserve mixed input order",
"clear settles an in-flight acknowledged write before its late resolve or reject",
"in-flight acknowledged input remains charged to entry and code-unit caps"
]
},
{
"file": "src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts",
"assertions": [
"ordinary, acknowledged, and immediate pre-connect input flushes in byte order",
"pending acknowledged input settles on connect, destroy, and spawn failure",
"disconnect, destroy, and natural exit cancel an in-flight acknowledged write without blocking connect",
"live acknowledged input blocks later ordinary and immediate writes at its invocation position",
"the preconnect-to-live transition preserves the same input FIFO",
"disconnect and detach retire a late fresh spawn and suppress late failures before they reach current callbacks",
"natural exit fences queued ordinary and acknowledged chunks across same-id reuse",
"buffered exit and attach failure clear retained input",
"ordinary and acknowledged write failures drop later input without leaving promises pending",
"entry and code-unit ceilings bound pre-connect input retention",
"local recovery metadata observes the resolved split CWD",
"a stale fresh-spawn completion cannot retire a newer same-ID owner"
]
},
{
"file": "src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts",
"assertions": [
"hands the same cwd promise and buffered input to a remounted leaf in order",
"keeps input across repeated remounts and fences stale owners",
"releases an unmounted owner without dropping its pending handoff",
"retains input within the shared preconnect entry and code-unit caps",
"evicts the oldest handoff when the record cap is reached",
"expires an abandoned handoff after the bounded remount window"
]
},
{
"file": "tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts",
"assertions": [
"writes a passing benchmark report to the requested artifact path",
"fails when the benchmark artifact path cannot be written"
]
},
{
"file": "tests/e2e/terminal-split-activation-latency-main-probe.ts",
"assertions": [
"attributes CWD and PTY spawn request/settlement events to the source and child PTYs",
"captures the fixture unlock carriage return on both ordinary and acknowledged IPC channels",
"restores the intercepted IPC handlers and listener when the probe is disposed"
]
},
{
"file": "tests/e2e/terminal-split-activation-latency-phases.ts",
"assertions": [
"merges main-process events by operation and PTY identity without cross-cycle attribution",
"requires every activation, fixture, input, echo, pane, PTY, and cleanup observation for success",
"reports each attributed phase distribution with non-negative cross-clock durations"
]
},
{
"file": "tests/e2e/terminal-split-activation-latency-report.unit.test.ts",
"assertions": [
"attributes main-process phases to the matching source and child PTYs",
"embeds schema-v2 revision identity and summarizes the attributed phases",
"invalidates a sample when the actual fixture-unlock IPC write is missing"
]
},
{
"file": "tests/e2e/terminal-split-activation-latency.spec.ts",
"assertions": [
"requires a visible BrowserWindow and visible document before sampling",
"records schema-v2 revision identity plus attributed CWD, spawn, bind, fixture-ready, input, and echo phases",
"records 3 warmups, then 20 measured real-shortcut cycles after cold inherited-CWD intervals",
"requires every split to focus, bind a PTY distinct from its source, and echo immediate input",
"observes each closed child PTY exit before starting the next cycle",
"publishes headline latency only for a fully successful 3-warmup/20-measured run"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-30",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts --reporter=dot",
"durationSeconds": 56.59,
"summary": "Seven focused files and 78 tests passed. Vitest reported 49.92 seconds and the measured wall time was 56.59 seconds; coverage includes split creation and focus ordering, nested CWD lineage, promise-identity and SSH authority-rotation cleanup fencing, full pre-bind detach fencing, resolved-CWD detach handoff, close-cancellation, single-FIFO ordering, late-spawn retirement and error suppression, preservation of a newer same-ID owner, generation fencing, in-flight settlement across explicit teardown and natural exit, attach cleanup, bounded retention, and existing input-write contracts."
},
{
"date": "2026-08-31",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts --reporter=dot",
"durationSeconds": 44.43,
"summary": "Nine focused files and 96 tests passed. Vitest reported 37.78 seconds and measured wall time was 44.43 seconds; the run adds stable-pane CWD/input handoff, repeated-remount stale-owner fencing, bounded 64-record/15-second retention, seeded-input caps, ordered ordinary/acknowledged/immediate replay, predecessor acknowledged-promise settlement, capture-callback failure containment, and benchmark-artifact write-failure coverage to the existing split, detach, CWD, and local transport contracts."
},
{
"date": "2026-08-31",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts tests/e2e/terminal-split-activation-latency-main-probe.ts tests/e2e/terminal-split-activation-latency-phases.ts tests/e2e/terminal-split-activation-latency-report.unit.test.ts --reporter=dot",
"durationSeconds": 4.14,
"summary": "The updated twelve-path focused command passed 99 tests (10 runnable test files plus 2 benchmark support modules), including schema-v2 main-process phase attribution, report revision identity, fixture IPC-write validation, stable-pane handoff, ordered pre-connect/live input, cleanup, detach, failure, and same-ID ownership contracts."
},
{
"date": "2026-08-30",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=baseline-df14d1a2983d8339e788d0e521f1c4affd9c6d5f-headful-run1 ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-baseline-df14d1a2983d8339e788d0e521f1c4affd9c6d5f-headful-run1.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"durationSeconds": 72,
"summary": "At baseline df14d1a2983d8339e788d0e521f1c4affd9c6d5f, the visible BrowserWindow and document completed 3/3 warmups followed by 20/20 measured cold-CWD cycles with every event present, distinct child PTYs, and observed child exits. Shortcut-to-focus p50/p95/max was 65.7/88.7/102.6 ms, PTY bind was 122.8/154.0/159.7 ms, and first echo was 203.8/264.2/332.7 ms. Artifact SHA-256: 6d860cd0cd210f55f2349a197318248af488042117b20c08b6831160950c3277."
},
{
"date": "2026-08-30",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-d453ffcdb704764daced1b2917fddee7224389f0-headful-run2 ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-candidate-d453ffcdb704764daced1b2917fddee7224389f0-headful-run2.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"durationSeconds": 72,
"summary": "At candidate d453ffcdb704764daced1b2917fddee7224389f0, the visible BrowserWindow and document completed 3/3 warmups followed by 20/20 measured cold-CWD cycles with every event present, distinct child PTYs, and observed child exits. Shortcut-to-focus p50/p95/max was 12.8/14.4/16.5 ms, PTY bind was 177.0/316.1/333.3 ms, and first echo was 268.6/627.4/710.7 ms. Artifact SHA-256: 9aef7fa842c732eb74f0066a77b2a0336e8f956a06ca61387f9999d6e76213cc."
},
{
"date": "2026-08-31",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-962faacec8c-headful-current ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-962faacec8c-headful.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"durationSeconds": 72,
"summary": "At exact HEAD 962faacec8c, the visible BrowserWindow and document completed 3/3 warmups followed by 20/20 measured cold-CWD cycles with every event present, distinct child PTYs, and observed child exits. Shortcut-to-focus p50/p95/max was 12.6/13.7/13.7 ms, PTY bind was 140.5/399.1/464.1 ms, and first echo was 192.0/519.3/2343.1 ms. Artifact SHA-256: 875e9d37dc711472a81438e4bbdbc8cbc7aada8c961d14195c024d8da350b9e2."
},
{
"date": "2026-08-31",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-962faacec8c-headful-run2 ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-962faacec8c-headful-run2.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"durationSeconds": 72,
"summary": "At exact HEAD 962faacec8c, the visible BrowserWindow and document completed 3/3 warmups followed by 20/20 measured cold-CWD cycles with every event present, distinct child PTYs, and observed child exits. Shortcut-to-focus p50/p95/max was 12.8/14.0/14.3 ms, PTY bind was 236.5/654.0/687.3 ms, and first echo was 566.8/1191.5/1219.7 ms. Artifact SHA-256: 4f66b93e5c936ade05f880010f4ec027385d5c89893430169af91c7fdfbe1d06."
},
{
"date": "2026-08-31",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "ORCA_TERMINAL_SPLIT_LATENCY_BENCH=1 ORCA_TERMINAL_SPLIT_LATENCY_LABEL=candidate-073e6c7b0eb-headful-clean ORCA_TERMINAL_SPLIT_LATENCY_OUTPUT=/private/tmp/orca-terminal-split-activation-073e6c7b0eb-headful-clean.json pnpm exec playwright test tests/e2e/terminal-split-activation-latency.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"durationSeconds": 87.6,
"summary": "At exact clean HEAD 073e6c7b0eb1c0ccbb115db1528b641901997c73, the schema-v2 artifact records dirty=false, a visible BrowserWindow/document, and 3/3 warmups plus 20/20 measured cycles with every missing-event counter at zero, distinct child PTYs, and observed child exits. Measured focus p50/p95/max was 12.0/13.6/14.7 ms; attributed CWD lookup was 40/97/103 ms, CWD-settle to spawn request 1/4/4 ms, spawn request to result 48/120/122 ms, and spawn result to bind 2.1/2.5/2.5 ms. Fixture unlock request to IPC write was 0.5/0.8/0.9 ms, IPC write to fixture-ready parse was 35.1/87.4/141.8 ms, and input to first echo was 1.3/3.5/3.9 ms. Total shortcut-to-bind was 113.5/161.3/162.7 ms and shortcut-to-first-echo was 158.5/240.7/291.2 ms. Artifact SHA-256: 1e7ff9e658b717056273d64ecdf662cc6b5776bb6dae1827ed213b7647e4a5fb."
}
],
"evidenceProcedure": "Run the benchmark spec in a visible macOS Electron project from a clean primary worktree, complete 3 warmups followed by 20 measured cold-CWD cycles, require zero missing events and successful cleanup, save the schema-v2 JSON report, and record its SHA-256 plus embedded revision identity. The four older records are schema-v1 historical audit records whose labels do not verify the checkout or include phase attribution; the clean schema-v2 record is the current candidate evidence. A paired baseline/final rerun with one revision-verifying harness remains required before promotion or a readiness comparison claim.",
"runtimeBudget": {
"p95Seconds": 240,
"scope": "the full listed gate command set: one current twelve-path focused invocation (ten runnable test files plus two benchmark support modules), one historical nine-file unit invocation, one historical seven-file unit invocation, and five opt-in 3-warmup/20-measured visible Electron benchmark invocations (four schema-v1 historical records plus one clean schema-v2 record)"
},
"flakeHistory": {
"status": "not-started",
"evidence": "The focused promise-barrier, remount-handoff, benchmark-artifact, and schema-v2 attribution suite passes locally in 99 tests; the clean visible benchmark passes 3/3 warmups and 20/20 measured cycles with zero missing events. Its first attempt hit a transient warmup cleanup-dialog click timeout, then the exact command passed on retry without a launch, profile, or port workaround. Routed CI and soak history have not started. Historical benchmark labels do not verify the product checkout or embed revision identity."
},
"redGreenEvidence": {
"status": "partial",
"evidence": "Before the production seam landed, the split assertion failed with zero manager calls while CWD was pending, and the transport assertion rejected the first pre-connect input. Before the detach fence, a deferred split could be removed after CWD resolved but before PTY bind, dropping its pre-connect input. Before the single-flight hardening, the concurrent-flush oracle delivered ordinary input before the earlier acknowledged write and clear left the in-flight promise pending. Before stale-spawn ownership fencing, the combined deferred-connect and newer same-ID attach fixture called kill on the current PTY. An intentional one-line revert of deferred-CWD cleanup in the stale direct-SSH claim branch failed its focused callback assertion with zero calls instead of one; restoring it passed the prior focused tests. The remount-handoff, transport, artifact-write, and schema-v2 attribution regressions are green in the 99-test twelve-path run, but isolated intentional-revert evidence for each cleanup branch remains outstanding."
},
"performanceBudget": {
"required": true,
"evidence": "Pane creation and focus add no timer, polling, provider inventory, or subprocess work. CWD resolution remains one existing bounded request off the visible activation path, and detach admission adds only bounded map and record lookups. The remount handoff adds one module-level map lookup per pane lifecycle, a 64-record cap, and a 15-second expiry; it retains no unbounded payload. Pre-connect input, including an in-flight acknowledged write and remount seed replay, is capped at 1,024 entries and a conservative UTF-16 ceiling derived from the existing terminal-input byte limit, drains through one worker in order, and clears on teardown or failed connect. The historical schema-v1 same-mode pair recorded shortcut-to-focus p50/p95/max changing from 65.7/88.7/102.6 ms to 12.8/14.4/16.5 ms; those labels do not embed revision identity, so the comparison is directional evidence only. The clean schema-v2 candidate attributes focus at 12.0/13.6/14.7 ms while CWD lookup takes 40/97/103 ms and spawn request-to-result takes 48/120/122 ms, demonstrating that provider/process startup follows activation rather than blocking it. In that clean run, shortcut-to-bind is 113.5/161.3/162.7 ms, fixture IPC-write-to-ready is 35.1/87.4/141.8 ms, and input-to-echo is 1.3/3.5/3.9 ms; these readiness phases are diagnostic, one-host descriptive measurements, and no clean schema-v2 baseline exists to support a readiness improvement or regression claim. The n=20 empirical p95 values are descriptive, are not a distribution guarantee, and are not CI-enforced."
},
"promotionCriteria": [
"Record complete red/green evidence for close, remount/rehome handoff, mixed-input ordering, metadata, and failure cleanup.",
"Collect 100 consecutive focused CI passes or 14 days without an unexplained flake.",
"Run the committed real-shortcut Electron benchmark in routed CI or soak before enforcing a latency budget.",
"Collect physical local-daemon, SSH or WSL plus Linux, Windows, and folder-workspace evidence before claiming provider-complete coverage."
],
"knownGaps": [
"The clean schema-v2 candidate run and the historical schema-v1 comparison records ran on one Apple-silicon macOS host with a synthetic POSIX echo shell and a git-backed workspace; their n=20 empirical p95 values are descriptive and not CI-enforced.",
"No physical local-daemon, SSH, WSL, Linux, Windows, or folder-workspace latency journey has run; the synthetic fixture is currently skipped on Windows because it requires a POSIX shell.",
"Remote-runtime split creation remains host-delegated and its transport does not consume the local pre-connect seed/capture options; CWD handoff is covered, but remote-runtime pre-connect input replay has no implementation or evidence.",
"The four stored schema-v1 artifacts predate the final harness attribution/reporting and do not embed revision identity; the clean schema-v2 candidate artifact is revision-verified, but both product revisions still need a paired schema-v2 rerun with one committed harness before promotion.",
"No forced-failure visible benchmark artifact has been recorded; the focused artifact-write and missing-event report contracts verify local failure handling, while failure-report serialization remains unverified by a full visible run.",
"No clean schema-v2 baseline phase artifact exists, so the attributed CWD, spawn, fixture-ready, bind, and echo timings diagnose where time is spent but do not establish a shell-readiness improvement or regression."
],
"demotionRule": "Keep experimental or demote if pane activation waits on CWD, a deferred split can detach before PTY bind, a remount or rehome loses its stable CWD/input handoff, stale owners mutate a successor record, detached cwd is lost, input reorders or remains pending after cleanup, a closed pane can spawn, stale retirement kills a newer same-ID owner, remote-runtime delegation creates a competing local pane, or the focused suite flakes without an identified product or harness cause."
},
{
"id": "terminal-session.kill-all-surface-cleanup",
"title": "Kill all sessions removes only the confirmed terminal surfaces and current bindings",
@@ -8403,7 +8693,7 @@
"providers": ["local", "daemon", "wsl"],
"coveredPlatforms": ["windows"],
"coveredProviders": ["daemon"],
"coverageNotes": "Issue #8048 now has deterministic wrapper and cold-restore re-anchor tests plus a Windows PR-CI harness that drives the built daemon through 25 real ConPTY workspace-close races while an unrelated witness PTY stays alive. Keyboard reset, CJK repaint, WSL, and full visible Electron coverage remain gaps.",
"coverageNotes": "Issue #8048 now has deterministic wrapper and cold-restore re-anchor tests plus a Windows PR-CI harness that drives the built daemon through 25 real ConPTY workspace-close races while an unrelated witness PTY stays alive. A Windows-only patched-node-pty test injects EAGAIN on one ConPTY input pipe and requires only that PTY to close while an unrelated PTY remains writable; a daemon-level classifier test keeps the native exception backstop narrow. Keyboard reset, CJK repaint, WSL, and full visible Electron coverage remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/6541",
"https://github.com/stablyai/orca/pull/6858",
@@ -8411,18 +8701,21 @@
"https://github.com/stablyai/orca/pull/6968",
"https://github.com/stablyai/orca/pull/6970",
"https://github.com/stablyai/orca/pull/6999",
"https://github.com/stablyai/orca/issues/8048"
"https://github.com/stablyai/orca/issues/8048",
"https://github.com/stablyai/orca/issues/17027"
],
"invariant": "Windows local and daemon terminals must spawn with the intended shell, survive overlapping graceful/forced workspace teardown without affecting unrelated PTYs, retain recovered scrollback across the fresh daemon's first checkpoint, accept normal Enter/Backspace/Arrow input after agent or TUI exit, render cursor/CJK/wide-glyph redraws without stale cells, and converge to nonzero applied size.",
"oracle": "The issue #8048 slice asserts one node-pty ConPTY close for a graceful-then-force sequence, atomically seeds recovered history before fresh shell output and re-anchoring, preserves recovery after seed failure plus adapter restart, and runs 25 built-daemon close races while checking victim session/PID reaping, a stable daemon PID, and a live witness PTY. A broader Windows live gate still needs shell input, resize, cursor, and CJK/wide-glyph pixel evidence.",
"invariant": "Windows local and daemon terminals must spawn with the intended shell, survive overlapping graceful/forced workspace teardown without affecting unrelated PTYs, contain an asynchronous ConPTY input-pipe failure to the affected terminal without killing the daemon, retain recovered scrollback across the fresh daemon's first checkpoint, accept normal Enter/Backspace/Arrow input after agent or TUI exit, render cursor/CJK/wide-glyph redraws without stale cells, and converge to nonzero applied size.",
"oracle": "The issue #8048 slice asserts one node-pty ConPTY close for a graceful-then-force sequence, atomically seeds recovered history before fresh shell output and re-anchoring, preserves recovery after seed failure plus adapter restart, and runs 25 built-daemon close races while checking victim session/PID reaping, a stable daemon PID, and a live witness PTY. The EAGAIN slice emits an error from one real patched node-pty Windows input socket, requires its terminal to become unwritable and run the normal per-PTY kill path, then writes through an unrelated PTY without an uncaught exception. A broader Windows live gate still needs shell input, resize, cursor, and CJK/wide-glyph pixel evidence.",
"commands": [
"pnpm vitest run src/main/daemon/pty-subprocess.test.ts src/main/daemon/daemon-pty-adapter.test.ts",
"pnpm vitest run src/main/daemon/pty-subprocess.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/node-pty-windows-input-error.win32.test.ts src/main/daemon/daemon-native-pty-exception.test.ts",
"pnpm build:electron-vite && node config/scripts/windows-daemon-workspace-close-repro.mjs",
"node config/scripts/windows-daemon-workspace-close-repro.mjs"
],
"testFiles": [
"src/main/daemon/pty-subprocess.test.ts",
"src/main/daemon/daemon-pty-adapter.test.ts",
"src/main/daemon/node-pty-windows-input-error.win32.test.ts",
"src/main/daemon/daemon-native-pty-exception.test.ts",
"config/scripts/windows-daemon-workspace-close-repro.mjs"
],
"assertionRefs": [
@@ -8439,6 +8732,18 @@
"a failed atomic history seed remains non-authoritative across adapter restart and cannot overwrite the recovery files"
]
},
{
"file": "src/main/daemon/node-pty-windows-input-error.win32.test.ts",
"assertions": [
"an EAGAIN event retires only the affected patched node-pty terminal while a witness remains writable"
]
},
{
"file": "src/main/daemon/daemon-native-pty-exception.test.ts",
"assertions": [
"the daemon suppresses native PTY errno failures while rejecting non-Error and unrelated logic failures"
]
},
{
"file": "config/scripts/windows-daemon-workspace-close-repro.mjs",
"assertions": [
@@ -8480,6 +8785,7 @@
],
"knownGaps": [
"Real IME composition may require a separate lower-layer/native-text-forwarding gate.",
"The EAGAIN oracle injects the real input socket event rather than inducing kernel resource exhaustion on a packaged Windows host.",
"The built-daemon harness proves process/session liveness but not renderer pixels; visible shell input, resize, cursor, and CJK repaint remain uncovered."
],
"demotionRule": "Cannot promote while Windows E2E is flaky, silently skipped, or screenshot-only."
@@ -10324,6 +10630,8 @@
"remote-runtime host surface materialization",
"remote-runtime mirror polling",
"remote-runtime network recovery",
"pane-scoped remote terminal recovery errors",
"bounded terminal error surfaces",
"paired client sleep/wake reconnect",
"terminal create idempotency",
"provider listing",
@@ -10335,7 +10643,7 @@
"providers": ["ssh", "remote-runtime", "wsl"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["ssh", "remote-runtime"],
"coverageNotes": "Deterministic renderer coverage proves startup publishes the state returned by ssh.connect, retained native and runtime SSH payloads are admitted through production routes only with valid complete authority, stale cleanup cannot unregister a replacement runtime terminal, direct SSH Git and folder panes clear and retry by exact authority, one authority chain stops after two automatic attempts even when each timeout exceeds the rolling window, rejected acknowledgements mutate no store maps, and one shared exact attempt admits every concurrent split-pane spawn and reattach while preserving the first PTY as the tab fallback. A later sibling failure rotates the tab once, stale callbacks from the prior attempt mutate no state, split remount activity suppression is counted per leaf, primary PTY exit promotes a bound survivor or preserves an empty continuation gap for a late sibling, and primary, non-primary, or null-PTY detach preserves exact authority on both resulting tabs. Intentional pane disposal cancels its settlement timer without breaking StrictMode remount timeout ownership. Target snapshot hydration/reconnect preserves sibling SSH/local/WSL/runtime state, and a mounted remote-runtime terminal survives repeated transport partitions without changing PTY identity. A real encrypted-WebSocket oracle proves a successful reachability probe can replace a pre-ready shared-control socket without rejecting or duplicating the waiting RPC. Direct SSH coordinator tests cover immediate terminal finalization, hydration correction, damping, bounded retry, and telemetry non-interference. Client/server heartbeat tests cover timer suspension, socket generations fence stale callbacks, cold restored-terminal attachment retries, cached pixels remain unhealthy until authoritative replay, automatic retries stop after one minute, manual reconnect preserves the PTY, and pane closure releases recovery UI state. Current macOS Electron journeys against an ephemeral Linux Docker SSH target cover exact-authority repo/worktree hydration, live terminal recovery after disconnect/reconnect, and eager six-terminal remount after renderer reload. A Windows remote-runtime smoke covers reachability and PTY round-trip. Multi-target live fanout, paired-close, WSL, and patched live partition journeys remain gaps.",
"coverageNotes": "Deterministic renderer coverage proves startup publishes the state returned by ssh.connect, retained native and runtime SSH payloads are admitted through production routes only with valid complete authority, stale cleanup cannot unregister a replacement runtime terminal, direct SSH Git and folder panes clear and retry by exact authority, one authority chain stops after two automatic attempts even when each timeout exceeds the rolling window, rejected acknowledgements mutate no store maps, and one shared exact attempt admits every concurrent split-pane spawn and reattach while preserving the first PTY as the tab fallback. A later sibling failure rotates the tab once, stale callbacks from the prior attempt mutate no state, split remount activity suppression is counted per leaf, primary PTY exit promotes a bound survivor or preserves an empty continuation gap for a late sibling, and primary, non-primary, or null-PTY detach preserves exact authority on both resulting tabs. Intentional pane disposal cancels its settlement timer without breaking StrictMode remount timeout ownership. Target snapshot hydration/reconnect preserves sibling SSH/local/WSL/runtime state, superseded readiness and placement waits release immediately, delayed worktree placement keeps only the latest arrival's waiter per target, stale arrivals cannot hydrate or publish push status, an incoming snapshot revokes replace-session upload authority before preparation yields, already-captured uploads revalidate that exact authority after local persistence settles, and main rejects their applied revision when a newer host snapshot arrives before IPC admission or while queued. A mounted remote-runtime terminal survives repeated transport partitions without changing PTY identity. A real encrypted-WebSocket oracle proves a successful reachability probe can replace a pre-ready shared-control socket without rejecting or duplicating the waiting RPC. Direct SSH coordinator tests cover immediate terminal finalization, hydration correction, damping, bounded retry, and telemetry non-interference. Client/server heartbeat tests cover timer suspension, socket generations fence stale callbacks, cold restored-terminal attachment retries, cached pixels remain unhealthy until authoritative replay, automatic retries stop after one minute, manual reconnect preserves the PTY, and pane closure releases recovery UI state. Current-transport recovery clears every error that transport surfaced without clearing or displaying a sibling pane's errors, while transport suppression, pane retention, and the rendered surface are independently bounded. Current macOS Electron journeys against an ephemeral Linux Docker SSH target cover exact-authority repo/worktree hydration, live terminal recovery after disconnect/reconnect, and eager six-terminal remount after renderer reload. A Windows remote-runtime smoke covers reachability and PTY round-trip. Multi-target live fanout, paired-close, WSL, and patched live partition journeys remain gaps.",
"motivatingLinks": [
"https://linear.app/stably/issue/STA-3107",
"https://github.com/stablyai/orca/pull/12664",
@@ -10344,17 +10652,25 @@
"https://github.com/stablyai/orca/pull/6979",
"https://github.com/stablyai/orca/pull/7009",
"https://github.com/stablyai/orca/pull/8597",
"https://github.com/stablyai/orca/issues/11541"
"https://github.com/stablyai/orca/issues/11541",
"https://github.com/stablyai/orca/issues/15141",
"https://github.com/stablyai/orca/issues/12685",
"https://github.com/stablyai/orca/issues/12902"
],
"invariant": "SSH, WSL, and remote-runtime restore paths must treat provider listing failures and unknown liveness as unknown, not dead, while still avoiding duplicate spawn and clearing expired relay leases exactly once. Direct SSH reconnect must atomically clear only exact-target live PTY bindings, preserve relay identity, retry Git and folder panes without paired close or provider shutdown, and allow at most two automatic attempts in one authority chain even when each settlement exceeds the rolling window. A rejected acknowledgement mutates no store map. A successful exact split-pane spawn or reattach must retain that attempt as shared live authority until sibling leaves settle; the first success cannot consume sibling authority, a sibling failure can start at most one second tab-wide attempt, and prior-attempt callbacks become inert after rotation. Once the retry budget is exhausted, a failure cannot start attempt three or revoke attempt-two authority from siblings that may still settle. Primary PTY exit must promote a bound survivor or preserve exact authority through an empty activation gap, and split detach must project that authority to both resulting tabs. Hydrated PTY hints cannot supersede a current exact-attempt owner, and target snapshot hydration/reconnect cannot reset sibling SSH, local, WSL, or runtime-owned state. Every restored remote terminal must preserve its provider PTY identity, including the authoritative incarnation returned by a successful session-ID reattach. After a recoverable partition the same authenticated runtime must reattach the same PTY, reject detached input, apply the latest viewport, and report healthy only after authoritative replay. A successful one-shot reachability probe may replace a pre-ready shared-control socket, but waiting RPCs must continue onto the replacement under their original deadline without duplicate host delivery or retained request bytes. Automatic PTY recovery stops after one bounded minute without a fatal terminal error; a manual reconnect starts a newly fenced epoch against the same PTY, and closed panes retain no recovery UI state. One capability-gated terminal-create mutation must produce at most one host PTY across an unknown response outcome, remain manually retryable after cutoff, and never let a stale completion replace a newer pane lifecycle. Reconnect must alternate exact activation with authoritative inventory so neither a stale activation response nor an activation failure can strand or retire a pane, and activating a parked surface whose persisted binding was already retired must respawn it rather than report a changed owner after signalling its exit.",
"oracle": "Deterministic tests cover bounded stale-handle replacement, suspended heartbeat clocks, cold and established subscription failure, ten partition/recovery cycles, automatic-recovery cutoff, manual reconnect, and exact direct SSH binding recovery. They assert one atomic store publication clears only exact-target PTY indexes, null-PTY activation remains unchanged, relay identity survives, Git and folder panes retry symmetrically, another target/local/WSL/runtime panes remain byte-identical through target snapshot hydration and reconnect, only an accepted exact failure or timeout starts the second attempt, two 31-second timeouts cannot start a third settlement-triggered attempt, rejected stale/mismatched acknowledgements preserve every store map, and concurrent split-pane spawn and reattach callbacks both commit under the same attempt ID after the first success replaces pending state with live shared authority. A sibling failure revokes that shared authority and starts exactly one second attempt; duplicate failures and late first-attempt PTY callbacks preserve the second attempt and every state map. Attempt-two failure retains continuation authority for later siblings, primary exit promotes a bound survivor or preserves the lease until a late sibling binds, and primary plus non-primary detach retain exact authority and history on both resulting tabs. Both remount callbacks consume split-count activity suppression, intentional dispose emits no failure/timeout, and a same-attempt StrictMode remount still owns one timeout. Hydration clears an untrusted PTY hint without clearing its current pending owner, healthy current-authority bindings suppress correction, hydration finalizes once, and reconnect emits no paired close lifecycle. A provider-level session-ID reattach returns an incarnation, then a legacy exit without an incarnation must resolve to that returned identity rather than minting a fallback identity. The shared-control oracle withholds the first encrypted ready frame, starts one RPC, triggers the successful-probe refresh, then requires exactly two client connections, one host request, a successful response, zero pending calls, and zero retained request bytes. Tests also assert one unsubscribe per remote-runtime epoch, observable recovery phases, stable PTY identity, resumed snapshot/output/input, no healthy state before replay, no retry or input after cutoff, a new manual epoch against the same PTY, quiet recovery UI with an explicit Reconnect action, pane-close state cleanup, one stable create mutation id, old-runtime no-retry behavior, cross-process PTY adoption, and bounded in-flight coordination.",
"invariant": "SSH, WSL, and remote-runtime restore paths must treat provider listing failures and unknown liveness as unknown, not dead, while still avoiding duplicate spawn and clearing expired relay leases exactly once. Direct SSH reconnect must atomically clear only exact-target live PTY bindings, preserve relay identity, retry Git and folder panes without paired close or provider shutdown, and allow at most two automatic attempts in one authority chain even when each settlement exceeds the rolling window. A rejected acknowledgement mutates no store map. A successful exact split-pane spawn or reattach must retain that attempt as shared live authority until sibling leaves settle; the first success cannot consume sibling authority, a sibling failure can start at most one second tab-wide attempt, and prior-attempt callbacks become inert after rotation. Once the retry budget is exhausted, a failure cannot start attempt three or revoke attempt-two authority from siblings that may still settle. Primary PTY exit must promote a bound survivor or preserve exact authority through an empty activation gap, and split detach must project that authority to both resulting tabs. Hydrated PTY hints cannot supersede a current exact-attempt owner, and target snapshot hydration/reconnect cannot reset sibling SSH, local, WSL, or runtime-owned state. Snapshot arrival work may leave at most one readiness or placement timer and one placement subscription live per target; a newer arrival or sync stop must release the previous wait immediately, no superseded snapshot may hydrate after its cancellation, a superseded revision-zero upload may not publish sync status, an incoming snapshot target must be upload-ineligible before snapshot preparation yields, and every upload captured earlier must revalidate the same target authority after its local write and before result publication. Every restored remote terminal must preserve its provider PTY identity, including the authoritative incarnation returned by a successful session-ID reattach. After a recoverable partition the same authenticated runtime must reattach the same PTY, reject detached input, apply the latest viewport, and report healthy only after authoritative replay. A successful one-shot reachability probe may replace a pre-ready shared-control socket, but waiting RPCs must continue onto the replacement under their original deadline without duplicate host delivery or retained request bytes. Automatic PTY recovery stops after one bounded minute without a fatal terminal error; a manual reconnect starts a newly fenced epoch against the same PTY, and closed panes retain no recovery UI state. Successful current-transport recovery must clear every stale error surfaced for that pane without clearing or displaying a sibling pane's error; a later genuine failure must remain visible. Each transport retains at most eight suppressed messages, each pane retains at most eight distinct messages, and the rendered surface retains at most 24 lines and 4,000 characters. One capability-gated terminal-create mutation must produce at most one host PTY across an unknown response outcome, remain manually retryable after cutoff, and never let a stale completion replace a newer pane lifecycle. Reconnect must alternate exact activation with authoritative inventory so neither a stale activation response nor an activation failure can strand or retire a pane, and activating a parked surface whose persisted binding was already retired must respawn it rather than report a changed owner after signalling its exit.",
"oracle": "Deterministic tests cover bounded stale-handle replacement, suspended heartbeat clocks, cold and established subscription failure, ten partition/recovery cycles, automatic-recovery cutoff, manual reconnect, and exact direct SSH binding recovery. They assert one atomic store publication clears only exact-target PTY indexes, null-PTY activation remains unchanged, relay identity survives, Git and folder panes retry symmetrically, another target/local/WSL/runtime panes remain byte-identical through target snapshot hydration and reconnect, only an accepted exact failure or timeout starts the second attempt, two 31-second timeouts cannot start a third settlement-triggered attempt, rejected stale/mismatched acknowledgements preserve every store map, and concurrent split-pane spawn and reattach callbacks both commit under the same attempt ID after the first success replaces pending state with live shared authority. A sibling failure revokes that shared authority and starts exactly one second attempt; duplicate failures and late first-attempt PTY callbacks preserve the second attempt and every state map. Attempt-two failure retains continuation authority for later siblings, primary exit promotes a bound survivor or preserves the lease until a late sibling binds, and primary plus non-primary detach retain exact authority and history on both resulting tabs. Both remount callbacks consume split-count activity suppression, intentional dispose emits no failure/timeout, and a same-attempt StrictMode remount still owns one timeout. Hydration clears an untrusted PTY hint without clearing its current pending owner, healthy current-authority bindings suppress correction, hydration finalizes once, and reconnect emits no paired close lifecycle. A provider-level session-ID reattach returns an incarnation, then a legacy exit without an incarnation must resolve to that returned identity rather than minting a fallback identity. The shared-control oracle withholds the first encrypted ready frame, starts one RPC, triggers the successful-probe refresh, then requires exactly two client connections, one host request, a successful response, zero pending calls, and zero retained request bytes. Tests also assert one unsubscribe per remote-runtime epoch, observable recovery phases, stable PTY identity, resumed snapshot/output/input, no healthy state before replay, no retry or input after cutoff, a new manual epoch against the same PTY, quiet recovery UI with an explicit Reconnect action, pane-close state cleanup, one stable create mutation id, old-runtime no-retry behavior, cross-process PTY adoption, and bounded in-flight coordination. A 32-arrival unplaced-snapshot burst requires exactly one live placement listener and timer throughout, then applies only revision 51 after the catalog arrives and releases both resources; a second 32-arrival burst requires exactly one readiness timer and releases it on stop, while a deferred revision-zero upload cannot publish after a newer snapshot arrives. The real persistence subscriber must also exclude a previously hydrated target from replace-session uploads while incoming snapshot capture is pending, including when the upload captured that target before its local disk write stalled. Stopping with an active placement wait releases it immediately and hydrates nothing. A focused recovery oracle surfaces two distinct failures through one transport, completes authoritative replay, and requires both matching clear callbacks; pane-state tests require only the active pane's error to render, matching-pane recovery to preserve sibling and unrelated errors, dismissal to re-admit later failures, and distinct storms to retain only the newest eight messages within 24 lines and 4,000 characters.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/providers/ssh-pty-provider-reattach-incarnation.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/paired-reconnect-multi-pane-materialization.test.ts src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/remote-workspace-snapshot-arrival-coordinator.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/startup/ssh-startup-reconnect.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts src/renderer/src/runtime/web-session-terminal-handle-events.test.ts src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-outcome-recovery.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-recovery-state.test.ts src/renderer/src/components/terminal-pane/TerminalRemoteRuntimeReconnectBanner.test.tsx src/renderer/src/components/terminal-pane/terminal-remote-runtime-recovery-ui-state.test.ts src/shared/remote-runtime-socket-liveness.test.ts src/shared/remote-runtime-shared-control-connection.test.ts src/shared/remote-runtime-shared-control-socket-generation.test.ts src/shared/remote-runtime-client-error-classification.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/rpc/methods/terminal-create-idempotency.test.ts src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-retry.test.ts src/renderer/src/store/slices/direct-ssh-pane-detach-ledger.test.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-retry.test.ts src/renderer/src/store/slices/direct-ssh-pane-detach-ledger.test.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/remote-workspace-snapshot-arrival-coordinator.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/remote-workspace-cache.test.ts src/main/ipc/remote-workspace.test.ts src/main/ipc/remote-workspace-patch-queue.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/repos-remote.test.ts src/main/ipc/ssh.test.ts src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts src/main/ipc/worktrees-lineage-hydration.test.ts src/main/runtime/public-ssh-state.test.ts src/main/ssh/ssh-connection-manager.test.ts src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-provider-authority.test.ts src/preload/ssh-authority-forwarding.test.ts src/renderer/src/runtime/runtime-client-events.test.ts src/renderer/src/runtime/runtime-environment-ssh-state.test.ts src/shared/ssh-retained-payload-admission.test.ts src/shared/ssh-types.test.ts --reporter=dot",
"pnpm exec electron-vite build --mode e2e",
"pnpm run build:web-from-renderer",
@@ -10376,6 +10692,7 @@
"src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts",
"src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts",
"src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts",
"src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts",
"src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts",
"src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts",
"src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts",
@@ -10406,6 +10723,11 @@
"src/renderer/src/hooks/direct-ssh-host-hydration.test.ts",
"src/renderer/src/hooks/direct-ssh-state-routing.test.ts",
"src/renderer/src/hooks/remote-workspace-target-sync.test.ts",
"src/renderer/src/hooks/remote-workspace-snapshot-arrival-coordinator.test.ts",
"src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx",
"src/main/ipc/remote-workspace-cache.test.ts",
"src/main/ipc/remote-workspace.test.ts",
"src/main/ipc/remote-workspace-patch-queue.test.ts",
"src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts",
"src/main/ipc/repos-remote.test.ts",
"src/main/ipc/ssh.test.ts",
@@ -10466,7 +10788,17 @@
"assertions": [
"cold restored-terminal subscription failure retries and resumes snapshot, output, and input without a fatal error",
"a canonical close before subscription readiness opens exactly one replacement stream without surfacing a fatal error",
"cached terminal pixels remain disconnected until authoritative replay completes"
"cached terminal pixels remain disconnected until authoritative replay completes",
"authoritative current-stream replay clears every distinct error surfaced by that transport"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts",
"assertions": [
"only the active pane's errors render and matching-pane recovery preserves sibling and unrelated messages",
"individual messages and the joined surface remain within 24 lines and 4,000 characters",
"a distinct error storm retains only the newest eight messages for one pane while whole-message dedup remains intact",
"repeated split-pane close churn releases every closed pane id while preserving the live sibling"
]
},
{
@@ -10717,7 +11049,47 @@
"assertions": [
"snapshot hydration preserves newer local recovery and keeps imported PTY ids retryable until exact-attempt transport acknowledgement",
"stale operation tokens cannot apply an older snapshot over current authority",
"target snapshot projection and persisted-terminal reconnect are host-qualified and preserve sibling SSH, local, WSL, and runtime state"
"target snapshot projection and persisted-terminal reconnect are host-qualified and preserve sibling SSH, local, WSL, and runtime state",
"placement and readiness bursts retain one latest-only listener or timer per target, apply only the newest arrival, and release immediately on stop",
"a superseded revision-zero push cannot publish stale status"
]
},
{
"file": "src/renderer/src/hooks/remote-workspace-snapshot-arrival-coordinator.test.ts",
"assertions": [
"completed target generations are released across repeated target churn",
"a superseded operation that ignores abort cannot become current through generation reuse after a newer operation completes"
]
},
{
"file": "src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx",
"assertions": [
"an unsolicited snapshot synchronously revokes its target's upload authority before preparation awaits",
"a session write while snapshot capture is pending cannot replace the host's newly cached tabs",
"an upload captured before snapshot arrival is revalidated after its pending local write and cannot overwrite the incoming revision",
"same-lineage local writes remain uploadable when an earlier overlapping result advances the renderer's acknowledged revision",
"a transient unavailable result retains its observed host token and revision so the next local edit retries"
]
},
{
"file": "src/main/ipc/remote-workspace-cache.test.ts",
"assertions": [
"contiguous same-client patch revisions retain queued renderer bases until a host observation replaces the lineage",
"snapshot eviction removes its upload-revision authority with the same bounded cache entry"
]
},
{
"file": "src/main/ipc/remote-workspace.test.ts",
"assertions": [
"replace-session upload admission requires an explicit applied revision for every hydrated target"
]
},
{
"file": "src/main/ipc/remote-workspace-patch-queue.test.ts",
"assertions": [
"token A is rejected without a patch when a different same-revision host observation arrives before admission or while the upload waits in the same-target queue",
"an evicted token A fails closed after a same-revision refetch stamps a new observation token",
"same-client notification-before-response ordering preserves token A and overlapping queued writes at relay bases 7 then 8"
]
},
{
@@ -10768,6 +11140,51 @@
}
],
"evidenceRuns": [
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/remote-workspace-snapshot-arrival-coordinator.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 0.1,
"summary": "Two coordinator tests passed, proving completed target entries are released under repeated churn and generations remain monotonic until every superseded operation settles, preventing ABA re-admission."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/remote-workspace-cache.test.ts src/main/ipc/remote-workspace.test.ts src/main/ipc/remote-workspace-patch-queue.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 4.04,
"summary": "Three main-process remote-workspace files and 17 tests passed, including token-A rejection after same-revision observations before admission and while queued, eviction/refetch fail-closed behavior, same-client overlap at bases 7 then 8, revision-zero compatibility, reset-relay fallback, and bounded cache lineage."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/remote-workspace-snapshot-arrival-coordinator.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx --reporter=dot",
"result": "passed",
"durationSeconds": 10.44,
"summary": "Three renderer remote-workspace files and 28 tests passed, including acknowledged observation-token propagation, incoming-lineage upload cancellation, same-lineage overlapping upload continuation, transient-unavailable retry authority, and latest-only snapshot arrival fencing."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 4.3,
"summary": "Two focused files and 26 tests passed, including pane-scoped rendering and recovery, sibling-error retention, split-close pane-id churn cleanup, current-transport multi-error clearing, dismissal re-admission, multi-line deduplication, and 8-message/24-line/4,000-character bounds."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-retry.test.ts src/renderer/src/store/slices/direct-ssh-pane-detach-ledger.test.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 10.7,
"summary": "Fourteen direct-SSH files and 155 tests passed, including 32-arrival placement and readiness bursts with one listener/timer maximum, immediate supersession and stop cleanup, latest-only hydration, stale-push fencing, pending-capture and in-flight-write upload exclusion, and existing retry, authority, hydration, split-pane, and detach contracts."
},
{
"date": "2026-08-23",
"runner": "local",
@@ -10808,7 +11225,7 @@
"date": "2026-07-28",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-retry.test.ts src/renderer/src/store/slices/direct-ssh-pane-detach-ledger.test.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-retry.test.ts src/renderer/src/store/slices/direct-ssh-pane-detach-ledger.test.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/hooks/remote-workspace-target-sync.test.ts src/renderer/src/app-shell/remote-workspace-unplaced-upload-suppression.test.tsx src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 15.8,
"summary": "Twelve direct SSH files and 647 tests passed, including exact lease revalidation after asynchronous SSH preparation, primary-exit continuation gaps, pending-only and live null-PTY two-sided split-detach authority, delayed post-success sibling admission, stale-authority provider retirement, late ownership-provenance rejection, and deleted-tab ledger pruning."
@@ -10864,7 +11281,7 @@
},
"performanceBudget": {
"required": true,
"evidence": "Direct SSH terminal invalidation and retry each use one exact-target store publication and execute before provider discovery; another target's five occupied provider slots cannot delay terminal finalization. Each split-pane completion or delayed mount adds constant-time pending/live lease lookups and no provider listing, polling, subprocess, cross-tab scan, or new fanout; two mounted leaves still perform exactly their two existing provider operations. The scheduler caps locally unsettled detected-worktree work at five with a two-call late-work allowance. Remote-runtime recovery allocates at most one backoff timer and one one-minute deadline per detached pane, then stops all PTY retry work until explicit user action. Timers, accepted-snapshot listeners, stale streams, and pane UI entries are released on health, cutoff, rebind, removal, detach, or destroy; ten-cycle tests prove one unsubscribe per epoch. Common terminal input/output paths add only constant-time state checks. No live large-terminal-map direct SSH timing is claimed."
"evidence": "Direct SSH terminal invalidation and retry each use one exact-target store publication and execute before provider discovery; another target's five occupied provider slots cannot delay terminal finalization. Each split-pane completion or delayed mount adds constant-time pending/live lease lookups and no provider listing, polling, subprocess, cross-tab scan, or new fanout; two mounted leaves still perform exactly their two existing provider operations. Delayed snapshot placement retains at most one 10-second store subscription and timer per target; supersession and stop abort both immediately, remove the listener, and clear the timer. The scheduler caps locally unsettled detected-worktree work at five with a two-call late-work allowance. Remote-runtime recovery allocates at most one backoff timer and one one-minute deadline per detached pane, then stops all PTY retry work until explicit user action. Each transport suppresses at most eight distinct error strings and emits at most eight clear callbacks on recovery; each live pane retains at most eight messages, and every message and joined display is clipped to 24 lines and 4,000 characters. Explicit split close and pane replacement release their entries. This adds no provider request, polling, subprocess, or terminal-output work. Timers, accepted-snapshot listeners, stale streams, and pane UI entries are released on health, cutoff, rebind, removal, detach, or destroy; ten-cycle tests prove one unsubscribe per epoch. Common terminal input/output paths add only bounded state checks. No live large-terminal-map direct SSH timing is claimed."
},
"promotionCriteria": [
"Use deterministic fake providers for failure and unknown-liveness cases.",
@@ -12517,7 +12934,7 @@
"large persisted mismatches route in 50-row pages with coalesced per-mailbox wakes",
"Run pointers pin the existing mixed-version-compatible --run identity",
"direct and Dispatch mail remain durable without unpinned synthetic terminal turns",
"a delayed pointer submit cannot press Enter after the agent becomes working",
"a staged pointer submits once when Codex or Claude becomes working and still withholds Enter for permission",
"an explicit check releases its staged pointer reservation without redrive",
"accepted pointer text is durably deduplicated before delayed Enter and provider Enter refusal",
"a known PTY exit releases staged rows for the replacement process",
@@ -13836,14 +14253,14 @@
"providers": ["local", "daemon", "ssh"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "ssh"],
"coverageNotes": "Deterministic renderer and IPC-transport tests prove count and text ceilings, oldest-reply shedding, explicit query-reply source routing, ordinary-input preservation, one-reply-per-write delivery for OSC, DA1, and CPR replies, real xterm OSC reply generation, drain-failure containment, and clear/reuse generation fencing. Remote-runtime tests preserve separate query-reply writes across pending input, async validation, and viewport-claim buffering. Host-contract tests prove a later DA1/CPR reply cannot overtake a deferred OSC reply, including a coalesced legacy-client payload. Live macOS Electron tests cover local PTY OSC replies and interactive typing; a macOS-hosted Docker OpenSSH test proves an upstream-node-pty Linux relay keeps OSC/DA1 replies out of the next fish child's stdin. No live daemon, paired-runtime, WSL, physical Linux/Windows client, or binary mixed-version run is registered.",
"coverageNotes": "Deterministic renderer and IPC-transport tests prove count and text ceilings, oldest-reply shedding, explicit query-reply source routing, ordinary-input preservation, acknowledged-write FIFO barriers, single-worker drain reentrancy, one-reply-per-write delivery for OSC, DA1, and CPR replies, real xterm OSC reply generation, drain-failure containment, teardown settlement, and clear/reuse generation fencing. Remote-runtime tests preserve separate query-reply writes across pending input, async validation, and viewport-claim buffering. Host-contract tests prove a later DA1/CPR reply cannot overtake a deferred OSC reply, including a coalesced legacy-client payload. Live macOS Electron tests cover local PTY OSC replies and interactive typing; a macOS-hosted Docker OpenSSH test proves an upstream-node-pty Linux relay keeps OSC/DA1 replies out of the next fish child's stdin. No live daemon, paired-runtime, WSL, physical Linux/Windows client, or binary mixed-version run is registered.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/13137",
"https://github.com/stablyai/orca/issues/7329",
"https://github.com/stablyai/orca/issues/13892"
],
"invariant": "The desktop PTY input queue retains at most 64 explicitly sourced pending terminal query replies and 4096 UTF-16 code units. Every retained reply reaches the provider as one atomic write, and the host writes each reply the moment it accepts it, so replies reach the PTY in the order they were produced with no queue that could reorder them. A reply's own echo is contained on the output side by projecting its known echo shapes; the ESC-initial verbatim shape is matched only when complete, never held as a partial, so a query torn at its own ESC is still answered. Overflow removes only the oldest query replies, never ordinary input except the documented modified-F3/CPR byte collision, and drain failures cannot clear a newer queue generation.",
"oracle": "Synchronously enqueue separate 10,000-entry OSC and DA1 reply floods before the scheduled drain and assert that only the initial immediate reply and newest 64 pending replies are written, each as one provider write, before a trailing keystroke. At the host boundary, defer an OSC reply and assert that separate or legacy-coalesced DA1/CPR replies flush after it in observed query order. At the remote-runtime boundary, preserve separate writes around pending ordinary input, async validation, and viewport-claim buffering. Repeat behind 10,000 ordinary inputs and exercise the text ceiling, real xterm generation, provider-write failure, rejected yield, and clear/reuse generation fencing.",
"invariant": "The desktop PTY input queue retains at most 64 explicitly sourced pending terminal query replies and 4096 UTF-16 code units. Every retained reply reaches the provider as one atomic write, and ordinary, acknowledged, and reply input share one invocation-ordered FIFO so no later write overtakes an acknowledged write. Reentrant write callbacks cannot start a second drain worker or strand input admitted after clear/reuse. A reply's own echo is contained on the output side by projecting its known echo shapes; the ESC-initial verbatim shape is matched only when complete, never held as a partial, so a query torn at its own ESC is still answered. Overflow removes only the oldest query replies, never ordinary input except the documented modified-F3/CPR byte collision, and failure or teardown cannot clear a newer queue generation or strand an acknowledged promise.",
"oracle": "Synchronously enqueue separate 10,000-entry OSC and DA1 reply floods before the scheduled drain and assert that only the initial immediate reply and newest 64 pending replies are written, each as one provider write, before a trailing keystroke. Stall an acknowledged write between earlier and later ordinary/reply input, then require teardown to settle it and same-id reuse to receive no stale tail. Reenter the queue synchronously from an acknowledged write with both enqueue and clear/reuse, requiring one drain and fresh input delivery only after the stale acknowledged write settles false. At the host boundary, defer an OSC reply and assert that separate or legacy-coalesced DA1/CPR replies flush after it in observed query order. At the remote-runtime boundary, preserve separate writes around pending ordinary input, async validation, and viewport-claim buffering. Repeat behind 10,000 ordinary inputs and exercise the text ceiling, real xterm generation, provider-write failure, rejected yield, and clear/reuse generation fencing.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/shared/terminal-query-reply.test.ts src/shared/pty-startup-ingress-live-query-reply.test.ts src/shared/pty-startup-reply-echo-shapes.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-query-reply-immediate.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-coalescing.test.ts",
@@ -13875,14 +14292,20 @@
"real xterm OSC 10/11 query handlers remain subject to the same retention ceiling",
"retained OSC, DA1, and CPR replies stay one provider write each and both OSC and DA1 floods remain bounded",
"provider-write and yield failures settle without unhandled rejection, repeated same-generation admission, or stale-generation clearing",
"clear releases saturated reply accounting and fences in-flight validation before later input"
"clear releases saturated reply accounting and fences in-flight validation before later input",
"acknowledged input is serialized between earlier and later ordinary/reply writes",
"clear settles active and pending acknowledged input before same-id queue reuse",
"reentrant enqueue cannot start a second drain worker past an unacknowledged write",
"reentrant clear captures the stale cancellation and continues draining fresh input"
]
},
{
"file": "src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts",
"assertions": [
"sendInputImmediate applies the reply ceiling while sendInput preserves a reply-shaped ordinary payload in exact IPC write order",
"a thrown renderer write triggers one owning-transport recovery callback and rejects later input in that queue generation"
"a thrown renderer write triggers one owning-transport recovery callback and rejects later input in that queue generation",
"live and preconnect acknowledged writes remain FIFO barriers for later ordinary and immediate input",
"disconnect, detach, and natural exit settle acknowledged writes and fence same-id stale chunks"
]
},
{
@@ -13938,13 +14361,13 @@
],
"evidenceRuns": [
{
"date": "2026-08-25",
"date": "2026-08-30",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/shared/terminal-query-reply.test.ts src/shared/pty-startup-ingress-live-query-reply.test.ts src/shared/pty-startup-reply-echo-shapes.test.ts",
"result": "passed",
"durationSeconds": 1.05,
"summary": "Five files and 92 tests passed, including the live-pty echo-shape transcript, including explicit IPC reply-source routing, the 10,000-reply count ceiling, text ceiling, 10,000-entry ordinary backlog preservation, real xterm OSC query flood, single-shot drain-failure recovery, one-reply-per-write echo containment, and clear/reuse generation fencing."
"durationSeconds": 15,
"summary": "Five files and 149 tests passed in 15.16 seconds, including explicit IPC reply-source routing, the 10,000-reply count and text ceilings, 10,000-entry ordinary backlog preservation, acknowledged-write FIFO barriers, synchronous reentrancy fencing, prompt teardown settlement, same-id generation fencing, real xterm OSC query floods, single-shot drain-failure recovery, and one-reply-per-write echo containment."
},
{
"date": "2026-08-09",
@@ -13993,7 +14416,7 @@
},
"redGreenEvidence": {
"status": "partial",
"evidence": "Without the branch's admission cap, the 10,000-reply fixture writes all replies before the trailing keystroke. Before the source-routing repair, the queue had no API capable of distinguishing reply-shaped ordinary input; before failure containment, a thrown provider write rejected waitForDrain and Vitest recorded an unhandled rejection; the first containment pass retried a failed generation and invoked recovery twice; before generation fencing, a rejected stale yield cleared fresh input. No saved intentional-break artifact is attached yet."
"evidence": "Without the branch's admission cap, the 10,000-reply fixture writes all replies before the trailing keystroke. Before the source-routing repair, the queue had no API capable of distinguishing reply-shaped ordinary input; before failure containment, a thrown provider write rejected waitForDrain and Vitest recorded an unhandled rejection; the first containment pass retried a failed generation and invoked recovery twice; before generation fencing, a rejected stale yield cleared fresh input. Before reentrancy fencing, a synchronous accepted-write callback started a second drain that falsely accepted the pending write; clear/reuse also captured the replacement generation's cancellation and stranded fresh input. No saved intentional-break artifact is attached yet."
},
"performanceBudget": {
"required": true,
@@ -17006,29 +17429,66 @@
"protection": "partial",
"owner": "terminal-runtime-graph",
"layer": "renderer-runtime-graph-and-terminal-stream",
"surfaces": ["host terminal cold park", "paired remote viewer", "multi-pane runtime graph"],
"surfaces": [
"host terminal cold park",
"parked CLI terminal split",
"paired remote viewer",
"multi-pane runtime graph",
"headless runtime restart",
"pending terminal handle recovery"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["local-daemon", "ssh-daemon", "paired-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local-daemon", "paired-runtime"],
"coverageNotes": "Deterministic policy and multiplex tests separate renderer parking from authoritative stream liveness, while runtime-graph tests cover exact parked leaf, pane runtime ID, title, multi-pane active-leaf, and disposal behavior. One headed paired journey proves input and echoed output while the host pane remains cold-parked.",
"coverageNotes": "Deterministic policy and multiplex tests separate renderer parking from authoritative stream liveness, while runtime-graph tests cover exact parked leaf, pane runtime ID, title, multi-pane active-leaf, disposal behavior, and queued split routing to an exact cold-parked tab. A real Electron journey runs the shipped dev CLI against the app's isolated profile, proves the exact parked target alone remounts under a bounded lease, completes before the historical 10-second timeout without stealing active tab or focus, re-parks, then reveals two stable-identity panes with independent keyboard/output round trips. Focused mirror-recovery tests preserve a verified binding across pending-handle snapshots, quarantine positive PTY-identity mismatches, accept authoritative ready replacement or removal, and fence recovery to the pairing revision. Daemon attach-only tests prove a replacement runtime re-registers one active session and one history writer. Headed cold-park and headless runtime-restart paired journeys prove rendered output, input, resize convergence, process identity, authoritative close, checkpoint continuity, and post-restart history append against real daemon PTYs.",
"motivatingLinks": [
"https://linear.app/stably/issue/STA-2854",
"https://github.com/stablyai/orca/pull/15514"
"https://github.com/stablyai/orca/pull/15514",
"https://github.com/stablyai/orca/issues/12115",
"https://github.com/stablyai/orca/issues/17297"
],
"invariant": "Cold parking a host renderer pane never retires its live PTY's runtime-graph leaf or interrupts a paired subscriber's stream, input, reconnect, pane identity, or multi-pane routing; the leaf retires only when exact PTY ownership ends.",
"oracle": "Cold-park a host-owned pane while a paired client actively views it, require the host manager to unmount, then type through the client and require the same PTY to receive and echo the token without any disconnected sample. Separately publish multi-pane parked leaves with exact pane runtime IDs and require per-PTY disposal to remove only the retired leaf.",
"invariant": "Cold parking or restarting a host renderer/runtime never retires a live PTY's runtime-graph leaf, erases a paired viewer's last verified leaf binding merely because the host temporarily publishes pending-handle, or drops the surviving PTY's history writer. A split request for a cold-parked tab replays against its stable source leaf after exact-tab remount without switching workspaces or focusing the tab. The paired stream, input, resize, pane identity, multi-pane routing, checkpoint, and output log converge to the authoritative ready handle; the binding clears only after positive replacement, mismatch, or two consecutive authoritative absence observations without an intervening ready surface.",
"oracle": "Cold-park a host-owned pane while a paired client actively views it, require the host manager to unmount, then type through the client and require the same PTY to receive and echo the token without any disconnected sample. Restart a real pinned-port headless runtime while its daemon PTY survives, observe a pending-handle snapshot, require the viewer binding never to become empty, then require rendered output, focused keyboard input, a post-recovery window resize, the pre-restart checkpoint, and post-restart output-log append to reach the same PTY and process before authoritative close clears the binding. Invoke the real `orca-dev terminal split` against a cold-parked renderer-owned tab, require only that tab to acquire and release the existing background-mount lease, complete before the historical timeout, preserve the active worktree/tab/focus, then reveal two rendered panes and type through each while retaining the stable source leaf and PTY. Deterministically require synchronous stable-leaf replay after lifecycle registration and fail closed for a missing or stale leaf. Separately publish multi-pane parked leaves with exact pane runtime IDs and require per-PTY disposal to remove only the retired leaf. Require two consecutive successful authoritative inventory absences before removing a missing surface, and reset that confirmation after every healthy ready frame.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts tests/e2e/host-cold-park-remote-subscriber.unit.test.ts --reporter=dot",
"pnpm exec playwright test tests/e2e/host-parked-pane-remote-viewer.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts src/main/runtime/orca-runtime-terminal-split-authority.test.ts src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts tests/e2e/host-cold-park-remote-subscriber.unit.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts src/main/daemon/daemon-pty-adapter-session-adoption.test.ts src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts src/main/daemon/daemon-pty-adapter-history-recovery.test.ts --reporter=dot",
"pnpm exec playwright test tests/e2e/host-parked-pane-remote-viewer.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"pnpm exec playwright test tests/e2e/terminal-parked-cli-split.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"pnpm exec playwright test tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
],
"testFiles": [
"src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts",
"src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts",
"src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts",
"src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts",
"src/main/runtime/orca-runtime-terminal-split-authority.test.ts",
"src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts",
"src/renderer/src/runtime/web-runtime-session.test.ts",
"src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts",
"src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts",
"src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts",
"src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts",
"src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts",
"src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts",
"src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts",
"src/main/daemon/daemon-pty-adapter-session-adoption.test.ts",
"src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts",
"src/main/daemon/daemon-pty-adapter-history-recovery.test.ts",
"tests/e2e/host-cold-park-remote-subscriber.unit.test.ts",
"tests/e2e/host-parked-pane-remote-viewer.spec.ts"
"tests/e2e/host-parked-pane-remote-viewer.spec.ts",
"tests/e2e/terminal-parked-cli-split.spec.ts",
"tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts"
],
"assertionRefs": [
{
"file": "src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts",
"assertions": [
"a cold-parked split request acquires only the exact tab's background-mount lease and replays synchronously on lifecycle registration",
"a reminted numeric pane ID resolves through the stable source leaf while a missing or stale leaf fails closed",
"expired, canceled, closed-tab, and overflowed requests release their bounded queue and lease state"
]
},
{
"file": "src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts",
"assertions": [
@@ -17047,45 +17507,128 @@
"assertions": [
"a cold-parked host pane carries a complete paired-client input and echo round trip"
]
},
{
"file": "tests/e2e/terminal-parked-cli-split.spec.ts",
"assertions": [
"the shipped dev CLI completes an exact parked-tab split before the historical 10-second timeout",
"only the parked target mounts under the bounded lease while the decoy worktree, tab, active leaf, and keyboard focus stay unchanged",
"the target re-parks, then reveals two visible panes with the stable source leaf, PTY, and handle intact",
"both the source and created pane accept scoped keyboard input and render independently generated output"
]
},
{
"file": "src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts",
"assertions": [
"a present pending surface cannot erase a verified binding when its prior handle is absent or still host-owned",
"ready replacement, positive PTY mismatch, orphan adoption, and authoritative removal remain distinct evidence states"
]
},
{
"file": "src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts",
"assertions": [
"stable unsupported adoption failures dedupe an identical claim frame",
"transport and queue-overload adoption failures retry on the same semantic snapshot",
"malformed adoption results retain the verified surface without unbounded RPC churn"
]
},
{
"file": "src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts",
"assertions": [
"one successful authoritative absence retains the last verified surface",
"two consecutive authoritative absences remove the missing surface",
"a healthy ready frame resets the absence confirmation"
]
},
{
"file": "src/renderer/src/runtime/web-runtime-session.test.ts",
"assertions": [
"eager worktree-switch and post-create snapshots pass through the same pairing-fenced recovery seam"
]
},
{
"file": "src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts",
"assertions": [
"attach-only adoption registers one active session and exactly one history writer",
"a pre-restart checkpoint remains readable and later output appends to the same output log"
]
},
{
"file": "tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts",
"assertions": [
"a real headless runtime restart never empties the viewer's verified binding while the daemon PTY survives",
"rendered output, focused input, and resized grid delivery converge after the replacement runtime is ready",
"the original fixture process survives while its pre-restart checkpoint and post-restart history append remain durable",
"authoritative tab close removes the viewer binding"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-23",
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts tests/e2e/host-cold-park-remote-subscriber.unit.test.ts --reporter=dot",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts src/main/runtime/orca-runtime-terminal-split-authority.test.ts src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts tests/e2e/host-cold-park-remote-subscriber.unit.test.ts --reporter=dot",
"durationSeconds": 8,
"summary": "Three focused files passed 64 watcher, multi-pane runtime-graph, cold-park policy, and authoritative-stream tests."
"summary": "Fifteen deterministic files passed watcher, exact cold-parked split routing, multi-pane graph, pending-binding retention, exact adoption, mismatch quarantine, consecutive-absence confirmation, queue/cache bound, revision-fence, and eager-refresh tests."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts src/main/daemon/daemon-pty-adapter-session-adoption.test.ts src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts src/main/daemon/daemon-pty-adapter-history-recovery.test.ts --reporter=dot",
"durationSeconds": 4,
"summary": "Four daemon files passed 99 attach-only, legacy/current protocol, history registration, checkpoint, and append-continuity tests."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec playwright test tests/e2e/terminal-parked-cli-split.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"durationSeconds": 24,
"summary": "A real isolated-profile Electron run passed in 23.0 seconds: the shipped CLI split the exact cold-parked renderer tab before its historical timeout, only that tab mounted and released its background lease, the decoy selection and focus remained unchanged, and both stable-identity panes completed rendered keyboard/output round trips after reveal."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec playwright test tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"durationSeconds": 36,
"summary": "A clean exact-source build passed the real pinned-port serve replacement in 27.4 seconds: the daemon PTY and fixture PID survived, the renderer observed pending-handle without an empty or divergent binding, output/input/resize converged, history checkpoint and output.log continuity held, and authoritative close removed the surface. An exact-bundle repeat also passed."
}
],
"runtimeBudget": {
"p95Seconds": 180,
"scope": "focused deterministic contracts plus one serial headed paired cold-park journey"
"p95Seconds": 360,
"scope": "focused deterministic contracts plus serial headed cold-park, parked CLI split, and headless runtime-restart paired journeys"
},
"flakeHistory": {
"status": "not-started",
"evidence": "The existing sentinel is newly source-routed for PR collection; route-specific CI history has not started."
"evidence": "The cold-park, parked CLI split, and restart sentinels are source-routed for PR collection; route-specific CI history has not started."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "On the affected STA-2854 path, cold parking removed the runtime-graph leaf and the paired stream disconnected; retaining the exact live parked watcher leaf keeps every sampled client phase connected and completes input plus echo."
"evidence": "On the affected STA-2854 path, cold parking removed the runtime-graph leaf and the paired stream disconnected; retaining the exact live parked watcher leaf keeps every sampled client phase connected and completes input plus echo. Issue #17297 records the same-handle Windows A/B: the parked split failed after 11 seconds while an explicit switch made it immediate; the fixed macOS Electron journey completes before that 10-second internal timeout without a switch, preserves the decoy context, and renders independent round trips in both panes. Before the #12115 fix, the real restart delivered pending-handle and the viewer's tab identity rotated away from its retained layout binding; output still painted but post-restart keyboard output never reached output.log because attach-only adoption registered no active history writer. The fixed journey keeps one identity and appends history, while deterministic tests separate ready replacement, positive mismatch, safe retention, exact adoption, and removal."
},
"performanceBudget": {
"required": true,
"evidence": "Publication reads the existing bounded parked-watcher registry and adds no timer, polling, provider scan, subprocess, or wire field; the 90-second live timeline is selected only by exact parking and graph authorities."
"evidence": "Publication reads the existing bounded parked-watcher registry. Parked split routing retains at most 32 requests and 32 exact-tab leases, uses the existing three-second background mount window, cancels on close, and adds no polling or provider scan. Pending recovery is event-driven, capped at 64 exact candidates, and pairing-revision fenced. Each environment/revision/worktree key runs one active request plus only the latest trailing frame; the global RPC lane permits at most 4 active and 64 waiting calls; dispositions are capped at 512 fingerprints; ready/removal frames overtake degraded recovery. A repeated semantic snapshot version can retry a transient adoption failure without polling. The path adds no wire field, and its live journeys are selected only by exact parking, split-routing, recovery, stream, and daemon-attach authorities."
},
"promotionCriteria": [
"Collect 100 consecutive routed CI passes or 14 days without an unexplained flake.",
"Collect Linux, Windows, WSL, and physical SSH cold-park evidence.",
"Keep exact per-leaf disposal, multi-pane identity, and full client round-trip assertions green."
"Collect Linux, Windows, WSL, and physical SSH cold-park/restart evidence.",
"Keep exact per-leaf disposal, multi-pane identity, pending-binding retention, and full client round-trip assertions green."
],
"knownGaps": [
"The live paired cold-park journey is macOS-only and uses a local daemon PTY.",
"Physical SSH, WSL, Linux, Windows, and host-restart cold-park journeys are not recorded."
"The live paired cold-park, parked CLI split, and runtime-restart journeys are macOS-only and use a local daemon PTY; the #17297 report itself was Windows 11.",
"Physical SSH, WSL, Linux, and Windows cold-park/restart journeys are not recorded.",
"Persisted-empty viewer rows recover through an exact host pane resolution when the host exposes a valid UUID leaf and matching connected PTY; legacy/malformed layouts or hosts without terminal.resolvePane remain pending until a newer authoritative snapshot.",
"A normal daemon-preserving runtime restart keeps the terminal handle stable; deterministic unit coverage, not the live restart, proves convergence to a distinct ready replacement handle and rejection of recycled-handle PTY mismatch."
],
"demotionRule": "Demote if a live parked PTY loses its exact graph leaf, a retired PTY remains published, multi-pane identity drifts, or the routed client round trip flakes without a diagnosed cause."
"demotionRule": "Demote if a live parked/restarting PTY loses its exact graph leaf or verified viewer binding, a retired PTY remains published, multi-pane identity drifts, or either routed client round trip flakes without a diagnosed cause."
},
{
"id": "agent-browser.owner-boundary-cleanup",
@@ -0,0 +1,14 @@
# Files allowed to construct a `ws` server that binds a port without pinning `host`.
#
# `ws` accepts `{ port }` alone and silently binds the wildcard address. A server
# reached over 127.0.0.1 must pin `host: '127.0.0.1'`, or a foreign loopback
# listener can hold the same port and answer in its place -- which is how
# relay-control-client.test.ts came to fail with a real HTTP 401 in a test that
# was simulating silence.
#
# This list only shrinks. Adding a line also requires raising the pin in
# websocket-server-loopback-bind.test.ts, which is deliberate friction.
# Deliberate, not drift: this mock is dialled by a phone on the LAN, so it has to
# be reachable on a real interface. A loopback bind would make it unreachable.
mobile/scripts/mock-server.ts
@@ -95,6 +95,66 @@ describe('benchmark artifact comparison', () => {
})
})
it('compares terminal split headline metrics in milliseconds', () => {
const dir = makeTempDir()
const baselinePath = writeArtifact(dir, 'split-baseline.json', {
label: 'split baseline',
headlineMs: {
shortcutToFocusP50: 284.2,
shortcutToFocusP95: 676.3
}
})
const candidatePath = writeArtifact(dir, 'split-candidate.json', {
label: 'split candidate',
headlineMs: {
shortcutToFocusP50: 12.7,
shortcutToFocusP95: 13.7
}
})
const comparison = comparePaths(baselinePath, candidatePath)
expect(comparison.baseline.kind).toBe('terminal-split-activation')
expect(comparison.metrics).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'shortcutToFocusP50',
unit: 'ms',
baseline: 284.2,
candidate: 12.7,
status: 'improved'
}),
expect.objectContaining({
key: 'shortcutToFocusP95',
unit: 'ms',
baseline: 676.3,
candidate: 13.7,
status: 'improved'
})
])
)
})
it('rejects invalid benchmark artifacts before comparing partial metrics', () => {
const dir = makeTempDir()
const baselinePath = writeArtifact(dir, 'split-invalid.json', {
label: 'invalid split',
status: 'failed',
valid: false,
headlineMs: { shortcutToFocusP50: 0 }
})
const candidatePath = writeArtifact(dir, 'split-valid.json', {
label: 'valid split',
status: 'passed',
valid: true,
headlineMs: { shortcutToFocusP50: 10 }
})
expect(() => comparePaths(baselinePath, candidatePath)).toThrow(
'split-invalid.json: benchmark artifact is marked invalid'
)
})
it('compares numeric Playwright annotation metrics and omits metadata fields', () => {
const dir = makeTempDir()
const baselinePath = writeArtifact(dir, 'baseline-playwright.json', {
+204
View File
@@ -0,0 +1,204 @@
/**
* Read the top-level option keys of a call's object-literal argument out of raw
* source text.
*
* Text rather than an AST because typescript@7 no longer ships the classic
* compiler API and every installed parser is a transitive dependency. The
* tradeoff is handled by refusing to guess: any shape this cannot read comes
* back as `unreadable` with a reason, and callers must treat that as a failure
* rather than as an absence of keys.
*/
export type CallOptionKeys =
| { readonly readable: true; readonly keys: readonly string[] }
| { readonly readable: false; readonly reason: string }
type ScanState = 'code' | 'line' | 'block' | 'single' | 'double' | 'template'
function closesString(state: ScanState, current: string): boolean {
return (
(state === 'single' && current === "'") ||
(state === 'double' && current === '"') ||
(state === 'template' && current === '`')
)
}
function opensNonCode(current: string, next: string | undefined): ScanState | null {
if (current === '/' && next === '/') {
return 'line'
}
if (current === '/' && next === '*') {
return 'block'
}
if (current === "'") {
return 'single'
}
if (current === '"') {
return 'double'
}
if (current === '`') {
return 'template'
}
return null
}
/**
* Text between an open paren and its match, tracking strings and comments so a
* brace inside either cannot unbalance the count. Null when it never closes.
*/
function balancedArguments(text: string, openIndex: number): string | null {
let depth = 0
let state: ScanState = 'code'
for (let index = openIndex; index < text.length; index++) {
const current = text[index]
const next = text[index + 1]
if (state === 'code') {
const opened = opensNonCode(current, next)
if (opened) {
state = opened
if (opened === 'line' || opened === 'block') {
index++
}
} else if (current === '(' || current === '{' || current === '[') {
depth++
} else if (current === ')' || current === '}' || current === ']') {
depth--
if (depth === 0) {
return text.slice(openIndex + 1, index)
}
if (depth < 0) {
return null
}
}
continue
}
if (state === 'line') {
if (current === '\n') {
state = 'code'
}
continue
}
if (state === 'block') {
if (current === '*' && next === '/') {
state = 'code'
index++
}
continue
}
if (current === '\\') {
index++
continue
}
// Brace tracking inside `${}` would need its own depth; templates never
// appear as options, so report one as unreadable instead of guessing.
if (state === 'template' && current === '$' && next === '{') {
return null
}
if (closesString(state, current)) {
state = 'code'
}
}
return null
}
/** Keys at depth 0 of an object literal body, with anything non-identifier kept verbatim. */
function objectLiteralKeys(body: string): string[] {
const keys: string[] = []
let depth = 0
let state: ScanState = 'code'
let inValue = false
let token = ''
const flush = (): void => {
const name = token.trim()
token = ''
if (name && depth === 0) {
keys.push(name)
}
}
for (let index = 0; index < body.length; index++) {
const current = body[index]
const next = body[index + 1]
if (state === 'code') {
const opened = opensNonCode(current, next)
if (opened) {
state = opened
if (opened === 'line' || opened === 'block') {
index++
}
} else if (current === '(' || current === '{' || current === '[') {
depth++
if (!inValue) {
token += current
}
} else if (current === ')' || current === '}' || current === ']') {
depth--
if (!inValue) {
token += current
}
} else if (current === ':' && depth === 0 && !inValue) {
flush()
inValue = true
} else if (current === ',' && depth === 0) {
// A shorthand or a spread ends here having never seen a colon.
if (inValue) {
inValue = false
token = ''
} else {
flush()
}
} else if (!inValue) {
token += current
}
continue
}
if (state === 'line') {
if (current === '\n') {
state = 'code'
}
continue
}
if (state === 'block') {
if (current === '*' && next === '/') {
state = 'code'
index++
}
continue
}
if (current === '\\') {
index++
continue
}
if (closesString(state, current)) {
state = 'code'
}
}
if (!inValue) {
flush()
}
return keys
}
/**
* Option keys of the call whose argument list opens at `parenIndex`, or the
* reason the shape could not be read. Spreads and computed keys land in the
* latter: either can carry a key this would otherwise report as absent.
*/
export function readCallOptionKeys(text: string, parenIndex: number): CallOptionKeys {
const args = balancedArguments(text, parenIndex)
if (args === null) {
return { readable: false, reason: 'argument list never closes' }
}
if (!args.trim()) {
return { readable: false, reason: 'called with no options argument' }
}
const trimmed = args.trim()
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
return { readable: false, reason: 'options are not an object literal' }
}
const keys = objectLiteralKeys(trimmed.slice(1, -1))
const unreadable = keys.find((key) => !/^[A-Za-z_$][\w$]*$/.test(key))
if (unreadable !== undefined) {
return { readable: false, reason: `unreadable option key \`${unreadable}\`` }
}
return { readable: true, keys }
}
+12 -3
View File
@@ -4,6 +4,7 @@ import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.mjs'
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
const SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?)$/
export const OXLINT_SCANS = [
@@ -24,6 +25,13 @@ export const OXLINT_SCANS = [
]
const SUPPRESSED_REACT_DOCTOR_DIAGNOSTICS = new Map([
[
'react-doctor(no-adjust-state-on-prop-change)',
new Set([
'src/renderer/src/components/use-task-page-github-issue-draft.ts',
'src/renderer/src/components/use-task-page-jira-creation-state.ts'
])
],
[
'react-doctor(no-derived-state-effect)',
new Set([
@@ -278,11 +286,12 @@ function isSuppressedDiagnostic(diagnostic, root) {
}
function runOxlintScan(root, scan, files) {
const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const result = spawnSync(pnpm, ['exec', 'oxlint', ...scan.args, '--format', 'json', ...files], {
const { command, prefixArgs } = resolveOxlintInvocation(root)
const result = spawnSync(command, [...prefixArgs, ...scan.args, '--format', 'json', ...files], {
cwd: root,
encoding: 'utf8',
maxBuffer: 128 * 1024 * 1024
maxBuffer: 128 * 1024 * 1024,
windowsHide: true
})
if (result.error) {
throw result.error
+17 -3
View File
@@ -1,16 +1,30 @@
import { spawnSync } from 'node:child_process'
import process from 'node:process'
import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.mjs'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
const requestedBase =
process.argv.slice(2).find((argument) => argument !== '--') ??
process.env.ORCA_CODE_QUALITY_BASE ??
'origin/main'
const base = resolvePullRequestDiffBase(process.cwd(), requestedBase)
const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
// Why validate rather than trust: `base` arrives from argv or the environment and
// below it can reach cmd.exe unquoted, because resolvePnpmCliInvocation still
// falls back to a shell when it cannot find a directly spawnable pnpm. It accepts
// SHAs, tags, ref paths and the ^ ~ .. suffixes -- not reflog syntax like HEAD@{1},
// because braces stay out of anything bound for cmd.exe. The error names the base.
const GIT_REVISION = /^[A-Za-z0-9._/@^~-]+$/
if (!GIT_REVISION.test(base)) {
throw new Error(`Refusing to pass an unsafe diff base to pnpm: ${base}`)
}
// Why the shim and not a direct binary: `dlx` fetches react-doctor on demand, so
// only the pnpm CLI can run it. resolvePnpmCliInvocation prefers whatever
// npm_execpath exposes -- pnpm 12's own pnpm.exe, spawned with no shell.
const { command, prefixArgs, shell } = resolvePnpmCliInvocation()
const result = spawnSync(
pnpm,
command,
[
...prefixArgs,
'dlx',
'react-doctor@0.9.1',
'.',
@@ -26,7 +40,7 @@ const result = spawnSync(
'--blocking',
'error'
],
{ stdio: 'inherit' }
{ stdio: 'inherit', shell, windowsHide: true }
)
if (result.error) {
@@ -0,0 +1,29 @@
import { spawnSync } from 'node:child_process'
import path from 'node:path'
import process from 'node:process'
import { describe, expect, it } from 'vitest'
const repoRoot = path.resolve(import.meta.dirname, '..', '..')
const script = path.join(repoRoot, 'config', 'scripts', 'check-react-doctor-changed.mjs')
function runWithBase(base) {
return spawnSync(process.execPath, [script, base], {
cwd: repoRoot,
encoding: 'utf8',
windowsHide: true
})
}
describe('check-react-doctor-changed diff base', () => {
// The pnpm invocation can still fall back to a shell, so an unvalidated base
// would reach cmd.exe unquoted. Rejection has to happen before the spawn.
it.each(['main & calc', 'main | whoami', 'main"x', '%PATH%', 'main $(id)'])(
'refuses %j',
(base) => {
const result = runWithBase(base)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('Refusing to pass an unsafe diff base')
}
)
})
+230
View File
@@ -0,0 +1,230 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
// Ratchet gate for the `@ts-nocheck` directive.
//
// TypeScript only honours `@ts-nocheck` in a comment before the first statement, and
// once present it disables type checking for the ENTIRE file. PR #17605 split a single
// 43,928-line class into ~172 modules whose linear mixin-inheritance chain cannot yet
// express forward references, so each carries a grandfathered `@ts-nocheck` header. This
// check freezes that set (the baseline) and fails CI when a NEW file adds the directive —
// the existing files are grandfathered; new ones must fix their types instead. The
// baseline may only shrink.
const BASELINE_PATH = 'config/ts-nocheck-baseline.txt'
// These two files legitimately contain the directive text as data (regex, fixtures),
// so scanning them would self-flag. The ratchet does not police itself.
const SELF_FILES = new Set([
'config/scripts/check-ts-nocheck-ratchet.mjs',
'config/scripts/check-ts-nocheck-ratchet.test.mjs'
])
// True if `@ts-nocheck` appears in a comment before the first statement, matching the
// TypeScript rule. Limitation: only the leading run of blank lines / line comments /
// block comments at the top of the file is scanned, so a directive-looking string deeper
// in a block comment that itself starts at the top is still checked — but anything after
// real code (or inside a string literal, which never opens the leading comment run) is not.
export function hasTsNoCheck(sourceText) {
let i = 0
const n = sourceText.length
while (i < n) {
const rest = sourceText.slice(i)
const blank = /^[ \t]*\r?\n/.exec(rest)
if (blank) {
i += blank[0].length
continue
}
if (rest.startsWith('//')) {
const end = sourceText.indexOf('\n', i)
const line = end === -1 ? sourceText.slice(i) : sourceText.slice(i, end)
if (/^\/\/\s*@ts-nocheck\b/.test(line)) {
return true
}
i = end === -1 ? n : end + 1
continue
}
if (rest.startsWith('/*')) {
const end = sourceText.indexOf('*/', i + 2)
const block = end === -1 ? sourceText.slice(i) : sourceText.slice(i, end + 2)
if (/^\/\*\s*@ts-nocheck\b/.test(block)) {
return true
}
i = end === -1 ? n : end + 2
continue
}
break
}
return false
}
export function parseBaseline(text) {
return new Set(
text
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('#'))
)
}
export function diffBaseline(current, baseline) {
const cur = new Set(current)
const base = baseline instanceof Set ? baseline : new Set(baseline)
const added = [...cur].filter((e) => !base.has(e)).sort()
const stale = [...base].filter((e) => !cur.has(e)).sort()
return { added, stale }
}
// Collect every currently tracked file that carries a `@ts-nocheck` header.
export function collectCurrentTsNoCheckFiles(root = process.cwd()) {
const tracked = execFileSync('git', ['ls-files', '*.ts', '*.tsx', '*.mts', '*.cts'], {
cwd: root,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024
})
.split('\n')
.filter(Boolean)
.filter((f) => !SELF_FILES.has(f))
const entries = []
for (const rel of tracked) {
let src
try {
src = fs.readFileSync(path.join(root, rel), 'utf8')
} catch {
continue
}
if (hasTsNoCheck(src)) {
entries.push(rel)
}
}
return entries.sort()
}
function printAddedFailure(added) {
for (const entry of added) {
console.error(`::error::New @ts-nocheck not allowed: ${entry}`)
}
console.error('')
console.error('╭────────────────────────────────────────────────────────────────────────────╮')
console.error('│ ❌ ts-nocheck ratchet failed — a NEW file adds a @ts-nocheck directive. │')
console.error('╰────────────────────────────────────────────────────────────────────────────╯')
console.error('')
console.error(` ${added.length} file(s) newly add a \`@ts-nocheck\` header:`)
console.error('')
for (const entry of added) {
console.error(` • ${entry}`)
}
console.error('')
console.error(' `@ts-nocheck` disables ALL type checking for the whole file, not just one line.')
console.error(
' The grandfathered entries exist only because the split runtime mixin chain cannot'
)
console.error(' express forward references yet — that is not a general license to suppress.')
console.error('')
console.error(' ✅ Fix it: fix the types instead of suppressing the whole file.')
console.error('')
console.error(' (If you are intentionally, with reviewer sign-off, adding an unavoidable')
console.error(` exception, add the exact line(s) above to ${BASELINE_PATH}.)`)
console.error('')
}
function printStaleFailure(stale) {
for (const entry of stale) {
console.error(`::error::Stale ts-nocheck baseline entry (prune it): ${entry}`)
}
console.error('')
console.error('╭────────────────────────────────────────────────────────────────────────────╮')
console.error('│ ⚠️ ts-nocheck baseline is out of date — nice work removing a suppression! │')
console.error('╰────────────────────────────────────────────────────────────────────────────╯')
console.error('')
console.error(` ${stale.length} baseline entr(y/ies) no longer have a @ts-nocheck directive.`)
console.error(
' The baseline may only shrink, so these must be removed to keep re-adding blocked:'
)
console.error('')
for (const entry of stale) {
console.error(` • ${entry}`)
}
console.error('')
console.error(` ✅ Fix it (one command): pnpm check:ts-nocheck-ratchet --prune`)
console.error('')
}
export function main(root = process.cwd()) {
const baselineFile = path.join(root, BASELINE_PATH)
if (!fs.existsSync(baselineFile)) {
console.error(
`::error::Missing ${BASELINE_PATH}. Generate it with: node config/scripts/check-ts-nocheck-ratchet.mjs --init`
)
return 1
}
const baseline = parseBaseline(fs.readFileSync(baselineFile, 'utf8'))
const current = collectCurrentTsNoCheckFiles(root)
const { added, stale } = diffBaseline(current, baseline)
if (added.length > 0) {
printAddedFailure(added)
if (stale.length > 0) {
console.error(
` (Also: ${stale.length} stale baseline entr(y/ies) can be pruned — see below.)`
)
printStaleFailure(stale)
}
return 1
}
if (stale.length > 0) {
printStaleFailure(stale)
return 1
}
console.log(
`ts-nocheck ratchet OK — ${current.length} grandfathered file(s), no new suppressions.`
)
return 0
}
function writeBaseline(root, entries) {
const header = [
'# Files currently allowed to carry a `@ts-nocheck` header.',
'# This is a RATCHET: the list may only SHRINK. These exist only because the split',
'# runtime mixin chain cannot express forward references yet — do NOT add entries to',
'# get CI green; fix the types instead.',
'# Regenerate/prune: pnpm check:ts-nocheck-ratchet --prune (removes stale entries only)',
''
].join('\n')
fs.writeFileSync(path.join(root, BASELINE_PATH), `${header}${entries.join('\n')}\n`)
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const root = process.cwd()
const arg = process.argv[2]
if (arg === '--init') {
// One-time bootstrap: capture the current @ts-nocheck set as the baseline.
const entries = collectCurrentTsNoCheckFiles(root)
writeBaseline(root, entries)
console.log(`Wrote ${BASELINE_PATH} with ${entries.length} entries.`)
process.exit(0)
}
if (arg === '--prune') {
// Remove baseline entries whose @ts-nocheck is gone (shrink only; never adds).
const current = new Set(collectCurrentTsNoCheckFiles(root))
const baseline = parseBaseline(fs.readFileSync(path.join(root, BASELINE_PATH), 'utf8'))
const kept = [...baseline].filter((e) => current.has(e)).sort()
const newlyAdded = [...current].filter((e) => !baseline.has(e))
writeBaseline(root, kept)
console.log(
`Pruned baseline to ${kept.length} entries (removed ${baseline.size - kept.length}).`
)
if (newlyAdded.length > 0) {
console.error(
`::error::--prune does not add entries; ${newlyAdded.length} new suppression(s) remain — fix those files' types.`
)
process.exit(1)
}
process.exit(0)
}
process.exit(main(root))
}
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest'
import { diffBaseline, hasTsNoCheck, parseBaseline } from './check-ts-nocheck-ratchet.mjs'
describe('hasTsNoCheck', () => {
it('detects a line-comment form', () => {
expect(hasTsNoCheck('// @ts-nocheck\nexport const a = 1\n')).toBe(true)
})
it('detects a block-comment form', () => {
expect(hasTsNoCheck('/* @ts-nocheck */\nexport const a = 1\n')).toBe(true)
})
it('detects the no-space form', () => {
expect(hasTsNoCheck('//@ts-nocheck\nexport const a = 1\n')).toBe(true)
})
it('detects a directive with a -- Why reason', () => {
expect(
hasTsNoCheck(
'// @ts-nocheck -- Why: mechanically split, covered by AST tests.\nimport x from "y"\n'
)
).toBe(true)
})
it('allows blank lines and other leading comments before the directive', () => {
const src =
'\n// Copyright notice.\n\n/* another leading comment */\n// @ts-nocheck\nexport const a = 1\n'
expect(hasTsNoCheck(src)).toBe(true)
})
it('does not match once a statement has started', () => {
const src = 'export const a = 1\n// @ts-nocheck\n'
expect(hasTsNoCheck(src)).toBe(false)
})
it('does not match inside a string literal', () => {
const src = 'export const a = "// @ts-nocheck"\n'
expect(hasTsNoCheck(src)).toBe(false)
})
it('returns false for ordinary source', () => {
expect(hasTsNoCheck('export function f() {\n return 42\n}\n')).toBe(false)
})
})
describe('parseBaseline', () => {
it('drops comments and blank lines', () => {
const b = parseBaseline('# header\n\nsrc/a.ts\nsrc/b.ts\n')
expect(b).toEqual(new Set(['src/a.ts', 'src/b.ts']))
})
})
describe('diffBaseline', () => {
it('reports added and stale entries', () => {
const { added, stale } = diffBaseline(
['src/b.ts', 'src/c.ts'],
new Set(['src/a.ts', 'src/b.ts'])
)
expect(added).toEqual(['src/c.ts']) // new suppression
expect(stale).toEqual(['src/a.ts']) // suppression removed
})
it('is clean when current matches baseline', () => {
const { added, stale } = diffBaseline(['src/a.ts'], new Set(['src/a.ts']))
expect(added).toEqual([])
expect(stale).toEqual([])
})
})
+13 -1
View File
@@ -74,6 +74,9 @@ export function readBenchmarkArtifact(path) {
}
export function normalizeBenchmarkArtifact(path, artifact = readBenchmarkArtifact(path)) {
if (artifact?.valid === false || artifact?.status === 'failed') {
throw new Error(`${path}: benchmark artifact is marked invalid`)
}
if (artifact?.summaryMedianMs != null) {
return normalizeNumericObject(path, artifact, 'startup', artifact.summaryMedianMs, () => 'ms')
}
@@ -82,6 +85,15 @@ export function normalizeBenchmarkArtifact(path, artifact = readBenchmarkArtifac
key.endsWith('Count') || key.endsWith('After') ? 'count' : 'ms'
)
}
if (artifact?.headlineMs != null) {
return normalizeNumericObject(
path,
artifact,
'terminal-split-activation',
artifact.headlineMs,
() => 'ms'
)
}
if (artifact?.suites != null) {
return normalizePlaywrightArtifact(path, artifact)
}
@@ -89,7 +101,7 @@ export function normalizeBenchmarkArtifact(path, artifact = readBenchmarkArtifac
return normalizeSummaryArtifact(path, artifact)
}
throw new Error(
`${path}: unsupported benchmark artifact; expected summaryMedianMs, summaryMedian, Playwright suites, or top-level summary`
`${path}: unsupported benchmark artifact; expected summaryMedianMs, summaryMedian, headlineMs, Playwright suites, or top-level summary`
)
}
@@ -12,11 +12,33 @@ const stubPath = join(projectDir, 'skills', 'computer-use', 'SKILL.md')
const bundledGuide = BUNDLED_SKILL_GUIDES.find((guide) => guide.name === 'computer-use')?.markdown
describe('computer-use skill guidance', () => {
it('keeps discovery scoped to desktop control and out of the embedded browser', () => {
const frontmatter = /^---\n([\s\S]*?)\n---\n/u.exec(readFileSync(guidePath, 'utf8'))?.[1] ?? ''
const description = frontmatter.replace(/\s+/gu, ' ')
expect(description).toContain('OS/window-level inspection and input')
expect(description).toContain('external browser window')
expect(description).toContain("Do not use for Orca's embedded browser")
expect(description).toContain('page-only browser automation')
expect(description).toContain("`orca-cli` for Orca's embedded pages")
expect(description).toContain(
'page-automation tool such as Playwright or CDP for external pages'
)
expect(description).not.toContain('read Slack')
expect(description).not.toContain('get app state')
const orcaCli = readFileSync(join(projectDir, 'skill-guides', 'orca-cli.md'), 'utf8').replace(
/\s+/gu,
' '
)
expect(orcaCli).toContain('browser embedded inside the Orca app')
})
it('keeps web-app targeting on the computer-use surface', () => {
const skill = readFileSync(guidePath, 'utf8')
expect(skill).toContain('Use this skill for desktop UI through `orca computer`')
expect(skill).toContain('operate the desktop browser app/window that contains the page')
expect(skill).toContain('external desktop browser window that needs desktop-level control')
expect(skill).not.toContain('orca goto')
expect(skill).not.toContain('orca snapshot')
expect(skill).not.toContain('orca click')
@@ -1,3 +1,25 @@
import { execFileSync } from 'node:child_process'
/** Written once a bundle is fully built; its absence is what marks a build still in flight. */
export const DEV_BUNDLE_MARKER_FILENAME = 'orca-dev-electron-app.json'
export function getDevBundleProcessTable(execFile = execFileSync) {
// Not pgrep: macOS pgrep has no -a (a Linux procps extension) and silently prints bare PIDs,
// which reads as "nothing is running" and deletes a live bundle. -ww keeps the command column
// from being truncated. The raw text is searched directly; see isDevBundleInUse for why it is
// deliberately not parsed into paths.
try {
return execFile('/bin/ps', ['-Awwo', 'command='], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000
})
} catch {
// Treating a failure as "nothing live" would risk deleting a running bundle, so skip pruning.
return null
}
}
// Why this module exists: `out/electron-dev` accumulates one ~270MB copy of Electron.app per
// (branch title x Electron version x bundle layout). The runner only ever clears the directory it is
// about to rebuild, so siblings from renamed branches and past upgrades are never reclaimed --
@@ -0,0 +1,116 @@
import { existsSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { basename } from 'node:path'
import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const electronBuilderConfig = require('../electron-builder.config.cjs')
const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx']
// The exact shape app-builder-lib's APP_ASSOCIATE emits: a write to the DEFAULT ("")
// value of Software\Classes\.<ext>. Additive `WriteRegNone ...\OpenWithProgids` must not
// match, or the guard below would be unfalsifiable.
const DEFAULT_HANDLER_WRITE = /WriteRegStr\s+SHELL_CONTEXT\s+"Software\\Classes\\\.[a-z]+"\s+""/i
// The hooks file documents the forbidden line in prose, so match executable script only.
const stripNsisCommentLines = (source) =>
source
.split('\n')
.filter((line) => !/^\s*[;#]/.test(line))
.join('\n')
const readInstallerHooks = () => readFile(electronBuilderConfig.nsis.include, 'utf8')
describe('electron-builder markdown file associations', () => {
// Why: any top-level (or `win.`) fileAssociations entry makes app-builder-lib's NSIS
// packager emit `!insertmacro APP_ASSOCIATE`, whose first line writes that DEFAULT value
// — silently taking .md from whichever editor owns it, for every existing user on their
// next UPDATE, with APP_UNASSOCIATE never restoring it. `rank: 'Alternate'` cannot
// prevent this; it is LSHandlerRank and applies to macOS only. So the mac block must
// stay under `mac.` — hoisting it up "to share it with Windows" is what this test blocks.
it('never claims the Windows default markdown handler', () => {
expect(electronBuilderConfig.fileAssociations).toBeUndefined()
expect(electronBuilderConfig.win?.fileAssociations).toBeUndefined()
})
it('joins the macOS Open With list for every markdown extension without owning it', () => {
const associations = electronBuilderConfig.mac.fileAssociations
// One entry per extension: an array `ext` would break the Linux packager's `*.${ext}` glob.
expect([...associations].map((association) => association.ext).sort()).toEqual(
[...MARKDOWN_EXTENSIONS].sort()
)
for (const association of associations) {
expect(association).toMatchObject({ role: 'Editor', rank: 'Alternate' })
}
})
// Why mimeTypes and not linux.fileAssociations: shared-mime-info already maps markdown to
// text/markdown, so the desktop entry only adds a handler and mimeapps.list keeps owning
// the default. A fileAssociations entry would ship a redundant glob override instead.
it('reuses the existing shared-mime-info markdown type on Linux', () => {
expect(electronBuilderConfig.linux.mimeTypes).toContain('text/markdown')
expect(electronBuilderConfig.linux.fileAssociations).toBeUndefined()
})
it('points the single NSIS include at the installer hooks file on disk', () => {
const includePath = electronBuilderConfig.nsis.include
expect(existsSync(includePath)).toBe(true)
expect(basename(includePath)).toBe('orca-installer-hooks.nsh')
})
// Guard for the guard: proves DEFAULT_HANDLER_WRITE really matches a takeover line, so
// the assertion below is a live check rather than a regex that can never fire.
it('recognizes an APP_ASSOCIATE-style default-handler write', () => {
for (const takeover of [
' WriteRegStr SHELL_CONTEXT "Software\\Classes\\.md" "" "Orca.Markdown"',
'WriteRegStr SHELL_CONTEXT "Software\\Classes\\.markdown" "" "$0"'
]) {
expect(takeover).toMatch(DEFAULT_HANDLER_WRITE)
}
expect(
'WriteRegNone SHELL_CONTEXT "Software\\Classes\\.md\\OpenWithProgids" "Orca.Markdown"'
).not.toMatch(DEFAULT_HANDLER_WRITE)
// Comment stripping must drop prose that quotes the bad line without swallowing a real
// one that happens to carry a trailing comment.
const stripped = stripNsisCommentLines(
[
'; WriteRegStr SHELL_CONTEXT "Software\\Classes\\.md" "" "<ProgID>"',
' WriteRegStr SHELL_CONTEXT "Software\\Classes\\.md" "" "$0" ; oops'
].join('\n')
)
expect(stripped.split('\n')).toHaveLength(1)
expect(stripped).toMatch(DEFAULT_HANDLER_WRITE)
})
it('registers Windows markdown Open With additively, never as the default', async () => {
const hooks = await readInstallerHooks()
expect(stripNsisCommentLines(hooks)).not.toMatch(DEFAULT_HANDLER_WRITE)
// The additive hint that puts Orca in Explorer's "Open with" list.
expect(hooks).toMatch(
/WriteRegNone\s+SHELL_CONTEXT\s+"Software\\Classes\\\$\{EXT\}\\OpenWithProgids"/
)
expect(hooks).toMatch(/!macro\s+ORCA_REGISTER_MARKDOWN_OPEN_WITH\s+EXT/)
for (const ext of MARKDOWN_EXTENSIONS) {
expect(hooks).toContain(`ORCA_REGISTER_MARKDOWN_OPEN_WITH ".${ext}"`)
expect(hooks).toContain(`ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".${ext}"`)
}
expect(hooks).toMatch(/!macro\s+customInstall\b/)
expect(hooks).toMatch(/!macro\s+customUnInstall\b/)
})
// Why: this include was renamed from daemon-host-uninstall.nsh to carry the markdown
// hooks too. electron-builder allows only one include, so a merge that drops the daemon
// sweep would silently orphan a running orca-terminal-daemon.exe on every uninstall.
it('keeps the daemon-host uninstall sweep across the include rename', async () => {
const hooks = await readInstallerHooks()
expect(hooks).toContain('orca-terminal-daemon.exe')
expect(hooks).toContain('$LOCALAPPDATA\\Orca\\daemon-host')
// Without this guard, uninstallOldVersion would kill the daemon on every update —
// defeating the relocation that keeps terminals alive across updates.
expect(hooks).toMatch(/\$\{ifNot\}\s+\$\{isUpdated\}/)
})
})
+16
View File
@@ -0,0 +1,16 @@
/** Where the Electron executable sits inside a `dist` tree, relative to it. */
export function getElectronPlatformPath(targetPlatform) {
switch (targetPlatform) {
case 'mas':
case 'darwin':
return 'Electron.app/Contents/MacOS/Electron'
case 'freebsd':
case 'openbsd':
case 'linux':
return 'electron'
case 'win32':
return 'electron.exe'
default:
throw new Error(`Electron builds are not available on platform: ${targetPlatform}`)
}
}
@@ -20,7 +20,7 @@ import { createRequire } from 'node:module'
import { electronViteConfig } from '../../electron.vite.config'
import { BOOTSTRAP_FATAL_EXIT_GUARD_KEY } from '../../src/main/startup/bootstrap-fatal-exit-guard'
const targetConfig = readFileSync('config/electron-vite-target.config.ts', 'utf8')
const targetConfig = readFileSync('config/electron-vite-target.config.cts', 'utf8')
const devRunner = readFileSync('config/scripts/run-electron-vite-dev.mjs', 'utf8')
type BootstrapProcessMock = EventEmitter & {
@@ -109,6 +109,41 @@ describe('bundled skill guide generator', () => {
expect(source).toContain('name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"')
})
it.skipIf(process.platform === 'win32')(
'resolves snapshot cleanup through Orca user-data precedence',
async () => {
const source = await readFile(
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
'utf8'
)
const assignment =
'orca_user_data_path="${ORCA_USER_DATA_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/orca}"'
expect(source).toContain(assignment)
const renderPath = async (env) =>
(
await execFileAsync(
'bash',
['-u', '-c', `${assignment}; printf '%s' "$orca_user_data_path"`],
{
env
}
)
).stdout
await expect(renderPath({ HOME: '/home/orca' })).resolves.toBe('/home/orca/.config/orca')
await expect(
renderPath({ HOME: '/home/orca', XDG_CONFIG_HOME: '/srv/config' })
).resolves.toBe('/srv/config/orca')
await expect(
renderPath({
HOME: '/home/orca',
XDG_CONFIG_HOME: '/srv/config',
ORCA_USER_DATA_PATH: '/var/lib/orca-custom'
})
).resolves.toBe('/var/lib/orca-custom')
}
)
it.skipIf(process.platform === 'win32')(
'keeps Vercel sandbox names valid while preserving the instance suffix',
async () => {
@@ -87,7 +87,7 @@ const parent = `${head}~1`
const CANDIDATES = [
'src/main/git/status.ts',
'src/shared/agent-hook-listener.ts',
'src/renderer/src/components/TaskPage.tsx'
'src/renderer/src/components/task-page/TaskPage.tsx'
]
const files = []
@@ -0,0 +1,210 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { copyScriptWithLocalModules } from './script-module-dependencies.mjs'
const sourceScriptPath = fileURLToPath(
new URL('./install-electron-package-binary.mjs', import.meta.url)
)
/** Matches the fake package version and the platform/arch runInstallScript installs for. */
export const sharedEntryName = '41.5.0-linux-x64'
export const sharedEntryNameFor = (version) => `${version}-linux-x64`
export function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-install-electron-'))
copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts'))
return projectDir
}
export function runInstallScript(projectDir, extraEnv = {}) {
return spawnSync(process.execPath, ['config/scripts/install-electron-package-binary.mjs'], {
cwd: projectDir,
encoding: 'utf8',
env: {
...process.env,
ELECTRON_CACHE: undefined,
ORCA_ELECTRON_PACKAGE_CACHE_ROOT: undefined,
npm_config_platform: 'linux',
npm_config_arch: 'x64',
ORCA_ELECTRON_PACKAGE_EXTRACTOR: join(projectDir, 'fake-extractor.cjs'),
...extraEnv
}
})
}
export function writeFakeElectronPackage(
projectDir,
{ lazyRequireMarker = null, version = '41.5.0' } = {}
) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(electronDir, { recursive: true })
writeFileSync(join(electronDir, 'package.json'), JSON.stringify({ name: 'electron', version }))
writeFileSync(join(electronDir, 'checksums.json'), '{}')
writeFileSync(
join(electronDir, 'index.js'),
`
const fs = require('node:fs')
const path = require('node:path')
${lazyRequireMarker ? `fs.writeFileSync(${JSON.stringify(lazyRequireMarker)}, 'required')` : ''}
const pathFile = path.join(__dirname, 'path.txt')
if (!fs.existsSync(pathFile)) {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
module.exports = path.join(__dirname, 'dist', fs.readFileSync(pathFile, 'utf8'))
`
)
}
export function writeFakeElectronDist(
projectDir,
{ version = 'v41.5.0', executableContents = '', pathContents } = {}
) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(join(electronDir, 'dist'), { recursive: true })
writeFileSync(join(electronDir, 'dist/version'), version)
writeFileSync(join(electronDir, 'dist/electron'), executableContents)
if (pathContents !== undefined) {
writeFileSync(join(electronDir, 'path.txt'), pathContents)
}
}
export function writeFakeElectronGet(
projectDir,
{
downloadNeverSettles = false,
downloadFailures = 0,
downloadErrorCode = 'ECONNRESET',
downloadHttpStatus = null
} = {}
) {
const getDir = join(projectDir, 'node_modules', 'electron', 'node_modules', '@electron', 'get')
mkdirSync(getDir, { recursive: true })
writeFileSync(
join(getDir, 'index.js'),
`
const { mkdirSync, writeFileSync, appendFileSync } = require('node:fs')
const { join } = require('node:path')
let downloadAttempt = 0
exports.downloadArtifact = async function downloadArtifact(details) {
downloadAttempt += 1
appendFileSync(
'electron-get.log',
'cacheRoot=' + details.cacheRoot + ' platform=' + details.platform + ' arch=' + details.arch + ' force=' + details.force + '\\n'
)
if (${JSON.stringify(downloadNeverSettles)}) {
return new Promise(() => {})
}
if (downloadAttempt <= ${JSON.stringify(downloadFailures)}) {
if (${JSON.stringify(downloadHttpStatus)} != null) {
const error = new Error('Response code ' + ${JSON.stringify(downloadHttpStatus)})
error.response = { status: ${JSON.stringify(downloadHttpStatus)} }
throw error
}
const cause = Object.assign(new Error('download failed'), {
code: ${JSON.stringify(downloadErrorCode)}
})
throw Object.assign(new TypeError('fetch failed'), { cause })
}
mkdirSync(details.cacheRoot, { recursive: true })
const artifactPath = join(details.cacheRoot, 'electron.zip')
writeFileSync(artifactPath, 'fake zip')
return artifactPath
}
`
)
}
export function writeFakeExtractor(projectDir, { createExecutable, version = '41.5.0' }) {
writeFileSync(
join(projectDir, 'fake-extractor.cjs'),
`
const { appendFileSync, mkdirSync, symlinkSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
const extractDir = process.argv[3]
appendFileSync(join(__dirname, 'fake-extractor.log'), extractDir + '\\n')
mkdirSync(join(extractDir, 'locales'), { recursive: true })
if (${JSON.stringify(createExecutable)}) {
writeFileSync(join(extractDir, 'electron'), '')
writeFileSync(join(extractDir, 'electron.exe'), '')
writeFileSync(join(extractDir, 'electron.d.ts'), 'replacement types')
writeFileSync(join(extractDir, 'version'), ${JSON.stringify(`v${version}`)})
if (process.platform !== 'win32') {
symlinkSync('version', join(extractDir, 'version-link'))
}
}
`
)
}
export function writeTypeDefPublishFailurePreload(projectDir) {
const preloadPath = join(projectDir, 'type-def-publish-failure.cjs')
writeFileSync(
preloadPath,
`
const fs = require('node:fs')
const { syncBuiltinESMExports } = require('node:module')
const { basename, dirname } = require('node:path')
const renameSync = fs.renameSync
fs.renameSync = (source, target) => {
if (basename(source) === 'electron.d.ts' && basename(dirname(source)) === 'dist') {
const error = new Error('injected Electron type definition publish failure')
error.code = 'EACCES'
throw error
}
return renameSync(source, target)
}
syncBuiltinESMExports()
`
)
return preloadPath
}
export function initGitRepo(projectDir) {
runGit(projectDir, ['init', '--quiet', '--initial-branch=main'])
runGit(projectDir, ['config', 'user.email', 'orca-test@example.com'])
runGit(projectDir, ['config', 'user.name', 'Orca Test'])
runGit(projectDir, ['commit', '--quiet', '--allow-empty', '-m', 'init'])
}
export function addSiblingWorktree(projectDir, siblingDir) {
runGit(projectDir, ['worktree', 'add', '--quiet', '-b', 'sibling', siblingDir])
copyScriptWithLocalModules(sourceScriptPath, join(siblingDir, 'config', 'scripts'))
return siblingDir
}
function runGit(projectDir, args) {
execFileSync('git', ['-C', projectDir, ...args], { stdio: 'ignore' })
}
export function sharedCacheRoot(repoDir) {
return join(repoDir, '.git', 'orca-cache', 'electron')
}
export function readSharedDistMarker(projectDir) {
try {
return readFileSync(join(projectDir, 'node_modules/electron/.orca-shared-dist'), 'utf8')
} catch {
return null
}
}
export function readExtractorCallCount(projectDir) {
try {
return readFileSync(join(projectDir, 'fake-extractor.log'), 'utf8').trim().split('\n').length
} catch {
return 0
}
}
export function writeNonDarwinPlatformPreload(projectDir) {
const preloadPath = join(projectDir, 'non-darwin-platform.cjs')
writeFileSync(
preloadPath,
`
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true })
`
)
return preloadPath
}
@@ -15,6 +15,14 @@ import { spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { platform as osPlatform, tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { getElectronPlatformPath } from './electron-platform-path.mjs'
import {
shareElectronDistFromCache,
hasAdoptedSharedElectronDist,
publishSharedElectronDist,
recordAdoptedSharedElectronDist,
resolveSharedElectronDistEntry
} from './shared-electron-dist-cache.mjs'
const projectDir = resolve(import.meta.dirname, '../..')
const electronPackageDir = resolve(projectDir, 'node_modules/electron')
@@ -54,7 +62,18 @@ try {
async function main() {
repairElectronPathFile()
const sharedEntry = resolveSharedElectronDistEntry({
repoRoot: projectDir,
electronPackageDir,
version: electronVersion,
targetPlatform,
targetArch
})
if (electronPackageIsUsable()) {
if (sharedEntry !== null && !hasAdoptedSharedElectronDist(sharedEntry)) {
shareExistingElectronDist(sharedEntry)
}
return
}
@@ -62,7 +81,7 @@ async function main() {
// Node. Install only Electron's npm package binary here; do not run the full
// Electron native-module rebuild path, which would undo the Node ABI rebuild.
console.log('[electron-package] Electron package binary is missing; running Electron install.')
await installElectronPackageBinary()
await installElectronPackageBinary(sharedEntry)
repairElectronPathFile()
@@ -122,8 +141,11 @@ function repairElectronPathFile() {
}
}
async function installElectronPackageBinary() {
async function installElectronPackageBinary(sharedEntry) {
const electronDistDir = resolve(electronPackageDir, 'dist')
if (sharedEntry !== null && adoptSharedElectronDist(sharedEntry, electronDistDir)) {
return
}
const tempDir = mkdtempSync(resolve(tmpdir(), 'orca-electron-'))
const persistentCacheRoot =
process.env.ORCA_ELECTRON_PACKAGE_CACHE_ROOT || process.env.ELECTRON_CACHE || null
@@ -158,11 +180,73 @@ async function installElectronPackageBinary() {
}
moveExtractedElectronDist(extractDir, electronDistDir)
if (sharedEntry !== null) {
publishElectronDistForSiblingWorktrees(sharedEntry, electronDistDir)
}
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
}
/**
* Point this worktree's dist at the copy its siblings already share, so the ~295MB tree costs one
* allocation per repository instead of one per worktree.
*
* Staged inside node_modules/electron on purpose: clonefile only shares blocks within a volume, and
* staging elsewhere would silently downgrade the publish rename to a cross-device byte copy.
*/
function adoptSharedElectronDist(sharedEntry, electronDistDir) {
const stageRoot = mkdtempSync(resolve(electronPackageDir, '.dist-clone-'))
try {
const stagePath = join(stageRoot, 'dist')
if (
!shareElectronDistFromCache(sharedEntry, stagePath, {
version: electronVersion,
platformPath
})
) {
return false
}
moveExtractedElectronDist(stagePath, electronDistDir)
recordAdoptedSharedElectronDist(sharedEntry, writeFileSync)
console.log(
`[electron-package] Shared Electron ${electronVersion} from ${sharedEntry.entryPath}`
)
return true
} catch (error) {
// The download path below is always a correct fallback, so sharing never fails an install.
console.warn(`[electron-package] Shared Electron dist unavailable: ${formatShareError(error)}`)
return false
} finally {
rmSync(stageRoot, { recursive: true, force: true })
}
}
/** An already-installed dist joins the cache: clone from it if it exists, seed it otherwise. */
function shareExistingElectronDist(sharedEntry) {
const electronDistDir = resolve(electronPackageDir, 'dist')
if (!adoptSharedElectronDist(sharedEntry, electronDistDir)) {
publishElectronDistForSiblingWorktrees(sharedEntry, electronDistDir)
}
}
function publishElectronDistForSiblingWorktrees(sharedEntry, electronDistDir) {
const published = publishSharedElectronDist(electronDistDir, sharedEntry, {
version: electronVersion,
platformPath
})
if (published) {
console.log(
`[electron-package] Published Electron ${electronVersion} to ${sharedEntry.entryPath}`
)
recordAdoptedSharedElectronDist(sharedEntry, writeFileSync)
}
}
function formatShareError(error) {
return error instanceof Error ? error.message : String(error)
}
async function downloadElectronArtifactWithRetry(downloadOptions, { cacheRootIsPersistent }) {
const retryDelays = getDownloadRetryDelays()
@@ -438,19 +522,3 @@ function getElectronTargetPlatform() {
function getElectronTargetArch() {
return process.env.ELECTRON_INSTALL_ARCH || process.env.npm_config_arch || process.arch
}
function getElectronPlatformPath(targetPlatform) {
switch (targetPlatform) {
case 'mas':
case 'darwin':
return 'Electron.app/Contents/MacOS/Electron'
case 'freebsd':
case 'openbsd':
case 'linux':
return 'electron'
case 'win32':
return 'electron.exe'
default:
throw new Error(`Electron builds are not available on platform: ${targetPlatform}`)
}
}
@@ -1,22 +1,32 @@
import {
copyFileSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const sourceScriptPath = fileURLToPath(
new URL('./install-electron-package-binary.mjs', import.meta.url)
)
import {
addSiblingWorktree,
initGitRepo,
mkTempProject,
readExtractorCallCount,
readSharedDistMarker,
runInstallScript,
sharedCacheRoot,
sharedEntryName,
sharedEntryNameFor,
writeFakeElectronDist,
writeFakeElectronGet,
writeFakeElectronPackage,
writeFakeExtractor,
writeNonDarwinPlatformPreload,
writeTypeDefPublishFailurePreload
} from './install-electron-package-binary-test-fixtures.mjs'
describe('install-electron-package-binary', () => {
it('installs Electron from an isolated cache and repairs path.txt', () => {
@@ -250,7 +260,9 @@ describe('install-electron-package-binary', () => {
expect(result.status, result.stderr).toBe(0)
expect(existsSync(join(cacheRoot, 'preserved.marker'))).toBe(true)
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8').trim().split('\n')).toHaveLength(2)
expect(
readFileSync(join(projectDir, 'electron-get.log'), 'utf8').trim().split('\n')
).toHaveLength(2)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@@ -401,6 +413,199 @@ describe('install-electron-package-binary', () => {
}
})
// The shared cache is macOS-only: it exists to avoid a second copy via APFS clonefile.
it('publishes a shared Electron dist entry after a fresh download', () => {
const projectDir = mkTempProject()
try {
initGitRepo(projectDir)
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
const result = runInstallScript(projectDir, { CI: '' })
const entryPath = join(sharedCacheRoot(projectDir), sharedEntryName)
expect(result.status, result.stderr).toBe(0)
expect(lstatSync(entryPath).isDirectory()).toBe(true)
expect(lstatSync(entryPath).isSymbolicLink()).toBe(false)
expect(readFileSync(join(entryPath, 'version'), 'utf8')).toBe('v41.5.0')
expect(existsSync(join(entryPath, 'electron'))).toBe(true)
expect(readSharedDistMarker(projectDir)).toBe(sharedEntryName)
expect(result.stdout).toMatch(/Published Electron 41\.5\.0 to .*41\.5\.0-linux-x64$/m)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('shares the Electron dist into a sibling worktree without downloading', () => {
const projectDir = mkTempProject()
const siblingDir = `${projectDir}-sibling`
try {
initGitRepo(projectDir)
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
expect(runInstallScript(projectDir, { CI: '' }).status).toBe(0)
addSiblingWorktree(projectDir, siblingDir)
writeFakeElectronPackage(siblingDir)
writeFakeElectronGet(siblingDir)
writeFakeExtractor(siblingDir, { createExecutable: true })
const result = runInstallScript(siblingDir, { CI: '' })
const siblingDistDir = join(siblingDir, 'node_modules/electron/dist')
expect(result.status, result.stderr).toBe(0)
expect(readExtractorCallCount(siblingDir)).toBe(0)
expect(existsSync(join(siblingDir, 'electron-get.log'))).toBe(false)
expect(lstatSync(siblingDistDir).isDirectory()).toBe(true)
expect(lstatSync(siblingDistDir).isSymbolicLink()).toBe(false)
expect(readFileSync(join(siblingDistDir, 'version'), 'utf8')).toBe('v41.5.0')
expect(existsSync(join(siblingDistDir, 'electron'))).toBe(true)
expect(readFileSync(join(siblingDir, 'node_modules/electron/path.txt'), 'utf8')).toBe(
'electron'
)
expect(readSharedDistMarker(siblingDir)).toBe(sharedEntryName)
expect(result.stdout).toContain('Shared Electron 41.5.0 from')
} finally {
rmSync(siblingDir, { recursive: true, force: true })
rmSync(projectDir, { recursive: true, force: true })
}
})
it('publishes an already installed Electron dist that predates the shared cache', () => {
const projectDir = mkTempProject()
try {
initGitRepo(projectDir)
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
writeFakeElectronDist(projectDir, {
executableContents: 'existing executable',
pathContents: 'electron'
})
const result = runInstallScript(projectDir, { CI: '' })
const entryPath = join(sharedCacheRoot(projectDir), sharedEntryName)
const distDir = join(projectDir, 'node_modules/electron/dist')
expect(result.status, result.stderr).toBe(0)
expect(readExtractorCallCount(projectDir)).toBe(0)
expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false)
expect(readFileSync(join(entryPath, 'version'), 'utf8')).toBe('v41.5.0')
expect(readFileSync(join(entryPath, 'electron'), 'utf8')).toBe('existing executable')
expect(readSharedDistMarker(projectDir)).toBe(sharedEntryName)
expect(readFileSync(join(distDir, 'electron'), 'utf8')).toBe('existing executable')
expect(readFileSync(join(distDir, 'version'), 'utf8')).toBe('v41.5.0')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('replaces a corrupt shared Electron dist entry instead of re-downloading forever', () => {
const projectDir = mkTempProject()
try {
initGitRepo(projectDir)
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
const entryPath = join(sharedCacheRoot(projectDir), sharedEntryName)
mkdirSync(entryPath, { recursive: true })
writeFileSync(join(entryPath, 'version'), 'v40.0.0')
writeFileSync(join(entryPath, 'electron'), 'stale executable')
const result = runInstallScript(projectDir, { CI: '' })
const distDir = join(projectDir, 'node_modules/electron/dist')
expect(result.status, result.stderr).toBe(0)
expect(result.stderr).not.toContain('Failed to install Electron package binary')
expect(readExtractorCallCount(projectDir)).toBe(1)
expect(readFileSync(join(distDir, 'version'), 'utf8')).toBe('v41.5.0')
expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe(
'electron'
)
// Why not just fall back: an entry left corrupt makes every sibling worktree download again.
expect(readFileSync(join(entryPath, 'version'), 'utf8')).toBe('v41.5.0')
expect(readSharedDistMarker(projectDir)).toBe(sharedEntryName)
expect(readdirSync(sharedCacheRoot(projectDir))).toEqual([sharedEntryName])
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('hardlinks the shared Electron dist on a host without copy-on-write', () => {
const projectDir = mkTempProject()
try {
initGitRepo(projectDir)
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
const preloadPath = writeNonDarwinPlatformPreload(projectDir)
const nonDarwinEnv = {
CI: '',
NODE_OPTIONS: [process.env.NODE_OPTIONS, `--require=${preloadPath}`]
.filter(Boolean)
.join(' ')
}
const result = runInstallScript(projectDir, nonDarwinEnv)
const entryPath = join(sharedCacheRoot(projectDir), sharedEntryName)
const distDir = join(projectDir, 'node_modules/electron/dist')
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(join(distDir, 'version'), 'utf8')).toBe('v41.5.0')
expect(readFileSync(join(entryPath, 'version'), 'utf8')).toBe('v41.5.0')
// Why read-only: these are the same inodes, so an extract over dist would otherwise rewrite
// the cache and every sibling worktree at once.
expect(statSync(join(entryPath, 'electron')).mode & 0o222).toBe(0)
expect(statSync(join(entryPath, 'electron')).ino).toBe(
statSync(join(distDir, 'electron')).ino
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('gives an Electron upgrade its own cache entry and leaves the old one for other branches', () => {
const projectDir = mkTempProject()
try {
initGitRepo(projectDir)
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
expect(runInstallScript(projectDir, { CI: '' }).status).toBe(0)
expect(readSharedDistMarker(projectDir)).toBe(sharedEntryNameFor('41.5.0'))
// Upgrade the pinned Electron, exactly as a branch bumping the dependency would.
writeFakeElectronPackage(projectDir, { version: '42.0.0' })
writeFakeExtractor(projectDir, { createExecutable: true, version: '42.0.0' })
const upgraded = runInstallScript(projectDir, { CI: '' })
const cacheRoot = sharedCacheRoot(projectDir)
expect(upgraded.status, upgraded.stderr).toBe(0)
expect(readFileSync(join(projectDir, 'node_modules/electron/dist/version'), 'utf8')).toBe(
'v42.0.0'
)
expect(readSharedDistMarker(projectDir)).toBe(sharedEntryNameFor('42.0.0'))
// Why the old entry stays: sibling worktrees on the previous branch still share it.
expect(readdirSync(cacheRoot).sort()).toEqual([
sharedEntryNameFor('41.5.0'),
sharedEntryNameFor('42.0.0')
])
expect(readFileSync(join(cacheRoot, sharedEntryNameFor('41.5.0'), 'version'), 'utf8')).toBe(
'v41.5.0'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('does not exit successfully when Electron download never settles', () => {
const projectDir = mkTempProject()
@@ -419,155 +624,3 @@ describe('install-electron-package-binary', () => {
}
})
})
function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-install-electron-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
copyFileSync(
sourceScriptPath,
join(projectDir, 'config', 'scripts', 'install-electron-package-binary.mjs')
)
return projectDir
}
function runInstallScript(projectDir, extraEnv = {}) {
return spawnSync(process.execPath, ['config/scripts/install-electron-package-binary.mjs'], {
cwd: projectDir,
encoding: 'utf8',
env: {
...process.env,
ELECTRON_CACHE: undefined,
ORCA_ELECTRON_PACKAGE_CACHE_ROOT: undefined,
npm_config_platform: 'linux',
npm_config_arch: 'x64',
ORCA_ELECTRON_PACKAGE_EXTRACTOR: join(projectDir, 'fake-extractor.cjs'),
...extraEnv
}
})
}
function writeFakeElectronPackage(projectDir, { lazyRequireMarker = null } = {}) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(electronDir, { recursive: true })
writeFileSync(
join(electronDir, 'package.json'),
JSON.stringify({ name: 'electron', version: '41.5.0' })
)
writeFileSync(join(electronDir, 'checksums.json'), '{}')
writeFileSync(
join(electronDir, 'index.js'),
`
const fs = require('node:fs')
const path = require('node:path')
${lazyRequireMarker ? `fs.writeFileSync(${JSON.stringify(lazyRequireMarker)}, 'required')` : ''}
const pathFile = path.join(__dirname, 'path.txt')
if (!fs.existsSync(pathFile)) {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
module.exports = path.join(__dirname, 'dist', fs.readFileSync(pathFile, 'utf8'))
`
)
}
function writeFakeElectronDist(
projectDir,
{ version = 'v41.5.0', executableContents = '', pathContents } = {}
) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(join(electronDir, 'dist'), { recursive: true })
writeFileSync(join(electronDir, 'dist/version'), version)
writeFileSync(join(electronDir, 'dist/electron'), executableContents)
if (pathContents !== undefined) {
writeFileSync(join(electronDir, 'path.txt'), pathContents)
}
}
function writeFakeElectronGet(
projectDir,
{
downloadNeverSettles = false,
downloadFailures = 0,
downloadErrorCode = 'ECONNRESET',
downloadHttpStatus = null
} = {}
) {
const getDir = join(projectDir, 'node_modules', 'electron', 'node_modules', '@electron', 'get')
mkdirSync(getDir, { recursive: true })
writeFileSync(
join(getDir, 'index.js'),
`
const { mkdirSync, writeFileSync, appendFileSync } = require('node:fs')
const { join } = require('node:path')
let downloadAttempt = 0
exports.downloadArtifact = async function downloadArtifact(details) {
downloadAttempt += 1
appendFileSync(
'electron-get.log',
'cacheRoot=' + details.cacheRoot + ' platform=' + details.platform + ' arch=' + details.arch + ' force=' + details.force + '\\n'
)
if (${JSON.stringify(downloadNeverSettles)}) {
return new Promise(() => {})
}
if (downloadAttempt <= ${JSON.stringify(downloadFailures)}) {
if (${JSON.stringify(downloadHttpStatus)} != null) {
const error = new Error('Response code ' + ${JSON.stringify(downloadHttpStatus)})
error.response = { status: ${JSON.stringify(downloadHttpStatus)} }
throw error
}
const cause = Object.assign(new Error('download failed'), {
code: ${JSON.stringify(downloadErrorCode)}
})
throw Object.assign(new TypeError('fetch failed'), { cause })
}
mkdirSync(details.cacheRoot, { recursive: true })
const artifactPath = join(details.cacheRoot, 'electron.zip')
writeFileSync(artifactPath, 'fake zip')
return artifactPath
}
`
)
}
function writeFakeExtractor(projectDir, { createExecutable }) {
writeFileSync(
join(projectDir, 'fake-extractor.cjs'),
`
const { mkdirSync, symlinkSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
const extractDir = process.argv[3]
mkdirSync(join(extractDir, 'locales'), { recursive: true })
if (${JSON.stringify(createExecutable)}) {
writeFileSync(join(extractDir, 'electron'), '')
writeFileSync(join(extractDir, 'electron.exe'), '')
writeFileSync(join(extractDir, 'electron.d.ts'), 'replacement types')
writeFileSync(join(extractDir, 'version'), 'v41.5.0')
if (process.platform !== 'win32') {
symlinkSync('version', join(extractDir, 'version-link'))
}
}
`
)
}
function writeTypeDefPublishFailurePreload(projectDir) {
const preloadPath = join(projectDir, 'type-def-publish-failure.cjs')
writeFileSync(
preloadPath,
`
const fs = require('node:fs')
const { syncBuiltinESMExports } = require('node:module')
const { basename, dirname } = require('node:path')
const renameSync = fs.renameSync
fs.renameSync = (source, target) => {
if (basename(source) === 'electron.d.ts' && basename(dirname(source)) === 'dist') {
const error = new Error('injected Electron type definition publish failure')
error.code = 'EACCES'
throw error
}
return renameSync(source, target)
}
syncBuiltinESMExports()
`
)
return preloadPath
}
+5 -4
View File
@@ -1,5 +1,6 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
const SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?)$/
@@ -31,11 +32,11 @@ if (lintTargets.length === 0) {
process.exit(0)
}
const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const { command, prefixArgs } = resolveOxlintInvocation()
const result = spawnSync(
pnpm,
['exec', 'oxlint', '--config', 'config/oxlint-react-doctor.json', ...lintTargets],
{ stdio: 'inherit' }
command,
[...prefixArgs, '--config', 'config/oxlint-react-doctor.json', ...lintTargets],
{ stdio: 'inherit', windowsHide: true }
)
if (result.error) {
@@ -3,11 +3,10 @@ import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/mobile-pairing-qrcode-import.mjs')
const oxlintPath = path.resolve(
process.platform === 'win32' ? 'node_modules/.bin/oxlint.cmd' : 'node_modules/.bin/oxlint'
)
const oxlint = resolveOxlintInvocation()
function lintSource(source) {
const directory = mkdtempSync(path.join(tmpdir(), 'orca-qrcode-import-lint-'))
@@ -22,9 +21,11 @@ function lintSource(source) {
rules: { 'mobile-pairing/no-eager-qrcode-import': 'error' }
})
)
const result = spawnSync(oxlintPath, ['--config', configPath, '--format', 'json', sourcePath], {
encoding: 'utf8'
})
const result = spawnSync(
oxlint.command,
[...oxlint.prefixArgs, '--config', configPath, '--format', 'json', sourcePath],
{ encoding: 'utf8', windowsHide: true }
)
if (result.error) {
throw result.error
}
@@ -18,6 +18,24 @@ function readSkill(path = guidePath) {
}
describe('orca CLI skill guidance', () => {
it('keeps external browser routing at the OS/page boundary', () => {
const skill = readSkill(guidePath)
const description = skill.replace(/\s+/gu, ' ')
expect(description).toContain(
'Use Computer Use for external browser windows, webviews, or desktop UI only when the task requires OS/window-level control such as focus, menus, dialogs, coordinates, or screenshots.'
)
expect(description).toContain(
"`orca-cli` for Orca's embedded pages and a page-automation tool such as Playwright or CDP for external pages."
)
expect(skill).toContain(
'For external Chrome/Safari/webviews or Orca app chrome/settings, use the Computer Use skill/tool only when the task requires OS/window-level control'
)
expect(skill).toContain(
"Use `orca-cli` for Orca's embedded pages and a page-automation tool such as Playwright or CDP for external pages"
)
})
it('keeps independent worktree lineage separate from Git base selection', () => {
const skill = readSkill()
@@ -25,6 +25,17 @@ function getSection(markdown, heading) {
}
describe('orchestration skill guidance', () => {
it('keeps external browser routing at the OS/page boundary', () => {
const description = readFileSync(guidePath, 'utf8').replace(/\s+/gu, ' ')
expect(description).toContain(
"Use Computer Use for external browser windows, webviews, Orca app UI, or desktop UI outside Orca's embedded browser only when the task requires OS/window-level control such as focus, menus, dialogs, coordinates, or screenshots."
)
expect(description).toContain(
"`orca-cli` for Orca's embedded pages and a page-automation tool such as Playwright or CDP for external pages."
)
})
it('requires Orca runtime state before claiming a worker was orchestrated', () => {
const skill = readSkill()
const toolBoundary = getSection(skill, 'Tool Boundary')
+23
View File
@@ -0,0 +1,23 @@
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
// Why not `pnpm exec oxlint` / `node_modules/.bin/oxlint.cmd`: both land on a
// Windows .cmd shim, and Node >= 20 refuses to spawn one without `shell: true`
// (the CVE-2024-27980 mitigation), so every lint gate died with EINVAL before
// linting anything. Oxlint's bin is a plain Node script, so run it under this
// process's own node — no shim, no shell, no quoting question.
export function resolveOxlintInvocation(root = process.cwd()) {
const requireFromRoot = createRequire(path.join(root, 'package.json'))
// Oxlint's "exports" hides ./bin, so read the manifest and walk to its bin entry.
const manifestPath = requireFromRoot.resolve('oxlint/package.json')
const binField = requireFromRoot('oxlint/package.json').bin
const binEntry = typeof binField === 'string' ? binField : binField?.oxlint
if (!binEntry) {
throw new Error('oxlint package.json declares no "oxlint" bin entry.')
}
return {
command: process.execPath,
prefixArgs: [path.resolve(path.dirname(manifestPath), binEntry)]
}
}
@@ -0,0 +1,33 @@
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { describe, expect, it } from 'vitest'
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
const repoRoot = path.resolve(import.meta.dirname, '..', '..')
describe('resolveOxlintInvocation', () => {
it('runs oxlint under this process node, never through a shim', () => {
const { command, prefixArgs } = resolveOxlintInvocation(repoRoot)
expect(command).toBe(process.execPath)
expect(prefixArgs).toHaveLength(1)
// The EINVAL that killed the changed-code gate came from spawning a .cmd.
expect(prefixArgs[0]).not.toMatch(/\.(cmd|bat)$/i)
expect(existsSync(prefixArgs[0])).toBe(true)
})
it('spawns without a shell and produces Oxlint JSON', () => {
const { command, prefixArgs } = resolveOxlintInvocation(repoRoot)
const result = spawnSync(
command,
[...prefixArgs, '--help'],
// shell:false is the point: the shim form throws EINVAL here on Windows.
{ cwd: repoRoot, encoding: 'utf8', shell: false, windowsHide: true }
)
expect(result.error).toBeUndefined()
expect(result.stdout).toContain('oxlint')
})
})
@@ -655,6 +655,8 @@ describe('Electron runtime package contract', () => {
expect(releaseWindowsRunStep.run).toContain(
'pnpm run --if-present test:e2e:windows-fresh-startup-golden'
)
expect(releaseWindowsRunStep.run).not.toContain('test:e2e:workspace-session-golden')
expect(releaseWindowsRunStep.run).not.toContain('test:e2e:source-control-golden')
expect(releaseEvidenceJob['continue-on-error']).toBe(true)
expect(
releaseEvidenceJob.strategy.matrix.include.map(({ platform }) => platform).sort()
@@ -266,6 +266,7 @@ describe('PR E2E gate contract', () => {
'tests/e2e/pty-input-write-queue-ssh.spec.ts',
'tests/e2e/ssh-cold-activation-restore.spec.ts',
'tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts',
'tests/e2e/ssh-docker-transport-drop-recovery.spec.ts',
'tests/e2e/ssh-port-forward-lifecycle.spec.ts',
'tests/e2e/ssh-reconnect-tab-destruction.spec.ts',
'tests/e2e/ssh-startup-exec-readiness.spec.ts',
@@ -315,6 +316,11 @@ describe('PR E2E gate contract', () => {
expect(
selectPrE2eSpecs(['src/renderer/src/hooks/remote-workspace-session-merge.test.ts'])
).toEqual([])
expect(
selectPrE2eSpecs([
'src/renderer/src/hooks/__tests__/remote-workspace-target-sync-test-harness.ts'
])
).toEqual([])
})
it('triggers the Docker-SSH lane from SSH source, not from a spec name', () => {
@@ -464,6 +470,50 @@ describe('PR E2E gate contract', () => {
expect(selectPrE2eSpecs([source.replace(/\.tsx?$/, '.test.ts')]), source).toEqual([])
expect(existsSync(join(projectDir, spec)), spec).toBe(true)
}
const parkedSplitSpec = 'tests/e2e/terminal-parked-cli-split.spec.ts'
for (const source of [
'src/main/window/attach-main-window-services.ts',
'src/preload/api/ui-command-event-api.ts',
'src/preload/index.ts',
'src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.ts',
'src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts',
'src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts',
'src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge.ts'
]) {
expect(selectPrE2eSpecs([source]), source).toContain(parkedSplitSpec)
expect(selectPrE2eSpecs([source.replace(/\.ts$/, '.test.ts')]), source).not.toContain(
parkedSplitSpec
)
}
expect(existsSync(join(projectDir, parkedSplitSpec)), parkedSplitSpec).toBe(true)
const restartContinuitySpec = 'tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts'
for (const source of [
'src/main/daemon/daemon-attach-only-retirement.ts',
'src/main/daemon/daemon-pty-applied-size.ts',
'src/main/daemon/daemon-pty-session-control.ts',
'src/main/daemon/daemon-pty-spawn-result.ts',
'src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts',
'src/renderer/src/components/terminal-pane/terminal-error-accumulation.ts',
'src/renderer/src/runtime/web-runtime-session.ts',
'src/renderer/src/runtime/web-session-tabs-sync.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-surface.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-inventory.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-inventory-validation.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-cache.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-pane.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-queue.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-recovery-rpc-lane.ts',
'src/renderer/src/runtime/web-session-terminal-orphan-topology.ts'
]) {
expect(selectPrE2eSpecs([source]), source).toContain(restartContinuitySpec)
expect(selectPrE2eSpecs([source.replace(/\.ts$/, '.test.ts')]), source).not.toContain(
restartContinuitySpec
)
}
expect(existsSync(join(projectDir, restartContinuitySpec)), restartContinuitySpec).toBe(true)
const quickCommandSpec = 'tests/e2e/terminal-quick-command-pre-bind-recovery.spec.ts'
for (const source of [
'src/renderer/src/components/terminal-pane/pty-connection.ts',
+20
View File
@@ -27,6 +27,7 @@ export const PR_E2E_SOURCE_ROUTES = [
'tests/e2e/pty-input-write-queue-ssh.spec.ts',
'tests/e2e/ssh-cold-activation-restore.spec.ts',
'tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts',
'tests/e2e/ssh-docker-transport-drop-recovery.spec.ts',
'tests/e2e/ssh-port-forward-lifecycle.spec.ts',
'tests/e2e/ssh-reconnect-tab-destruction.spec.ts',
'tests/e2e/ssh-startup-exec-readiness.spec.ts',
@@ -52,6 +53,7 @@ export const PR_E2E_SOURCE_ROUTES = [
],
matches: (file) =>
isProductSource(file) &&
!file.endsWith('-test-harness.ts') &&
/^(?:src\/main\/ipc\/remote-workspace|src\/shared\/remote-workspace-|src\/renderer\/src\/hooks\/remote-workspace-|src\/renderer\/src\/lib\/worktree-(?:initial-terminal-seeding|default-terminal-tabs)\.ts|src\/renderer\/src\/components\/terminal\/initial-terminal)/.test(
file
)
@@ -112,6 +114,24 @@ export const PR_E2E_SOURCE_ROUTES = [
file
)
},
{
id: 'terminal-session.parked-cli-split',
specs: ['tests/e2e/terminal-parked-cli-split.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/main\/window\/attach-main-window-services\.ts|src\/preload\/(?:index|api\/ui-command-event-api)\.ts|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-pane-split-request-routing|use-terminal-pane-lifecycle|use-terminal-tab-cold-parking)\.ts|src\/renderer\/src\/hooks\/ipc-events\/terminal-ui-routing-ipc-bridge\.ts)$/.test(
file
)
},
{
id: 'terminal-session.paired-serve-restart-binding-continuity',
specs: ['tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/main\/daemon\/(?:daemon-attach-only-retirement|daemon-pty-applied-size|daemon-pty-session-control|daemon-pty-spawn-result)\.ts|src\/renderer\/src\/components\/terminal-pane\/(?:remote-runtime-pty-transport|terminal-error-accumulation)\.ts|src\/renderer\/src\/runtime\/(?:web-runtime-session|web-session-tabs-sync|web-session-terminal-orphan-(?:topology|recovery(?:-(?:adoption|surface|inventory|inventory-validation|cache|queue|rpc-lane|pane))?))\.ts)$/.test(
file
)
},
{
id: 'terminal-provider.ssh-remote-reattach-contract',
specs: ['tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts'],
@@ -1,4 +1,4 @@
import { globSync, readFileSync } from 'node:fs'
import { existsSync, globSync, readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
@@ -59,6 +59,9 @@ describe('PR workflow parallelism', () => {
const primerInstall = workflow.jobs.test_native_cache.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
const nodeNextPrimerInstall = nodeNextWorkflow.jobs.test_native_cache.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(workflow.jobs.test.uses).toBe('./.github/workflows/unit-tests.yml')
expect(JSON.parse(workflow.jobs.test.with.node_versions)).toEqual(['24'])
@@ -80,6 +83,9 @@ describe('PR workflow parallelism', () => {
expect(primerInstall.with['native-runtime']).toBe('node')
expect(primerInstall.with['node-version']).toBe('24')
expect(workflow.jobs.test.needs).toContain('test_native_cache')
expect(nodeNextPrimerInstall.with['native-runtime']).toBe('node')
expect(nodeNextPrimerInstall.with['node-version']).toBe('26')
expect(nodeNextWorkflow.jobs.test.needs).toEqual(['test_native_cache'])
})
it('runs real-shell coverage once outside the general shards', () => {
@@ -182,6 +188,15 @@ describe('PR workflow parallelism', () => {
// Why this file is excluded: it carries the detector pattern as a literal
// and would otherwise match itself.
.filter((testFile) => testFile !== 'config/scripts/pr-workflow-parallelism.test.mjs')
// TypeScript builds can leave an ignored JavaScript companion beside a source
// test. Inspect the source file once so generated output cannot duplicate it.
.filter(
(testFile) =>
!testFile.endsWith('.js') ||
!['.ts', '.tsx', '.mjs', '.cjs'].some((extension) =>
existsSync(testFile.replace(/\.js$/, extension))
)
)
.filter((testFile) => realZshUsage.test(readFileSync(testFile, 'utf8')))
.sort()
@@ -1,4 +1,5 @@
import { existsSync, readFileSync, rmSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
@@ -44,7 +45,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
}
)
@@ -80,7 +81,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
)
).toBe('// napi.h\n')
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -104,7 +105,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
expect(readFileSync(join(runtimeDir, 'conpty.dll'), 'utf8')).toBe('conpty.dll x64')
expect(readFileSync(join(runtimeDir, 'OpenConsole.exe'), 'utf8')).toBe('OpenConsole.exe x64')
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -132,7 +133,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['windows-native-registry'])
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
}
)
@@ -162,7 +163,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
}
)
@@ -193,7 +194,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
expect(rebuildCall.ignoreModules).toEqual(['cpu-features'])
expect(rebuildCall.force).toBe(true)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
}
)
@@ -221,7 +222,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
}
)
@@ -251,7 +252,7 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.force).toBe(true)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
}
)
@@ -3,6 +3,7 @@ import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from '
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { copyScriptWithLocalModules } from './script-module-dependencies.mjs'
const sourceScriptPath = fileURLToPath(new URL('./rebuild-native-deps.mjs', import.meta.url))
const sourceInstallScriptPath = fileURLToPath(
@@ -19,10 +20,7 @@ export function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-rebuild-native-deps-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
copyFileSync(sourceScriptPath, join(projectDir, 'config', 'scripts', 'rebuild-native-deps.mjs'))
copyFileSync(
sourceInstallScriptPath,
join(projectDir, 'config', 'scripts', 'install-electron-package-binary.mjs')
)
copyScriptWithLocalModules(sourceInstallScriptPath, join(projectDir, 'config', 'scripts'))
copyFileSync(
sourceNodePtyJobOwnershipPath,
join(projectDir, 'config', 'scripts', 'node-pty-job-ownership.cjs')
+10 -9
View File
@@ -1,6 +1,7 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
import {
mkTempProject,
@@ -36,7 +37,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'download attempted\n'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -60,7 +61,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'Continuing postinstall because Electron binary installation failed'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -81,7 +82,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'Continuing postinstall because Electron binary installation failed'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -117,7 +118,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'stale-path'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -141,7 +142,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'platform=linux arch=arm64\ndownload attempted\n'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -162,7 +163,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
expect(result.status, result.stderr).toBe(0)
expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -188,7 +189,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'electron.exe'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -209,7 +210,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
'platform=linux arch=x64'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
@@ -230,7 +231,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
expect(result.stdout).toContain('Repaired Electron path.txt -> electron')
expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
removeTreeSync(projectDir)
}
})
})
@@ -0,0 +1,131 @@
#!/usr/bin/env node
// Removes idle `out/electron-dev` bundles across every worktree of a repository.
//
// The dev runner already prunes these, but only within the worktree it is starting and only when
// that worktree holds more than one bundle -- and a worktree almost always holds exactly one. So
// nothing ever reclaims a bundle belonging to a worktree you are not currently running, and one
// ~275MB copy per branch accumulates indefinitely.
//
// Bundles are pure build output: `pnpm dev` rebuilds one on demand, and since the Electron dist is
// now shared, rebuilding is cheap.
import { execFileSync } from 'node:child_process'
import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
import path from 'node:path'
import {
DEV_BUNDLE_MARKER_FILENAME,
getDevBundleProcessTable,
selectStaleDevBundleDirs
} from './dev-electron-bundle-cache.mjs'
const apply = process.argv.includes('--apply')
const repoRoot = process.argv.includes('--repo')
? path.resolve(process.argv[process.argv.indexOf('--repo') + 1])
: process.cwd()
function listWorktrees(root) {
const raw = execFileSync('git', ['-C', root, 'worktree', 'list', '--porcelain'], {
encoding: 'utf8'
})
return raw
.split('\n')
.filter((line) => line.startsWith('worktree '))
.map((line) => line.slice('worktree '.length).trim())
}
function measure(targetPath) {
let total = 0
let entries
try {
entries = readdirSync(targetPath, { withFileTypes: true })
} catch {
return 0
}
for (const entry of entries) {
const entryPath = path.join(targetPath, entry.name)
if (entry.isDirectory()) {
total += measure(entryPath)
} else if (!entry.isSymbolicLink()) {
total += statSync(entryPath, { throwIfNoEntry: false })?.size ?? 0
}
}
return total
}
export function collectDevBundles(worktree) {
const root = path.join(worktree, 'out', 'electron-dev')
try {
return readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const dir = path.join(root, entry.name)
return {
dir,
hasMarker: existsSync(path.join(dir, DEV_BUNDLE_MARKER_FILENAME)),
mtimeMs: statSync(dir, { throwIfNoEntry: false })?.mtimeMs ?? 0
}
})
} catch {
return []
}
}
function main() {
// The patched dev bundle is only built on macOS; elsewhere the dev app runs from dist directly.
if (process.platform !== 'darwin') {
console.log('No dev Electron bundles on this platform; nothing to reclaim.')
return
}
const processTable = getDevBundleProcessTable()
if (processTable === null) {
// Same rule the dev runner uses: no process table means we cannot prove a bundle is idle.
console.error('Could not read the process table; refusing to guess which bundles are idle.')
process.exitCode = 1
return
}
const bundles = listWorktrees(repoRoot).flatMap((worktree) => collectDevBundles(worktree))
// currentDir is null on purpose: unlike the dev runner, this sweep is not about to launch anything,
// so the only thing protecting a bundle is a live process or an in-flight build.
const stale = selectStaleDevBundleDirs({
bundles,
currentDir: null,
processTable,
nowMs: Date.now()
})
let reclaimed = 0
let removed = 0
for (const dir of stale) {
const size = measure(dir)
if (!apply) {
console.log(`would remove ${dir} ${(size / 1024 ** 3).toFixed(2)} GiB`)
reclaimed += size
removed += 1
continue
}
try {
rmSync(dir, { recursive: true, force: true })
reclaimed += size
removed += 1
console.log(`removed ${dir} ${(size / 1024 ** 3).toFixed(2)} GiB`)
} catch (error) {
console.warn(`skip ${dir} (${error instanceof Error ? error.message : String(error)})`)
}
}
const inUse = bundles.length - stale.length
console.log(
`\n${apply ? 'Removed' : 'Would remove'} ${removed} bundle(s); ` +
`${apply ? 'reclaimed' : 'reclaimable'} ~${(reclaimed / 1024 ** 3).toFixed(2)} GiB` +
`${inUse > 0 ? `; left ${inUse} in use or still building` : ''}` +
`${apply ? '' : '\nRe-run with --apply to do it.'}`
)
}
// Guarded so importing this module for tests does not sweep the whole repository.
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
main()
}
@@ -0,0 +1,97 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
DEV_BUNDLE_MARKER_FILENAME,
getDevBundleProcessTable,
selectStaleDevBundleDirs
} from './dev-electron-bundle-cache.mjs'
import { collectDevBundles } from './reclaim-dev-electron-bundles.mjs'
const roots: string[] = []
afterEach(() => {
while (roots.length > 0) {
rmSync(roots.pop()!, { recursive: true, force: true })
}
})
function makeWorktree(bundles: { name: string; marker: boolean }[]): string {
const worktree = mkdtempSync(path.join(tmpdir(), 'orca-dev-bundles-'))
roots.push(worktree)
for (const bundle of bundles) {
const dir = path.join(worktree, 'out', 'electron-dev', bundle.name)
mkdirSync(dir, { recursive: true })
if (bundle.marker) {
writeFileSync(path.join(dir, DEV_BUNDLE_MARKER_FILENAME), '{}')
}
}
return worktree
}
describe('collectDevBundles', () => {
it('reports each bundle and whether its build finished', () => {
const worktree = makeWorktree([
{ name: 'aaaa', marker: true },
{ name: 'bbbb', marker: false }
])
const bundles = collectDevBundles(worktree).sort((a, b) => a.dir.localeCompare(b.dir))
expect(bundles).toHaveLength(2)
expect(bundles[0].hasMarker).toBe(true)
expect(bundles[1].hasMarker).toBe(false)
expect(bundles[0].mtimeMs).toBeGreaterThan(0)
})
it('returns nothing for a worktree that has never run the dev app', () => {
const worktree = mkdtempSync(path.join(tmpdir(), 'orca-dev-bundles-'))
roots.push(worktree)
expect(collectDevBundles(worktree)).toEqual([])
})
})
describe('sweeping across worktrees', () => {
it('spares a bundle a live process is running from, and takes the idle ones', () => {
const worktree = makeWorktree([
{ name: 'live', marker: true },
{ name: 'idle', marker: true }
])
const bundles = collectDevBundles(worktree)
const live = bundles.find((bundle) => bundle.dir.endsWith('live'))!
// Why currentDir is null here: unlike the dev runner, the sweep is not about to launch
// anything, so only a live process or an in-flight build may protect a bundle.
const stale = selectStaleDevBundleDirs({
bundles,
currentDir: null,
processTable: `/usr/bin/foo ${live.dir}/Orca.app/Contents/MacOS/Electron`,
nowMs: Date.now()
})
expect(stale).toEqual([bundles.find((bundle) => bundle.dir.endsWith('idle'))!.dir])
})
it('spares a build still in flight, which has no marker yet', () => {
const worktree = makeWorktree([{ name: 'building', marker: false }])
const stale = selectStaleDevBundleDirs({
bundles: collectDevBundles(worktree),
currentDir: null,
processTable: '',
nowMs: Date.now()
})
expect(stale).toEqual([])
})
})
describe('getDevBundleProcessTable', () => {
it('returns null rather than an empty table when ps fails', () => {
expect(
getDevBundleProcessTable(() => {
throw new Error('ps unavailable')
})
).toBeNull()
})
it('reads the real process table on this host', () => {
const table = getDevBundleProcessTable()
expect(typeof table === 'string' || table === null).toBe(true)
})
})
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env node
// Converts worktrees that already have their own Electron dist over to the shared cache.
// A normal install only shares when Electron is (re)installed, and an existing healthy worktree
// never reaches that path -- so without this, sharing only arrives at the next Electron upgrade.
import { execFileSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import {
existsSync,
readFileSync,
readdirSync,
renameSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import path from 'node:path'
import {
hasAdoptedSharedElectronDist,
isUsableElectronDist,
publishSharedElectronDist,
recordAdoptedSharedElectronDist,
resolveSharedElectronDistEntry,
shareElectronDistFromCache
} from './shared-electron-dist-cache.mjs'
import { getElectronPlatformPath } from './electron-platform-path.mjs'
const apply = process.argv.includes('--apply')
const repoRoot = process.argv.includes('--repo')
? path.resolve(process.argv[process.argv.indexOf('--repo') + 1])
: process.cwd()
function listWorktrees(root) {
const raw = execFileSync('git', ['-C', root, 'worktree', 'list', '--porcelain'], {
encoding: 'utf8'
})
return raw
.split('\n')
.filter((line) => line.startsWith('worktree '))
.map((line) => line.slice('worktree '.length).trim())
}
/** Apparent size, walked in Node: `du` does not exist on Windows, where it silently reported 0. */
function measure(targetPath) {
let total = 0
let entries
try {
entries = readdirSync(targetPath, { withFileTypes: true })
} catch {
return 0
}
for (const entry of entries) {
const entryPath = path.join(targetPath, entry.name)
if (entry.isDirectory()) {
total += measure(entryPath)
} else if (!entry.isSymbolicLink()) {
total += statSync(entryPath, { throwIfNoEntry: false })?.size ?? 0
}
}
return total
}
/** Swap in a shared copy behind a rename, so an interrupted run never leaves a partial dist. */
function adoptInto(distPath, entry, identity) {
const stagePath = `${distPath}.reclaim-${process.pid}-${randomUUID()}`
if (!shareElectronDistFromCache(entry, stagePath, identity)) {
rmSync(stagePath, { recursive: true, force: true })
return false
}
const previousPath = `${distPath}.previous-${process.pid}-${randomUUID()}`
renameSync(distPath, previousPath)
try {
renameSync(stagePath, distPath)
} catch (error) {
renameSync(previousPath, distPath)
rmSync(stagePath, { recursive: true, force: true })
throw error
}
rmSync(previousPath, { recursive: true, force: true })
return true
}
function main() {
let reclaimed = 0
let converted = 0
let skipped = 0
for (const worktree of listWorktrees(repoRoot)) {
const electronPackageDir = path.join(worktree, 'node_modules', 'electron')
const distPath = path.join(electronPackageDir, 'dist')
if (!existsSync(path.join(electronPackageDir, 'package.json')) || !existsSync(distPath)) {
continue
}
if (statSync(distPath, { throwIfNoEntry: false })?.isDirectory() !== true) {
continue
}
let version
try {
version = JSON.parse(
readFileSync(path.join(electronPackageDir, 'package.json'), 'utf8')
).version
} catch {
continue
}
const targetPlatform = process.platform
const targetArch = process.arch
let platformPath
try {
platformPath = getElectronPlatformPath(targetPlatform)
} catch {
continue
}
if (!isUsableElectronDist(distPath, version, platformPath)) {
console.log(`skip ${worktree} (dist is not a complete Electron ${version})`)
skipped += 1
continue
}
const entry = resolveSharedElectronDistEntry({
repoRoot: worktree,
electronPackageDir,
version,
targetPlatform,
targetArch
})
if (entry === null) {
continue
}
if (hasAdoptedSharedElectronDist(entry)) {
continue
}
const size = measure(distPath)
if (!apply) {
console.log(`would share ${worktree} ${(size / 1024 ** 3).toFixed(2)} GiB (${version})`)
reclaimed += size
converted += 1
continue
}
try {
if (!existsSync(entry.entryPath)) {
if (publishSharedElectronDist(distPath, entry, { version, platformPath })) {
recordAdoptedSharedElectronDist(entry, writeFileSync)
console.log(`seeded ${worktree} -> ${entry.entryPath}`)
converted += 1
}
continue
}
if (adoptInto(distPath, entry, { version, platformPath })) {
recordAdoptedSharedElectronDist(entry, writeFileSync)
reclaimed += size
converted += 1
console.log(`shared ${worktree} reclaimed ${(size / 1024 ** 3).toFixed(2)} GiB`)
}
} catch (error) {
// A worktree that fails is left exactly as it was; it still has its own working dist.
console.warn(`skip ${worktree} (${error instanceof Error ? error.message : String(error)})`)
skipped += 1
}
}
console.log(
`\n${apply ? 'Shared' : 'Would share'} ${converted} worktree(s); ` +
`${apply ? 'reclaimed' : 'reclaimable'} ~${(reclaimed / 1024 ** 3).toFixed(2)} GiB` +
`${skipped > 0 ? `; skipped ${skipped}` : ''}` +
`${apply ? '' : '\nRe-run with --apply to do it.'}`
)
}
// Guarded so importing this module for tests does not sweep the whole repository.
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
main()
}
@@ -41,14 +41,24 @@ describe('release-cut source map publication', () => {
expect(publish.with.command).toContain('runner.temp')
})
it('fails the release when no source maps were emitted', () => {
// Why: a silent regression of build.sourcemap would ship an undecodable
// release rather than an obviously broken one.
it('fails only when a map-enabled cut emits no source maps', () => {
// Why: legacy tags predate source-map publication, but a newer tag that
// enables hidden maps must still fail loudly if the build regresses.
const bundle = buildSteps[stepIndex('Bundle main-process source maps')]
expect(bundle.id).toBe('bundle-main-sourcemaps')
expect(bundle.run).toContain('grep -Eq')
expect(bundle.run).toContain('sourcemap:[[:space:]]*')
expect(bundle.run).toContain('has_maps=false')
expect(bundle.run).toContain('has_maps=true')
expect(bundle.run).toContain('::error::')
expect(bundle.run).toContain('exit 1')
})
it('skips publication for legacy cut refs without hidden source maps', () => {
const publish = buildSteps[stepIndex('Publish main-process source maps')]
expect(publish.if).toContain("steps.bundle-main-sourcemaps.outputs.has_maps == 'true'")
})
it('bundles maps after the build and before packaging strips them', () => {
const bundle = stepIndex('Bundle main-process source maps')
expect(bundle).toBeGreaterThan(stepIndex('Build app'))
@@ -31,6 +31,7 @@ const EXPECTED_MATRIX = {
},
[`${RELEASE_WORKFLOW}#post-release-e2e`]: { actions: 'write' },
[`${RELEASE_WORKFLOW}#publish-release`]: { contents: 'write' },
[`${RELEASE_WORKFLOW}#release-preflight`]: { contents: 'read' },
[`${RELEASE_WORKFLOW}#skill-sharing-linux-floor-release-gate`]: { contents: 'read' },
[`${RELEASE_WORKFLOW}#skill-sharing-release-gate`]: { contents: 'read' },
[`${RELEASE_WORKFLOW}#terminal-rendering-golden`]: { contents: 'read' },
+13 -24
View File
@@ -1,7 +1,6 @@
import { execFileSync, spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
@@ -16,8 +15,15 @@ import {
import net from 'node:net'
import { createRequire } from 'node:module'
import path from 'node:path'
import { prepareDevCliTerminalWrappers } from './dev-cli-terminal-wrapper.mjs'
import { isDevBundleInUse, selectStaleDevBundleDirs } from './dev-electron-bundle-cache.mjs'
import {
DEV_BUNDLE_MARKER_FILENAME,
getDevBundleProcessTable,
isDevBundleInUse,
selectStaleDevBundleDirs
} from './dev-electron-bundle-cache.mjs'
import { copyPrivateTree } from './space-sharing-copy.mjs'
import {
DEV_BUNDLE_ID,
getDevBundlePlistPatches,
@@ -116,23 +122,6 @@ function sanitizeMacAppBundleName(value) {
)
}
function getDevBundleProcessTable() {
// Not pgrep: macOS pgrep has no -a (a Linux procps extension) and silently prints bare PIDs,
// which reads as "nothing is running" and deletes a live bundle. -ww keeps the command column
// from being truncated. The raw text is searched directly; see dev-electron-bundle-cache.mjs
// for why it is deliberately not parsed into paths.
try {
return execFileSync('/bin/ps', ['-Awwo', 'command='], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000
})
} catch {
// Treating a failure as "nothing live" would risk deleting a running bundle, so skip pruning.
return null
}
}
function pruneStaleDevBundles(distDir) {
const root = path.dirname(distDir)
let bundles
@@ -143,7 +132,7 @@ function pruneStaleDevBundles(distDir) {
const dir = path.join(root, entry.name)
return {
dir,
hasMarker: existsSync(path.join(dir, 'orca-dev-electron-app.json')),
hasMarker: existsSync(path.join(dir, DEV_BUNDLE_MARKER_FILENAME)),
mtimeMs: getMtimeMs(dir)
}
})
@@ -203,7 +192,7 @@ function prepareMacDevElectronApp() {
// and it sits outside the code signature, so varying it does not disturb the cdhash.
const appBundleName = `${sanitizeMacAppBundleName(title)}.app`
const appPath = path.join(distDir, appBundleName)
const markerPath = path.join(distDir, 'orca-dev-electron-app.json')
const markerPath = path.join(distDir, DEV_BUNDLE_MARKER_FILENAME)
// Why: one stable id for every dev instance. Per-instance ids registered a
// new macOS Notification Settings entry for each branch × Electron version,
// piling up "Orca: <branch>" rows forever and breaking the notification
@@ -298,9 +287,9 @@ function prepareMacDevElectronApp() {
rmSync(distDir, { recursive: true, force: true })
mkdirSync(distDir, { recursive: true })
// Why: Electron.framework uses relative symlinks for its bundle resources;
// resolving them to pnpm-store absolutes breaks Chromium's bundle lookup.
cpSync(sourceAppPath, appPath, { recursive: true, verbatimSymlinks: true })
// Why clone-first: this ~280MB copy is made per branch title x Electron version, and only the
// plist/helper/codesign bytes patched below ever diverge from the source.
copyPrivateTree(sourceAppPath, appPath)
restoreElectronFrameworkSymlinks(appPath)
const plistPath = path.join(appPath, 'Contents', 'Info.plist')
@@ -2,7 +2,9 @@ import { spawn } from 'node:child_process'
import { fileURLToPath } from 'node:url'
const buildScript = fileURLToPath(new URL('./run-electron-vite-build.mjs', import.meta.url))
const targetConfig = fileURLToPath(new URL('../electron-vite-target.config.ts', import.meta.url))
// Keep this wrapper CommonJS (the `.cts` extension) so electron-vite can load
// each parallel target without sharing its timestamp-named ESM temp file.
const targetConfig = fileURLToPath(new URL('../electron-vite-target.config.cts', import.meta.url))
const targets = ['main', 'preload', 'renderer']
function buildTarget(target) {
+1
View File
@@ -72,6 +72,7 @@ const result = spawnSync(
'tests/e2e/ssh-cold-activation-restore.spec.ts',
'tests/e2e/ssh-cold-hydration-gap-tab-seeding.spec.ts',
'tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts',
'tests/e2e/ssh-docker-transport-drop-recovery.spec.ts',
'tests/e2e/ssh-external-image-preview.spec.ts',
'tests/e2e/ssh-lost-kill-tab-resurrection.spec.ts',
'tests/e2e/ssh-pi-compatible-agent-title.spec.ts',
@@ -0,0 +1,26 @@
import { copyFileSync, mkdirSync, readFileSync } from 'node:fs'
import { basename, dirname, join } from 'node:path'
/**
* Copy a script and every co-located module it imports into a fixture's `config/scripts`.
*
* Walked rather than listed: a module the script needs but the fixture never copied fails every
* test in the suite with a module-resolution error that looks nothing like the defect it hides.
*/
export function copyScriptWithLocalModules(sourceScriptPath, destinationScriptsDir) {
mkdirSync(destinationScriptsDir, { recursive: true })
for (const modulePath of collectScriptModules(sourceScriptPath)) {
copyFileSync(modulePath, join(destinationScriptsDir, basename(modulePath)))
}
}
function collectScriptModules(scriptPath, seen = new Set()) {
if (seen.has(scriptPath)) {
return seen
}
seen.add(scriptPath)
for (const [, specifier] of readFileSync(scriptPath, 'utf8').matchAll(/from '(\.\/[^']+)'/g)) {
collectScriptModules(join(dirname(scriptPath), specifier), seen)
}
return seen
}
@@ -0,0 +1,189 @@
import { execFileSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
import path from 'node:path'
import { makeTreeReadOnly, shareTree } from './space-sharing-copy.mjs'
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
// Sibling of path.txt, never inside dist: an install replaces dist wholesale.
const MARKER_FILENAME = '.orca-shared-dist'
/**
* Where sibling worktrees of one repository keep their shared extracted Electron.
*
* Null means "install normally" -- every caller treats a missing entry as "do what you did before".
*/
export function resolveSharedElectronDistEntry(options) {
const { repoRoot, version, targetPlatform, targetArch } = options
const env = options.env ?? process.env
// Packaging jobs get a fresh checkout per run, so a cache only adds a failure mode.
if (env.CI === '1' || env.CI === 'true') {
return null
}
if (![version, targetPlatform, targetArch].every((part) => IDENTITY_PATTERN.test(part ?? ''))) {
return null
}
let gitCommonDir
try {
gitCommonDir = resolveGitCommonDir(repoRoot, options.execFile ?? execFileSync)
} catch {
return null // Folder workspace, or no Git on PATH.
}
const cacheRoot = path.join(gitCommonDir, 'orca-cache', 'electron')
return {
cacheRoot,
entryPath: path.join(cacheRoot, `${version}-${targetPlatform}-${targetArch}`),
markerPath: path.join(options.electronPackageDir, MARKER_FILENAME)
}
}
export function resolveGitCommonDir(repoRoot, execFile = execFileSync) {
const rawPath = execFile('git', ['-C', repoRoot, 'rev-parse', '--git-common-dir'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim()
if (!rawPath) {
throw new Error('Git returned an empty common directory')
}
return path.resolve(repoRoot, rawPath)
}
/** True once this worktree's dist is already a clone of the current cache entry. */
export function hasAdoptedSharedElectronDist(entry) {
try {
return readFileSync(entry.markerPath, 'utf8') === path.basename(entry.entryPath)
} catch {
return false
}
}
export function recordAdoptedSharedElectronDist(entry, write) {
try {
write(entry.markerPath, path.basename(entry.entryPath))
} catch {
// The marker is only an optimization: a missing one costs one extra clone.
}
}
/**
* Share a validated cache entry into `stagePath`. Deliberately never falls back to a byte copy --
* with no storage to share the caller is better off with its normal install, which this must not
* slow down.
*/
export function shareElectronDistFromCache(entry, stagePath, options) {
const { version, platformPath } = options
if (!isUsableElectronDist(entry.entryPath, version, platformPath)) {
return false
}
try {
;(options.share ?? shareTree)(entry.entryPath, stagePath)
} catch {
return false
}
return isUsableElectronDist(stagePath, version, platformPath)
}
/**
* Publish this worktree's dist as the shared entry, best effort.
*
* No lock: staging names are unique and `rename` onto a populated directory fails with ENOTEMPTY,
* so a concurrent publisher either wins the rename or cleans up its own staging tree. Neither can
* observe a half-written entry, and a usable entry already in place is never overwritten.
*/
export function publishSharedElectronDist(distPath, entry, options = {}) {
const { version, platformPath } = options
const uuid = options.uuid ?? randomUUID
const canValidate = Boolean(version) && Boolean(platformPath)
// Without an identity to check against, "unusable" is unknowable -- never discard on a guess.
if (existsSync(entry.entryPath) && (!canValidate || isUsable(entry, version, platformPath))) {
return false
}
const stagePath = `${entry.entryPath}.staging-${process.pid}-${uuid()}`
try {
mkdirSync(entry.cacheRoot, { recursive: true })
;(options.share ?? shareTree)(distPath, stagePath)
// Before publishing, not after: an entry is visible the instant the rename lands, and under
// hardlink sharing this is the only thing standing between a stray write and every worktree.
;(options.protect ?? makeTreeReadOnly)(stagePath)
} catch {
rmSync(stagePath, { recursive: true, force: true })
return false
}
return swapInElectronDistEntry(entry, stagePath, {
canValidate,
version,
platformPath,
uuid,
rename: options.rename ?? renameSync
})
}
/**
* Replace the entry with a staged tree, re-checking first.
*
* Sharing the tree above takes seconds, and a sibling worktree can publish a perfectly good entry
* in that time. Re-validating here, immediately before the destructive rename, keeps us from
* discarding that entry -- and restoring the quarantine on a failed swap keeps a losing publisher
* from leaving the cache empty.
*/
function swapInElectronDistEntry(entry, stagePath, options) {
const { canValidate, version, platformPath, uuid, rename } = options
let quarantinePath = null
if (existsSync(entry.entryPath)) {
// Same rule as before staging: an entry we cannot judge, or one that is good, is never
// displaced. Both mean another worktree got there first, so keep theirs.
if (!canValidate || isUsable(entry, version, platformPath)) {
rmSync(stagePath, { recursive: true, force: true })
return false
}
quarantinePath = `${entry.entryPath}.unusable-${process.pid}-${uuid()}`
try {
renameSync(entry.entryPath, quarantinePath)
} catch {
rmSync(stagePath, { recursive: true, force: true })
return false // Another worktree is already replacing it.
}
}
try {
rename(stagePath, entry.entryPath)
} catch {
rmSync(stagePath, { recursive: true, force: true })
if (quarantinePath !== null) {
// Put it back rather than leave no entry at all; a bad entry still beats an empty cache,
// because the next publisher re-validates and replaces it.
try {
renameSync(quarantinePath, entry.entryPath)
return false
} catch {
rmSync(quarantinePath, { recursive: true, force: true })
}
}
return false
}
if (quarantinePath !== null) {
rmSync(quarantinePath, { recursive: true, force: true })
}
return true
}
function isUsable(entry, version, platformPath) {
return isUsableElectronDist(entry.entryPath, version, platformPath)
}
export function isUsableElectronDist(distPath, version, platformPath) {
try {
if (!lstatSync(distPath).isDirectory()) {
return false
}
const installedVersion = readFileSync(path.join(distPath, 'version'), 'utf8')
.trim()
.replace(/^v/, '')
return installedVersion === version && existsSync(path.join(distPath, platformPath))
} catch {
return false
}
}
@@ -0,0 +1,358 @@
import type { execFileSync } from 'node:child_process'
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
rmSync,
statSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
shareElectronDistFromCache,
hasAdoptedSharedElectronDist,
isUsableElectronDist,
publishSharedElectronDist,
recordAdoptedSharedElectronDist,
resolveSharedElectronDistEntry
} from './shared-electron-dist-cache.mjs'
import { makeTreeReadOnly } from './space-sharing-copy.mjs'
const VERSION = '43.4.1'
const PLATFORM_PATH = path.join('Electron.app', 'Contents', 'MacOS', 'Electron')
const identity = { version: VERSION, platformPath: PLATFORM_PATH }
const roots: string[] = []
afterEach(() => {
while (roots.length > 0) {
rmSync(roots.pop()!, { recursive: true, force: true })
}
})
function makeRoot(): string {
const root = mkdtempSync(path.join(tmpdir(), 'orca-shared-electron-'))
roots.push(root)
return root
}
function writeDist(distPath: string, version = VERSION): string {
mkdirSync(path.join(distPath, path.dirname(PLATFORM_PATH)), { recursive: true })
writeFileSync(path.join(distPath, 'version'), `v${version}\n`)
writeFileSync(path.join(distPath, PLATFORM_PATH), 'electron')
return distPath
}
function makeEntry(root: string, entryName = `${VERSION}-darwin-arm64`) {
const cacheRoot = path.join(root, 'cache')
return {
cacheRoot,
entryPath: path.join(cacheRoot, entryName),
markerPath: path.join(root, '.orca-shared-dist')
}
}
const baseOptions = {
repoRoot: '/repo',
electronPackageDir: '/repo/node_modules/electron',
version: VERSION,
targetPlatform: 'darwin',
targetArch: 'arm64',
hostPlatform: 'darwin' as const,
env: {} as NodeJS.ProcessEnv,
execFile: (() => '/repo/.git\n') as unknown as typeof execFileSync
}
describe('resolveSharedElectronDistEntry', () => {
it('keys the entry by version, platform, and arch under the git common dir', () => {
const entry = resolveSharedElectronDistEntry(baseOptions)
expect(entry?.cacheRoot).toBe(path.join('/repo/.git', 'orca-cache', 'electron'))
expect(entry?.entryPath).toBe(
path.join('/repo/.git', 'orca-cache', 'electron', '43.4.1-darwin-arm64')
)
expect(entry?.markerPath).toBe(path.join('/repo/node_modules/electron', '.orca-shared-dist'))
})
it('offers an entry on every platform a worktree is developed on', () => {
for (const hostPlatform of ['darwin', 'linux', 'win32']) {
expect(resolveSharedElectronDistEntry({ ...baseOptions, hostPlatform })).not.toBeNull()
}
})
it('declines on CI, where every job gets a fresh checkout', () => {
expect(resolveSharedElectronDistEntry({ ...baseOptions, env: { CI: '1' } })).toBeNull()
expect(resolveSharedElectronDistEntry({ ...baseOptions, env: { CI: 'true' } })).toBeNull()
expect(resolveSharedElectronDistEntry({ ...baseOptions, env: { CI: 'false' } })).not.toBeNull()
})
it('declines outside a Git worktree so folder workspaces install normally', () => {
const execFile = (() => {
throw new Error('not a git repository')
}) as unknown as typeof execFileSync
expect(resolveSharedElectronDistEntry({ ...baseOptions, execFile })).toBeNull()
})
it('declines an identity that would not be a single safe path segment', () => {
expect(resolveSharedElectronDistEntry({ ...baseOptions, targetArch: '../escape' })).toBeNull()
expect(resolveSharedElectronDistEntry({ ...baseOptions, targetPlatform: 'dar/win' })).toBeNull()
expect(resolveSharedElectronDistEntry({ ...baseOptions, version: '' })).toBeNull()
})
})
describe('isUsableElectronDist', () => {
it('accepts a complete dist and tolerates the leading v in the version file', () => {
const root = makeRoot()
expect(isUsableElectronDist(writeDist(path.join(root, 'dist')), VERSION, PLATFORM_PATH)).toBe(
true
)
})
it('rejects a version mismatch, a missing executable, and a missing directory', () => {
const root = makeRoot()
expect(
isUsableElectronDist(writeDist(path.join(root, 'a'), '40.0.0'), VERSION, PLATFORM_PATH)
).toBe(false)
const partial = path.join(root, 'b')
mkdirSync(partial, { recursive: true })
writeFileSync(path.join(partial, 'version'), `v${VERSION}`)
expect(isUsableElectronDist(partial, VERSION, PLATFORM_PATH)).toBe(false)
expect(isUsableElectronDist(path.join(root, 'missing'), VERSION, PLATFORM_PATH)).toBe(false)
})
it('rejects a symlink so a redirected entry is never treated as cache content', () => {
const root = makeRoot()
writeDist(path.join(root, 'real'))
symlinkSync(path.join(root, 'real'), path.join(root, 'link'), 'dir')
expect(isUsableElectronDist(path.join(root, 'link'), VERSION, PLATFORM_PATH)).toBe(false)
})
})
describe('publishSharedElectronDist', () => {
it('publishes through a staging directory and an atomic rename', () => {
const root = makeRoot()
const entry = makeEntry(root)
const dist = writeDist(path.join(root, 'dist'))
const share = vi.fn((source: string, destination: string) => {
expect(path.basename(destination)).toMatch(/^43\.4\.1-darwin-arm64\.staging-/)
writeDist(destination)
expect(source).toBe(dist)
})
expect(publishSharedElectronDist(dist, entry, { share, ...identity })).toBe(true)
expect(isUsableElectronDist(entry.entryPath, VERSION, PLATFORM_PATH)).toBe(true)
expect(readdirSync(entry.cacheRoot)).toEqual([path.basename(entry.entryPath)])
})
it('publishes the entry read-only, before it is reachable under its final name', () => {
const root = makeRoot()
const entry = makeEntry(root)
const dist = writeDist(path.join(root, 'dist'))
const protectedPaths: string[] = []
const share = (source: string, destination: string) => {
writeDist(destination)
expect(source).toBe(dist)
}
const protect = (target: string) => {
// Why order matters: a reader can clone the entry the instant the rename lands.
expect(existsSync(entry.entryPath)).toBe(false)
protectedPaths.push(target)
makeTreeReadOnly(target)
}
expect(publishSharedElectronDist(dist, entry, { share, protect, ...identity })).toBe(true)
expect(protectedPaths).toHaveLength(1)
expect(statSync(path.join(entry.entryPath, 'version')).mode & 0o222).toBe(0)
// Entry directories stay removable, which is what the install transaction actually needs.
expect(() => rmSync(entry.entryPath, { recursive: true })).not.toThrow()
})
it('never overwrites an entry another worktree already published', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath)
writeFileSync(path.join(entry.entryPath, 'marker'), 'first-writer')
const share = vi.fn()
expect(publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share })).toBe(
false
)
expect(share).not.toHaveBeenCalled()
expect(existsSync(path.join(entry.entryPath, 'marker'))).toBe(true)
})
it('loses a publish race without clobbering the winner or leaking staging', () => {
const root = makeRoot()
const entry = makeEntry(root)
const share = (_source: string, destination: string) => {
writeDist(destination)
// The winner lands between our existence check and our rename.
writeDist(entry.entryPath)
writeFileSync(path.join(entry.entryPath, 'marker'), 'winner')
}
expect(publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share })).toBe(
false
)
expect(existsSync(path.join(entry.entryPath, 'marker'))).toBe(true)
expect(readdirSync(entry.cacheRoot)).toEqual([path.basename(entry.entryPath)])
})
it('keeps a good entry a sibling published while this one was still sharing', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath, '40.0.0') // Unusable, so this worktree intends to replace it.
const share = (_source: string, destination: string) => {
writeDist(destination)
// A sibling replaces the bad entry with a good one while this share is still running.
rmSync(entry.entryPath, { recursive: true, force: true })
writeDist(entry.entryPath)
writeFileSync(path.join(entry.entryPath, 'marker'), 'sibling')
}
expect(
publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share, ...identity })
).toBe(false)
expect(existsSync(path.join(entry.entryPath, 'marker'))).toBe(true)
expect(readdirSync(entry.cacheRoot)).toEqual([path.basename(entry.entryPath)])
})
it('restores the quarantined entry rather than leaving the cache empty', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath, '40.0.0')
writeFileSync(path.join(entry.entryPath, 'marker'), 'stale')
const share = (_source: string, destination: string) => writeDist(destination)
// The swap itself fails; a bad entry still beats no entry, since the next publisher replaces it.
const failingRename = () => {
throw new Error('rename failed')
}
expect(
publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, {
share,
rename: failingRename,
...identity
})
).toBe(false)
expect(existsSync(path.join(entry.entryPath, 'marker'))).toBe(true)
expect(readdirSync(entry.cacheRoot)).toEqual([path.basename(entry.entryPath)])
})
it('replaces an entry that fails validation instead of stranding every worktree', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath, '40.0.0')
const share = (_source: string, destination: string) => writeDist(destination)
expect(
publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share, ...identity })
).toBe(true)
expect(isUsableElectronDist(entry.entryPath, VERSION, PLATFORM_PATH)).toBe(true)
expect(readdirSync(entry.cacheRoot)).toEqual([path.basename(entry.entryPath)])
})
it('never discards an entry it was given no identity to check', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath, '40.0.0')
const share = vi.fn()
expect(publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share })).toBe(
false
)
expect(share).not.toHaveBeenCalled()
expect(existsSync(path.join(entry.entryPath, 'version'))).toBe(true)
})
it('leaves no entry and no staging tree when sharing fails', () => {
const root = makeRoot()
const entry = makeEntry(root)
const share = (_source: string, destination: string) => {
writeDist(destination)
throw new Error('no shareable storage')
}
expect(publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share })).toBe(
false
)
expect(existsSync(entry.entryPath)).toBe(false)
expect(readdirSync(entry.cacheRoot)).toEqual([])
})
})
describe('shareElectronDistFromCache', () => {
it('shares a validated entry with real filesystem semantics', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath)
const stagePath = path.join(root, 'stage')
expect(
shareElectronDistFromCache(entry, stagePath, {
version: VERSION,
platformPath: PLATFORM_PATH
})
).toBe(true)
expect(isUsableElectronDist(stagePath, VERSION, PLATFORM_PATH)).toBe(true)
})
it('refuses an entry that fails validation instead of installing it', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath, '40.0.0')
const stagePath = path.join(root, 'stage')
expect(
shareElectronDistFromCache(entry, stagePath, {
version: VERSION,
platformPath: PLATFORM_PATH
})
).toBe(false)
expect(existsSync(stagePath)).toBe(false)
})
it('reports failure rather than falling back to a full copy', () => {
const root = makeRoot()
const entry = makeEntry(root)
mkdirSync(entry.cacheRoot, { recursive: true })
writeDist(entry.entryPath)
// Injected rather than provoked: what counts as an unshareable destination differs per
// mechanism, and a byte-copy fallback here would defeat the point of the cache.
const stagePath = path.join(root, 'stage')
const share = () => {
throw new Error('no shareable storage')
}
expect(
shareElectronDistFromCache(entry, stagePath, {
version: VERSION,
platformPath: PLATFORM_PATH,
share
})
).toBe(false)
expect(existsSync(stagePath)).toBe(false)
})
})
describe('shared dist marker', () => {
it('reports adoption only for the entry the marker names', () => {
const root = makeRoot()
const entry = makeEntry(root)
expect(hasAdoptedSharedElectronDist(entry)).toBe(false)
recordAdoptedSharedElectronDist(entry, writeFileSync)
expect(hasAdoptedSharedElectronDist(entry)).toBe(true)
expect(
hasAdoptedSharedElectronDist({
...entry,
entryPath: path.join(entry.cacheRoot, '44.0.0-darwin-arm64')
})
).toBe(false)
})
it('swallows a marker write failure, which only costs one extra share', () => {
const entry = makeEntry(makeRoot())
expect(() =>
recordAdoptedSharedElectronDist(entry, () => {
throw new Error('read-only node_modules')
})
).not.toThrow()
})
})
@@ -0,0 +1,70 @@
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* Guard the `.gitattributes` pin that keeps `config/scripts` scripts on LF.
*
* `core.autocrlf=true` ships in the Git-for-Windows system config, so without a
* pin a Windows checkout gets CRLF. Vite's SSR transform locates the shebang
* with `/^#!.*\n/` — `\r` is a JS regex line terminator, so `.` never matches it
* and the pattern misses on CRLF. The hoisted import/export preamble then lands
* at offset 0, ahead of the shebang, which in turn defeats the `code[0] === '#'`
* guard that blanks it. A literal `#!` survives into the middle of the module and
* every suite importing the script dies at load with a SyntaxError.
*
* Scoped to `config/scripts` because that is where tests import scripts. Other
* shebanged `.mjs` in the tree are spawned, not imported, so they cannot hit this.
*/
const projectDir = resolve(import.meta.dirname, '../..')
const SCRIPT_DIRECTORY = 'config/scripts'
function git(args) {
return execFileSync('git', args, { cwd: projectDir, encoding: 'utf8' })
}
/** `git check-attr -z` emits NUL-separated path/attr/value triples. */
function eolAttributes(paths) {
const fields = git(['check-attr', '-z', 'eol', '--', ...paths]).split('\0')
const found = new Map()
for (let index = 0; index + 2 < fields.length; index += 3) {
found.set(fields[index], fields[index + 2])
}
return found
}
function shebangScripts() {
return git(['ls-files', '-z', '--', `${SCRIPT_DIRECTORY}/*.mjs`])
.split('\0')
.filter(Boolean)
.filter((path) => readFileSync(join(projectDir, path), 'utf8').startsWith('#!'))
}
describe('config/scripts line-ending pin', () => {
it('pins every shebanged script to LF', () => {
const scripts = shebangScripts()
expect(scripts.length).toBeGreaterThan(0)
const attributes = eolAttributes(scripts)
const unpinned = scripts.filter((path) => attributes.get(path) !== 'lf')
expect(
unpinned,
'A shebanged script left on the platform default gets CRLF on Windows, ' +
'which makes every suite importing it fail to load. Pin it in .gitattributes.'
).toEqual([])
})
// Why: without these the assertion above still passes against a pattern so broad
// it says nothing, or so narrow it only covers the files that exist today.
it.each([
['config/scripts/example.mjs', 'lf'],
['config/scripts/nested/deeper/example.mjs', 'lf'],
['config/scripts-extra/example.mjs', 'unspecified'],
['vendor/config/scripts/example.mjs', 'unspecified'],
['config/scripts/example.mjsx', 'unspecified']
])('resolves %s to eol=%s', (path, expected) => {
expect(eolAttributes([path]).get(path)).toBe(expected)
})
})
@@ -10,7 +10,28 @@ function stepNamed(job, name) {
}
describe('skill-sharing release workflow', () => {
it('blocks publication on native Windows, macOS, and the Linux floor', () => {
it('keeps artifact builds behind every blocking release gate', () => {
const preflight = workflow.jobs['release-preflight']
const build = workflow.jobs.build
const macBuild = workflow.jobs['build-mac']
expect(preflight.needs).toEqual([
'cut',
'terminal-rendering-golden',
'skill-sharing-release-gate',
'skill-sharing-linux-floor-release-gate'
])
expect(preflight.if).toContain('always()')
expect(preflight.if).toContain("needs.terminal-rendering-golden.result == 'success'")
expect(preflight.if).toContain("needs.skill-sharing-release-gate.result == 'success'")
expect(preflight.if).toContain(
"needs.skill-sharing-linux-floor-release-gate.result == 'success'"
)
expect(build.needs).toContain('release-preflight')
expect(macBuild.needs).toContain('release-preflight')
})
it('blocks on macOS and the Linux floor while keeping Windows diagnostic', () => {
const platform = workflow.jobs['skill-sharing-release-gate']
const linux = workflow.jobs['skill-sharing-linux-floor-release-gate']
const publishNeeds = workflow.jobs['publish-release'].needs
@@ -19,6 +40,7 @@ describe('skill-sharing release workflow', () => {
{ os: 'macos-15', platform: 'mac' },
{ os: 'windows-2022', platform: 'windows' }
])
expect(platform['continue-on-error']).toBe("${{ matrix.platform == 'windows' }}")
expect(linux.container).toBe('ubuntu:20.04')
expect(publishNeeds).toContain('skill-sharing-release-gate')
expect(publishNeeds).toContain('skill-sharing-linux-floor-release-gate')
+175
View File
@@ -0,0 +1,175 @@
import { execFileSync } from 'node:child_process'
import {
chmodSync,
cpSync,
linkSync,
mkdirSync,
readdirSync,
readlinkSync,
rmSync,
statSync,
symlinkSync
} from 'node:fs'
import { join } from 'node:path'
// -c asks for clonefile(2). -P keeps Electron.framework's relative symlinks as symlinks; resolving
// them breaks Chromium's bundle lookup.
export const MACOS_CLONE_ARGS = Object.freeze(['-c', '-R', '-P'])
// -a implies -d (no symlink following) and preserves mode. --reflink=always fails loudly on a
// filesystem without reflinks rather than silently writing a second full copy.
export const LINUX_REFLINK_ARGS = Object.freeze(['--reflink=always', '-a'])
/**
* Copy a directory tree so the destination costs no new storage.
*
* Three mechanisms, strongest isolation first. Clone and reflink are copy-on-write, so the
* destination is genuinely private. Hardlinks are not: the two trees share inodes, and a write
* through either mutates both. That is only sound for a tree nothing writes to, which is why
* `makeTreeReadOnly` exists and why the caller must apply it.
*
* Throws when no mechanism is available, so a caller can fall back to installing normally rather
* than silently paying for a second full copy.
*/
export function shareTree(sourcePath, destinationPath, options = {}) {
const platform = options.platform ?? process.platform
const errors = []
for (const mechanism of getShareMechanisms(platform)) {
try {
;(options[mechanism] ?? shareMechanisms[mechanism])(sourcePath, destinationPath)
return mechanism
} catch (error) {
errors.push(error)
// A mechanism can fail part-way through a tree; the next one needs a clean destination.
rmSync(destinationPath, { recursive: true, force: true })
}
}
throw new AggregateError(errors, `Could not share storage for ${destinationPath}`)
}
function getShareMechanisms(platform) {
switch (platform) {
case 'darwin':
// APFS only. HFS+ has no clonefile, and hardlinking a 585-entry bundle buys little.
return ['clone']
case 'linux':
// reflink covers btrfs/XFS/bcachefs/ZFS; ext4 has none, which is most developers.
return ['reflink', 'hardlink']
case 'win32':
// Block cloning is ReFS-only, so NTFS gets hardlinks or nothing.
return ['hardlink']
default:
return []
}
}
const shareMechanisms = {
clone: (sourcePath, destinationPath) =>
execFileSync('/bin/cp', [...MACOS_CLONE_ARGS, sourcePath, destinationPath], {
stdio: 'ignore'
}),
reflink: (sourcePath, destinationPath) =>
execFileSync('cp', [...LINUX_REFLINK_ARGS, sourcePath, destinationPath], { stdio: 'ignore' }),
hardlink: hardlinkTree
}
export function hardlinkTree(sourcePath, destinationPath) {
mkdirSync(destinationPath, { recursive: true })
for (const entry of readdirSync(sourcePath, { withFileTypes: true })) {
const from = join(sourcePath, entry.name)
const to = join(destinationPath, entry.name)
if (entry.isDirectory()) {
hardlinkTree(from, to)
} else if (entry.isSymbolicLink()) {
symlinkSync(readlinkSync(from), to)
} else {
linkSync(from, to)
}
}
}
/**
* Drop write permission across a tree.
*
* This is what makes hardlink sharing safe: Electron's own install.js extracts over an existing
* dist with O_TRUNC, which through a hardlink would rewrite every sibling worktree and the cache at
* once. Read-only turns that into EPERM. Directories stay writable because unlink needs a writable
* parent, not a writable file, so the install transaction's renames still work.
*/
export function makeTreeReadOnly(targetPath, chmod = chmodSync) {
for (const entry of readdirSync(targetPath, { withFileTypes: true })) {
const entryPath = join(targetPath, entry.name)
if (entry.isDirectory()) {
makeTreeReadOnly(entryPath, chmod)
} else if (!entry.isSymbolicLink()) {
// Clear the write bits and nothing else. A flat 0o555 would strip setuid from
// chrome-sandbox, and under hardlink sharing it would strip it in every worktree at once.
const mode = statSync(entryPath, { throwIfNoEntry: false })?.mode
chmod(entryPath, mode === undefined ? 0o555 : mode & ~0o222)
}
}
chmod(targetPath, 0o755)
}
/**
* Restore owner write permission across a private copy.
*
* Counterpart to `makeTreeReadOnly`: clonefile, reflink and `cpSync` all carry the source's mode
* across, so a tree copied from the write-protected shared cache lands read-only and every patch
* the caller then makes -- `plutil -replace`, `codesign` -- fails with EACCES. Only the owner bit
* comes back; group and other stay as the source left them.
*/
export function makeTreeWritable(targetPath, chmod = chmodSync) {
for (const entry of readdirSync(targetPath, { withFileTypes: true })) {
const entryPath = join(targetPath, entry.name)
if (entry.isDirectory()) {
makeTreeWritable(entryPath, chmod)
} else if (!entry.isSymbolicLink()) {
const mode = statSync(entryPath, { throwIfNoEntry: false })?.mode
chmod(entryPath, mode === undefined ? 0o644 : mode | 0o200)
}
}
chmod(targetPath, 0o755)
}
/**
* Share storage when possible, otherwise copy the bytes.
*
* Never hardlinks: this is for trees the caller goes on to patch, where shared inodes would write
* through into the source. The copy is unprotected on the way out for the same reason -- a private
* tree the caller cannot write to is useless to it.
*/
export function copyPrivateTree(sourcePath, destinationPath, options = {}) {
const platform = options.platform ?? process.platform
const copy = options.copy ?? copyTreeVerbatim
const unprotect = options.unprotect ?? makeTreeWritable
const privateMechanisms = new Set(['clone', 'reflink'])
let result = { mechanism: null, copyError: null }
if (getShareMechanisms(platform).some((mechanism) => privateMechanisms.has(mechanism))) {
try {
result = {
mechanism: shareTree(sourcePath, destinationPath, {
...options,
hardlink: () => {
throw new Error('hardlinks would not be private')
}
}),
copyError: null
}
} catch (copyError) {
copy(sourcePath, destinationPath)
result = { mechanism: null, copyError }
}
} else {
copy(sourcePath, destinationPath)
}
unprotect(destinationPath)
return result
}
function copyTreeVerbatim(sourcePath, destinationPath) {
cpSync(sourcePath, destinationPath, {
recursive: true,
dereference: false,
verbatimSymlinks: true
})
}
+256
View File
@@ -0,0 +1,256 @@
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readlinkSync,
rmSync,
statSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
LINUX_REFLINK_ARGS,
MACOS_CLONE_ARGS,
copyPrivateTree,
hardlinkTree,
makeTreeReadOnly,
makeTreeWritable,
shareTree
} from './space-sharing-copy.mjs'
const roots: string[] = []
afterEach(() => {
while (roots.length > 0) {
rmSync(roots.pop()!, { recursive: true, force: true })
}
})
function makeTree(): { root: string; source: string } {
const root = mkdtempSync(path.join(tmpdir(), 'orca-share-'))
roots.push(root)
const source = path.join(root, 'source')
mkdirSync(path.join(source, 'nested'), { recursive: true })
writeFileSync(path.join(source, 'nested', 'file'), 'contents')
symlinkSync(path.join('nested', 'file'), path.join(source, 'relative-link'))
return { root, source }
}
describe('shareTree', () => {
// Mechanism selection is asserted with stubs, because the real mechanisms only exist on the host
// that owns them: /bin/cp -c is macOS-only and `cp --reflink` is GNU-only.
it('prefers the strongest isolation each platform offers', () => {
const stub = () =>
vi.fn((_source: string, target: string) => mkdirSync(target, { recursive: true }))
const stubs = { clone: stub(), reflink: stub(), hardlink: stub() }
const { root, source } = makeTree()
expect(shareTree(source, path.join(root, 'a'), { platform: 'darwin', ...stubs })).toBe('clone')
expect(shareTree(source, path.join(root, 'b'), { platform: 'linux', ...stubs })).toBe('reflink')
expect(shareTree(source, path.join(root, 'c'), { platform: 'win32', ...stubs })).toBe(
'hardlink'
)
})
it('keeps relative symlinks unresolved on whatever this host supports', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'shared')
expect(shareTree(source, destination)).toBeTruthy()
expect(readFileSync(path.join(destination, 'nested', 'file'), 'utf8')).toBe('contents')
expect(readlinkSync(path.join(destination, 'relative-link'))).toBe(path.join('nested', 'file'))
})
it('falls from reflink to hardlink on Linux, where ext4 has no reflinks', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'shared')
const reflink = vi.fn(() => {
throw new Error('failed to clone: Invalid cross-device link')
})
expect(shareTree(source, destination, { platform: 'linux', reflink })).toBe('hardlink')
expect(reflink).toHaveBeenCalledOnce()
expect(statSync(path.join(destination, 'nested', 'file')).ino).toBe(
statSync(path.join(source, 'nested', 'file')).ino
)
})
it('hardlinks on Windows, the only mechanism NTFS offers', () => {
const { root, source } = makeTree()
expect(shareTree(source, path.join(root, 'shared'), { platform: 'win32' })).toBe('hardlink')
})
it('clears a part-way tree before trying the next mechanism', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'shared')
const reflink = (_source: string, target: string) => {
mkdirSync(target, { recursive: true })
writeFileSync(path.join(target, 'half-written'), 'partial')
throw new Error('reflink failed midway')
}
expect(shareTree(source, destination, { platform: 'linux', reflink })).toBe('hardlink')
expect(existsSync(path.join(destination, 'half-written'))).toBe(false)
})
it('throws rather than silently paying for a second full copy', () => {
const { root, source } = makeTree()
expect(() => shareTree(source, path.join(root, 'shared'), { platform: 'freebsd' })).toThrow(
/Could not share storage/
)
})
it('fails loudly instead of degrading, on both copy-out mechanisms', () => {
expect(MACOS_CLONE_ARGS).toContain('-P')
expect(LINUX_REFLINK_ARGS).toContain('--reflink=always')
})
})
describe('hardlinkTree', () => {
it('shares inodes for files but recreates symlinks as their own entries', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'linked')
hardlinkTree(source, destination)
expect(statSync(path.join(destination, 'nested', 'file')).ino).toBe(
statSync(path.join(source, 'nested', 'file')).ino
)
expect(readlinkSync(path.join(destination, 'relative-link'))).toBe(path.join('nested', 'file'))
})
it('propagates a write through the shared inode, which is why callers must protect it', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'linked')
hardlinkTree(source, destination)
writeFileSync(path.join(destination, 'nested', 'file'), 'mutated')
expect(readFileSync(path.join(source, 'nested', 'file'), 'utf8')).toBe('mutated')
})
})
describe('makeTreeReadOnly', () => {
it('drops write permission on files while leaving directories traversable and unlinkable', () => {
const { source } = makeTree()
makeTreeReadOnly(source)
expect(statSync(path.join(source, 'nested', 'file')).mode & 0o222).toBe(0)
// Asserted as behavior, not mode bits: Windows maps chmod onto the read-only attribute alone,
// so a directory there never reports 0o755. What has to hold everywhere is that the install
// transaction can still rename dist aside and remove it.
expect(() => rmSync(path.join(source, 'nested'), { recursive: true })).not.toThrow()
})
it('turns an extract-over-dist write into an error instead of silent shared corruption', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'linked')
hardlinkTree(source, destination)
makeTreeReadOnly(destination)
expect(() => writeFileSync(path.join(destination, 'nested', 'file'), 'mutated')).toThrow()
expect(readFileSync(path.join(source, 'nested', 'file'), 'utf8')).toBe('contents')
})
it.runIf(process.platform !== 'win32')('preserves setuid, which chrome-sandbox needs', () => {
const { source } = makeTree()
const sandbox = path.join(source, 'chrome-sandbox')
writeFileSync(sandbox, 'binary')
chmodSync(sandbox, 0o4755)
makeTreeReadOnly(source)
expect(statSync(sandbox).mode & 0o4000).toBe(0o4000)
expect(statSync(sandbox).mode & 0o222).toBe(0)
})
it.runIf(process.platform !== 'win32')(
'keeps the executable bit, which Electron needs to launch',
() => {
const { source } = makeTree()
const executable = path.join(source, 'electron')
writeFileSync(executable, 'binary', { mode: 0o755 })
makeTreeReadOnly(source)
// Verified on real ext4: 0o555. Windows has no execute bit -- the read-only attribute does
// not gate execution there, confirmed by running a read-only hardlinked .exe on NTFS.
expect(statSync(executable).mode & 0o111).toBe(0o111)
}
)
})
describe('makeTreeWritable', () => {
it.runIf(process.platform !== 'win32')('undoes makeTreeReadOnly for the owner', () => {
const { source } = makeTree()
makeTreeReadOnly(source)
makeTreeWritable(source)
const file = path.join(source, 'nested', 'file')
expect(statSync(file).mode & 0o200).toBe(0o200)
expect(() => writeFileSync(file, 'mutated')).not.toThrow()
})
it.runIf(process.platform !== 'win32')('adds no write permission beyond the owner', () => {
const { source } = makeTree()
const executable = path.join(source, 'electron')
writeFileSync(executable, 'binary')
chmodSync(executable, 0o555)
makeTreeWritable(source)
expect(statSync(executable).mode & 0o777).toBe(0o755)
})
})
describe('copyPrivateTree', () => {
it.runIf(process.platform !== 'win32')(
'hands back a tree the caller can patch, even from a write-protected source',
() => {
const { root, source } = makeTree()
const destination = path.join(root, 'private')
makeTreeReadOnly(source)
copyPrivateTree(source, destination)
// The regression this guards: the shared Electron dist is read-only, clonefile/reflink/cpSync
// all carry that across, and `pn dev` then died patching the copied bundle's Info.plist.
expect(() => writeFileSync(path.join(destination, 'nested', 'file'), 'patched')).not.toThrow()
expect(readFileSync(path.join(source, 'nested', 'file'), 'utf8')).toBe('contents')
}
)
it('never hardlinks, because the caller patches what it gets back', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'private')
const hardlink = vi.fn()
const result = copyPrivateTree(source, destination, { platform: 'linux', hardlink })
expect(hardlink).not.toHaveBeenCalled()
expect(statSync(path.join(destination, 'nested', 'file')).ino).not.toBe(
statSync(path.join(source, 'nested', 'file')).ino
)
expect(result.mechanism === 'reflink' || result.mechanism === null).toBe(true)
})
it('copies bytes on a platform with no private mechanism at all', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'private')
const hardlink = vi.fn()
expect(copyPrivateTree(source, destination, { platform: 'win32', hardlink })).toEqual({
mechanism: null,
copyError: null
})
expect(hardlink).not.toHaveBeenCalled()
expect(readFileSync(path.join(destination, 'nested', 'file'), 'utf8')).toBe('contents')
expect(readlinkSync(path.join(destination, 'relative-link'))).toBe(path.join('nested', 'file'))
})
it('reports the private mechanism it used', () => {
const { root, source } = makeTree()
const clone = vi.fn((_source: string, target: string) => mkdirSync(target, { recursive: true }))
expect(
copyPrivateTree(source, path.join(root, 'private'), { platform: 'darwin', clone })
).toEqual({
mechanism: 'clone',
copyError: null
})
})
it('falls back to a byte copy when the private mechanism fails', () => {
const { root, source } = makeTree()
const destination = path.join(root, 'private')
const clone = () => {
throw new Error('clonefile unsupported')
}
const result = copyPrivateTree(source, destination, { platform: 'darwin', clone })
expect(result.mechanism).toBeNull()
expect(result.copyError).toBeInstanceOf(Error)
expect(readFileSync(path.join(destination, 'nested', 'file'), 'utf8')).toBe('contents')
})
})
@@ -0,0 +1,172 @@
import { readFileSync, readdirSync } from 'node:fs'
import { join, relative } from 'node:path'
import { readCallOptionKeys } from './call-site-option-keys'
/**
* Locate every `new WebSocketServer(...)` in the tree and say, for each, whether
* it pins a bind address.
*
* `ws` accepts `{ port }` alone and silently binds the wildcard address, so a
* server the caller then dials on 127.0.0.1 sits at a port a foreign loopback
* listener can also hold -- and the more specific listener wins the connection,
* answering in that server's place.
*
* Anything unreadable is reported as `opaque` rather than skipped. A matcher
* that silently exempts the shapes it fails to parse is worse than no matcher,
* because it reads as coverage.
*/
export type BindSite = { path: string; line: number }
export type OpaqueSite = BindSite & { reason: string }
export type WebSocketServerBindScan = {
filesScanned: number
/** Every construction recognized, however it was then classified. */
constructions: number
/** Binds a port with no `host`: reachable at an address the dialer never named. */
wildcardBound: BindSite[]
/** Shape that could not be read; never treated as safe. */
opaque: OpaqueSite[]
/** Binds a port and pins `host`. */
loopbackBound: BindSite[]
/** No `port`: attaches to a server that owns the bind itself. */
attached: BindSite[]
}
const IGNORED_DIRECTORIES = new Set([
'node_modules',
'dist',
'out',
'build',
'.git',
'__fixtures__',
'coverage',
// Full snapshots of older releases; their bind sites are not this tree's to fix.
'.cross-version-checkouts'
])
const SCANNED_EXTENSIONS = /\.(?:ts|tsx|mts|cts)$/
const SCANNED_ROOTS = ['src', 'mobile', 'config', 'tests']
const WS_IMPORT_HINT = /from\s*['"]ws['"]/
function collectSourceFiles(root: string, found: string[] = []): string[] {
let entries: ReturnType<typeof readdirSync<{ withFileTypes: true }>>
try {
entries = readdirSync(root, { withFileTypes: true })
} catch {
return found
}
for (const entry of entries) {
if (IGNORED_DIRECTORIES.has(entry.name)) {
continue
}
const full = join(root, entry.name)
if (entry.isDirectory()) {
collectSourceFiles(full, found)
} else if (SCANNED_EXTENSIONS.test(entry.name)) {
found.push(full)
}
}
return found
}
/** Local names bound to ws's server class, following `as` aliases and namespace imports. */
function webSocketServerNames(text: string): { direct: Set<string>; namespaces: Set<string> } {
const direct = new Set<string>()
const namespaces = new Set<string>()
// One statement at a time: a pattern reaching for `from 'ws'` would swallow
// every import above it and lose the specifier names in the blob.
for (const match of text.matchAll(/\bimport\b([\s\S]*?)\bfrom\s*(['"])([^'"]+)\2/g)) {
if (match[3] !== 'ws') {
continue
}
const clause = match[1]
if (/^\s*type\b/.test(clause)) {
continue
}
const namespace = clause.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/)
if (namespace) {
namespaces.add(namespace[1])
}
const named = clause.match(/\{([\s\S]*)\}/)
if (!named) {
continue
}
for (const specifier of named[1].split(',')) {
const trimmed = specifier.trim()
if (!trimmed || /^type\s/.test(trimmed)) {
continue
}
const parts = trimmed.split(/\s+as\s+/)
// `Server` is ws's own alias for WebSocketServer.
if (parts[0].trim() === 'WebSocketServer' || parts[0].trim() === 'Server') {
direct.add((parts[1] ?? parts[0]).trim())
}
}
}
return { direct, namespaces }
}
function classify(
scan: WebSocketServerBindScan,
site: BindSite,
text: string,
paren: number
): void {
const options = readCallOptionKeys(text, paren)
if (!options.readable) {
scan.opaque.push({ ...site, reason: options.reason })
return
}
if (!options.keys.includes('port')) {
scan.attached.push(site)
return
}
if (!options.keys.includes('host')) {
scan.wildcardBound.push(site)
return
}
scan.loopbackBound.push(site)
}
export function scanWebSocketServerBinds(repoRoot: string): WebSocketServerBindScan {
const files = SCANNED_ROOTS.flatMap((directory) => collectSourceFiles(join(repoRoot, directory)))
const scan: WebSocketServerBindScan = {
filesScanned: files.length,
constructions: 0,
wildcardBound: [],
opaque: [],
loopbackBound: [],
attached: []
}
for (const file of files) {
const text = readFileSync(file, 'utf8')
// Filter on the import, not on the class name: `Server as Wss` never spells
// WebSocketServer, and keying on that name silently skipped the whole alias.
if (!WS_IMPORT_HINT.test(text)) {
continue
}
const { direct, namespaces } = webSocketServerNames(text)
if (!direct.size && !namespaces.size) {
continue
}
const path = relative(repoRoot, file).split('\\').join('/')
const patterns = [
...[...direct].map((name) => new RegExp(`\\bnew\\s+${name}\\s*\\(`, 'g')),
...[...namespaces].map(
(name) => new RegExp(`\\bnew\\s+${name}\\.(?:WebSocketServer|Server)\\s*\\(`, 'g')
)
]
for (const pattern of patterns) {
for (const match of text.matchAll(pattern)) {
scan.constructions++
const line = text.slice(0, match.index).split('\n').length
classify(scan, { path, line }, text, match.index + match[0].length - 1)
}
}
}
return scan
}
export function formatSites(sites: readonly BindSite[]): string[] {
return sites.map((site) => `${site.path}:${site.line}`)
}
@@ -0,0 +1,106 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { formatSites, scanWebSocketServerBinds } from './websocket-server-bind-scan'
/**
* Hold the bind address at the tree level rather than per call site.
*
* Every one of the ~30 `.listen(0, ...)` calls in this repo already passes
* '127.0.0.1'; 7 of 7 `new WebSocketServer({ port })` calls did not. Authors know
* the convention -- `ws` just never asks, because `{ port }` alone binds the
* wildcard without a word. That silence is what this test replaces.
*
* The allowlist only shrinks. A new wildcard bind fails here even where it looks
* harmless today, because harmless-looking is exactly what the seven were.
*/
/** The ratchet, held as data so it reads as the list it is. */
const WILDCARD_BIND_ALLOWLIST: readonly string[] = readFileSync(
join(__dirname, '__fixtures__', 'websocket-server-wildcard-bind-allowlist.txt'),
'utf8'
)
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith('#'))
/**
* The true count of constructions that bind a port without pinning a host.
*
* May only ever be DECREASED, and only by pinning a host. Raising it is never
* the fix.
*/
const WILDCARD_BIND_PIN = 1
/**
* A floor under the constructions the scanner still recognizes.
*
* This is the guard against the scanner going blind: an import pattern it stops
* following reports zero offenders and reads exactly like a clean tree. During
* development a single wrong regex dropped this from 24 to 3.
*/
const RECOGNIZED_CONSTRUCTION_FLOOR = 20
describe('WebSocketServer loopback bind boundary', () => {
const repoRoot = resolve(__dirname, '..', '..')
const scan = scanWebSocketServerBinds(repoRoot)
const offenders = scan.wildcardBound.map((site) => site.path)
it('scans a plausible number of files', () => {
// A broken root or extension list would make the guard silently vacuous.
expect(scan.filesScanned).toBeGreaterThan(5_000)
})
it('still recognizes the known construction sites', () => {
expect(
scan.constructions,
`Only ${scan.constructions} WebSocketServer constructions were recognized; the floor is ` +
`${RECOGNIZED_CONSTRUCTION_FLOOR}. The scanner has probably stopped following an import ` +
'shape rather than the tree having lost that many servers.'
).toBeGreaterThanOrEqual(RECOGNIZED_CONSTRUCTION_FLOOR)
})
it('can read the options of every construction it found', () => {
// An unreadable shape is never assumed safe: it could be hiding a host, or
// hiding the absence of one. Rewrite it as a plain object literal.
expect(
scan.opaque.map((site) => `${site.path}:${site.line} -- ${site.reason}`),
'WebSocketServer options that this guard cannot read.'
).toEqual([])
})
it('has no wildcard-bound server outside the allowlist', () => {
const unlisted = scan.wildcardBound.filter(
(site) => !WILDCARD_BIND_ALLOWLIST.includes(site.path)
)
expect(
formatSites(unlisted),
"New WebSocketServer that binds a port without a host. Pass host: '127.0.0.1' so a foreign " +
'loopback listener cannot claim the port and answer in its place.'
).toEqual([])
})
it('has no stale allowlist entry', () => {
// Why this direction matters too: an entry left behind after the file was
// fixed hides the next regression in that same path.
const stale = WILDCARD_BIND_ALLOWLIST.filter((path) => !offenders.includes(path))
expect(stale, 'Allowlist entry no longer binds the wildcard — delete the line.').toEqual([])
})
it('holds the wildcard-bind count at the pin', () => {
// Bounding by the allowlist's own length would prove nothing: the two move
// together, so appending a line to silence a failure would keep the bound
// satisfied. The pin is a literal so that widening takes a second edit.
expect(
scan.wildcardBound.length,
`${scan.wildcardBound.length} constructions bind the wildcard; the pin is ` +
`${WILDCARD_BIND_PIN}. Never raise the pin -- pass host: '127.0.0.1' instead.`
).toBeLessThanOrEqual(WILDCARD_BIND_PIN)
// A pin left above reality is how a ratchet rots: it re-opens room for the
// next wildcard bind to land for free.
expect(
scan.wildcardBound.length,
`Only ${scan.wildcardBound.length} constructions bind the wildcard. Lower ` +
`WILDCARD_BIND_PIN to ${scan.wildcardBound.length} to keep the ground you just took.`
).toBeGreaterThanOrEqual(WILDCARD_BIND_PIN)
})
})
@@ -0,0 +1,673 @@
import { readFileSync, statSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import { scanSourceTree, stripComments } from '../../src/shared/source-scan/source-tree-scan'
import { classifyPrJobs } from './pr-code-change-scope.mjs'
/**
* Every Windows-gated test file must be registered in BOTH Windows-lane lists.
*
* PR CI has exactly one job on a Windows runner -- asserted below on any
* `runs-on` spelling that could land there, because that premise is what makes
* this guard meaningful -- and it runs a curated explicit file list. Everything else runs on `ubuntu-latest`, where a Windows-gated
* suite self-skips and reports success. So a new Windows-gated file that nobody
* registers executes on no machine and passes green, silently. A recent
* security effort added six such files; five ran nowhere, including one whose
* whole point was asserting a native addon's bytes no longer contain a flagged
* primitive. Registering the instances did not hold -- a sixth arrived from
* unrelated work while the first five were being fixed -- so the class needs a
* guard.
*
* Both lists matter and being in one is not enough: `WINDOWS_PACKAGE_TESTS` in
* pr-code-change-scope.mjs decides whether the `package_windows` job RUNS at
* all for a diff, and the workflow step's vitest argv decides whether the FILE
* runs once the job started.
*
* WHAT THIS DETECTS -- a file is Windows-gated when its name is `*.win32.test.*`
* / `*.win32.spec.*`, or when it contains ANY suite-level gate, nested ones
* included, spelled:
* - `describe.runIf(<true only on win32>)`, `describe.skipIf(<false only on win32>)`
* - `const d = <true only on win32> ? describe : describe.skip`, and the
* `? describe.skip : describe` inversion
* where the condition is `process.platform === 'win32'` / `!== 'win32'`, a
* compound `<win32 check> && <anything>`, or a `const`/`let` in the same file
* assigned from either -- so `const RUN_REAL = platform === 'win32' && env…`
* used as `describe.runIf(RUN_REAL)` is detected, whatever the flag is named
* and whichever polarity it was written in. Quote style, spacing and the
* `describe`/`suite` spelling are tolerated. Nested gates count because the
* Windows lane runs whole files: a win32-only block buried three levels down
* still runs on no machine unless the file is registered.
*
* WHAT THIS CANNOT DETECT -- known blind spots, each deliberate:
* - `it`/`test`-level gates. A single win32-only case inside a cross-platform
* suite still leaves the file running its other cases on ubuntu, and
* pulling all such files -- about thirty, though the figure moves with
* which gate spellings you count, so do not lean on it -- into the serial
* Windows job is not the trade CI wants. This is the largest limit, and it
* is a policy choice, not an oversight: a suite-level gate means a whole
* block exists only for Windows, which is the shape worth a lane entry.
* - a gate whose condition crosses a module boundary or a function call --
* an imported flag, an imported `describeOnWindows`, `isWindows()`.
* `legacy-wsl-runtime-auth-drain-apply-script.test.ts` imports its
* `isWindows`; it happens to be a POSIX-only gate, so nothing is missed
* today, but a win32-only one written that way would be.
* - `runIf(<win32> || <x>)` and `skipIf(<not win32> && <x>)` are rejected on
* purpose: both can run off Windows, so neither is a win32-only gate. That
* holds whether the condition is written at the gate or routed through a
* named flag -- the two spellings used to disagree.
* - whether a registered suite EXECUTES. Registration is what is asserted. A
* suite gated on win32 plus an env var stays skipped on the CI runner even
* when registered -- see MANUAL_OPT_IN -- and a path registered but gated
* for another platform is not caught either.
* - whether the `package_windows` job is triggered for a given diff, or
* whether the registered test asserts anything worth running.
*
* Growth of the two grandfathered lists is capped by literals, but only review
* stops someone raising a cap. The caps make that an explicit, visible edit.
*/
const projectDir = resolve(import.meta.dirname, '../..')
const WINDOWS_LANE_JOB = 'package_windows'
const WINDOWS_LANE_STEP = 'Test Windows-specific boundaries'
const WINDOWS_LANE_RUNNER = 'windows-2022'
/**
* Windows-gated files that predate this guard and are registered in neither
* list. Shrink-only: registering one means deleting its line here. Never add.
*/
const UNREGISTERED_ON_MAIN = [
// Suite gated with `describe.skipIf(platform !== 'win32')`; the cross-platform
// half of the file still runs on ubuntu, the Windows half runs nowhere.
'src/main/antigravity/windows-hook-payload-delivery.test.ts',
// `.win32.test.ts` by name yet in neither list -- the plainest instance of the class.
'src/main/daemon/node-pty-windows-input-error.win32.test.ts',
// Same shape as the antigravity file: a win32-only sibling suite that never runs.
'src/main/grok/windows-grok-hook-script.test.ts',
// Whole file is `describe.runIf(platform === 'win32')`; runs on no machine.
'src/main/ipc/preflight-windows-path-refresh.repro.test.ts',
// Nested `describe.skipIf(!isWindows)` real-shell block; never exercised in CI.
'src/main/ipc/pty-encoding.test.ts',
// `describeWindows` ternary over the whole file; runs on no machine.
'src/main/providers/windows-shell-preflight-runtime.windows.test.ts',
// Whole file is `describe.runIf(platform === 'win32')`; runs on no machine.
'src/main/startup/windows-shell-path-restoration.windows.test.ts',
// Whole file is `describe.skipIf(platform !== 'win32')`; runs on no machine.
'src/shared/setup-agent-sequencing.windows.test.ts'
]
/**
* Windows-gated suites that ALSO require an opt-in env var, so registering them
* would not make them execute -- they are run by hand against a real distro or
* a real filesystem. Excluded deliberately and visibly rather than by accident
* of a regex; each entry is asserted below to be genuinely env-gated, so this
* list cannot become a place to park a file someone did not want to register.
*/
const MANUAL_OPT_IN = [
// `runIf(platform === 'win32' && Boolean(distro))`, distro from ORCA_TEST_WSL_DISTRO.
'src/main/git/runner-wsl-linked-gitdir-windows.test.ts',
// `runRealWsl = … && ORCA_REAL_WSL_BANNER_TEST === '1'`; needs a real distro.
'src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts',
// `RUN_REAL_WINDOWS = platform === 'win32' && ORCA_REAL_WINDOWS_SKILL_TEST === '1'`.
'src/main/skills/skill-windows-rename-contention.integration.test.ts',
// Same flag; installs into a real Windows workspace.
'src/main/skills/skill-windows-workspace.integration.test.ts',
// `RUN_REAL_WSL = … && ORCA_REAL_WSL_SKILL_TEST === '1'`; real distro filesystem.
'src/main/skills/skill-wsl-delete.integration.test.ts',
// Same flag; real WSL install transactions.
'src/main/skills/skill-wsl-install-transaction.integration.test.ts',
// Same flag; real WSL POSIX semantics.
'src/main/skills/skill-wsl-posix-semantics.integration.test.ts',
// `runRealWsl = … && ORCA_REAL_WSL_DELETE_TEST === '1'`; real distro traversal race.
'src/main/wsl-approved-root-race.wsl.test.ts',
// Same flag; real UNC delete against a distro.
'src/main/wsl-unc-delete.wsl.test.ts',
// `enabled = platform === 'win32' && ORCA_REAL_WSL_RUNNER_TEST === '1'`; mutates a real distro's ~/.profile.
'src/main/wsl/wsl-runner.wsl.test.ts'
]
/** Caps so growing either list is two deliberate edits, not one. */
const UNREGISTERED_MAX = 8
const MANUAL_OPT_IN_MAX = 10
/**
* Floor for the Windows-gated population, so a broken walk or a regex that
* stops matching cannot make the guard pass by finding nothing. Only ever
* lowered, and only when a gated file is genuinely deleted.
*/
const GATED_FILE_FLOOR = 23
const TEST_FILE_PATTERN = /\.(?:test|spec)\.(?:ts|tsx|mjs|cjs|js)$/
/**
* Mobile has its own vitest run and never touches the desktop Windows job:
* `classifyPrJobs` reports `package_windows: false` for every `mobile/` path,
* so a gated file there could not satisfy this guard even in principle.
*/
const UNREACHABLE_BY_THE_WINDOWS_LANE = 'mobile/'
/**
* This file quotes every gate spelling as a fixture, so it matches its own
* matcher. It is not gated -- it must run on ubuntu, since a guard about
* Windows CI that only ran on Windows would be self-defeating. Exempt by exact
* path, never by directory, so a real gated file in config/scripts is caught.
*/
const SCANNER_SELF_PATH = 'config/scripts/win32-test-lane-registration.test.mjs'
export function isScannerSelfPath(path) {
return path === SCANNER_SELF_PATH
}
const WIN32_TRUE_EXPRESSION = String.raw`process\.platform\s*===\s*['"]win32['"]`
const WIN32_FALSE_EXPRESSION = String.raw`process\.platform\s*!==\s*['"]win32['"]`
const SUITE = String.raw`(?:describe|suite)`
/**
* Named flags resolved from their assignment in the same file, so polarity is
* read rather than guessed from the name.
*
* Why the trailing lookahead: `const d = platform === 'win32' ? describe : …`
* is a suite alias, not a boolean, and must not be collected as one.
*
* Why the two patterns differ on `&&`: a second conjunct NARROWS a
* truthy-on-Windows flag, which stays Windows-only, but WIDENS a
* falsy-on-Windows one -- `p = platform !== 'win32' && x` used as `skipIf(p)`
* runs on Windows AND on POSIX whenever `x` is false, so it is not a
* Windows-only gate. One lookahead shared across both polarities had that
* backwards, and routing the condition through a named flag flipped the answer
* the literal form got right. `||` is excluded from both.
*/
const FLAG_TRUE_ASSIGNMENT = new RegExp(
String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${WIN32_TRUE_EXPRESSION}(?=\s*(?:&&|;|\r?\n|$))`,
'g'
)
const FLAG_FALSE_ASSIGNMENT = new RegExp(
String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${WIN32_FALSE_EXPRESSION}(?=\s*(?:;|\r?\n|$))`,
'g'
)
function escapeForAlternation(name) {
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/** Never-matching branch, so an empty flag set cannot widen a pattern. */
const MATCHES_NOTHING = String.raw`(?!)`
function alternation(names) {
return names.length === 0 ? MATCHES_NOTHING : names.map(escapeForAlternation).join('|')
}
function buildGates(source) {
const trueOnWindows = [...source.matchAll(FLAG_TRUE_ASSIGNMENT)].map(([, name]) => name)
const falseOnWindows = [...source.matchAll(FLAG_FALSE_ASSIGNMENT)].map(([, name]) => name)
const isTrue = `(?:${WIN32_TRUE_EXPRESSION}|\\b(?:${alternation(trueOnWindows)})\\b)`
const isFalse = `(?:${WIN32_FALSE_EXPRESSION}|!\\s*(?:${alternation(trueOnWindows)})\\b|\\b(?:${alternation(falseOnWindows)})\\b)`
return [
// `\)` or `&&` after the condition: a bare gate, or a compound one whose
// remaining conjuncts only narrow it further. Anchoring on `\)` alone was
// this guard's own bug -- `runIf(win32 && hasAddon)` went undetected.
new RegExp(String.raw`\b${SUITE}\s*\.\s*runIf\s*\(\s*${isTrue}\s*(?:\)|&&)`),
new RegExp(String.raw`\b${SUITE}\s*\.\s*skipIf\s*\(\s*${isFalse}\s*(?:\)|\|\|)`),
// `(?!\s*\.\s*skip)`: `platform === 'win32' ? describe.skip : describe` is
// the POSIX-only gate, the exact opposite of the class, and seven files
// use it.
new RegExp(String.raw`=\s*${isTrue}\s*\?\s*${SUITE}\s*(?!\s*\.\s*skip)`),
new RegExp(String.raw`=\s*${isFalse}\s*\?\s*${SUITE}\s*\.\s*skip`)
]
}
/** Exported shape of the rule, so the fixtures below exercise the real matcher. */
export function isWindows32GatedTestFile(path, source) {
if (/\.win32\.(?:test|spec)\./.test(path)) {
return true
}
// Prose about a gate is not a gate; the shared stripper tracks quote state so
// a slash-star inside a string cannot blank live code.
const code = stripComments(source)
return buildGates(code).some((gate) => gate.test(code))
}
/**
* True when the env read REACHES the gate: the win32 check is compound, and one
* of its other conjuncts either reads `process.env` itself or names a const
* that does.
*
* "Mentions an env var anywhere in the file" is not enough and was the earlier
* bug here. `runIf(platform === 'win32' && hasAddon)` in a file that happens to
* read `process.env.RUNNER_TEMP` for a temp dir is a test CI COULD run -- the
* native-addon-bytes shape, exactly what this effort exists to keep in CI --
* and it would have parked in MANUAL_OPT_IN unnoticed. Only the cap number
* stood in the way, and a number is not an argument.
*
* One hop is enough for every real case: `distro = process.env.ORCA_TEST_WSL_DISTRO`
* then `runIf(platform === 'win32' && Boolean(distro))`. Deeper chains fail
* closed -- the file reads as registrable, which is the safe direction.
*/
const WIN32_CONJUNCT = new RegExp(String.raw`${WIN32_TRUE_EXPRESSION}\s*&&([^\n]*)`, 'g')
const ENV_READ = /process\.env\.[A-Za-z0-9_]+/
const IDENTIFIER = /[A-Za-z_$][\w$]*/g
function isAssignedFromEnv(name, code) {
return new RegExp(
String.raw`(?:const|let|var)\s+${escapeForAlternation(name)}\s*=[^\n]*process\.env\.`
).test(code)
}
export function requiresEnvOptIn(source) {
const code = stripComments(source)
return [...code.matchAll(WIN32_CONJUNCT)].some(([, conjunct]) => {
if (ENV_READ.test(conjunct)) {
return true
}
return [...conjunct.matchAll(IDENTIFIER)].some(([name]) => isAssignedFromEnv(name, code))
})
}
/**
* Any `runs-on` that could put a job on Windows.
*
* Not an equality test against `windows-2022`: `windows-latest` resolves to the
* same image today, a label array or `{ group, labels }` object is valid YAML
* here, and a `${{ matrix.os }}` expression cannot be resolved from the file at
* all. An unresolvable expression counts as "could be Windows" so it fails
* closed -- someone has to look rather than have a second lane appear silently.
*/
export function couldRunOnWindows(runsOn) {
const labels =
typeof runsOn === 'string'
? [runsOn]
: Array.isArray(runsOn)
? runsOn
: [...(runsOn?.labels ?? []), runsOn?.group ?? ''].flat()
return labels.some((label) => /windows/i.test(String(label)) || String(label).includes('${{'))
}
/** The vitest argv of the one Windows job's one curated-file step. */
function readWindowsWorkflow() {
const workflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
const jobs = Object.entries(workflow.jobs ?? {})
const windowsJobs = jobs.filter(([, job]) => couldRunOnWindows(job?.['runs-on']))
const steps = workflow.jobs?.[WINDOWS_LANE_JOB]?.steps ?? []
const step = steps.find((candidate) => candidate?.name === WINDOWS_LANE_STEP)
if (!step) {
throw new Error(
`No "${WINDOWS_LANE_STEP}" step in the ${WINDOWS_LANE_JOB} job of .github/workflows/pr.yml. ` +
'If it was renamed, update WINDOWS_LANE_STEP here -- do not delete this guard.'
)
}
const run = String(step.run ?? '')
if (!run.includes('vitest run')) {
throw new Error(
`The "${WINDOWS_LANE_STEP}" step no longer invokes vitest; this guard is stale.`
)
}
return {
windowsJobNames: windowsJobs.map(([name]) => name),
laneFiles: run.split(/\s+/).filter((token) => TEST_FILE_PATTERN.test(token))
}
}
const { windowsJobNames, laneFiles } = readWindowsWorkflow()
const scannedTestFiles = scanSourceTree(projectDir, {
includeTests: true,
extensions: TEST_FILE_PATTERN
}).filter(({ relativePath }) => !relativePath.startsWith(UNREACHABLE_BY_THE_WINDOWS_LANE))
const gatedFiles = scannedTestFiles
.filter(({ relativePath }) => !isScannerSelfPath(relativePath))
.filter(({ relativePath, source }) => isWindows32GatedTestFile(relativePath, source))
.map(({ relativePath }) => relativePath)
/**
* Why the classifier and not the literal list: `WINDOWS_PACKAGE_TESTS` is not
* exported, and the classifier is what CI actually consults. It inherits
* `classifyPrJobs`'s force-all, so a path under GLOBAL_FORCE_PREFIXES would
* read as registered without being listed -- no test file is one today.
*/
function isInClassifier(path) {
return classifyPrJobs([path])[WINDOWS_LANE_JOB] === true
}
function registrationFailure(path) {
const missing = []
if (!laneFiles.includes(path)) {
missing.push(
`add "${path}" to the "${WINDOWS_LANE_STEP}" vitest argv in .github/workflows/pr.yml ` +
`(job ${WINDOWS_LANE_JOB})`
)
}
if (!isInClassifier(path)) {
missing.push(
`add '${path}' to WINDOWS_PACKAGE_TESTS in config/scripts/pr-code-change-scope.mjs`
)
}
return missing.length === 0 ? null : `${path}: ${missing.join('; and ')}`
}
function sourceOf(path) {
return readFileSync(join(projectDir, path), 'utf8')
}
describe('Windows-gated test files are registered in the Windows CI lane', () => {
it('scans a plausible number of test files', () => {
// A broken root or extension filter would make every assertion below vacuous.
expect(scannedTestFiles.length).toBeGreaterThan(5000)
})
it('has exactly one windows-2022 job to register into', () => {
// The whole premise: one Windows lane, one curated list. A second lane would
// mean a file could be registered in the wrong one and still run nowhere.
expect(
windowsJobNames,
`Expected only ${WINDOWS_LANE_JOB} to run on ${WINDOWS_LANE_RUNNER}.`
).toEqual([WINDOWS_LANE_JOB])
})
it('parses a plausible Windows lane invocation', () => {
expect(laneFiles.length).toBeGreaterThan(15)
const missingFromDisk = laneFiles.filter((path) => {
try {
return !statSync(join(projectDir, path)).isFile()
} catch {
return true
}
})
expect(
missingFromDisk,
'The Windows lane invokes vitest on paths that do not exist -- vitest will run nothing for them.'
).toEqual([])
})
it('rediscovers Windows-gated files that are already registered', () => {
// Both discovery paths, proven against real files rather than fixtures: one
// found by filename plus ternary alias, one found only by its gate
// expression because its name says nothing about Windows gating.
expect(gatedFiles).toContain('src/shared/child-process/windows-command-line.win32.test.ts')
expect(gatedFiles).toContain('src/main/agent-hooks/windows-hook-payload-delivery.test.ts')
// And a compound gate, the case this guard was blind to at first.
expect(gatedFiles).toContain('src/main/git/runner-wsl-linked-gitdir-windows.test.ts')
})
it('exempts itself, and nothing else, from the scan', () => {
expect(scannedTestFiles.map(({ relativePath }) => relativePath)).toContain(SCANNER_SELF_PATH)
// The exemption is load-bearing only while the fixtures below still match.
expect(isWindows32GatedTestFile(SCANNER_SELF_PATH, sourceOf(SCANNER_SELF_PATH))).toBe(true)
expect(gatedFiles).not.toContain(SCANNER_SELF_PATH)
// The other half of the claim: no sibling rides the exemption.
expect(isScannerSelfPath('config/scripts/pr-code-change-scope.test.mjs')).toBe(false)
})
it('holds the Windows-gated population at or above the floor', () => {
// Bounding by the grandfathered lists' lengths would be trivially true --
// they move together. The floor is a literal for that reason.
expect(
gatedFiles.length,
`Found ${gatedFiles.length} Windows-gated test files; the floor is ${GATED_FILE_FLOOR}. ` +
'A drop means the scan stopped matching, not that the files went away. Lower the floor ' +
'only for a genuine deletion.'
).toBeGreaterThanOrEqual(GATED_FILE_FLOOR)
})
it('confirms the classifier distinguishes registered from unregistered paths', () => {
// Without this, a classifier that answered true for everything would make
// the registration assertion below pass for free.
expect(isInClassifier('src/main/windows/windows-pty-job.win32.test.ts')).toBe(true)
expect(isInClassifier('src/main/windows/not-a-real-file.win32.test.ts')).toBe(false)
})
it('has every Windows-gated test file in both registration lists', () => {
const grandfathered = new Set([...UNREGISTERED_ON_MAIN, ...MANUAL_OPT_IN])
const failures = gatedFiles
.filter((path) => !grandfathered.has(path))
.map(registrationFailure)
.filter((failure) => failure !== null)
expect(
failures,
'A Windows-gated test file is missing from a Windows CI registration list. It self-skips on ' +
'ubuntu and reports success, so it runs on no machine. Both lists are required: ' +
'WINDOWS_PACKAGE_TESTS decides whether the package_windows job runs for a diff, the ' +
'workflow argv decides whether the file runs once it started. Fix each line below.'
).toEqual([])
})
it('has no stale entry in either grandfathered list', () => {
const stale = [...UNREGISTERED_ON_MAIN, ...MANUAL_OPT_IN].filter(
(path) => !gatedFiles.includes(path) || registrationFailure(path) === null
)
expect(
stale,
'These files are no longer unregistered Windows-gated debt -- they were registered, ' +
'renamed, un-gated, or deleted. Delete each line from UNREGISTERED_ON_MAIN or ' +
'MANUAL_OPT_IN; the lists only ever shrink.'
).toEqual([])
})
it('caps growth of both grandfathered lists', () => {
expect(
UNREGISTERED_ON_MAIN.length,
'Never raise UNREGISTERED_MAX. Register the file instead.'
).toBeLessThanOrEqual(UNREGISTERED_MAX)
expect(
MANUAL_OPT_IN.length,
'Never raise MANUAL_OPT_IN_MAX to avoid registering a file that CI could actually run.'
).toBeLessThanOrEqual(MANUAL_OPT_IN_MAX)
})
it('keeps MANUAL_OPT_IN to suites CI genuinely cannot run', () => {
// Otherwise this list is just a quieter way to skip registration.
const notActuallyOptIn = MANUAL_OPT_IN.filter((path) => !requiresEnvOptIn(sourceOf(path)))
expect(
notActuallyOptIn,
'A MANUAL_OPT_IN entry has no env-var opt-in, so registering it WOULD make it run. ' +
'Register it in both lists and delete the line.'
).toEqual([])
})
it('keeps every UNREGISTERED_ON_MAIN file ineligible for MANUAL_OPT_IN', () => {
// The two lists must not be interchangeable: debt that CI could run must
// not be re-labelled as manual to make the debt cap look better.
const movable = UNREGISTERED_ON_MAIN.filter((path) => requiresEnvOptIn(sourceOf(path)))
expect(
movable,
'This file is registrable; it cannot be reclassified as MANUAL_OPT_IN.'
).toEqual([])
})
})
describe('manual opt-in classification', () => {
it('requires the env read to reach the gate', () => {
// The parking attack: a compound gate CI could satisfy, in a file that
// happens to read an unrelated env var. This is the native-addon-bytes
// shape, and it must read as registrable.
expect(
requiresEnvOptIn(
"const tmp = process.env.RUNNER_TEMP\ndescribe.runIf(process.platform === 'win32' && hasAddon)('x', () => {})"
)
).toBe(false)
// One hop through a const: the real shape of the ten listed suites.
expect(
requiresEnvOptIn(
"const distro = process.env.ORCA_TEST_WSL_DISTRO\ndescribe.runIf(process.platform === 'win32' && Boolean(distro))('x', () => {})"
)
).toBe(true)
// Read inline in the conjunct: the other real shape.
expect(
requiresEnvOptIn(
"const RUN = process.platform === 'win32' && process.env.ORCA_REAL_X === '1'"
)
).toBe(true)
})
it('requires the gate to be compound at all', () => {
// A bare `runIf(win32)` file -- which CI can run -- must never park as
// manual, however much `process.env` the file reads elsewhere.
expect(
requiresEnvOptIn(
"const t = process.env.CI\ndescribe.runIf(process.platform === 'win32')('x', () => {})"
)
).toBe(false)
// The case that makes the `&&` in WIN32_CONJUNCT load-bearing rather than
// decorative: an env read on the SAME line as a bare gate. Drop the `&&`
// and this reads as manual, which is the parking hole reopened.
expect(
requiresEnvOptIn(
"describe.runIf(process.platform === 'win32')(`x ${process.env.ORCA_TAG}`, () => {})"
)
).toBe(false)
})
})
describe('Windows runner detection', () => {
it('reads every runs-on spelling that could land on Windows', () => {
expect(couldRunOnWindows('windows-2022')).toBe(true)
// The spelling that would have slipped past an equality test.
expect(couldRunOnWindows('windows-latest')).toBe(true)
expect(couldRunOnWindows(['self-hosted', 'Windows', 'X64'])).toBe(true)
expect(couldRunOnWindows({ group: 'windows-runners', labels: ['x64'] })).toBe(true)
// Unresolvable from the file, so it fails closed rather than reading as safe.
expect(couldRunOnWindows('${{ matrix.os }}')).toBe(true)
expect(couldRunOnWindows('ubuntu-latest')).toBe(false)
expect(couldRunOnWindows(['self-hosted', 'linux'])).toBe(false)
expect(couldRunOnWindows(undefined)).toBe(false)
})
})
describe('Windows-gate detection', () => {
// Each positive is paired with the near-miss it must reject. The pairs are
// written from the shapes that exist in the repo, not from the regexes above.
const cases = [
[
'describe.runIf equality',
"describe.runIf(process.platform === 'win32')('x', () => {})",
"describe.runIf(process.platform !== 'win32')('x', () => {})"
],
[
'describe.skipIf inequality',
"describe.skipIf(process.platform !== 'win32')('x', () => {})",
"describe.skipIf(process.platform === 'win32')('x', () => {})"
],
[
'ternary describe alias',
"const d = process.platform === 'win32' ? describe : describe.skip",
"const d = process.platform === 'win32' ? describe.skip : describe"
],
[
'inverted ternary describe alias',
"const d = process.platform !== 'win32' ? describe.skip : describe",
"const d = process.platform !== 'win32' ? describe : describe.skip"
],
[
'local isWindows flag',
"const isWindows = process.platform === 'win32'\ndescribe.skipIf(!isWindows)('x', () => {})",
"const isWindows = process.platform === 'win32'\ndescribe.skipIf(isWindows)('x', () => {})"
],
[
'local isWindows flag, runIf',
"const isWindows = process.platform === 'win32'\ndescribe.runIf(isWindows)('x', () => {})",
"const isWindows = process.platform === 'win32'\ndescribe.runIf(!isWindows)('x', () => {})"
],
[
// The blocking miss: a second conjunct made the gate invisible.
'compound gate with a second conjunct',
"describe.runIf(process.platform === 'win32' && Boolean(distro))('x', () => {})",
"describe.runIf(process.platform === 'win32' || Boolean(distro))('x', () => {})"
],
[
'compound gate behind a named flag assigned on the next line',
"const RUN_REAL =\n process.platform === 'win32' && process.env.X === '1'\ndescribe.runIf(RUN_REAL)('x', () => {})",
"const RUN_REAL =\n process.platform !== 'win32' && process.env.X === '1'\ndescribe.runIf(RUN_REAL)('x', () => {})"
],
[
'named flag driving a ternary suite alias',
"const enabled = process.platform === 'win32' && process.env.X === '1'\nconst d = enabled ? describe : describe.skip",
"const enabled = process.platform === 'win32' && process.env.X === '1'\nconst d = enabled ? describe.skip : describe"
],
[
'compound skipIf widened with ||',
"describe.skipIf(process.platform !== 'win32' || !hasAddon)('x', () => {})",
"describe.skipIf(process.platform !== 'win32' && !hasAddon)('x', () => {})"
],
[
'double-quoted and loosely spaced',
'describe . runIf ( process.platform === "win32" )("x", () => {})',
'describe . runIf ( process.platform === "darwin" )("x", () => {})'
]
]
for (const [label, gated, nearMiss] of cases) {
it(`detects ${label} and rejects its near miss`, () => {
expect(isWindows32GatedTestFile('src/x/sample.test.ts', gated)).toBe(true)
expect(isWindows32GatedTestFile('src/x/sample.test.ts', nearMiss)).toBe(false)
})
}
it('detects the .win32 filename with no gate expression at all', () => {
expect(isWindows32GatedTestFile('src/x/sample.win32.test.ts', 'describe("x", () => {})')).toBe(
true
)
// Near miss: `.win32.ts` is production source, not a test the lane can run.
expect(isWindows32GatedTestFile('src/x/sample.win32.ts', 'export const x = 1')).toBe(false)
})
it('does not read a flag whose name merely starts the same', () => {
// Without word boundaries `isWindows` would swallow `isWindowsHost`.
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"const isWindows = process.platform === 'win32'\ndescribe.runIf(isWindowsHost)('x', () => {})"
)
).toBe(false)
})
it('rejects the documented blind spots rather than half-detecting them', () => {
// it-level gate inside a cross-platform suite: out of scope by design.
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"describe('x', () => { it.skipIf(process.platform !== 'win32')('y', () => {}) })"
)
).toBe(false)
// A platform branch inside a test body is not a gate.
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"it('x', () => { if (process.platform === 'win32') { return } })"
)
).toBe(false)
// An imported flag: the assignment is not in this file, so polarity is unknowable.
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"import { isWindows } from './f'\ndescribe.runIf(isWindows)('x', () => {})"
)
).toBe(false)
})
it('does not treat a widening conjunct behind a named flag as Windows-only', () => {
// `!== 'win32' && x` skips only when BOTH hold, so the suite runs on
// Windows and on POSIX when `x` is false. The literal form is rejected by
// the `||` pair above; this is the same condition routed through a flag,
// which is where the shared lookahead used to flip the answer.
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"const p = process.platform !== 'win32' && Boolean(x)\ndescribe.skipIf(p)('x', () => {})"
)
).toBe(false)
// The narrowing direction still counts: `=== 'win32' && x` is Windows-only.
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"const p = process.platform === 'win32' && Boolean(x)\ndescribe.runIf(p)('x', () => {})"
)
).toBe(true)
})
it('ignores a gate that only appears in prose', () => {
expect(
isWindows32GatedTestFile(
'src/x/sample.test.ts',
"// describe.runIf(process.platform === 'win32')\ndescribe('x', () => {})"
)
).toBe(false)
})
})
@@ -0,0 +1,127 @@
import { readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* Guard the one idiom that keeps re-killing Windows tooling.
*
* Node >= 20 refuses to spawn a Windows batch shim without `shell: true` (the
* CVE-2024-27980 mitigation), so `spawnSync('pnpm.cmd', …)` throws EINVAL
* before the command runs at all. On Windows that reads as a broken toolchain
* rather than a failing check, so the failure gets shrugged off — which is
* exactly how `check:code-quality:changed` ran dead for months.
*
* `src/` has its own chokepoint (runProcess) and its own ratchet. These trees
* are plain `.mjs` run by bare `node`, outside that module boundary, so they
* need this narrower one: a batch-shim command literal may not appear in a new
* script. The list only shrinks. Resolve the real executable instead —
* `oxlint-cli-invocation.mjs` and `windows-process-tree-gyp-rebuild.mjs` show
* the shape.
*
* Deliberately a text match on any `.cmd`/`.bat` literal, not on a list of
* runner names: these trees already spawn vitest, playwright, electron-builder
* and tsc, and the next offender is as likely to be one of those as it is to be
* pnpm. A literal is all a copy-paste carries.
*
* Two shapes this does not catch, both accepted. A shim assembled in a template
* literal, and a drive-lettered path — 'C:\tools\pnpm.cmd' — since a colon is
* not in the class. Real code builds those with path.join, whose 'pnpm.cmd'
* argument is caught. Also note codeText only drops lines that BEGIN with a
* comment marker, so a trailing `// 'pnpm.cmd'` false-positives; that fails
* closed. All of which is the ceiling of a text ratchet, and the reason `src/`
* gets a real chokepoint instead.
*/
const WINDOWS_SHIM_LITERAL = /['"][\w./\\-]*\.(?:cmd|bat)['"]/i
const SCANNED_ROOTS = ['config/scripts', 'tests/tools']
/** Scripts that still name a batch shim, held as data so it reads as the list it is. */
const WINDOWS_SHIM_SPAWN_ALLOWLIST = [
// Owns the pnpm invocation decision for every other script.
'config/scripts/pnpm-cli-invocation.mjs',
'config/scripts/pnpm-cli-invocation.test.mjs',
// Write or assert on shim files rather than spawning one.
'config/scripts/dev-cli-terminal-wrapper.mjs',
'config/scripts/dev-cli-terminal-wrapper.test.mjs',
'config/scripts/electron-builder-config.test.mjs',
'config/scripts/ensure-native-runtime.test.mjs',
'config/scripts/live-remote-freeze-rpc.mjs',
'config/scripts/remote-agent-session-authority-repro.mjs',
// macOS-only build path; the win32 branch is dead code there.
'config/scripts/build-mac-local.mjs',
// Benchmarks, repros and e2e drivers — developer-invoked or Linux-only in CI.
'config/scripts/build-orcad-prebuilds.mjs',
'config/scripts/run-ai-vault-typing-bench.mjs',
'config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs',
'config/scripts/run-local-ssh-browser-routing-e2e.mjs',
'config/scripts/run-multi-client-navigation-e2e.mjs',
'config/scripts/run-multi-workspace-typing-bench.mjs',
'config/scripts/run-nested-runtime-ssh-e2e.mjs',
'config/scripts/run-ssh-client-hosted-browser-drop-reconnect-e2e.mjs',
'config/scripts/run-ssh-codex-artifacts-repro-e2e.mjs',
'config/scripts/run-ssh-docker-e2e.mjs',
'config/scripts/run-ssh-docker-perf-e2e.mjs',
'config/scripts/run-ssh-docker-terminal-parking-e2e.mjs',
'config/scripts/run-ssh-docker-watcher-isolation-e2e.mjs',
'config/scripts/run-ssh-staged-upload-reliability.mjs',
'config/scripts/run-terminal-ibus-hangul-e2e.mjs',
'config/scripts/run-terminal-scale-perf-e2e.mjs',
// Routes its shim through an explicit `cmd.exe /d /s /c`, which is the correct form.
'config/scripts/verify-skill-update-roundtrip.mjs',
'tests/tools/benchmarks/startup-time-bench.mjs',
'tests/tools/benchmarks/worktree-deletion-dev-bench.mjs',
'tests/tools/repro-terminal-send-submit.mjs'
]
/** Drop comment-only lines so prose about the old idiom is not an offender. */
function codeText(contents) {
return contents
.split('\n')
.filter((line) => !/^\s*(?:\/\/|\/\*|\*)/.test(line))
.join('\n')
}
// Why recursive: a future config/scripts/<subdir>/ would otherwise escape silently.
function collectScripts(directory, repoRoot, found = []) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const full = path.join(directory, entry.name)
if (entry.isDirectory()) {
if (entry.name !== 'node_modules') {
collectScripts(full, repoRoot, found)
}
continue
}
if (/\.[cm]?js$/.test(entry.name)) {
found.push(path.relative(repoRoot, full).split(path.sep).join('/'))
}
}
return found
}
describe('windows batch shim spawn boundary', () => {
const repoRoot = path.resolve(import.meta.dirname, '..', '..')
const scripts = SCANNED_ROOTS.flatMap((root) =>
collectScripts(path.join(repoRoot, root), repoRoot)
)
const offenders = scripts.filter((relativePath) =>
WINDOWS_SHIM_LITERAL.test(codeText(readFileSync(path.join(repoRoot, relativePath), 'utf8')))
)
it('scans a plausible number of scripts', () => {
// A broken root or extension filter would make the guard silently vacuous.
expect(scripts.length).toBeGreaterThan(100)
})
it('has no unlisted script naming a Windows batch shim', () => {
const unlisted = offenders.filter((name) => !WINDOWS_SHIM_SPAWN_ALLOWLIST.includes(name))
expect(
unlisted,
'Node cannot spawn a Windows batch shim without a shell. Resolve the real executable — see oxlint-cli-invocation.mjs.'
).toEqual([])
})
it('has no stale allowlist entry', () => {
const stale = WINDOWS_SHIM_SPAWN_ALLOWLIST.filter((name) => !offenders.includes(name))
expect(stale, 'Script no longer names a batch shim — delete the line.').toEqual([])
})
})
@@ -38,7 +38,7 @@ describe('Windows signing workflow contract', () => {
const installStep = steps[installStepIndexes[0]]
expect(installStep.if).toBe("matrix.platform == 'win'")
expect(installStep.if).toBe("matrix.platform == 'win' && github.run_attempt == 1")
expect(installStep.uses).toBe('./.github/actions/install-signpath-module')
expect(installStep.run).toBeUndefined()
@@ -139,6 +139,34 @@ describe('Windows signing workflow contract', () => {
expect(installRun).toContain('throw "SHA-256 mismatch for $source')
})
it('never recreates Windows signing requests on a workflow rerun', () => {
const parsedWorkflow = readWorkflow('.github/workflows/release-cut.yml')
const steps = parsedWorkflow.jobs.build.steps
const stepNames = steps.map((step) => step.name)
const skipStep = steps.find((step) => step.name === 'Skip Windows artifact rebuild on rerun')
expect(skipStep?.if).toBe("matrix.platform == 'win' && github.run_attempt != 1")
expect(skipStep?.run).toContain('Existing signed release assets must be reused')
const signingStepNames = [
'Build Windows release artifacts',
'Stage unsigned inner PE files for signing',
'Upload unsigned inner binaries for SignPath',
'Submit inner binaries signing request',
'Download signed inner binaries from SignPath',
'Upload unsigned Windows installer for SignPath',
'Submit Windows installer signing request',
'Download signed Windows installer from SignPath',
'Stage signed Windows release assets',
'Publish signed Windows release artifacts'
]
for (const stepName of signingStepNames) {
const step = steps[stepNames.indexOf(stepName)]
expect(step?.if, stepName).toContain('github.run_attempt == 1')
}
})
it('shares one SignPath module install path between release and rehearsal', () => {
const rehearsalWorkflow = readWorkflow('.github/workflows/windows-signing-rehearsal.yml')
const stepNames = rehearsalWorkflow.jobs.rehearse.steps.map((step) => step.name)
+176
View File
@@ -0,0 +1,176 @@
# Files currently allowed to carry a `@ts-nocheck` header.
# This is a RATCHET: the list may only SHRINK. These exist only because the split
# runtime mixin chain cannot express forward references yet — do NOT add entries to
# get CI green; fix the types instead.
# Regenerate/prune: pnpm check:ts-nocheck-ratchet --prune (removes stale entries only)
src/main/runtime/orca-runtime-activate-managed-worktree.ts
src/main/runtime/orca-runtime-adopt-terminal-orphans-from-inventory.ts
src/main/runtime/orca-runtime-agent-teams-launch-plan.ts
src/main/runtime/orca-runtime-apply-layout.ts
src/main/runtime/orca-runtime-apply-mobile-display-mode.ts
src/main/runtime/orca-runtime-apply-mobile-session-tab-navigation.ts
src/main/runtime/orca-runtime-apply-tracked-pty-title.ts
src/main/runtime/orca-runtime-attach-remote-terminal-source-range-consumer.ts
src/main/runtime/orca-runtime-attach-window.ts
src/main/runtime/orca-runtime-bind-pty-incarnation-handle.ts
src/main/runtime/orca-runtime-build-headless-mobile-session-browser-tabs.ts
src/main/runtime/orca-runtime-build-pty-terminal-summary.ts
src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts
src/main/runtime/orca-runtime-close-headless-mobile-terminal-tab.ts
src/main/runtime/orca-runtime-close-mobile-session-tab.ts
src/main/runtime/orca-runtime-close-structured-agent-session-tab.ts
src/main/runtime/orca-runtime-collect-mobile-visible-graph-changed-worktrees.ts
src/main/runtime/orca-runtime-controller-knows-pty-is-live.ts
src/main/runtime/orca-runtime-core.ts
src/main/runtime/orca-runtime-create-agent-prompt-render-gate.ts
src/main/runtime/orca-runtime-create-agent-session.ts
src/main/runtime/orca-runtime-create-managed-remote-worktree.ts
src/main/runtime/orca-runtime-create-managed-worktree.ts
src/main/runtime/orca-runtime-create-mobile-session-terminal.ts
src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts
src/main/runtime/orca-runtime-create-runtime-owned-mobile-session-terminal.ts
src/main/runtime/orca-runtime-create-terminal-desktop.ts
src/main/runtime/orca-runtime-create-terminal-side-effect-command-code-detector.ts
src/main/runtime/orca-runtime-create-terminal.ts
src/main/runtime/orca-runtime-deliver-pending-messages.ts
src/main/runtime/orca-runtime-emit-daemon-pty-transient-fact.ts
src/main/runtime/orca-runtime-fence-automation-owner.ts
src/main/runtime/orca-runtime-file-commands.ts
src/main/runtime/orca-runtime-fit-override-listeners.ts
src/main/runtime/orca-runtime-focus-terminal.ts
src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts
src/main/runtime/orca-runtime-get-orchestration-dispatch-authority.ts
src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts
src/main/runtime/orca-runtime-get-runtime-id.ts
src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts
src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts
src/main/runtime/orca-runtime-get-worktree-ps.ts
src/main/runtime/orca-runtime-get-worktree-terminal-provisioning-host.ts
src/main/runtime/orca-runtime-handle-mobile-subscribe-internal.ts
src/main/runtime/orca-runtime-handle-mobile-subscribe.ts
src/main/runtime/orca-runtime-handle-mobile-unsubscribe.ts
src/main/runtime/orca-runtime-has-exact-persisted-terminal-surface-identity.ts
src/main/runtime/orca-runtime-has-live-or-persisted-serve-or-ssh-owned-pty-binding.ts
src/main/runtime/orca-runtime-has-recent-terminal-output-path.ts
src/main/runtime/orca-runtime-has-terminals-for-worktree.ts
src/main/runtime/orca-runtime-hydrate-headless-mobile-session-tabs-from-workspace-session.ts
src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts
src/main/runtime/orca-runtime-linear-commands.ts
src/main/runtime/orca-runtime-list-known-resolved-worktrees-for-explicit-target.ts
src/main/runtime/orca-runtime-list-managed-worktrees.ts
src/main/runtime/orca-runtime-mark-pty-liveness-unverifiable.ts
src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts
src/main/runtime/orca-runtime-merge-preserved-headless-mobile-session-tabs.ts
src/main/runtime/orca-runtime-mobile-took-floor.ts
src/main/runtime/orca-runtime-move-headless-mobile-session-tab.ts
src/main/runtime/orca-runtime-notify-ssh-state-changed.ts
src/main/runtime/orca-runtime-on-client-disconnected.ts
src/main/runtime/orca-runtime-on-pty-data.ts
src/main/runtime/orca-runtime-on-pty-exit.ts
src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts
src/main/runtime/orca-runtime-persist-headless-session-tab-props.ts
src/main/runtime/orca-runtime-persist-headless-terminal-title.ts
src/main/runtime/orca-runtime-persist-terminal-surface-retirements.ts
src/main/runtime/orca-runtime-pick-most-recent-actor.ts
src/main/runtime/orca-runtime-postlude.ts
src/main/runtime/orca-runtime-prepare-pty-execution-context.ts
src/main/runtime/orca-runtime-preserved-branch-cleanup.ts
src/main/runtime/orca-runtime-prove-recovered-structured-tui-pty-process.ts
src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts
src/main/runtime/orca-runtime-pty-foreground-process-reads.ts
src/main/runtime/orca-runtime-publish-pty-backed-mobile-session-terminal.ts
src/main/runtime/orca-runtime-reclaim-terminal-for-desktop.ts
src/main/runtime/orca-runtime-reconcile-headless-mobile-session-browser-tabs.ts
src/main/runtime/orca-runtime-record-agent-prompt-lifecycle-state.ts
src/main/runtime/orca-runtime-record-pty-worktree.ts
src/main/runtime/orca-runtime-refresh-floating-workspace-pty-liveness.ts
src/main/runtime/orca-runtime-refresh-pty-worktree-records-from-controller.ts
src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts
src/main/runtime/orca-runtime-refresh-repo-worktree-scan.ts
src/main/runtime/orca-runtime-refuse-unattributed-mobile-session-tab-close.ts
src/main/runtime/orca-runtime-register-pty.ts
src/main/runtime/orca-runtime-remove-managed-worktree.ts
src/main/runtime/orca-runtime-remove-orphan-or-folder-worktree.ts
src/main/runtime/orca-runtime-resolve-authoritative-terminal-wait-permission.ts
src/main/runtime/orca-runtime-resolve-browser-network-execution-host-for-worktree.ts
src/main/runtime/orca-runtime-resolve-exit-waiters.ts
src/main/runtime/orca-runtime-resolve-known-workspace-file-target.ts
src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts
src/main/runtime/orca-runtime-resolve-recovered-structured-tui-transcript.ts
src/main/runtime/orca-runtime-resolve-terminal-pane.ts
src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts
src/main/runtime/orca-runtime-resolve-waiter.ts
src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts
src/main/runtime/orca-runtime-resolve-worktree-selector.ts
src/main/runtime/orca-runtime-restore-live-paired-renderer-session-owned-mobile-terminals.ts
src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts
src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts
src/main/runtime/orca-runtime-runtime-id.ts
src/main/runtime/orca-runtime-schedule-mobile-session-tabs-changed.ts
src/main/runtime/orca-runtime-schedule-wait-blocked-check.ts
src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts
src/main/runtime/orca-runtime-serialize-headless-terminal-buffer.ts
src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts
src/main/runtime/orca-runtime-serialize-terminal-buffer-from-available-state.ts
src/main/runtime/orca-runtime-sleep-resolved-worktree-terminals.ts
src/main/runtime/orca-runtime-split-pty-backed-terminal.ts
src/main/runtime/orca-runtime-split-terminal.ts
src/main/runtime/orca-runtime-start-tui-idle-visible-read-probe.ts
src/main/runtime/orca-runtime-state-fields.ts
src/main/runtime/orca-runtime-stop-exact-terminals-for-worktree.ts
src/main/runtime/orca-runtime-stop-explicitly-closed-tab-ptys.ts
src/main/runtime/orca-runtime-stop-requested-pty-ids.ts
src/main/runtime/orca-runtime-stop-structured-session-process.ts
src/main/runtime/orca-runtime-stop-terminals-for-worktree.ts
src/main/runtime/orca-runtime-stored-mobile-snapshot-has-stale-preserved-tab.ts
src/main/runtime/orca-runtime-structured-agent-session-launch-tui.ts
src/main/runtime/orca-runtime-structured-agent-session-recover-tui-owner.ts
src/main/runtime/orca-runtime-structured-agent-session-reprove-tui-owner.ts
src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts
src/main/runtime/orca-runtime-sync-mobile-session-tabs.ts
src/main/runtime/orca-runtime-sync-window-graph.ts
src/main/runtime/orca-runtime-terminal-create-deduplication.ts
src/main/runtime/orca-runtime-terminal-drivers.ts
src/main/runtime/orca-runtime-touch-mobile-session-tabs-for-worktree.ts
src/main/runtime/orca-runtime-transition-graph-reload-to-terminal-state.ts
src/main/runtime/orca-runtime-verify-orchestration-compatibility-caller.ts
src/main/runtime/orca-runtime-visible-snapshot-preview.ts
src/main/runtime/orca-runtime-wait-for-leaf-pty-id.ts
src/main/runtime/orca-runtime-wait-for-mobile-terminal-surface.ts
src/main/runtime/orca-runtime-wait-for-session-tabs-inventory-publication.ts
src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts
src/main/runtime/orca-runtime-write-terminal-agent-prompt.ts
src/main/runtime/runtime-browser-commands-active-screencasts-by-page-id.ts
src/main/runtime/runtime-browser-commands-browser-clear.ts
src/main/runtime/runtime-browser-commands-browser-click.ts
src/main/runtime/runtime-browser-commands-browser-command-target-params.ts
src/main/runtime/runtime-browser-commands-browser-network-log.ts
src/main/runtime/runtime-browser-commands-browser-profile-import-from-browser.ts
src/main/runtime/runtime-browser-commands-browser-screencast.ts
src/main/runtime/runtime-browser-commands-browser-set-headers.ts
src/main/runtime/runtime-browser-commands-browser-tab-close.ts
src/main/runtime/runtime-browser-commands-browser-tab-create.ts
src/main/runtime/runtime-browser-commands-browser-tab-list.ts
src/main/runtime/runtime-browser-commands-browser-tab-set-profile.ts
src/main/runtime/runtime-browser-commands-list-logical-browser-tabs.ts
src/main/runtime/runtime-browser-commands-state.ts
src/main/runtime/runtime-file-command-host.ts
src/main/runtime/runtime-file-commands-active-runtime-text-searches.ts
src/main/runtime/runtime-file-commands-assert-remote-terminal-file-grant-path-still-canonical.ts
src/main/runtime/runtime-file-commands-constructor.ts
src/main/runtime/runtime-file-commands-create-file-explorer-dir-no-clobber.ts
src/main/runtime/runtime-file-commands-mobile-file-list-limit.ts
src/main/runtime/runtime-file-commands-read-file-explorer-preview.ts
src/main/runtime/runtime-file-commands-read-mobile-file.ts
src/main/runtime/runtime-file-commands-resolve-allowed-terminal-artifact-path.ts
src/main/runtime/runtime-file-commands-resolve-terminal-path.ts
src/main/runtime/runtime-file-commands-revoke-terminal-file-grants-for-client.ts
src/main/runtime/runtime-file-commands-search-local-runtime-files.ts
src/main/runtime/runtime-file-commands-search-remote-quick-open-file-paths.ts
src/main/runtime/runtime-file-commands-search-runtime-files.ts
src/main/runtime/runtime-file-commands-ssh-file-watcher-rearm.ts
src/main/runtime/runtime-file-commands-terminal-artifact-access.ts
src/main/runtime/runtime-file-commands-terminal-file-paths.ts
src/main/runtime/runtime-file-commands-write-file-explorer-file.ts
src/main/runtime/runtime-file-commands-write-terminal-artifact-file.ts
src/main/runtime/runtime-file-watcher-leases.ts
+1
View File
@@ -117,6 +117,7 @@
"../src/main/hermes/hermes-home-filesystem.ts",
"../src/main/hermes/hermes-managed-plugin-source.ts",
"../src/main/hermes/hook-service.ts",
"../src/main/in-flight-run-dedupe.ts",
"../src/main/kimi/hook-service.ts",
"../src/main/kimi/kimi-hook-config-toml.ts",
"../src/main/openclaude/hook-service.ts",
+14
View File
@@ -6,18 +6,32 @@
"../src/renderer/src/**/*.tsx",
"../src/preload/api-types.ts",
"../src/preload/api/**/*",
"../src/preload/browser-client-page-renderer-requests.ts",
"../src/preload/browser-find-subscriptions.ts",
"../src/preload/close-active-tab-payload-admission.ts",
"../src/preload/e2e-config.ts",
"../src/preload/gitlab.ts",
"../src/preload/preload-runtime-support.ts",
"../src/preload/renderer-heap-statistics-reader.ts",
"../src/preload/renderer-process-memory-reader.ts",
"../src/preload/renderer-restart-wiring.ts",
"../src/preload/runtime-environment-subscriptions.ts",
"../src/preload/usage-provider-api.ts",
"../src/shared/**/*",
"../src/main/gitlab/mappers.ts",
"../src/main/ipc/worktree-branch-name.ts",
"../src/main/ipc/worktree-logic.ts",
"../src/main/ipc/worktree-display-name.ts",
"../src/main/ipc/worktree-linked-work-item-metadata.ts",
"../src/main/ipc/worktree-metadata-merge.ts",
"../src/main/ipc/worktree-path-comparison.ts",
"../src/main/wsl-availability.ts",
"../src/main/wsl-directory-probe-command.ts",
"../src/main/wsl-distro-list-output.ts",
"../src/main/wsl-distro-retry.ts",
"../src/main/wsl-running-distro-cache.ts",
"../src/main/wsl.ts",
"../src/main/wsl-interop-spawn-directory.ts",
"../src/main/persistence/applying-settings/ui-state-read.ts",
"../src/main/persistence/applying-settings/ui-state-update.ts",
"../src/main/persistence/applying-settings/ui-selection-normalization.ts",
+4 -4
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 34m">
<title>downloads: 34m</title>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 36m">
<title>downloads: 36m</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
<text x="37" y="14">downloads</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">34m</text>
<text x="90" y="14">34m</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">36m</text>
<text x="90" y="14">36m</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 935 B

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 394 KiB

+2 -2
View File
@@ -243,9 +243,9 @@ Associez-la à l'app de bureau pour surveiller et piloter vos agents depuis votr
- **Discord :** Rejoignez la communauté sur **[Discord](https://discord.gg/fzjDKHxv8Q)**.
- **Twitter / X :** Suivez **[@orca_build](https://x.com/orca_build)** pour les news et annonces.
- **WeChat :** Scannez pour rejoindre le groupe WeChat 7 de la communauté Orca.
- **WeChat :** Scannez pour rejoindre le groupe WeChat 8 de la communauté Orca.
<img src="../assets/wechat-qr-group7.jpg" alt="QR code WeChat groupe 7 de la communauté Orca" width="160" />
<img src="../assets/wechat-qr-group8.jpg" alt="QR code WeChat groupe 8 de la communauté Orca" width="160" />
- **Feedback &amp; idées :** On ship vite. Il manque quelque chose ? [Demandez une feature](https://github.com/stablyai/orca/issues).
- **Confidentialité :** Voir la [doc confidentialité &amp; télémétrie](https://www.onorca.dev/docs/telemetry) pour ce qu'Orca collecte en anonyme et comment désactiver la télémétrie.
+2 -2
View File
@@ -238,9 +238,9 @@ yay -S stably-orca-bin
- **Discord:** **[Discord](https://discord.gg/fzjDKHxv8Q)** 커뮤니티에 참여하세요.
- **Twitter / X:** 업데이트와 공지는 **[@orca_build](https://x.com/orca_build)** 를 팔로우하세요.
- **WeChat:** QR 코드를 스캔해 Orca 커뮤니티 WeChat 그룹 7에 참여하세요.
- **WeChat:** QR 코드를 스캔해 Orca 커뮤니티 WeChat 그룹 8에 참여하세요.
<img src="../assets/wechat-qr-group7.jpg" alt="Orca 커뮤니티 WeChat 그룹 7 QR 코드" width="160" />
<img src="../assets/wechat-qr-group8.jpg" alt="Orca 커뮤니티 WeChat 그룹 8 QR 코드" width="160" />
- **피드백과 아이디어:** 우리는 빠르게 출시합니다. 필요한 기능이 있나요? [새 기능을 요청](https://github.com/stablyai/orca/issues)하세요.
- **개인정보 보호:** Orca가 수집하는 익명 사용 데이터와 수집 거부 방법은 [개인정보 및 텔레메트리 문서](https://www.onorca.dev/docs/telemetry)를 참고하세요.
+1 -2
View File
@@ -235,9 +235,8 @@ yay -S stably-orca-bin
- **Discord:** 加入 **[Discord](https://discord.gg/fzjDKHxv8Q)** 社区。
- **Twitter / X:** 关注 **[@orca_build](https://x.com/orca_build)** 获取更新和公告。
- **微信:** 扫码加入 Orca 社区微信第 7 群。如果第 7 群已满,请使用第 8 群。
- **微信:** 扫码加入 Orca 社区微信第 8 群。
<img src="../assets/wechat-qr-group7.jpg" alt="Orca 社区微信第 7 群二维码" width="160" />&nbsp;&nbsp;
<img src="../assets/wechat-qr-group8.jpg" alt="Orca 社区微信第 8 群二维码" width="160" />
- **反馈与想法:** 我们发布很快。缺少什么功能?[提交功能请求](https://github.com/stablyai/orca/issues)。
+11
View File
@@ -42,6 +42,17 @@ authority.
| `merge-tree-write-tree` | Derive real-merge conflicts and no-op tree proofs | Omit the conflict summary and keep conservative branch cleanup behavior before Git 2.38 |
| `merge-tree-merge-base` | Supply the already-resolved merge base | Use the older two-commit `merge-tree --write-tree` form |
### Placeholders That Fail Open
`GitCapabilityCache` records commands Git *rejects*. A `git log --format`
placeholder Git does not know is not rejected: Git echoes it verbatim and exits
zero, so there is no error to remember and no probe to cache. Ask for both forms
in one record and pick at parse time.
| Placeholder | Preferred behavior | Compatibility behavior |
| ---------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `%(decorate:…)` | Git 2.43 separates commit decorations with `\x1f`, so ref names containing commas survive | The same record also carries `%D` (Git 2.10); an unexpanded `%(decorate` placeholder selects it, at the cost of comma-splitting |
## Why Not `simple-git`
`simple-git` is a process wrapper around the installed Git binary. Its custom
+8
View File
@@ -40,6 +40,14 @@ Two ways remote work _can_ actually stop:
Reconnect re-attaches to the same live PTYs and replays a bounded buffer (`REPLAY_BUFFER_MAX`, a 102,400-code-unit tail). Output beyond that while you were away is lost to the client even though the process was never interrupted: **the transcript is truncated; the work stays `live`.**
## Updating Orca strands relay-backed terminals
There is a third outcome that is neither of the two above, and the vocabulary matters: the work does not stop, it becomes permanently unreachable.
The relay's install directory — and therefore its socket path — is namespaced by a content hash of the relay bundle (`computeRemoteRelayDir` in `src/main/ssh/ssh-relay-versioned-install.ts`, consumed by `resolveRemoteInstallState` in `src/main/ssh/ssh-relay-deploy.ts`), and the daemon refuses any client whose bundle hash differs (`handleDaemonHandshakeFrame` in `src/relay/relay-handshake.ts`, exit `EXIT_CODE_VERSION_MISMATCH` 42). Two builds whose relay protocol is byte-identical still refuse each other. So the first reconnect after an app update deploys a new relay at a path the incumbent was never listening on, and cannot reach it even in principle. Every PTY the incumbent owns is `unverifiable` — running, unreachable, and never `exited`. The client's leases are attempted against a relay that never minted their ids, expired on the not-found answer (`handlePtyReattachFailure` in `src/main/ssh/ssh-relay-session.ts`), and the pane falls back to a cold-restore agent resume — or to a bare shell when no resumable provider session was captured for it. The old relay keeps its directory pinned against GC, because its socket really is live (`hasLiveRelaySocket` in `src/main/ssh/remote-install-gc.ts`). See #13852.
The peer model does not have this failure, and that is the concrete reason behind "One host, one model" below. The daemon's endpoint is namespaced by a **semantic protocol version** rather than a build (`daemon-v<N>.sock`, from `getDaemonSocketPath` in `src/main/daemon/daemon-spawner.ts`), every earlier protocol version stays attachable (`PROTOCOL_VERSION` in `src/main/daemon/daemon-protocol-version.ts`), and a daemon holding live sessions is preserved across a version change instead of replaced (`shouldPreserveDaemonWithLiveSessions` in `src/main/daemon/daemon-replacement-preflight.ts`).
## Control plane
On an SSH host, `orca` is a shim (`~/.orca-relay/bin/orca`) that proxies **back to the client's runtime** over the relay socket. Your repository, processes, and files remain remote — only the control plane is on the client. This is correct for an SSH target, but it has a consequence worth stating plainly:
+401
View File
@@ -0,0 +1,401 @@
# Windows EDR signal surface
Orca's Windows process tree is shaped like the thing behavioural EDR is built to
find. An enterprise Windows 11 / Intune tenant opened **six Microsoft Defender
for Endpoint incidents against Orca 1.4.192 in eight days**. All six fired as
active incidents and stayed open; three closed only because a human classified
them by hand in the portal. Defender never downgraded or closed one on its own.
None were signature hits. Every one was behavioural process-tree scoring, and
two escalated to multi-stage incidents carrying ATT&CK tactic mappings
(Execution, Collection).
The framing this document keeps throughout, because both halves matter:
> **Defender is not malfunctioning. It is describing the code accurately.** Orca
> really does copy its own signed image under a different name, really does read
> every process's memory on a timer, really does run base64-encoded PowerShell
> with the execution policy bypassed, and really does take screenshots and
> synthesise input from a runtime-compiled assembly. Each of those is a
> deliberate engineering choice with issue history behind it. The problem is not
> that the capabilities are illegitimate — it is that their **behavioural
> signature overlaps with attack techniques**, and an EDR scoring behaviour
> cannot see the difference.
Do not read this as a bug report against Defender, and do not read it as a claim
that Orca is malware. It is a map of which of our behaviours are legible to an
EDR as attack-technique-shaped, why each one exists, and what engineers and
administrators can do about it.
## What the tenant actually saw
Four independent evidence clusters, from six incidents:
| Cluster | Incidents | Evidence |
| ----------------- | --------- | -------------------------------------------------------------------------------------------------------------------- |
| **Update** | A, B, C | `orca-windows-setup.exe` → `old-uninstaller.exe`, `Uninstall Orca.exe` (electron-builder generates these; they are in no repo file) |
| **Spawn** | all six | `Orca.exe` → `orca-terminal-daemon.exe` → `powershell.exe` / `pwsh.exe` / `cmd.exe` / `reg.exe` → `claude.exe`, `gh.exe`, `codex.cmd` |
| **Process table** | D | "suspicious memory activity" — `OpenProcess` plus a PEB read against every process on a repeating cadence |
| **Computer use** | E, F | `runtime.ps1`, `computer-sidecar.js`, many `operation.json`, a burst of ~10 short-lived `powershell.exe` |
Incident E is the one to look at hardest: 5 alerts, 37 evidence items, ATT&CK
**Execution + Collection**, and a description reading _"Screenshots were taken
unexpectedly on this device… Screen capture code was found in a script launched
by powershell.exe."_ Incident F added _"suspicious MSIL code"_, from the
`Add-Type -TypeDefinition` that recompiles inline C# P/Invoke on every
operation.
In the update cluster the uninstaller is genuinely `NotSigned`, while `Orca.exe`
and `orca-terminal-daemon.exe` report `Valid CN=SignPath Foundation`.
## The behaviours, and why each one exists
### The daemon runs from a renamed copy of our own image
`src/main/daemon/daemon-host-relocation.ts` copies the Electron runtime into
`%LOCALAPPDATA%\Orca\daemon-host\<version>\` and renames `Orca.exe` to
`orca-terminal-daemon.exe`. The comment on `DAEMON_HOST_EXE_NAME` states the
reason without varnish: _"so the NSIS updater's `taskkill /IM Orca.exe` can't
match it."_
It exists because the NSIS installer deletes the old install directory and force-
kills every process imaged under it. Without relocation, an auto-update kills the
terminal daemon and every live terminal with it. The copy is a run-as-node
`Orca.exe` rather than `node.exe` so there is no console flash and asar still
resolves; `config/nsis/daemon-host-uninstall.nsh` reaps it on a real uninstall
(guarded by `${isUpdated}` so an update's `uninstallOldVersion` never fires it).
**How an EDR reads it: MITRE T1036, masquerading.** A signed executable copied
out of the install directory into `%LOCALAPPDATA%` under a different name, which
then spawns shells, matches the textbook description closely enough that no
behavioural engine can be expected to score it low.
### Every process gets a handle, on a timer
`src/main/windows/windows-process-table.ts` takes a Toolhelp32 snapshot under one
of two flag sets. Identity (`None | CreationTime`) answers pid/ppid/name from the
snapshot alone and opens nothing; the detailed set adds `CommandLine`, which
costs one `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` per process. `Memory`
is retired — it took a second handle carrying `PROCESS_VM_READ` and never read
through it.
It exists because seven independent readers used to fork `powershell.exe` for a
`Get-CimInstance Win32_Process` scan. That cost, measured: a PowerShell
Transcription policy recorded **~289 GB across 1.4 million files** because a scan
ran every ~2 seconds (#15209); a Group Policy or AV block turned a query into
"unavailable", which callers read as "no evidence", which is how a PTY tree
survived its own teardown (#9045, #10475); and the scan cost ~700 ms per pane, so
panes multiplied it (#15036). The native snapshot answers the same question in
15.9 ms against 706 ms for CIM — p50, measured on Windows 11 at 1050 processes.
See
[`windows-process-enumeration.md`](./windows-process-enumeration.md).
Asking for fewer fields is cheaper, and since the split the module does: one
cache per flag set, so teardown identity and the session owner probe open no
handle at all (6.3 ms p50) while only the callers that read a command line pay
for one (12.3 ms p50, at 492 processes). Each cache still single-flights within
itself, and one gate serializes the native reads because the vendored wrapper
coalesces the flags of two overlapping calls.
**How an EDR reads it:** a cross-process handle plus a remote memory read against
every process on the box, repeating on a cadence, is the read half of the
telemetry that credential dumping and process injection produce. MDE surfaced it
as "suspicious memory activity". The memory read is gone: the command line now
comes from the kernel, through `NtQueryInformationProcess`'s
`ProcessCommandLineInformation` class, which needs only
`PROCESS_QUERY_LIMITED_INFORMATION`. `ReadProcessMemory` is absent from the
compiled addon, asserted against the binary's import table because the published
prebuild loads fine and emits byte-identical strings. What is left to declare to
administrators is the per-process handle itself.
### Encoded, policy-bypassing PowerShell
Three sites are named in the incident analysis:
- `src/relay/windows-port-scan.ts` ran
`-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand` over a
`Get-NetTCPConnection -State Listen` script to find dev-server ports.
Enumerating listening ports is **MITRE T1049**, network service discovery, and
doing it through an encoded policy-bypassed shell is the aggravating factor
rather than the finding itself. The ordinary scan now starts no PowerShell at
all — `netstat.exe -ano`, with the owning process name projected off the shared
native table — and that payload survives only as the last-resort fallback, as
`-Command` with no policy override.
- `src/main/daemon/shell-ready.ts` uses `-EncodedCommand` for the OSC 133
bootstrap.
- `src/main/agent-hooks/windows-powershell-hook-launcher.ts` wraps managed hooks.
**No site spells the pair any more.** `src/main/ssh/ssh-remote-powershell.ts`,
`src/shared/setup-agent-sequencing.ts`,
`src/shared/windows-cmd-runner-delayed-launch.ts` and
`src/shared/windows-interactive-login-spawn.ts` each dropped
`-ExecutionPolicy Bypass` as a measured no-op: the policy gates script *files*,
never `-EncodedCommand`. Where the bypass was load-bearing it moved in-payload as
a process-scope `Set-ExecutionPolicy` (`setup-agent-sequencing.ts`), which is the
pattern to copy rather than restoring the switch — the switch loses to a GPO
scope anyway, so it never covered the locked-down case.
What remains is `-EncodedCommand` without the bypass: the PTY bootstraps
(`src/main/daemon/shell-ready.ts`, `src/main/providers/local-pty-shell-ready.ts`,
`src/main/providers/windows-shell-args.ts`), the hook wrappers
(`src/main/agent-hooks/windows-powershell-hook-launcher.ts` and its callers
`src/main/agent-hooks/runtime-home-hook-command.ts`,
`src/main/agent-hooks/installer-utils.ts`, `src/main/claude/hook-settings.ts`),
`src/main/runtime/windows-default-route-interfaces.ts`,
`src/main/runtime/orchestration/setup-completion-signal.ts`,
`src/shared/hermes-startup-query.ts`, and the four ex-bypass sites above.
`src/main/runtime/windows-mobile-firewall.ts` encodes a script and launches it
_elevated_ through `Start-Process -Verb RunAs`, which is a stronger shape than
any of those; only that hop is encoded, because `-ArgumentList` re-splits an
unquoted parameter string on whitespace.
One site still spells `-ExecutionPolicy Bypass` with **no** encoding, the weaker
signal: `src/main/cli/wsl-cli-scripts.ts` (`-File`, and it is a real script
file, so the switch is not a no-op there). `src/main/system-fonts.ts` dropped it
for plain `-Command`; `src/shared/secure-path-windows-acl.ts` no longer runs
PowerShell at all, having moved to `icacls.exe`; and computer use now asks for
`-ExecutionPolicy RemoteSigned` in
`src/main/computer/windows-powershell-execution-policy.ts`, falling back to
`Bypass` only after a policy-blocked start.
Regenerate with `rg -- '-EncodedCommand|-ExecutionPolicy' src/` rather than
trusting the lists above, and note that a raw grep under-reports: the hook sites
reach `-EncodedCommand` through `wrapWindowsPowerShellEncodedCommand` and never
spell the flag themselves.
Encoding is not gratuitous: it shields paths and switches from `cmd.exe` and MSYS
rewriting (#6078, #14815), which is a real class of corruption. But
`-EncodedCommand` is a first-class Defender alert title ("Suspicious PowerShell
command line"), and base64 raises the score rather than lowering it, because it
denies the analyser the payload it would otherwise clear.
The hook launcher is prior art worth knowing about. #16003 measured, on a
reporting Kaspersky host, that `-WindowStyle Hidden` paired with
`-EncodedCommand` was denied at `CreateProcess` with exit 126 regardless of
payload — `exit 0` was denied too. The fix was to stop *spelling* the flags:
`WINDOWS_POWERSHELL_HOOK_SWITCHES` is now just `-NoProfile`, and separately, in
#16576, the execution policy bypass moved in-payload as a process-scope
`Set-ExecutionPolicy` — a real command-line signal reduction, though #16003's
measured denial keyed on `-WindowStyle Hidden` + `-EncodedCommand`, not on the
bypass. It is also honest that the underlying behaviour did not change.
Copy the pattern, but copy its caveat too. `windows-powershell-hook-launcher.ts`
records that dropping `-WindowStyle Hidden` was a real tradeoff whose suppression
"was never measured" and "remains unverified on a real box". Reducing spelled
flags is the right instinct; treat any specific claim about what a removed flag
was doing as unproven until someone measures it.
### `cmd.exe /c` carrying caret-escaped free text
`buildWindowsCmdShimCommandLine` in
`src/shared/child-process/windows-command-line.ts` builds `/d /v:off /s /c "…"`
for the `.cmd` and `.bat` targets Windows can only start through `cmd.exe`
(`codex.cmd` being the one that matters). Because cmd expands `%VAR%` even inside
a quoted token, each `%` is broken with `"^%"`.
The escaping is not decorative. Measured on Windows 11 against a real `.cmd`
shim, `["a b", 'c"d', "e%F%g", "h&i", "j^k"]` came back as `["a b", 'c"d',
"e^%F^%g", "h"]` — the `&` truncated the argument *and* ran the remainder as a
command.
**How an EDR reads it:** caret escaping is the canonical obfuscation marker in
`cmd.exe` command lines, and the free text being escaped here is an agent prompt,
so the line is long, high-entropy, and attacker-shaped. It is the exact input an
obfuscated-command-line detector is tuned on.
### The spawn tree itself
`Orca.exe` → `orca-terminal-daemon.exe` → a shell → an agent CLI is what a
terminal multiplexer for coding agents *is*. `reg.exe` appears from
`src/main/win32-utils.ts`,
`src/main/agent-hooks/managed-hook-owner-identity.ts` and
`src/relay/pty-shell-utils.ts` (reading the OpenSSH `DefaultShell`).
Nothing here is avoidable in principle. What is controllable is depth and
breadth: every interpreter hop between Orca and the thing the user asked for adds
a scored edge, which is why the shipped doctrine of #15520 and #15595 is to
*shorten the interpreter chain* rather than to hide a window.
### Computer use: screen capture, synthetic input, runtime-compiled MSIL
`native/computer-use-windows/runtime.ps1` is a large PowerShell script.
`src/main/computer/desktop-script-provider-bridge.ts` launches it as
`powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned
-File runtime.ps1 <operation.json>`, retrying once at `Bypass` only if the start
comes back policy-blocked — **once per operation**, with
`desktop-script-provider-client.ts` writing a fresh `operation.json` into a new
temp directory each time. On every launch the script runs `Add-Type
-TypeDefinition` over inline C# that P/Invokes `SendInput` and the window APIs,
then captures the screen through `Graphics.CopyFromScreen`.
That is four separate high-signal behaviours stacked in one process:
| Behaviour | How it is scored |
| ----------------------------------------------- | ---------------------------------------------------- |
| `Graphics.CopyFromScreen` | **MITRE T1113**, screen capture — Collection tactic |
| `SendInput` synthetic keyboard/mouse | input synthesis against other applications |
| `Add-Type -TypeDefinition` on every operation | MSIL compiled at runtime; incident F's "suspicious MSIL code" |
| One `powershell.exe` per operation | a burst of short-lived interpreters under one parent |
The bottom two rows are the two the incident text named directly, and they are
also the two a persistent runtime host would remove: a long-lived helper compiles
its P/Invoke stubs once and answers operations over a channel, so neither the
MSIL recompilation nor the interpreter burst repeats. A change doing that is in
flight and unmerged at the time of writing; check the code rather than this
paragraph for what the shipped build does. Screen capture and `SendInput` are
inherent to the feature and no refactor removes them.
## Signing is not the gate
The most useful calibration in the whole incident set came from the reporter's
own machine: **Antigravity IDE's main executable is `NotSigned` and was not
flagged, while Orca's is signed and was flagged six times.** Their conclusion:
_"signing is not the gate here — behaviour is."_
The mechanism is that Defender reputation is signer **plus prevalence**, and
prevalence is keyed on **file hash**. A widely installed unsigned binary clears
on install count alone. Orca's signature is a free OV certificate from SignPath
Foundation (`config/electron-builder.config.cjs` sets
`win.signtoolOptions.publisherName`; `config/scripts/verify-windows-inner-signature.mjs`
pins `CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US`),
shared across many OSS projects, with no independent SmartScreen or MAPS
reputation of its own. Every release ships new hashes, so whatever prevalence a
build accumulates resets on the next update. Dev channels ship unsigned by
design, because SignPath's approval waits cannot fit a dev cadence
(`config/scripts/verify-dev-channel-packaging.mjs`).
Signing the uninstaller is worth doing — an unsigned `old-uninstaller.exe`
running under a signed installer is a gratuitous contribution to the update
cluster — but do not expect it to change the behavioural verdict. The three
non-update clusters contain no unsigned binary at all.
## What we do not know
Two limits the incident analysis recorded, kept here rather than smoothed over:
- **No data on Hermes.** Nothing in this document describes how Hermes behaves
under the same tenant policy — though `src/shared/hermes-startup-query.ts` does
spell `-EncodedCommand`, so the gap is telemetry, not surface.
- **Antigravity not being flagged is absence of evidence, not proof.** It is one
reporter's recollection from one machine, not a measurement. It is strong
enough to falsify "the problem is that we are not signed well enough"; it is
not strong enough to support a positive claim about how Defender scores that
product.
Add to those: this is one tenant with one policy configuration. Whether the same
build scores the same way elsewhere is unmeasured.
## Guidance for engineers
Fixes for several of the shapes above are in flight in separate changes; nothing
in this section should be read as a statement that a given site has already
changed. Check the code before relying on it.
The checklist. On Windows, do not reach for:
| Don't | Instead |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `-ExecutionPolicy Bypass` on the command line | Set the policy in-payload at process scope, as `windows-powershell-hook-launcher.ts` does, or do not run a `.ps1` at all |
| `-EncodedCommand` | A temp `.ps1` with an argument, or no PowerShell hop: prefer a native API or an existing Node path |
| `cmd.exe /c` carrying escaped free text | Spawn the real target directly. `cmd.exe` is only unavoidable for `.cmd`/`.bat`; keep free text out of the line where you can |
| Forking `powershell.exe` to read system state | The native reader — [`windows-process-enumeration.md`](./windows-process-enumeration.md) is the standing rule for the process table |
| A process per operation in a loop | One long-lived helper with a request channel. A burst of short-lived interpreters under one parent is itself the signal |
| `Add-Type -TypeDefinition` at runtime | A precompiled, signed assembly, or a native helper |
| Copying our own image under a different name | An installer or updater that does not need the rename. Where the rename is load-bearing, document it as such |
| Deriving a script runner from a UI preference | [`windows-setup-shell.md`](./windows-setup-shell.md) — the script declares its own interpreter |
Two framing rules that outlast the table:
- **Shorten the interpreter chain.** Each hop between Orca and the user's actual
target is a scored edge and a place for AV to deny a `CreateProcess`. This is
the shipped doctrine of #15520 and #15595.
- **Do not spell a flag you can avoid spelling.** #16003 measured a denial that
was independent of the payload and keyed purely on the switch combination on
the command line. What is on the line is itself the detection surface.
## Guidance for administrators deploying Orca
### Path exclusions alone will not silence these
This is the single most important operational point, and it is the one most
commonly got wrong. The six incidents are **MDE EDR behavioural alerts**.
Defender Antivirus path exclusions suppress *scan* detections; they do not
suppress EDR behavioural alerts the same way. Adding
`%LOCALAPPDATA%\Programs\orca\` to the AV exclusion list and expecting the
incidents to stop will not work.
### What actually stops incidents being created
An **MDE alert suppression rule** scoped to the process tree. Build it in
Microsoft 365 Defender (Settings → Endpoints → Alert suppression), conditioned
on:
- **Alert titles** — `A suspicious file was observed` and
`Suspicious PowerShell command line`, plus any further titles your tenant
actually produced. Take the titles from your own incidents rather than from
this list.
- **File paths** — `Orca.exe` and `orca-terminal-daemon.exe` under
`%LOCALAPPDATA%\Programs\orca\` and `%LOCALAPPDATA%\Orca\daemon-host\`.
Scope it as narrowly as your tenant will tolerate, and review it when Orca
updates: the `daemon-host` path carries a `<version>` segment, so a rule pinned
to one version will silently stop matching. Two traps in that path in particular.
Materialization stages into a `<version>.staging-<hex>` sibling before renaming
it into place, so an exact-version rule misses the tree **mid-update** — which is
precisely when the update-cluster incidents fire. And the root falls back to the
Electron `userData` path when `LOCALAPPDATA` is unset, so
`%LOCALAPPDATA%\Orca\daemon-host\` is the normal location rather than a
guaranteed one. Prefer a prefix match on `…\Orca\daemon-host\` over a rule
pinned to one full path.
Add AV path exclusions for those two directories as well — they cut scan cost on
a tree that is rewritten on every update — but understand the division of
labour. The exclusions reduce scanning; **the suppression rule is what stops
incidents being created.**
### Check your ASR rules
Check whether the tenant has the Attack Surface Reduction rule **"Block
executable files from running unless they meet a prevalence, age, or trusted list
criterion"** enabled. If it is, that alone explains a freshly signed Orca build
being hit immediately after every update: each release ships new hashes, so every
build starts at zero prevalence and zero age no matter how it is signed. Either
allowlist the Orca install paths for that rule or expect a hit on each update.
### Expect the alerts to recur after each update
Prevalence is keyed on file hash. An update replaces the hashes, the reputation
starts over, and a suppression rule is the only thing carrying across.
## Computer use: decide before you deploy
Read this section before enabling computer use on a monitored endpoint, not
after.
> **On a monitored endpoint, an alert reading "Screenshots were taken
> unexpectedly on this device" is not the kind of finding a SOC dismisses on
> sight.**
Incident E is the shape to expect: 5 alerts, 37 evidence items, a multi-stage
incident mapped to ATT&CK **Execution + Collection**, and a description naming
screen capture found in a script launched by `powershell.exe`. Incident F adds
runtime-compiled MSIL to the same tree.
Every part of that is an accurate description of what the feature does. Orca's
computer use takes screenshots, synthesises keyboard and mouse input into other
applications, and compiles the P/Invoke stubs it needs at runtime. An
organisation that monitors for Collection-tactic activity — and any organisation
running MDE with default incident creation does — will see it, and will see it
as Collection.
So decide deliberately, in advance:
- **Allowlist it**, with a suppression rule covering the computer-use tree
(`powershell.exe` with `-File …\runtime.ps1`) as well as the base Orca paths,
and tell your SOC what it is before the first incident rather than during it.
- **Or leave it disabled** on monitored endpoints.
What does not work is deploying it un-triaged and handling the incidents
reactively. By the time a Collection-tactic incident is open, an analyst is
already reading a description of screenshots being taken without the user's
knowledge, and the burden of proof has moved to you.
+101
View File
@@ -2,6 +2,8 @@
Two properties of `wsl.exe` decide how every guest invocation has to be written. Both are silent
when you get them wrong: the command still runs and still exits 0, it just returns the wrong bytes.
Those are sections 1 and 2. A closing section answers the question that running Orca's writes
inside a distro raises next: what happens to the distro's disk image.
## 1. Always `--exec`, never `--`
@@ -70,3 +72,102 @@ wsl.exe -d <distro> --exec /usr/bin/env PATH=… HOME=… /usr/bin/git -C <dir>
This is what the direct-git read path does. It is immune to both problems above by construction and
avoids paying login-shell startup on every call, which also sidesteps profiles that block or print.
Resolve the PATH/HOME once through a fenced probe, cache it per distro, then use this form.
## Disk: the distro VHDX only grows
Everything above puts Orca's writes inside the distro, which raises a separate question. WSL2 keeps
the entire guest filesystem in a single dynamically-expanding `ext4.vhdx`. Deleting files inside
the distro does free the blocks — for ext4 to reuse — but the host-visible `.vhdx` does not shrink
on its own.
### Finding the file
The path depends on how the distro was installed, so do not assume one:
| Install method | `ext4.vhdx` lives under |
| ---------------------- | --------------------------------------------------------- |
| recent `wsl --install` | `%LOCALAPPDATA%\wsl\{guid}\` |
| Microsoft Store | `%LOCALAPPDATA%\Packages\<PackageFamilyName>\LocalState\` |
| `wsl --import` | wherever the operator pointed it |
The install-agnostic answer is the registry, which records every distro's directory as `BasePath`:
```powershell
Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss |
ForEach-Object { Get-ItemProperty $_.PSPath } |
Select-Object DistributionName, BasePath
```
### Measured behavior
WSL 2.7.11.0 / Ubuntu-24.04, one machine. Sizes are **size on disk** — allocated bytes, via
`GetCompressedFileSize`, not the logical file length. That distinction matters below: on this
machine the vhdx was not sparse, so the two numbers were identical, but on a sparse vhdx the
logical size stays pinned at the high-water mark while only size on disk falls when space is
reclaimed. Measure the wrong one and reclaim looks like it did nothing.
| Step | `ext4.vhdx` size on disk (bytes) |
| ---------------------------------- | -------------------------------- |
| baseline | 21,673,017,344 |
| write 1 GiB | 22,746,759,168 |
| delete it | 22,746,759,168 |
| write a fresh incompressible 1 GiB | 22,746,759,168 |
| hold 3 GiB live at once | 24,894,242,816 |
The fourth row is the point: the second gigabyte cost zero growth, because ext4 handed it the
blocks the first one freed. Only exceeding the previous peak moved the file.
### Reclaiming space
Sparse mode lets the guest hand freed blocks back to the host, so the file can shrink instead of
only growing. Two preconditions, both easy to miss: the distro has to be stopped (the vhdx cannot
be converted while it is mounted), and `wsl --manage` exists only on WSL 2.5 and newer — check with
`wsl --version`.
```
wsl --terminate <distro>
wsl --manage <distro> --set-sparse true
```
The equivalent for distros not yet created is `sparseVhd = true` under `[experimental]` in
`%UserProfile%\.wslconfig` — that file lives in the Windows user profile, **not** inside the distro
and not at `~/.wslconfig`, and does not exist until you create it.
Neither touches slack that already exists. For that, shut WSL down and compact the file by hand
from an **elevated** prompt. `compact vdisk` on its own fails because no virtual disk is selected,
so the `select` and the read-only `attach` are required, not optional:
```
wsl --shutdown
diskpart
DISKPART> select vdisk file="C:\path\to\ext4.vhdx"
DISKPART> attach vdisk readonly
DISKPART> compact vdisk
DISKPART> detach vdisk
DISKPART> exit
```
Two caveats, neither verified here: field reports say `compact vdisk` is a no-op on a vhdx that is
already sparse (convert back with `--set-sparse false` first), and sparse mode's runtime cost was
not measured. Microsoft's [disk-space guide](https://learn.microsoft.com/windows/wsl/disk-space)
carries the current locate/expand/compact procedure and the `--manage` version floor;
[`.wslconfig`](https://learn.microsoft.com/windows/wsl/wsl-config) carries `sparseVhd`. Enabling
sparse mode and compacting are both per-machine decisions; Orca does not make either.
On the measured machine the vhdx was **not** sparse: `fsutil sparse queryflag` reported "NOT set as
sparse", and no `%UserProfile%\.wslconfig` existed to opt in. Microsoft documents `sparseVhd` as
defaulting to `false`, so that is the expected state rather than a local quirk — but the flag is
per-vhdx, set when the disk is created or by an explicit conversion, so check your own distro
rather than assuming either way.
### What this means for Orca
A vhdx that grows as speculative worktree preparation and mirrored worktrees write into the distro
is expected. Its size is monotonically non-decreasing and roughly tracks peak concurrent usage —
but it can drift above peak, and the measurement above is the best case for reuse: the second
gigabyte was allocated immediately after the first was freed, out of the same block group. Under
sustained churn — many worktrees created and removed over weeks, no `fstrim`/discard, sparse off —
allocation spreads and the file settles higher than live peak.
So growth on its own is not evidence of a leak. Growth well above live peak usage is worth
investigating, and is the case the reclaim steps above address.
+7 -3
View File
@@ -3,18 +3,22 @@ title: How to use GLM-5.2 in Orca ADE
description: Configure Claude Code and other CLI agent harnesses to run GLM-5.2 inside Orca worktrees.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
GLM-5.2 works in Orca through the agent harness you already use. Configure GLM-5.2 in Claude Code, OpenCode, Cline, Kilo Code, Roo Code, Droid, OpenClaw, or another CLI agent, then launch that agent from Orca's picker.
Orca supplies the isolated worktree, terminal panes, browser tab, review flow, and session management. Your [Z.ai CodePlan subscription](https://z.ai/subscribe) and agent config supply the model access.
<Callout title="Prerequisite">
You need an active [Z.ai CodePlan subscription](https://z.ai/subscribe) with GLM Coding Plan access before configuring GLM-5.2 in an agent harness. OpenAI-compatible harnesses also need a Z.ai API key. Orca does not include or resell GLM access.
You need an active [Z.ai CodePlan subscription](https://z.ai/subscribe) with GLM Coding Plan
access before configuring GLM-5.2 in an agent harness. OpenAI-compatible harnesses also need a
Z.ai API key. Orca does not include or resell GLM access.
</Callout>
<Callout title="Source">
This page documents the GLM-5.2 configuration tested with Orca. Z.ai's [model guide](https://docs.z.ai/devpack/latest-model) may list newer models; verify model names, context limits, and harness compatibility there before substituting one.
This page documents the GLM-5.2 configuration tested with Orca. Z.ai's [model
guide](https://docs.z.ai/devpack/latest-model) may list newer models; verify model names, context
limits, and harness compatibility there before substituting one.
</Callout>
## Claude Code
@@ -3,12 +3,13 @@ title: Agent hibernation
description: Let Orca pause idle background agent terminals and auto-resume them when you reopen the worktree.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
When you keep dozens of worktrees open, idle agents add up — each one is a live PTY holding a model session in memory. Agent hibernation lets Orca quietly stop those terminals once they've been done and untouched long enough, then resume the same session the next time you open the worktree.
<Callout title="Experimental">
Agent hibernation is off by default. Turn it on under **Settings → Experimental → Agent hibernation** while we keep tuning the safety model.
Agent hibernation is off by default. Turn it on under **Settings → Experimental → Agent
hibernation** while we keep tuning the safety model.
</Callout>
## What gets hibernated
@@ -2,7 +2,7 @@
title: Agent hooks & memory
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Orca plays nicely with the agent hook and memory conventions Claude Code and Codex already use — it reads them, respects them, and gives you a UI for the ones that make sense in an IDE context.
@@ -27,5 +27,6 @@ Claude's `CLAUDE.md` and Codex's `AGENTS.md` (at repo root or nested) are left a
Hook endpoints are written to disk (`{userData}/agent-hooks/endpoint.env` on POSIX, `endpoint.cmd` on Windows) and re-sourced on every hook invocation, so long-lived agent sessions keep reaching the live Orca server even after an app restart — no more dead-port POSTs from a PTY that outlived the previous session.
<Callout>
The Orca CLI exposes a commented worktree status field agents can update themselves. See [Worktree checkpoints](/docs/cli/worktree-checkpoints).
The Orca CLI exposes a commented worktree status field agents can update themselves. See [Worktree
checkpoints](/docs/cli/worktree-checkpoints).
</Callout>
@@ -3,7 +3,7 @@ title: Chat UI (native chat)
description: Optional chat surface over supported agent terminals — skills, model pickers, and transcript view.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Chat UI is an experimental view layered on supported agent terminal sessions. The terminal remains the source of truth; Chat UI is a structured transcript + composer for the same PTY. Transcript decoding covers **Claude**, **Codex**, **Grok**, and **OMP** — OMP sessions open in Chat UI like the others instead of staying raw-terminal-only.
@@ -30,5 +30,6 @@ When Claude shows an **AskUserQuestion** (or similar structured permission/quest
Chat UI ships on desktop for supported local and remote (paired server) agent sessions. The [mobile companion](/docs/mobile) reuses chat-style transcript patterns for the same paired sessions.
<Callout title="Experimental">
Transcript fidelity, streaming, and terminal parity are still under active tuning. Prefer the raw TUI when you need every OSC/status detail.
Transcript fidelity, streaming, and terminal parity are still under active tuning. Prefer the raw
TUI when you need every OSC/status detail.
</Callout>
@@ -3,7 +3,7 @@ title: Agent session history
description: Browse and resume past Claude, Codex, Cursor, Gemini, and other agent sessions from Orca's right sidebar.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Orca scans the on-disk session transcripts that supported agent CLIs leave behind and lists them in a right-sidebar panel called **Agent Session History**. Pick a past session, click **Resume**, and Orca runs the agent's resume command in a fresh terminal — same `cwd`, same session ID, no manual `--resume` flag wrangling.
@@ -39,13 +39,16 @@ Click a session row to open its details: working directory, branch, model, messa
- **Resume** — opens a new terminal in the session's `cwd` and runs the agent's resume command (e.g. `claude --resume <id>`, `codex resume <id>`, `pi --session <session_file>`, `prime-agent --resume <path>`, `cursor-agent --resume <id>`, `acli rovodev run --restore <id>`). Codex sessions also re-export `CODEX_HOME` when the original session set one.
Pi resumes from the on-disk session file reported by its hooks (`--session <path>`), not from a bare session id. If that file is missing, Resume is unavailable for that row even when a session id exists.
- **Copy resume command** — copies the same shell command to the clipboard for use in an external terminal.
- **Copy session ID** / **Copy log path** — for scripting or attaching transcripts to bug reports.
- **Open log** / **Reveal log** — open the raw transcript file in Orca, or jump to it in your OS file manager.
- **Open cwd** — open the session's working directory as a workspace.
<Callout title="Resume needs a local workspace">
Resume runs the agent CLI on the machine where Orca is rendering. If you're connected to a remote workspace, switch back to a local one (or use **Copy resume command** and run it on the remote yourself) before clicking **Resume**.
Resume runs the agent CLI on the machine where Orca is rendering. If you're connected to a remote
workspace, switch back to a local one (or use **Copy resume command** and run it on the remote
yourself) before clicking **Resume**.
</Callout>
## Where the transcripts come from
+42 -39
View File
@@ -3,12 +3,15 @@ title: Supported agents
description: Every agent Orca ships with out of the box.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Orca works with **any CLI agent** — the agent combobox just launches a process in a terminal. The following ship preconfigured in the built-in agent picker with one-click launch/setup; deeper hooks, status, usage tracking, and account switching are noted where supported.
<Callout title="Permission safety">
The defaults below pass each agent's permission-bypass flag for new launches. A worktree is an isolated checkout, not a security sandbox: the agent can still access files and network resources available to its process. Choose **Manual** in **Settings → Agents → Agent Permissions** unless you intentionally trust the agent and the task.
The defaults below pass each agent's permission-bypass flag for new launches. A worktree is an
isolated checkout, not a security sandbox: the agent can still access files and network resources
available to its process. Choose **Manual** in **Settings → Agents → Agent Permissions** unless
you intentionally trust the agent and the task.
</Callout>
## Permissions default
@@ -19,40 +22,40 @@ Use **Settings → Agents → Agent Permissions** when you want to switch all un
To restore prompts for one agent only, edit that agent's default arguments or environment in Settings. Orca treats a non-empty custom value as an explicit override and opts that agent out of future permission-mode migrations.
| Agent | Notes | Docs |
| --- | --- | --- |
| Claude Code | Deep integration: usage, hot-swap, hooks | [Anthropic](https://docs.anthropic.com/claude/docs/claude-code) |
| Claude Agent Teams | Disabled by default — enable under Settings → Agents to launch via `orca claude-teams` with native panes for each teammate | [Anthropic](https://code.claude.com/docs/agent-teams) |
| Codex | Deep integration: usage, hot-swap | [OpenAI](https://github.com/openai/codex) |
| Grok | Auto-setup | [xAI](https://x.ai/cli) |
| GitHub Copilot CLI | Auto-setup | [GitHub](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) |
| OpenCode | Auto-setup, status | [OpenCode](https://opencode.ai/docs/cli/) |
| Pi | Auto-setup, hooks, status | [Pi](https://pi.dev) |
| OMP | Auto-setup, hooks, status | [OMP](https://omp.sh) |
| Prime Agent | Auto-setup, hooks, status, session history | [Prime Intellect](https://github.com/PrimeIntellect-ai/prime-agent) |
| Gemini | Auto-setup | [Google](https://github.com/google-gemini/gemini-cli) |
| Antigravity | Auto-setup, hooks, status | [Google](https://antigravity.google/docs/cli-overview) |
| Ante | Auto-setup, status | [Ante](https://github.com/AntigmaLabs/ante-preview) |
| Aider | Auto-setup | [Aider](https://aider.chat/docs/) |
| Goose | Auto-setup | [Block](https://block.github.io/goose/docs/quickstart/) |
| Amp | Auto-setup | [Amp](https://ampcode.com/manual#install) |
| Kilocode | Auto-setup | [Kilo](https://kilo.ai/docs/cli) |
| Kiro | Auto-setup | [Kiro](https://kiro.dev/docs/cli/) |
| Charm Crush | Auto-setup | [Charm](https://github.com/charmbracelet/crush) |
| Auggie | Auto-setup | [Augment](https://docs.augmentcode.com/cli/overview) |
| Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) |
| Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) |
| Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) |
| Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) |
| Continue | Auto-setup | [Continue](https://docs.continue.dev/guides/cli) |
| Cursor CLI | Deep integration | [Cursor](https://cursor.com/cli) |
| Devin | Auto-setup | [Devin](https://devin.ai/cli) |
| Droid (Factory) | Auto-setup, hooks, status | [Factory](https://docs.factory.ai/cli/getting-started/quickstart) |
| Kimi | Auto-setup | [Moonshot](https://www.kimi.com/code/docs/en/kimi-code-cli/getting-started.html) |
| Mistral Vibe | Auto-setup | [Mistral](https://github.com/mistralai/mistral-vibe) |
| MiniMax | Auto-setup, usage tracking, rate-limit tracking | [MiniMax](https://www.minimax.chat) |
| Qwen Code | Auto-setup via the installed `qwen` executable | [Qwen](https://github.com/QwenLM/qwen-code) |
| Rovo Dev | Auto-setup | [Atlassian](https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/) |
| Hermes | Auto-setup | [Nous](https://hermes-agent.nousresearch.com/docs/) |
| OpenClaw | Auto-setup | [OpenClaw](https://github.com/openclaw/openclaw) |
| Trae | Auto-setup via `traecli` (TRAE CN CLI) | [Trae](https://www.trae.ai/) |
| Agent | Notes | Docs |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Claude Code | Deep integration: usage, hot-swap, hooks | [Anthropic](https://docs.anthropic.com/claude/docs/claude-code) |
| Claude Agent Teams | Disabled by default — enable under Settings → Agents to launch via `orca claude-teams` with native panes for each teammate | [Anthropic](https://code.claude.com/docs/agent-teams) |
| Codex | Deep integration: usage, hot-swap | [OpenAI](https://github.com/openai/codex) |
| Grok | Auto-setup | [xAI](https://x.ai/cli) |
| GitHub Copilot CLI | Auto-setup | [GitHub](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) |
| OpenCode | Auto-setup, status | [OpenCode](https://opencode.ai/docs/cli/) |
| Pi | Auto-setup, hooks, status | [Pi](https://pi.dev) |
| OMP | Auto-setup, hooks, status | [OMP](https://omp.sh) |
| Prime Agent | Auto-setup, hooks, status, session history | [Prime Intellect](https://github.com/PrimeIntellect-ai/prime-agent) |
| Gemini | Auto-setup | [Google](https://github.com/google-gemini/gemini-cli) |
| Antigravity | Auto-setup, hooks, status | [Google](https://antigravity.google/docs/cli-overview) |
| Ante | Auto-setup, status | [Ante](https://github.com/AntigmaLabs/ante-preview) |
| Aider | Auto-setup | [Aider](https://aider.chat/docs/) |
| Goose | Auto-setup | [Block](https://block.github.io/goose/docs/quickstart/) |
| Amp | Auto-setup | [Amp](https://ampcode.com/manual#install) |
| Kilocode | Auto-setup | [Kilo](https://kilo.ai/docs/cli) |
| Kiro | Auto-setup | [Kiro](https://kiro.dev/docs/cli/) |
| Charm Crush | Auto-setup | [Charm](https://github.com/charmbracelet/crush) |
| Auggie | Auto-setup | [Augment](https://docs.augmentcode.com/cli/overview) |
| Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) |
| Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) |
| Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) |
| Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) |
| Continue | Auto-setup | [Continue](https://docs.continue.dev/guides/cli) |
| Cursor CLI | Deep integration | [Cursor](https://cursor.com/cli) |
| Devin | Auto-setup | [Devin](https://devin.ai/cli) |
| Droid (Factory) | Auto-setup, hooks, status | [Factory](https://docs.factory.ai/cli/getting-started/quickstart) |
| Kimi | Auto-setup | [Moonshot](https://www.kimi.com/code/docs/en/kimi-code-cli/getting-started.html) |
| Mistral Vibe | Auto-setup | [Mistral](https://github.com/mistralai/mistral-vibe) |
| MiniMax | Auto-setup, usage tracking, rate-limit tracking | [MiniMax](https://www.minimax.chat) |
| Qwen Code | Auto-setup via the installed `qwen` executable | [Qwen](https://github.com/QwenLM/qwen-code) |
| Rovo Dev | Auto-setup | [Atlassian](https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/) |
| Hermes | Auto-setup | [Nous](https://hermes-agent.nousresearch.com/docs/) |
| OpenClaw | Auto-setup | [OpenClaw](https://github.com/openclaw/openclaw) |
| Trae | Auto-setup via `traecli` (TRAE CN CLI) | [Trae](https://www.trae.ai/) |
@@ -16,7 +16,7 @@ Orca reads the local usage state each agent maintains on disk (under `~/.claude`
## Multi-account accounting
The status bar always reflects the *active* account. Other configured accounts are visible in the account switcher with their own usage.
The status bar always reflects the _active_ account. Other configured accounts are visible in the account switcher with their own usage.
## Usage roster
@@ -2,11 +2,14 @@
title: Design Mode
---
import { ImagePlaceholder } from '@/components/docs/prose';
import { ImagePlaceholder } from '@/components/docs/prose'
Design Mode turns the Orca browser into a pointer-to-code tool. Toggle it on, click any UI element on the rendered page, and the element drops into the agent chat as rich context — with its DOM, computed styles, and a screenshot.
<ImagePlaceholder src="/docs/orca-design-mode.gif" caption="Design Mode: click a button, it lands in the agent chat" />
<ImagePlaceholder
src="/docs/orca-design-mode.gif"
caption="Design Mode: click a button, it lands in the agent chat"
/>
## Turn it on
+5 -2
View File
@@ -2,11 +2,14 @@
title: Per-worktree browser
---
import { ImagePlaceholder } from '@/components/docs/prose';
import { ImagePlaceholder } from '@/components/docs/prose'
Every Orca worktree has its own browser. It's a real Chromium window — address bar, history, devtools — embedded in a pane. Tabs are scoped to the worktree, so the app you're building against stays out of the way of your other work.
<ImagePlaceholder src="/docs/orca-design-mode.gif" caption="Per-worktree browser pane with address bar and tab strip" />
<ImagePlaceholder
src="/docs/orca-design-mode.gif"
caption="Per-worktree browser pane with address bar and tab strip"
/>
## Controls
+4 -2
View File
@@ -3,12 +3,14 @@ title: Computer use
description: Drive local desktop apps from an agent via accessibility trees, screenshots, and safe UI actions.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
The `orca computer` CLI lets an agent inspect and control native desktop apps — list running apps, read accessibility trees, click controls, set values, type text, scroll, and take screenshots. Use it when a task needs to operate the OS or a third-party app rather than a terminal or the built-in browser.
<Callout title="Beta">
Computer use ships native helpers per platform and requires Accessibility (and on macOS, Screen Recording) permission. The command surface is stable enough for skills to build against, but flag names may still shift.
Computer use ships native helpers per platform and requires Accessibility (and on macOS, Screen
Recording) permission. The command surface is stable enough for skills to build against, but flag
names may still shift.
</Callout>
## First-time setup
+6 -3
View File
@@ -3,18 +3,21 @@ title: Orchestration
description: Coordinate agents with Runs, tasks, supervised workers, messages, and decision gates.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Orchestration is Orca's structured multi-agent layer: a **Run** (namespace + coordinator inbox), **Tasks**, **Dispatches**, supervised **workers**, messages, and decision gates.
Use it when you need ownership, completion tracking, or a DAG. For one-off prompts, use `orca terminal send`. For full ownership handoffs without supervision, use worktree/terminal commands from the `orca-cli` skill.
<Callout title="Experimental">
Enable orchestration under Settings → Experimental before using these commands. The CLI talks to the running Orca runtime, so `orca status --json` should succeed first.
Enable orchestration under Settings → Experimental before using these commands. The CLI talks to
the running Orca runtime, so `orca status --json` should succeed first.
</Callout>
<Callout title="Legacy commands retired">
`orca orchestration run` and `run-stop` (and `coordinator-start` / `coordinator-stop`) perform **no effects**. They return recovery text pointing at `orca skills get orchestration --full`. Use the Run + worker-start flow below.
`orca orchestration run` and `run-stop` (and `coordinator-start` / `coordinator-stop`) perform
**no effects**. They return recovery text pointing at `orca skills get orchestration --full`. Use
the Run + worker-start flow below.
</Callout>
## Core model
+4 -2
View File
@@ -10,7 +10,7 @@ keywords:
- agent CLI
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
The Orca CLI is the `orca` command-line interface for scripting a running Orca editor from any shell. Use it to create and inspect worktrees, drive agent terminals, open files and diffs, automate the built-in browser, run scheduled automations, share HTML/Markdown artifacts, and control Orca-native tools from scripts or AI agents.
@@ -118,5 +118,7 @@ orca emulator kill --json
Use `--worktree <selector>`, `--device <udid-or-name>`, or `--emulator <id>` when a script needs an explicit target.
<Callout>
For the full command surface including tabs, waits, cookies, and frames, see [Orca CLI reference](/docs/cli/reference), then install the Orca CLI skill (see [Skills registry](/docs/cli/skills)) and point your agent at it.
For the full command surface including tabs, waits, cookies, and frames, see [Orca CLI
reference](/docs/cli/reference), then install the Orca CLI skill (see [Skills
registry](/docs/cli/skills)) and point your agent at it.
</Callout>
+3 -2
View File
@@ -3,7 +3,7 @@ title: Orca CLI reference
description: Commands, selectors, and agent-friendly patterns for driving Orca from a shell.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
The `orca` CLI talks to a running Orca runtime. Use it when a shell script or agent needs to inspect worktrees, launch terminals, open files, automate the built-in browser, or report progress back into Orca.
@@ -116,7 +116,8 @@ orca terminal close --terminal <handle> --json
Omit `--terminal` to target the active terminal in the current worktree. Read before sending when you are not sure what the terminal is waiting for.
<Callout title="Terminal handles">
Terminal handles are runtime-scoped. If Orca restarts or a command reports a stale terminal handle, run `orca terminal list --json` and reacquire the handle.
Terminal handles are runtime-scoped. If Orca restarts or a command reports a stale terminal
handle, run `orca terminal list --json` and reacquire the handle.
</Callout>
`terminal list` reports each terminal's `executionHostId` when Orca can verify it, plus a result-level `hostScope` with covered and omitted host IDs. Treat a missing host identity or scope as **unverifiable**, not local. A missing terminal is evidence that it exited only when its execution host is listed in `hostScope.hostIds`.
+10 -10
View File
@@ -12,21 +12,21 @@ keywords:
- orca-emulator-android skill
---
Orca ships **skills** that agents install into their skill directories. Public install packages are **hybrid discovery stubs**: short `SKILL.md` files that tell the agent *when* to engage Orca and how to load the full guide from the running CLI. Command flags live in the binary so they cannot drift from the app version.
Orca ships **skills** that agents install into their skill directories. Public install packages are **hybrid discovery stubs**: short `SKILL.md` files that tell the agent _when_ to engage Orca and how to load the full guide from the running CLI. Command flags live in the binary so they cannot drift from the app version.
## Installable Orca skills
Use `npx skills add` with the public Orca repo and the skill name. Default agent setup usually installs `orca-cli`, `computer-use`, and `orchestration`.
| Skill | Install | Use it for |
| --- | --- | --- |
| [`orca-cli`](#orca-cli) | `npx skills add https://github.com/stablyai/orca --skill orca-cli --global` | Worktrees, terminals, files, automations, embedded browser. |
| [`orchestration`](#orchestration) | `npx skills add https://github.com/stablyai/orca --skill orchestration --global` | Multi-agent Runs, tasks, supervised workers, messages, gates. |
| [`computer-use`](#computer-use) | `npx skills add https://github.com/stablyai/orca --skill computer-use --global` | Desktop apps via accessibility trees and safe UI actions. |
| [`orca-linear`](#orca-linear) | `npx skills add https://github.com/stablyai/orca --skill orca-linear --global` | Linear ticket read/write through `orca linear`. |
| [`orca-emulator`](#orca-emulator) | `npx skills add https://github.com/stablyai/orca --skill orca-emulator --global` | iOS Simulator control. |
| [`orca-emulator-android`](#orca-emulator-android) | `npx skills add https://github.com/stablyai/orca --skill orca-emulator-android --global` | Android emulator/device via adb. |
| [`orca-per-workspace-env`](#orca-per-workspace-env) | `npx skills add https://github.com/stablyai/orca --skill orca-per-workspace-env --global` | Per-workspace environment recipes (`orca.yaml`). |
| Skill | Install | Use it for |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`orca-cli`](#orca-cli) | `npx skills add https://github.com/stablyai/orca --skill orca-cli --global` | Worktrees, terminals, files, automations, embedded browser. |
| [`orchestration`](#orchestration) | `npx skills add https://github.com/stablyai/orca --skill orchestration --global` | Multi-agent Runs, tasks, supervised workers, messages, gates. |
| [`computer-use`](#computer-use) | `npx skills add https://github.com/stablyai/orca --skill computer-use --global` | Desktop apps via accessibility trees and safe UI actions. |
| [`orca-linear`](#orca-linear) | `npx skills add https://github.com/stablyai/orca --skill orca-linear --global` | Linear ticket read/write through `orca linear`. |
| [`orca-emulator`](#orca-emulator) | `npx skills add https://github.com/stablyai/orca --skill orca-emulator --global` | iOS Simulator control. |
| [`orca-emulator-android`](#orca-emulator-android) | `npx skills add https://github.com/stablyai/orca --skill orca-emulator-android --global` | Android emulator/device via adb. |
| [`orca-per-workspace-env`](#orca-per-workspace-env) | `npx skills add https://github.com/stablyai/orca --skill orca-per-workspace-env --global` | Per-workspace environment recipes (`orca.yaml`). |
## Hybrid stubs vs the live guide
+6 -6
View File
@@ -34,12 +34,12 @@ YAML and TOML front matter is shown in the rich editor and rendered preview by d
In rich markdown tables:
| Key | Behavior |
| --- | --- |
| **Tab** / **Shift-Tab** | Next / previous cell; Tab past the last cell inserts a row |
| **Enter** | Move to the cell below; on the last row, add a row |
| **Backspace** on a fully empty row | Delete the row (or the whole table if it is the last row) |
| **Backspace** in an empty cell when the row still has content | Step to the previous cell |
| Key | Behavior |
| ------------------------------------------------------------- | ---------------------------------------------------------- |
| **Tab** / **Shift-Tab** | Next / previous cell; Tab past the last cell inserts a row |
| **Enter** | Move to the cell below; on the last row, add a row |
| **Backspace** on a fully empty row | Delete the row (or the whole table if it is the last row) |
| **Backspace** in an empty cell when the row still has content | Step to the previous cell |
Use **Shift-Tab** to unindent a list item or the selected lines of a code block.
+3 -2
View File
@@ -2,7 +2,7 @@
title: HTML, Mermaid, PDF & image viewers
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Orca includes built-in viewers for the formats that show up in most repos.
@@ -35,5 +35,6 @@ Scroll, zoom, and text selection. Useful for design docs checked into the repo.
`.ipynb` files open in a notebook viewer with rendered markdown, syntax-highlighted code cells, and saved outputs. Editing cells writes back to the on-disk `.ipynb` while preserving nbformat, so diffs stay clean.
<Callout title="Beta">
The notebook editor is marked beta. Cell execution and richer output rendering are still settling — file an issue if a notebook in your repo doesn't load cleanly.
The notebook editor is marked beta. Cell execution and richer output rendering are still settling
— file an issue if a notebook in your repo doesn't load cleanly.
</Callout>
+3 -2
View File
@@ -3,7 +3,7 @@ title: Your first 3-agent session
description: From empty app to three agents running in parallel in under five minutes.
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
This is the single most important page in the docs. By the end you'll have three agents running in parallel on three different approaches to the same task, with one PR shipped.
@@ -48,5 +48,6 @@ Once agents settle, open each worktree's diff view. Use [Annotate AI Diff](/docs
Commit and push directly from Orca — see [Commit & push from Orca](/docs/review/commit-push). The other two worktrees can be deleted with one click; their branches go with them.
<Callout title="That's it">
This flow — add → worktree → agent → split → diff → ship — is the whole of Orca. Every other page in these docs is a deeper look at one of those steps.
This flow — add → worktree → agent → split → diff → ship — is the whole of Orca. Every other page
in these docs is a deeper look at one of those steps.
</Callout>
+18 -18
View File
@@ -9,14 +9,14 @@ This page covers the errors you’ll see most often and how to fix them.
## Quick triage
| What you see | Likely cause | First thing to try |
| --- | --- | --- |
| “GitHub is rate-limiting requests” / “rate limit exceeded (core)” | GitHub REST (core) quota exhausted for your user | Wait for reset; stop extra `gh` / agent / Orca usage; check [Settings → Git → GitHub API Budget](/docs/settings) |
| “GitHub authentication is unavailable” / `gh auth` prompts | `gh` not logged in, expired token, or bad `GITHUB_TOKEN` | `gh auth status`, then `gh auth login` |
| “GitHub did not allow access” / HTTP 403 (not rate limit) | Missing scopes or no access to the repo | Re-auth with `repo` (and needed org SSO); confirm you can open the PR in the browser |
| “repository is unavailable” / HTTP 404 | Wrong remote, private repo without access, or renamed repo | Check `git remote -v` and browser access |
| “GitHub is unreachable” / timeouts | Network, proxy, VPN, or GitHub outage | Check [githubstatus.com](https://www.githubstatus.com/); retry off VPN |
| “GitHub CLI is unavailable” | `gh` missing from PATH Orca uses | Install `gh` and restart Orca |
| What you see | Likely cause | First thing to try |
| ----------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| “GitHub is rate-limiting requests” / “rate limit exceeded (core)” | GitHub REST (core) quota exhausted for your user | Wait for reset; stop extra `gh` / agent / Orca usage; check [Settings → Git → GitHub API Budget](/docs/settings) |
| “GitHub authentication is unavailable” / `gh auth` prompts | `gh` not logged in, expired token, or bad `GITHUB_TOKEN` | `gh auth status`, then `gh auth login` |
| “GitHub did not allow access” / HTTP 403 (not rate limit) | Missing scopes or no access to the repo | Re-auth with `repo` (and needed org SSO); confirm you can open the PR in the browser |
| “repository is unavailable” / HTTP 404 | Wrong remote, private repo without access, or renamed repo | Check `git remote -v` and browser access |
| “GitHub is unreachable” / timeouts | Network, proxy, VPN, or GitHub outage | Check [githubstatus.com](https://www.githubstatus.com/); retry off VPN |
| “GitHub CLI is unavailable” | `gh` missing from PATH Orca uses | Install `gh` and restart Orca |
## Rate limits (most common)
@@ -24,11 +24,11 @@ GitHub gives each **authenticated user** a shared hourly budget. **Every tool on
### Buckets Orca cares about
| Bucket | What it covers | Typical limit (authenticated) |
| --- | --- | --- |
| **REST (core)** | Most PR/issue/API calls (`gh pr view`, checks metadata, many REST endpoints) | 5,000 / hour |
| **GraphQL** | Project/Tasks and some richer PR queries | 5,000 points / hour |
| **Search** | Search-driven lists | 30 / minute |
| Bucket | What it covers | Typical limit (authenticated) |
| --------------- | ---------------------------------------------------------------------------- | ----------------------------- |
| **REST (core)** | Most PR/issue/API calls (`gh pr view`, checks metadata, many REST endpoints) | 5,000 / hour |
| **GraphQL** | Project/Tasks and some richer PR queries | 5,000 points / hour |
| **Search** | Search-driven lists | 30 / minute |
When a primary bucket is exhausted, GitHub returns HTTP **403** with a message like `API rate limit exceeded`. Orca classifies that as rate-limited, keeps the last known PR status when it can, and **stops spawning more `gh` calls** for a short window so a single limit doesn’t turn into a storm of failures.
@@ -106,11 +106,11 @@ GitHub auth is **per host**. Logging in on your laptop does not log in `gh` on a
## Permission and repository errors
| Symptom | Meaning |
| --- | --- |
| HTTP 403 without “rate limit” | Token lacks scope or you’re not allowed to see the resource |
| HTTP 404 / “could not resolve to a Repository” | Repo missing, renamed, or invisible to this token |
| “resource not accessible by integration” | App/token type can’t perform that action |
| Symptom | Meaning |
| ---------------------------------------------- | ----------------------------------------------------------- |
| HTTP 403 without “rate limit” | Token lacks scope or you’re not allowed to see the resource |
| HTTP 404 / “could not resolve to a Repository” | Repo missing, renamed, or invisible to this token |
| “resource not accessible by integration” | App/token type can’t perform that action |
Fixes:
+5 -3
View File
@@ -1,9 +1,9 @@
---
title: What is Orca?
description: "A 60-second pitch: who Orca is for and when to reach for it."
description: 'A 60-second pitch: who Orca is for and when to reach for it.'
---
import { Callout } from '@/components/docs/prose';
import { Callout } from '@/components/docs/prose'
Orca is a desktop IDE for running multiple AI coding agents side by side. Every task gets its own git worktree, its own agent terminal, and its own browser tab — so you can fan out work across Claude Code, Codex, Cursor CLI, and friends without stashing, branch-juggling, or losing flow.
@@ -25,5 +25,7 @@ Orca is designed for people who already write code for a living and want to use
- **Not a hosted VPS product.** Orca runs on your desktop by default. Remote compute uses machines and cloud accounts you control — [SSH targets](/docs/ssh), [self-hosted Orca servers](/docs/remote-servers), or [Cloud VMs / per-workspace environments](/docs/ways-to-run#4-cloud-vms-per-workspace-environments).
<Callout title="Next steps">
Head to [Install](/docs/install), then walk through [Your first 3-agent session](/docs/first-session) — the single most important page in these docs. When you're ready to move agents off the laptop, start with [Ways to run Orca](/docs/ways-to-run).
Head to [Install](/docs/install), then walk through [Your first 3-agent
session](/docs/first-session) — the single most important page in these docs. When you're ready to
move agents off the laptop, start with [Ways to run Orca](/docs/ways-to-run).
</Callout>

Some files were not shown because too many files have changed in this diff Show More