Merge remote-tracking branch 'origin/main' into brennanb2025/pty-provider-types-split

# Conflicts:
#	src/main/providers/types.ts
This commit is contained in:
Brennan Benson
2026-07-29 20:52:42 -07:00
1945 changed files with 194295 additions and 24026 deletions
+5
View File
@@ -0,0 +1,5 @@
# Product source remains owned by its normal reviewers; localization inputs need focused review.
/src/renderer/src/i18n/locales/ @brennanb2025
/config/scripts/*localization*.mjs @brennanb2025
/config/scripts/*locale*.mjs @brennanb2025
/i18next.config.ts @brennanb2025
@@ -6,6 +6,10 @@ inputs:
description: Native runtime to prepare after the script-free install (none, node, or electron).
required: false
default: none
node-version:
description: Node.js version override; defaults to the version declared in package.json.
required: false
default: ''
runs:
using: composite
@@ -17,11 +21,19 @@ runs:
run_install: false
- name: Setup Node.js
if: inputs.node-version == ''
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: pnpm
- name: Setup requested Node.js
if: inputs.node-version != ''
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: pnpm
- name: Validate native runtime
shell: bash
env:
+2
View File
@@ -9,6 +9,7 @@ on:
- 'config/scripts/build-windows-cli-launcher.mjs'
- 'config/scripts/build-windows-cli-launcher.test.mjs'
- 'config/scripts/computer-e2e-workflow.test.mjs'
- 'config/scripts/computer-use-modifier-safety.test.mjs'
- 'config/scripts/computer-use-skill-guidance.test.mjs'
- 'config/scripts/computer-use-smoke.mjs'
- 'config/scripts/computer-use-smoke.test.mjs'
@@ -84,6 +85,7 @@ jobs:
config/scripts/build-windows-cli-launcher.test.mjs
src/main/ssh/ssh-remote-cli-launcher.test.ts
config/scripts/computer-e2e-workflow.test.mjs
config/scripts/computer-use-modifier-safety.test.mjs
config/scripts/computer-use-skill-guidance.test.mjs
config/scripts/computer-use-smoke.test.mjs
src/main/computer/computer-provider-lifecycle.test.ts
+17 -2
View File
@@ -1,5 +1,11 @@
name: E2E
run-name: E2E ${{ inputs.ref || github.ref }}
# Why: checkout + artifact upload only; callers can only further restrict.
permissions:
contents: read
on:
workflow_call:
inputs:
@@ -76,7 +82,7 @@ jobs:
name: e2e ${{ matrix.shard_name }}
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -178,7 +184,9 @@ jobs:
name: ssh docker watcher isolation
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
# Why 35: the terminal parking + retention specs below add two more
# docker-rig tests capped at 240s each on top of the watcher isolation pair.
timeout-minutes: 35
steps:
- name: Checkout
@@ -221,6 +229,13 @@ jobs:
- name: Run Docker SSH watcher isolation E2E
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-watcher-isolation
# Why always(): these specs gate the C1 terminal parking/retention budget
# over a real SSH host and run in no other lane, so a watcher-isolation
# failure above must not silently skip them.
- name: Run Docker SSH terminal parking + retention E2E
if: always()
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-terminal-parking
- name: Upload watcher isolation traces
if: failure()
uses: actions/upload-artifact@v7
+93 -4
View File
@@ -24,6 +24,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
@@ -31,8 +32,20 @@ jobs:
- name: Lint
run: pnpm exec oxlint --format github
- name: Check styled scrollbars
run: pnpm check:styled-scrollbars
- name: Enforce focused code-quality plugins
run: pnpm run audit:code-quality:native
- name: Enforce type-aware code-quality baseline
run: pnpm run audit:code-quality:type-aware
- name: Enforce changed-code quality
run: pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}"
- name: Enforce React Doctor on changed lines
run: pnpm run check:react-doctor:changed -- "${{ github.event.pull_request.base.sha }}"
- name: Check Zustand selector fan-out budget
run: pnpm run check:zustand-selector-fanout
- name: Check reliability gate manifest
run: pnpm run check:reliability-gates
@@ -46,6 +59,22 @@ jobs:
- name: Verify skill freshness manifest
run: pnpm run verify:skill-bundle-manifest
- name: Verify localization catalog
run: pnpm run verify:localization-catalog
# Why: extraction writes sorted evidence to an isolated temporary path,
# so feature PRs need one normalized AST pass rather than a three-OS matrix.
- name: Verify localization extraction
run: pnpm run verify:localization-extraction
- name: Verify localization coverage
run: pnpm run verify:localization-coverage
# Why: project-owned type declarations must live in .ts so tsc
# actually checks them. TypeScript's skipLibCheck: true (inherited
# from @electron-toolkit/tsconfig) silently widens unresolved names
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
# past typecheck. See docs/preload-typecheck-hole.md.
- name: Guard against project-owned .d.ts in preload/shared
run: |
matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true)
@@ -155,12 +184,14 @@ jobs:
src/shared/posix-command-path-lookup.test.ts
test:
name: tests ${{ matrix.shard }}/${{ strategy.job-total }}
name: tests node ${{ matrix.node }} ${{ matrix.shard }}/${{ matrix.shard_total }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['24', '26']
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
shard_total: [16]
steps:
- name: Checkout
@@ -171,6 +202,7 @@ jobs:
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
node-version: ${{ matrix.node }}
- name: Install Electron package binary for tests
run: node config/scripts/install-electron-package-binary.mjs
@@ -184,7 +216,7 @@ jobs:
--exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \
--exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \
--exclude=src/shared/posix-command-path-lookup.test.ts \
--shard=${{ matrix.shard }}/${{ strategy.job-total }}
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
package:
name: package
@@ -241,6 +273,55 @@ jobs:
- name: Smoke packaged CLI
run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked
# Why: regression specs under tests/e2e/** used to merge green without ever
# running — e2e.yml only fired on schedule/release (#10518). Path-filter so
# ordinary PRs stay light; any E2E suite change still gets a full shard run.
e2e-paths:
name: detect e2e path changes
runs-on: ubuntu-latest
if: github.event.pull_request.draft != true
# Why: detector only needs to read the checkout; do not inherit repo defaults.
permissions:
contents: read
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Filter E2E-relevant paths
id: filter
run: |
set -euo pipefail
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
# Why: capture first so a failed git diff does not look like "no matches"
# (pipeline status in `if` is not aborted by set -e). Merge-base limits the
# list to files this PR introduced, not base-branch drift.
CHANGED="$(git diff --name-only --merge-base "$BASE" "$HEAD")"
# Why: tests/playwright.config.ts sits beside tests/e2e/, not inside it, so
# it needs its own pattern — a bare `playwright.` prefix matches no tracked
# file and would silently skip E2E when the runner config changes.
if printf '%s\n' "$CHANGED" | grep -E '^(tests/e2e/|tests/playwright\.|\.github/workflows/e2e\.yml$)' >/dev/null; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "E2E path changes detected"
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "No E2E path changes"
fi
e2e:
name: e2e
needs: e2e-paths
if: needs.e2e-paths.outputs.should_run == 'true'
# Why: reusable e2e.yml only checkouts, builds, and uploads artifacts.
permissions:
contents: read
uses: ./.github/workflows/e2e.yml
verify:
if: always()
needs:
@@ -253,6 +334,14 @@ jobs:
runs-on: ubuntu-latest
steps:
# Why: e2e is deliberately absent from needs. The suite is currently red on
# main (every scheduled run), so gating merges on it would block any PR that
# touches tests/e2e/** — including the ones fixing the suite. Until it is
# green the job runs and reports for E2E-path PRs without blocking. To flip
# it on: add `e2e` to needs, add E2E to the env below, and require
# `"$E2E" = success || skipped` after the loop — skipped is the normal
# result for a path-filtered job and must keep passing, so it has to be
# checked outside the loop or it would excuse the jobs above.
- name: Require successful checks
env:
STATIC_ANALYSIS: ${{ needs.static_analysis.result }}
+27 -10
View File
@@ -777,7 +777,7 @@ jobs:
echo "## Release E2E Signal"
echo ""
echo "- Terminal rendering golden is release-blocking."
echo "- Full E2E is diagnostic/non-blocking release evidence."
echo "- Full E2E runs separately after publication and cannot change the release result."
echo "- Terminal rendering release evidence is diagnostic/non-blocking."
echo ""
echo "Publishing behavior is controlled by the existing job dependencies; this summary does not change release gating."
@@ -807,15 +807,6 @@ jobs:
node config/scripts/create-draft-release.mjs "$TAG"
# Why: tag-scoped E2E gives release visibility, but the suite is flaky enough
# that publish-release must not depend on it.
e2e:
needs: cut
if: needs.cut.outputs.should_release == 'true'
uses: ./.github/workflows/e2e.yml
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
terminal-rendering-golden:
needs: cut
if: needs.cut.outputs.should_release == 'true'
@@ -1930,6 +1921,32 @@ jobs:
--prerelease="$prerelease" \
--repo "$GITHUB_REPOSITORY"
post-release-e2e:
needs:
- cut
- publish-release
if: ${{ needs.cut.outputs.tag != '' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Dispatch tag-scoped E2E
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.cut.outputs.tag }}
run: |
for attempt in 1 2 3; do
if gh workflow run e2e.yml \
--repo "$GITHUB_REPOSITORY" \
--ref "$TAG" \
--raw-field "ref=refs/tags/$TAG"; then
echo "Dispatched post-release E2E for $TAG."
exit 0
fi
[[ "$attempt" -eq 3 ]] || sleep "$((attempt * 5))"
done
echo "::warning::Failed to dispatch post-release E2E for $TAG after 3 attempts."
homebrew-bump-published-rc-draft:
needs:
- cut
+99
View File
@@ -0,0 +1,99 @@
name: Terminal IME E2E
on:
pull_request:
paths:
- '.github/workflows/terminal-ime-e2e.yml'
- 'config/patches/@xterm__xterm@6.1.0-beta.287.patch'
- 'config/scripts/run-terminal-ibus-hangul-e2e.mjs'
- 'config/scripts/terminal-ime-e2e-workflow.test.mjs'
- 'package.json'
- 'pnpm-lock.yaml'
- 'src/renderer/src/components/terminal-pane/keyboard-handlers.ts'
- 'src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx'
- 'src/renderer/src/components/terminal-pane/pty-connection.ts'
- 'src/renderer/src/components/terminal-pane/pty-connection.test.ts'
- 'src/renderer/src/components/terminal-pane/terminal-ime-*'
- 'src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts'
- 'src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts'
- 'src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts'
- 'tests/e2e/chinese-ime-chat-input-repro.spec.ts'
- 'tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts'
- 'tests/e2e/terminal-ibus-hangul-native.spec.ts'
- 'tests/e2e/terminal-ime-*.ts'
workflow_dispatch:
schedule:
- cron: '30 9 * * *'
permissions:
contents: read
jobs:
linux-x11:
name: Linux X11 terminal IME
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install native build and IME tools
run: >-
sudo apt-get update &&
sudo apt-get install -y
build-essential
dbus-x11
dconf-gsettings-backend
ibus
ibus-hangul
libglib2.0-bin
python3
xdotool
xfwm4
xvfb
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
run_install: false
- name: Use external node-gyp to avoid pnpm bundled copy
run: |
npm install -g node-gyp@11.5.0
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Electron app for E2E
run: pnpm exec electron-vite build --mode e2e
- name: Run deterministic terminal IME boundary tests
run: >-
xvfb-run --auto-servernum
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1
pnpm run test:e2e --
tests/e2e/terminal-ime-exact-byte.spec.ts
--workers=1
- name: Run native IBus Hangul exact-byte tests
env:
SKIP_BUILD: '1'
run: pnpm run test:e2e:terminal-ime-native
- name: Upload terminal IME evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: terminal-ime-evidence
path: test-results/
retention-days: 7
if-no-files-found: ignore
+37 -5
View File
@@ -1,10 +1,33 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "react", "react-hooks", "react-perf", "unicorn"],
"jsPlugins": [
{
"name": "mobile-pairing",
"specifier": "./config/oxlint-plugins/mobile-pairing-qrcode-import.mjs"
},
{
"name": "app-store-performance",
"specifier": "./config/oxlint-plugins/app-store-performance.mjs"
},
{
"name": "quadratic-buffer-concat",
"specifier": "./config/oxlint-plugins/quadratic-buffer-concat.mjs"
},
{
"name": "renderer-scrollbar-style",
"specifier": "./config/oxlint-plugins/renderer-scrollbar-style.mjs"
}
],
"categories": {
"correctness": "error"
},
"rules": {
"app-store-performance/require-selector": "error",
"app-store-performance/no-identity-selector": "error",
"app-store-performance/no-fresh-selector-result": "error",
"no-fallthrough": "error",
"quadratic-buffer-concat/no-loop-carried-concat": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-undef": "error",
"react/no-children-prop": "error",
@@ -57,15 +80,11 @@
"name": "@linear/sdk",
"allowTypeImports": true,
"message": "Value-importing @linear/sdk hoists its ~2.6MB CJS bundle into the eager top-level require block and defeats the lazy loader. Use `import type` plus loadLinearSdk() from src/main/linear/linear-sdk.ts."
},
{
"name": "qrcode",
"allowTypeImports": true,
"message": "qrcode is only reachable from mobile pairing; a static import parses it at launch for everyone. Use `import type` plus `await import('qrcode')` at the call site."
}
]
}
],
"mobile-pairing/no-eager-qrcode-import": "error",
"no-useless-return": "error",
"prefer-template": "error",
"unicorn/consistent-empty-array-spread": "error",
@@ -91,6 +110,19 @@
"unicorn/throw-new-error": "error"
},
"overrides": [
{
"files": ["src/renderer/src/**/*.{ts,tsx}"],
"rules": {
"renderer-scrollbar-style/require-styled-vertical-scrollbar": "error"
}
},
{
"files": ["**/*.test.*", "**/*.spec.*", "**/*-benchmark.*"],
"rules": {
"quadratic-buffer-concat/no-loop-carried-concat": "off",
"renderer-scrollbar-style/require-styled-vertical-scrollbar": "off"
}
},
{
"files": ["**/*.ts"],
"rules": {
-604
View File
@@ -1,604 +0,0 @@
# Orchestration Structured Worker Output
Status: implemented; physical local, mixed-version, and Windows-home to Mac-worker validation complete
Scope: orchestration `worker-read` only
Last updated: 2026-07-24
## Summary
`orca orchestration worker-read` currently reads bounded terminal output. That is always available,
but full-screen agent TUIs can make it noisy or incomplete.
Orca already knows more than the terminal logo suggests. Agent hooks associate an exact pane with:
- the agent type, such as Codex or Claude;
- the provider session or conversation ID; and
- when available, the provider-reported transcript path.
The sidebar, session resume, sleeping-agent recovery, and native chat already use this information.
The missing piece is a narrow orchestration path from an exact Dispatch to that exact session on the
server where the worker runs.
The proposed behavior is:
```text
worker-read
exact supported transcript is available -> structured transcript page
otherwise -> bounded terminal page
```
This does not add orchestration strategy, a dashboard, a scheduler, or a universal provider layer.
It makes one existing observation command return the best source Orca can prove.
## User-facing goal
A coordinator should be able to inspect a worker with one predictable command:
```bash
orca orchestration worker-read --dispatch <dispatch-id> --json
```
The coordinator should not need to know:
- which Orca server owns the worker;
- the worker's terminal or pane handle;
- the provider session ID;
- where a transcript lives on disk; or
- whether structured reading is supported by that provider/server version.
The response must always say which source was used. It must never silently read a different agent
session.
## Why the existing sidebar is relevant
The agent logo identifies the detected agent type. By itself, that is not enough to choose a
transcript.
The richer sidebar status also carries pane-scoped provider-session metadata reported by hooks.
That is the useful foundation:
```text
Dispatch
-> exact worker process and terminal
-> exact tab/leaf pane
-> hook-reported provider session
-> provider transcript locator
```
Some live status ownership is currently renderer-centric, while headless and mobile graph paths
also retain compatible status snapshots. Implementation therefore needs one runtime-owned lookup
that exposes the current exact pane association to `worker-read`. This is a small bridge over
existing status data, not a second agent-status system.
## Design principles
### Exactness over convenience
- Never select the "latest session in this directory."
- Never select a transcript from a terminal title or logo alone.
- Never switch sources or sessions in the middle of a cursor chain.
- If Orca cannot prove the association, return a labeled terminal fallback.
### Resource-local reads
The server running the worker resolves and reads its transcript. A Run home on macOS must not try
to interpret a Windows path, and a Windows Run home must not try to interpret a macOS path.
Only bounded output data crosses the federation connection. Transcript paths do not.
### One simple agent command
Agents should not choose provider adapters or supply session metadata. `worker-read` defaults to
automatic source selection. Source selection flags exist for debugging and explicit policy, not
because they are required in the normal loop.
### Narrow provider support
Initial support should cover only providers for which Orca already has:
1. an exact pane-scoped session association; and
2. an existing bounded transcript reader with test coverage.
Codex is the required first provider. Claude may ship in the same change only if it uses the same
proven reader path without adding a second architecture. Other agents receive terminal fallback.
### Honest compatibility
Connected servers can run different Orca versions. A server without structured-read support must
continue to return bounded terminal output rather than failing the whole Run.
## Public command contract
### Request
```bash
orca orchestration worker-read \
--dispatch <dispatch-id> \
[--source auto|transcript|terminal] \
[--cursor <opaque-cursor>] \
[--limit <count>] \
[--json]
```
`--source` behavior:
| Value | Behavior |
| ------------ | -------------------------------------------------------------------------------------- |
| `auto` | Use an exact supported transcript; otherwise use terminal output. This is the default. |
| `transcript` | Require an exact supported transcript. Return a typed error instead of falling back. |
| `terminal` | Use the current bounded terminal reader. |
Existing numeric terminal cursors remain accepted. New responses return an opaque cursor that can
pin either source without exposing provider paths.
### Response
The implemented structured response has this shape:
```json
{
"dispatchId": "dispatch_123",
"source": "transcript",
"sourceIdentity": "opaque-source-fingerprint",
"provider": "codex",
"transcript": {
"messages": [],
"nextCursor": "opaque-next-cursor",
"limited": false,
"returnedMessageCount": 0
},
"cursor": "opaque-next-cursor",
"status": {
"worker": "running",
"terminal": "running"
},
"fallbackReason": null,
"warnings": []
}
```
Terminal fallback uses the same envelope and keeps the existing terminal data:
```json
{
"dispatchId": "dispatch_123",
"source": "terminal",
"sourceIdentity": "opaque-terminal-incarnation",
"terminal": {
"tail": ["..."],
"status": "running",
"nextCursor": "..."
},
"cursor": "opaque-next-cursor",
"fallbackReason": "session_not_reported",
"warnings": []
}
```
For backward compatibility:
- the terminal branch retains the existing `terminal.tail`, `terminal.status`, and
`terminal.nextCursor` fields;
- non-JSON output prints readable transcript entries or terminal lines without requiring an agent
to branch on JSON manually;
- a mixed-version federated read can return the legacy terminal shape, which the Run home wraps as
a labeled terminal response.
### Fallback reasons
Fallback reasons are bounded typed values, not arbitrary policy:
- `provider_unsupported`
- `session_not_reported`
- `transcript_missing`
- `transcript_unreadable`
- `transcript_parse_failed`
- `remote_capability_unavailable`
Warnings may provide safe diagnostic context, but must not contain a remote filesystem path.
### Typed errors
Errors are used when returning any source would be misleading:
- `dispatch_not_found`
- `worker_identity_changed`
- `source_changed`
- `cursor_invalid`
- `cursor_dispatch_mismatch`
- `transcript_required`
- the existing connected-server unavailable/unknown result
`source_changed` means the exact pane is now associated with a different provider session than the
one pinned by the cursor. The caller starts a new read without the old cursor. Orca never silently
jumps to the replacement session.
## Source identity and cursor rules
The cursor is opaque to clients and contains only non-sensitive routing data:
- cursor version;
- Dispatch ID;
- source kind;
- an opaque digest of the exact source identity;
- provider-specific or terminal paging position.
It must not contain a transcript path.
On every continued read, the worker server:
1. revalidates the Dispatch's exact process attachment;
2. resolves the current pane/session association;
3. compares its source digest with the cursor;
4. reads only when they still match; and
5. otherwise returns `source_changed` or `worker_identity_changed`.
The cursor is source-pinned even when the request uses `--source auto`. `auto` chooses only on the
first page.
The implementation uses a versioned stateless token. The token is not an authority credential:
every read revalidates the Dispatch, process, pane, source digest, and provider session before
returning data. This lets paging survive an Orca restart without adding cursor-secret persistence.
## Runtime architecture
### 1. Resolve the Dispatch at its Run home
The Run home remains authoritative for Task and Dispatch state. It looks up the Dispatch and its
pinned worker server exactly as `worker-show` and the current `worker-read` do.
No automatic placement or server selection is added.
Current implementation anchors:
- `src/main/runtime/rpc/methods/orchestration-worker-control.ts` owns local and federated
`worker-read` routing.
- `src/main/runtime/rpc/methods/orchestration-worker-observation.ts` validates the exact attached
worker.
- `src/cli/handlers/orchestration.ts` owns the current CLI request and terminal rendering.
### 2. Route the read to the worker server
For a local worker, the Run home and worker server are the same runtime.
For a federated worker, the Run home calls the existing federation read route on the server pinned
to the Dispatch. The request contains the Dispatch ID, source preference, cursor, and limit—not a
terminal handle, session ID, or transcript path chosen by the coordinator.
### 3. Revalidate the exact worker
The worker server uses the existing Dispatch attachment to verify:
- the exact managed pane;
- the exact terminal/process incarnation; and
- that the Dispatch has not been replaced, stopped, or detached.
This preserves the same no-cross-worker rule already used by worker lifecycle and terminal reads.
### 4. Resolve the pane's provider session
Add one runtime-owned resolver that returns a snapshot similar to:
```ts
type ExactWorkerProviderSession = {
paneKey: string
agent: TuiAgent
providerSession: AgentProviderSessionMetadata
observedAt: number
}
```
The resolver may use the current runtime graph/headless retained status, but it must accept the
Dispatch's exact pane identity as input. It must not search all sessions by working directory or
agent type.
The source association is considered usable only when:
- it belongs to the Dispatch's exact current pane/process;
- the hook metadata is fresh enough to belong to that process incarnation;
- the provider session metadata passes existing normalization/canonicalization; and
- a supported adapter can resolve an exact transcript.
If these checks fail under `auto`, the read falls back to terminal output.
Current session/status anchors:
- `src/shared/agent-session-resume.ts` defines normalized provider-session metadata.
- `src/renderer/src/store/slices/agent-status.ts` maintains pane-scoped live agent status.
- `src/shared/runtime-types.ts` carries compatible agent status in runtime/mobile graph snapshots.
- `src/main/runtime/orca-runtime.ts` preserves provider-session metadata when it publishes those
snapshots.
### 5. Read through a narrow adapter
Reuse the existing bounded native-chat transcript parser rather than adding another parser stack.
Extract or wrap its pure reader behind a small orchestration adapter:
```ts
type WorkerTranscriptReader = {
provider: 'codex' | 'claude' | 'openclaude' | 'grok'
readPage(input: ExactTranscriptRead): Promise<ExactTranscriptPage>
}
```
This is deliberately not a registry for every possible agent capability. Add an adapter only when
an exact locator and a tested reader already exist.
Current reader anchors:
- `src/main/native-chat/transcript-watch.ts` provides bounded transcript reads/subscriptions.
- `src/main/runtime/rpc/methods/native-chat.ts` exposes the existing reader over runtime RPC.
- `src/main/ipc/native-chat.ts` exposes the same reader to the desktop renderer.
The transcript response should preserve the existing structured message/block representation and
the supported fields that the proven reader already understands. Unknown or skipped input should
produce parsing warnings rather than being silently presented as a complete transcript.
### 6. Return bounded data
Every path enforces:
- at most 50 transcript messages per page (40 by default);
- a maximum serialized response size;
- existing clipping/redaction rules for large tool input and output;
- opaque projection of transcript-position fallback IDs and redaction of Dispatch capability tokens;
- deterministic pagination; and
- no transcript path leakage.
Transcript observation is read-only. A failure or unknown network result must never trigger worker
restart, retry, stop, or Task mutation.
## Federation and cross-platform behavior
The core topology is:
```text
Mac Run home
-> authenticated connected-server RPC
-> Windows worker server
-> exact Windows pane/session
-> Windows-local transcript reader
-> bounded structured page back to Mac
```
The reverse direction must work identically.
Platform rules:
- Use Node path operations only on the server that owns the path.
- Do not normalize Windows paths on macOS or macOS/Linux paths on Windows.
- SSH and WSL execution remain behind their owning Orca server.
- If the exact transcript is accessible only on an SSH/WSL execution host, the worker server must
use an existing host-aware read mechanism or fall back to terminal. Do not copy the path to the
Run home.
- Mixed-version capability negotiation applies only at the Orca server protocol boundary.
- A server that does not advertise structured worker read receives the existing terminal-read RPC.
Federation adds one narrow additive RPC, `orchestration.federationReadOutput`. The Run home probes
it by calling it. If the worker server returns `method_not_found`, the Run home calls the existing
`orchestration.federationRead` terminal method and wraps that result as a labeled
`remote_capability_unavailable` fallback. No generalized capability matrix is added.
## Lifecycle behavior
### Provider session appears after worker start
Hooks may report a session after the TUI becomes ready. An initial `auto` read may therefore return
terminal output. A later first-page `auto` read may select transcript output.
Once a cursor is returned, that cursor remains pinned to its selected source.
### Provider session changes
Compaction, resume, or process replacement may produce a new provider session:
- a fresh read without a cursor may select the new exact session;
- a cursor for the old session returns `source_changed`;
- Orca does not merge the old and new transcripts implicitly.
### Orca restart
After restart, the worker server re-establishes the exact pane/process association using the same
runtime graph and retained-hook mechanisms used by sidebar/session recovery.
- If exact identity and session still match, paging continues.
- If process identity is uncertain, return `worker_identity_changed`.
- If only transcript identity is unavailable, `auto` may start a new terminal page but must not
reinterpret an old transcript cursor as a terminal cursor.
### Disconnect
A disconnected federated read is a read-only unknown result. Reissuing the same read is safe.
No mutation request ledger, durable outbox, automatic failover, or worker replacement is needed.
## Implementation plan
### Work package 1 — Correct the contract and types
- Update the orchestration checklist to replace the inaccurate claim that exact pane-to-session
association does not exist.
- Add the source preference, response envelope, fallback enums, and opaque cursor types.
- Keep the existing terminal response fields compatible.
- Add the CLI `--source` option and accept both legacy numeric and new opaque cursors.
Exit gate: contract tests cover legacy terminal JSON and the new labeled envelopes.
### Work package 2 — Runtime exact-session resolver
- Add a runtime method that resolves agent status for an exact Dispatch pane/process.
- Reuse the existing graph/headless retained status and provider-session normalization.
- Reject stale pane or process-incarnation associations.
- Test multiple panes and multiple sessions in the same worktree and directory.
Exit gate: the resolver can never return a sibling pane's session.
### Work package 3 — Codex transcript adapter
- Reuse the current bounded native-chat Codex reader.
- Add deterministic page conversion and parsing warnings.
- Enforce entry and byte limits.
- Produce a path-free source identity digest and cursor.
- Add Claude only if it follows this same path without new infrastructure.
Exit gate: exact Codex transcript pages are stable, bounded, and contain no local path.
### Work package 4 — Local `worker-read`
- Resolve and validate the exact worker.
- Implement `auto`, `transcript`, and `terminal`.
- Pin the source across cursor pages.
- Preserve the existing terminal fallback and non-JSON rendering.
Exit gate: local dogfood proves correct selection with several simultaneous same-directory Codex
sessions.
### Work package 5 — Federated `worker-read`
- Add the narrow connected-server capability.
- Route transcript resolution and reading to the worker server.
- Wrap legacy remote terminal responses as labeled fallbacks.
- Reject mismatched Dispatch/server/session cursors.
- Ensure paths and internal server identity remain out of ordinary output.
Exit gate: physical Mac-to-Windows and Windows-to-Mac reads both pass.
### Work package 6 — Restart, fallback, and documentation
- Cover runtime restart, renderer restart, disconnect, stale status, missing hooks, unreadable
transcript, unsupported providers, and mixed server versions.
- Update CLI help, the orchestration skill, and the implementation checklist.
- Dogfood the common coordinator loop using only the documented commands.
Exit gate: every fallback is truthful and no fallback changes worker lifecycle state.
## Validation plan
### Unit and contract tests
| Area | Required proof |
| --------------------- | ------------------------------------------------------------------------------------ |
| Exact resolution | A Dispatch resolves only its attached pane and process incarnation. |
| No directory guessing | Two Codex sessions in the same worktree cannot cross-read. |
| Source choice | `auto` prefers an exact supported transcript and otherwise labels terminal fallback. |
| Explicit source | `transcript` fails truthfully when unavailable; `terminal` never probes transcript. |
| Cursor pinning | Continued pages stay on the same source and provider session. |
| Session replacement | An old cursor returns `source_changed`. |
| Cursor custody | A cursor for another Dispatch is rejected. |
| Parsing | Malformed/skipped transcript records produce bounded warnings. |
| Limits | Entry count, block size, and total serialized response are bounded. |
| Privacy | Responses and cursors contain no transcript path. |
| Compatibility | Existing terminal fields and numeric cursors continue to work. |
Likely focused test locations:
- provider-session normalization and pane association tests;
- native-chat transcript reader tests;
- orchestration worker-control RPC tests;
- orchestration worker CLI tests; and
- federation protocol and physical harness tests.
### Local integration scenarios
1. Start a Codex worker and confirm the sidebar reports its provider session.
2. Read the Dispatch and verify `source=transcript`.
3. Start two Codex workers in the same worktree.
4. Give them distinct prompts and verify neither read contains the other's content.
5. Page both transcripts and verify stable source identities.
6. replace or resume one session and verify its old cursor returns `source_changed`.
7. Disable hooks and verify a labeled terminal fallback.
8. Remove or make the transcript unreadable and verify a safe fallback or
`transcript_required`, depending on the requested source.
### Physical federation matrix
| Run home | Worker server | Worker location | Required outcome |
| -------- | ------------- | --------------- | --------------------------------------------------------------- |
| macOS | Windows | native Windows | Exact structured page or labeled supported fallback |
| Windows | macOS | native macOS | Exact structured page or labeled supported fallback |
| macOS | macOS/Linux | SSH host | Exact host-aware page or terminal fallback without path leakage |
| Windows | Windows | WSL | Exact host-aware page or terminal fallback without path leakage |
For both Mac/Windows directions:
- run multiple workers at once;
- page beyond the first response;
- restart the Run home;
- restart the worker server;
- disconnect and reconnect the server;
- verify mixed-version fallback with one server lacking the new capability; and
- compare the selected provider session with the sidebar/native-chat session for the same pane.
### Dogfood procedure
The dogfood is successful only if a coordinator can follow this loop without internal IDs:
1. Create/use a Run.
2. Start one local worker and one connected-server worker.
3. Wait for both starts to settle.
4. Call `worker-read --source auto` for each Dispatch.
5. Continue each cursor through at least two pages.
6. Confirm output belongs to the correct prompt and machine.
7. Trigger one fallback case.
8. Complete both workers and confirm reads never altered lifecycle state.
Record:
- command and response;
- chosen source and fallback reason;
- worker server/platform;
- provider and session match;
- cursor behavior;
- path-leak check;
- restart/disconnect outcome; and
- any agent confusion using only CLI help and the orchestration skill.
## Acceptance criteria
Implementation is complete only when:
- `worker-read` selects an exact transcript or returns a clearly labeled terminal fallback.
- No test or dogfood scenario reads a sibling or previous provider session.
- A cursor never switches source or provider session silently.
- Mac-to-Windows and Windows-to-Mac physical reads pass.
- Runtime restart and disconnect behavior are safe and understandable.
- Unsupported agents and mixed versions retain useful terminal output.
- Transcript paths never leave the server that owns them.
- Existing terminal-read clients remain compatible.
- The common agent path remains one command with no server/session/path inputs.
- No UI, scheduling, retry, integration tracking, or generalized provider framework is added.
## Explicit non-goals
- No dashboard or sidebar changes.
- No coordinator chat changes.
- No automatic worker placement, retry, replacement, or recovery.
- No commit, test, branch, merge, or integration tracking.
- No provider-session locking or resume orchestration.
- No live transcript subscription in the orchestration API.
- No universal transcript/event ontology.
- No cross-server filesystem access from the Run home.
- No replicated Run database or automatic Run-home failover.
- No generalized access-control or capability framework.
## Main risks and mitigations
| Risk | Mitigation |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Live session status is stale or renderer-owned | Resolve through a runtime-owned exact-pane snapshot and bind it to process incarnation. |
| Two sessions share a directory | Never use directory/latest-session lookup; require exact pane/session metadata. |
| A session changes between pages | Pin source identity in the cursor and return `source_changed`. |
| Remote path is meaningless or sensitive | Read only on the worker server and never serialize the path. |
| Transcript parser drops data | Preserve supported structured blocks and return parsing warnings. |
| Transcript metadata exposes a path/credential | Make file-position IDs opaque and redact Dispatch capabilities from all structured text/payloads. |
| Mixed server versions | Negotiate one narrow capability and fall back to existing terminal read. |
| Full-screen terminal output remains noisy | Prefer structured output only when exact; retain terminal as the universal safety path. |
| Scope expands into a provider platform | Ship Codex first and require proven exact association plus an existing reader for every addition. |
## Decision
Implement structured worker output as a narrow extension of `worker-read`.
The prerequisite is not a new sidebar or status system: Orca already tracks exact pane-scoped
provider sessions. The work is to make that existing association available to the worker-owning
runtime, read the transcript locally through proven readers, pin pagination to that source, and
federate only the bounded result.
+5 -4
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.32](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.32/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.36](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.36/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.32](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.32/app-release.apk)
- **Android:** [Download APK 0.0.36](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.36/app-release.apk)
---
@@ -238,9 +238,10 @@ 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:** All other groups are full, now we're on group 5.
- **WeChat:** If group 5 is full, you can join group 6.
<img src="docs/assets/wechat-qr.jpg" alt="WeChat QR code for the Orca community" width="160" />
<img src="docs/assets/wechat-qr.jpg" alt="WeChat group 5 QR code for the Orca community" width="160" style="margin-right: 16px;" />
<img src="docs/assets/wechat-qr-group6.jpg" alt="WeChat group 6 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).
- **Privacy:** See the [privacy &amp; telemetry docs](https://www.onorca.dev/docs/telemetry) for what anonymous usage data Orca collects and how to opt out.
+1
View File
@@ -21,6 +21,7 @@ type OutputChunk = Rollup.OutputChunk
const PLAIN_NODE_ENTRY_NAMES = [
'daemon-entry',
'parcel-watcher-process-entry',
'main-thread-hang-watchdog-entry',
'computer-sidecar',
'agent-hooks/managed-agent-hook-controls',
'codex/codex-app-server-grant-entry'
+31 -2
View File
@@ -12,10 +12,13 @@ const {
verifyPackagedMainRuntimeDeps
} = require('./packaged-runtime-node-modules.cjs')
const { verifyLinuxGlibcFloor } = require('./scripts/verify-linux-glibc-floor.cjs')
const { writeMacBuildCompatibility } = require('./scripts/mac-build-compatibility.cjs')
const { verifyPackagedPluginResources } = require('./scripts/verify-packaged-plugin-resources.cjs')
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1'
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
const localBuildVersion = isMacRelease ? undefined : process.env.ORCA_LOCAL_BUILD_VERSION
const appId = 'com.stablyai.orca'
const featureWallResources = {
from: 'resources/onboarding/feature-wall',
to: 'onboarding/feature-wall'
@@ -59,8 +62,9 @@ const winSpeechNativeResource = {
/** @type {import('electron-builder').Configuration} */
module.exports = {
appId: 'com.stablyai.orca',
appId,
productName: 'Orca',
...(localBuildVersion ? { extraMetadata: { version: localBuildVersion } } : {}),
directories: {
buildResources: 'resources/build'
},
@@ -79,12 +83,17 @@ module.exports = {
'!skill-guides{,/**/*}',
'!skill-stubs{,/**/*}',
'!tests{,/**/*}',
// Why: examples/ is plugin authoring documentation with no runtime consumer —
// bundled plugins ship via extraResources from resources/plugins/launch/. It also
// carries hostile-panel, the adversarial fixture the containment tests point at,
// which must never reach a user's install.
'!examples{,/**/*}',
// Why: pr-evidence/ is a local e2e screenshot output (ORCA_CAPTURE_EVIDENCE);
// it is gitignored, but exclude it defensively so a stray local capture at
// package time never bloats app.asar.
'!pr-evidence{,/**/*}',
'!Casks{,/**/*}',
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md}',
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md,ORCHESTRATION_IMPLEMENTATION_CHECKLIST.md,ORCHESTRATION_STRUCTURED_OUTPUT_DESIGN.md}',
'!out/**/*.test.js',
// Why: Vite's manifest is only used to project the paired web client.
'!out/renderer/.vite{,/**/*}',
@@ -144,6 +153,7 @@ module.exports = {
'out/main/plugin-host-entry.js',
'out/main/computer-sidecar.js',
'out/main/parcel-watcher-process-entry.js',
'out/main/main-thread-hang-watchdog-entry.js',
'out/main/chunks/**',
'resources/**',
'node_modules/ws/**',
@@ -171,6 +181,25 @@ module.exports = {
if (!existsSync(resourcesDir)) {
return
}
if (context.electronPlatformName === 'darwin') {
const architectureByEnum = { 1: 'x64', 3: 'arm64' }
const architecture = architectureByEnum[context.arch]
if (!architecture) {
throw new Error(`Unsupported local-build compatibility architecture: ${context.arch}`)
}
const version = context.packager.appInfo.version
let commit = process.env.ORCA_BUILD_COMMIT || process.env.GITHUB_SHA || 'unknown'
if (commit === 'unknown') {
try {
commit = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], {
encoding: 'utf8'
}).trim()
} catch {
// Source archives can still produce a signed build with an explicit version.
}
}
writeMacBuildCompatibility(resourcesDir, { version, commit, architecture })
}
prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName, context.arch)
verifyPackagedMainRuntimeDeps(resourcesDir)
// Why: boot the packaged daemon-entry under plain Node, but only for the
+23 -7
View File
@@ -52,11 +52,27 @@ Sync catalog keys after adding or removing `translate(...)` calls:
pnpm run sync:localization-catalog
```
The sync command adds missing `en.json` entries from each call's string fallback,
copies untranslated English placeholders into other locale catalogs to keep
parity, removes locale entries whose English key was deleted, and repairs
placeholder mismatches. Run the machine-translation bootstrap commands only when
refreshing real translations, not for ordinary UI copy changes.
The sync command adds missing `en.json` entries from each call's string fallback.
It never edits target catalogs: missing values remain absent and use the existing
runtime English fallback. Existing placeholder mismatches fail validation until
a localization PR fixes or retires the target entry.
Run maintained source extraction without committing a second English catalog:
```sh
pnpm run verify:localization-extraction
```
Extraction fails when a statically extracted key is absent from `en.json` or an
inline default has incompatible placeholders. Existing unreferenced English keys
and wording-only fallback drift are reported as migration debt; the permanent
bilingual translation source will reconcile them without a large disposition
database. Reviewed and stale translation state is likewise deferred to that
source rather than inferred permanently from Git history.
The legacy free-endpoint bootstrap and whole-catalog repair scripts intentionally
have no package-script entry points. Ordinary product and localization work must
not invoke tools that can overwrite an entire target catalog.
The coverage gate compares current candidates against
`config/localization-coverage-allowlist.json`. The committed allowlist is empty:
@@ -97,8 +113,8 @@ Recommended migration order:
The final gate should combine three checks:
1. Scanner coverage: no unclassified localizable candidates remain.
2. Catalog coverage: every supported locale has the same keys as English, with
matching interpolation variables.
2. Catalog correctness: existing translations have matching interpolation
variables; missing target entries are reported rather than rejected.
3. Runtime coverage: pseudo-localization and real locale smoke tests show no
obvious English leftovers or layout clipping in core screens.
@@ -40,5 +40,33 @@
"text": "Idioma",
"dynamic": false,
"count": 1
},
{
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
"kind": "object-property:keywords",
"text": "Ghostty",
"dynamic": false,
"count": 2
},
{
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
"kind": "object-property:keywords",
"text": "ghostty",
"dynamic": false,
"count": 2
},
{
"filePath": "src/renderer/src/components/settings/terminal-pane-appearance-search.ts",
"kind": "object-property:keywords",
"text": "Ghostty",
"dynamic": false,
"count": 1
},
{
"filePath": "src/renderer/src/components/settings/terminal-pane-appearance-search.ts",
"kind": "object-property:keywords",
"text": "ghostty",
"dynamic": false,
"count": 1
}
]
@@ -0,0 +1,48 @@
{
"$schema": "../node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "jsx-a11y", "react-hooks", "vitest"],
"categories": {
"correctness": "off",
"suspicious": "off",
"pedantic": "off",
"perf": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"import/export": "warn",
"import/named": "warn",
"import/namespace": "warn",
"import/no-cycle": ["warn", { "maxDepth": 3 }],
"import/no-duplicates": "warn",
"import/no-self-import": "warn"
},
"overrides": [
{
"files": ["src/renderer/src/**/*.{ts,tsx}"],
"rules": {
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-role": "warn",
"jsx-a11y/click-events-have-key-events": "warn"
}
},
{
"files": ["**/*.{test,spec}.{ts,tsx}", "tests/**/*.{ts,tsx}"],
"rules": {
"vitest/no-conditional-tests": "warn",
"vitest/no-focused-tests": "warn",
"vitest/no-identical-title": "warn"
}
},
{
"files": ["mobile/**/*.{ts,tsx}"],
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
],
"ignorePatterns": ["**/node_modules", "**/dist", "**/out"]
}
@@ -11,9 +11,21 @@
"nursery": "off"
},
"rules": {
"typescript/await-thenable": "warn",
"typescript/restrict-plus-operands": "warn",
"typescript/restrict-template-expressions": "warn",
"typescript/switch-exhaustiveness-check": [
"error",
{ "allowDefaultCaseForExhaustiveSwitch": false }
]
}
},
"overrides": [
{
"files": ["**/*.test.*", "**/*.spec.*"],
"rules": {
"typescript/await-thenable": "off"
}
}
],
"ignorePatterns": ["**/node_modules", "**/dist", "**/out"]
}
@@ -0,0 +1,244 @@
const ALLOCATING_METHODS = new Set([
'filter',
'flat',
'flatMap',
'map',
'toReversed',
'toSorted',
'toSpliced',
'with'
])
function identifierName(node) {
return node?.type === 'Identifier' ? node.name : null
}
function propertyName(node) {
if (node?.type !== 'MemberExpression') {
return null
}
if (!node.computed) {
return identifierName(node.property)
}
return node.property?.type === 'Literal' && typeof node.property.value === 'string'
? node.property.value
: null
}
function returnedExpressions(selector) {
if (selector?.type !== 'ArrowFunctionExpression' && selector?.type !== 'FunctionExpression') {
return []
}
if (selector.body.type !== 'BlockStatement') {
return [selector.body]
}
const expressions = []
const visit = (node) => {
if (!node || typeof node !== 'object') {
return
}
if (
node !== selector.body &&
['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression'].includes(node.type)
) {
return
}
if (node.type === 'ReturnStatement') {
if (node.argument) {
expressions.push(node.argument)
}
return
}
for (const [key, child] of Object.entries(node)) {
if (key === 'parent') {
continue
}
if (Array.isArray(child)) {
child.forEach(visit)
} else {
visit(child)
}
}
}
visit(selector.body)
return expressions
}
function unwrapShallowSelector(selector, shallowHooks) {
if (
selector?.type === 'CallExpression' &&
selector.callee.type === 'Identifier' &&
shallowHooks.has(selector.callee.name)
) {
return { selector: selector.arguments[0], shallow: true }
}
return { selector, shallow: false }
}
function isIdentitySelector(selector) {
if (selector?.type !== 'ArrowFunctionExpression' && selector?.type !== 'FunctionExpression') {
return false
}
const parameter = selector.params[0]
if (parameter?.type !== 'Identifier') {
return false
}
return returnedExpressions(selector).some(
(expression) => expression.type === 'Identifier' && expression.name === parameter.name
)
}
function isAllocatingExpression(expression) {
if (expression?.type === 'ConditionalExpression') {
return (
isAllocatingExpression(expression.consequent) || isAllocatingExpression(expression.alternate)
)
}
if (expression?.type === 'LogicalExpression') {
return isAllocatingExpression(expression.left) || isAllocatingExpression(expression.right)
}
if (
expression?.type === 'ArrayExpression' ||
expression?.type === 'ObjectExpression' ||
expression?.type === 'NewExpression'
) {
return true
}
if (expression?.type !== 'CallExpression') {
return false
}
const method = propertyName(expression.callee)
if (method && ALLOCATING_METHODS.has(method)) {
return true
}
const callee = expression.callee
return (
callee.type === 'MemberExpression' &&
identifierName(callee.object) === 'Object' &&
['assign', 'create', 'entries', 'fromEntries', 'keys', 'values'].includes(propertyName(callee))
)
}
function importedLocalName(specifier, importedName) {
if (specifier.type !== 'ImportSpecifier' || identifierName(specifier.imported) !== importedName) {
return null
}
return identifierName(specifier.local)
}
function createRuleState() {
return {
appStoreHooks: new Set(),
shallowHooks: new Set()
}
}
function recordImports(node, state) {
if (node.source?.value === 'zustand/react/shallow') {
for (const specifier of node.specifiers) {
const localName = importedLocalName(specifier, 'useShallow')
if (localName) {
state.shallowHooks.add(localName)
}
}
}
for (const specifier of node.specifiers) {
const localName = importedLocalName(specifier, 'useAppStore')
if (localName) {
state.appStoreHooks.add(localName)
}
}
}
function isAppStoreCall(node, state) {
return (
node.callee.type === 'Identifier' &&
state.appStoreHooks.has(node.callee.name) &&
node.optional !== true
)
}
function requireSelectorRule() {
const state = createRuleState()
return {
ImportDeclaration(node) {
recordImports(node, state)
},
CallExpression(node) {
if (isAppStoreCall(node, state) && node.arguments.length === 0) {
this.report({
node,
message:
'Pass a selector to useAppStore so the component does not rerender for every store write.'
})
}
}
}
}
function noIdentitySelectorRule() {
const state = createRuleState()
return {
ImportDeclaration(node) {
recordImports(node, state)
},
CallExpression(node) {
if (!isAppStoreCall(node, state)) {
return
}
const { selector } = unwrapShallowSelector(node.arguments[0], state.shallowHooks)
if (isIdentitySelector(selector)) {
this.report({
node: selector,
message:
'Select the smallest required fields instead of subscribing to the entire app store.'
})
}
}
}
}
function noFreshSelectorResultRule() {
const state = createRuleState()
return {
ImportDeclaration(node) {
recordImports(node, state)
},
CallExpression(node) {
if (!isAppStoreCall(node, state)) {
return
}
const { selector, shallow } = unwrapShallowSelector(node.arguments[0], state.shallowHooks)
if (shallow) {
return
}
const freshResult = returnedExpressions(selector).find(isAllocatingExpression)
if (freshResult) {
this.report({
node: freshResult,
message:
'This selector returns a fresh reference on every store write; select a stable field, cache the result, or use useShallow.'
})
}
}
}
}
function bindContext(createVisitors) {
return (context) => {
const visitors = createVisitors()
for (const [nodeType, visit] of Object.entries(visitors)) {
visitors[nodeType] = visit.bind(context)
}
return visitors
}
}
export default {
meta: { name: 'app-store-performance' },
rules: {
'require-selector': { create: bindContext(requireSelectorRule) },
'no-identity-selector': { create: bindContext(noIdentitySelectorRule) },
'no-fresh-selector-result': { create: bindContext(noFreshSelectorResultRule) }
}
}
@@ -0,0 +1,29 @@
const MESSAGE =
"qrcode is only reachable from mobile pairing. Use `import type` plus `await import('qrcode')` so startup does not parse its bundle."
function hasRuntimeImport(node) {
if (node.importKind === 'type') {
return false
}
return (
node.specifiers.length === 0 ||
node.specifiers.some((specifier) => specifier.importKind !== 'type')
)
}
export default {
meta: { name: 'mobile-pairing' },
rules: {
'no-eager-qrcode-import': {
create(context) {
return {
ImportDeclaration(node) {
if (node.source?.value === 'qrcode' && hasRuntimeImport(node)) {
context.report({ node, message: MESSAGE })
}
}
}
}
}
}
}
@@ -0,0 +1,266 @@
const LOOP_TYPES = new Set([
'ForStatement',
'ForInStatement',
'ForOfStatement',
'WhileStatement',
'DoWhileStatement'
])
const ASSIGNMENT_OPERATORS = new Set(['=', '+=', '??=', '||=', '&&='])
const EXPRESSION_WRAPPERS = new Set([
'ChainExpression',
'TSAsExpression',
'TSNonNullExpression',
'TSSatisfiesExpression',
'TypeCastExpression'
])
function normalizeReferenceText(text) {
return text.replaceAll(/\s+/g, '')
}
function sourceText(context, node) {
return context.sourceCode.getText(node)
}
function memberPropertyName(node) {
if (node?.type !== 'MemberExpression') {
return null
}
if (!node.computed && node.property.type === 'Identifier') {
return node.property.name
}
return node.property.type === 'Literal' && typeof node.property.value === 'string'
? node.property.value
: null
}
function rootReferenceText(context, node) {
if (node?.type === 'Identifier') {
return node.name
}
if (node?.type === 'MemberExpression') {
return node.object.type === 'ThisExpression'
? normalizeReferenceText(sourceText(context, node))
: rootReferenceText(context, node.object)
}
if (node?.type === 'CallExpression') {
return rootReferenceText(context, node.callee)
}
if (EXPRESSION_WRAPPERS.has(node?.type)) {
return rootReferenceText(context, node.expression)
}
return null
}
function isBufferConcatCall(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Buffer' &&
memberPropertyName(node.callee) === 'concat' &&
node.arguments[0]?.type === 'ArrayExpression'
)
}
function enclosingLoop(node) {
for (let current = node.parent; current; current = current.parent) {
if (LOOP_TYPES.has(current.type)) {
return current
}
}
return null
}
function nodeStart(node) {
return node.start ?? node.range?.[0] ?? 0
}
function nodeEnd(node) {
return node.end ?? node.range?.[1] ?? 0
}
function isDeclaredInsideLoop(declarationStart, loop) {
if (declarationStart < nodeStart(loop) || declarationStart >= nodeEnd(loop)) {
return false
}
if (loop.type !== 'ForStatement' || !loop.init) {
return true
}
return declarationStart < nodeStart(loop.init) || declarationStart >= nodeEnd(loop.init)
}
function collectBindingNames(pattern, names) {
if (!pattern) {
return
}
if (pattern.type === 'Identifier') {
names.push(pattern.name)
} else if (pattern.type === 'RestElement') {
collectBindingNames(pattern.argument, names)
} else if (pattern.type === 'AssignmentPattern') {
collectBindingNames(pattern.left, names)
} else if (pattern.type === 'ObjectPattern') {
for (const property of pattern.properties) {
collectBindingNames(
property.type === 'RestElement' ? property.argument : property.value,
names
)
}
} else if (pattern.type === 'ArrayPattern') {
for (const element of pattern.elements) {
collectBindingNames(element, names)
}
}
}
function visitChildren(node, visit) {
for (const [key, child] of Object.entries(node)) {
if (['parent', 'loc', 'range'].includes(key)) {
continue
}
if (Array.isArray(child)) {
for (const item of child) {
if (item?.type) {
visit(item)
}
}
} else if (child?.type) {
visit(child)
}
}
}
function collectAssignedRoots(context, loop) {
const assigned = new Set()
const visit = (node) => {
if (node.type === 'AssignmentExpression' && ASSIGNMENT_OPERATORS.has(node.operator)) {
const root = rootReferenceText(context, node.left)
if (root) {
assigned.add(root)
}
}
visitChildren(node, visit)
}
visit(loop.body)
return assigned
}
function assignmentTargetOf(context, call) {
let node = call
let parent = node.parent
while (
parent &&
(EXPRESSION_WRAPPERS.has(parent.type) ||
(parent.type === 'ConditionalExpression' && parent.test !== node))
) {
node = parent
parent = parent.parent
}
if (parent?.type !== 'AssignmentExpression' || parent.operator !== '=' || parent.right !== node) {
return null
}
return {
text: normalizeReferenceText(sourceText(context, parent.left)),
root: rootReferenceText(context, parent.left)
}
}
function concatOperands(context, call) {
return call.arguments[0].elements.filter(Boolean).map((element) => {
const spread = element.type === 'SpreadElement'
const expression = spread ? element.argument : element
return {
spread,
text: normalizeReferenceText(sourceText(context, expression)),
root: rootReferenceText(context, expression)
}
})
}
function isLoopCarried(root, loop, declarations) {
const starts = declarations.get(root)
return !starts || !starts.some((start) => isDeclaredInsideLoop(start, loop))
}
function quadraticAccumulator(context, call, loop, declarations, assignedRoots) {
const operands = concatOperands(context, call)
const target = assignmentTargetOf(context, call)
const selfOperand = target
? operands.find((operand) => operand.text === target.text || operand.root === target.root)
: null
if (selfOperand && target.root && isLoopCarried(target.root, loop, declarations)) {
return target.text
}
for (const operand of operands) {
if (
!operand.spread &&
operand.root &&
assignedRoots.has(operand.root) &&
isLoopCarried(operand.root, loop, declarations)
) {
return operand.root
}
}
return null
}
function createRule(context) {
const declarations = new Map()
const assignedRootsByLoop = new WeakMap()
const recordBindings = (pattern, owner) => {
const names = []
collectBindingNames(pattern, names)
for (const name of names) {
const starts = declarations.get(name) ?? []
starts.push(nodeStart(owner))
declarations.set(name, starts)
}
}
const recordParameters = (node) => {
for (const parameter of node.params) {
recordBindings(parameter, parameter)
}
}
return {
VariableDeclarator(node) {
recordBindings(node.id, node)
},
FunctionDeclaration: recordParameters,
FunctionExpression: recordParameters,
ArrowFunctionExpression: recordParameters,
CatchClause(node) {
recordBindings(node.param, node.param)
},
CallExpression(node) {
if (!isBufferConcatCall(node)) {
return
}
const loop = enclosingLoop(node)
if (!loop) {
return
}
let assignedRoots = assignedRootsByLoop.get(loop)
if (!assignedRoots) {
assignedRoots = collectAssignedRoots(context, loop)
assignedRootsByLoop.set(loop, assignedRoots)
}
const accumulator = quadraticAccumulator(context, node, loop, declarations, assignedRoots)
if (accumulator) {
context.report({
node,
message: `Buffer.concat rebuilds loop-carried ${accumulator}; collect chunks and concatenate once after the loop.`
})
}
}
}
}
export default {
meta: { name: 'quadratic-buffer-concat' },
rules: {
'no-loop-carried-concat': { create: createRule }
}
}
@@ -0,0 +1,292 @@
const STYLED_SCROLLBAR_CLASSES = new Set([
'scrollbar-sleek',
'scrollbar-editor',
'worktree-sidebar-scrollbar'
])
const VERTICAL_SCROLL_CLASSES = new Set([
'overflow-auto',
'overflow-scroll',
'overflow-y-auto',
'overflow-y-scroll'
])
const VERTICAL_SCROLL_STYLE_VALUES = new Set(['auto', 'scroll'])
function withoutImportantModifier(className) {
const withoutPrefix = className.startsWith('!') ? className.slice(1) : className
return withoutPrefix.endsWith('!') ? withoutPrefix.slice(0, -1) : withoutPrefix
}
export function plainClassName(token) {
const normalizedToken = token.startsWith('!') ? token.slice(1) : token
const parts = []
let bracketDepth = 0
let currentPart = ''
for (const char of normalizedToken) {
if (char === '[') {
bracketDepth += 1
} else if (char === ']') {
bracketDepth = Math.max(0, bracketDepth - 1)
}
if (char === ':' && bracketDepth === 0) {
parts.push(currentPart)
currentPart = ''
} else {
currentPart += char
}
}
parts.push(currentPart)
return withoutImportantModifier(parts.at(-1) ?? '')
}
function classTokenParts(token) {
const variants = []
let bracketDepth = 0
let currentPart = ''
for (const char of token.startsWith('!') ? token.slice(1) : token) {
if (char === '[') {
bracketDepth += 1
} else if (char === ']') {
bracketDepth = Math.max(0, bracketDepth - 1)
}
if (char === ':' && bracketDepth === 0) {
variants.push(currentPart)
currentPart = ''
} else {
currentPart += char
}
}
return { className: withoutImportantModifier(currentPart), variants: variants.filter(Boolean) }
}
function classTokens(text) {
return text.split(/\s+/).filter(Boolean).map(classTokenParts)
}
function sameVariants(left, right) {
return left.length === right.length && left.every((variant, index) => variant === right[index])
}
function literalHasScrollbarForVertical(text, verticalToken) {
return classTokens(text).some(
(candidate) =>
STYLED_SCROLLBAR_CLASSES.has(candidate.className) &&
(candidate.variants.length === 0 || sameVariants(candidate.variants, verticalToken.variants))
)
}
function uncoveredVerticalClass(text) {
return classTokens(text).find(
(token) =>
VERTICAL_SCROLL_CLASSES.has(token.className) && !literalHasScrollbarForVertical(text, token)
)
}
function stringLiteralTexts(node) {
if (node?.type === 'Literal' && typeof node.value === 'string') {
return [node.value]
}
if (node?.type !== 'TemplateLiteral') {
return []
}
return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw)
}
function visitChildren(node, visit) {
for (const [key, child] of Object.entries(node)) {
if (['parent', 'loc', 'range'].includes(key)) {
continue
}
if (Array.isArray(child)) {
for (const item of child) {
if (item?.type) {
visit(item)
}
}
} else if (child?.type) {
visit(child)
}
}
}
function collectClassLiteralReports(node) {
const reports = []
const visit = (current) => {
for (const text of stringLiteralTexts(current)) {
const uncovered = uncoveredVerticalClass(text)
if (uncovered) {
reports.push({ node: current, detail: uncovered.className })
}
}
visitChildren(current, visit)
}
visit(node)
return reports
}
function expressionHasStyledScrollbarLiteral(node) {
let found = false
const visit = (current) => {
if (found || current.type === 'ConditionalExpression' || current.type === 'LogicalExpression') {
return
}
found = stringLiteralTexts(current).some((text) =>
classTokens(text).some((token) => STYLED_SCROLLBAR_CLASSES.has(token.className))
)
if (!found) {
visitChildren(current, visit)
}
}
visit(node)
return found
}
function propertyName(node) {
if (node?.type !== 'Property') {
return null
}
if (!node.computed && node.key.type === 'Identifier') {
return node.key.name
}
return node.key.type === 'Literal' && typeof node.key.value === 'string' ? node.key.value : null
}
function styleValueIsVerticalScroll(name, value) {
const parts = value.trim().toLowerCase().split(/\s+/).filter(Boolean)
if (parts.length === 0) {
return false
}
if (name === 'overflowY' || name === 'overflow-y') {
return VERTICAL_SCROLL_STYLE_VALUES.has(parts[0])
}
if (name !== 'overflow') {
return false
}
return VERTICAL_SCROLL_STYLE_VALUES.has(parts.length > 1 ? parts[1] : parts[0])
}
function collectStyleReports(node) {
const reports = []
const visit = (current) => {
if (current.type === 'Property') {
const name = propertyName(current)
for (const value of name ? stringLiteralTexts(current.value) : []) {
if (styleValueIsVerticalScroll(name, value)) {
reports.push({ node: current, detail: 'inline vertical scroll' })
}
}
visit(current.value)
return
}
visitChildren(current, visit)
}
visit(node)
return reports
}
function unwrapExpression(node) {
if (
['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'ChainExpression'].includes(
node?.type
)
) {
return unwrapExpression(node.expression)
}
return node
}
function spreadPropExpressions(expression, propName) {
const node = unwrapExpression(expression)
if (!node) {
return []
}
if (node.type === 'ConditionalExpression') {
return [
...spreadPropExpressions(node.consequent, propName),
...spreadPropExpressions(node.alternate, propName)
]
}
if (node.type === 'LogicalExpression' || node.type === 'BinaryExpression') {
return [
...spreadPropExpressions(node.left, propName),
...spreadPropExpressions(node.right, propName)
]
}
if (node.type !== 'ObjectExpression') {
return []
}
return node.properties.flatMap((property) => {
if (property.type === 'SpreadElement') {
return spreadPropExpressions(property.argument, propName)
}
return propertyName(property) === propName ? [property.value] : []
})
}
function jsxAttributeExpression(attribute) {
if (attribute.value?.type === 'Literal') {
return attribute.value
}
return attribute.value?.type === 'JSXExpressionContainer' ? attribute.value.expression : null
}
function jsxElementReports(node) {
let classExpression = null
const styleExpressions = []
for (const attribute of node.attributes) {
if (attribute.type === 'JSXSpreadAttribute') {
const spreadClassExpression = spreadPropExpressions(attribute.argument, 'className').at(-1)
if (spreadClassExpression) {
classExpression = spreadClassExpression
}
styleExpressions.push(...spreadPropExpressions(attribute.argument, 'style'))
} else if (attribute.name?.name === 'className') {
classExpression = jsxAttributeExpression(attribute)
} else if (attribute.name?.name === 'style') {
const expression = jsxAttributeExpression(attribute)
if (expression) {
styleExpressions.push(expression)
}
}
}
const reports = classExpression ? collectClassLiteralReports(classExpression) : []
if (!classExpression || !expressionHasStyledScrollbarLiteral(classExpression)) {
for (const expression of styleExpressions) {
reports.push(...collectStyleReports(expression))
}
}
return reports
}
function bindContext(createVisitors) {
return (context) => {
const visitors = createVisitors()
for (const [nodeType, visit] of Object.entries(visitors)) {
visitors[nodeType] = visit.bind(context)
}
return visitors
}
}
export default {
meta: { name: 'renderer-scrollbar-style' },
rules: {
'require-styled-vertical-scrollbar': {
create: bindContext(() => ({
JSXOpeningElement(node) {
for (const report of jsxElementReports(node)) {
this.report({
node: report.node,
message: `Vertical scroll container (${report.detail}) must use scrollbar-sleek, scrollbar-editor, or worktree-sidebar-scrollbar.`
})
}
}
}))
}
}
}
+10 -1
View File
@@ -12,9 +12,18 @@
},
"jsPlugins": [{ "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" }],
"rules": {
"react-doctor/effect-needs-cleanup": "warn",
"react-doctor/no-array-index-as-key": "warn",
"react-doctor/no-adjust-state-on-prop-change": "warn",
"react-doctor/no-create-store-in-render": "warn",
"react-doctor/no-derived-state-effect": "warn",
"react-doctor/no-initialize-state": "warn"
"react-doctor/no-initialize-state": "warn",
"react-doctor/no-side-effect-in-state-updater-function": "warn",
"react-doctor/no-unstable-nested-components": "warn",
"react-doctor/zustand-no-fresh-selector-result": "warn",
"react-doctor/zustand-no-get-during-initialization": "warn",
"react-doctor/zustand-no-mutating-state": "warn",
"react-doctor/zustand-no-whole-store-destructure": "warn"
},
"ignorePatterns": ["**/node_modules", "**/dist", "**/out"]
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/app-store-performance.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
pluginName: 'app-store-performance',
pluginPath,
source,
rules: {
'app-store-performance/require-selector': 'warn',
'app-store-performance/no-identity-selector': 'warn',
'app-store-performance/no-fresh-selector-result': 'warn'
}
})
}
describe('app store performance Oxlint plugin', () => {
it('reports whole-store and fresh-reference subscriptions', () => {
const diagnostics = lintSource(`
import { useAppStore as useStore } from '@/store'
const WholeStore = () => useStore()
const Identity = () => useStore((state) => state)
const Fresh = () => useStore((state) => ({ active: state.active }))
const Conditional = () => useStore((state) => state.active ? state.items : [])
const Nested = () => useStore((state) => {
if (state.active) return state.items.filter(Boolean)
return state.items
})
`)
expect(diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
'app-store-performance(require-selector)',
'app-store-performance(no-identity-selector)',
'app-store-performance(no-fresh-selector-result)',
'app-store-performance(no-fresh-selector-result)',
'app-store-performance(no-fresh-selector-result)'
])
})
it('allows focused, cached, and useShallow selectors', () => {
const diagnostics = lintSource(`
import { useAppStore } from '@/store'
import { useShallow as shallow } from 'zustand/react/shallow'
const selectActive = (state) => state.active
const Focused = () => useAppStore(selectActive)
const Cached = () => useAppStore((state) => state.cachedProjection)
const Shallow = () => useAppStore(shallow((state) => ({ active: state.active })))
`)
expect(diagnostics).toEqual([])
})
})
@@ -0,0 +1,190 @@
#!/usr/bin/env node
// Benchmark: the head-of-chain reads in getBranchCompare (src/main/git/status.ts).
//
// Four spawns ran strictly in series before any compare work started: branch
// --show-current, the base-ref probe, rev-parse HEAD, and rev-parse <base>. compareRef is
// display-only metadata and HEAD's oid does not depend on the base ref, so the first three
// can overlap. The probe oid also replaces the fourth spawn when it proves refs/heads/*;
// remote-tracking refs require a raw rev-parse because they may store annotated tags.
//
// This spawns the real git binary against this repo, so it measures actual process-launch
// cost rather than a model of it. Over SSH these are host-local spawns inside the relay,
// so the saving applies to remote spawn time, not to network round trips.
//
// Both arms are compared for identical resolved values before timing.
//
// Run with: node config/scripts/branch-compare-head-benchmark.mjs
import { execFile } from 'node:child_process'
import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
import { readBranchCompareHead } from '../../src/shared/git-branch-compare-head.ts'
const REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url))
const ITERATIONS = Number(process.env.ORCA_BRANCH_COMPARE_BENCH_ITERATIONS ?? '8')
const WARMUP = Number(process.env.ORCA_BRANCH_COMPARE_BENCH_WARMUP ?? '2')
const ROUNDS = 6
for (const [name, value] of [
['ORCA_BRANCH_COMPARE_BENCH_ITERATIONS', ITERATIONS],
['ORCA_BRANCH_COMPARE_BENCH_WARMUP', WARMUP]
]) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer, received ${value}`)
}
}
function git(args) {
return new Promise((resolve, reject) => {
execFile('git', args, { cwd: REPO_ROOT, maxBuffer: 64 * 1024 * 1024 }, (error, stdout) =>
error ? reject(error) : resolve(stdout.trim())
)
})
}
async function probeOid(qualifiedRef) {
try {
const out = await git(['rev-parse', '--verify', '--quiet', `${qualifiedRef}^{commit}`])
return out.length > 0 ? out : null
} catch {
return null
}
}
// Pre-fix: serial chain, and the probe's oid discarded then re-resolved.
async function readSerial(baseRef) {
const compareRef = (await git(['branch', '--show-current']).catch(() => '')) || 'HEAD'
let resolvedBaseRef = baseRef
if (!baseRef.startsWith('refs/')) {
const candidates = baseRef.includes('/')
? [`refs/remotes/${baseRef}`, `refs/heads/${baseRef}`]
: [`refs/heads/${baseRef}`]
for (const candidate of candidates) {
if ((await probeOid(candidate)) !== null) {
resolvedBaseRef = candidate
break
}
}
}
const headOid = await git(['rev-parse', '--verify', '--end-of-options', 'HEAD'])
const baseOid = await git(['rev-parse', '--verify', '--end-of-options', resolvedBaseRef])
return { compareRef, resolvedBaseRef, headOid, baseOid }
}
// Production head reader: overlaps independent reads and reuses only safe probe oids.
async function readConcurrent(baseRef) {
const reusableProbedOidByRef = new Map()
const resolveBaseRef = async () => {
if (baseRef.startsWith('refs/')) {
return baseRef
}
const candidates = baseRef.includes('/')
? [`refs/remotes/${baseRef}`, `refs/heads/${baseRef}`]
: [`refs/heads/${baseRef}`]
for (const candidate of candidates) {
const oid = await probeOid(candidate)
if (oid !== null) {
if (candidate.startsWith('refs/heads/')) {
reusableProbedOidByRef.set(candidate, oid)
}
return candidate
}
}
return baseRef
}
const result = await readBranchCompareHead({
readCompareRef: () =>
git(['branch', '--show-current'])
.then((out) => out || 'HEAD')
.catch(() => 'HEAD'),
resolveBaseRef,
readHeadOid: () => git(['rev-parse', '--verify', '--end-of-options', 'HEAD']),
readBaseOid: (resolvedBaseRef) => {
const reusableOid = reusableProbedOidByRef.get(resolvedBaseRef)
return reusableOid === undefined
? git(['rev-parse', '--verify', '--end-of-options', resolvedBaseRef])
: Promise.resolve(reusableOid)
}
})
if (!result.headOidResult.ok) {
throw result.headOidResult.error
}
if (!result.baseOidResult.ok) {
throw result.baseOidResult.error
}
return {
compareRef: result.compareRef,
resolvedBaseRef: result.resolvedBaseRef,
headOid: result.headOidResult.oid,
baseOid: result.baseOidResult.oid
}
}
function median(samples) {
const sorted = [...samples].sort((a, b) => a - b)
const mid = sorted.length / 2
return (sorted[mid - 1] + sorted[mid]) / 2
}
async function timeArm(read, baseRef) {
const start = performance.now()
for (let index = 0; index < ITERATIONS; index += 1) {
await read(baseRef)
}
return (performance.now() - start) / ITERATIONS
}
// Arms alternate which one leads so within-round drift cannot favour either.
async function measure(baseRef) {
for (let index = 0; index < WARMUP; index += 1) {
await readSerial(baseRef)
await readConcurrent(baseRef)
}
const serialSamples = []
const concurrentSamples = []
for (let round = 0; round < ROUNDS; round += 1) {
if (round % 2 === 0) {
serialSamples.push(await timeArm(readSerial, baseRef))
concurrentSamples.push(await timeArm(readConcurrent, baseRef))
} else {
concurrentSamples.push(await timeArm(readConcurrent, baseRef))
serialSamples.push(await timeArm(readSerial, baseRef))
}
}
return { serialMs: median(serialSamples), concurrentMs: median(concurrentSamples) }
}
const pad = (value, width) => String(value).padStart(width)
console.log('getBranchCompare head-of-chain reads, per call. Lower is better.')
console.log(`iterations=${ITERATIONS} warmup=${WARMUP} rounds=${ROUNDS} (per-arm medians)`)
console.log(
`${pad('base ref', 30)} ${pad('serial', 11)} ${pad('concurrent', 11)} ${pad('speedup', 9)}`
)
// A short remote label is the common case (Orca's base picker emits `origin/main`); the
// already-qualified ref skips the probe entirely, so only the concurrency half applies.
const upstream = await git(['rev-parse', '--abbrev-ref', 'HEAD@{upstream}']).catch(() => null)
const baseRefs = ['origin/main', 'refs/remotes/origin/main', 'main']
if (upstream && !baseRefs.includes(upstream)) {
baseRefs.push(upstream)
}
for (const baseRef of baseRefs) {
const serial = await readSerial(baseRef)
const concurrent = await readConcurrent(baseRef)
if (JSON.stringify(serial) !== JSON.stringify(concurrent)) {
throw new Error(
`resolved values differ for ${baseRef}:\n serial ${JSON.stringify(serial)}\n concurrent ${JSON.stringify(concurrent)}`
)
}
if (!serial.headOid) {
throw new Error(`fixture resolved no HEAD oid for ${baseRef}`)
}
const { serialMs, concurrentMs } = await measure(baseRef)
console.log(
`${pad(baseRef, 30)} ${pad(`${serialMs.toFixed(1)} ms`, 11)} ${pad(`${concurrentMs.toFixed(1)} ms`, 11)} ${pad(`${(serialMs / concurrentMs).toFixed(2)}x`, 9)}`
)
}
console.log(
'\nThe already-qualified refs/... row skips the probe by design, so it only shows the\nconcurrency half. This times the native/WSL head-of-chain reads, not the whole compare;\nthe relay path has separate production-concurrency coverage.'
)
+46
View File
@@ -0,0 +1,46 @@
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
export function createLocalBuildVersion(baseVersion, timestamp, commit) {
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(baseVersion)) {
throw new Error(`Package version is not valid semver: ${baseVersion}`)
}
if (!Number.isSafeInteger(timestamp) || timestamp <= 0) {
throw new Error('Local build timestamp is invalid.')
}
const sanitizedCommit = commit.replace(/[^0-9A-Za-z-]/g, '').slice(0, 12)
if (!sanitizedCommit) {
throw new Error('Git commit identity is empty.')
}
const suffix = `local.${timestamp}.${sanitizedCommit}`
return baseVersion.includes('-') ? `${baseVersion}.${suffix}` : `${baseVersion}-${suffix}`
}
export function getLocalBuildIdentity() {
const packageJson = JSON.parse(readFileSync(resolve('package.json'), 'utf8'))
const commit = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], {
encoding: 'utf8'
}).trim()
return {
commit,
version: createLocalBuildVersion(packageJson.version, Date.now(), commit)
}
}
if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) {
const identity = getLocalBuildIdentity()
console.log(`[build:mac] local update version ${identity.version}`)
execFileSync(
process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
['exec', 'electron-builder', '--config', 'config/electron-builder.config.cjs', '--mac'],
{
env: {
...process.env,
ORCA_BUILD_COMMIT: identity.commit,
ORCA_LOCAL_BUILD_VERSION: identity.version
},
stdio: 'inherit'
}
)
}
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { createLocalBuildVersion } from './build-mac-local.mjs'
describe('createLocalBuildVersion', () => {
it('creates unique valid prerelease versions without changing the release base', () => {
expect(createLocalBuildVersion('1.4.159-rc.0', 123456, 'abc123')).toBe(
'1.4.159-rc.0.local.123456.abc123'
)
expect(createLocalBuildVersion('1.4.159', 123456, 'abc123')).toBe('1.4.159-local.123456.abc123')
})
it('sanitizes commit identifiers', () => {
expect(createLocalBuildVersion('1.0.0', 1, 'abc/def')).toBe('1.0.0-local.1.abcdef')
})
})
@@ -0,0 +1,222 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.mjs'
const SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?)$/
export const OXLINT_SCANS = [
{
// Why: no --config, so Oxlint keeps discovering nested configs. Pinning the root
// config would apply root rules to mobile/, whose .oxlintrc.json turns them off.
label: 'code quality',
args: ['--report-unused-disable-directives-severity', 'warn']
},
{
label: 'type-aware code quality',
args: ['--type-aware', '--config', 'config/oxlint-code-quality-type-aware.json']
},
{
label: 'React Doctor',
args: ['--config', 'config/oxlint-react-doctor.json']
}
]
export function parseAddedLineRanges(diff) {
const ranges = []
const hunkPattern = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/
for (const line of diff.split(/\r?\n/)) {
const match = hunkPattern.exec(line)
if (!match) {
continue
}
const start = Number.parseInt(match[1], 10)
const count = match[2] === undefined ? 1 : Number.parseInt(match[2], 10)
if (count > 0) {
ranges.push({ start, end: start + count - 1 })
}
}
return ranges
}
export function overlapsAddedLines(startLine, endLine, ranges) {
return ranges.some((range) => startLine <= range.end && endLine >= range.start)
}
function runGit(root, args, options = {}) {
return execFileSync('git', args, {
cwd: root,
encoding: options.encoding ?? 'utf8',
maxBuffer: 64 * 1024 * 1024
})
}
function splitNullDelimited(output) {
return output.split('\0').filter(Boolean)
}
function resolveBase(root, requestedBase) {
for (const candidate of [
requestedBase,
process.env.ORCA_CODE_QUALITY_BASE,
'origin/main',
'main'
]) {
if (!candidate) {
continue
}
const result = spawnSync('git', ['rev-parse', '--verify', `${candidate}^{commit}`], {
cwd: root,
stdio: 'ignore'
})
if (result.status === 0) {
return candidate
}
}
throw new Error('Pass the pull request base SHA or make origin/main available locally.')
}
export function collectAddedLineRanges(root, requestedBase) {
const base = resolveBase(root, requestedBase)
const mergeBase = runGit(root, ['merge-base', base, 'HEAD']).trim()
const comparisonBase = resolvePullRequestDiffBase(root, mergeBase)
const changedFiles = splitNullDelimited(
runGit(root, ['diff', '--name-only', '-z', '--diff-filter=ACMRTUB', comparisonBase, '--'])
)
const untrackedFiles = splitNullDelimited(
runGit(root, ['ls-files', '--others', '--exclude-standard', '-z'])
)
const rangesByFile = new Map()
for (const file of changedFiles) {
if (!SOURCE_FILE_PATTERN.test(file) || !existsSync(path.join(root, file))) {
continue
}
const diff = runGit(root, ['diff', '--unified=0', '--no-color', comparisonBase, '--', file])
const ranges = parseAddedLineRanges(diff)
if (ranges.length > 0) {
rangesByFile.set(file, ranges)
}
}
for (const file of untrackedFiles) {
const absolutePath = path.join(root, file)
if (!SOURCE_FILE_PATTERN.test(file) || !existsSync(absolutePath)) {
continue
}
const lineCount = readFileSync(absolutePath, 'utf8').split(/\r?\n/).length
rangesByFile.set(file, [{ start: 1, end: lineCount }])
}
return { base, comparisonBase, rangesByFile }
}
function parseOxlintOutput(stdout, label) {
const start = stdout.indexOf('{')
const end = stdout.lastIndexOf('}')
if (start === -1 || end === -1) {
throw new Error(`${label} did not return Oxlint JSON output.`)
}
return JSON.parse(stdout.slice(start, end + 1))
}
function normalizedDiagnosticPath(root, filename) {
const absolutePath = path.isAbsolute(filename) ? filename : path.join(root, filename)
return path.relative(root, absolutePath).split(path.sep).join('/')
}
function diagnosticLineRange(root, filename, span) {
const startLine = span.line
if (!Number.isInteger(startLine)) {
return null
}
if (!Number.isInteger(span.offset) || !Number.isInteger(span.length) || span.length === 0) {
return { start: startLine, end: startLine }
}
const absolutePath = path.isAbsolute(filename) ? filename : path.join(root, filename)
const source = readFileSync(absolutePath)
const highlighted = source.subarray(span.offset, span.offset + span.length).toString('utf8')
return { start: startLine, end: startLine + (highlighted.match(/\n/g)?.length ?? 0) }
}
export function diagnosticTouchesAddedLines(diagnostic, rangesByFile, root = process.cwd()) {
const file = normalizedDiagnosticPath(root, diagnostic.filename)
const ranges = rangesByFile.get(file)
if (!ranges) {
return false
}
return (diagnostic.labels ?? []).some((label) => {
const lineRange = diagnosticLineRange(root, diagnostic.filename, label.span)
return lineRange !== null && overlapsAddedLines(lineRange.start, lineRange.end, ranges)
})
}
function annotationValue(value) {
return String(value).replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A')
}
function printDiagnostic(diagnostic, root) {
const file = normalizedDiagnosticPath(root, diagnostic.filename)
const line = diagnostic.labels?.[0]?.span?.line ?? 1
const code = diagnostic.code ?? 'oxlint'
console.error(
`::error file=${annotationValue(file)},line=${line},title=${annotationValue(code)}::${annotationValue(diagnostic.message)}`
)
console.error(`${file}:${line} ${code}: ${diagnostic.message}`)
}
function runOxlintScan(root, scan, files) {
const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const result = spawnSync(pnpm, ['exec', 'oxlint', ...scan.args, '--format', 'json', ...files], {
cwd: root,
encoding: 'utf8',
maxBuffer: 128 * 1024 * 1024
})
if (result.error) {
throw result.error
}
if (!result.stdout.trim()) {
process.stderr.write(result.stderr)
throw new Error(`${scan.label} failed before producing diagnostics.`)
}
return parseOxlintOutput(result.stdout, scan.label).diagnostics ?? []
}
export function main(
root = process.cwd(),
requestedBase = process.argv.slice(2).find((argument) => argument !== '--')
) {
const { base, comparisonBase, rangesByFile } = collectAddedLineRanges(root, requestedBase)
const files = [...rangesByFile.keys()]
if (files.length === 0) {
console.log(`Changed-code quality gate: no changed JavaScript or TypeScript since ${base}.`)
return 0
}
let failures = 0
for (const scan of OXLINT_SCANS) {
const diagnostics = runOxlintScan(root, scan, files).filter((diagnostic) =>
diagnosticTouchesAddedLines(diagnostic, rangesByFile, root)
)
for (const diagnostic of diagnostics) {
printDiagnostic(diagnostic, root)
}
failures += diagnostics.length
console.log(
`${scan.label}: ${diagnostics.length} new finding(s) across ${files.length} changed file(s).`
)
}
if (failures > 0) {
console.error(
`Changed-code quality gate failed with ${failures} finding(s) since ${comparisonBase.slice(0, 12)}.`
)
return 1
}
console.log(`Changed-code quality gate passed since ${comparisonBase.slice(0, 12)}.`)
return 0
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exit(main())
}
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest'
import {
OXLINT_SCANS,
diagnosticTouchesAddedLines,
overlapsAddedLines,
parseAddedLineRanges
} from './check-changed-code-quality.mjs'
describe('changed-code quality line matching', () => {
it('parses added and replaced hunk ranges while ignoring deletions', () => {
const ranges = parseAddedLineRanges(
['@@ -10,2 +10,3 @@', '@@ -20 +21 @@', '@@ -40,4 +42,0 @@', '@@ -50 +48,2 @@'].join('\n')
)
expect(ranges).toEqual([
{ start: 10, end: 12 },
{ start: 21, end: 21 },
{ start: 48, end: 49 }
])
})
it('matches diagnostics that overlap any added line', () => {
const ranges = [
{ start: 5, end: 7 },
{ start: 12, end: 12 }
]
expect(overlapsAddedLines(3, 5, ranges)).toBe(true)
expect(overlapsAddedLines(8, 11, ranges)).toBe(false)
expect(overlapsAddedLines(12, 14, ranges)).toBe(true)
})
it('normalizes absolute diagnostic paths before matching', () => {
const root = process.cwd()
const file = 'config/scripts/check-changed-code-quality.test.mjs'
const diagnostic = {
filename: `${root}/${file}`,
labels: [{ span: { line: 24 } }]
}
expect(
diagnosticTouchesAddedLines(diagnostic, new Map([[file, [{ start: 24, end: 24 }]]]), root)
).toBe(true)
})
// Why: pinning --config disables nested-config discovery, so root rules that
// mobile/.oxlintrc.json turns off would fail the gate on mobile files.
it('lets the untyped scan discover nested configs instead of pinning the root config', () => {
const scan = OXLINT_SCANS.find((candidate) => candidate.label === 'code quality')
expect(scan.args).not.toContain('--config')
expect(scan.args).not.toContain('--disable-nested-config')
})
})
@@ -1,343 +0,0 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
import ts from 'typescript-api'
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cts'])
const SKIP_PATH_PARTS = new Set(['node_modules', 'dist', 'out', '.git', '__snapshots__'])
const SCAN_ROOTS = ['src', 'config/scripts', 'tools', 'tests', 'mobile']
const LOOP_KINDS = new Set([
ts.SyntaxKind.ForStatement,
ts.SyntaxKind.ForInStatement,
ts.SyntaxKind.ForOfStatement,
ts.SyntaxKind.WhileStatement,
ts.SyntaxKind.DoStatement
])
const ASSIGNMENT_OPERATORS = new Set([
ts.SyntaxKind.EqualsToken,
ts.SyntaxKind.PlusEqualsToken,
ts.SyntaxKind.QuestionQuestionEqualsToken,
ts.SyntaxKind.BarBarEqualsToken,
ts.SyntaxKind.AmpersandAmpersandEqualsToken
])
function normalizeReferenceText(text) {
return text.replaceAll(/\s+/g, '')
}
// The variable a member/call chain ultimately reads, so `carry.subarray(0, n)`
// still resolves to `carry`. `this.x` stops at the property: the class field is
// the accumulator.
function rootReferenceText(node) {
if (ts.isIdentifier(node)) {
return node.text
}
if (ts.isPropertyAccessExpression(node)) {
return node.expression.kind === ts.SyntaxKind.ThisKeyword
? normalizeReferenceText(node.getText())
: rootReferenceText(node.expression)
}
if (
ts.isElementAccessExpression(node) ||
ts.isCallExpression(node) ||
ts.isNonNullExpression(node) ||
ts.isParenthesizedExpression(node) ||
ts.isAsExpression(node)
) {
return rootReferenceText(node.expression)
}
return undefined
}
function isBufferConcatCall(node) {
return (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'concat' &&
ts.isIdentifier(node.expression.expression) &&
node.expression.expression.text === 'Buffer' &&
node.arguments.length > 0 &&
ts.isArrayLiteralExpression(node.arguments[0])
)
}
function enclosingLoop(node) {
for (let current = node.parent; current; current = current.parent) {
if (LOOP_KINDS.has(current.kind)) {
return current
}
}
return undefined
}
// A `for (let acc = ...; ;)` binding is loop-carried even though it sits inside
// the loop node, so the initializer does not count as loop-local.
function isDeclaredInsideLoop(declarationStart, loop) {
if (declarationStart < loop.getStart() || declarationStart >= loop.end) {
return false
}
const initializer = ts.isForStatement(loop) ? loop.initializer : undefined
if (!initializer) {
return true
}
return declarationStart < initializer.getStart() || declarationStart >= initializer.end
}
function collectBindingNames(name, into) {
if (ts.isIdentifier(name)) {
into.push(name)
return
}
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
for (const element of name.elements) {
if (ts.isBindingElement(element)) {
collectBindingNames(element.name, into)
}
}
}
}
function collectDeclarations(sourceFile) {
const declarations = new Map()
const record = (identifier, node) => {
const existing = declarations.get(identifier.text)
const start = node.getStart(sourceFile)
if (existing) {
existing.push(start)
} else {
declarations.set(identifier.text, [start])
}
}
const visit = (node) => {
if (ts.isVariableDeclaration(node) || ts.isParameter(node)) {
const names = []
collectBindingNames(node.name, names)
for (const identifier of names) {
record(identifier, node)
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return declarations
}
function collectAssignedRoots(loop) {
const assigned = new Set()
const visit = (node) => {
if (ts.isBinaryExpression(node) && ASSIGNMENT_OPERATORS.has(node.operatorToken.kind)) {
const root = rootReferenceText(node.left)
if (root) {
assigned.add(root)
}
}
ts.forEachChild(node, visit)
}
visit(loop.statement)
return assigned
}
// Unwrap the wrappers a concat result passes through before it lands on the
// left-hand side, so `acc = cond ? Buffer.concat([acc, c]) : c` still counts.
function assignmentTargetOf(call) {
let node = call
let parent = node.parent
while (
parent &&
(ts.isParenthesizedExpression(parent) ||
ts.isAsExpression(parent) ||
ts.isNonNullExpression(parent) ||
(ts.isConditionalExpression(parent) && parent.condition !== node))
) {
node = parent
parent = parent.parent
}
if (
parent &&
ts.isBinaryExpression(parent) &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
parent.right === node
) {
return {
text: normalizeReferenceText(parent.left.getText()),
root: rootReferenceText(parent.left)
}
}
return undefined
}
function concatOperands(call) {
return call.arguments[0].elements.map((element) => {
const spread = ts.isSpreadElement(element)
const expression = spread ? element.expression : element
return {
spread,
text: normalizeReferenceText(expression.getText()),
root: rootReferenceText(expression)
}
})
}
// A binding reset on every iteration cannot accumulate; only one that outlives
// the iteration carries the cost forward.
function isLoopCarried(root, loop, declarations) {
const starts = declarations.get(root)
if (!starts) {
return true
}
return !starts.some((start) => isDeclaredInsideLoop(start, loop))
}
function quadraticAccumulator(call, loop, declarations, assignedRoots) {
const operands = concatOperands(call)
const target = assignmentTargetOf(call)
// The result feeds straight back into its own input: every iteration re-copies
// everything accumulated so far.
const selfOperand = target
? operands.find((operand) => operand.text === target.text || operand.root === target.root)
: undefined
if (selfOperand && target.root && isLoopCarried(target.root, loop, declarations)) {
return target.text
}
// No direct self-assignment, so look for a loop-carried operand reassigned
// inside the loop: the concat result reaches it by some other path. Spread
// operands are the sanctioned chunk-list fix, not this bug, so skip them.
for (const operand of operands) {
if (operand.spread || !operand.root || !assignedRoots.has(operand.root)) {
continue
}
if (isLoopCarried(operand.root, loop, declarations)) {
return operand.root
}
}
return undefined
}
export function reportQuadraticBufferConcat(filePath, sourceText) {
if (!sourceText.includes('Buffer.concat')) {
return []
}
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true)
const declarations = collectDeclarations(sourceFile)
const assignedRootsByLoop = new Map()
const reports = []
const seen = new Set()
const visit = (node) => {
if (isBufferConcatCall(node)) {
const loop = enclosingLoop(node)
if (loop) {
let assignedRoots = assignedRootsByLoop.get(loop)
if (!assignedRoots) {
assignedRoots = collectAssignedRoots(loop)
assignedRootsByLoop.set(loop, assignedRoots)
}
const accumulator = quadraticAccumulator(node, loop, declarations, assignedRoots)
if (accumulator) {
const start = node.getStart(sourceFile)
if (!seen.has(start)) {
seen.add(start)
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start)
reports.push({
filePath,
line: line + 1,
column: character + 1,
accumulator,
text: node.getText()
})
}
}
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return reports
}
export function normalizePath(root, filePath) {
return path.relative(root, filePath).split(path.sep).join('/')
}
function isSkippedFile(root, filePath) {
const relative = normalizePath(root, filePath)
// Benchmarks keep the pre-fix shape on purpose so they can measure against it.
if (
relative.includes('.test.') ||
relative.includes('.spec.') ||
relative.includes('-benchmark.')
) {
return true
}
return relative.split('/').some((part) => SKIP_PATH_PARTS.has(part))
}
async function collectSourceFiles(root, dir) {
let entries
try {
entries = await fs.readdir(dir, { withFileTypes: true })
} catch {
return []
}
const files = []
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
if (!SKIP_PATH_PARTS.has(entry.name)) {
files.push(...(await collectSourceFiles(root, fullPath)))
}
} else if (
entry.isFile() &&
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
!isSkippedFile(root, fullPath)
) {
files.push(fullPath)
}
}
return files
}
function formatReports(root, reports) {
return reports
.map(
(report) =>
`${normalizePath(root, report.filePath)}:${report.line}:${report.column} ${report.accumulator} — ${report.text.replaceAll(/\s+/g, ' ')}`
)
.join('\n')
}
export async function main(root = process.cwd()) {
const reports = []
for (const scanRoot of SCAN_ROOTS) {
const files = await collectSourceFiles(root, path.join(root, scanRoot))
for (const filePath of files) {
const sourceText = await fs.readFile(filePath, 'utf8')
reports.push(...reportQuadraticBufferConcat(filePath, sourceText))
}
}
if (reports.length === 0) {
return 0
}
console.error('Buffer.concat must not rebuild a loop-carried accumulator.')
console.error('Each iteration re-copies everything accumulated so far, so the loop is O(n^2).')
console.error('Collect the pieces in a Buffer[] and Buffer.concat(chunks) once, after the loop.')
console.error('')
console.error(formatReports(root, reports))
return 1
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exit(await main())
}
@@ -1,146 +0,0 @@
import { describe, expect, it } from 'vitest'
import { reportQuadraticBufferConcat } from './check-quadratic-buffer-concat.mjs'
describe('check-quadratic-buffer-concat', () => {
it('reports an accumulator rebuilt from itself in a for-of loop', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([acc, chunk]) }'
)
expect(reports).toHaveLength(1)
expect(reports[0].accumulator).toBe('acc')
})
it('reports the accumulator when it trails the new chunk', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([chunk, acc]) }'
)
expect(reports).toHaveLength(1)
})
it('reports a spread of the accumulator into its own concat', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([...acc, chunk]) }'
)
expect(reports).toHaveLength(1)
})
it('reports the real pre-fix ai-vault stream carry', () => {
const reports = reportQuadraticBufferConcat(
'session-scanner-parse-cache.ts',
`async function read(stream) {
let remainder = null
for await (const chunk of stream) {
const data = remainder ? Buffer.concat([remainder, chunk]) : chunk
remainder = Buffer.from(data.subarray(lineStart))
}
}`
)
expect(reports).toHaveLength(1)
expect(reports[0].accumulator).toBe('remainder')
})
it('reports the real pre-fix transcript reader carry, which never names itself on the left', () => {
const reports = reportQuadraticBufferConcat(
'agent-hook-listener.ts',
`function read(fd, size) {
let carryBytes = Buffer.alloc(0)
while (bytesRead < size) {
const combined = Buffer.concat([buffer.subarray(0, n), carryBytes])
carryBytes = combined.subarray(0, firstNewline)
}
}`
)
expect(reports).toHaveLength(1)
expect(reports[0].accumulator).toBe('carryBytes')
})
it('accepts the chunk-list fix that joins once after the loop', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'const parts = []; for (const chunk of chunks) { parts.push(chunk) } const out = Buffer.concat(parts)'
)
expect(reports).toHaveLength(0)
})
it('accepts spreading a carry chunk list, which is the sanctioned fix', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
`let carryChunks = []
while (scanEnd > 0) {
const region = carryChunks.length === 0 ? buffer : Buffer.concat([buffer, ...carryChunks])
carryChunks = [buffer.subarray(0, firstNewline)]
}`
)
expect(reports).toHaveLength(0)
})
it('accepts a concat whose result never outlives the iteration', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'for (const group of groups) { const frame = Buffer.concat([group.header, group.body]); send(frame) }'
)
expect(reports).toHaveLength(0)
})
it('accepts a loop-local buffer declared and rebuilt inside the same iteration', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'for (const chunk of chunks) { let framed = HEADER; framed = Buffer.concat([framed, chunk]); send(framed) }'
)
expect(reports).toHaveLength(0)
})
it('reports an accumulator declared in a classic for initializer', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'for (let acc = Buffer.alloc(0), i = 0; i < n; i++) { acc = Buffer.concat([acc, chunks[i]]) }'
)
expect(reports).toHaveLength(1)
})
it('reports a class field accumulator rebuilt inside a loop', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'class Reader { read() { while (this.open) { this.pending = Buffer.concat([this.pending, chunk]) } } }'
)
expect(reports).toHaveLength(1)
expect(reports[0].accumulator).toBe('this.pending')
})
it('reports an accumulator guarded by an emptiness ternary', () => {
const reports = reportQuadraticBufferConcat(
'Example.ts',
'let acc = Buffer.alloc(0); while (open) { acc = acc.length === 0 ? chunk : Buffer.concat([acc, chunk]) }'
)
expect(reports).toHaveLength(1)
})
it('ignores a per-event accumulator with no enclosing loop', () => {
const reports = reportQuadraticBufferConcat(
'scrcpy-stream-session.ts',
'class S { handleVideoChunk(chunk) { let buffer = Buffer.concat([this.pendingVideo, chunk]); this.pendingVideo = parse(buffer).pending } }'
)
expect(reports).toHaveLength(0)
})
it('ignores files with no Buffer.concat at all', () => {
expect(reportQuadraticBufferConcat('Example.ts', 'export const x = 1')).toHaveLength(0)
})
})
@@ -0,0 +1,35 @@
import { spawnSync } from 'node:child_process'
import process from 'node:process'
import { resolvePullRequestDiffBase } from './git-pull-request-diff-base.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'
const result = spawnSync(
pnpm,
[
'dlx',
'react-doctor@0.9.1',
'.',
'--yes',
'--scope',
'lines',
'--base',
base,
'--include-untracked',
'--no-dead-code',
'--no-supply-chain',
'--no-telemetry',
'--blocking',
'error'
],
{ stdio: 'inherit' }
)
if (result.error) {
throw result.error
}
process.exit(result.status ?? 1)
@@ -1,87 +0,0 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
import { reportUnstyledScrollbars } from './styled-scrollbars/styled-scrollbar-jsx-check.mjs'
export {
plainClassName,
reportUnstyledScrollbars
} from './styled-scrollbars/styled-scrollbar-jsx-check.mjs'
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
const SKIP_PATH_PARTS = new Set(['node_modules', 'dist', 'out', '.git', '__snapshots__'])
export function normalizePath(root, filePath) {
return path.relative(root, filePath).split(path.sep).join('/')
}
function isSkippedFile(root, filePath) {
const relative = normalizePath(root, filePath)
if (relative.includes('.test.') || relative.includes('.spec.')) {
return true
}
return relative.split('/').some((part) => SKIP_PATH_PARTS.has(part))
}
async function collectSourceFiles(root, dir) {
const entries = await fs.readdir(dir, { withFileTypes: true })
const files = []
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
if (!SKIP_PATH_PARTS.has(entry.name)) {
files.push(...(await collectSourceFiles(root, fullPath)))
}
} else if (
entry.isFile() &&
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
!isSkippedFile(root, fullPath)
) {
files.push(fullPath)
}
}
return files
}
async function collectUnstyledScrollbarReports(root) {
const sourceRoot = path.join(root, 'src', 'renderer', 'src')
const files = await collectSourceFiles(root, sourceRoot)
const reports = []
for (const filePath of files) {
const sourceText = await fs.readFile(filePath, 'utf8')
reports.push(...reportUnstyledScrollbars(filePath, sourceText))
}
return reports
}
function formatReports(root, reports) {
return reports
.map(
(report) =>
`${normalizePath(root, report.filePath)}:${report.line}:${report.column} ${report.text.replace(/\s+/g, ' ')}`
)
.join('\n')
}
export async function main(root = process.cwd()) {
const reports = await collectUnstyledScrollbarReports(root)
if (reports.length === 0) {
return 0
}
console.error('Renderer vertical scroll containers must use an Orca scrollbar style.')
console.error('Put the scrollbar class in the same class literal as the vertical overflow class.')
console.error('Use scrollbar-sleek, scrollbar-editor, or worktree-sidebar-scrollbar.')
console.error('')
console.error(formatReports(root, reports))
return 1
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exit(await main())
}
@@ -1,199 +0,0 @@
import { describe, expect, it } from 'vitest'
import { plainClassName, reportUnstyledScrollbars } from './check-styled-scrollbars.mjs'
describe('check-styled-scrollbars', () => {
it('reports renderer vertical scroll containers without an Orca scrollbar style', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="max-h-64 overflow-y-auto" /> }'
)
expect(reports).toHaveLength(1)
})
it('accepts obvious styled vertical scroll containers', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="max-h-64 overflow-auto scrollbar-sleek" /> }'
)
expect(reports).toHaveLength(0)
})
it('does not accept nonexistent scrollbar classes as Orca scrollbar styles', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="max-h-64 overflow-auto scrollbar-none" /> }'
)
expect(reports).toHaveLength(1)
})
it('fails closed when a separate class composer argument supplies the scrollbar style', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div className={cn('max-h-64 overflow-y-auto', 'scrollbar-sleek')} /> }"
)
expect(reports).toHaveLength(1)
})
it('accepts static class composer arguments when the same literal is styled', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div className={cn('max-h-64 overflow-y-auto scrollbar-sleek')} /> }"
)
expect(reports).toHaveLength(0)
})
it('fails closed when a scrollbar class is only conditionally present', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example({ enabled }) { return <div className={cn('overflow-y-auto', enabled && 'scrollbar-sleek')} /> }"
)
expect(reports).toHaveLength(1)
})
it('accepts conditional branches when overflow and scrollbar live in the same class literal', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example({ enabled }) { return <div className={cn(enabled && 'overflow-y-auto scrollbar-sleek')} /> }"
)
expect(reports).toHaveLength(0)
})
it('reports vertical scroll inside arbitrary wrappers when the literal is unstyled', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div className={identity('overflow-y-auto')} /> }"
)
expect(reports).toHaveLength(1)
})
it('does not require a vertical scrollbar style for horizontal-only overflow', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <pre className="max-w-full overflow-x-auto" /> }'
)
expect(reports).toHaveLength(0)
})
it('does not let responsive scrollbar variants satisfy unconditional overflow', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="overflow-y-auto md:scrollbar-sleek" /> }'
)
expect(reports).toHaveLength(1)
})
it('accepts matching responsive overflow and scrollbar variants', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="md:overflow-y-auto md:scrollbar-sleek" /> }'
)
expect(reports).toHaveLength(0)
})
it('accepts unconditional scrollbar styles for responsive overflow', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="md:overflow-y-auto scrollbar-sleek" /> }'
)
expect(reports).toHaveLength(0)
})
it('reports inline vertical overflow without an Orca scrollbar class', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div style={{ overflowY: 'auto' }} /> }"
)
expect(reports).toHaveLength(1)
})
it('accepts inline vertical overflow with a stable Orca scrollbar class', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="scrollbar-editor" style={{ overflow: \'auto\' }} /> }'
)
expect(reports).toHaveLength(0)
})
it('reports inline vertical overflow when the scrollbar class is conditional or short-circuited', () => {
for (const classNameExpression of [
"enabled && 'scrollbar-sleek'",
"enabled ? 'scrollbar-sleek' : undefined",
"enabled || 'scrollbar-sleek'",
"enabled ?? 'scrollbar-sleek'"
]) {
const reports = reportUnstyledScrollbars(
'Example.tsx',
`export function Example({ enabled }) { return <div className={${classNameExpression}} style={{ overflowY: 'auto' }} /> }`
)
expect(reports, classNameExpression).toHaveLength(1)
}
})
it('reports logical inline style spreads without an Orca scrollbar class', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example({ open }) { return <div style={{ ...(open && { overflowY: 'auto' }) }} /> }"
)
expect(reports).toHaveLength(1)
})
it('reports JSX spread className props with unstyled vertical overflow', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div {...{ className: 'overflow-y-auto' }} /> }"
)
expect(reports).toHaveLength(1)
})
it('accepts JSX spread className props when the same literal is styled', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div {...{ className: 'overflow-y-auto scrollbar-sleek' }} /> }"
)
expect(reports).toHaveLength(0)
})
it('uses later spread className props over earlier explicit className props', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
'export function Example() { return <div className="scrollbar-sleek" {...{ className: \'overflow-y-auto\' }} /> }'
)
expect(reports).toHaveLength(1)
})
it('supports variant helper className config', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div className={buttonVariants({ className: 'overflow-y-auto scrollbar-sleek' })} /> }"
)
expect(reports).toHaveLength(0)
})
it('normalizes Tailwind variants and important prefixes before matching', () => {
expect(plainClassName('md:overflow-y-auto')).toBe('overflow-y-auto')
expect(plainClassName('[&:hover]:overflow-y-auto')).toBe('overflow-y-auto')
expect(plainClassName('md:!scrollbar-editor')).toBe('scrollbar-editor')
expect(plainClassName('!scrollbar-editor')).toBe('scrollbar-editor')
})
})
@@ -12,11 +12,10 @@
// other work would be a regression, not a win.
//
// Run with: node config/scripts/claude-usage-yield-benchmark.mjs
import { readFileSync } from 'node:fs'
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { performance } from 'node:perf_hooks'
import { readdirSync, statSync } from 'node:fs'
const REPO_ROOT = new URL('../..', import.meta.url)
const ROUNDS = Number(process.env.ORCA_YIELD_BENCH_ROUNDS ?? '10')
@@ -0,0 +1,283 @@
#!/usr/bin/env node
// Benchmark: CLI process startup with the RuntimeClient module graph deferred.
//
// src/cli/index.ts used to value-import RuntimeClient at module scope, and five
// modules that load on every invocation (args, flags, dispatch, format,
// selectors) pulled RuntimeClientError from the ./runtime-client barrel. Either
// edge alone drags in the whole client graph: zod (via shared/pairing ->
// shared/mobile-relay-pairing-offer), ws + tweetnacl (via websocket-transport),
// plus the environment store and secure-file stack.
//
// The fix repoints those five at ./runtime/types (zero children) and loads the
// client through `await import()` after flag validation, so --help, `help
// <cmd>` and every command/flag error return without ever touching it.
//
// Both arms are REAL tsc emits of real source: the baseline arm restores the
// seven touched files from a git rev and compiles that. Each sample is a FRESH
// process (module-graph cost is a once-per-process cost; timing it in-process
// would measure a warm require cache).
//
// Arms alternate lead across an even number of rounds and report per-arm
// medians. Byte-for-byte output equality is checked BEFORE timing.
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
const REPO = fileURLToPath(new URL('../..', import.meta.url))
const ROUNDS = Number(process.env.ORCA_CLI_DEFER_BENCH_ROUNDS ?? '30')
const WARMUP = Number(process.env.ORCA_CLI_DEFER_BENCH_WARMUP ?? '3')
for (const [name, value] of [
['ORCA_CLI_DEFER_BENCH_ROUNDS', ROUNDS],
['ORCA_CLI_DEFER_BENCH_WARMUP', WARMUP]
]) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer, received ${value}`)
}
}
if (ROUNDS % 2 !== 0) {
// Why: arms alternate which one leads; an odd count biases one arm.
throw new Error(`ORCA_CLI_DEFER_BENCH_ROUNDS must be even, received ${ROUNDS}`)
}
const TOUCHED = [
'src/cli/args.ts',
'src/cli/dispatch.ts',
'src/cli/flags.ts',
'src/cli/format.ts',
'src/cli/index.ts',
'src/cli/runtime/client.ts',
'src/cli/selectors.ts'
]
// Why: if the deferral is reverted or reshaped, both arms would compile to the
// same thing and this would quietly report 1.00x forever. Re-read the real call
// forms out of the source. Matching the CALL form (not a bare identifier) so a
// comment that merely names the function cannot satisfy the check.
function assertMarkersFresh() {
const checks = [
['src/cli/index.ts', "await import('./runtime-client.js')"],
['src/cli/index.ts', 'await loadRuntimeClientClass()'],
['src/cli/index.ts', "import type { RuntimeClient } from './runtime-client'"],
['src/cli/runtime/client.ts', "await import('./websocket-transport.js')"],
['src/cli/runtime/client.ts', 'await loadSendWebSocketRequest()'],
['src/cli/args.ts', "import { RuntimeClientError } from './runtime/types'"],
['src/cli/flags.ts', "import { RuntimeClientError } from './runtime/types'"],
['src/cli/dispatch.ts', "import { RuntimeClientError } from './runtime/types'"],
['src/cli/selectors.ts', "import { RuntimeClientError } from './runtime/types'"],
['src/cli/format.ts', "} from './runtime/types'"]
]
for (const [file, marker] of checks) {
if (!readFileSync(join(REPO, file), 'utf8').includes(marker)) {
throw new Error(
`${file} no longer contains \`${marker}\` — cli-runtime-client-deferral-benchmark.mjs is stale`
)
}
}
}
function buildArm(label, baselineRev) {
// Why: a build under /tmp cannot resolve the repo's node_modules, so the
// output has to live inside the repo.
const outDir = join(REPO, `.bench-out-${label}`)
rmSync(outDir, { recursive: true, force: true })
const restore = []
try {
if (baselineRev) {
for (const file of TOUCHED) {
const path = join(REPO, file)
restore.push([path, readFileSync(path)])
writeFileSync(
path,
execFileSync('git', ['show', `${baselineRev}:${file}`], {
cwd: REPO,
maxBuffer: 64 * 1024 * 1024
})
)
}
}
execFileSync(
'npx',
[
'tsc',
'-p',
'config/tsconfig.cli.json',
'--outDir',
outDir,
'--composite',
'false',
'--incremental',
'false'
],
{ cwd: REPO, stdio: 'inherit' }
)
} finally {
for (const [path, contents] of restore) {
writeFileSync(path, contents)
}
}
return join(outDir, 'cli/index.js')
}
function run(entry, argv, env) {
const result = spawnSync(process.execPath, [entry, ...argv], {
cwd: REPO,
env: { ...process.env, ...env },
encoding: 'buffer'
})
if (result.error) {
throw result.error
}
return {
status: result.status,
stdout: result.stdout.toString('utf8'),
stderr: result.stderr.toString('utf8')
}
}
// Counts the eager CommonJS module graph of a built entry point by hooking
// Module._load in a child process. This is the quantity the change moves.
function countEagerModules(entry) {
const probe = `
const Module = require('module')
const original = Module._load
const seen = new Set()
Module._load = function (request, parent, isMain) {
try { seen.add(Module._resolveFilename(request, parent, isMain)) } catch { seen.add(request) }
return original.apply(this, arguments)
}
require(${JSON.stringify(entry)})
const all = [...seen]
process.stdout.write(JSON.stringify({
total: all.length,
nodeModules: all.filter((p) => p.includes('node_modules')).length
}))
`
const result = spawnSync(process.execPath, ['-e', probe], { cwd: REPO, encoding: 'utf8' })
if (result.status !== 0) {
throw new Error(`module probe failed: ${result.stderr}`)
}
return JSON.parse(result.stdout)
}
const median = (values) => {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.floor(sorted.length / 2)]
}
const baselineIndex = process.argv.indexOf('--baseline')
const baselineRev = baselineIndex === -1 ? 'HEAD' : process.argv[baselineIndex + 1]
assertMarkersFresh()
const userDataPath = mkdtempSync(join(REPO, '.bench-userdata-'))
try {
console.log(`Building eager baseline (${baselineRev}) …`)
const eagerEntry = buildArm('eager', baselineRev)
console.log('Building deferred (working tree) …')
const deferredEntry = buildArm('deferred', null)
const eagerGraph = countEagerModules(eagerEntry)
const deferredGraph = countEagerModules(deferredEntry)
console.log(
`\nEager modules at process load: ${eagerGraph.total} -> ${deferredGraph.total} ` +
`(node_modules ${eagerGraph.nodeModules} -> ${deferredGraph.nodeModules})`
)
if (deferredGraph.total >= eagerGraph.total) {
throw new Error(
'deferred arm loads no fewer modules — the fixture does not exercise the change'
)
}
// Each case is (label, argv, env). The runtime-dependent ones point at an
// empty user-data dir so both arms get the same deterministic answer.
const isolated = { ORCA_USER_DATA_PATH: userDataPath }
/** @type {Array<[string, string[], Record<string, string>]>} */
const cases = [
['orca --help', ['--help'], {}],
['orca help worktree', ['help', 'worktree'], {}],
['orca (no args)', [], {}],
['unknown command', ['no-such-command'], {}],
['unknown flag', ['worktree', 'list', '--nope'], {}],
['orca agent-context --json', ['agent-context', '--json'], {}],
['orca status --json', ['status', '--json'], isolated],
['orca worktree list --json', ['worktree', 'list', '--json'], isolated]
]
// Why: a semantically broken arm that prints nothing would look fastest.
// Compare bytes and exit codes BEFORE timing anything.
for (const [label, argv, env] of cases) {
const before = run(eagerEntry, argv, env)
const after = run(deferredEntry, argv, env)
if (
before.status !== after.status ||
before.stdout !== after.stdout ||
before.stderr !== after.stderr
) {
throw new Error(`arms disagree for "${label}" — refusing to report a timing`)
}
if (before.stdout.length + before.stderr.length === 0) {
throw new Error(`"${label}" produced no output on either arm; it proves nothing`)
}
}
const pad = (value, width) => String(value).padStart(width)
console.log('\nFresh process per sample, wall clock. Lower is better.')
console.log(`rounds=${ROUNDS} warmup=${WARMUP} (per-arm median, arms alternate lead)`)
console.log(`${pad('case', 26)} ${pad('eager', 10)} ${pad('deferred', 10)} ${pad('speedup', 9)}`)
// Accumulated so V8 cannot treat the spawn loop as dead code.
let consumed = 0
for (const [label, argv, env] of cases) {
for (let index = 0; index < WARMUP; index += 1) {
consumed += run(eagerEntry, argv, env).stdout.length
consumed += run(deferredEntry, argv, env).stdout.length
}
const samples = { eager: [], deferred: [] }
for (let round = 0; round < ROUNDS; round += 1) {
// Alternate which arm leads so a drifting machine load cannot be
// attributed to one arm.
const order =
round % 2 === 0
? [
['eager', eagerEntry],
['deferred', deferredEntry]
]
: [
['deferred', deferredEntry],
['eager', eagerEntry]
]
for (const [arm, entry] of order) {
const started = performance.now()
const result = run(entry, argv, env)
samples[arm].push(performance.now() - started)
consumed += result.stdout.length
}
}
const eagerMs = median(samples.eager)
const deferredMs = median(samples.deferred)
console.log(
`${pad(label, 26)} ${pad(`${eagerMs.toFixed(1)} ms`, 10)} ${pad(`${deferredMs.toFixed(1)} ms`, 10)} ${pad(`${(eagerMs / deferredMs).toFixed(2)}x`, 9)}`
)
}
if (consumed === 0) {
throw new Error('no output consumed — the timing loop was optimised away')
}
console.log(
'\nThe help and error rows are the ones the change targets: they return\n' +
'before any client construction, so they drop the whole graph. `status` and\n' +
'`worktree list` still construct a client, so they only save the eager parse\n' +
'of the parts the local path never uses (ws/tweetnacl via websocket-transport).'
)
} finally {
rmSync(userDataPath, { recursive: true, force: true })
for (const label of ['eager', 'deferred']) {
rmSync(join(REPO, `.bench-out-${label}`), { recursive: true, force: true })
}
}
@@ -0,0 +1,485 @@
#!/usr/bin/env node
// Equivalence check for deferring the RuntimeClient module graph in the CLI.
//
// Builds the CLI twice with the REAL tsc emit — once from the working tree and
// once with the seven touched files restored from git HEAD~ (the pre-deferral
// implementation) — then compares stdout, stderr and exit code BYTE FOR BYTE
// across a matrix of invocations.
//
// Why a script and not a vitest case: this compiles two full CLI trees. It is
// the artifact that proves the refactor is behaviour-preserving; the fast
// invariants (class identity, no eager barrel import) live in
// src/cli/runtime-client-deferral.test.ts and run in the normal suite.
//
// Usage: node config/scripts/cli-runtime-client-deferral-equivalence.mjs [--baseline <rev>]
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const REPO = fileURLToPath(new URL('../..', import.meta.url))
// The files this change touches. Restoring exactly these from the baseline rev
// reconstructs the old implementation without disturbing anything else.
const TOUCHED = [
'src/cli/args.ts',
'src/cli/dispatch.ts',
'src/cli/flags.ts',
'src/cli/format.ts',
'src/cli/index.ts',
'src/cli/runtime/client.ts',
'src/cli/selectors.ts'
]
// Why: if the deferral is ever reverted or reshaped, the "old" arm would
// silently become identical to the new one and every case would pass
// vacuously. Re-read the real call form out of the source and fail loudly.
function assertMarkersFresh() {
const index = readFileSync(join(REPO, 'src/cli/index.ts'), 'utf8')
const client = readFileSync(join(REPO, 'src/cli/runtime/client.ts'), 'utf8')
const checks = [
['src/cli/index.ts', index, 'await loadRuntimeClientClass()'],
['src/cli/index.ts', index, "await import('./runtime-client.js')"],
['src/cli/index.ts', index, "import type { RuntimeClient } from './runtime-client'"],
['src/cli/runtime/client.ts', client, 'await loadSendWebSocketRequest()'],
['src/cli/runtime/client.ts', client, "await import('./websocket-transport.js')"]
]
for (const [file, source, marker] of checks) {
// Match the call form, not a bare word: a comment naming the function must
// not satisfy the check.
if (!source.includes(marker)) {
throw new Error(
`${file} no longer contains \`${marker}\` — cli-runtime-client-deferral-equivalence.mjs is stale`
)
}
}
const repointed = ['src/cli/args.ts', 'src/cli/flags.ts', 'src/cli/dispatch.ts']
for (const file of repointed) {
const source = readFileSync(join(REPO, file), 'utf8')
if (!source.includes("import { RuntimeClientError } from './runtime/types'")) {
throw new Error(`${file} no longer repoints RuntimeClientError at ./runtime/types — stale`)
}
}
}
function buildTree(label, baselineRev) {
// Why: builds under /tmp cannot resolve the repo's node_modules, so the
// output dir has to live inside the repo.
const outDir = join(REPO, `.equiv-out-${label}`)
rmSync(outDir, { recursive: true, force: true })
const restored = []
try {
if (baselineRev) {
for (const file of TOUCHED) {
const path = join(REPO, file)
restored.push([path, readFileSync(path)])
const old = execFileSync('git', ['show', `${baselineRev}:${file}`], {
cwd: REPO,
maxBuffer: 64 * 1024 * 1024
})
writeFileSync(path, old)
}
}
execFileSync(
'npx',
[
'tsc',
'-p',
'config/tsconfig.cli.json',
'--outDir',
outDir,
'--composite',
'false',
'--incremental',
'false'
],
{ cwd: REPO, stdio: 'inherit' }
)
} finally {
for (const [path, contents] of restored) {
writeFileSync(path, contents)
}
}
return join(outDir, 'cli/index.js')
}
// Why: every case is a one-shot CLI invocation that must exit on its own. An
// unbounded spawnSync turns "this argv reached a blocking command" into an
// indefinite stall — a guard that can hang instead of failing is not a guard.
const RUN_TIMEOUT_MS = 30_000
function run(entry, argv, env) {
const result = spawnSync(process.execPath, [entry, ...argv], {
cwd: REPO,
env: { ...process.env, ...env },
encoding: 'buffer',
timeout: RUN_TIMEOUT_MS,
killSignal: 'SIGKILL'
})
if (result.error) {
if (result.error.code === 'ETIMEDOUT') {
throw new Error(
`orca ${argv.join(' ')} did not exit within ${RUN_TIMEOUT_MS} ms — it reached a blocking command`
)
}
throw result.error
}
return {
status: result.status,
stdout: result.stdout.toString('utf8'),
stderr: result.stderr.toString('utf8')
}
}
// Every case must produce identical bytes on both arms. The runtime-dependent
// ones (status/worktree list) are pointed at an empty user-data dir so the
// answer is deterministic "not running" rather than whatever the dev machine
// happens to be doing.
function buildCases(isolatedUserData) {
const isolated = { ORCA_USER_DATA_PATH: isolatedUserData }
const cases = [
// Paths that must never load the runtime client at all.
[[], {}],
[['--help'], {}],
[['help'], {}],
[['help', 'worktree'], {}],
[['help', 'browser'], {}],
[['worktree', '--help'], {}],
[['help', 'no-such-command'], {}],
[['no-such-command'], {}],
[['no-such-command'], { ORCA_PAIRING_CODE: 'garbage' }],
[['wrktree', 'list'], {}],
[['agent-context'], {}],
[['agent-context', '--json'], {}],
// Flag validation must still fire before any runtime lookup.
[['worktree', 'list', '--nonexistent-flag'], {}],
[['worktree', 'list', '--nonexistent-flag', '--json'], {}],
[['browser', 'snapshot', '--nonexistent-flag'], {}],
// resolveRemotePairing throws from the RuntimeClient CONSTRUCTOR.
[['status', '--pairing-code', 'x', '--environment', 'y'], isolated],
[['status', '--pairing-code', 'x', '--environment', 'y', '--json'], isolated],
[['status', '--pairing-code', 'not-a-pairing-code'], isolated],
[['status', '--pairing-code', 'not-a-pairing-code', '--json'], isolated],
[['status', '--pairing-code', 'orca://pair?code=zzzz'], isolated],
[['status', '--environment', 'no-such-environment'], isolated],
[['status', '--environment', 'no-such-environment', '--json'], isolated],
[['worktree', 'list', '--environment', 'no-such-environment', '--json'], isolated],
// The env-var fallback must stay live for non-suppressed commands...
[['status', '--json'], { ...isolated, ORCA_PAIRING_CODE: 'not-a-pairing-code' }],
[['status', '--json'], { ...isolated, ORCA_REMOTE_PAIRING: 'not-a-pairing-code' }],
[['status', '--json'], { ...isolated, ORCA_ENVIRONMENT: 'no-such-environment' }],
// ...and must stay suppressed for the local-only command groups.
//
// NOTE: the only commands that both live in a suppressed group AND touch
// ctx.client are `agent hooks on|off`, which rewrite the user's real agent
// hook configuration in ~/.claude and friends — far outside
// ORCA_USER_DATA_PATH. They are deliberately NOT invoked here. The
// null-vs-undefined suppression they would exercise is covered
// side-effect-free by the constructor-argument assertions in
// src/cli/runtime-client-deferral.test.ts instead.
[['environment', 'list', '--json'], { ...isolated, ORCA_ENVIRONMENT: 'no-such-environment' }],
[['environment', 'list', '--json'], { ...isolated, ORCA_PAIRING_CODE: 'not-a-pairing-code' }],
[['agent-context', '--json'], { ...isolated, ORCA_PAIRING_CODE: 'not-a-pairing-code' }],
// Runtime-unavailable reporting (RuntimeClientError formatting).
[['status'], isolated],
[['status', '--json'], isolated],
[['worktree', 'list', '--json'], isolated],
[['terminal', 'list', '--json'], isolated]
]
// Fuzz: random argv drawn from real command tokens, flags and hostile
// strings. Seeded so a failure is reproducible. Every token here must be
// safe to actually execute — see UNSAFE_TOKENS, which is cross-checked
// against this list before a single case runs.
const tokens = [
'worktree',
'list',
'status',
'browser',
'snapshot',
'terminal',
'environment',
'agent',
'agent-context',
'vm',
'--json',
'--help',
'--pairing-code',
'--environment',
'--worktree',
'orca://pair?code=!!!',
'',
'-',
'--',
'--=',
'a'.repeat(300),
'näme-ünicode',
'💥',
'../..',
'x\ty'
]
let seed = 0x9e3779b9
const next = () => {
seed ^= seed << 13
seed ^= seed >>> 17
seed ^= seed << 5
return (seed >>> 0) / 0x100000000
}
// Why: check the draw POOL, not just the 400 cases it happens to produce.
// `serve` sat in this array for the whole review because the per-case scan
// never named it, and the cases that drew it only survived by accident.
assertTokensSafe(tokens, 'fuzz token pool')
assertFuzzPoolDeclaredReadOnly(tokens)
for (let index = 0; index < 400; index += 1) {
const length = 1 + Math.floor(next() * 4)
const argv = []
for (let part = 0; part < length; part += 1) {
argv.push(tokens[Math.floor(next() * tokens.length)])
}
cases.push([argv, isolated])
}
for (const [argv] of cases) {
assertTokensSafe(argv, `argv ${JSON.stringify(argv)}`)
}
return cases
}
// Why: this script shells out to the REAL CLI with the developer's own HOME and
// PATH, so an argv that reaches the wrong verb does real damage. Two classes:
//
// 1. FOREGROUND — `orca serve` runs Orca until Ctrl+C and `orca open` /
// `claude-teams` spawn processes that outlive the case. A blocking case
// does not fail the run, it stalls it, which is worse than a mismatch.
// 2. MUTATING — writes outside ORCA_USER_DATA_PATH (`agent hooks off` parks
// the real ~/.claude hooks) or drives real browser/desktop input.
//
// Group tokens whose subcommands split read/write (`capture`, `intercept`,
// `label`, `relation`) are denied wholesale: the fuzzer cannot tell them apart.
const FOREGROUND_TOKENS = ['serve', 'open', 'claude-teams', 'exec', 'eval', 'launch', 'attach']
const MUTATING_TOKENS = [
// persistent config and registry state
'on',
'off',
'hooks',
'create',
'remove',
'rm',
'delete',
'add',
'edit',
'set',
'set-value',
'set-base-ref',
'setup-clone',
'setup-create',
'setup-update',
'setup-delete',
'setup-existing-folder',
'install',
'uninstall',
'reinstall',
'update',
'clone',
'apply',
'write',
'reset',
'clear',
'enable',
'disable',
'use-default',
'permissions',
// process and pane lifecycle
'run',
'run-stop',
'start',
'stop',
'kill',
'shutdown',
'close',
'split',
'switch',
'rename',
'focus',
// orchestration writes
'send',
'reply',
'dispatch',
'task-create',
'task-update',
'gate-create',
'gate-resolve',
// issue-tracker writes
'save-issue',
'comment',
'label',
'relation',
'assignee',
'priority',
'estimate',
'due-date',
// browser / computer / emulator input and navigation
'goto',
'back',
'forward',
'reload',
'click',
'dblclick',
'hover',
'fill',
'type',
'type-text',
'select',
'select-all',
'uncheck',
'keypress',
'press-key',
'inserttext',
'hotkey',
'paste-text',
'perform-secondary-action',
'drag',
'scroll',
'scrollintoview',
'wheel',
'move',
'up',
'down',
'tap',
'gesture',
'button',
'rotate',
'upload',
'download',
'dismiss',
'accept',
'highlight',
'viewport',
'geolocation',
'headers',
'credentials',
'offline',
'media',
'device',
'capture',
'intercept'
]
const UNSAFE_TOKENS = new Map([
...FOREGROUND_TOKENS.map((token) => [token, 'runs in the foreground or spawns a process']),
...MUTATING_TOKENS.map((token) => [token, 'can write outside ORCA_USER_DATA_PATH'])
])
// Why: the deny list only catches verbs someone already thought of — `serve`
// sat in the fuzz pool for the whole review because nobody added it. The pool
// is therefore ALSO checked against this allowlist, so a token added to the
// pool fails closed until it is consciously declared read-only here.
const READ_ONLY_FUZZ_TOKENS = new Set([
// command tokens: every path they can form is a list/show or a parse error
'agent',
'agent-context',
'browser',
'environment',
'list',
'snapshot',
'status',
'terminal',
'vm',
'worktree',
// global flags and hostile strings, which reach no handler at all
'--json',
'--help',
'--pairing-code',
'--environment',
'--worktree',
'orca://pair?code=!!!',
'',
'-',
'--',
'--=',
'a'.repeat(300),
'näme-ünicode',
'💥',
'../..',
'x\ty'
])
function assertTokensSafe(tokens, context) {
for (const token of tokens) {
const reason = UNSAFE_TOKENS.get(token)
if (reason) {
throw new Error(`Refusing to run ${context}: "${token}" ${reason}`)
}
}
}
// Why: a token on neither list (or, worse, on both) means the two lists have
// drifted apart. Fail before any case runs rather than sampling and hoping.
function assertFuzzPoolDeclaredReadOnly(tokens) {
for (const token of tokens) {
if (!READ_ONLY_FUZZ_TOKENS.has(token)) {
throw new Error(
`Fuzz token ${JSON.stringify(token)} is not declared in READ_ONLY_FUZZ_TOKENS — declare it read-only or drop it`
)
}
}
for (const token of READ_ONLY_FUZZ_TOKENS) {
if (UNSAFE_TOKENS.has(token)) {
throw new Error(
`Token ${JSON.stringify(token)} is declared both read-only and unsafe — the two lists disagree`
)
}
}
}
const baselineIndex = process.argv.indexOf('--baseline')
const baselineRev = baselineIndex === -1 ? 'HEAD' : process.argv[baselineIndex + 1]
assertMarkersFresh()
const isolatedUserData = mkdtempSync(join(REPO, '.equiv-userdata-'))
mkdirSync(join(isolatedUserData, 'empty'), { recursive: true })
let oldEntry
let newEntry
try {
// Why: build the case list (and run its safety guards) BEFORE the two tsc
// compiles, so an unsafe token fails in a second instead of two minutes in.
const cases = buildCases(isolatedUserData)
console.log(`Building baseline (${baselineRev}) …`)
oldEntry = buildTree('old', baselineRev)
console.log('Building working tree …')
newEntry = buildTree('new', null)
console.log(`Comparing ${cases.length} invocations byte for byte …`)
let mismatches = 0
for (const [argv, env] of cases) {
const before = run(oldEntry, argv, env)
const after = run(newEntry, argv, env)
if (
before.status !== after.status ||
before.stdout !== after.stdout ||
before.stderr !== after.stderr
) {
mismatches += 1
console.error(`\nMISMATCH argv=${JSON.stringify(argv)} env=${JSON.stringify(env)}`)
console.error(` exit before=${before.status} after=${after.status}`)
if (before.stdout !== after.stdout) {
console.error(` stdout before=${JSON.stringify(before.stdout.slice(0, 400))}`)
console.error(` after =${JSON.stringify(after.stdout.slice(0, 400))}`)
}
if (before.stderr !== after.stderr) {
console.error(` stderr before=${JSON.stringify(before.stderr.slice(0, 400))}`)
console.error(` after =${JSON.stringify(after.stderr.slice(0, 400))}`)
}
}
}
if (mismatches > 0) {
throw new Error(`${mismatches} of ${cases.length} invocations differ`)
}
console.log(`\nAll ${cases.length} invocations byte-identical (stdout, stderr, exit code).`)
} finally {
rmSync(isolatedUserData, { recursive: true, force: true })
for (const label of ['old', 'new']) {
rmSync(resolve(REPO, `.equiv-out-${label}`), { recursive: true, force: true })
}
}
@@ -47,6 +47,7 @@ describe('computer-use e2e workflow', () => {
expect(triggerPaths).toEqual(
expect.arrayContaining([
'config/scripts/computer-e2e-workflow.test.mjs',
'config/scripts/computer-use-modifier-safety.test.mjs',
'config/scripts/computer-use-skill-guidance.test.mjs',
'config/scripts/computer-use-smoke.mjs',
'config/scripts/computer-use-smoke.test.mjs',
@@ -72,6 +73,7 @@ describe('computer-use e2e workflow', () => {
const regressionRun = nativeSmokeRuns.find((run) => run.includes('pnpm vitest run'))
const expectedRegressionFiles = [
'config/scripts/computer-e2e-workflow.test.mjs',
'config/scripts/computer-use-modifier-safety.test.mjs',
'config/scripts/computer-use-skill-guidance.test.mjs',
'config/scripts/computer-use-smoke.test.mjs',
'src/main/computer/computer-provider-lifecycle.test.ts',
@@ -0,0 +1,72 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const projectDir = resolve(import.meta.dirname, '../..')
function source(path) {
return readFileSync(join(projectDir, path), 'utf8')
}
function sourceBetween(contents, startMarker, endMarker) {
const start = contents.indexOf(startMarker)
const end = contents.indexOf(endMarker, start + startMarker.length)
if (start < 0 || end < 0) {
throw new Error(`Missing source boundary: ${startMarker} → ${endMarker}`)
}
return contents.slice(start, end)
}
describe('computer-use modifier safety', () => {
it('uses mouse-event flags instead of held modifier keys on macOS', () => {
const macOS = source('native/computer-use-macos/Sources/OrcaComputerUseMacOS/main.swift')
const clickInput = sourceBetween(macOS, 'static func click(', 'static func scroll(')
const mouseInput = sourceBetween(
macOS,
'private static func mouse(',
'private static func keyEvent('
)
expect(mouseInput).toContain('event.flags = flags')
expect(clickInput.match(/flags: flags/gu)).toHaveLength(3)
expect(clickInput).not.toContain('down: true')
})
it('submits each modified Windows click in a closed, timed SendInput batch', () => {
const windows = source('native/computer-use-windows/runtime.ps1')
const modifiedClick = sourceBetween(
windows,
'public static void SendModifiedClick',
'private static INPUT KeyboardInput'
)
const mouseClick = sourceBetween(
windows,
'function Send-OrcaMouseClick',
'function Send-OrcaDrag'
)
expect(modifiedClick).toContain('SendInput((uint)values.Length, values')
expect(modifiedClick).toContain('SendInput((uint)releaseValues.Length, releaseValues')
expect(modifiedClick).toContain('if (sent != (uint)values.Length)')
expect(modifiedClick).toContain('releases.Add(MouseInput(mouseInput, mouseUp))')
expect(modifiedClick).not.toContain('int count')
expect(mouseClick).toMatch(
/for \(\$i = 0; \$i -lt \$clickCount; \$i\+\+\) \{\s+\[OrcaDesktopWin32\]::SendModifiedClick\(/
)
expect(mouseClick).toContain('if ($i + 1 -lt $clickCount) { Start-Sleep -Milliseconds 35 }')
expect(windows).not.toContain('keybd_event')
})
it('keeps Linux modifier release in the xdotool sequence and a fallback', () => {
const linux = source('native/computer-use-linux/runtime.py')
const modifiedClick = sourceBetween(linux, 'def modified_click_at(', 'def scroll_at(')
expect(modifiedClick).toContain('command.extend(["keyup", modifier])')
expect(modifiedClick).toContain('is_wayland')
expect(modifiedClick).toContain('modified clicks require xdotool on an X11 session')
expect(modifiedClick).toContain('finally:')
expect(modifiedClick).toContain('check=False')
expect(modifiedClick).toContain('timeout=5')
expect(modifiedClick).toContain('timeout=2')
})
})
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { BUNDLED_SKILL_GUIDES } from '../../src/cli/bundled-skill-guides'
const projectDir = resolve(import.meta.dirname, '../..')
// Why: computer-use now ships a hybrid discovery stub, so its version-sensitive command
@@ -8,6 +9,7 @@ const projectDir = resolve(import.meta.dirname, '../..')
// installable stub projection is checked separately below.
const guidePath = join(projectDir, 'skill-guides', 'computer-use.md')
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 web-app targeting on the computer-use surface', () => {
@@ -44,6 +46,15 @@ describe('computer-use skill guidance', () => {
expect(skill).toContain('`result.snapshot.treeText`')
expect(skill).not.toContain('`result.elements`')
})
it('requires atomic modifier-click actions in the source and bundled guide', () => {
expect(bundledGuide).toBeDefined()
for (const skill of [readFileSync(guidePath, 'utf8'), bundledGuide]) {
expect(skill).toContain('click --modifiers <chord>')
expect(skill).toContain('Never synthesize separate modifier-down and modifier-up commands')
}
})
})
describe('computer-use install stub', () => {
@@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const electronBuilderConfig = require('../electron-builder.config.cjs')
const { FileMatcher } = require('app-builder-lib/out/fileMatcher')
const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs')
const {
createPackagedRuntimeNodeModuleResources,
@@ -19,6 +20,12 @@ const {
} = require('../packaged-runtime-node-modules.cjs')
describe('electron-builder config', () => {
it('keeps the packaged app identity aligned with local-build validation', () => {
expect(electronBuilderConfig.appId).toBe(
require('../../src/shared/local-build-compatibility-contract.json').appId
)
})
it('excludes repo-only source trees from app.asar', () => {
expect(electronBuilderConfig.files).toEqual(
expect.arrayContaining([
@@ -32,15 +39,39 @@ describe('electron-builder config', () => {
'!skill-stubs{,/**/*}',
'!resources/skills/**',
'!tests{,/**/*}',
'!examples{,/**/*}',
'!pr-evidence{,/**/*}',
'!Casks{,/**/*}',
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md}',
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md,ORCHESTRATION_IMPLEMENTATION_CHECKLIST.md,ORCHESTRATION_STRUCTURED_OUTPUT_DESIGN.md}',
'!out/**/*.test.js',
'!resources/plugins/launch/**'
])
)
})
// Why: `files` is an all-negation list, so electron-builder's default `**/*` packs
// anything without an explicit `!` entry — examples/ landed without one and shipped
// hostile-panel, the adversarial containment fixture, into 1.4.160-rc.3's app.asar.
// Drive the real matcher: pinning the pattern string cannot prove it excludes the tree.
it('keeps plugin authoring examples out of app.asar', () => {
const matcher = new FileMatcher('/app', '/dest', (value) => value, electronBuilderConfig.files)
// copyFiles() prepends this itself once the pattern list is all-negation.
matcher.prependPattern('**/*')
const isPacked = matcher.createFilter()
const packs = (repoPath) => isPacked(join('/app', repoPath), { isDirectory: () => false })
for (const authoringOnly of [
'examples/plugins/hostile-panel/panel.html',
'examples/plugins/hostile-panel/orca-plugin.json',
'examples/plugins/hello-orca/main.mjs',
'examples/plugins/hello-orca/orca-plugin.json'
]) {
expect(packs(authoringOnly)).toBe(false)
}
// The negation stays anchored at the app root, so nested `examples` segments still ship.
expect(packs('out/main/examples/index.js')).toBe(true)
})
it('keeps runtime resources available through extraResources', () => {
const bundledPluginResources = expect.objectContaining({
from: 'resources/plugins/launch',
@@ -135,6 +166,14 @@ describe('electron-builder config', () => {
)
})
// Why: the watchdog only arms in packaged builds, and its ELECTRON_RUN_AS_NODE
// fork resolves the entry from app.asar.unpacked — inside the asar it never runs.
it('unpacks the forked main-thread hang-watchdog entry', () => {
expect(electronBuilderConfig.asarUnpack).toEqual(
expect.arrayContaining(['out/main/main-thread-hang-watchdog-entry.js'])
)
})
it('uses the multi-size icon source for Linux packages', () => {
expect(electronBuilderConfig.linux.icon).toBe('resources/build/icon.icns')
})
@@ -173,6 +212,58 @@ describe('electron-builder config', () => {
}
})
it('overrides packaged semver only for local macOS builds', () => {
const configPath = require.resolve('../electron-builder.config.cjs')
const original = process.env.ORCA_LOCAL_BUILD_VERSION
const originalMacRelease = process.env.ORCA_MAC_RELEASE
try {
delete require.cache[configPath]
delete process.env.ORCA_MAC_RELEASE
process.env.ORCA_LOCAL_BUILD_VERSION = '1.4.159-rc.0.local.123.abc'
expect(require('../electron-builder.config.cjs').extraMetadata).toEqual({
version: '1.4.159-rc.0.local.123.abc'
})
} finally {
if (originalMacRelease === undefined) {
delete process.env.ORCA_MAC_RELEASE
} else {
process.env.ORCA_MAC_RELEASE = originalMacRelease
}
if (original === undefined) {
delete process.env.ORCA_LOCAL_BUILD_VERSION
} else {
process.env.ORCA_LOCAL_BUILD_VERSION = original
}
delete require.cache[configPath]
require('../electron-builder.config.cjs')
}
})
it('never applies local semver to release packaging', () => {
const configPath = require.resolve('../electron-builder.config.cjs')
const originalLocalVersion = process.env.ORCA_LOCAL_BUILD_VERSION
const originalMacRelease = process.env.ORCA_MAC_RELEASE
try {
delete require.cache[configPath]
process.env.ORCA_LOCAL_BUILD_VERSION = '1.4.159-local.123.abc'
process.env.ORCA_MAC_RELEASE = '1'
expect(require('../electron-builder.config.cjs').extraMetadata).toBeUndefined()
} finally {
if (originalLocalVersion === undefined) {
delete process.env.ORCA_LOCAL_BUILD_VERSION
} else {
process.env.ORCA_LOCAL_BUILD_VERSION = originalLocalVersion
}
if (originalMacRelease === undefined) {
delete process.env.ORCA_MAC_RELEASE
} else {
process.env.ORCA_MAC_RELEASE = originalMacRelease
}
delete require.cache[configPath]
require('../electron-builder.config.cjs')
}
})
it('uses Orca native rebuild hook instead of electron-builder default rebuild', () => {
expect(electronBuilderConfig.beforeBuild).toBe(electronBuilderNativeRebuild)
expect(electronBuilderConfig.npmRebuild).toBe(true)
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import { electronViteConfig } from '../../electron.vite.config'
const targetConfig = readFileSync('config/electron-vite-target.config.ts', 'utf8')
const devRunner = readFileSync('config/scripts/run-electron-vite-dev.mjs', 'utf8')
describe('Electron Vite output contract', () => {
it('keeps main-process and plain-Node entries at stable CommonJS paths', () => {
@@ -39,4 +40,12 @@ describe('Electron Vite output contract', () => {
it('rejects prototype properties as build targets', () => {
expect(targetConfig).toContain('Object.prototype.hasOwnProperty.call(configByTarget, target)')
})
it('gives the dev terminal daemon helper the TCC identity watched by Orca', () => {
expect(devRunner).toContain('const helperBundleId = `${bundleId}.helper`')
expect(devRunner).toContain("'Electron Helper.app',")
expect(devRunner).toContain(
"setPlistValue(helperPlistPath, 'CFBundleIdentifier', helperBundleId)"
)
})
})
@@ -0,0 +1,23 @@
import { execFileSync } from 'node:child_process'
import process from 'node:process'
export function selectPullRequestDiffBase(requestedBase, headParents, eventName) {
if (eventName === 'pull_request' && headParents.length >= 2) {
return headParents[0]
}
return requestedBase
}
export function resolvePullRequestDiffBase(
root,
requestedBase,
eventName = process.env.GITHUB_EVENT_NAME
) {
const [, ...headParents] = execFileSync('git', ['rev-list', '--parents', '-n', '1', 'HEAD'], {
cwd: root,
encoding: 'utf8'
})
.trim()
.split(/\s+/)
return selectPullRequestDiffBase(requestedBase, headParents, eventName)
}
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { selectPullRequestDiffBase } from './git-pull-request-diff-base.mjs'
describe('pull request diff base selection', () => {
it('uses the merge commit first parent for pull request checkouts', () => {
expect(
selectPullRequestDiffBase('event-base', ['current-base', 'pull-request-head'], 'pull_request')
).toBe('current-base')
})
it('keeps the requested base outside synthetic pull request merges', () => {
expect(selectPullRequestDiffBase('requested-base', ['parent'], 'pull_request')).toBe(
'requested-base'
)
expect(selectPullRequestDiffBase('requested-base', ['parent', 'other'], 'push')).toBe(
'requested-base'
)
})
})
@@ -0,0 +1,178 @@
#!/usr/bin/env node
// Benchmark: the per-id worktree/tab lookups in session hydration and terminal reconnect.
//
// Four sites in store/slices/terminals.ts re-flattened worktreesByRepo (or tabsByWorktree)
// and linearly searched it once per loop iteration -- O(rows x ids) for O(rows + ids)
// distinct work. The fix builds one first-wins index per loop.
//
// This runs on the renderer's synchronous cold-start path and gates workspaceSessionReady,
// which blocks terminal pane mounting, so the cost is paid before the first frame.
//
// Both arms produce the resolved rows and are compared for equality before timing, so an
// index that resolved differently could not be reported as a win.
//
// Run with: node config/scripts/hydrate-worktree-lookup-benchmark.mjs
import { performance } from 'node:perf_hooks'
const ITERATIONS = Number(process.env.ORCA_HYDRATE_BENCH_ITERATIONS ?? '60')
const WARMUP = Number(process.env.ORCA_HYDRATE_BENCH_WARMUP ?? '10')
const ROUNDS = 6
for (const [name, value] of [
['ORCA_HYDRATE_BENCH_ITERATIONS', ITERATIONS],
['ORCA_HYDRATE_BENCH_WARMUP', WARMUP]
]) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer, received ${value}`)
}
}
// Pre-fix: re-flatten and linear-search per id.
function resolveByFlatten(worktreesByRepo, ids) {
const resolved = []
for (const id of ids) {
const worktree = Object.values(worktreesByRepo)
.flat()
.find((entry) => entry.id === id)
resolved.push(worktree ? worktree.repoId : null)
}
return resolved
}
// Post-fix: mirrors buildWorktreeByIdIndex in store/slices/worktree-by-id-index.ts.
function resolveByIndex(worktreesByRepo, ids) {
const index = new Map()
for (const worktrees of Object.values(worktreesByRepo)) {
for (const worktree of worktrees) {
if (!index.has(worktree.id)) {
index.set(worktree.id, worktree)
}
}
}
const resolved = []
for (const id of ids) {
const worktree = index.get(id)
resolved.push(worktree ? worktree.repoId : null)
}
return resolved
}
function makeStore(repoCount, worktreesPerRepo) {
const worktreesByRepo = {}
for (let repo = 0; repo < repoCount; repo += 1) {
const repoId = `repo-${repo}`
worktreesByRepo[repoId] = Array.from({ length: worktreesPerRepo }, (_value, index) => ({
id: `${repoId}/wt-${index}`,
repoId,
path: `/Users/dev/worktrees/${repoId}/wt-${index}`,
branch: `feature/branch-${index}`
}))
}
// Why a deliberate duplicate: `.find()` is first-wins, so an index that overwrote on
// collision would resolve a different repo. Without a collision in the fixture that
// difference is unobservable and the equality check below would pass a broken index.
if (repoCount > 1) {
const [firstRepo, secondRepo] = Object.keys(worktreesByRepo)
worktreesByRepo[secondRepo] = [
{ ...worktreesByRepo[firstRepo][0], repoId: secondRepo },
...worktreesByRepo[secondRepo]
]
}
return worktreesByRepo
}
// Why a miss fraction: SSH worktrees are absent from worktreesByRepo at cold start, so
// the real workload includes ids that scan the whole list without matching -- the worst
// case for the linear arm, and the one the code comments call out explicitly.
function makeIds(worktreesByRepo, count) {
const all = Object.values(worktreesByRepo).flat()
const ids = Array.from({ length: count }, (_value, index) =>
index % 7 === 0 ? `absent/wt-${index}` : all[(index * 31) % all.length].id
)
// Always look up the duplicated id, so first-wins is exercised, not just present.
ids[1] = all[0].id
return ids
}
function timeArm(resolve, worktreesByRepo, ids) {
let sink = 0
const start = performance.now()
for (let index = 0; index < ITERATIONS; index += 1) {
// Consume the result so V8 cannot drop the call as dead.
sink += resolve(worktreesByRepo, ids).length
}
const elapsed = (performance.now() - start) / ITERATIONS
if (sink === -1) {
throw new Error('unreachable')
}
return elapsed
}
function median(samples) {
const sorted = [...samples].sort((a, b) => a - b)
const mid = sorted.length / 2
return (sorted[mid - 1] + sorted[mid]) / 2
}
// Arms alternate which one leads so within-round drift cannot favour either.
function measure(worktreesByRepo, ids) {
for (let index = 0; index < WARMUP; index += 1) {
resolveByFlatten(worktreesByRepo, ids)
resolveByIndex(worktreesByRepo, ids)
}
const flattenSamples = []
const indexSamples = []
for (let round = 0; round < ROUNDS; round += 1) {
if (round % 2 === 0) {
flattenSamples.push(timeArm(resolveByFlatten, worktreesByRepo, ids))
indexSamples.push(timeArm(resolveByIndex, worktreesByRepo, ids))
} else {
indexSamples.push(timeArm(resolveByIndex, worktreesByRepo, ids))
flattenSamples.push(timeArm(resolveByFlatten, worktreesByRepo, ids))
}
}
return { flattenMs: median(flattenSamples), indexMs: median(indexSamples) }
}
const pad = (value, width) => String(value).padStart(width)
console.log('Session-hydration worktree lookup, per cold start. Lower is better.')
console.log(`iterations=${ITERATIONS} warmup=${WARMUP} rounds=${ROUNDS} (per-arm medians)`)
console.log(
`${pad('repos', 6)} ${pad('worktrees', 10)} ${pad('ids', 5)} ${pad('flatten', 11)} ${pad('indexed', 11)} ${pad('speedup', 9)}`
)
// Row shapes are synthetic, sized against a real orca-data.json on a heavy machine
// (10 repos / 423 worktrees / 188 pending reconnect). They are not that dataset: the
// generator spreads worktrees evenly and injects one duplicate id, so treat the counts
// as "about this scale", not a replay.
for (const [repoCount, worktreesPerRepo, idCount] of [
[1, 5, 5],
[3, 20, 20],
[10, 42, 188],
[10, 100, 400]
]) {
const worktreesByRepo = makeStore(repoCount, worktreesPerRepo)
const ids = makeIds(worktreesByRepo, idCount)
const flattenResult = resolveByFlatten(worktreesByRepo, ids)
const indexResult = resolveByIndex(worktreesByRepo, ids)
if (JSON.stringify(flattenResult) !== JSON.stringify(indexResult)) {
throw new Error(`resolution differs at ${repoCount} repos x ${worktreesPerRepo} worktrees`)
}
if (!flattenResult.some((value) => value !== null)) {
throw new Error(`fixture resolved nothing at ${repoCount} repos`)
}
if (!flattenResult.some((value) => value === null)) {
throw new Error(`fixture had no absent ids at ${repoCount} repos`)
}
// Count the generated rows rather than multiplying: makeStore injects a duplicate
// id for multi-repo cases, so the product would misreport the fixture by one.
const worktreeCount = Object.values(worktreesByRepo).reduce((sum, rows) => sum + rows.length, 0)
const { flattenMs, indexMs } = measure(worktreesByRepo, ids)
console.log(
`${pad(repoCount, 6)} ${pad(worktreeCount, 10)} ${pad(idCount, 5)} ${pad(`${flattenMs.toFixed(4)} ms`, 11)} ${pad(`${indexMs.toFixed(4)} ms`, 11)} ${pad(`${(flattenMs / indexMs).toFixed(1)}x`, 9)}`
)
}
console.log(
'\nFixtures are synthetic at real-world scale, not a replay of a real session.\nThis times one of the four lookup sites. A one-repo session sees almost nothing;\nthe win scales with worktrees x pending ids, and lands on the cold-start path that\ngates terminal pane mounting.'
)
@@ -200,7 +200,7 @@ export async function setupTerminal(page, repoPath, logPhase) {
return pane?.container?.dataset?.ptyId ?? null
})
)
logPhase('setup.pty-bound', `ptyId=${ptyId}`)
logPhase('setup.pty-bound', `ptyId=${String(ptyId)}`)
return ptyId
}
@@ -1,5 +1,6 @@
import { CROSS_LOCALE_KEY_OVERRIDES } from './locale-cross-locale-key-overrides.mjs'
import { KO_KEY_OVERRIDES } from './locale-ko-key-overrides.mjs'
import { MACOS_TCC_KEY_OVERRIDES } from './locale-macos-tcc-key-overrides.mjs'
export function mergeLocaleKeyOverrides(base) {
const merged = { ...base }
@@ -10,5 +11,8 @@ export function mergeLocaleKeyOverrides(base) {
// KO split overrides can share keys with zh/ja repairs; merge per locale.
merged[key] = { ...merged[key], ...overrides }
}
for (const [key, overrides] of Object.entries(MACOS_TCC_KEY_OVERRIDES)) {
merged[key] = { ...merged[key], ...overrides }
}
return merged
}
@@ -2231,9 +2231,6 @@
"auto.components.settings.DeveloperPermissionsPane.16381e040a": {
"ko": "마이크"
},
"auto.components.settings.DeveloperPermissionsPane.7ca17b62c8": {
"ko": "프로젝트, 워크트리 또는 심볼릭 링크된 파일이 macOS 보호 폴더에 닿을 때 권장됩니다."
},
"auto.components.settings.DeveloperPermissionsPane.e119f0d66b": {
"ko": "자동화"
},
@@ -0,0 +1,18 @@
export const MACOS_TCC_KEY_OVERRIDES = {
'auto.hooks.useMacosTccPromptNotice.description': {
es: 'macOS atribuye a Orca el acceso a archivos realizado por tus agentes y herramientas de terminal. Conceder acceso total al disco reduce estos avisos.',
ja: 'エージェントやターミナルツールがファイルにアクセスすると、macOS はそのアクセス元を Orca として扱います。フルディスクアクセスを許可すると、これらの確認を減らせます。',
ko: '에이전트와 터미널 도구의 파일 접근은 macOS에서 Orca의 접근으로 표시됩니다. 전체 디스크 접근 권한을 허용하면 이러한 요청을 줄일 수 있습니다.',
zh: 'macOS 会将代理和终端工具的文件访问归因于 Orca。授予“完全磁盘访问权限”可减少此类提示。'
},
'auto.components.settings.DeveloperPermissionsPane.7ca17b62c8': {
es: 'Cuando los agentes que ejecuta Orca leen datos de otras apps, macOS muestra el nombre de Orca porque es el proceso responsable de los comandos de terminal. Concede este permiso a Orca y Orca Helper para reducir esos avisos. Después, cierra Orca, finaliza cualquier proceso de Orca Helper que siga en ejecución y vuelve a abrir Orca.',
ja: 'Orca が実行するエージェントがほかのアプリのデータを読み取ると、ターミナルコマンドの実行元プロセスである Orca の名前が macOS に表示されます。これらの確認を減らすには、Orca と Orca Helper にこの権限を許可してください。その後、Orca を終了し、残っている Orca Helper プロセスもすべて終了してから、Orca を再度開いてください。',
ko: 'Orca가 실행하는 에이전트가 다른 앱의 데이터를 읽으면, macOS는 터미널 명령을 실행하는 프로세스인 Orca를 표시합니다. 이러한 요청을 줄이려면 Orca와 Orca Helper에 이 권한을 허용하세요. 그런 다음 Orca를 종료하고, 남아 있는 Orca Helper 프로세스도 모두 종료한 후 Orca를 다시 여세요.',
zh: '当 Orca 运行的代理读取其他应用的数据时,macOS 会显示 Orca,因为 Orca 是执行终端命令的进程。请为 Orca 和 Orca Helper 授予此权限,以减少此类提示。然后退出 Orca,结束所有仍在运行的 Orca Helper 进程,再重新打开 Orca。'
},
'auto.components.settings.DeveloperPermissionsPane.c566bca278': {
ko: '전체 디스크 접근 권한',
zh: '完全磁盘访问权限'
}
}
@@ -0,0 +1,19 @@
const LOCALIZED_PROSE_TERM_KEYS = new Set([
'auto.hooks.useMacosTccPromptNotice.description',
'auto.components.settings.DeveloperPermissionsPane.7ca17b62c8'
])
const LOCALIZABLE_PROSE_TERMS = new Set([
'Agent',
'Agents',
'agent',
'agents',
'Terminal',
'Terminals',
'terminal',
'terminals'
])
export function isLocalizedProseTermContext(term, key) {
return LOCALIZED_PROSE_TERM_KEYS.has(key) && LOCALIZABLE_PROSE_TERMS.has(term)
}
@@ -1,4 +1,5 @@
import { CJK_LATIN_SPACED_TERMS } from './locale-cjk-latin-spaced-terms.mjs'
import { isLocalizedProseTermContext } from './locale-prose-term-exemptions.mjs'
import { isScreenCursorContext } from './locale-screen-cursor-exemptions.mjs'
import { LOCALE_KEY_OVERRIDES } from './locale-key-overrides.mjs'
import { LOCALE_PHRASE_FIXES } from './locale-phrase-fixes.mjs'
@@ -382,6 +383,9 @@ function applyBrandMistranslationFixes(enValue, localeValue, locale, key = '') {
if (isScreenCursorContext(brand, enValue, key)) {
continue
}
if (isLocalizedProseTermContext(brand, key)) {
continue
}
if (includesPreservedLatinTerm(result, brand)) {
continue
}
@@ -0,0 +1,22 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
describe('localization package scripts', () => {
const scripts = JSON.parse(readFileSync('package.json', 'utf8')).scripts
it('keeps safe catalog and extraction verification available', () => {
expect(scripts['verify:localization-catalog']).toBeDefined()
expect(scripts['sync:localization-catalog']).toBeDefined()
expect(scripts['verify:localization-extraction']).toBeDefined()
})
it('does not expose whole-catalog translation and repair commands', () => {
expect(scripts['bootstrap:locale-catalog']).toBeUndefined()
expect(scripts['bootstrap:zh-catalog']).toBeUndefined()
expect(scripts['bootstrap:ko-catalog']).toBeUndefined()
expect(scripts['bootstrap:ja-catalog']).toBeUndefined()
expect(scripts['bootstrap:es-catalog']).toBeUndefined()
expect(scripts['repair:locale-catalog']).toBeUndefined()
})
})
@@ -0,0 +1,34 @@
const { writeFileSync } = require('node:fs')
const { join } = require('node:path')
const compatibilityContract = require('../../src/shared/local-build-compatibility-contract.json')
const MAC_BUILD_COMPATIBILITY_FILENAME = 'orca-local-build.json'
function createMacBuildCompatibility({ version, commit, architecture }) {
if (architecture !== 'arm64' && architecture !== 'x64') {
throw new Error(`Unsupported macOS build architecture: ${architecture}`)
}
return {
...compatibilityContract,
buildId: `${version}-${commit}-${architecture}`,
version,
commit,
platform: 'darwin',
architecture
}
}
function writeMacBuildCompatibility(resourcesDir, identity) {
const compatibility = createMacBuildCompatibility(identity)
writeFileSync(
join(resourcesDir, MAC_BUILD_COMPATIBILITY_FILENAME),
`${JSON.stringify(compatibility, null, 2)}\n`,
'utf8'
)
}
module.exports = {
MAC_BUILD_COMPATIBILITY_FILENAME,
createMacBuildCompatibility,
writeMacBuildCompatibility
}
@@ -0,0 +1,36 @@
import { createRequire } from 'node:module'
import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const { createMacBuildCompatibility } = require('./mac-build-compatibility.cjs')
describe('mac build compatibility metadata', () => {
it('binds version, commit, and architecture into the packaged contract', () => {
expect(
createMacBuildCompatibility({
version: '1.2.3-local.1',
commit: 'abc123',
architecture: 'arm64'
})
).toMatchObject({
formatVersion: 1,
appId: 'com.stablyai.orca',
buildId: '1.2.3-local.1-abc123-arm64',
version: '1.2.3-local.1',
commit: 'abc123',
stateSchemaVersion: 1,
platform: 'darwin',
architecture: 'arm64'
})
})
it('rejects unsupported architecture metadata', () => {
expect(() =>
createMacBuildCompatibility({
version: '1.2.3',
commit: 'abc123',
architecture: 'universal'
})
).toThrow('Unsupported macOS build architecture')
})
})
@@ -0,0 +1,50 @@
import fs from 'node:fs'
import { describe, expect, it } from 'vitest'
import { repairTranslatedValue } from './locale-translation-policy.mjs'
const LOCALES = ['es', 'ja', 'ko', 'zh']
const KEYS = [
'auto.hooks.useMacosTccPromptNotice.title',
'auto.hooks.useMacosTccPromptNotice.description',
'auto.hooks.useMacosTccPromptNotice.openSettings',
'auto.hooks.useMacosTccPromptNotice.dismiss',
'auto.components.settings.DeveloperPermissionsPane.7ca17b62c8',
'auto.components.settings.DeveloperPermissionsPane.c566bca278'
]
function readCatalog(locale) {
return JSON.parse(
fs.readFileSync(
new URL(`../../src/renderer/src/i18n/locales/${locale}.json`, import.meta.url),
'utf8'
)
)
}
function getValue(catalog, key) {
return key.split('.').reduce((value, part) => value[part], catalog)
}
describe('macOS TCC prompt localization', () => {
it('survives the canonical catalog repair policy', () => {
const english = readCatalog('en')
for (const locale of LOCALES) {
const catalog = readCatalog(locale)
for (const key of KEYS) {
const enValue = getValue(english, key)
const localeValue = getValue(catalog, key)
expect(repairTranslatedValue({ key, enValue, localeValue, locale })).toBe(localeValue)
}
}
})
it('uses the macOS Full Disk Access labels in Korean and Chinese', () => {
expect(
getValue(readCatalog('ko'), 'auto.components.settings.DeveloperPermissionsPane.c566bca278')
).toBe('전체 디스크 접근 권한')
expect(
getValue(readCatalog('zh'), 'auto.components.settings.DeveloperPermissionsPane.c566bca278')
).toBe('完全磁盘访问权限')
})
})
@@ -0,0 +1,47 @@
import { spawnSync } from 'node:child_process'
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
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'
)
function lintSource(source) {
const directory = mkdtempSync(path.join(tmpdir(), 'orca-qrcode-import-lint-'))
const sourcePath = path.join(directory, 'sample.ts')
const configPath = path.join(directory, 'oxlint.json')
writeFileSync(sourcePath, source)
writeFileSync(
configPath,
JSON.stringify({
categories: { correctness: 'off' },
jsPlugins: [{ name: 'mobile-pairing', specifier: pluginPath }],
rules: { 'mobile-pairing/no-eager-qrcode-import': 'error' }
})
)
const result = spawnSync(oxlintPath, ['--config', configPath, '--format', 'json', sourcePath], {
encoding: 'utf8'
})
if (result.error) {
throw result.error
}
return JSON.parse(result.stdout).diagnostics
}
describe('mobile pairing qrcode import rule', () => {
it('rejects eager runtime imports', () => {
const diagnostics = lintSource("import QRCode from 'qrcode'\nvoid QRCode")
expect(diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
'mobile-pairing(no-eager-qrcode-import)'
])
})
it('allows type-only and lazy imports', () => {
expect(lintSource("import type QRCode from 'qrcode'\nlet qr: typeof QRCode")).toEqual([])
expect(lintSource("const QRCode = await import('qrcode')\nvoid QRCode")).toEqual([])
})
})
@@ -40,7 +40,10 @@ describe('Windows SSH relay node-pty console-list patch', () => {
const tamperedPatch = writeNodePtyFixture('1.1.0', publishedAgentSource())
patchNodePtyConsoleListAgent(tamperedPatch.root)
writeFileSync(tamperedPatch.agentPath, `${readFileSync(tamperedPatch.agentPath)}\n// drift`)
writeFileSync(
tamperedPatch.agentPath,
`${readFileSync(tamperedPatch.agentPath, 'utf8')}\n// drift`
)
expect(() => assertPatchedNodePtyConsoleListAgent(tamperedPatch.root)).toThrow('not installed')
})
})
@@ -51,18 +51,39 @@ describe('orchestration skill guidance', () => {
)
})
it('teaches the hard cutover without reviving a legacy executor', () => {
it('teaches attested adoption without reviving the retired scheduler', () => {
const skill = readSkill()
const migration = getSection(skill, 'Contract Migration')
expect(migration).toContain('hard cutover')
expect(migration).toContain('effectsApplied')
expect(migration).toContain('skills get orchestration --full')
expect(migration).toContain('Do not retry the rejected command unchanged')
expect(migration).toContain('no longer supervised')
expect(migration).toContain('task-list --run run_legacy_local')
expect(migration).toContain('Read-only inspection never consumes legacy mail')
expect(migration).toContain('does not run a legacy scheduler, translate old writes, or drain')
expect(migration).toContain(
'adopts a live pre-update orchestration assignment into an ordinary Run'
)
expect(migration).toContain(
'preserves the existing agent process, PTY/session, terminal handle, tab/leaf/pane, worktree or folder workspace, Task, and Dispatch'
)
expect(migration).toContain('never restarts or replaces the worker')
expect(migration).toContain('The retired scheduler is not revived')
expect(migration).toContain('[LEGACY COMPATIBILITY]')
expect(migration).toContain('[LEGACY READ-ONLY]')
expect(migration).toContain(
'Loss of lifecycle authority does not invalidate the existing assignment, process, or filesystem work.'
)
expect(migration).toContain(
'It must not spawn, write, signal, stop, switch, focus, split, or inject a terminal.'
)
expect(migration).not.toContain('task-list --run run_legacy_local')
expect(migration).toContain('run_legacy_local is an empty audit tombstone')
expect(migration).toContain('Recovered orchestration work from a contract update')
expect(migration).toContain('run-show --id <adopted_run_id>')
expect(migration).toContain('task-list --run <adopted_run_id>')
expect(migration).toContain('Legacy inspection remains available without consuming mail')
expect(migration).toContain('run-use --id <adopted_run_id> --takeover-legacy')
expect(migration).toContain('Takeover fences only the old coordinator')
expect(migration).toContain('Live legacy workers keep their original Tasks, Dispatches')
expect(migration).toContain(
'keep the original worker as the only editor until it reaches a stable handoff point'
)
expect(migration).toContain('a conflict-free placement for any remaining work')
})
it('treats long-running worker waits as liveness checkpoints, not failures', () => {
@@ -0,0 +1,58 @@
import { spawnSync } from 'node:child_process'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import path from 'node:path'
import process from 'node:process'
const oxlintPackageDirectory = path.dirname(
createRequire(import.meta.url).resolve('oxlint/package.json')
)
const oxlintPath = path.join(oxlintPackageDirectory, 'bin', 'oxlint')
export function runOxlintPluginOnSource({
pluginName,
pluginPath,
rules,
source,
extension = 'tsx'
}) {
const directory = mkdtempSync(path.join(tmpdir(), `orca-${pluginName}-lint-`))
const sourcePath = path.join(directory, `sample.${extension}`)
const configPath = path.join(directory, 'oxlint.json')
try {
writeFileSync(sourcePath, source)
writeFileSync(
configPath,
JSON.stringify({
plugins: [],
categories: {
correctness: 'off',
suspicious: 'off',
pedantic: 'off',
perf: 'off',
style: 'off',
restriction: 'off',
nursery: 'off'
},
jsPlugins: [{ name: pluginName, specifier: pluginPath }],
rules
})
)
const result = spawnSync(
process.execPath,
[oxlintPath, '--config', configPath, '--format', 'json', sourcePath],
{ encoding: 'utf8' }
)
if (result.error) {
throw result.error
}
if (!result.stdout.trim()) {
throw new Error(result.stderr || `${pluginName} did not produce Oxlint output`)
}
return JSON.parse(result.stdout).diagnostics
} finally {
rmSync(directory, { recursive: true, force: true })
}
}
@@ -0,0 +1,64 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const projectDir = resolve(import.meta.dirname, '../..')
const prWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
const filterStep = prWorkflow.jobs['e2e-paths'].steps.find(
(step) => step.name === 'Filter E2E-relevant paths'
)
const verifyStep = prWorkflow.jobs.verify.steps.find(
(step) => step.name === 'Require successful checks'
)
describe('PR E2E gate contract', () => {
it('keeps E2E advisory while the suite is red on main', () => {
// Why: pin the deliberate choice so it reads as intentional rather than as
// the "forgot to wire the gate" bug this file originally caught. Gating on a
// suite that fails every scheduled run would block the PRs that fix it.
// Flipping to blocking means updating this expectation too — see the comment
// on verify's Require-successful-checks step for the exact wiring.
expect(prWorkflow.jobs.verify.needs).not.toContain('e2e')
expect(verifyStep.env.E2E).toBeUndefined()
expect(verifyStep.run).not.toContain('$E2E')
})
it('runs E2E only when the detector says the PR touches E2E paths', () => {
// Why: without this the job could lose its filter and run on every PR — the
// cost the path filter exists to avoid — while the gate assertions above
// stay green.
expect(prWorkflow.jobs.e2e.needs).toBe('e2e-paths')
expect(prWorkflow.jobs.e2e.if).toBe("needs.e2e-paths.outputs.should_run == 'true'")
expect(prWorkflow.jobs['e2e-paths'].outputs.should_run).toBe(
'${{ steps.filter.outputs.should_run }}'
)
})
it('enforces every job verify depends on', () => {
// Why: derive from verify.needs rather than hardcoding, so adding a required
// job without adding it to the strict loop fails here instead of silently
// leaving that job unenforced. This is what caught GIT_COMPATIBILITY and
// SHELL_CONTRACTS being absent from an earlier hardcoded list.
const strictLoop = verifyStep.run.slice(0, verifyStep.run.indexOf('done'))
for (const job of prWorkflow.jobs.verify.needs) {
const envVar = job.toUpperCase()
expect(verifyStep.env[envVar]).toBe(`\${{ needs.${job}.result }}`)
expect(strictLoop).toContain(`"$${envVar}"`)
}
})
it('matches the Playwright config where it actually lives', () => {
// Why: the config is tests/playwright.config.ts, beside tests/e2e/ rather
// than inside it. A bare `playwright.` prefix matches no tracked file, so
// editing the runner config would silently skip E2E.
expect(filterStep.run).toContain('tests/playwright\\.')
expect(filterStep.run).not.toMatch(/\(\^?\|\|]tests\/e2e\/\|playwright\\\./)
})
it('scopes detection to the PR range so base drift cannot false-trigger', () => {
expect(filterStep.run).toContain('git diff --name-only --merge-base "$BASE" "$HEAD"')
expect(filterStep.run).toContain('set -euo pipefail')
})
})
@@ -0,0 +1,86 @@
import { readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
// Why: pr.yml re-lists the `lint` chain as individual steps so a single failure
// does not mask the rest and oxlint keeps its `--format github` annotations.
// Hand-maintained mirrors drift (#10601: three verifiers never ran on PRs), so
// this gate fails the moment a `lint` step has no counterpart in pr.yml.
// Flags that only change reporting, so they must not split two otherwise identical commands.
const REPORTING_FLAGS_WITH_VALUE = new Set(['--format', '--reporter'])
const REPORTING_FLAGS = new Set(['--quiet'])
const PACKAGE_RUNNER_TOKENS = new Set(['pnpm', 'npm', 'yarn', 'npx', 'run', 'exec', 'node'])
function splitCommandChain(command) {
return command
.split(/\n|&&|;/)
.map((part) => part.trim())
.filter(Boolean)
}
function canonicalize(command) {
const tokens = command.split(/\s+/)
const canonical = []
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index]
if (canonical.length === 0 && PACKAGE_RUNNER_TOKENS.has(token)) {
continue
}
if (REPORTING_FLAGS.has(token)) {
continue
}
if (REPORTING_FLAGS_WITH_VALUE.has(token)) {
index += 1
continue
}
canonical.push(token)
}
return canonical.join(' ')
}
/** Expands `pnpm run x` indirection until every entry is a real binary invocation. */
function resolveLeafCommands(command, scripts, seen = new Set()) {
const leaves = []
for (const part of splitCommandChain(command)) {
const scriptName = part.match(/^(?:pnpm|npm|yarn)(?:\s+run)?\s+([\w:-]+)$/)?.[1]
if (scriptName && scripts[scriptName] && !seen.has(scriptName)) {
leaves.push(
...resolveLeafCommands(scripts[scriptName], scripts, new Set([...seen, scriptName]))
)
continue
}
leaves.push(canonicalize(part))
}
return leaves
}
describe('PR workflow lint parity', () => {
it('runs every `pnpm lint` step on pull requests', () => {
const { scripts } = JSON.parse(readFileSync('package.json', 'utf8'))
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
// Scan every job: which one hosts the lint steps is an organizational
// detail that has already been renamed once (verify -> static_analysis).
const workflowCommands = new Set(
Object.values(workflow.jobs)
.flatMap((job) => job.steps ?? [])
.filter((step) => typeof step.run === 'string')
.flatMap((step) => resolveLeafCommands(step.run, scripts))
)
const missing = resolveLeafCommands(scripts.lint, scripts).filter(
(leaf) => !workflowCommands.has(leaf)
)
expect(
missing,
`.github/workflows/pr.yml is missing lint steps: ${missing.join(', ')}. ` +
'Add a step for each one so PR CI matches `pnpm lint`.'
).toEqual([])
})
})
@@ -37,13 +37,19 @@ describe('PR workflow parallelism', () => {
expect(workflow.permissions).toEqual({ contents: 'read' })
})
it('shards the general test suite across sixteen runners', () => {
it('shards the general test suite across Node 24 and Node 26', () => {
expect(workflow.jobs.test.strategy.matrix.node).toEqual(['24', '26'])
expect(workflow.jobs.test.strategy.matrix.shard).toEqual(
Array.from({ length: 16 }, (_, index) => index + 1)
)
expect(workflow.jobs.test.strategy.matrix.shard_total).toEqual([16])
const testStep = workflow.jobs.test.steps.find((step) => step.name === 'Test shard')
const installStep = workflow.jobs.test.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(testStep.run).toContain('--shard=${{ matrix.shard }}/${{ strategy.job-total }}')
expect(installStep.with['node-version']).toBe('${{ matrix.node }}')
expect(testStep.run).toContain('--shard=${{ matrix.shard }}/${{ matrix.shard_total }}')
for (const testFile of nativeShellContractFiles) {
expect(testStep.run).toContain(`--exclude=${testFile}`)
}
@@ -95,9 +101,15 @@ describe('PR workflow parallelism', () => {
const steps = dependencyAction.runs.steps
const pnpmIndex = steps.findIndex((step) => step.name === 'Setup pnpm')
const nodeIndex = steps.findIndex((step) => step.name === 'Setup Node.js')
const requestedNodeIndex = steps.findIndex((step) => step.name === 'Setup requested Node.js')
expect(pnpmIndex).toBeLessThan(nodeIndex)
expect(pnpmIndex).toBeLessThan(requestedNodeIndex)
expect(steps[nodeIndex].with.cache).toBe('pnpm')
expect(steps[nodeIndex].if).toBe("inputs.node-version == ''")
expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''")
expect(steps[requestedNodeIndex].with['node-version']).toBe('${{ inputs.node-version }}')
expect(steps[requestedNodeIndex].with.cache).toBe('pnpm')
})
it('restores Electron downloads before preparing the package runtime', () => {
@@ -98,7 +98,7 @@ export async function publishCompleteDraftReleases({
for (const release of candidates) {
const tag = release.tag_name
if (!(await isDraftBuiltFromCurrentRef({ tag, release }))) {
if (!(await Promise.resolve(isDraftBuiltFromCurrentRef({ tag, release })))) {
const reason = 'tag is not built from the current release ref'
skipped.push({ tag, reason })
log(`Skipping stale RC draft release ${tag}: ${reason}`)
@@ -0,0 +1,113 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/quadratic-buffer-concat.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
pluginName: 'quadratic-buffer-concat',
pluginPath,
source,
extension: 'ts',
rules: {
'quadratic-buffer-concat/no-loop-carried-concat': 'warn'
}
})
}
const violations = [
[
'self accumulator',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([acc, chunk]) }',
'acc'
],
[
'trailing accumulator',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([chunk, acc]) }',
'acc'
],
[
'spread self accumulator',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([...acc, chunk]) }',
'acc'
],
[
'indirect stream carry',
`async function read(stream) {
let remainder = null
for await (const chunk of stream) {
const data = remainder ? Buffer.concat([remainder, chunk]) : chunk
remainder = Buffer.from(data.subarray(lineStart))
}
}`,
'remainder'
],
[
'indirect transcript carry',
`function read(fd, size) {
let carryBytes = Buffer.alloc(0)
while (bytesRead < size) {
const combined = Buffer.concat([buffer.subarray(0, n), carryBytes])
carryBytes = combined.subarray(0, firstNewline)
}
}`,
'carryBytes'
],
[
'classic for initializer',
'for (let acc = Buffer.alloc(0), i = 0; i < n; i++) { acc = Buffer.concat([acc, chunks[i]]) }',
'acc'
],
[
'class field accumulator',
'class Reader { read() { while (this.open) { this.pending = Buffer.concat([this.pending, chunk]) } } }',
'this.pending'
],
[
'guarded accumulator',
'let acc = Buffer.alloc(0); while (open) { acc = acc.length === 0 ? chunk : Buffer.concat([acc, chunk]) }',
'acc'
]
]
const accepted = [
[
'single concat after loop',
'const parts = []; for (const chunk of chunks) { parts.push(chunk) } const out = Buffer.concat(parts)'
],
[
'spread chunk list',
`let carryChunks = []
while (scanEnd > 0) {
const region = carryChunks.length === 0 ? buffer : Buffer.concat([buffer, ...carryChunks])
carryChunks = [buffer.subarray(0, firstNewline)]
}`
],
[
'iteration-local result',
'for (const group of groups) { const frame = Buffer.concat([group.header, group.body]); send(frame) }'
],
[
'iteration-local accumulator',
'for (const chunk of chunks) { let framed = HEADER; framed = Buffer.concat([framed, chunk]); send(framed) }'
],
[
'no enclosing loop',
'class S { handle(chunk) { const buffer = Buffer.concat([this.pending, chunk]); this.pending = parse(buffer).pending } }'
],
['no Buffer concat', 'export const x = 1']
]
describe('quadratic Buffer.concat Oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source, accumulator) => {
const diagnostics = lintSource(source)
expect(diagnostics).toHaveLength(1)
expect(diagnostics[0].message).toContain(accumulator)
})
it.each(accepted)('accepts %s', (_name, source) => {
expect(lintSource(source)).toEqual([])
})
})
@@ -0,0 +1,218 @@
#!/usr/bin/env node
// Benchmark: the relay's per-PTY-chunk replay buffer append (src/relay/pty-handler.ts).
//
// appendReplayBuffer did `buffered += data` then, over the cap, `buffered.slice(-CAP)`.
// Once a PTY has produced CAP bytes -- which a long-lived shell does almost immediately
// -- every subsequent chunk flattened and copied the whole 100 KB window. The append is
// called per raw node-pty emission, before batching, so it is per chunk, not per flush.
//
// The fix reuses RecentPtyOutputBuffer: keep chunks, drop from the head, and defer the
// join to read(), which only attach/adopt/revive call.
//
// Both arms are compared for an identical retained tail before timing.
//
// Run with: node config/scripts/relay-replay-buffer-benchmark.mjs
import { readFileSync } from 'node:fs'
import { performance } from 'node:perf_hooks'
const ROUNDS = 6
const SECONDS = Number(process.env.ORCA_REPLAY_BENCH_SECONDS ?? '1')
if (!Number.isFinite(SECONDS) || SECONDS <= 0) {
throw new Error(`ORCA_REPLAY_BENCH_SECONDS must be positive, received ${SECONDS}`)
}
// Why re-read the sources: the claim is that the relay now appends into a chunk deque
// with the relay's own cap. If either reverts, these numbers stop meaning what they say.
const HANDLER_SOURCE = readFileSync(
new URL('../../src/relay/pty-handler.ts', import.meta.url),
'utf8'
)
if (!/managed\.buffered\.append\(/.test(HANDLER_SOURCE)) {
throw new Error('relay no longer appends into a chunk deque; this benchmark is stale')
}
const capMatch = HANDLER_SOURCE.match(/REPLAY_BUFFER_MAX = ([\d *]+)/)
if (!capMatch) {
throw new Error('REPLAY_BUFFER_MAX not found; this benchmark is stale')
}
// The regex admits only digits, spaces, and `*`, so the literal is a plain product.
const REPLAY_BUFFER_MAX = capMatch[1]
.split('*')
.map((factor) => Number(factor.trim()))
.reduce((product, factor) => product * factor, 1)
if (!Number.isSafeInteger(REPLAY_BUFFER_MAX) || REPLAY_BUFFER_MAX <= 0) {
throw new Error(`could not read REPLAY_BUFFER_MAX from source, got ${capMatch[1]}`)
}
// Pre-fix: rolling string, re-sliced once over the cap.
function appendString(state, data) {
if (data.length === 0) {
return state
}
const next = state + data
return next.length > REPLAY_BUFFER_MAX ? next.slice(-REPLAY_BUFFER_MAX) : next
}
// Post-fix: mirrors RecentPtyOutputBuffer's append/read for the relay's options.
class ChunkDeque {
constructor(limit) {
this.chunks = []
this.headIndex = 0
this.headOffset = 0
this.totalLen = 0
this.limit = limit
}
append(data) {
if (data.length === 0) {
return
}
if (data.length >= this.limit) {
this.chunks = [data.slice(-this.limit)]
this.headIndex = 0
this.headOffset = 0
this.totalLen = this.limit
return
}
this.chunks.push(data)
this.totalLen += data.length
while (this.totalLen > this.limit) {
const headRemaining = this.chunks[this.headIndex].length - this.headOffset
const excess = this.totalLen - this.limit
if (headRemaining <= excess) {
this.chunks[this.headIndex] = ''
this.headIndex += 1
this.headOffset = 0
this.totalLen -= headRemaining
} else {
this.headOffset += excess
this.totalLen -= excess
}
}
if (this.headIndex >= 1024) {
this.chunks = this.chunks.slice(this.headIndex)
this.headIndex = 0
}
}
read() {
if (this.chunks.length - this.headIndex > 1) {
const retained = this.chunks.slice(this.headIndex)
if (this.headOffset > 0) {
retained[0] = retained[0].slice(this.headOffset)
this.headOffset = 0
}
this.chunks = [retained.join('')]
this.headIndex = 0
} else if (this.headOffset > 0) {
this.chunks[this.headIndex] = this.chunks[this.headIndex].slice(this.headOffset)
this.headOffset = 0
}
return this.chunks[this.headIndex] ?? ''
}
}
function makeChunks(chunkBytes, chunkCount) {
// Vary content so V8 cannot dedupe or treat the appends as loop-invariant.
return Array.from({ length: chunkCount }, (_value, index) =>
`${index}:`.padEnd(chunkBytes, 'abcdefghijklmnopqrstuvwxyz')
)
}
// Why pre-saturate: the interesting regime is a PTY that has already filled the window,
// which is where the old form copied 100 KB on literally every chunk. Timing from empty
// would average in a cheap warm-up the real process leaves behind in milliseconds.
function saturate(chunks) {
let stringState = ''
const deque = new ChunkDeque(REPLAY_BUFFER_MAX)
const preload = 'p'.repeat(REPLAY_BUFFER_MAX)
stringState = appendString(stringState, preload)
deque.append(preload)
return { stringState, deque, chunks }
}
function median(samples) {
const sorted = [...samples].sort((a, b) => a - b)
const mid = sorted.length / 2
return (sorted[mid - 1] + sorted[mid]) / 2
}
function timeString(chunks) {
let state = 'p'.repeat(REPLAY_BUFFER_MAX)
const start = performance.now()
for (const chunk of chunks) {
state = appendString(state, chunk)
}
const elapsed = performance.now() - start
if (state.length !== REPLAY_BUFFER_MAX) {
throw new Error('string arm lost its window')
}
return elapsed
}
function timeDeque(chunks) {
const deque = new ChunkDeque(REPLAY_BUFFER_MAX)
deque.append('p'.repeat(REPLAY_BUFFER_MAX))
const start = performance.now()
for (const chunk of chunks) {
deque.append(chunk)
}
const elapsed = performance.now() - start
return elapsed
}
// Arms alternate which one leads so within-round drift cannot favour either.
function measure(chunks) {
timeString(chunks)
timeDeque(chunks)
const stringSamples = []
const dequeSamples = []
for (let round = 0; round < ROUNDS; round += 1) {
if (round % 2 === 0) {
stringSamples.push(timeString(chunks))
dequeSamples.push(timeDeque(chunks))
} else {
dequeSamples.push(timeDeque(chunks))
stringSamples.push(timeString(chunks))
}
}
return { stringMs: median(stringSamples), dequeMs: median(dequeSamples) }
}
const pad = (value, width) => String(value).padStart(width)
console.log('Relay PTY replay-buffer append, per second of output. Lower is better.')
console.log(
`cap=${(REPLAY_BUFFER_MAX / 1024).toFixed(0)} KiB rounds=${ROUNDS} (per-arm medians, pre-saturated)`
)
console.log(
`${pad('workload', 30)} ${pad('rolling str', 12)} ${pad('chunk deque', 12)} ${pad('speedup', 9)}`
)
for (const [label, chunkBytes, chunksPerSecond] of [
['interactive shell 64B x200', 64, 200],
['agent TUI 512B x400', 512, 400],
['build log 4KiB x256 (1 MiB/s)', 4 * 1024, 256],
['dump 8KiB x512 (4 MiB/s)', 8 * 1024, 512],
['firehose 16KiB x1024 (16 MiB/s)', 16 * 1024, 1024]
]) {
const chunks = makeChunks(chunkBytes, Math.round(chunksPerSecond * SECONDS))
const { stringState, deque } = saturate(chunks)
let stringTail = stringState
for (const chunk of chunks) {
stringTail = appendString(stringTail, chunk)
deque.append(chunk)
}
if (deque.read() !== stringTail) {
throw new Error(`retained tail differs for ${label}`)
}
if (stringTail.length !== REPLAY_BUFFER_MAX) {
throw new Error(`fixture never saturated the window for ${label}`)
}
const { stringMs, dequeMs } = measure(chunks)
console.log(
`${pad(label, 30)} ${pad(`${stringMs.toFixed(3)} ms`, 12)} ${pad(`${dequeMs.toFixed(3)} ms`, 12)} ${pad(`${(stringMs / dequeMs).toFixed(0)}x`, 9)}`
)
}
console.log(
"\nThis is per PTY, and the relay runs on the user's SSH host. Reads (attach, adopt,\nrevive) now pay the join instead, but those are rare and were already O(window)."
)
@@ -0,0 +1,37 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const projectDir = resolve(import.meta.dirname, '../..')
const releaseWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8')
)
const e2eWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/e2e.yml'), 'utf8'))
describe('release E2E dispatch contract', () => {
it('dispatches tag-scoped E2E only after publication', () => {
const dispatchJob = releaseWorkflow.jobs['post-release-e2e']
const dispatchStep = dispatchJob.steps.find((step) => step.name === 'Dispatch tag-scoped E2E')
expect(releaseWorkflow.jobs.e2e).toBeUndefined()
expect(dispatchJob.needs).toEqual(['cut', 'publish-release'])
expect(dispatchJob.if).toBe("${{ needs.cut.outputs.tag != '' }}")
expect(dispatchJob.permissions.actions).toBe('write')
expect(dispatchStep.env.TAG).toBe('${{ needs.cut.outputs.tag }}')
expect(dispatchStep.run).toContain('gh workflow run e2e.yml')
expect(dispatchStep.run).toContain('--ref "$TAG"')
expect(dispatchStep.run).toContain('--raw-field "ref=refs/tags/$TAG"')
expect(dispatchStep.run).toContain('for attempt in 1 2 3')
expect(dispatchStep.run).toContain('[[ "$attempt" -eq 3 ]] || sleep')
expect(dispatchStep.run).toContain('::warning::Failed to dispatch post-release E2E')
})
it('keeps detached E2E identifiable and manually dispatchable by ref', () => {
const refInput = e2eWorkflow.on.workflow_dispatch.inputs.ref
expect(e2eWorkflow['run-name']).toBe('E2E ${{ inputs.ref || github.ref }}')
expect(refInput.type).toBe('string')
expect(refInput.required).toBe(false)
})
})
@@ -0,0 +1,116 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { plainClassName } from '../oxlint-plugins/renderer-scrollbar-style.mjs'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/renderer-scrollbar-style.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
pluginName: 'renderer-scrollbar-style',
pluginPath,
source,
rules: {
'renderer-scrollbar-style/require-styled-vertical-scrollbar': 'warn'
}
})
}
const violations = [
['unstyled class', 'export const X = () => <div className="max-h-64 overflow-y-auto" />'],
[
'unstyled suffix-important class',
'export const X = () => <div className="max-h-64 overflow-y-auto!" />'
],
[
'unknown scrollbar class',
'export const X = () => <div className="overflow-auto scrollbar-none" />'
],
[
'separate class composer arguments',
"export const X = () => <div className={cn('overflow-y-auto', 'scrollbar-sleek')} />"
],
[
'conditional scrollbar',
"export const X = ({ enabled }) => <div className={cn('overflow-y-auto', enabled && 'scrollbar-sleek')} />"
],
[
'arbitrary class wrapper',
"export const X = () => <div className={identity('overflow-y-auto')} />"
],
[
'mismatched responsive variants',
'export const X = () => <div className="overflow-y-auto md:scrollbar-sleek" />'
],
['inline overflow', "export const X = () => <div style={{ overflowY: 'auto' }} />"],
[
'logical inline style spread',
"export const X = ({ open }) => <div style={{ ...(open && { overflowY: 'auto' }) }} />"
],
['JSX spread class', "export const X = () => <div {...{ className: 'overflow-y-auto' }} />"],
[
'later spread override',
'export const X = () => <div className="scrollbar-sleek" {...{ className: \'overflow-y-auto\' }} />'
]
]
const accepted = [
[
'styled vertical class',
'export const X = () => <div className="overflow-auto scrollbar-sleek" />'
],
[
'styled suffix-important classes',
'export const X = () => <div className="overflow-y-auto! scrollbar-sleek!" />'
],
[
'same composer literal',
"export const X = () => <div className={cn('overflow-y-auto scrollbar-sleek')} />"
],
[
'same conditional literal',
"export const X = ({ enabled }) => <div className={cn(enabled && 'overflow-y-auto scrollbar-sleek')} />"
],
['horizontal-only overflow', 'export const X = () => <pre className="overflow-x-auto" />'],
[
'matching responsive variants',
'export const X = () => <div className="md:overflow-y-auto md:scrollbar-sleek" />'
],
[
'unconditional scrollbar',
'export const X = () => <div className="md:overflow-y-auto scrollbar-sleek" />'
],
[
'styled inline overflow',
'export const X = () => <div className="scrollbar-editor" style={{ overflow: \'auto\' }} />'
],
[
'styled JSX spread class',
"export const X = () => <div {...{ className: 'overflow-y-auto scrollbar-sleek' }} />"
],
[
'variant configuration',
"export const X = () => <div className={buttonVariants({ className: 'overflow-y-auto scrollbar-sleek' })} />"
]
]
describe('renderer scrollbar style Oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source) => {
expect(lintSource(source)).toHaveLength(1)
})
it.each(accepted)('accepts %s', (_name, source) => {
expect(lintSource(source)).toEqual([])
})
it.each([
['md:overflow-y-auto', 'overflow-y-auto'],
['[&:hover]:overflow-y-auto', 'overflow-y-auto'],
['md:!scrollbar-editor', 'scrollbar-editor'],
['!scrollbar-editor', 'scrollbar-editor'],
['overflow-y-auto!', 'overflow-y-auto'],
['md:scrollbar-editor!', 'scrollbar-editor']
])('normalizes %s', (token, expected) => {
expect(plainClassName(token)).toBe(expected)
})
})
@@ -0,0 +1,202 @@
#!/usr/bin/env node
// Measures the complete ProseMirror doc traversal used by the two doc-link plugins.
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
import { Schema } from '@tiptap/pm/model'
import {
canHoldDocLink,
DOC_LINK_PATTERN,
isDocLinkLiteralCodeTextNode
} from '../../src/renderer/src/components/editor/rich-markdown-doc-link-scan.ts'
const REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url))
const ITERATIONS = Number(process.env.ORCA_DOC_LINK_BENCH_ITERATIONS ?? '41')
const WARMUP_ITERATIONS = Math.min(9, ITERATIONS)
if (!Number.isSafeInteger(ITERATIONS) || ITERATIONS <= 0) {
throw new Error(`ORCA_DOC_LINK_BENCH_ITERATIONS must be a positive integer, got ${ITERATIONS}`)
}
const schema = new Schema({
nodes: {
doc: { content: 'paragraph+' },
paragraph: { content: 'text*' },
text: { group: 'inline' }
}
})
function walkUngated(doc) {
let matches = 0
let visited = 0
doc.descendants((node, _pos, parent) => {
visited += 1
if (node.type.name !== 'text' || !node.text || isDocLinkLiteralCodeTextNode(node, parent)) {
return
}
for (const _match of node.text.matchAll(DOC_LINK_PATTERN)) {
matches += 1
}
})
return { matches, visited }
}
function walkGated(doc) {
let matches = 0
let visited = 0
doc.descendants((node, _pos, parent) => {
visited += 1
if (!canHoldDocLink(node, parent)) {
return
}
for (const _match of node.text.matchAll(DOC_LINK_PATTERN)) {
matches += 1
}
})
return { matches, visited }
}
function countMatches(source) {
let matches = 0
for (const _match of source.matchAll(DOC_LINK_PATTERN)) {
matches += 1
}
return matches
}
function loadDocs() {
const files = execFileSync('git', ['ls-files', '*.md', 'docs/*.md'], {
cwd: REPO_ROOT,
maxBuffer: 256 * 1024 * 1024
})
.toString()
.split('\n')
.filter(Boolean)
const docs = []
for (const file of files) {
try {
const source = readFileSync(join(REPO_ROOT, file), 'utf8')
const lines = source.split('\n').filter(Boolean)
if (lines.length > 0) {
docs.push({ file, lines, size: source.length, matches: countMatches(source) })
}
} catch {
// Indexed paths can disappear while the benchmark is running.
}
}
return docs
}
function createFixture(doc, nonce) {
const paragraphs = doc.lines.map((line, index) => {
const text = index === 0 ? `${line} bench-${nonce}` : line
return schema.node('paragraph', null, text ? schema.text(text) : undefined)
})
return schema.node('doc', null, paragraphs)
}
function median(samples) {
const sorted = [...samples].sort((a, b) => a - b)
return sorted[Math.floor(sorted.length / 2)]
}
function measureCorpus(docs) {
const samples = { ungated: [], gated: [] }
const totals = {
ungated: { matches: 0, visited: 0 },
gated: { matches: 0, visited: 0 }
}
const seenFixtures = new WeakSet()
let expectedMatches = 0
let expectedVisited = 0
const measuredOrder = []
for (let round = -WARMUP_ITERATIONS; round < ITERATIONS; round += 1) {
const doc = docs[(round + WARMUP_ITERATIONS) % docs.length]
const measured = round >= 0
const order =
(round + WARMUP_ITERATIONS) % 2 === 0 ? ['ungated', 'gated'] : ['gated', 'ungated']
const fixtures = {
ungated: createFixture(doc, String(round)),
gated: createFixture(doc, String(round))
}
for (const arm of order) {
const fixture = fixtures[arm]
if (seenFixtures.has(fixture)) {
throw new Error('timed fixture was reused')
}
seenFixtures.add(fixture)
const start = performance.now()
const result = arm === 'ungated' ? walkUngated(fixture) : walkGated(fixture)
const elapsed = performance.now() - start
if (measured) {
samples[arm].push(elapsed)
totals[arm].matches += result.matches
totals[arm].visited += result.visited
measuredOrder.push(arm)
}
}
if (measured) {
expectedMatches += doc.matches
expectedVisited += doc.lines.length * 2
}
}
if (totals.ungated.matches !== expectedMatches || totals.gated.matches !== expectedMatches) {
throw new Error(
`gate changed matches: expected ${expectedMatches}, ungated ${totals.ungated.matches}, gated ${totals.gated.matches}`
)
}
if (totals.ungated.visited !== expectedVisited || totals.gated.visited !== expectedVisited) {
throw new Error(
`full traversal result was not consumed: expected ${expectedVisited}, ungated ${totals.ungated.visited}, gated ${totals.gated.visited}`
)
}
for (let index = 0; index < measuredOrder.length; index += 2) {
const pair = measuredOrder.slice(index, index + 2).join(',')
const previousPair = index === 0 ? null : measuredOrder.slice(index - 2, index).join(',')
if (!['ungated,gated', 'gated,ungated'].includes(pair) || pair === previousPair) {
throw new Error('benchmark arms were not interleaved in alternating order')
}
}
return { ungated: median(samples.ungated), gated: median(samples.gated) }
}
const docs = loadDocs()
if (docs.length === 0) {
throw new Error('no markdown files found in the index')
}
const large = docs.filter((doc) => doc.size > 3000)
const biggest = docs.reduce((a, b) => (b.size > a.size ? b : a))
const pad = (value, width) => String(value).padStart(width)
console.log('Doc-link ProseMirror traversal, per editor transaction. Lower is better.')
console.log(
`docs=${docs.length} (>3KB: ${large.length}) iterations=${ITERATIONS} (interleaved median)`
)
console.log(
`${pad('corpus', 26)} ${pad('ungated', 11)} ${pad('gated', 11)} ${pad('delta', 11)} ${pad('speedup', 9)}`
)
for (const [label, set] of [
['all repo markdown', docs],
['docs over 3 KB', large],
[`biggest (${biggest.file.split('/').pop()})`, [biggest]]
]) {
if (set.length === 0) {
console.log(`${pad(label, 26)} ${pad('no docs in this corpus — skipped', 44)}`)
continue
}
const { ungated, gated } = measureCorpus(set)
const delta = ungated - gated
console.log(
`${pad(label, 26)} ${pad(`${(ungated * 1000).toFixed(1)} us`, 11)} ${pad(`${(gated * 1000).toFixed(1)} us`, 11)} ${pad(`${(delta * 1000).toFixed(1)} us`, 11)} ${pad(`${(ungated / gated).toFixed(2)}x`, 9)}`
)
}
console.log(
'\nAuto-conversion pays this once per keystroke; preview decorations pay it once\nper keystroke and once per caret move.'
)
+12 -4
View File
@@ -128,10 +128,8 @@ function prepareMacDevElectronApp() {
const title = process.env.ORCA_DEV_DOCK_TITLE || 'Orca: dev'
const identityKey = process.env.ORCA_DEV_INSTANCE_KEY || repoRoot
// v6: bundle the notification-status helper (real permission readout) and
// ad-hoc re-sign after plist edits so Notification Center accepts the
// bundle; bumping forces stale cached copies to be recreated.
const bundleLayoutVersion = 'dock-title-app-preserve-framework-symlinks-v6'
// v7: give the terminal daemon helper an Orca-specific TCC identity.
const bundleLayoutVersion = 'dock-title-app-preserve-framework-symlinks-v7'
const hash = createHash('sha1')
.update(
`${sourceAppPath}\0${electronVersion ?? ''}\0${title}\0${identityKey}\0${bundleLayoutVersion}`
@@ -155,6 +153,7 @@ function prepareMacDevElectronApp() {
// Electron drops clicks for notification ids it didn't create, so the
// click is lost, not misdirected.
const bundleId = 'com.stablyai.orca.dev'
const helperBundleId = `${bundleId}.helper`
process.env.ORCA_DEV_MACOS_BUNDLE_ID = bundleId
const expectedMarker = JSON.stringify(
{ title, appBundleName, bundleId, sourceAppPath, electronVersion, bundleLayoutVersion },
@@ -205,9 +204,18 @@ function prepareMacDevElectronApp() {
restoreElectronFrameworkSymlinks(appPath)
const plistPath = path.join(appPath, 'Contents', 'Info.plist')
const helperPlistPath = path.join(
appPath,
'Contents',
'Frameworks',
'Electron Helper.app',
'Contents',
'Info.plist'
)
setPlistValue(plistPath, 'CFBundleName', title)
setPlistValue(plistPath, 'CFBundleDisplayName', title)
setPlistValue(plistPath, 'CFBundleIdentifier', bundleId)
setPlistValue(helperPlistPath, 'CFBundleIdentifier', helperBundleId)
// Why: the notification-status helper reads the app's real macOS
// notification authorization (UNUserNotificationCenter has no Electron
+1 -1
View File
@@ -572,7 +572,7 @@ async function main() {
if (options.output) {
mkdirSync(path.dirname(path.resolve(options.output)), { recursive: true })
writeFileSync(options.output, `${JSON.stringify(report, null, 2)}\n`)
console.log(`[idle-cpu] wrote ${options.output}`)
console.log(`[idle-cpu] wrote ${String(options.output)}`)
}
console.log(
JSON.stringify(
@@ -0,0 +1,45 @@
import { spawnSync } from 'node:child_process'
const rawExtraArgs = process.argv.slice(2)
const extraArgs = rawExtraArgs[0] === '--' ? rawExtraArgs.slice(1) : rawExtraArgs
const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const env = {
...process.env,
ORCA_E2E_SSH_DOCKER: '1'
}
// Why: Node's CVE-2024-27980 hardening rejects .cmd spawns without shell on Windows.
const spawnOptions = {
stdio: 'inherit',
env,
shell: process.platform === 'win32'
}
const runtime = spawnSync(pnpm, ['run', 'ensure:electron-runtime'], spawnOptions)
if (runtime.status !== 0) {
process.exit(runtime.status ?? 1)
}
// Why both specs in one runner: they share the docker SSH rig and the same
// ORCA_E2E_SSH_DOCKER gate — the SSH parking spec proves SSH panes park and
// restore, the retention spec proves the C1 budget bounds what they retain.
const result = spawnSync(
pnpm,
[
'exec',
'playwright',
'test',
'tests/e2e/ssh-terminal-parking.spec.ts',
'tests/e2e/terminal-retention-budget.spec.ts',
'--config',
'tests/playwright.config.ts',
'--project',
'electron-headless',
'--workers=1',
...extraArgs
],
spawnOptions
)
process.exit(result.status ?? 1)
@@ -0,0 +1,306 @@
import { spawn, spawnSync } from 'node:child_process'
import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
const projectDir = path.resolve(import.meta.dirname, '../..')
const scriptPath = import.meta.filename
const insideSessionFlag = '--inside-session'
const processStopTimeoutMs = 5_000
const processKillTimeoutMs = 1_000
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}
function waitForExit(child) {
return new Promise((resolve, reject) => {
child.once('error', reject)
child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)))
})
}
function processGroupMembers(processGroupId) {
const result = spawnSync('ps', ['-o', 'pid=,ppid=,pgid=,comm=', '-g', String(processGroupId)], {
encoding: 'utf8'
})
if (result.status !== 0) {
return []
}
return result.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
}
async function stopOwnedProcessGroup(processGroupId) {
let members = processGroupMembers(processGroupId)
if (members.length === 0) {
return []
}
console.error(
`[terminal-ime] stopping owned process group ${processGroupId}: ${members.join('; ')}`
)
try {
process.kill(-processGroupId, 'SIGTERM')
} catch (error) {
if (error?.code !== 'ESRCH') {
throw error
}
}
const deadline = Date.now() + processStopTimeoutMs
while (Date.now() < deadline) {
members = processGroupMembers(processGroupId)
if (members.length === 0) {
return []
}
await delay(100)
}
try {
process.kill(-processGroupId, 'SIGKILL')
} catch (error) {
if (error?.code !== 'ESRCH') {
throw error
}
}
const killDeadline = Date.now() + processKillTimeoutMs
do {
members = processGroupMembers(processGroupId)
if (members.length === 0) {
return []
}
await delay(100)
} while (Date.now() < killDeadline)
return members
}
function commandOutput(command, args) {
const result = spawnSync(command, args, { encoding: 'utf8' })
return result.status === 0 ? result.stdout.trim() : result.stderr.trim()
}
function configureHangulEngine() {
for (const [key, value] of [
['initial-input-mode', 'hangul'],
['hangul-keyboard', '2']
]) {
const result = spawnSync(
'gsettings',
['set', 'org.freedesktop.ibus.engine.hangul', key, value],
{ encoding: 'utf8' }
)
if (result.status !== 0) {
throw new Error(`Failed to configure IBus Hangul ${key}: ${result.stderr.trim()}`)
}
}
}
async function waitForHangulEngine(ibusProcess) {
const deadline = Date.now() + 15_000
while (Date.now() < deadline) {
if (ibusProcess.exitCode !== null) {
throw new Error(`ibus-daemon exited early with code ${ibusProcess.exitCode}`)
}
const result = spawnSync('ibus', ['engine', 'hangul'], { stdio: 'pipe' })
if (result.status === 0) {
return
}
await delay(100)
}
throw new Error('Timed out while selecting the IBus Hangul engine')
}
async function runInsideSession(evidenceDir) {
const ibusLogPath = path.join(evidenceDir, 'ibus-daemon.log')
const ibusLogFd = openSync(ibusLogPath, 'w')
const windowManagerLogPath = path.join(evidenceDir, 'xfwm4.log')
const windowManagerLogFd = openSync(windowManagerLogPath, 'w')
const evidence = {
display: process.env.DISPLAY ?? null,
ibusDaemonPid: null,
ibusGroupBeforeCleanup: [],
ibusGroupAfterCleanup: [],
playwrightPid: null,
windowManagerPid: null,
windowManagerGroupAfterCleanup: []
}
let ibusProcess
let windowManagerProcess
let testExitCode = 1
try {
configureHangulEngine()
windowManagerProcess = spawn('xfwm4', ['--compositor=off'], {
detached: true,
env: process.env,
stdio: ['ignore', windowManagerLogFd, windowManagerLogFd]
})
if (!windowManagerProcess.pid) {
throw new Error('xfwm4 did not return a PID')
}
evidence.windowManagerPid = windowManagerProcess.pid
console.error(`[terminal-ime] started xfwm4 PID ${windowManagerProcess.pid}`)
ibusProcess = spawn(
'ibus-daemon',
['--xim', '--verbose', '--panel=disable', '--emoji-extension=disable'],
{
detached: true,
env: process.env,
stdio: ['ignore', ibusLogFd, ibusLogFd]
}
)
if (!ibusProcess.pid) {
throw new Error('ibus-daemon did not return a PID')
}
evidence.ibusDaemonPid = ibusProcess.pid
console.error(`[terminal-ime] started ibus-daemon PID ${ibusProcess.pid}`)
await waitForHangulEngine(ibusProcess)
console.error(`[terminal-ime] IBus version: ${commandOutput('ibus', ['version'])}`)
console.error(`[terminal-ime] IBus engine: ${commandOutput('ibus', ['engine'])}`)
console.error(
`[terminal-ime] Hangul initial mode: ${commandOutput('gsettings', [
'get',
'org.freedesktop.ibus.engine.hangul',
'initial-input-mode'
])}`
)
console.error(
`[terminal-ime] Hangul keyboard: ${commandOutput('gsettings', [
'get',
'org.freedesktop.ibus.engine.hangul',
'hangul-keyboard'
])}`
)
evidence.ibusGroupBeforeCleanup = processGroupMembers(ibusProcess.pid)
console.error(`[terminal-ime] owned IBus group: ${evidence.ibusGroupBeforeCleanup.join('; ')}`)
const testProcess = spawn(
process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
[
'run',
'test:e2e:headful',
'--workers=1',
'--',
'tests/e2e/terminal-ibus-hangul-native.spec.ts'
],
{
cwd: projectDir,
env: {
...process.env,
ORCA_E2E_FORWARD_APP_LOGS: '1',
ORCA_E2E_NATIVE_IBUS_HANGUL: '1'
},
stdio: 'inherit'
}
)
if (!testProcess.pid) {
throw new Error('Playwright did not return a PID')
}
evidence.playwrightPid = testProcess.pid
console.error(`[terminal-ime] started Playwright PID ${testProcess.pid}`)
testExitCode = await waitForExit(testProcess)
} finally {
if (ibusProcess?.pid) {
evidence.ibusGroupBeforeCleanup = processGroupMembers(ibusProcess.pid)
evidence.ibusGroupAfterCleanup = await stopOwnedProcessGroup(ibusProcess.pid)
}
if (windowManagerProcess?.pid) {
evidence.windowManagerGroupAfterCleanup = await stopOwnedProcessGroup(
windowManagerProcess.pid
)
}
closeSync(ibusLogFd)
closeSync(windowManagerLogFd)
mkdirSync(path.join(projectDir, 'test-results'), { recursive: true })
copyFileSync(
ibusLogPath,
path.join(projectDir, 'test-results', 'terminal-ibus-hangul-native-ibus.log')
)
copyFileSync(
windowManagerLogPath,
path.join(projectDir, 'test-results', 'terminal-ibus-hangul-native-xfwm4.log')
)
writeFileSync(
path.join(projectDir, 'test-results', 'terminal-ibus-hangul-native-processes.json'),
`${JSON.stringify(evidence, null, 2)}\n`
)
}
if (evidence.ibusGroupAfterCleanup.length > 0) {
throw new Error(
`Owned IBus processes survived cleanup: ${evidence.ibusGroupAfterCleanup.join('; ')}`
)
}
if (evidence.windowManagerGroupAfterCleanup.length > 0) {
throw new Error(
`Owned window-manager processes survived cleanup: ${evidence.windowManagerGroupAfterCleanup.join('; ')}`
)
}
return testExitCode
}
async function runOuter() {
if (process.platform !== 'linux') {
throw new Error('The native IBus Hangul E2E runner requires Linux/X11')
}
const evidenceDir = mkdtempSync(path.join(os.tmpdir(), 'orca-terminal-ime-e2e-'))
const runtimeDir = path.join(evidenceDir, 'runtime')
mkdirSync(runtimeDir, { mode: 0o700 })
mkdirSync(path.join(evidenceDir, 'config'))
mkdirSync(path.join(evidenceDir, 'cache'))
console.error(`[terminal-ime] evidence directory: ${evidenceDir}`)
const sessionProcess = spawn(
'xvfb-run',
[
'--auto-servernum',
'dbus-run-session',
'--',
process.execPath,
scriptPath,
insideSessionFlag,
evidenceDir
],
{
cwd: projectDir,
detached: true,
env: {
...process.env,
GTK_IM_MODULE: 'ibus',
IBUS_ENABLE_SYNC_MODE: '1',
LANG: process.env.LANG || 'C.UTF-8',
QT_IM_MODULE: 'ibus',
XDG_CACHE_HOME: path.join(evidenceDir, 'cache'),
XDG_CONFIG_HOME: path.join(evidenceDir, 'config'),
XDG_RUNTIME_DIR: runtimeDir,
XMODIFIERS: '@im=ibus'
},
stdio: 'inherit'
}
)
if (!sessionProcess.pid) {
throw new Error('xvfb-run did not return a PID')
}
console.error(`[terminal-ime] started isolated X11 session PID ${sessionProcess.pid}`)
const exitCode = await waitForExit(sessionProcess)
const remaining = await stopOwnedProcessGroup(sessionProcess.pid)
if (remaining.length > 0) {
throw new Error(`Owned X11 session processes survived cleanup: ${remaining.join('; ')}`)
}
return exitCode
}
const insideSession = process.argv[2] === insideSessionFlag
try {
if (insideSession && !process.argv[3]) {
throw new Error(`${insideSessionFlag} requires an evidence directory argument`)
}
process.exitCode = insideSession ? await runInsideSession(process.argv[3]) : await runOuter()
} catch (error) {
console.error(`[terminal-ime] ${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
}
@@ -1,312 +0,0 @@
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
import ts from 'typescript-api'
const STYLED_SCROLLBAR_CLASSES = new Set(
'scrollbar-sleek scrollbar-editor worktree-sidebar-scrollbar'.split(' ')
)
// Why: vertical scroll is where native scrollbar drift keeps recurring. The
// guard intentionally ignores horizontal-only overflow.
const VERTICAL_SCROLL_CLASSES = new Set(
'overflow-auto overflow-scroll overflow-y-auto overflow-y-scroll'.split(' ')
)
const VERTICAL_SCROLL_STYLE_VALUES = new Set(['auto', 'scroll'])
export function plainClassName(token) {
const normalizedToken = token.startsWith('!') ? token.slice(1) : token
const parts = []
let bracketDepth = 0
let currentPart = ''
for (const char of normalizedToken) {
if (char === '[') {
bracketDepth += 1
} else if (char === ']') {
bracketDepth = Math.max(0, bracketDepth - 1)
}
if (char === ':' && bracketDepth === 0) {
parts.push(currentPart)
currentPart = ''
continue
}
currentPart += char
}
parts.push(currentPart)
const className = parts.at(-1) ?? ''
return className.startsWith('!') ? className.slice(1) : className
}
function classTokenParts(token) {
const variants = []
let bracketDepth = 0
let currentPart = ''
for (const char of token.startsWith('!') ? token.slice(1) : token) {
if (char === '[') {
bracketDepth += 1
} else if (char === ']') {
bracketDepth = Math.max(0, bracketDepth - 1)
}
if (char === ':' && bracketDepth === 0) {
variants.push(currentPart)
currentPart = ''
continue
}
currentPart += char
}
return { className: plainClassName(token), variants: variants.filter(Boolean) }
}
function classTokens(text) {
return text.split(/\s+/).filter(Boolean).map(classTokenParts)
}
function sameVariants(left, right) {
return left.length === right.length && left.every((variant, index) => variant === right[index])
}
function literalHasScrollbarForVertical(text, verticalToken) {
return classTokens(text).some((candidate) => {
if (!STYLED_SCROLLBAR_CLASSES.has(candidate.className)) {
return false
}
return (
candidate.variants.length === 0 || sameVariants(candidate.variants, verticalToken.variants)
)
})
}
function uncoveredVerticalClass(text) {
return classTokens(text).find((token) => {
return (
VERTICAL_SCROLL_CLASSES.has(token.className) && !literalHasScrollbarForVertical(text, token)
)
})
}
function reportAt(node, filePath, sourceFile, text) {
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
return {
filePath,
line: position.line + 1,
column: position.character + 1,
text
}
}
function stringLiteralTexts(node) {
if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
return [node.text]
}
if (!ts.isTemplateExpression(node)) {
return []
}
return [node.head.text, ...node.templateSpans.map((span) => span.literal.text)]
}
function collectClassLiteralReports(node, filePath, sourceFile) {
const reports = []
function visit(current) {
for (const text of stringLiteralTexts(current)) {
const uncovered = uncoveredVerticalClass(text)
if (uncovered) {
reports.push(reportAt(current, filePath, sourceFile, uncovered.className))
}
}
ts.forEachChild(current, visit)
}
visit(node)
return reports
}
function expressionHasStyledScrollbarLiteral(node) {
let hasStyledScrollbar = false
function visit(current) {
if (hasStyledScrollbar) {
return
}
if (
stringLiteralTexts(current).some((text) =>
classTokens(text).some((token) => STYLED_SCROLLBAR_CLASSES.has(token.className))
)
) {
hasStyledScrollbar = true
return
}
// Why: a scrollbar literal that only renders on some branches must not be
// treated as covering an unconditional inline overflow. Skip conditional
// and short-circuit expressions when proving unconditional coverage.
if (ts.isConditionalExpression(current)) {
return
}
if (
ts.isBinaryExpression(current) &&
(current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
current.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)
) {
return
}
ts.forEachChild(current, visit)
}
visit(node)
return hasStyledScrollbar
}
function propertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) {
return name.text
}
if (ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression)) {
return name.expression.text
}
return undefined
}
function styleValueIsVerticalScroll(propertyName, value) {
const parts = value.trim().toLowerCase().split(/\s+/).filter(Boolean)
if (parts.length === 0) {
return false
}
if (propertyName === 'overflowY' || propertyName === 'overflow-y') {
return VERTICAL_SCROLL_STYLE_VALUES.has(parts[0])
}
if (propertyName !== 'overflow') {
return false
}
const verticalValue = parts.length > 1 ? parts[1] : parts[0]
return VERTICAL_SCROLL_STYLE_VALUES.has(verticalValue)
}
function collectStyleReports(node, filePath, sourceFile) {
const reports = []
function visit(current) {
if (ts.isPropertyAssignment(current)) {
const propertyName = propertyNameText(current.name)
for (const value of propertyName ? stringLiteralTexts(current.initializer) : []) {
if (styleValueIsVerticalScroll(propertyName, value)) {
reports.push(reportAt(current, filePath, sourceFile, 'inline vertical scroll'))
}
}
ts.forEachChild(current.initializer, visit)
return
}
ts.forEachChild(current, visit)
}
visit(node)
return reports
}
function jsxAttributeName(attribute) {
return ts.isIdentifier(attribute.name) ? attribute.name.text : undefined
}
function jsxAttributeExpression(attribute) {
if (ts.isStringLiteral(attribute.initializer)) {
return attribute.initializer
}
if (attribute.initializer && ts.isJsxExpression(attribute.initializer)) {
return attribute.initializer.expression
}
return undefined
}
function spreadPropExpressions(node, propName) {
if (
ts.isParenthesizedExpression(node) ||
ts.isAsExpression(node) ||
ts.isSatisfiesExpression(node)
) {
return spreadPropExpressions(node.expression, propName)
}
if (ts.isConditionalExpression(node)) {
return [
...spreadPropExpressions(node.whenTrue, propName),
...spreadPropExpressions(node.whenFalse, propName)
]
}
if (ts.isBinaryExpression(node)) {
return [
...spreadPropExpressions(node.left, propName),
...spreadPropExpressions(node.right, propName)
]
}
if (!ts.isObjectLiteralExpression(node)) {
return []
}
return node.properties.flatMap((property) => {
if (ts.isSpreadAssignment(property)) {
return spreadPropExpressions(property.expression, propName)
}
if (ts.isPropertyAssignment(property) && propertyNameText(property.name) === propName) {
return [property.initializer]
}
return []
})
}
function jsxElementReports(node, filePath, sourceFile) {
const reports = []
let classExpression
const styleExpressions = []
for (const attribute of node.attributes.properties) {
if (ts.isJsxSpreadAttribute(attribute)) {
// Why: at runtime React applies attributes in source order, so a later
// spread that supplies className overrides an earlier explicit className.
const spreadClassExpression = spreadPropExpressions(attribute.expression, 'className').at(-1)
if (spreadClassExpression) {
classExpression = spreadClassExpression
}
styleExpressions.push(...spreadPropExpressions(attribute.expression, 'style'))
} else if (jsxAttributeName(attribute) === 'className') {
classExpression = jsxAttributeExpression(attribute)
} else if (jsxAttributeName(attribute) === 'style') {
const expression = jsxAttributeExpression(attribute)
if (expression) {
styleExpressions.push(expression)
}
}
}
if (classExpression) {
reports.push(...collectClassLiteralReports(classExpression, filePath, sourceFile))
}
if (classExpression && expressionHasStyledScrollbarLiteral(classExpression)) {
return reports
}
for (const expression of styleExpressions) {
reports.push(...collectStyleReports(expression, filePath, sourceFile))
}
return reports
}
export function reportUnstyledScrollbars(filePath, sourceText) {
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX
)
const reports = []
function visit(node) {
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
reports.push(...jsxElementReports(node, filePath, sourceFile))
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return reports
}
@@ -0,0 +1,2 @@
export const BUILD_IDENTITY_RE = /\b(?:const|let|var)\s+BUILD_IDENTITY\s*=\s*"(rc|stable)"/
export const WRITE_KEY_RE = /\b(?:const|let|var)\s+WRITE_KEY\s*=\s*"(phc_[A-Za-z0-9_-]+)"/
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { BUILD_IDENTITY_RE, WRITE_KEY_RE } from './telemetry-bundle-constant-patterns.mjs'
describe('telemetry bundle constant patterns', () => {
it.each(['const', 'let', 'var'])('accepts %s declarations', (declaration) => {
expect(`${declaration} BUILD_IDENTITY = "rc"`).toMatch(BUILD_IDENTITY_RE)
expect(`${declaration} WRITE_KEY = "phc_example-key_123"`).toMatch(WRITE_KEY_RE)
})
it('rejects assignments and invalid values', () => {
expect('BUILD_IDENTITY = "rc"').not.toMatch(BUILD_IDENTITY_RE)
expect('const BUILD_IDENTITY = "dev"').not.toMatch(BUILD_IDENTITY_RE)
expect('const WRITE_KEY = null').not.toMatch(WRITE_KEY_RE)
expect('const WRITE_KEY = "example-key"').not.toMatch(WRITE_KEY_RE)
})
})
@@ -1,6 +1,54 @@
#!/usr/bin/env node
// Mirrors the complete pre/post stripTerminalControl paths at their production size caps.
// Compares legacy, slice-run, and production-adaptive control stripping.
import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import nodeModule from 'node:module'
import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
if (!process.execArgv.includes('--experimental-transform-types')) {
const result = spawnSync(
process.execPath,
['--experimental-transform-types', '--no-warnings', import.meta.filename],
{ stdio: 'inherit' }
)
process.exit(result.status ?? 1)
}
nodeModule.registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier.startsWith('.') && !/\.[cm]?[jt]s$/.test(specifier) && context.parentURL) {
const candidate = new URL(`${specifier}.ts`, context.parentURL)
if (existsSync(fileURLToPath(candidate))) {
return { url: candidate.href, shortCircuit: true }
}
}
return nextResolve(specifier, context)
}
})
const PRODUCTION_SOURCE = readFileSync(
new URL('../../src/shared/terminal-control-stripping.ts', import.meta.url),
'utf8'
)
for (const marker of [
'export function stripTerminalControl(data: string): string',
'strippedInBlock === CONTROL_DENSITY_FALLBACK_COUNT',
'output += withoutAnsi.slice(runStart, index)',
// Retuning either density constant changes which fixtures sit above/below the trigger, so
// pin the literals rather than the names: a silent retune would leave the sub-threshold
// fixture measuring a boundary that no longer exists.
'const CONTROL_DENSITY_BLOCK_CODE_UNITS = 64',
'const CONTROL_DENSITY_FALLBACK_COUNT = 32'
]) {
if (!PRODUCTION_SOURCE.includes(marker)) {
throw new Error(`terminal-control-stripping.ts no longer contains \`${marker}\``)
}
}
const { stripTerminalControl: stripAdaptive } = await import(
new URL('../../src/shared/terminal-control-stripping.ts', import.meta.url).href
)
const ESC = String.fromCharCode(0x1b)
const BEL = String.fromCharCode(0x07)
@@ -15,6 +63,9 @@ const INCOMPLETE_ANSI_ESCAPE_RE = new RegExp(
const HISTORY_LIMIT = 300
const SCAN_LIMIT = 4096
const SAMPLE_ID_LENGTH = 24
// Mirrors terminal-control-stripping.ts; the marker guard above fails if either is retuned.
const CONTROL_DENSITY_BLOCK_CODE_UNITS = 64
const CONTROL_DENSITY_FALLBACK_COUNT = 32
const ITERATIONS = Number(process.env.ORCA_STRIP_BENCH_ITERATIONS ?? '501')
let resultChecksum = 0
let validatedPairs = 0
@@ -75,6 +126,25 @@ function stripSliceRuns(data) {
return runStart === 0 ? withoutAnsi : output + withoutAnsi.slice(runStart)
}
function adaptiveFallbackIndex(data) {
const withoutAnsi = data.replace(ANSI_ESCAPE_RE, '').replace(INCOMPLETE_ANSI_ESCAPE_RE, '')
let strippedInBlock = 0
let blockEnd = 64
for (let index = 0; index < withoutAnsi.length; index += 1) {
if (index === blockEnd) {
strippedInBlock = 0
blockEnd += 64
}
if (isStrippedCode(withoutAnsi.charCodeAt(index))) {
strippedInBlock += 1
if (strippedInBlock === 32) {
return index
}
}
}
return -1
}
function fixedSampleId(sampleId) {
return `sample:${sampleId}`.padEnd(SAMPLE_ID_LENGTH, '_').slice(0, SAMPLE_ID_LENGTH)
}
@@ -95,6 +165,26 @@ function makeTuiFixture(length, sampleId, strippedControl) {
return text + 'x'.repeat(length - text.length)
}
// 31 controls per 64-unit block: one below the fallback trigger, so the adaptive path keeps
// slice-run bookkeeping on a shape dense enough to lose to the per-character legacy. This is the
// worst surviving case; it exists so the narrowed adverse window stays visible instead of hiding
// behind the 50% fixture, where the fallback fires and wins.
function makeSubThresholdDenseFixture(length, sampleId, strippedControl) {
const id = fixedSampleId(sampleId)
const units = []
for (let index = 0; index < length; index += 1) {
const blockOffset = index % CONTROL_DENSITY_BLOCK_CODE_UNITS
if (index < id.length) {
units.push(id[index])
} else if (blockOffset % 2 === 1 && blockOffset < (CONTROL_DENSITY_FALLBACK_COUNT - 1) * 2) {
units.push(strippedControl)
} else {
units.push(String.fromCharCode(97 + (index % 26)))
}
}
return units.join('')
}
function makeDenseFixture(length, sampleId, strippedControl) {
const prefix = `\x1b[35m${fixedSampleId(sampleId)}`
const suffix = '\x1b[0m'
@@ -120,33 +210,35 @@ function consumeOutput(output) {
resultChecksum ^= output.charCodeAt(Math.floor(output.length / 2))
}
function recordPair(fixture, sampleId, perCharFirst, samples) {
const perCharFixture = fixture.make(sampleId, perCharFirst ? '\x01' : '\x02')
const sliceRunsFixture = fixture.make(sampleId, perCharFirst ? '\x02' : '\x01')
if (
perCharFixture === sliceRunsFixture ||
perCharFixture.length !== fixture.length ||
sliceRunsFixture.length !== fixture.length
) {
const IMPLEMENTATIONS = [
['perChar', stripPerChar, '\x01'],
['sliceRuns', stripSliceRuns, '\x02'],
['adaptive', stripAdaptive, '\x03']
]
function recordRotation(fixture, sampleId, lead, samples) {
const inputs = IMPLEMENTATIONS.map(([name, strip, control]) => ({
name,
strip,
input: fixture.make(sampleId, control)
}))
if (inputs.some(({ input }) => input.length !== fixture.length)) {
throw new Error(`invalid inputs for ${fixture.label}, sample ${sampleId}`)
}
let perCharResult
let sliceRunsResult
if (perCharFirst) {
perCharResult = measure(stripPerChar, perCharFixture)
sliceRunsResult = measure(stripSliceRuns, sliceRunsFixture)
} else {
sliceRunsResult = measure(stripSliceRuns, sliceRunsFixture)
perCharResult = measure(stripPerChar, perCharFixture)
const results = new Map()
for (let offset = 0; offset < inputs.length; offset += 1) {
const entry = inputs[(lead + offset) % inputs.length]
results.set(entry.name, measure(entry.strip, entry.input))
}
if (perCharResult.output !== sliceRunsResult.output) {
const outputs = [...results.values()].map(({ output }) => output)
if (new Set(outputs).size !== 1) {
throw new Error(`strip mismatch for ${fixture.label}, sample ${sampleId}`)
}
consumeOutput(perCharResult.output)
consumeOutput(sliceRunsResult.output)
for (const [name, result] of results) {
consumeOutput(result.output)
samples[name].push(result.elapsed)
}
validatedPairs += 1
samples.perChar.push(perCharResult.elapsed)
samples.sliceRuns.push(sliceRunsResult.elapsed)
}
const denseBodyLength = SCAN_LIMIT - '\x1b[35m'.length - SAMPLE_ID_LENGTH - '\x1b[0m'.length
@@ -171,32 +263,68 @@ const fixtures = [
label: `${SCAN_LIMIT} scan ${denseControlPercent}% C0`,
length: SCAN_LIMIT,
make: (sampleId, control) => makeDenseFixture(SCAN_LIMIT, sampleId, control)
},
{
label: `${SCAN_LIMIT} scan 31/block C0`,
length: SCAN_LIMIT,
make: (sampleId, control) => makeSubThresholdDenseFixture(SCAN_LIMIT, sampleId, control)
}
]
const selectorFixtures = [
{ label: '31 controls', data: `${'\x01'.repeat(31)}${'a'.repeat(33)}`, expected: -1 },
{ label: '32 controls', data: `${'\x01'.repeat(32)}${'a'.repeat(32)}`, expected: 31 },
{
label: 'block reset at 64',
data: `${'\x01'.repeat(31)}${'a'.repeat(33)}${'\x01'.repeat(32)}`,
expected: 95
},
{
label: 'late dense block',
data: `${'a'.repeat(64 * 3)}${'\x01'.repeat(32)}tail`,
expected: 223
},
{
label: 'routine TUI',
data: makeTuiFixture(SCAN_LIMIT, 'selector', '\x01'),
expected: -1
},
{
label: '31/block never triggers',
data: makeSubThresholdDenseFixture(SCAN_LIMIT, 'selector', '\x01'),
expected: -1
}
]
for (const fixture of selectorFixtures) {
const actual = adaptiveFallbackIndex(fixture.data)
if (actual !== fixture.expected) {
throw new Error(`${fixture.label} fallback index ${actual}, expected ${fixture.expected}`)
}
}
const pad = (value, width) => String(value).padStart(width)
console.log('Complete stripTerminalControl path. Lower is better.')
console.log(`iterations=${ITERATIONS} (${ITERATIONS * 3} rotated samples/implementation, median)`)
console.log(
`iterations=${ITERATIONS} (${ITERATIONS * 2} first-touch samples/implementation, counterbalanced median)`
)
console.log(
`${pad('fixture', 25)} ${pad('per-char', 11)} ${pad('slice runs', 12)} ${pad('speedup', 9)}`
`${pad('fixture', 25)} ${pad('per-char', 11)} ${pad('slice runs', 12)} ${pad('adaptive', 11)} ${pad('vs legacy', 10)} ${pad('vs slice', 9)}`
)
for (const fixture of fixtures) {
const samples = { perChar: [], sliceRuns: [] }
const samples = { perChar: [], sliceRuns: [], adaptive: [] }
for (let index = 0; index < ITERATIONS; index += 1) {
const orders = index % 2 === 0 ? [true, false] : [false, true]
for (const perCharFirst of orders) {
const orderLabel = perCharFirst ? 'per-first' : 'slice-first'
recordPair(fixture, `${index}:${orderLabel}`, perCharFirst, samples)
for (let lead = 0; lead < IMPLEMENTATIONS.length; lead += 1) {
recordRotation(fixture, `${index}:lead-${lead}`, lead, samples)
}
}
const perChar = median(samples.perChar)
const sliceRuns = median(samples.sliceRuns)
const adaptive = median(samples.adaptive)
console.log(
`${pad(fixture.label, 25)} ${pad(`${(perChar * 1000).toFixed(1)} us`, 11)} ${pad(`${(sliceRuns * 1000).toFixed(1)} us`, 12)} ${pad(`${(perChar / sliceRuns).toFixed(2)}x`, 9)}`
`${pad(fixture.label, 25)} ${pad(`${(perChar * 1000).toFixed(1)} us`, 11)} ${pad(`${(sliceRuns * 1000).toFixed(1)} us`, 12)} ${pad(`${(adaptive * 1000).toFixed(1)} us`, 11)} ${pad(`${(perChar / adaptive).toFixed(2)}x`, 10)} ${pad(`${(sliceRuns / adaptive).toFixed(2)}x`, 9)}`
)
}
console.log(`\nvalidated=${validatedPairs} measured pairs, result checksum=${resultChecksum >>> 0}`)
console.log(
`\nvalidated=${validatedPairs} measured rotations, result checksum=${resultChecksum >>> 0}`
)
console.log(`selector checks=${selectorFixtures.length}`)
console.log('Production calls are bounded to 4096, 4096, 300, and 301 code units.')
@@ -0,0 +1,93 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const projectDir = resolve(import.meta.dirname, '../..')
describe('terminal IME e2e workflow', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/terminal-ime-e2e.yml'), 'utf8')
)
it('runs for xterm patch and terminal IME regression changes', () => {
expect(workflow.on.pull_request.paths).toEqual(
expect.arrayContaining([
'config/patches/@xterm__xterm@6.1.0-beta.287.patch',
'config/scripts/run-terminal-ibus-hangul-e2e.mjs',
'src/renderer/src/components/terminal-pane/keyboard-handlers.ts',
'src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx',
'src/renderer/src/components/terminal-pane/pty-connection.ts',
'src/renderer/src/components/terminal-pane/pty-connection.test.ts',
'src/renderer/src/components/terminal-pane/terminal-ime-*',
'src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts',
'src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts',
'src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts',
'tests/e2e/korean-ime-terminal-shift-enter-commit.spec.ts',
'tests/e2e/terminal-ibus-hangul-native.spec.ts',
'tests/e2e/terminal-ime-*.ts'
])
)
})
it('installs native IBus Hangul and X11 input tools', () => {
const runs = workflow.jobs['linux-x11'].steps
.map((step) => step.run)
.filter((run) => typeof run === 'string')
const installRun = runs.find((run) => run.includes('apt-get install'))
expect(installRun).toBeDefined()
expect(installRun).toContain('ibus-hangul')
expect(installRun).toContain('xdotool')
expect(installRun).toContain('xfwm4')
expect(installRun).toContain('xvfb')
expect(installRun).toContain('dbus-x11')
expect(installRun).toContain('dconf-gsettings-backend')
expect(installRun).toContain('libglib2.0-bin')
})
it('runs deterministic boundaries before the real IBus suite', () => {
const runs = workflow.jobs['linux-x11'].steps
.map((step) => step.run)
.filter((run) => typeof run === 'string')
const deterministicIndex = runs.findIndex((run) =>
run.includes('terminal-ime-exact-byte.spec.ts')
)
const nativeIndex = runs.findIndex((run) => run.includes('test:e2e:terminal-ime-native'))
expect(deterministicIndex).toBeGreaterThanOrEqual(0)
expect(nativeIndex).toBeGreaterThan(deterministicIndex)
})
it('keeps IBus lifecycle scoped to owned processes', () => {
const runner = readFileSync(
join(projectDir, 'config/scripts/run-terminal-ibus-hangul-e2e.mjs'),
'utf8'
)
expect(runner).toContain(
"['--xim', '--verbose', '--panel=disable', '--emoji-extension=disable']"
)
expect(runner).toContain("spawn('xfwm4', ['--compositor=off']")
expect(runner).toContain("['initial-input-mode', 'hangul']")
expect(runner).toContain("['hangul-keyboard', '2']")
expect(runner).toContain("process.kill(-processGroupId, 'SIGTERM')")
expect(runner).toContain("process.kill(-processGroupId, 'SIGKILL')")
expect(runner).toContain('const killDeadline = Date.now() + processKillTimeoutMs')
expect(runner).toMatch(
/'test:e2e:headful',\s*'--workers=1',\s*'--',\s*'tests\/e2e\/terminal-ibus-hangul-native\.spec\.ts'/
)
expect(runner).not.toContain("'--replace'")
expect(runner).not.toContain('killall')
expect(runner).not.toContain('pkill')
})
it('bounds blocking native input commands', () => {
const nativeSpec = readFileSync(
join(projectDir, 'tests/e2e/terminal-ibus-hangul-native.spec.ts'),
'utf8'
)
expect(nativeSpec.match(/timeout: NATIVE_COMMAND_TIMEOUT_MS/g)).toHaveLength(3)
})
})
@@ -0,0 +1,431 @@
#!/usr/bin/env node
// Benchmark: iterateTerminalOutputFrameChunks, which every byte of remote terminal
// output passes through on its way to a mobile/remote-desktop multiplex stream.
//
// The pre-fix loop was `for (const part of data)`: V8 materializes a fresh 1-2 code
// unit string per code point, then measureClipboardTextByteLength re-walked that
// string through codePointAt to get its UTF-8 width, and the chunk was rebuilt with
// `chunk += part`. The gate in front of it (terminalStreamByteLengthExceeds) ran the
// same per-code-point walk over the whole payload a second time.
//
// The fix: one charCodeAt scan computing UTF-8 width inline, slices for chunk text,
// and bounded byte probes when UTF-16 length alone cannot prove fit or overflow.
//
// BOTH arms run the complete production path, encodeTerminalStreamText included, and
// their emitted frames (base64 + seq + opcode) are compared before any timing, so an
// arm that split differently or renumbered a seq cannot be reported as a win.
import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import nodeModule from 'node:module'
import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
// terminal-stream-protocol.ts declares a TS enum, which Node's default strip-only
// loader rejects; re-exec once with type transformation rather than re-modelling
// the opcodes here (a hand copy could drift from the wire contract).
if (!process.execArgv.includes('--experimental-transform-types')) {
const result = spawnSync(
process.execPath,
['--experimental-transform-types', '--no-warnings', import.meta.filename],
{ stdio: 'inherit' }
)
process.exit(result.status ?? 1)
}
// The app's TS sources import siblings without an extension; Node's ESM resolver needs it.
nodeModule.registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier.startsWith('.') && !/\.[cm]?[jt]s$/.test(specifier) && context.parentURL) {
const candidate = new URL(`${specifier}.ts`, context.parentURL)
if (existsSync(fileURLToPath(candidate))) {
return { url: candidate.href, shortCircuit: true }
}
}
return nextResolve(specifier, context)
}
})
const ITERATIONS = Number(process.env.ORCA_FRAME_CHUNK_BENCH_ITERATIONS ?? '40')
const GATE_ITERATIONS = Number(process.env.ORCA_FRAME_GATE_BENCH_ITERATIONS ?? '2000')
const WARMUP = Number(process.env.ORCA_FRAME_CHUNK_BENCH_WARMUP ?? '8')
const ROUNDS = Number(process.env.ORCA_FRAME_CHUNK_BENCH_ROUNDS ?? '6')
for (const [name, value] of [
['ORCA_FRAME_CHUNK_BENCH_ITERATIONS', ITERATIONS],
['ORCA_FRAME_GATE_BENCH_ITERATIONS', GATE_ITERATIONS],
['ORCA_FRAME_CHUNK_BENCH_WARMUP', WARMUP],
['ORCA_FRAME_CHUNK_BENCH_ROUNDS', ROUNDS]
]) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer, received ${value}`)
}
}
if (ROUNDS % 2 !== 0) {
throw new Error(`ORCA_FRAME_CHUNK_BENCH_ROUNDS must be even so each arm leads equally`)
}
const CHUNK_SOURCE = readFileSync(
new URL('../../src/main/runtime/rpc/terminal-output-frame-chunks.ts', import.meta.url),
'utf8'
)
const CLIPBOARD_SOURCE = readFileSync(
new URL('../../src/shared/clipboard-text.ts', import.meta.url),
'utf8'
)
// Match executable source markers so a stale benchmark fails instead of misleading.
for (const [source, label, marker] of [
[
CHUNK_SOURCE,
'terminal-output-frame-chunks.ts',
'export function exceedsTerminalStreamChunkBytes(data: string): boolean'
],
[CHUNK_SOURCE, 'terminal-output-frame-chunks.ts', 'TERMINAL_STREAM_BYTE_PROBE_CODE_UNITS'],
[
CHUNK_SOURCE,
'terminal-output-frame-chunks.ts',
'terminalStreamByteLength(data.slice(start, end))'
],
[CHUNK_SOURCE, 'terminal-output-frame-chunks.ts', 'const text = data.slice(chunkStart, end)'],
[CHUNK_SOURCE, 'terminal-output-frame-chunks.ts', 'data.charCodeAt(index + 1)'],
[CLIPBOARD_SOURCE, 'clipboard-text.ts', 'export function measureClipboardTextByteLength('],
[CLIPBOARD_SOURCE, 'clipboard-text.ts', 'text.codePointAt(index)']
]) {
if (!source.includes(marker)) {
throw new Error(`${label} no longer contains \`${marker}\`; this benchmark is stale`)
}
}
if (CHUNK_SOURCE.includes('for (const part of data)')) {
throw new Error(
'terminal-output-frame-chunks.ts still iterates code points as strings; this benchmark is stale'
)
}
const { TERMINAL_STREAM_CHUNK_BYTES } = await import(
new URL('../../src/shared/terminal-multiplex-flow-control.ts', import.meta.url).href
)
const { measureClipboardTextByteLength } = await import(
new URL('../../src/shared/clipboard-text.ts', import.meta.url).href
)
const { TerminalStreamOpcode, encodeTerminalStreamJson, encodeTerminalStreamText } = await import(
new URL('../../src/shared/terminal-stream-protocol.ts', import.meta.url).href
)
const { exceedsTerminalStreamChunkBytes, iterateTerminalOutputFrameChunks } = await import(
new URL('../../src/main/runtime/rpc/terminal-output-frame-chunks.ts', import.meta.url).href
)
function previousGate(data) {
return (
data.length > TERMINAL_STREAM_CHUNK_BYTES ||
Buffer.byteLength(data, 'utf8') > TERMINAL_STREAM_CHUNK_BYTES
)
}
// Pre-fix arm: the exact code that shipped, including the second full walk in the gate.
function* iterateBefore(data, meta) {
const rawLength = meta?.rawLength ?? data.length
if (meta?.transformed || rawLength !== data.length) {
yield {
opcode: TerminalStreamOpcode.OutputSpan,
bytes: encodeTerminalStreamJson({ data, rawLength, transformed: true }),
seq: meta?.seq
}
return
}
if (
!measureClipboardTextByteLength(data, { stopAfterBytes: TERMINAL_STREAM_CHUNK_BYTES })
.exceededLimit
) {
yield { bytes: encodeTerminalStreamText(data), seq: meta?.seq }
return
}
const canPreserveChunkSeq = typeof meta?.seq === 'number' && rawLength === data.length
const shouldDelayFinalSeq = !canPreserveChunkSeq && typeof meta?.seq === 'number'
const startSeq = canPreserveChunkSeq ? meta.seq - rawLength : undefined
let chunk = ''
let chunkBytes = 0
let chunkStartOffset = 0
let offset = 0
let delayedChunk = null
const takeChunk = () => {
if (!chunk) {
return null
}
const chunkSeq = canPreserveChunkSeq ? startSeq + chunkStartOffset + chunk.length : undefined
const current = { text: chunk, seq: chunkSeq }
chunk = ''
chunkBytes = 0
chunkStartOffset = offset
return current
}
for (const part of data) {
const partBytes = measureClipboardTextByteLength(part).byteLength
if (chunkBytes > 0 && chunkBytes + partBytes > TERMINAL_STREAM_CHUNK_BYTES) {
const nextChunk = takeChunk()
if (nextChunk) {
if (shouldDelayFinalSeq) {
if (delayedChunk) {
yield { bytes: encodeTerminalStreamText(delayedChunk.text) }
}
delayedChunk = nextChunk
} else {
yield { bytes: encodeTerminalStreamText(nextChunk.text), seq: nextChunk.seq }
}
}
}
chunk += part
chunkBytes += partBytes
offset += part.length
}
const finalChunk = takeChunk()
if (shouldDelayFinalSeq) {
if (finalChunk) {
if (delayedChunk) {
yield { bytes: encodeTerminalStreamText(delayedChunk.text) }
}
delayedChunk = finalChunk
}
if (delayedChunk) {
yield { bytes: encodeTerminalStreamText(delayedChunk.text), seq: meta.seq }
}
return
}
if (finalChunk) {
yield { bytes: encodeTerminalStreamText(finalChunk.text), seq: finalChunk.seq }
}
}
// The multiplex stream consumes every frame's bytes/seq/opcode; charge both arms for it.
let frameChecksum = 0
function drain(iterate, data, meta) {
let frames = 0
let bytes = 0
let seqSum = 0
for (const frame of iterate(data, meta)) {
frames += 1
bytes += frame.bytes.byteLength
seqSum += frame.seq ?? 0
}
frameChecksum = Math.imul(frameChecksum ^ (frames + bytes + seqSum), 16777619) >>> 0
return frames
}
function describeFrames(iterate, data, meta) {
const shapes = []
for (const frame of iterate(data, meta)) {
shapes.push(
`${Buffer.from(frame.bytes).toString('base64')}|${frame.seq ?? 'u'}|${frame.opcode ?? 'u'}`
)
}
return shapes.join('\n')
}
const SURROGATE_PAIR = '\u{1f600}'
const LONE_HIGH = '\ud83d'
function repeatTo(unit, codeUnits) {
let out = ''
while (out.length < codeUnits) {
out += unit
}
return out.slice(0, out.length - (out.length % unit.length))
}
// A realistic agent-TUI line: SGR runs, a wide glyph, a currency sign, an emoji.
const TUI_LINE =
'\u001b[35m\u273b Thinking\u001b[0m about the \u20ac plan \u{1f600} 42 passed, 0 failed\r\n'
const fixtures = [
{
label: 'ascii 4KiB (typical batch)',
data: 'x'.repeat(4 * 1024),
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
label: `ascii ${TERMINAL_STREAM_CHUNK_BYTES}B (at cap)`,
data: 'x'.repeat(TERMINAL_STREAM_CHUNK_BYTES),
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
label: 'ascii 64KiB (batch cap, 2 chunks)',
data: 'x'.repeat(64 * 1024),
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
label: 'mixed TUI 64KiB (2 chunks)',
data: repeatTo(TUI_LINE, 64 * 1024),
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
// seq without an explicit rawLength: the batcher's ordinary shape.
label: 'mixed TUI 64KiB (implicit raw)',
data: repeatTo(TUI_LINE, 64 * 1024),
meta: () => ({ seq: 5_000_000 })
},
{
label: 'emoji 64KiB (4-byte, 2 chunks)',
data: repeatTo(SURROGATE_PAIR, 32 * 1024),
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
label: 'lone surrogates 64KiB',
data: repeatTo(LONE_HIGH, 64 * 1024),
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
label: 'late-wide gate miss (3 chunks)',
data: `${'a'.repeat(16_000)}${'\u20ac'.repeat(32_000)}`,
meta: (data) => ({ seq: 5_000_000, rawLength: data.length })
},
{
label: 'ascii 512KiB (snapshot chunking)',
data: 'x'.repeat(512 * 1024),
meta: () => undefined
}
]
function median(samples) {
const sorted = [...samples].sort((a, b) => a - b)
const middle = Math.floor(sorted.length / 2)
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]
}
// Why interleaved with an alternating lead: running one arm's whole batch first lets
// CPU frequency drift correlate with whichever arm is being measured. Elsewhere in this
// effort that alone reported 23.3x for a real 6.7x.
function measureInterleaved(data, meta) {
for (let index = 0; index < WARMUP; index += 1) {
drain(iterateBefore, data, meta)
drain(iterateTerminalOutputFrameChunks, data, meta)
}
const beforeSamples = []
const afterSamples = []
for (let round = 0; round < ROUNDS; round += 1) {
const runBefore = () => {
const start = performance.now()
for (let index = 0; index < ITERATIONS; index += 1) {
drain(iterateBefore, data, meta)
}
beforeSamples.push((performance.now() - start) / ITERATIONS)
}
const runAfter = () => {
const start = performance.now()
for (let index = 0; index < ITERATIONS; index += 1) {
drain(iterateTerminalOutputFrameChunks, data, meta)
}
afterSamples.push((performance.now() - start) / ITERATIONS)
}
if (round % 2 === 0) {
runBefore()
runAfter()
} else {
runAfter()
runBefore()
}
}
return { beforeMs: median(beforeSamples), afterMs: median(afterSamples) }
}
let gateChecksum = 0
function drainGate(gate, data) {
gateChecksum = Math.imul(gateChecksum ^ (gate(data) ? 1 : 0), 16777619) >>> 0
}
function measureGateInterleaved(data) {
for (let index = 0; index < WARMUP * 10; index += 1) {
drainGate(previousGate, data)
drainGate(exceedsTerminalStreamChunkBytes, data)
}
const previousSamples = []
const boundedSamples = []
for (let round = 0; round < ROUNDS; round += 1) {
const run = (gate, samples) => {
const start = performance.now()
for (let index = 0; index < GATE_ITERATIONS; index += 1) {
drainGate(gate, data)
}
samples.push((performance.now() - start) / GATE_ITERATIONS)
}
if (round % 2 === 0) {
run(previousGate, previousSamples)
run(exceedsTerminalStreamChunkBytes, boundedSamples)
} else {
run(exceedsTerminalStreamChunkBytes, boundedSamples)
run(previousGate, previousSamples)
}
}
return { previousMs: median(previousSamples), boundedMs: median(boundedSamples) }
}
const pad = (value, width) => String(value).padStart(width)
console.log('iterateTerminalOutputFrameChunks, per flushed batch. Lower is better.')
console.log(
`iterations=${ITERATIONS} warmup=${WARMUP} rounds=${ROUNDS} (alternating lead, per-arm median)`
)
console.log(
`${pad('fixture', 34)} ${pad('frames', 7)} ${pad('per-part', 11)} ${pad('scanned', 11)} ${pad('speedup', 9)}`
)
let comparedFixtures = 0
for (const fixture of fixtures) {
const meta = fixture.meta(fixture.data)
const before = describeFrames(iterateBefore, fixture.data, meta)
const after = describeFrames(iterateTerminalOutputFrameChunks, fixture.data, meta)
if (before !== after) {
throw new Error(`frames differ for ${fixture.label}`)
}
const frames = before.split('\n').length
// Guard against a fixture that never reaches the chunking loop; then both arms would
// just be measuring the gate and the comparison above would prove nothing about it.
if (fixture.data.length > TERMINAL_STREAM_CHUNK_BYTES && frames < 2) {
throw new Error(`${fixture.label} never split; fixture does not exercise the chunk loop`)
}
comparedFixtures += 1
const { beforeMs, afterMs } = measureInterleaved(fixture.data, meta)
console.log(
`${pad(fixture.label, 34)} ${pad(frames, 7)} ${pad(`${beforeMs.toFixed(3)} ms`, 11)} ${pad(`${afterMs.toFixed(3)} ms`, 11)} ${pad(`${(beforeMs / afterMs).toFixed(1)}x`, 9)}`
)
}
console.log(
`\nvalidated=${comparedFixtures} fixtures frame-identical before timing, checksum=${frameChecksum >>> 0}`
)
const gateFixtures = [
{ label: 'ascii 4KiB fit proof', data: 'x'.repeat(4 * 1024) },
{
label: 'three-byte exact-cap fit proof',
data: '\u20ac'.repeat(TERMINAL_STREAM_CHUNK_BYTES / 3)
},
{
label: 'late-wide fit after probes',
data: `${'a'.repeat(16_000)}${'\u20ac'.repeat(11_000)}`
},
{
label: 'late-wide miss during probes',
data: `${'a'.repeat(16_000)}${'\u20ac'.repeat(32_000)}`
},
{ label: 'ascii 64KiB overflow proof', data: 'x'.repeat(64 * 1024) }
]
console.log('\nTerminal frame-fit gate only. Lower is better.')
console.log(
`${pad('fixture', 34)} ${pad('result', 8)} ${pad('whole scan', 12)} ${pad('bounded', 11)} ${pad('speedup', 9)}`
)
for (const fixture of gateFixtures) {
const previous = previousGate(fixture.data)
const bounded = exceedsTerminalStreamChunkBytes(fixture.data)
if (previous !== bounded) {
throw new Error(`gate result differs for ${fixture.label}`)
}
const { previousMs, boundedMs } = measureGateInterleaved(fixture.data)
console.log(
`${pad(fixture.label, 34)} ${pad(bounded ? 'miss' : 'fit', 8)} ${pad(`${(previousMs * 1000).toFixed(2)} us`, 12)} ${pad(`${(boundedMs * 1000).toFixed(2)} us`, 11)} ${pad(boundedMs > 0 ? `${(previousMs / boundedMs).toFixed(2)}x` : 'n/a', 9)}`
)
}
console.log(`gate fixtures=${gateFixtures.length}, checksum=${gateChecksum >>> 0}`)
console.log(
'Every byte of remote terminal output crosses this function; the batcher flushes at\nTERMINAL_OUTPUT_BATCH_MAX_BYTES (64 KiB) or every 5 ms, so a busy remote agent pane\nruns it tens of times a second per subscribed stream.'
)
@@ -0,0 +1,416 @@
#!/usr/bin/env node
// Benchmarks the production terminal byte-measurement exports at their real budgets: the output
// batcher push and the snapshot budget scan. Every scenario asserts whether production invoked
// Buffer.byteLength, so implementation drift cannot preserve stale speedup claims.
import { spawnSync } from 'node:child_process'
import { performance } from 'node:perf_hooks'
import fs from 'node:fs'
import nodeModule from 'node:module'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
if (!process.execArgv.includes('--experimental-transform-types')) {
const result = spawnSync(
process.execPath,
['--experimental-transform-types', '--no-warnings', import.meta.filename],
{ stdio: 'inherit' }
)
process.exit(result.status ?? 1)
}
// The app's TS sources import siblings without an extension; Node's ESM resolver needs it.
nodeModule.registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier.startsWith('.') && !/\.[cm]?[jt]s$/.test(specifier) && context.parentURL) {
const candidate = new URL(`${specifier}.ts`, context.parentURL)
if (fs.existsSync(fileURLToPath(candidate))) {
return { url: candidate.href, shortCircuit: true }
}
}
return nextResolve(specifier, context)
}
})
const ROOT = path.resolve(import.meta.dirname, '../..')
const ITERATIONS = Number(process.env.ORCA_BYTE_LENGTH_BENCH_ITERATIONS ?? '61')
let resultChecksum = 0
let validatedPairs = 0
if (!Number.isSafeInteger(ITERATIONS) || ITERATIONS <= 0) {
throw new Error(`ORCA_BYTE_LENGTH_BENCH_ITERATIONS must be a positive integer, got ${ITERATIONS}`)
}
function readSource(relative) {
return fs.readFileSync(path.join(ROOT, relative), 'utf8')
}
const TERMINAL_SOURCE = readSource('src/main/runtime/rpc/methods/terminal.ts')
function requireCallForm(source, needle, label) {
if (!source.includes(needle)) {
throw new Error(`${label} is stale: expected call form \`${needle}\` was not found`)
}
}
const { TERMINAL_OUTPUT_BATCH_MAX_BYTES, TERMINAL_STREAM_CHUNK_BYTES } = await import(
new URL('../../src/shared/terminal-multiplex-flow-control.ts', import.meta.url).href
)
const { measureClipboardTextByteLength } = await import(
new URL('../../src/shared/clipboard-text.ts', import.meta.url).href
)
const {
MIN_NATIVE_BYTE_LENGTH_CODE_UNITS,
measureTerminalStreamByteLength,
terminalStreamByteLengthExceeds
} = await import(
new URL('../../src/main/runtime/rpc/terminal-stream-byte-length.ts', import.meta.url).href
)
const REQUESTED_SNAPSHOT_BYTE_BUDGET = (() => {
const match = /const REQUESTED_SNAPSHOT_BYTE_BUDGET = ([^\n]+)/.exec(TERMINAL_SOURCE)
if (!match) {
throw new Error('terminal.ts is stale: REQUESTED_SNAPSHOT_BYTE_BUDGET is gone')
}
return Number(new Function(`return (${match[1].trim()})`)())
})()
requireCallForm(TERMINAL_SOURCE, 'measureTerminalStreamByteLength(data, {', 'terminal.ts')
requireCallForm(TERMINAL_SOURCE, 'stopAfterBytes: remainingBudget', 'terminal.ts')
requireCallForm(
TERMINAL_SOURCE,
'terminalStreamByteLengthExceeds(data, REQUESTED_SNAPSHOT_BYTE_BUDGET)',
'terminal.ts'
)
const nativeByteLength = Buffer.byteLength
function runWithNativeCallCount(fn) {
let calls = 0
Buffer.byteLength = (...args) => {
calls += 1
return Reflect.apply(nativeByteLength, Buffer, args)
}
try {
return { output: fn(), calls }
} finally {
Buffer.byteLength = nativeByteLength
}
}
// ---- OLD ARM: the production implementation terminal.ts called before this change.
const legacyMeasure = measureClipboardTextByteLength
const legacyExceeds = (data, maxBytes) =>
measureClipboardTextByteLength(data, { stopAfterBytes: maxBytes }).exceededLimit
// ---- Fixtures. Deterministic, seeded, and varied per sample so V8 cannot hoist.
function mulberry32(seed) {
let state = seed >>> 0
return () => {
state = (state + 0x6d2b79f5) >>> 0
let t = state
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
// Realistic agent-TUI output: mostly ASCII with SGR runs, box drawing, and emoji status
// glyphs, plus a per-sample marker so no two measured strings are identical.
function makeTerminalText(codeUnits, sampleId) {
const random = mulberry32(sampleId * 2654435761)
const lines = [
'✻ Thinking…\r\n',
' ⏺ Running tests… 42 passed, 0 failed\r\n',
'│ src/main/runtime/rpc/methods/terminal.ts │\r\n',
' ✅ build succeeded in 12.4s — café naïve\r\n',
'+ added line\r\n'
]
let text = `sample:${sampleId}\r\n`
while (text.length < codeUnits) {
text += lines[Math.floor(random() * lines.length)]
}
return text.slice(0, codeUnits)
}
// Keystroke echo and tiny interactive writes: the shapes a PTY emits between key presses.
function makeInteractiveText(codeUnits, sampleId) {
const random = mulberry32(sampleId * 40503)
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789 ./-_'
let text = ''
while (text.length < codeUnits) {
text += alphabet[Math.floor(random() * alphabet.length)]
}
return text.slice(0, codeUnits)
}
// Trim to just under a BYTE budget so the legacy arm runs its full scan without tripping the limit.
function makeTerminalTextUnderBytes(byteBudget, sampleId) {
let text = makeTerminalText(byteBudget, sampleId)
while (Buffer.byteLength(text, 'utf8') > byteBudget) {
text = text.slice(0, Math.floor(text.length * (byteBudget / Buffer.byteLength(text, 'utf8'))))
}
return text
}
// The TRUE adversary for the exceeds gate: stay at the code-unit cap so `length > maxBytes`
// cannot short-circuit, but pack 3-byte BMP scalars so the legacy scan bails out after only a
// THIRD of the string while Buffer.byteLength still walks all of it.
function makeEarlyTripText(byteBudget, sampleId) {
const tripUnits = Math.ceil((byteBudget + 1) / 3)
const marker = String.fromCharCode(0x4e00 + (sampleId % 4096))
const prefix = `${marker}${'走'.repeat(tripUnits - 1)}`
return `${prefix}${'a'.repeat(byteBudget - tripUnits)}`
}
function median(samples) {
const sorted = [...samples].sort((a, b) => a - b)
const middle = Math.floor(sorted.length / 2)
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]
}
function consume(value) {
resultChecksum = Math.imul(resultChecksum ^ (value | 0), 16777619) >>> 0
}
// Small inputs are far below timer resolution, so batch them: build `repeats` distinct
// samples, time the whole loop, and report per-call cost. Consuming the running total
// inside the timed region keeps V8 from hoisting the calls out.
function runScenario(scenario) {
const repeats = scenario.repeats ?? 1
const samples = { legacy: [], next: [] }
const runArm = (fn, inputs) => {
const start = performance.now()
let total = 0
for (const input of inputs) {
total += scenario.checksum(fn(input))
}
const elapsed = performance.now() - start
return { elapsed, total }
}
for (let index = 0; index < ITERATIONS; index += 1) {
// Alternate which arm leads on every iteration so cache/JIT warmup is shared evenly.
for (const legacyFirst of index % 2 === 0 ? [true, false] : [false, true]) {
const batch = index * 2 + (legacyFirst ? 0 : 1)
const inputs = []
for (let repeat = 0; repeat < repeats; repeat += 1) {
inputs.push(scenario.make(batch * repeats + repeat))
}
const legacyOutputs = inputs.map(scenario.legacy)
const observed = runWithNativeCallCount(() => inputs.map(scenario.next))
for (let inputIndex = 0; inputIndex < inputs.length; inputIndex += 1) {
const input = inputs[inputIndex]
const legacyOutput = legacyOutputs[inputIndex]
const nextOutput = observed.output[inputIndex]
if (!scenario.equal(legacyOutput, nextOutput)) {
throw new Error(
`${scenario.label}: arms disagree on ${JSON.stringify(input.slice(0, 40))}`
)
}
scenario.assertResult(legacyOutput, input)
validatedPairs += 1
}
scenario.assertNativeCalls(inputs.length, observed.calls)
let legacyResult
let nextResult
if (legacyFirst) {
legacyResult = runArm(scenario.legacy, inputs)
nextResult = runArm(scenario.next, inputs)
} else {
nextResult = runArm(scenario.next, inputs)
legacyResult = runArm(scenario.legacy, inputs)
}
consume(legacyResult.total)
consume(nextResult.total)
samples.legacy.push(legacyResult.elapsed / repeats)
samples.next.push(nextResult.elapsed / repeats)
}
}
return { legacy: median(samples.legacy), next: median(samples.next) }
}
const measurementEqual = (a, b) =>
a.byteLength === b.byteLength && a.exceededLimit === b.exceededLimit
const measurementChecksum = (m) => m.byteLength + (m.exceededLimit ? 1 : 0)
const booleanChecksum = (value) => (value ? 1 : 0)
// Every scenario states which production branch it expects. Native fixtures require exactly one
// Buffer.byteLength call per input; fallback fixtures require none.
function requireBranch(expected) {
return (inputCount, calls) => {
const expectedCalls = expected === 'nativeFastPath' ? inputCount : 0
if (calls !== expectedCalls) {
throw new Error(
`expected the production ${expected} branch (${expectedCalls} Buffer.byteLength calls), got ${calls}`
)
}
}
}
const batchScenario = (label, make, options = {}) => ({
label,
make,
repeats: options.repeats,
legacy: (input) => legacyMeasure(input, { stopAfterBytes: TERMINAL_OUTPUT_BATCH_MAX_BYTES }),
next: (input) =>
measureTerminalStreamByteLength(input, {
stopAfterBytes: TERMINAL_OUTPUT_BATCH_MAX_BYTES
}),
equal: measurementEqual,
checksum: measurementChecksum,
assertResult: (out, input) => options.assert?.(out, input),
assertNativeCalls: requireBranch(options.branch)
})
const gateScenario = (label, budget, make, options = {}) => ({
label,
make,
repeats: options.repeats,
legacy: (input) => legacyExceeds(input, budget),
next: (input) => terminalStreamByteLengthExceeds(input, budget),
equal: (a, b) => a === b,
checksum: booleanChecksum,
assertResult: (out, input) => options.assert?.(out, input),
assertNativeCalls: requireBranch(options.branch)
})
const scenarios = [
batchScenario('batcher push 8KiB', (sampleId) => makeTerminalText(8 * 1024, sampleId), {
branch: 'nativeFastPath',
assert: (out) => {
if (out.exceededLimit) {
throw new Error('batcher push 8KiB should stay under the batch budget')
}
}
}),
batchScenario(
'batcher push over budget',
// Oversized on purpose: this is the case where the arms MUST both return the partial count.
(sampleId) => makeTerminalText(3 * TERMINAL_OUTPUT_BATCH_MAX_BYTES, sampleId),
{
branch: 'scanFallback',
assert: (out) => {
if (!out.exceededLimit) {
throw new Error('over-budget fixture never exceeded the limit')
}
}
}
),
// TRUE WORST CASE for the batcher push. The guard only takes the native count when
// length*3 <= stopAfterBytes, which PROVES the limit cannot trip, so the fast path can never
// pay for both a Buffer.byteLength and a scan. What is left is the shape where the native
// call replaces the fewest scan iterations: a chunk sitting just above the code-unit floor.
batchScenario(
`batcher push ${MIN_NATIVE_BYTE_LENGTH_CODE_UNITS}B (floor)`,
(sampleId) => makeInteractiveText(MIN_NATIVE_BYTE_LENGTH_CODE_UNITS, sampleId),
{ branch: 'nativeFastPath', repeats: 4096 }
),
// Below the floor the new arm deliberately keeps the scan, so it is the legacy code exactly.
batchScenario('batcher push 4B keystroke', (sampleId) => makeInteractiveText(4, sampleId), {
branch: 'scanFallback',
repeats: 4096
}),
gateScenario(
`snapshot scan ${(REQUESTED_SNAPSHOT_BYTE_BUDGET / (1024 * 1024)).toFixed(0)}MiB`,
REQUESTED_SNAPSHOT_BYTE_BUDGET,
(sampleId) => makeTerminalTextUnderBytes(REQUESTED_SNAPSHOT_BYTE_BUDGET, sampleId),
{
branch: 'nativeFastPath',
assert: (out) => {
if (out) {
throw new Error('snapshot fixture should sit under the budget so the full scan runs')
}
}
}
),
// TRUE WORST CASE for the boolean gate: at the code-unit cap so `length > maxBytes` cannot
// short-circuit, but 3-byte scalars let the legacy scan bail out a THIRD of the way in while
// Buffer.byteLength still walks the whole string. This is where the new arm can actually lose.
gateScenario(
'snapshot gate early-trip',
REQUESTED_SNAPSHOT_BYTE_BUDGET,
(sampleId) => makeEarlyTripText(REQUESTED_SNAPSHOT_BYTE_BUDGET, sampleId),
{
branch: 'nativeFastPath',
assert: (out, input) => {
if (!out) {
throw new Error('early-trip gate fixture must exceed the budget')
}
if (input.length > REQUESTED_SNAPSHOT_BYTE_BUDGET) {
throw new Error('early-trip fixture must not hit the code-unit short circuit')
}
}
}
),
gateScenario(
'chunk gate early-trip',
TERMINAL_STREAM_CHUNK_BYTES,
(sampleId) => makeEarlyTripText(TERMINAL_STREAM_CHUNK_BYTES, sampleId),
{
branch: 'nativeFastPath',
assert: (out, input) => {
if (!out) {
throw new Error('early-trip chunk fixture must exceed the budget')
}
if (input.length > TERMINAL_STREAM_CHUNK_BYTES) {
throw new Error('early-trip fixture must not hit the code-unit short circuit')
}
}
}
),
gateScenario(
`chunk gate ${TERMINAL_STREAM_CHUNK_BYTES / 1024}KiB`,
TERMINAL_STREAM_CHUNK_BYTES,
(sampleId) => makeTerminalTextUnderBytes(TERMINAL_STREAM_CHUNK_BYTES, sampleId),
{
branch: 'nativeFastPath',
assert: (out) => {
if (out) {
throw new Error('chunk gate fixture should sit under the chunk budget')
}
}
}
)
]
const pad = (value, width) => String(value).padStart(width)
const formatTime = (ms) =>
ms >= 0.001 ? `${(ms * 1000).toFixed(1)} us` : `${(ms * 1e6).toFixed(1)} ns`
console.log('Production terminal byte-measurement paths. Lower is better.')
console.log(
`iterations=${ITERATIONS} (${ITERATIONS * 2} counterbalanced batches/scenario, per-arm medians)`
)
console.log(`${pad('scenario', 30)} ${pad('legacy', 12)} ${pad('new', 12)} ${pad('speedup', 9)}`)
for (const scenario of scenarios) {
const { legacy, next } = runScenario(scenario)
console.log(
`${pad(scenario.label, 30)} ${pad(formatTime(legacy), 12)} ${pad(formatTime(next), 12)} ${pad(`${(legacy / next).toFixed(2)}x`, 9)}`
)
}
// Small-chunk sweep across real interactive PTY write sizes. The floor makes everything below
// MIN_NATIVE_BYTE_LENGTH_CODE_UNITS byte-identical to the legacy scan, so those rows must land
// at ~1.00x; anything materially below that is a regression the change would be shipping.
{
console.log(
`\nbatcher push small-chunk sweep (stopAfterBytes=${TERMINAL_OUTPUT_BATCH_MAX_BYTES}):`
)
console.log(
`${pad('bytes', 10)} ${pad('legacy', 12)} ${pad('new', 12)} ${pad('speedup', 9)} branch`
)
for (const codeUnits of [4, 8, 16, 64, 256, 1024, 4096]) {
const expectedBranch =
codeUnits >= MIN_NATIVE_BYTE_LENGTH_CODE_UNITS ? 'nativeFastPath' : 'scanFallback'
const { legacy, next } = runScenario(
batchScenario(`sweep ${codeUnits}`, (sampleId) => makeInteractiveText(codeUnits, sampleId), {
branch: expectedBranch,
repeats: Math.max(64, Math.min(4096, Math.ceil(2 ** 18 / codeUnits)))
})
)
const branch = expectedBranch === 'nativeFastPath' ? 'native' : 'scan (unchanged)'
console.log(
`${pad(`${codeUnits} B`, 10)} ${pad(formatTime(legacy), 12)} ${pad(formatTime(next), 12)} ${pad(`${(legacy / next).toFixed(2)}x`, 9)} ${branch}`
)
}
}
console.log(`\nvalidated=${validatedPairs} measured pairs, result checksum=${resultChecksum >>> 0}`)
+14 -90
View File
@@ -227,44 +227,6 @@ function setCatalogEntry(catalog, key, value) {
cursor[parts.at(-1)] = value
}
function deleteCatalogEntry(catalog, key) {
const parts = key.split('.')
const stack = []
let cursor = catalog
for (const part of parts.slice(0, -1)) {
if (
typeof cursor?.[part] !== 'object' ||
cursor[part] === null ||
Array.isArray(cursor[part])
) {
return false
}
stack.push([cursor, part])
cursor = cursor[part]
}
const leafKey = parts.at(-1)
if (!Object.hasOwn(cursor, leafKey)) {
return false
}
delete cursor[leafKey]
for (let index = stack.length - 1; index >= 0; index -= 1) {
const [parent, part] = stack[index]
const child = parent[part]
if (
typeof child === 'object' &&
child !== null &&
!Array.isArray(child) &&
Object.keys(child).length === 0
) {
delete parent[part]
}
}
return true
}
function collectLocaleParityIssues(enCatalog, localeCatalog) {
const enEntries = flattenCatalogEntries(enCatalog)
const localeEntries = flattenCatalogEntries(localeCatalog)
@@ -286,30 +248,6 @@ function collectLocaleParityIssues(enCatalog, localeCatalog) {
return { enEntries, localeEntries, missingInLocale, extraInLocale, interpolationMismatches }
}
function repairLocaleParity(enCatalog, localeCatalog) {
const { enEntries, missingInLocale, extraInLocale, interpolationMismatches } =
collectLocaleParityIssues(enCatalog, localeCatalog)
let changed = 0
for (const key of missingInLocale) {
setCatalogEntry(localeCatalog, key, enEntries.get(key))
changed += 1
}
for (const key of extraInLocale) {
if (deleteCatalogEntry(localeCatalog, key)) {
changed += 1
}
}
for (const key of interpolationMismatches) {
setCatalogEntry(localeCatalog, key, enEntries.get(key))
changed += 1
}
return changed
}
function referencesMissingFallbacks(missing) {
return missing.filter((reference) => typeof reference.fallback !== 'string')
}
@@ -344,23 +282,18 @@ function applyMissingEnglishEntries(catalog, missing) {
return changed
}
function verifyLocaleParity(enCatalog, localeName, localeCatalog) {
const { localeEntries, missingInLocale, extraInLocale, interpolationMismatches } =
function verifyLocaleCatalog(enCatalog, localeName, localeCatalog) {
const { enEntries, localeEntries, missingInLocale, extraInLocale, interpolationMismatches } =
collectLocaleParityIssues(enCatalog, localeCatalog)
if (
missingInLocale.length > 0 ||
extraInLocale.length > 0 ||
interpolationMismatches.length > 0
) {
console.error(`Locale catalog parity failed for ${localeName}.json.`)
if (missingInLocale.length > 0) {
console.error('')
console.error(formatMissingKeys('missing', missingInLocale.slice(0, 20)))
if (missingInLocale.length > 20) {
console.error(`...and ${missingInLocale.length - 20} more missing keys`)
}
}
// Why: feature PRs own English declarations; absent target leaves deliberately
// use i18next's existing English fallback until a localization PR supplies them.
console.log(
`${localeName}.json coverage: ${enEntries.size - missingInLocale.length}/${enEntries.size} translated, ${missingInLocale.length} missing.`
)
if (extraInLocale.length > 0 || interpolationMismatches.length > 0) {
console.error(`Locale catalog validation failed for ${localeName}.json.`)
if (extraInLocale.length > 0) {
console.error('')
console.error(formatMissingKeys('extra', extraInLocale.slice(0, 20)))
@@ -380,7 +313,7 @@ function verifyLocaleParity(enCatalog, localeName, localeCatalog) {
return 1
}
console.log(`Verified locale parity for ${localeName}.json (${localeEntries.size} keys).`)
console.log(`Verified ${localeEntries.size} existing ${localeName}.json entries.`)
return 0
}
@@ -527,19 +460,10 @@ export async function main(root = process.cwd(), options = parseArgs(process.arg
const localeName = fileName.replace(/\.json$/, '')
const localeCatalogPath = path.join(localesDir, fileName)
const localeCatalog = JSON.parse(await fs.readFile(localeCatalogPath, 'utf8'))
if (options.fix) {
const repaired = repairLocaleParity(catalog, localeCatalog)
if (repaired > 0) {
await fs.writeFile(localeCatalogPath, `${JSON.stringify(localeCatalog, null, 2)}\n`, 'utf8')
console.log(`Repaired ${fileName} parity (${repaired} key update(s)).`)
}
}
const exitCode = verifyLocaleParity(catalog, localeName, localeCatalog)
const exitCode = verifyLocaleCatalog(catalog, localeName, localeCatalog)
if (exitCode !== 0) {
if (!options.fix) {
console.error('')
console.error('Run `pnpm run sync:localization-catalog` to repair locale parity.')
}
console.error('')
console.error('Fix or retire the existing target entry in a localization PR.')
return exitCode
}
}
@@ -33,7 +33,7 @@ function makeProject({ sourceText, enCatalog = {}, esCatalog = {} }) {
}
describe('verify-localization-catalog', () => {
it('bootstraps missing catalog entries from string fallbacks', async () => {
it('bootstraps English entries without fabricating target translations', async () => {
const { root, localesDir } = makeProject({
sourceText:
"import { translate } from '@/i18n/i18n'\nexport const label = translate('auto.example.greeting', 'Hello {{name}}', { name: 'Orca' })\n"
@@ -45,12 +45,10 @@ describe('verify-localization-catalog', () => {
expect(readJson(path.join(localesDir, 'en.json'))).toEqual({
auto: { example: { greeting: 'Hello {{name}}' } }
})
expect(readJson(path.join(localesDir, 'es.json'))).toEqual({
auto: { example: { greeting: 'Hello {{name}}' } }
})
expect(readJson(path.join(localesDir, 'es.json'))).toEqual({})
})
it('repairs stale locale keys and interpolation mismatches', async () => {
it('never overwrites mismatched translations or removes target-only entries', async () => {
const { root, localesDir } = makeProject({
sourceText:
"import { translate } from '@/i18n/i18n'\nexport const label = translate('auto.example.greeting', 'Hello {{name}}', { name: 'Orca' })\n",
@@ -63,13 +61,29 @@ describe('verify-localization-catalog', () => {
}
})
await expect(verifyLocalizationCatalog(root, { fix: true })).resolves.toBe(0)
await expect(verifyLocalizationCatalog(root, { fix: true })).resolves.toBe(1)
expect(readJson(path.join(localesDir, 'es.json'))).toEqual({
auto: { example: { greeting: 'Hello {{name}}' } }
auto: {
example: { greeting: 'Hola' },
stale: { removed: 'Viejo' }
}
})
})
it('accepts sparse target catalogs when existing placeholders match', async () => {
const { root } = makeProject({
sourceText:
"import { translate } from '@/i18n/i18n'\nexport const label = translate('auto.example.greeting', 'Hello {{name}}', { name: 'Orca' })\n",
enCatalog: {
auto: { example: { greeting: 'Hello {{name}}', untranslated: 'English only' } }
},
esCatalog: { auto: { example: { greeting: 'Hola {{name}}' } } }
})
await expect(verifyLocalizationCatalog(root, { fix: false })).resolves.toBe(0)
})
it('does not invent values for keys without string fallbacks', async () => {
const { root, localesDir } = makeProject({
sourceText:
@@ -0,0 +1,123 @@
import { execFile } from 'node:child_process'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { promisify } from 'node:util'
import { pathToFileURL } from 'node:url'
const execFileAsync = promisify(execFile)
const EN_CATALOG_PATH = path.join('src', 'renderer', 'src', 'i18n', 'locales', 'en.json')
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/g
function flattenCatalog(value, prefix = '', entries = new Map()) {
if (typeof value === 'string') {
entries.set(prefix, value)
return entries
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return entries
}
for (const [key, child] of Object.entries(value)) {
flattenCatalog(child, prefix ? `${prefix}.${key}` : key, entries)
}
return entries
}
function placeholders(value) {
return [...(value.match(PLACEHOLDER_RE) ?? [])].sort().join('|')
}
export function compareExtraction(extractedCatalog, englishCatalog) {
const extracted = flattenCatalog(extractedCatalog)
const english = flattenCatalog(englishCatalog)
const dynamicDefaults = [...extracted.entries()]
.filter(([, value]) => value.length === 0)
.map(([key]) => key)
const missingFromEnglish = [...extracted.keys()].filter((key) => !english.has(key))
const orphans = [...english.keys()].filter((key) => !extracted.has(key))
const fallbackDrift = []
const placeholderMismatches = []
for (const [key, extractedValue] of extracted) {
const englishValue = english.get(key)
if (
extractedValue.length === 0 ||
englishValue === undefined ||
englishValue === extractedValue
) {
continue
}
fallbackDrift.push(key)
if (placeholders(extractedValue) !== placeholders(englishValue)) {
placeholderMismatches.push(key)
}
}
return {
extracted,
dynamicDefaults,
missingFromEnglish,
orphans,
fallbackDrift,
placeholderMismatches
}
}
function printKeys(label, keys) {
if (keys.length === 0) {
return
}
console.error(`${label}:`)
for (const key of keys.slice(0, 20)) {
console.error(` ${key}`)
}
if (keys.length > 20) {
console.error(` ...and ${keys.length - 20} more`)
}
}
async function extractToTemporaryCatalog(root, tempDir) {
const cliPath = path.join(root, 'node_modules', 'i18next-cli', 'dist', 'esm', 'cli.js')
const outputPattern = path.join(tempDir, '{{language}}.json')
// Why: extraction output is evidence for this check, not another committed
// catalog that feature authors must keep synchronized.
await execFileAsync(process.execPath, [cliPath, 'extract', '--sync-primary', '--quiet'], {
cwd: root,
env: {
...process.env,
ORCA_I18N_EXTRACTION_OUTPUT: outputPattern.split(path.sep).join('/')
}
})
return JSON.parse(await fs.readFile(path.join(tempDir, 'en.json'), 'utf8'))
}
export async function main(root = process.cwd()) {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'orca-i18next-extraction-'))
try {
const [extractedCatalog, englishCatalog] = await Promise.all([
extractToTemporaryCatalog(root, tempDir),
fs.readFile(path.join(root, EN_CATALOG_PATH), 'utf8').then(JSON.parse)
])
const result = compareExtraction(extractedCatalog, englishCatalog)
console.log(
`Extracted ${result.extracted.size} keys; ${result.dynamicDefaults.length} dynamic defaults are report-only, ${result.orphans.length} existing English entries are not statically referenced, and ${result.fallbackDrift.length} inline defaults differ.`
)
if (result.missingFromEnglish.length > 0 || result.placeholderMismatches.length > 0) {
printKeys('Extracted keys missing from en.json', result.missingFromEnglish)
printKeys('Extracted defaults with incompatible placeholders', result.placeholderMismatches)
return 1
}
return 0
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exit(await main())
}
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import { compareExtraction } from './verify-localization-extraction.mjs'
describe('verify-localization-extraction', () => {
it('reports legacy drift without requiring a committed disposition database', () => {
const result = compareExtraction(
{ menu: { open: 'Open now {{name}}' } },
{ menu: { open: 'Open {{name}}', legacy: 'Legacy copy' } }
)
expect(result.orphans).toEqual(['menu.legacy'])
expect(result.fallbackDrift).toEqual(['menu.open'])
expect(result.placeholderMismatches).toEqual([])
})
it('reports dynamic defaults without treating empty extractor values as catalog copy', () => {
const result = compareExtraction(
{ menu: { dynamic: '' } },
{ menu: { dynamic: '{{count}} items' } }
)
expect(result.dynamicDefaults).toEqual(['menu.dynamic'])
expect(result.fallbackDrift).toEqual([])
expect(result.placeholderMismatches).toEqual([])
})
it('rejects undeclared keys even when their defaults are dynamic', () => {
const result = compareExtraction({ menu: { dynamic: '' } }, {})
expect(result.missingFromEnglish).toEqual(['menu.dynamic'])
})
it('rejects missing English declarations and incompatible placeholders', () => {
const result = compareExtraction(
{
menu: {
missing: 'Missing',
open: 'Open {{name}}'
}
},
{ menu: { open: 'Open {{path}}' } }
)
expect(result.missingFromEnglish).toEqual(['menu.missing'])
expect(result.placeholderMismatches).toEqual(['menu.open'])
})
})
+3 -14
View File
@@ -38,6 +38,7 @@ import { join, resolve } from 'node:path'
// `node_modules`). If electron-builder ever drops it, promote this to a
// direct devDependency in package.json.
import { extractFile, listPackage } from '@electron/asar'
import { BUILD_IDENTITY_RE, WRITE_KEY_RE } from './telemetry-bundle-constant-patterns.mjs'
// Why resolve from import.meta.url instead of cwd: a release runner (or a
// developer debugging locally) may invoke this script from a non-root cwd.
@@ -118,20 +119,8 @@ for (const m of asarMatches) {
// Why these regexes: electron-vite's `define` block substitutes the bare
// identifiers `ORCA_BUILD_IDENTITY` and `ORCA_POSTHOG_WRITE_KEY` with their
// JSON-stringified values at build time. `src/main/telemetry/client.ts`
// then assigns those into module-local consts named `BUILD_IDENTITY` and
// `WRITE_KEY`. electron-vite's main config is not minified (Vite default for
// Electron main builds), so Rollup emits the substituted constants verbatim
// as `const BUILD_IDENTITY = "stable";`. Match that exact emitted shape so a
// regression — e.g. the env var unset and the substitution falling back to
// literal `null` — fails the grep instead of slipping through as a falsy-
// but-stringy value. NOTE: if `build.minify` is ever enabled on the main
// bundle, esbuild/terser will rename top-level consts and this regex must
// be revisited (or replaced with a value-based assertion).
//
// WRITE_KEY char class includes `_` and `-` because PostHog project API
// keys use URL-safe base64 alphabet beyond `phc_`.
const BUILD_IDENTITY_RE = /const\s+BUILD_IDENTITY\s*=\s*"(rc|stable)"/
const WRITE_KEY_RE = /const\s+WRITE_KEY\s*=\s*"(phc_[A-Za-z0-9_-]+)"/
// then assigns those into module-local declarations named `BUILD_IDENTITY`
// and `WRITE_KEY`. Rollup may preserve `const` or lower it to `var`.
function verifyAsar(asarPath) {
console.log(`Verifying ${asarPath}`)
@@ -0,0 +1,81 @@
#!/usr/bin/env node
import { performance } from 'node:perf_hooks'
import process from 'node:process'
import { createStore } from 'zustand/vanilla'
const SUBSCRIBERS = Number.parseInt(process.env.ORCA_ZUSTAND_BENCH_SUBSCRIBERS ?? '2500', 10)
const WRITES = Number.parseInt(process.env.ORCA_ZUSTAND_BENCH_WRITES ?? '2000', 10)
const MAX_MILLISECONDS_PER_WRITE = Number.parseFloat(
process.env.ORCA_ZUSTAND_BENCH_MAX_MS_PER_WRITE ?? '5'
)
for (const [name, value] of [
['ORCA_ZUSTAND_BENCH_SUBSCRIBERS', SUBSCRIBERS],
['ORCA_ZUSTAND_BENCH_WRITES', WRITES],
['ORCA_ZUSTAND_BENCH_MAX_MS_PER_WRITE', MAX_MILLISECONDS_PER_WRITE]
]) {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`${name} must be positive, received ${value}`)
}
}
function measureRound() {
const stableProjection = Object.freeze({ activeRepoId: 'repo-1' })
const store = createStore(() => ({ unrelatedWrite: 0, stableProjection }))
let selectorRuns = 0
let renderInvalidations = 0
const unsubscribe = Array.from({ length: SUBSCRIBERS }, () => {
let previous = store.getState().stableProjection
return store.subscribe((state) => {
selectorRuns += 1
const next = state.stableProjection
if (!Object.is(previous, next)) {
renderInvalidations += 1
}
previous = next
})
})
const start = performance.now()
for (let index = 1; index <= WRITES; index += 1) {
store.setState({ unrelatedWrite: index })
}
const elapsed = performance.now() - start
for (const release of unsubscribe) {
release()
}
return { elapsed, selectorRuns, renderInvalidations }
}
measureRound()
const rounds = Array.from({ length: 5 }, measureRound).sort(
(left, right) => left.elapsed - right.elapsed
)
const median = rounds[2]
const expectedSelectorRuns = SUBSCRIBERS * WRITES
const millisecondsPerWrite = median.elapsed / WRITES
if (median.selectorRuns !== expectedSelectorRuns) {
throw new Error(
`Expected ${expectedSelectorRuns} selector runs, observed ${median.selectorRuns}; update the fan-out model.`
)
}
if (median.renderInvalidations !== 0) {
throw new Error(
`${median.renderInvalidations} unrelated writes changed a stable selector result.`
)
}
console.log(
`Zustand fan-out: ${SUBSCRIBERS} subscribers × ${WRITES} unrelated writes = ${expectedSelectorRuns.toLocaleString()} selector runs`
)
console.log(
`Median ${median.elapsed.toFixed(2)} ms total, ${millisecondsPerWrite.toFixed(4)} ms/write, 0 render invalidations`
)
if (process.argv.includes('--check') && millisecondsPerWrite > MAX_MILLISECONDS_PER_WRITE) {
console.error(
`Zustand fan-out exceeded ${MAX_MILLISECONDS_PER_WRITE.toFixed(2)} ms/write. Inspect selector work and store subscription growth.`
)
process.exit(1)
}
+4
View File
@@ -9,7 +9,9 @@
"../src/main/agent-hooks/hooks-json-read.ts",
"../src/main/agent-hooks/installer-utils.ts",
"../src/main/agent-hooks/installer-utils-remote.ts",
"../src/main/agent-hooks/local-agent-cli-presence.ts",
"../src/main/agent-hooks/managed-agent-hook-controls.ts",
"../src/main/agent-hooks/managed-agent-hook-registry.ts",
"../src/main/amp/hook-service.ts",
"../src/main/antigravity/hook-service.ts",
"../src/main/claude/hook-settings.ts",
@@ -27,6 +29,7 @@
"../src/main/codex/codex-config-settings-removal.ts",
"../src/main/codex/codex-config-settings-upsert.ts",
"../src/main/codex/codex-home-paths.ts",
"../src/main/codex/codex-managed-home-resource-copy-marker.ts",
"../src/main/codex/codex-hook-identity.ts",
"../src/main/codex/codex-hook-trust-grant.ts",
"../src/main/codex/codex-managed-trust-reconciliation.ts",
@@ -67,6 +70,7 @@
"../src/main/kimi/kimi-hook-config-toml.ts",
"../src/main/openclaude/hook-service.ts",
"../src/main/rolling-file-backup.ts",
"../src/main/startup/hydrate-shell-path.ts",
"../src/main/runtime/runtime-metadata.ts",
"../src/main/win32-utils.ts"
],
+1
View File
@@ -4,6 +4,7 @@
"../electron.vite.config.*",
"../build-plugins/**/*",
"../src/main/**/*",
"../src/renderer/src/lib/skill-freshness-display-status.ts",
"../src/preload/**/*",
"../src/shared/**/*",
"../src/relay/**/*",
+2
View File
@@ -15,6 +15,8 @@ export default defineConfig({
},
test: {
environment: 'node',
// Why: Node 26's undefined Web Storage globals prevent Vitest from installing happy-dom's.
execArgv: ['--no-experimental-webstorage'],
include: [
'src/**/*.test.ts',
'src/**/*.test.tsx',
Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -243,9 +243,10 @@ 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 :** Les groupes 1 et 2 sont complets — vous pouvez rejoindre le troisième.
- **WeChat :** Si le groupe 5 est complet, vous pouvez rejoindre le groupe 6.
<img src="../assets/wechat-qr.jpg" alt="QR code WeChat de la communauté Orca" width="160" />
<img src="../assets/wechat-qr.jpg" alt="QR code WeChat groupe 5 de la communauté Orca" width="160" style="margin-right: 16px;" />
<img src="../assets/wechat-qr-group6.jpg" alt="QR code WeChat groupe 6 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.
+3 -2
View File
@@ -235,9 +235,10 @@ yay -S stably-orca-bin
- **Discord:** 加入 **[Discord](https://discord.gg/fzjDKHxv8Q)** 社区。
- **Twitter / X:** 关注 **[@orca_build](https://x.com/orca_build)** 获取更新和公告。
- **微信:** 其他群已满,现在加入第 5 群。
- **微信:** 如果第 5 群已满,可加入第 6 群。
<img src="../assets/wechat-qr.jpg" alt="Orca 社区微信群二维码" width="160" />
<img src="../assets/wechat-qr.jpg" alt="Orca 社区微信第 5 群二维码" width="160" style="margin-right: 16px;" />
<img src="../assets/wechat-qr-group6.jpg" alt="Orca 社区微信第 6 群二维码" width="160" />
- **反馈与想法:** 我们发布很快。缺少什么功能?[提交功能请求](https://github.com/stablyai/orca/issues)。
- **隐私:** 查看[隐私与遥测文档](https://www.onorca.dev/docs/telemetry),了解 Orca 收集哪些匿名使用数据以及如何退出。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,576 @@
# Preserve active orchestration work across contract updates
Status: implementation validation
Date: 2026-07-28
## Decision
Do not block updates, restart active workers, or revive the retired scheduler.
On first launch after the contract update, atomically adopt the durable
pre-Run graph into one ordinary lightweight Run. Preserve every Task,
Dispatch, message, gate, terminal, PTY, process, worktree, and prompt
identifier. Mark only the pre-update Dispatch attempts as legacy-contract
attempts and accept the exact command shapes already pasted into those
workers through a narrow adapter.
This is state migration plus a per-Dispatch protocol adapter. It is not a
second scheduler:
- the migrated Run owns the preserved graph and coordinator inbox;
- legacy attempts keep their original protocol until they settle;
- every new attempt uses the current protocol, even on a preserved Task; and
- no placement, polling, concurrency, or deadline loop is resurrected.
## User-visible outcome
- The update installs immediately.
- Existing coordinator and worker terminals keep the same process, PTY,
worktree, prompt, Task ID, Dispatch ID, and history.
- Orca does not inject text, focus a tab, or start a replacement editor.
- The legacy graph appears in an automatically adopted, normal Run instead of
being stranded in `run_legacy_local`.
- An original live worker with retained launch proof can keep using the
`heartbeat`, `worker_done`, `escalation`, `ask`, and `check` commands in its
existing prompt.
- An original live coordinator can keep listing and settling preserved Tasks,
dispatch remaining work with current commands, check direct legacy mail, and
use the pre-cutover `reply` CLI guidance.
- Unprovable, stale, ambiguous, or already-settled rows remain inspectable and
unmistakably read-only. They never advertise Reply or Ack actions.
- A pre-update WSL process whose older launcher did not propagate the hidden
token remains lifecycle read-only. Its assignment, process, terminal, and
filesystem work remain valid and inspectable.
The retired `orchestration run`, `coordinator-start`, and `coordinator-stop`
scheduler commands remain retired. Their in-memory loop cannot be reconstructed
truthfully. The coordinator agent and all durable work remain accessible; it
explicitly dispatches any ready work that was never assigned before the update.
## Evidence and constraints
- Schema v7 preserved old `messages`, `tasks`, `dispatch_contexts`, and
`decision_gates` by assigning them to `run_legacy_local`.
- The exact prompt at `8b154d686` used old `send`, `ask`, and consuming
`check` shapes. It omitted `worker_done --outcome`; a subject equal to
`Failed` or beginning `Failed:` represented failure. Its escalation carried
`taskId` but no `dispatchId`.
- `reply` existed in the old CLI and message formatter guidance, not in that
pinned worker prompt.
- Restored `OrcaRuntime` PTY records currently set `launchToken: null`, and
provider `listProcesses()` does not expose the launch token. Handles and
pane IDs alone therefore cannot authorize compatibility.
- Managed hooks source the owner-only endpoint file on every invocation, so an
old process reaches the new hook server after restart. The hook cache retains
the prior authenticated pane/launch-token association and distinguishes
hydrated rows from events observed by the current runtime.
- Older builds did not propagate `ORCA_AGENT_LAUNCH_TOKEN` through `WSLENV`, and
a running process cannot acquire it retroactively.
- The CLI transport mutation receipt is keyed by rotating runtime
authentication. It cannot deduplicate a legacy mutation across restart.
- Current invalid-capability lifecycle sends deliberately remain as auditable
rejected messages. Compatibility must not silently change that behavior.
- PR #11142 fixes background-tab materialization for newly dispatched workers.
It does not recover a pre-fix worker whose PTY survived an update after its
renderer tab binding was lost; workspace activation can otherwise mistake
that worker for a sleeping provider session and launch `codex resume`.
- The packaged Windows launcher keeps its Electron-as-Node child alive for the
lifetime of a blocking CLI command. A seamless design cannot assume that a
bounded RPC wait releases the installed executable.
## Non-negotiable invariants
1. Adoption never spawns, writes, signals, stops, focuses, or injects a
terminal and never mutates the assigned filesystem. It may restore one
persisted background tab/leaf binding for the exact already-live PTY.
2. Every durable row and identifier survives adoption; only its owning Run and
explicit protocol metadata change.
3. Compatibility is write-once per Dispatch. Migration never classifies a
current retry as legacy.
4. A legacy mutation requires one uniquely resolved legacy attempt or
coordinator principal plus current live proof of its original process tree.
5. Caller-supplied handles, pane keys, Task IDs, Dispatch IDs, or remote
attachments never create authority.
6. Current Dispatch behavior remains unchanged, including auditable rejected
lifecycle messages, capabilities, process incarnation, Run Delivery, and
question semantics.
7. Legacy direct mail and current Run Delivery are disjoint, even when their
Dispatches share one migrated Task and Run.
8. A B-era disconnect may replay accepted work; it may not lose mail or create
a second question, reply, completion, or settlement. A cut after an A-era
ask was already answered is explicitly ambiguous because A persisted no
invocation identity; recovery replays its answer through legacy check before
a same-text question is asked again.
9. Different reply bodies for the same source and principal conflict instead
of creating multiple replies.
10. Folder workspaces, SSH, WSL, remote runtimes, macOS, Linux, and Windows
follow the same proof rules. Missing proof degrades to inspection without
invalidating the retained assignment or filesystem work.
## Storage and atomic adoption
Raise the schema version and add:
```text
dispatch_contexts.contract_version INTEGER NOT NULL
dispatch_contexts.launch_token_hash TEXT
messages.delivery_contract TEXT NOT NULL
CHECK(delivery_contract IN ('legacy_direct', 'current_delivery', 'audit_only'))
legacy_adoptions(
source_run_id PRIMARY KEY,
adopted_run_id UNIQUE NOT NULL,
scheduler_state_lost INTEGER NOT NULL,
adopted_at
)
legacy_compatibility_principals(
id PRIMARY KEY,
run_id NOT NULL,
dispatch_id,
role CHECK(role IN ('worker', 'coordinator')),
host_scope NOT NULL,
terminal_handle NOT NULL,
pane_key NOT NULL,
launch_token_hash NOT NULL,
process_incarnation,
status CHECK(status IN ('committed', 'settled', 'revoked')),
UNIQUE(role, run_id, dispatch_id)
)
legacy_operation_receipts(
principal_id,
operation_key,
method,
payload_hash,
effect_id,
response_json,
completed_at,
PRIMARY KEY(principal_id, operation_key)
)
legacy_mail_receipts(
principal_id,
message_id,
acknowledged_at,
PRIMARY KEY(principal_id, message_id)
)
```
The schema migration runs in one `BEGIN IMMEDIATE` transaction:
1. create a persisted random Run ID and `legacy_adoptions` marker if one does
not already exist;
2. create an ordinary `legacy = 0` Run with a recovery objective;
3. move all `run_legacy_local` Tasks, Dispatches, decision gates, messages,
and question threads to it without changing their IDs, sequence, status, or
content;
4. mark pre-existing direct mail `legacy_direct` and hard-cutover rejection
rows `audit_only`;
5. mark a Dispatch legacy only when it predates the contract column and lacks
a current dispatch capability; and
6. fence any impossible outstanding legacy Delivery, move retained Delivery
history to the adopted Run, assert every Task/Dispatch/gate/message/question/
Delivery Run ID agrees, and leave `run_legacy_local` as an empty read-only
audit tombstone.
Every new Dispatch and message writes the current contract explicitly.
Repeated startup, a partially initialized fixture, and WAL recovery reuse the
persisted adoption marker. Reset and fresh-database paths cannot synthesize
legacy authority.
## Durable principals and live authority
After the hook server and PTY inventory are ready, a one-shot adoption service
may commit compatibility principals. It does not mutate terminal processes or
input.
Before renderer hydration, legacy Dispatch evidence also fences automatic
provider resume for the exact sleeping pane. Once the relevant local daemon or
SSH relay has authoritative inventory, Orca intersects the durable handle,
pane, PTY ID, PTY incarnation, workspace, and host owner. A unique live match
is adopted into its original tab/leaf as inactive background state while the
coordinator tab remains active. Missing, stale, reused, cross-host, or
ambiguous evidence stays fenced and inspectable. Ordinary provider resume is
unblocked only when authoritative inventory proves that exact PTY exited.
At hook-server startup, copy the validated hydrated status rows into an
immutable main-process-only startup snapshot before the server listens or
binds the new listener. Later live events update a separate current-runtime
observation map and cannot overwrite the pre-update commitment source. A
current event is preferred but is not required before the worker's first
post-restart lifecycle command when controller inventory proves the exact
original PTY and process incarnation are live.
For each candidate it resolves the union of:
- durable Dispatch assignee and pane;
- equivalent-pane aliases;
- remote attachment and host ownership;
- coordinator candidates from active legacy coordinator rows, Task creator
handles, Dispatch coordinator/parent provenance, and direct-message
sender/recipient history;
- the restored terminal inventory and process incarnation; and
- the hydrated, previously authenticated hook pane/launch-token association.
The candidate commits only when that union identifies exactly one role and
row. The orchestration database stores a SHA-256 token commitment, never the
raw token. The existing owner-only hook status cache is an input to the
one-time commitment, but a hydrated status is not enough to mutate.
Every compatibility call then requires:
- a hidden launch token and pane inherited by the calling process;
- the immutable pre-update hook commitment, plus either a matching
current-runtime hook event or exact current controller proof of the original
PTY and process incarnation;
- a live PTY with the committed handle, host scope, and process incarnation;
- the committed principal and exact active legacy Dispatch or adopted Run; and
- an operation-specific unique match.
The current hook event or exact controller inventory proves that the process
survived the restart; equality with the pre-update commitment proves it is the
same launch. If the hook cache was absent, the provider cannot report the
proof, the token changed, or any candidate is ambiguous, the row remains
read-only.
For new Dispatches, persist the launch-token hash at dispatch time so later
contract updates do not depend on hook-cache recovery. Never return or log raw
tokens. Add explicit RPC-envelope, structured-log, error, and CLI JSON
redaction tests.
Host scope is runtime-owned:
- local: stable local execution-host identity;
- WSL: stable native execution-host identity plus distro;
- SSH: saved-host authority plus remote pane/process identity; and
- federated runtime: authenticated peer fingerprint plus remote attachment.
`pickRemoteCliEnv`, SSH relay forwarding, WSLENV, and the legacy in-process SSH
fallback must carry the hidden evidence without exposing it in command text or
logs. A local runtime never accepts a raw remote token as local authority.
Pre-update WSL processes without the token cannot use the lifecycle adapter.
For SSH, the host passthrough and in-process fallback stamp the saved target,
`SshRelaySession` connection incarnation, and attachment provenance from the
authenticated runtime channel; caller environment cannot choose them. WSL
stamps the runtime-owned native host and distro in both the full bridge and
fallback bridge. The verifier intersects those stamps with the committed host
scope before considering token or pane evidence.
## Mixed-contract Run and mail routing
The adopted Run is a normal current Run. Tasks belong to it, so a current retry
of a preserved Task naturally routes to the same Run without a cross-Run Task
reference.
Message delivery is selected by `messages.delivery_contract`, not by Run ID:
- old and adapter-created direct messages are `legacy_direct`;
- current sends are `current_delivery`; and
- migration rejections and non-actionable history are `audit_only`.
Current Run Delivery creation, filtering, waiting, and acknowledgment select
only `current_delivery`. Legacy `check` selects only the exact principal's
`legacy_direct` address. History and inbox may show all three with explicit
labels but never attach actions to `audit_only`. Mixed legacy/current attempts
under one Task are covered across send, ask, check, reply, and settlement.
The original coordinator is automatically associated with the adopted Run
after its principal proves live. `run-use` may bind that same principal without
converting legacy Dispatches. A different coordinator cannot silently take
over while a legacy attempt is active; an explicit recovery takeover is a
separate current-contract action and revokes legacy coordinator mutation
authority before rebinding.
When that coordinator uses old direct `check`, the adapter also performs a
non-consuming existence check for `current_delivery` mail. It never mixes the
messages into legacy output; it prints the exact current Run check command and,
after that command creates a Delivery, the exact acknowledgment form. This
keeps an unchanged coordinator informed about current retries without
weakening current Delivery semantics.
## Exact legacy adapter
The CLI keeps accepting the old flags. It sends a hidden compatibility
envelope; the runtime, not the CLI, decides whether legacy behavior applies.
Version B generates a random hidden `compatibilityInvocationId` once per CLI
process and reuses it for every transport reconnect by that invocation.
`legacy_operation_receipts.operation_key` is this invocation ID for B calls.
The method and canonical payload hash must also match.
Attested compatibility calls bypass the rotating-auth generic mutation ledger
after the contract fence routes them to the adapter. Their only dedupe boundary
is the compatibility-principal receipt transaction described here; current
calls continue through the existing generic ledger unchanged.
Version A had no invocation ID. Cross-cutover reconstruction is therefore
limited to effects with a durable semantic identity: final settlement per
Dispatch, still-pending ask per normalized question/options/recipient, reply
per source/principal/body, and read recovery per message. Heartbeat and
escalation remain at-least-once across an A-to-B manual retry because
collapsing identical commands could suppress a legitimate later event. Tests
and guidance do not promise cross-cutover exactly-once behavior for those
repeatable signals.
An A-era ask that was already answered before its output was lost is also
ambiguous. B does not guess whether the same text means retry or a new
question. Its answer is in the durable recovery cohort and the error tells the
agent to run the exact legacy check command first. After that answer is printed
and acknowledged, a new B invocation with identical text creates a new
question. If multiple identical A threads exist, ask fails ambiguous and
recovery check drains them by message sequence; no arbitrary thread is chosen.
### Lifecycle `send`
- `heartbeat` and `worker_done`: resolve Task + Dispatch from the payload, then
intersect them with the verified worker principal.
- `escalation`: resolve Task + verified worker principal to exactly one active
legacy Dispatch because the old prompt supplied no Dispatch ID.
- Preserve the old direct coordinator recipient after verifying it belongs to
the adopted Run principal.
- Infer missing `worker_done` outcome as `failed` only when the trimmed subject
is `Failed` or begins `Failed:`; otherwise infer `succeeded`.
- Normalize to the current internal settlement model.
Compatibility validation happens before message insertion. Current lifecycle
handling is not refactored through this prevalidation: its existing
invalid-capability audit message behavior remains locked by regression tests.
The DB commits the direct message, lifecycle effect, and compatibility receipt
in one transaction. If a pre-update `worker_done` already settled the Dispatch,
the adapter reconstructs the matching semantic receipt from the existing
message and settlement. The persisted A settlement is authoritative: the
pinned A reconciler recorded even a `Failed:` subject as completed, so B
returns that original completed result rather than retroactively changing Task
or dependency state. Only a not-yet-accepted B call applies the corrected
failure inference. A conflicting retry fails.
### `check`
Legacy check is at-least-once:
1. a read RPC returns the exact unacknowledged `legacy_direct` message IDs
without changing them;
2. the CLI prints the complete response; and
3. a hidden ack atomically records receipts and marks those exact rows read.
A cut after read and before ack replays the same IDs. `--peek` and `--all`
remain read-only.
Version A could mark a message read before losing its response. On adoption,
every eligible read direct message connected to a still-active legacy attempt
and lacking a post-adoption receipt is inserted into a durable recovery cohort
using `legacy_mail_receipts` with `acknowledged_at = NULL`. Legacy check drains
that stable cohort in bounded sequence-ordered pages before ordinary unread
mail. Each page is labeled
`[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]`, replays until its post-print
ack, and then advances. The cohort never grows after adoption, so every
eligible row is covered without an unbounded response. Settled-at-cutover and
unrelated historical mail do not enter it.
### `ask`
Legacy ask is an idempotent mutation followed by a resumable read:
1. create or find the direct legacy gate/question by verified Dispatch,
normalized question, options, and recipient;
2. atomically commit its message/thread plus operation receipt and return the
stable question ID;
3. wait in bounded read-only slices using that ID; and
4. after printing the answer, hidden-ack that exact answer.
On first adoption, existing pending decision-gate threads are indexed into the
same semantic identity. A retry resumes them instead of creating another gate.
### `reply`
Legacy reply requires the source thread, the verified coordinator principal,
and a unique question owned by the adopted Run. One transaction inserts the
reply, resolves the exact gate, marks the exact source, and completes the
receipt.
An existing pre-update reply with the same source, principal, and body is
reconstructed as success. A second body for the same source/principal is a
conflict, not another reply.
### Coordinator and Task commands
After automatic binding, unchanged Task listing, Task settlement, dispatch,
and gate inspection use the adopted Run. A new attempt receives a current
preamble, capability, launch-token commitment, and current message contract.
No unrelated Task can be created until the migrated coordinator binding is
proven or explicitly taken over.
## Windows update behavior
No compatibility mutation holds a DB transaction while waiting. Windows
legacy `ask` commits the question, prints its stable ID and exact resume
command, then exits before a long wait whenever the installed launcher cannot
prove that the executable is replaceable.
The native packaged launcher sets an internal
`ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER=1` marker for its Electron-as-Node child.
After the DB commit, legacy `ask` writes and flushes the stable question ID and
the exact resume command, then exits with a documented `75` resume-required
status. The command name comes only from the validated inherited
`ORCA_CLI_COMMAND` packaged enum: `orca` for native packaged terminals or
`orca-ide` for packaged WSL. Arbitrary environment text is rejected rather
than rendered. The launcher propagates status 75. No long wait starts on this
path.
This compatibility path makes the accepted question recoverable without
claiming Windows updater or uninstaller process ownership. Generic executable
drain, replacement, and uninstall behavior remains separate updater
reliability work and requires its own Windows artifact proof.
## Rendering and authoritative guidance
Formatting is authority-aware:
- `[LEGACY COMPATIBILITY]`: live and attested; show only supported commands;
- `[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]`: one bounded cutover replay;
- `[LEGACY READ-ONLY]`: retained but unactionable; no Reply or Ack hint; and
- current messages: unchanged.
Update `skill-guides/orchestration.md` and regenerate its bundled source. The
guide explains automatic adoption, exact continuity, the retired scheduler
boundary, proof failures, current retries, and the Windows ask resume fallback.
It must not tell agents to restart a worker whose Dispatch was adopted.
Formatting cannot use database provenance alone; current liveness and
attestation are runtime facts. Recovery may reveal the exact already-live PTY
once as an inactive background tab. It never creates a PTY or process, writes
input, switches workspaces, focuses, splits, or injects a terminal.
## Deterministic verification
### Schema and authority
- Migrate exact pre-v7, hard-cutover, current, partial, WAL, reset, and fresh
fixtures; preserve IDs and row counts and reuse one adopted Run.
- Assert Task, Dispatch, decision-gate, message, question-thread, and retained
Delivery Run ownership agrees; outstanding legacy Deliveries are fenced.
- Prove current retries in the adopted Run remain current across repeated
migrations.
- Test unique worker/coordinator import from prior hook commitment plus current
live hook, and every forged, stale, wrong-host, wrong-pane, wrong-process,
duplicate-token, duplicate-role, equivalent-pane, and remote-attachment
ambiguity.
- Race a current-runtime hook before adoption and prove the immutable startup
commitment is neither overwritten nor confused with live proof.
- Cover manual coordinators with no `coordinator_runs` row and reject
conflicting candidates from Task, Dispatch, and message provenance.
- Assert all legacy authority failures occur before message/receipt/gate/state
effects while current invalid-capability rejection remains unchanged.
- Assert tokens are absent from RPC logs, errors, CLI output, and orchestration
storage.
### Exact old behavior and replay
- Replay the pinned prompt's heartbeat, successful and `Failed:`
`worker_done`, task-only escalation, ask, and consuming check.
- Replay the old formatter's reply command separately.
- Cover every operation once after B and once accepted by A immediately before
the cutover where a durable semantic identity exists, including the bounded
recovery replay for an A check and preservation of A's persisted settlement.
- Prove one B invocation reuses its invocation ID across reconnect, while
separate identical heartbeat/escalation invocations remain distinct.
- Inject real transport cuts after check read, ask commit, reply commit, and
completion commit; prove stable IDs and one receipt/effect.
- Page a recovery cohort larger than one response limit across repeated
disconnects and prove every eligible row appears until ack, then never again.
- Test mixed legacy/current attempts for every send/check/ask/reply route and
prove current Delivery cannot consume legacy direct mail and old coordinator
check only advertises the exact non-consuming current Delivery command.
### Live two-launch E2E
The focused renderer/daemon restart regression reconstructs the rc.3-era
orphan artifact in a shared candidate profile: a durable legacy Dispatch,
sleeping provider record, and live daemon PTY with its visual binding removed.
It proves startup restores the exact PID, PTY/incarnation, handle, tab/leaf,
Task, and Dispatch; emits no provider-resume spawn or interruption; preserves
coordinator focus; and remains inert after switching away and back. This
candidate/candidate fixture covers the renderer regression deterministically
but does not replace the distinct installed-A/candidate-B packaged cutover
journey below. It does not claim packaged Windows updater behavior.
Use distinct installed A and B artifacts plus an external append-only fake
agent spawn ledger:
1. launch A with coordinator and worker terminals and record the replaceable A
desktop/main PID, runtime ID, version, and artifact hash separately from the
durable daemon, coordinator, and worker identities;
2. accept one command with durable semantic identity on A—pending ask, reply,
completion, or check—cut its response, close A without stopping the daemon
or fake agent, install B, and prove B owns the relaunched runtime;
3. assert A's desktop/main process and runtime are gone, B has a different
desktop/main PID, runtime ID, version, and artifact hash, while the daemon
plus coordinator/worker PID and start time, PTY/session, terminal handle,
tab/leaf/pane, worktree, Task, and Dispatch remain byte-identical;
4. prove that exact pending ask, reply, completion, or check resumes or
deduplicates on B; do not use heartbeat, escalation, or an already-answered
A ask as an exactly-once oracle;
5. run the remaining exact old lifecycle commands and settle the same
Dispatch;
6. switch away and back and assert no second spawn/tab and no
`Conversation interrupted`; and
7. create a current attempt on preserved state and prove current grammar.
The append-only spawn ledger and an append-only interruption ledger, daemon
inventory, runtime terminal inventory, and every viewer DOM independently
prove one launch, process, handle, tab, and no `Conversation interrupted`.
Transport fault injection cuts the real CLI connection after check output but
before ack, and after ask, reply, and completion commit. Re-run the byte-exact A
argv under B and assert stable IDs plus exactly one message, gate, reply,
settlement, and receipt.
Retain #11142's local visible-inactive test and extend its oracle beyond DOM
counting with the ledgers and host inventory.
Run separate remote journeys:
- headed paired server A-to-B with a surviving external client;
- headless `orca serve` A-to-B with the same surviving-client oracle;
- both prove the remote PID/PTy continuity, both viewer states, zero
client-local handles/tabs, and wrong-host evidence with zero effects;
- Docker SSH restart/reconnect with a remote proof file binding the process,
pane, and token commitment;
- physical Windows WSL coverage proving the correct distro and rejecting a
wrong-distro token; and
- a git-independent folder workspace parameter through the same adoption
journey.
Packaged Windows updater continuity is outside this migration gate. A separate
updater change must prove candidate-built A/B install, executable release,
resume, and normal uninstall behavior on Windows before making that guarantee.
## Implementation slices
1. Schema, atomic Run adoption, per-row contract provenance, and mixed mailbox.
2. Hook-commitment import, live principal verifier, host scopes, and redaction.
3. Atomic compatibility receipts plus lifecycle send/escalation.
4. Check read/ack and cutover recovery replay.
5. Ask create/resume, reply conflict semantics, and coordinator binding.
6. CLI hidden envelope, SSH/WSL forwarding, Windows resume fallback.
7. Authority-aware formatting and the version-matched orchestration guide.
8. Two-launch, remote, platform, and #11142 regression coverage.
9. Focused unit/type/lint/E2E checks and final adversarial review.
## Definition of done
- Updates are never blocked.
- Every durable legacy Task is owned by an ordinary visible Run.
- Every provably original live legacy Dispatch can finish through its existing
prompt without changing process or worktree.
- New attempts use the current contract and mailbox in that same Run.
- No authority comes from IDs or handles alone.
- No adoption step starts, stops, writes, signals, focuses, splits, or injects
a terminal. Exact live-PTY recovery may restore one inactive background
renderer tab/leaf binding.
- B-era disconnects replay or deduplicate instead of losing or duplicating
work; A-era repeatable signals and already-answered asks follow the explicit
ambiguity recovery rules.
- Lost scheduler memory is reported precisely without stranding durable state.
- Local/background, remote, SSH, WSL, folder, and Windows guarantees have
deterministic proof.
- The PR contains this plan, focused implementation, and no unrelated changes.
@@ -0,0 +1,238 @@
# Workspace Space Scan Resource Bounds
## Problem
Resource Manager scans every non-prunable worktree to calculate workspace disk usage. A fleet with
hundreds of worktrees can make Orca and the host unresponsive while the scan is active.
Two independent resource failures are possible:
- macOS and Linux run recursive `du` processes for several worktrees at once. Competing full-tree
metadata walks can saturate local storage and stall unrelated applications.
- Windows, unsupported `du` variants, timeouts, and filesystem errors use a portable recursive
walker. The old walker allocated a promise and retained result node for every entry without a
capacity limit, so a large worktree could exhaust the main or relay process heap.
The July 27 incident occurred with 298 Orca worktrees. Renderer telemetry remained far below its
heap limit, while quitting Orca immediately restored host responsiveness. The installed binary also
contained the uncapped portable fallback.
## Root Cause
`src/main/workspace-space-analysis.ts` admitted up to three worktree scans per repository while
scanning two repositories concurrently. That allowed as many as six local `du` traversals.
Both the desktop portable walker and `src/relay/workspace-space-scan.ts` recursively called
`Promise.all(entries.map(...))`. Filesystem operation limiters capped active `stat` and `readdir`
calls, but they did not cap queued promises, retained paths, directory arrays, or aggregate result
nodes. A failed `du` scan then repeated the same traversal through this higher-memory path.
Shared fixed-worker traversal and capacity primitives already exist, but the active desktop and
relay scanners did not use them.
## Goals
- Keep the host responsive while scanning hundreds of local worktrees.
- Bound portable traversal memory before admitting filesystem entries.
- Preserve size totals, symlink behavior, top-level compaction, partial failures, cancellation, and
progress reporting.
- Apply equivalent bounds to local, folder-workspace, WSL, and SSH relay paths.
- Preserve the current WSL-aware Git worktree listing and abort signals.
## Non-Goals
- Make a 298-worktree scan finish quickly.
- Change Resource Manager UI, progress copy, deletion rules, or scan persistence.
- Exclude `node_modules`, build output, or other directories from reported size.
- Change Git worktree discovery or cleanup behavior.
- Introduce polling, caching, or background automatic scans.
## Design
### Local disk admission
Create one scan-wide limiter for local worktree traversal. Repository discovery and remote scans can
remain concurrent, but every local worktree must acquire the single local slot before invoking
`du` or the portable walker.
The slot is global to one Resource Manager scan, not per repository. This changes the maximum local
full-tree traversal count from six to one. Cancellation rejects queued admissions and stops the
active child process through the existing `AbortSignal`.
### Remote fallback admission
The desktop-side request-by-request SSH fallback runs its traversal inside the desktop main
process, so its budget is charged against Orca's heap rather than the remote host. Repository and
worktree concurrency alone would let six of these traversals hold six independent budgets at once.
A second scan-wide limiter admits at most two, capping aggregate admission at 2 × 64 MiB instead of
6 × 64 MiB. Bulk relay scans stay outside this limiter: their traversal memory lives on the remote
host, one hard capacity per request.
### Fixed-worker portable traversal
Use `scanWorkspaceSpaceEntryTree` for desktop local fallback, desktop remote-provider fallback, and
relay fallback scans. The traversal:
- owns only the configured number of live entry jobs;
- preserves source order and aggregate sizes;
- uses iterative directory frames instead of recursive promise fanout;
- stops on cancellation and removes its abort listener;
- treats unreadable or disappearing entries as partial failures;
- does not follow symlink targets.
Top-level `du` result processing uses `mapWithConcurrency` so unusually wide workspace roots do not
allocate one live operation per entry.
### Capacity admission
Every portable entry is admitted through `WorkspaceSpaceScanBudget` before retention:
- maximum entries in any one directory listing: 100,000;
- maximum estimated live scan state per worktree traversal: 64 MiB.
The entry cap is per listing rather than per traversal because only a single directory's width is
fixed by directory shape. A traversal-wide entry counter is charged by every worker holding a
listing at once, so its verdict scales with the configured concurrency: at 48 workers, 48 × 2,100
files (100,848 entries) was rejected while 100 × 1,500 (150,100 entries, 50% more) was admitted.
Aggregate live retention stays bounded by the 64 MiB byte cap, which all concurrent listings share.
The retained-byte estimate includes entry name UTF-16 storage plus conservative per-entry object
overhead, and each listing's parent path once. The parent path is charged per listing rather than
per entry because a listing's entries all share a single parent-path string; charging it per entry
multiplied it by the directory's width, so the 64 MiB cap tracked checkout depth instead of live
heap and rejected the layouts above once the worktree path passed ~58 characters. These are
admission limits, not post-allocation observations.
The budget measures what the traversal is holding **right now**, not what it has ever seen. A
directory listing is charged when admitted and released once every one of its entries has been
dispatched, so the caps bound the widest concurrent frontier rather than total tree size. This
distinction decides real workspaces: a cumulative counter charged an ordinary 76,788-entry Orca
worktree 61.2 MiB of its 64 MiB cap — 4% headroom, and tipping over purely because a longer branch
name lengthens every absolute path. The same worktree peaks between 4 MiB and 8 MiB of live state.
Neither cap may depend on where a user checks out their worktrees, so the estimate charges each
absolute path once per listing rather than once per entry. A live cap depends only on directory
shape, so it rejects genuinely pathological layouts (one directory holding six figures of entries)
and nothing else.
Top-level directory enumeration for the `du` path uses the same budget. A capacity error must not
fall through into the portable walker, because that would repeat an already rejected traversal.
### Failure behavior
- Desktop local scan: return an unavailable worktree row with the capacity error. `classifyError`
maps `WorkspaceSpaceScanCapacityError` to `unavailable` rather than `error`, because the workspace
is intact and readable, just too large to size safely. The Resource Manager renders that row as
"Unavailable" instead of "Failed".
- SSH relay scan: reject the bulk scan; the desktop provider converts it to an unavailable row.
- Generic `du` failure: use the bounded portable fallback.
- Cancellation: propagate the scan-cancelled error rather than converting it to a row failure.
- Missing or unreadable entries below the capacity limit: preserve existing partial-result
behavior.
### Platform and workspace parity
- macOS/Linux: one local `du` at a time; bounded Node fallback.
- Windows: one local bounded Node traversal at a time.
- WSL: retain `getLocalProjectWorktreeGitOptions` and its selected distro for worktree discovery;
UNC/native filesystem traversal uses the same local admission and capacity bounds.
- SSH: prefer the bulk relay scan; both the relay and the request-by-request compatibility fallback
use the shared capacity model.
- Folder workspaces: the synthetic main worktree passes through the same local limiter and scanner.
## Data Flow
1. Resource Manager starts one deduplicated scan and creates an abort controller.
2. Repository workers list worktrees with existing host-specific Git options and the scan signal.
3. Remote worktrees continue through provider concurrency. Local worktrees wait for the scan-wide
local slot.
4. POSIX local or relay scans try `du`.
5. Top-level entries are admitted against the scan budget and projected with bounded concurrency.
6. If `du` fails generically, the fixed-worker portable traversal scans within the same limits.
7. Capacity failures become unavailable rows; successful results keep existing compaction and
progress behavior.
## Alternatives Considered
### Only reduce `du` concurrency
This protects local storage but leaves Windows, SSH compatibility fallback, and failed `du` scans
capable of unbounded heap growth.
### Only add traversal capacity limits
This prevents OOM but still permits several recursive `du` processes to compete for local storage,
which does not address the observed whole-host stall.
### Remove the portable fallback
This would fail Windows scans and POSIX environments where `du -d` is unavailable or errors during
active filesystem churn.
### Skip dependency and build directories
This would make Resource Manager under-report the directories users most often want to reclaim.
## Measurement and Performance Budget
The deterministic regression measurements are:
- peak simultaneous local `du` calls across repositories: exactly one;
- peak simultaneous desktop-side SSH fallback traversals across repositories: at most two;
- portable traversal live entry jobs: no more than its configured worker count;
- portable entries in any one directory listing: at most 100,000;
- estimated live portable state: at most 64 MiB per worktree; an ordinary 76,788-entry worktree
peaks between 4 MiB and 8 MiB;
- progress and final row counts remain unchanged for scans below the limits.
A follow-up diagnostic span should record strategy (`du` or portable), worktree duration, fallback
reason, admitted entry count, estimated retained bytes, and peak local scan concurrency. It must
record counts and identifiers rather than raw paths or directory trees.
## Test Plan
- Desktop integration: force the portable path over its entry budget and assert an unavailable row.
- Relay integration: force the portable path over its entry budget and assert a capacity failure on
both the Windows/portable and POSIX `du` entry points.
- Remote fallback concurrency: hold six SSH fallback traversals across two repositories and assert
no more than two run at once.
- Concurrency: start local worktrees from two repositories, hold the first `du`, and assert the
second does not start until the first completes.
- Shared traversal: verify worker peak, source order, exact-limit success, over-limit failure, deep
trees, partial failures, and cancellation cleanup.
- Existing desktop coverage: local results, symlinks, progress, cancellation, WSL routing, SSH bulk
scans, disconnected SSH, `du` timeout fallback, and IPC deduplication.
- Run `pnpm run typecheck` and `pnpm run build`.
## Rollout
1. Connect the shared fixed-worker traversal and capacity budget to desktop and relay scanners.
2. Add the scan-wide single local traversal slot.
3. Add focused capacity and concurrency regression tests.
4. Ship without a migration or feature flag; behavior below the limits is unchanged.
5. Monitor capacity failures and scan duration before considering a higher local concurrency.
## Risks
- Scanning hundreds of worktrees takes longer because local work is serialized. The feature already
streams progress and allows users to leave the page, and host responsiveness takes priority over
scan throughput.
- A legitimate worktree above the capacity limit is reported unavailable instead of partially
sized. Failing closed avoids presenting an incomplete size as safe deletion evidence. With a live
cap this now requires a pathological directory rather than merely a large repository.
- Because the local slot is acquired inside the per-repository worker pool, a local worktree waiting
on the slot still occupies one of the six in-flight scan slots. On fleets mixing local and SSH
repositories this can delay remote repositories behind serialized local work, even though remote
scans never contend for the local disk.
- Remote hosts can still process concurrent worktrees, but each relay request has an independent
hard capacity and remote concurrency does not contend with the user's local disk.
## Validation
- Focused tests: 36 passed across desktop analysis, IPC, relay, shared traversal, scan budget, and
concurrency primitives.
- TypeScript: all node, CLI, and web projects passed.
- Production build: relay targets, CLI, Electron, renderer, web projection, and macOS native
components completed successfully.
- No live 298-worktree rescan is required for correctness validation; the concurrency and capacity
properties are deterministic tests.
+6
View File
@@ -208,6 +208,12 @@ export const electronViteConfig: UserConfig = {
// Why: forked with ELECTRON_RUN_AS_NODE so @parcel/watcher faults
// can't take down the main process (issue #7547).
'parcel-watcher-process-entry': resolve('src/main/ipc/parcel-watcher-process-entry.ts'),
// Why: forked with ELECTRON_RUN_AS_NODE so it survives a deadlocked
// main thread (macOS 26 AppKit scene-update deadlock) and can record
// the stall for the next launch to report.
'main-thread-hang-watchdog-entry': resolve(
'src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts'
),
// Why: run under ELECTRON_RUN_AS_NODE while the caller blocks on
// spawnSync — codex app-server trust grants need a live event loop
// but must finish before a Codex pane launch proceeds.
+10
View File
@@ -31,6 +31,12 @@
results.appendChild(li)
document.title = 'probes:' + results.children.length
}
function markNavigationInvocation(name) {
var marker = document.createElement('meta')
marker.dataset.navigationProbeInvoked = name
marker.content = 'true'
document.head.appendChild(marker)
}
// Probe 1: fetch() exfiltration — must be blocked by connect-src 'none'.
var cookieValue = 'x'
@@ -73,6 +79,7 @@
// Navigation probes are opt-in so a failed attempt cannot erase the
// network and bridge evidence before the harness observes it.
document.getElementById('navigate-top').addEventListener('click', function () {
markNavigationInvocation('top-navigation')
try {
window.top.location.href = 'https://example.com/'
setTimeout(function () {
@@ -84,6 +91,7 @@
})
document.getElementById('navigate-self').addEventListener('click', function () {
markNavigationInvocation('self-navigation')
try {
window.location.href = 'https://example.com/self-navigation'
setTimeout(function () {
@@ -95,6 +103,7 @@
})
document.getElementById('navigate-anchor-form').addEventListener('click', function () {
markNavigationInvocation('anchor-form-navigation')
var anchor = document.createElement('a')
anchor.href = 'https://example.com/anchor-navigation'
anchor.textContent = 'Navigation probe'
@@ -111,6 +120,7 @@
})
document.getElementById('navigate-meta').addEventListener('click', function () {
markNavigationInvocation('meta-refresh-navigation')
var refresh = document.createElement('meta')
refresh.httpEquiv = 'refresh'
refresh.content = '0;url=https://example.com/meta-refresh'
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig } from 'i18next-cli'
const output =
process.env.ORCA_I18N_EXTRACTION_OUTPUT ?? 'tmp/localization-extraction/{{language}}.json'
export default defineConfig({
locales: ['en'],
extract: {
input: ['src/**/*.{js,jsx,ts,tsx,mts,cts}'],
ignore: [
'**/*.test.*',
'**/*.spec.*',
'**/__tests__/**',
'**/__snapshots__/**',
'**/assets/**'
],
output,
defaultNS: false,
functions: ['t', '*.t', 'translate', 'translateMain'],
useTranslationNames: ['useTranslation'],
sort: true,
disablePlurals: true,
removeUnusedKeys: true
}
})
+18 -25
View File
@@ -15,12 +15,8 @@ import { useLocalSearchParams, useRouter } from 'expo-router'
import { ChevronLeft } from 'lucide-react-native'
import { colors, radii, spacing, typography } from '../../../src/theme/mobile-theme'
import { loadHosts, updateHostNameAndEndpoint } from '../../../src/transport/host-store'
import {
displayHostEndpoint,
endpointPort,
endpointScheme,
normalizeHostEndpoint
} from '../../../src/transport/host-endpoint'
import { displayHostEndpoint } from '../../../src/transport/host-endpoint'
import { resolveHostEndpointEdit } from '../../../src/transport/host-endpoint-edit'
import { useForceReconnect, usePrimeHosts } from '../../../src/transport/client-context'
import type { HostProfile } from '../../../src/transport/types'
@@ -68,27 +64,24 @@ export default function EditHostScreen() {
void load()
}, [load])
const fallbackPort = host ? endpointPort(host.endpoint) : undefined
const fallbackScheme = host ? endpointScheme(host.endpoint) : 'ws'
const normalizedEndpoint = useMemo(
() => normalizeHostEndpoint(address, { fallbackPort, fallbackScheme }),
[address, fallbackPort, fallbackScheme]
const endpointEdit = useMemo(
() => (host ? resolveHostEndpointEdit(host.endpoint, address) : null),
[address, host]
)
const nameTrimmed = name.trim()
const nameChanged = host != null && nameTrimmed.length > 0 && nameTrimmed !== host.name
const endpointChanged =
host != null && normalizedEndpoint.ok && normalizedEndpoint.endpoint !== host.endpoint
const endpointChanged = endpointEdit?.kind === 'changed'
const canSave =
host != null &&
endpointEdit != null &&
nameTrimmed.length > 0 &&
normalizedEndpoint.ok &&
endpointEdit.kind !== 'invalid' &&
(nameChanged || endpointChanged) &&
!saving
async function handleSave() {
if (!host || !hostId || savingRef.current) {
if (!host || !hostId || !endpointEdit || savingRef.current) {
return
}
const nextName = name.trim()
@@ -96,14 +89,14 @@ export default function EditHostScreen() {
setSaveError('Enter a name.')
return
}
if (!normalizedEndpoint.ok) {
setSaveError(normalizedEndpoint.error)
if (endpointEdit.kind === 'invalid') {
setSaveError(endpointEdit.error)
return
}
const willRename = nextName !== host.name
const willUpdateEndpoint = normalizedEndpoint.endpoint !== host.endpoint
if (!willRename && !willUpdateEndpoint) {
const nextEndpoint = endpointEdit.kind === 'changed' ? endpointEdit.endpoint : undefined
if (!willRename && nextEndpoint === undefined) {
router.back()
return
}
@@ -117,7 +110,7 @@ export default function EditHostScreen() {
// other, and a host removed mid-edit throws instead of no-oping.
await updateHostNameAndEndpoint(host.id, {
...(willRename ? { name: nextName } : {}),
...(willUpdateEndpoint ? { endpoint: normalizedEndpoint.endpoint } : {})
...(nextEndpoint !== undefined ? { endpoint: nextEndpoint } : {})
})
} catch (err) {
setSaveError(err instanceof Error ? err.message : 'Failed to save host.')
@@ -140,7 +133,7 @@ export default function EditHostScreen() {
setSaving(false)
router.back()
if (willUpdateEndpoint) {
if (nextEndpoint !== undefined) {
// Why: reconnect is a follow-on side effect of a save that already
// committed — its failure or a hang must not be reported as a save
// failure or block navigating back.
@@ -247,12 +240,12 @@ export default function EditHostScreen() {
(or 6768).
</Text>
{normalizedEndpoint.ok ? (
{endpointEdit == null ? null : endpointEdit.kind !== 'invalid' ? (
<Text style={styles.preview} numberOfLines={2}>
Connects to {normalizedEndpoint.endpoint}
Connects to {endpointEdit.endpoint}
</Text>
) : address.trim().length > 0 ? (
<Text style={styles.previewError}>{normalizedEndpoint.error}</Text>
<Text style={styles.previewError}>{endpointEdit.error}</Text>
) : null}
{saveError ? <Text style={styles.errorText}>{saveError}</Text> : null}
@@ -1,7 +1,9 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import { Animated, AppState, Linking, type AppStateStatus } from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {
Animated,
AppState,
Linking,
type AppStateStatus,
BackHandler,
FlatList,
Image,
@@ -17,6 +19,7 @@ import {
type LayoutChangeEvent,
type ListRenderItem
} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
import AsyncStorage from '@react-native-async-storage/async-storage'

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