Commit Graph
70 Commits
Author SHA1 Message Date
Neil d7123591ce perf(git): pack the loose refs Orca's own fetches leave behind (#17857)
* perf(git): pack the loose refs Orca's own fetches leave behind

Orca strips git's auto-maintenance off every fetch it issues
(GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS) and never compensated, so
nothing in an Orca-driven checkout ever packs refs. One real machine
reached 36,574 loose refs, where `git show-ref -- main` costs 5.2s and
every worktree create pays for it.

Add an idle-time, per-repo `git pack-refs --all --prune`, armed by the
fetches that create the debt. It runs only after ten minutes of quiet on
that repo, only above 1000 loose refs (probed with a walk bounded by that
threshold, not by the backlog), one at a time across the whole app, at
the background admission tier, and never while an agent is working, a
create is prepared or in flight, a worktree removal is deleting refs, the
app is quitting, or the machine is on battery. A user who set
`maintenance.auto=false` or `gc.auto=0` has opted out.

Measured on a 36,001-loose-ref fixture (macOS/APFS, git 2.44):
`show-ref` 5.5-12.2s -> 30-49ms, `for-each-ref` 4.0-10.8s -> 43-48ms.

Also fixes a pre-existing bug the split exposed: `--path-format=absolute`
is ignored before git 2.31, and taking rev-parse's stdout raw collapsed
every repo on such a host onto one fetch-serialization key.

Refs #17828

* perf(git): make idle ref maintenance preemptible and cheaper to probe

The idle veto was one-directional: it stopped a pack from starting during
a create, removal, or agent work, but nothing stopped those from starting
during a pack. A user-clicked Fetch, a branch delete, or a worktree
removal that needed `packed-refs.lock` mid-rewrite could fail with
`unable to create packed-refs.lock` -- a git error with no visible cause.

Make the pack cancellable end to end. An AbortSignal now reaches the
`pack-refs` child and both pre-pack probes, and `pause()` aborts what is
running, waits for it to actually stop, and holds a suspension count so
nothing new starts until the caller releases. Every entry point that
deletes a ref takes that pause: gitFetch, gitPull, gitFastForward,
removeWorktree, forceDeleteLocalBranch, prepareWorktreeCreateCheckout,
addWorktree. Five more triggers close the rest of the window: battery
drop, window focus, quit, the attempt deadline, and any other git command
queueing for an admission slot.

Judge a pack by re-probing the backlog rather than by the child's exit
code. Measured in the field: another Orca session moved a branch
mid-pack, git reported `cannot lock ref`, skipped that ref and packed the
rest -- 36,688 loose refs down to 3. On a machine running several
sessions that is the normal case, and retrying it would be wrong.

Probe with one batched `readdir` per directory instead of streaming
`opendir`, which issues a thread-pool round trip every 32 entries: 177ms
-> 23ms on a real 36,600-ref repository, with half the event-loop lag.
The walk stays strictly sequential so it can never occupy more than one
of libuv's four filesystem threads.

`PackRefsLockOwnership` makes a lock left by SIGKILL attributable, and
only reclaims one when a marker exists, the lock is older than any
pack-refs could run for, and the recorded process is gone.

Refs #17828

* fix(git): wait out the packed-refs lock instead of killing the pack

Measured on Git 2.55/APFS with 37k loose refs: a full `pack-refs --all
--prune` takes 23-32s but holds `packed-refs.lock` for only 0.03-1.37s of
it. The other ~95% is the prune phase, during which a concurrent `fetch
--prune`, `branch -D` or `update-ref` succeeds every time -- per-ref locks
last microseconds and git retries for `core.filesRefLockTimeout`.

So the abort-on-everything design was strictly harmful. SIGTERM into the
prune loop strands an empty `refs/**/*.lock` about one time in five
(9/30, 5/40, 6/30 kills): `tempfile.c` opens the lock O_EXCL before
`activate_tempfile()` links it into the list the signal handler walks,
and a pack does ~36k lock cycles. Afterwards `update-ref -d` on that ref
fails with `cannot lock ref ... File exists`, permanently. On Windows
`taskkill /f` never runs git's handlers at all, so an abort inside the
rewrite strands `packed-refs.lock` every time.

Never signal the child. `packRefs` no longer takes an abort signal; it
polls `packed-refs.lock` and reports the window through a
`PackedRefsLockReporter`. `pause()` resolves when the lock is released --
bounded, and free during the prune -- while the suspension counter still
blocks new attempts. Battery and window-focus become do-not-start rather
than stop-what-is-running, and quit waits for the lock and lets the child
finish orphaned.

For strands that already exist, `PackRefsLockOwnership` now also reclaims
`refs/**/*.lock` under the same three conditions plus a 0-byte check, and
a lock carrying our own not-yet-reclaimable marker records `locked` with
a 30min retry instead of the 6h failure cooldown -- so a Windows strand
self-heals in half an hour rather than six.

Reverts the git admission-scheduler event bus, which existed only to
drive the abort this removes.

Refs #17828

* test(git): make the ref-maintenance waits survive a loaded runner

CI shard 4/8 failed on `restarts every armed countdown when the user does
ref work themselves`, which passes locally. The `until()` helper spun a
fixed 200 event-loop turns and then returned silently, so on a contended
runner the filesystem probe had not finished and the assertion that
followed failed with an unrelated message.

Bound the wait by wall clock instead and throw a named error, which
immediately exposed a second latent bug: the single-flight test's second
wait could never succeed, because the deferred repo's retry is on a faked
`setTimeout` that spinning the real loop never advances. It had been
passing only because the old helper gave up quietly. Add a timer-aware
variant for those, and have the countdown test await a signal the fake
pack resolves rather than polling at all.

Verified stable across five sequential runs and once under load average
32 with six concurrent suites.

Refs #17828
2026-09-01 19:06:44 -07:00
Neil 28214e1ea1 fix(linux): stop re-extracting the AppImage on inode metadata churn
The extracted-payload cache key hashed ctime alongside dev/ino/size/mtime.
ctime moves on any inode metadata write -- `chmod +x`, which every AppImage
user is told to run, plus `chown`, an ACL or SELinux relabel, and a backup
restore -- none of which alter a byte of the payload.

Measured on Ubuntu 24.04: `chmod +x` leaves dev, ino, size and mtime
identical and moves ctime alone, so the key changed and the next launch paid
a full ~519 MB re-extraction and a multi-second stall to rebuild a payload it
already had, then pruned the old generation.

Key on content identity instead. An in-place content change moves mtime and
almost always size; a replacement moves the inode. The existing
replace-in-place test still passes.
2026-09-01 14:06:16 -07:00
Neil a310150e6a fix(linux): bound the CLI registration lock wait
`retries: 1000` caps the attempt count, not elapsed time, so at up to 1s per
attempt an IPC-driven registration could hang ~16 minutes against a wedged
holder with no feedback.

A legitimate holder is bounded by the extraction timeout, so wait that plus
slack and then fail with a message naming the lock file, rather than hanging.
`maxRetryTime` is forwarded verbatim to the `retry` package by proper-lockfile.
2026-09-01 14:06:16 -07:00
Neil 1767858ea7 fix(linux): reclaim superseded AppImage payloads and packaged symlinks
Pruning removed 3215 of 3216 files from a superseded generation and always
stranded resources/app.asar, leaking ~105 MB per version update. Electron's
asar shim reports a *.asar file as a directory, so the recursive remove tried
to rmdir a real file and failed with ENOTEMPTY; the .catch(() => {}) hid it.
Reproduced end to end on Ubuntu 24.04: 519M -> 623M across one update, and
519M again once the payload is actually reclaimed.

removeExtractedAppImagePayload holds process.noAsar for the removal, counted
so overlapping removals cannot hand the shim back early, and the prune site
now warns with the path instead of swallowing the rejection. All three
removal sites use it -- staging cleanup and displaced roots leaked the same
way.

Also reclaim symlinks left by a packaged deb/rpm install, which the
extracted-cache-only rule turned into a hard conflict on a deb -> AppImage
migration, and name the remedy in the conflict error.
2026-09-01 14:06:16 -07:00
Neil bbd2047066 refactor(linux): import bundled launcher directly 2026-09-01 14:06:16 -07:00
Neil a2c8859f76 fix(linux): accept extracted AppImage runtimes with APPDIR only 2026-09-01 14:06:16 -07:00
Neil d19a8cdf86 fix(linux): fence AppImage terminal shim mounts 2026-09-01 14:06:16 -07:00
Neil 0079fe2fa8 test(cli): assert registration lock serialization 2026-09-01 14:06:16 -07:00
Neil 4c24a28df0 refactor(linux): trim AppImage CLI registration seams 2026-09-01 14:06:16 -07:00
Neil da4a83bd22 fix(linux): give the CLI one entrypoint by extracting the AppImage once 2026-09-01 14:06:16 -07:00
Neil f116d2ca2a test(ci): retry Windows teardown EPERM and restart evaluate misses (#17780)
Restart-survival polls treated a recycled renderer as a hard failure.
Wrap those evaluates so "Execution context was destroyed" is a pending
miss. Windows package-lane teardowns after a force-kill used rmSync
with force:true only, which does not absorb EPERM; put them on the
shared maxRetries:8 policy.
2026-08-31 18:53:01 -07:00
Chen 07df4bf0be fix(orchestration): recover stripped task deps 2026-08-29 23:07:36 -07:00
Neil 7ee8b5e1a6 Refactor lower max-lines modules (#16760) 2026-08-27 16:10:51 -07:00
Neil 83ffc0df24 refactor Electron facilities modules (#16333)
* refactor oversized Electron facilities

* fix interactive process timeout and shortcut repeat guard

* chore(child-process): drop stale cli-installer allowlist entry

cli-installer.ts now routes privileged spawns through runProcess via
cli-privileged-processes.ts, so the shrink-only ratchet flags it as stale.

* refactor(child-process): extract the bounded output sink

runProcess's timeoutMs opt-out (required to preserve the unbounded osascript
admin prompt) pushed run-process.ts past the 300-line cap. Move createOutputSink
to its own module rather than add a max-lines bypass, which AGENTS.md forbids.
Moved verbatim; no behavior change.
2026-08-25 00:31:31 -07:00
Neil 0bbc6c80e8 refactor(host): route app paths and version through an AppEnvironment port (#16019)
* refactor(host): route app paths and version through an AppEnvironment port

`app.getPath('userData')` is the single largest Electron coupling in the main
process — 37 call sites — and it is one of the things stopping the Orca runtime
from booting on plain Node. Give it the same treatment as SecretStore.

- `src/shared/app-environment.ts` — the port plus a settable registry, covering
  the members the runtime's module graph actually reads: paths, app path,
  version, packaged flag, shutdown hook, exit, and Chromium process metrics.
  `getAppEnvironment()` throws until installed, for the same reason the secret
  store does: a silent default resolves `userData` to the wrong directory and the
  caller writes real state there before anyone notices. No `node:` imports,
  because `src/shared/**` is in the web build graph.
- `src/main/host/electron-app-environment.ts` — the desktop adapter, a
  pass-through to `electron.app`.
- 9 modules migrated: telemetry, opencode/mimo/pi hook services,
  terminal-history-paths, terminal-scrollback-snapshots, cli-installer,
  clipboard-image-temp-file, memory/collector.

Deliberately NOT migrated: `src/main/browser/**`. That cluster is Chromium-
adjacent by nature — cookie jars, download destinations, offscreen pages — and a
Node backend does not ship it at all, so porting it buys nothing and churns
heavily-mocked suites. Also left alone for now: the call sites that additionally
touch `app.asar` path literals or `app.setName`, which need more than a
mechanical swap.

`getAppMetrics` stays on the port rather than being injected because
memory/collector.ts is its only caller and reads it from module scope; a Node
host returns [], having no Chromium processes to measure.

Test wiring: the secret-store setup file becomes `vitest-host-ports-setup.ts` and
installs both ports, exporting `fakeAppEnvironment`/`installFakeAppEnvironment`
so suites needing one specific member state only that instead of restating all
seven — which is boilerplate, and had pushed one suite past the max-lines budget.

Verified: 159 files / 1651 tests pass across every touched area; `tsc` clean on
both the node and web projects; `oxlint` clean.

* fix(typecheck): list the vitest host-ports setup in the node project

Three suites import `installFakeAppEnvironment` from config/scripts, but that
directory is outside tsconfig.node.json's include list, so composite typecheck
failed with TS6307. Listing the one file matches how this config already pins
individual files it needs.

Local `tsc --composite false` does not reproduce this — only `pnpm typecheck`
does, which is what CI runs.

* refactor(host): drop two unused AppEnvironment exports

hasAppEnvironment() and resetAppEnvironmentForTests() had zero callers. The
secret-store equivalents are used, so these were mirror-symmetry rather than
need; add them back when something actually needs them.

* test(terminal-history): install the AppEnvironment fake instead of mocking electron

These three suites mocked `electron.app.getPath` to point at a fixture dir. The
production module now reads the port, so the mock was inert and the global test
default's temp dir won — which broke the WSL path assertions and every deletion
count.

Found by a full-suite run, not by the targeted checks around the migrated modules,
which is the argument for running the whole suite on a refactor this wide.

* test(host-ports): remove the per-environment temp dir on teardown

The setup allocated a mkdtemp directory at module scope, which vitest evaluates
once per test *environment* — one per test file, not one per worker. Nothing
removed them, so a full 6,000-file run left thousands behind.

Proven: with an isolated TMPDIR, a three-file run previously added directories and
now leaves zero.

* fix(app-environment): anchor the installed environment to a realm global

Same reason as the SecretStore: vi.resetModules() rebuilds the module registry,
and an environment installed before the reset read back as uninstalled.
2026-08-22 21:12:23 -07:00
Neil e9e238c883 refactor(wsl): delete the environment-policy layer the reviews kept failing on (#16007)
* refactor(wsl): delete the environment-policy layer the reviews kept failing on

A design council (Opus, Grok, GPT-5.6-Sol) reviewed the merged runner after it
took eleven review rounds to land. All three reached the same conclusion: the
invocation half is sound, the environment/probe half is not, and every round had
been debugging the second one.

The finding that settled it, from Opus: `environmentResolved` had **54
references, all in tests and the runner itself. Not one production reader.** The
safety mechanism the strict default existed for was never wired to anything, so
all 19 degrading sites reported absence with full confidence anyway -- #9725
live at every one, under comments claiming it was handled. Two of those comments
say so out loud; I wrote them.

Root cause, in one line: every knob existed only because a failed probe was
fatal. So it no longer is.

- `allowDegradedEnvironment` and `WslGuestEnvironmentUnavailableError` are gone.
  A missing login PATH is a fact in the result, not an exception. That deletes
  23 opt-outs, six catch-and-remap blocks, the transient/rejected cooldown
  split, `probedWithBudget`, and the 1.5x re-probe heuristic -- none of which
  had a reason to exist once the case stopped throwing.
- `lane` + `allowDegradedEnvironment` collapse into `loginPath: 'none' |
  'preferred'`. 19 of 23 sites passed the opt-out, and two said in comments that
  they did not want the login PATH at all: the flag had become the `'none'` the
  union was missing.
- The `interactive` lane is deleted. It had zero production callers and kept ~30
  lines of fence plumbing alive for tests only.

Net -98 production lines; the runner itself sheds 86 for 38.

Also carries three fixes from the W3 orphan-PR sweep I had not done:
- `WSL_UTF8=1` in the runner. My relay migration deleted the only place setting
  it, so wsl.exe's own error text arrived UTF-16LE and read as NUL-riddled.
  A regression I introduced. Credit: #9010 (Chang-Jin-Lee).
- `GITLAB_HOST` is now named in WSLENV, so a ported self-hosted host actually
  crosses into a distro-routed glab (#12557). Credit: #12558 (makoto-developer).
- The WSL skill-setup command pipes into `sh` instead of `eval "$(...)"`, whose
  nested quoting produced `word unexpected (expecting "in")` (#14292). Credit:
  #14785 (innocarpe).

* fix(wsl): restore the login PATH for the Codex availability lookup

loginPath:'none' on a PATH lookup reports an nvm-installed codex as absent,
which is #9725. A miss without a resolved environment is now 'could not
check', not 'not installed'.

Also hardens the guards that should have caught it:
- bashism ratchet is per-call, not per-file, and fails closed on lexer desync
- blankStringContents handles regex literals (an apostrophe in /'/g desynced
  the lexer, so the scan silently found zero calls)
- windowsHide allowlist 85 -> 80, stale once the lexer parsed those files

Credit: Grok (P0), GPT-Sol (ratchet gaps).

* test(wsl): close the two ratchet gaps that let planted spawns pass

- variable-indirected wsl.exe (`const b = 'wsl.exe'; spawnProcess(b)`) is now
  tracked, so the 5 files recorded only in a comment become real allowlist
  entries. Three actually spawn that way; the other two never spawned wsl.exe
  at all, so the prose record was wrong by three in the hiding direction.
- promisify(renamedAlias) is now resolved, so `const run = promisify(execFile)`
  behind an `execFile as x` import can no longer skip windowsHide.

Each verified by planting the violation, watching it fail, restoring, watching
it pass. Credit: GPT-Sol.

* fix(source-scan): stop the regex-literal reader from eating block comments

At index 0 there is no preceding token, so a file opening with a banner
comment had its `/*` read as a pattern and swallowed to the next slash --
110k characters of preload/index.ts, in the direction that hides offenders.

Measured across the tree, old lexer vs new: worst-case over-blanking drops
from -110564 to -1116 characters, and files that desync drop from 51 to 22.
The remaining extra blanking is regex interiors, which is the intent.

Regression tests for both lexer bugs, each verified to fail with its fix
reverted. The first draft of the comment test did not bind -- it asserted on
text after the swallowed span.

* fix(wsl): restore the unverifiable signal on the two remaining probe sites

Round 2. Three call sites used to throw when the login-PATH probe failed;
the redesign rewired one (Codex) and left two reporting confident absence.

- skill-wsl-provider-detection: the script ends in `|| true`, so a lookup
  without the login PATH exits 0 with empty stdout -- identical to 'nothing
  installed'. Callers skip the ~/.codex and ~/.claude skill roots on an empty
  list, losing an nvm-installed provider's skills.
- wsl-cli-installer: the dead catch is replaced by an explicit check. Its
  `case ":$PATH:"` probe otherwise answers from the distro default PATH and
  Settings states as fact that the CLI is not on PATH. Timeout is checked
  first, since a timed-out run also leaves the environment unresolved.

Also narrows the regex-literal prev-token set. '!', '+', '-', '>' and '}' are
value terminators as often as operators, so postfix `n-- / 2` and JSX
`<A size={14} /> : <B` were read as patterns and their spans blanked -- 13
live JSX spans, and one swallowed execFile call that left no desync behind.
False negatives only risk a desync, and desync fails closed.

Plus: WSL_UTF8 on the probe spawn (#9010 reached the runner, not the probe),
and the allowlist header I shuffled by sorting comments along with entries.

Credit: Grok (both P1s), Opus (lexer false positives).

* docs(wsl): drop the lane comments the redesign made false

The interactive lane is gone, so 'both lanes' and the fenced-stdout note
described code that no longer exists. Also states plainly that
environmentResolved is always true under loginPath:'none' -- the field cannot
rescue a PATH lookup that was mislabelled, which is how #9725 came back.

Credit: Grok.

* fix(wsl): stop piping user scripts into the shell's stdin

The W3 migration moved hooks from `wsl.exe --exec bash -c <script>` to a
script piped into `bash -s`. Anything the script runs that reads stdin then
drains the rest of the script, bash hits EOF and exits 0, and the caller logs
success -- an orca.yaml hook of `ssh -T git@github.com || true` followed by
`pnpm install` silently never installs.

Scripts now travel in argv by default, which is what the pre-migration code
did and what --exec makes safe. `scriptDelivery: 'stdin'` stays for the one
caller that needs it: the hook-relay installer embeds a base64 JS bundle far
past any command-line limit, and reads no stdin.

A runner test already described this exact EOF hazard -- for the login shell,
not for the guest command it was itself creating.

Credit: code review.

* fix(skills): make the unverifiable check unconditional, and stop double-probing

Round 3.

- provider detection threw only on an EMPTY result, so a degraded partial hit
  slipped through: `claude` visible on the default PATH via Windows interop
  plus an nvm-only `codex` returns a plausible ['claude'], and the caller then
  skips the ~/.codex skill roots for a provider that is installed. The
  installer already got this right with an unconditional throw.
- three sites asked for 'preferred' without needing it. The GROK_HOME probe
  runs its own `"$login_shell" -lc`, so the runner's probe was a second login
  shell eating up to half an 8s budget; the two skill scans are
  find/base64/head/printf/stat over $HOME.
- the indirection binder missed `private readonly x = 'wsl.exe'` (the
  modifier was captured as the name), backtick literals, and
  `spawnProcess(this.x)`. Commit 2bbbd99 claimed that gap closed; it now is,
  verified against all three shapes.

Credit: Grok.

* fix(child-process): keep the tail of output whose failure lands last

Two console-flash bugs the ratchet was carrying on its allowlist rather than
catching: daemon-process-inspection execs powershell.exe and the gemini
extractor execs `where gemini`, both console-subsystem, both without
windowsHide (#10488). Allowlist 80 -> 78.

And a migration regression: the hook-relay install used to keep a rolling
tail of stderr (`slice(-MAX)`), while runProcess's maxOutputBytes keeps the
head. A guest install that fails after pages of apt warnings therefore
reported the warnings instead of `mv: Read-only file system`. runProcess
takes retainOutput: 'tail' for output whose meaning is at the end.

Credit: code review.

* test(wsl): close the last two indirection shapes in the binder

`this.binary = 'wsl.exe'` has no declarator keyword, and a helper that just
returns the literal is a spawn one hop away that no regex can follow. The
return case fails closed only when the file also spawns something --
local-windows-terminal-runtime.ts returns the name as terminal metadata and
never spawns, so a blanket rule flagged it wrongly.

Verified against both shapes: planted, failed, restored, passed.

Credit: Opus.

* fix(preflight): stop reporting installed WSL CLIs as absent (#9725)

The last two probe sites that turned an unresolvable login PATH into a
confident negative. The native branch of detectInstalledAgents already
consults install dirs for exactly this reason ('PATH may still be unhydrated
on a cold GUI launch'); the WSL branch had no equivalent, so a cold distro
made an nvm-installed claude/codex read as not installed and told the user to
install a CLI their own terminal runs.

Ports that fallback to the guest: agent detection checks the version-manager
bin dirs for commands the PATH lookup missed, and the preflight command runner
APPENDS them to PATH -- append, never prepend, so a resolved login PATH stays
authoritative and a stale nvm version cannot shadow the real binary.

Tested by executing the generated scripts through /bin/sh against planted
binaries, since the behaviour is shell globbing and [ -x ]. Both the
nvm-discovery and the no-shadowing tests were verified to fail when reverted.

* fix(codex-accounts): hide the console on the legacy active-home migration

execFileSync('wsl.exe') with no windowsHide flashes a conhost and steals
foreground on a GUI-launched Orca (#10488). Sibling WSL spawns got this in
earlier commits; this one only had its quoting rewritten. Allowlist 75 -> 74.

Credit: code review.

* fix(windows): close the shell:true hole that made windowsHide a no-op

I un-allowlisted the gemini extractor after adding `windowsHide: true` to an
`exec()` call. `exec` implies `shell: true`, which this repo's own chokepoint
documents as silently making windowsHide a no-op (#14543) -- so the site still
flashed a conhost while reading as guarded. Now execFile('where.exe', …),
matching the relay sibling that already did it right.

The ratchet could not see that, which is why it passed. It now treats a call
that resolves to exec/execSync, or any `shell: true`, as unguarded regardless
of windowsHide -- including through renamed imports and a renamed promisify.

Also: a script over 8000 chars now falls back to stdin. Windows caps a command
line at 32767 and a user's orca.yaml hook is the one unbounded script Orca
runs (`run-both` concatenates two; a vendored installer is ~15KB), so argv
would fail to spawn outright. Degrading beats failing.

And the binder now sees `let p: string` ... `p = 'wsl.exe'`.

Each verified by planting. Credit: Grok.

* test(wsl): an opaque payload must declare its interpreter

My per-call bashism guard REPLACED the file-wide one, and that was a strict
regression: the real payloads are built in a separate function and passed as a
bare `script,`, so the bashism is never inside the call literal and the
per-call arm cannot fire. Deleting `shell: 'bash'` from skill-discovery-wsl
-- `done < <(find ...)` and `read -r -d ''`, the #14292 signature -- passed on
this branch and failed on main.

Reading through the identifier is guesswork. Requiring the call to name its
shell when the payload is not a literal is not, so seven POSIX call sites now
say `shell: 'sh'` -- no behaviour change, sh was already the default.

Two earlier attempts at this were wrong and are worth recording: a whole-file
BASHISM test blamed codex-accounts/service.ts, which correctly pins bash on its
four inline payloads and correctly leaves printf/mkdir unpinned; and excluding
call text still caught a bash payload belonging to a non-runner execFileSync.

Also: runProcessSync now refuses retainOutput:'tail' instead of silently
keeping the head, and the union docblock no longer describes stdin delivery.

Verified against both of the plants that exposed this. Credit: Opus.

* test(wsl): judge an opaque payload by the file, not by whether shell is set

Round 5. My previous rule -- opaque payload must have `shell:` -- was the
third guard fix in a row that came out weaker than what it replaced:
`shell: 'sh'` on a bash payload satisfied it, which is #14292 with extra
steps. Flipping skill-discovery-wsl's pin from bash to sh shipped green.

Now: strip the text of every call that already names bash, and if a bashism
survives anywhere in the file while a script-carrying call is not bash-pinned,
flag it. Stripping the bash-pinned calls is what keeps codex-accounts clean.

Also closes four ways to hide a call from the collector, each verified by
planting:
- `script: \`${bashism}\`` -- a template literal read as a visible literal
- `runWslProcess({ ...spec })` -- a spread hides script AND shell
- `Object.assign({ a }, { script })` -- the collector took the first `{`, so it
  now takes the whole argument list
- `import { runWslProcess as runWsl }` -- a renamed callee collected nothing,
  and zero calls read as zero violations

Not fixed, recorded instead: a computed `shell:` in the console guard. Matching
any non-false value also flags `shell: spawnConfig.shell`, a pass-through that
is false in every branch, and a false positive there costs an allowlist entry
that disables the guard for a whole correct file.

Credit: Grok.

* fix(preflight): make the guest fallback match the native one it claims to mirror

Three defects in the #9725 fix from earlier today, all found by executing the
generated scripts under real dash rather than reading them.

- $HOME containing a space word-split the unquoted dir list into a relative
  path, so every CLI read as absent -- the exact symptom the fix exists to
  remove. Each entry is quoted now; the nvm entry quotes only its prefix so the
  glob still expands.
- A directory passes `[ -x ]`, so ~/.local/bin/gemini/ was reported as an
  installed CLI that then fails to launch with EISDIR. The PATH half of the
  same script already guarded this, and so does the native twin.
- The header called this the "guest-side twin" of the native fallback while
  omitting four of its directories: volta, asdf, fnm and mise. A WSL user on
  any of those still had #9725 while the same user on native did not -- and
  asdf and mise are named in the motivating comment. The claim is now true.

Credit: Opus.

* test(wsl): mask bash-pinned calls by position, not by String.replace

`rest.replace(text, '')` with a string pattern removes only the FIRST match,
so two identically-written pinned calls left one behind and its bashism then
counted against an unrelated unpinned call in the same file. A body that also
occurred earlier as a substring would blank the wrong region entirely.

The collector now returns ranges and the mask is applied by index. Verified
both directions: two identical pinned bodies plus one unpinned call flags, and
the same file with all three pinned stays clean.

* test(wsl): fail closed on call shapes a regex cannot attribute

Round 6. Rather than widen the pattern again, treat the shapes it cannot
reason about as unreadable.

A regex cannot tell which object a key belongs to, so every round produced
another way to put the pin in one place and the payload in another:
`cond ? {pinned} : {unpinned}`, `{...} as WslSpec`, `Object.assign({a},{b})`.
A call whose SPEC is chosen by a ternary or spread -- one appearing before the
first `{` -- or which carries an `as` assertion is now flagged whenever the
file has a bashism, with no `shell: 'bash'` escape, because the substring test
that would grant the escape is exactly what cannot be trusted on these shapes.

A ternary INSIDE the object is not exotic: claude-accounts/service.ts:977 uses
one to choose a script line in a call that is already pinned, and treating that
as opaque would demand a second pin it already has. Nor is a nested call --
`script: `x ${shellQuote(p)}`` is how every payload here is built, and flagging
it would demand bash on POSIX payloads that must not have it.

Also follows `const run = runWslProcess`, generics and optional chaining, and
counts collected calls against mentions so a shape that slips the pattern reads
as unreadable rather than clean.

I tried the TypeScript parser first, which would remove the class outright.
TypeScript 7 is the native port and exposes no JS compiler API; oxc-parser
works but is transitive, and declaring it surfaced an unmet peer warning.
Recorded here so the next person does not repeat the detour.

Credit: Grok.

* fix(wsl): fish is a PATH lookup, and my lint check could not fail

Two things Opus caught that I had verified wrongly.

`wsl-fish-history-cleanup` passes `program: 'fish'` -- a bare name, so a PATH
lookup by definition, the exact class the earlier rounds hunted. I mapped it to
'none' and then defended that in an audit, because I read
`allowDegradedEnvironment: true` as "does not need the login PATH". It does not
mean that: it means "do not fail when the probe fails". The old call still USED
the login PATH whenever it got one, which is 'preferred'. Under 'none' a fish
from linuxbrew or nix is invisible and the cleanup throws. The truncated
comment left behind when the flag was deleted is finished too.

And `pnpm lint` has been failing on this branch while I reported it clean: I
grepped for `error eslint|error oxlint`, but oxlint prints the rule category
(`error typescript(array-type)`, `error unicorn(prefer-ternary)`). The grep
could not match, so it never failed. Checking the exit code instead surfaced a
third violation hidden behind the first two.

Credit: Opus.

* chore(wsl): clear the round-7 P2s

- Formatting: the branch owned 22 of the tree's 26 oxfmt failures because I
  never ran the formatter. Branch files now own none.
- resolveScriptDelivery was computed twice, in two places that must agree
  about argv shape and stdin payload. Resolved once and threaded through.
- The allowlist header said the list only shrinks while the branch added three
  entries. It grew because the scanner learned to follow a variable-bound
  'wsl.exe'; those three were previously recorded in prose, so the count was
  wrong by three in the direction that hides offenders. The header now says so.
- Two test comments still explained behaviour via the deleted
  allowDegradedEnvironment flag; a stray triple blank line; two adjacent JSDoc
  blocks where only the second attached.

Not taken: platform-guarding addWslEnvKeys. WSLENV is inert off Windows, and
the guard broke a test that asserts the key directly -- more surface than the
tidy is worth, so the reason is recorded at the call site instead.

Credit: Opus.

* test(preflight): plant a fabricated CLI name, not a real one

CI caught what my local run could not: the runner has a real /usr/bin/gh, so
`command -v gh` resolved to it and the planted nvm stub was never reached. The
fallback APPENDS, so that is the code behaving correctly -- the test was
asserting a property of my machine.

Both real-shell suites now plant `orca-fake-cli`, which exists nowhere.
Re-verified the same way as before: with the PATH fallback disabled the test
fails, with it restored it passes.

I declared this branch merge-ready without looking at CI. Local green is not
the gate.

* refactor(wsl): delete two knobs and a duplicated fallback

Elegance pass. The branch had grown from a deletion into a net addition, and
most of the growth was optional axes with one caller each.

- `retainOutput` is gone. One production caller wanted the tail of a 64KiB
  buffer; head-truncation only hurt because of that cap. The caller drops the
  cap, keeps the default, and slices the tail itself -- which is what the live
  relay next door already does. Two mechanisms for one job became one.
- `scriptDelivery` is gone. The size rule was already the whole design:
  argv unless the script is too long for a Windows command line. The option
  existed so a small Orca script could opt into stdin, and no such caller ever
  appeared. Both behaviours stay pinned: a huge script still goes to stdin, an
  ordinary one still leaves the hook's stdin free.
- Agent detection no longer walks the fallback dirs itself. It prepends the
  same PATH prelude the preflight command runner uses and lets the ordinary
  lookup do the work. Its bespoke walk had duplicated the lookup script's
  `! -d` guard -- and had missed it once, which is how a directory read as an
  installed CLI.

All 17 detection tests still pass unchanged, including the $HOME-with-a-space,
directory-is-not-a-CLI, and volta/asdf/fnm/mise cases, so the collapse is
behaviour-preserving rather than assumed to be.

Credit: Grok.

* fix(wsl): never name a path-shaped variable in WSLENV

`buildHostEnv` forwarded every caller-supplied key into WSLENV. wsl.exe
translates path-shaped variables between Windows and Linux form, so a caller
passing PATH would have replaced the guest's own PATH with a translated
Windows one -- silently, and fatally for every lookup after it.

No caller passes PATH today. The point of a chokepoint is that it does not
depend on that staying true.
2026-08-22 20:35:59 -07:00
Neil 5651662494 fix(wsl): migrate 21 call sites onto the WSL runner (#15923)
* fix(wsl): migrate 21 call sites onto the runner, after five review rounds

Rebased onto main now that the runner (#15903) has landed.

21 sites across 15 files move off ad-hoc `execFile('wsl.exe', ...)`. Allowlist
23 -> 16 on the WSL guard; 163 -> 152 on the W1 child_process guard, which moved
as a consequence.

Five review rounds, each finding real defects -- several introduced by the
previous round's fixes:

1. Hooks ran user orca.yaml scripts under dash; probe failure fell back to the
   login shell, reintroducing the ~/.profile stall the runner exists to remove.
2. An unparseable probe was cached permanently, disabling every WSL feature on
   the distro; hooks regressed from "runs degraded" to "fails".
3. Exit 127 had no expiry; a starved 5s probe hard-failed the 10s scan behind
   it; a joiner burned its budget on someone else's probe.
4. The comment stripper blanked live code, so the windowsHide guard walked past
   a real unguarded spawn and reported the file clean; an ownership-probe
   timeout silently deselected the user's Claude account.
5. Verification of the guards themselves.

The recurring finding -- a call answering "is this installed?" on a degraded
PATH -- was eventually fixed structurally rather than per-caller: the runner
refuses an unresolved guest PATH unless the caller opts in. Per-site vigilance
was demonstrably not holding; 3 of 8 sites had already forgotten the analogous
exit-code check.

Remaining 16 files need a runner mode that does not exist: a long-lived
streaming child (OAuth logins, hook relay), a synchronous caller, or a
host-level flag like --status that the guest-command API cannot express.

* fix(wsl): close round 5's P1s -- degrade where PATH was never needed

Round 5 measured the guards by re-executing their algorithms standalone rather
than reading them, and found four things.

P1 -- four skill/plugin paths gained a hard dependency on the login-shell probe
that they never had. They ran under a plain non-login `sh -c` on main, so a
probe failure now breaks WSL skill discovery and install on exactly the distro
the runner was built for: one with a slow `~/.profile`. Worse, the throw escapes
before each site's own error mapping, so the UI gets a raw internal string. They
degrade now, per the rule this branch already wrote down in
`wsl-fish-history-cleanup.ts`.

P1 -- Codex and Claude were asymmetric. Claude's five credential sites degrade;
Codex's were strict, so adding a WSL Codex account failed where adding a Claude
one succeeded. Three of the four are byte-equivalent to Claude sites, and their
scripts read `$HOME`/`$WSL_DISTRO_NAME`, which wsl.exe supplies without a login
shell. `assertWslCodexCliAvailable` stays strict on purpose -- that one really
does answer "is this installed?" (#9725).

P1 -- the ownership-probe timeout fix did not survive the rebase onto main. A
timeout still returned "not owned", which the caller *persists*, clearing the
user's account selection.

P1 -- `blankStringContents` desynced on a nested template literal
(`` `${`x`}` ``), leaving 116 lines of a child_process importer outside the
ratchet, with 27 importers structurally at risk. Now tracks template depth.
Regenerating against the fixed blanker: 70 -> 68 offenders.

Also: the windowsHide vacuity check could not fail while the allowlist alone
exceeded its bound -- the exact defect the sibling guard documents avoiding. It
now names a file that definitely offends.

* fix(wsl): close round 6 -- my blanker fix had traded a false positive for a miss

Round 6 re-derived the guard's answer from a TypeScript AST instead of trusting
the regex, and caught two things.

P1 -- the nested-template fix I shipped in round 5 introduced a worse bug than
the one it closed. Switching to "code mode" inside `${...}` without also
resetting the quote at a newline meant an apostrophe in a regex literal --
`` `'${value.replace(/'/g, "'\\''")}'` `` , which is exactly the shellQuote
shape all over this codebase -- inverted the lexer for the rest of the file.
`claude-accounts/service.ts` went blind from line 96, hiding a REAL unguarded
`spawn` at :1097: the WSL Claude managed-login path, which opens a console and
steals foreground on Windows. Round 5 traded one false positive for one false
negative and I did not notice, because the offender count went down.

The blanker now resets non-backtick quotes at a newline (the rule stripComments
already had) and tracks brace depth per interpolation. The spawn is fixed rather
than allowlisted, and the count is 69 -- the number the AST predicted.

P1 -- the ownership-timeout guard was dead code: it threw into its own `catch`
three lines below, which returned null, which the caller persists as "not owned"
and clears the user's account selection. Now a typed sentinel the catch rethrows.

P2 -- `WslGuestEnvironmentUnavailableError` reached the UI verbatim from the CLI
installer and the Codex availability check. Both mapped.

Method note: I had been regenerating the allowlist with a Python transcription
of the scanner, and the two drifted -- the same two-implementations problem this
workstream keeps finding. The allowlist is now generated by running the shipped
test with an empty list and taking what it reports.

* fix(guards): stop patching the lexer -- make the scanner fail closed instead

Round 7 proved my round-6 fix also did not work, by planting a plainly-named
unguarded `spawn` in `claude-accounts/service.ts` and watching the guard pass
3/3. That is three consecutive attempts at an exact lexer, each shipping a
desync that hid real calls, and each time the offender count went DOWN, which I
read as progress. Round 6's diagnosis was wrong too: the culprit is the
`templates` brace-depth stack, which nothing resets, not quote state.

So stop trying to be exact. `blankStringContentsDesynced` reports when the lexer
lost its bearings, and the guard treats that as an offender. Over-reporting is a
nuisance; under-reporting is a false clean, and a false clean is what let a real
console-flash spawn out of the ratchet twice. The allowlist goes 69 -> 82: the
13 extra are files whose scan cannot be trusted, now named rather than assumed
fine.

The planted violation is now caught.

Also from round 7:
- `SPAWN_CALL` missed promisified and renamed bindings, so `exec('where gemini')`
  (a real Windows cmd.exe spawn) and a detached `shell: true` in
  `cli/runtime/launch.ts` were invisible. Added execAsync/execFileAsync/
  execFileCb/spawnDetached.
- `BASHISM` matched `set -o pipefail` but not `set -euo pipefail`, which is the
  only spelling this tree uses -- so the check could not have caught the #14292
  signature it exists for. Fixed, and it immediately flagged a file; that one
  turned out to be a comment, so the bashism scan now strips comments too.
- The CLI installer error mapping my round-6 commit claimed was "both mapped"
  was never applied -- only the Codex side had been. Now actually mapped.

* fix(guards): close the four holes round 8 found by planting violations

Round 8 stopped reasoning about the guard and planted spawns into it. Four
holes, none of which reading had found:

- `windowsHide: false` **passed**. The check was `args.includes('windowsHide')`,
  a substring test. Now matches `windowsHide: true`.
- A ternary first argument was silently skipped: the method-declaration filter
  `/^\(\s*\w+\s*[:?]/` also matches `exec(useAlt ? 'a' : 'b', …)`. Now requires
  a type after the colon.
- Renamed bindings were not covered, despite the comment I wrote saying they
  were -- I had hardcoded three names. Aliases are now resolved from the import.

Each is verified closed by planting it and watching the guard fail.

`fork` is deliberately still unscanned. Round 8 is right that Node forwards the
option, but `ForkOptions` does not declare it, so the two live sites cannot be
fixed without a cast. Recorded in the verification doc rather than left as a
silent gap, along with two others worth knowing: the allowlist is file-granular,
so its ~18 false-positive entries carry a standing pre-approval for real
regressions in those files and cannot be retired by fixing code; and
`stripComments` has no desync report, so the fail-closed check is only half
applied.

The doc now also says how to verify a guard change: plant a violation. Every
guard fix here that was verified by reading was wrong.

* fix(wsl): stop preflight reporting installed CLIs as absent on a slow distro

Round 9's merge blocker, and the sharpest finding of the whole workstream: the
branch built to close #9725 had reopened it from the other side.

`preflight-wsl-command.ts` was one of five sites without
`allowDegradedEnvironment`, so a guest-PATH probe failure threw. Every consumer
collapses a throw into a verdict: `isCommandAvailable` and `isCommandOnPath`
catch to `false` ("not installed"), `isGhAuthenticated` and `isGlabAuthenticated`
read an empty payload as "not authenticated". So a slow distro made WSL git, gh
and glab read as missing.

Two things made it likely rather than theoretical. The probe took two thirds of
a 5s budget, leaving the command ~1667ms where main gave it the full 5s inside
its own login shell -- a cold WSL VM start routinely lands in that band. And a
probe timeout is cached for 30s with a re-probe threshold of 1.5x the failed
budget, which a 5s caller can never clear, so every preflight command
short-circuited without spawning wsl.exe at all -- and Re-check does not
invalidate the cache.

Fixes: preflight degrades instead of refusing, and the probe is capped at half
the caller's budget and at 4s, so no caller ends up with less time than it had
before the runner existed.

Also fixes a real console flash found on the way: `preflight-command-exec.ts`
spawns git/gh/node through `promisify(execFile)` with no `windowsHide`.

Round 9 also confirmed the credential paths are now *safer* than main: all 11
account sites degrade, every destructive guest operation is still marker-gated,
and main's `getOwnedManagedAuthPath` could disown an account on a 5s timeout --
which this branch turns into a failed launch instead of a destroyed selection.

* fix(wsl): make "Try again" able to succeed, and test the round-9 fix

Round 10 returned MERGE with one residual worth closing first.

A transient probe failure left the null-resolving promise in `inFlight`, so the
only way back was `retryAfter` -- and the 4s probe cap made the 1.5x budget
escape unreachable, because no caller can pass more than 4s. For the full 30s
window the four non-degrading sites returned their error *without spawning
wsl.exe at all*, and each of those errors says "Try again". The advice was
guaranteed to fail.

The entry is now dropped on a transient outcome and an explicit cooldown gate
replaces it, so the window alone decides. The window drops 30s -> 5s: long
enough to stop a stampede, short enough that the user's next click reaches a
distro that has since warmed up.

Round 10 also noted the round-9 fix shipped untested, which was fair. Added: the
probe-budget floor for 5s/8s/10s callers, and preflight's degrade opt-in plus
its stdout/stderr-carrying rejection, which isGhAuthenticated reads off the
caught error as an auth-success fallback.

* test(wsl): make the probe-budget guard actually guard

Round 11 caught that the regression test I added for the probe cap did not
bind: it seeded the guest environment, so the probe resolved in ~0ms and the
assertion read the command leg's timeout instead. Reverting the cap to the old
2/3 split left all three cases green.

Dropping the seed and asserting on the probe leg fixes it -- verified by
reverting the cap and watching all three fail.

A regression guard that cannot fail is the shape that has cost the most in this
workstream: the windowsHide guard silently passed a real unguarded spawn twice
for the same reason.
2026-08-22 05:45:21 -07:00
OrcaWinandOrcaWin 6e8da1df8d fix(wsl): pass guest argv verbatim through --exec (#15039)
* fix(wsl): pass guest argv verbatim through --exec

`wsl.exe <...> -- <argv>` expands `$name` in every argument against the
guest environment before the guest ever runs. It does this even when no
shell is involved, so `-- /usr/bin/printf %s '$HOME'` prints /home/you.

Every WSL invocation went through that preprocessor, so scripts arrived
already rewritten: `awk '{print $2}'` lost its field reference, and a
POSIX script asking for the literal `$HOME` got the expanded path.

`escapeWslShCommandForWindows` tried to compensate by escaping `$`, but
it skipped any `$` preceded by a backslash, so a script containing `\$`
was still corrupted -- and half the call sites never applied it at all.

Route every invocation through `--exec`, which passes argv through
untouched, and delete the escaper. The direct-git path already used
`--exec`, so this is not a new compatibility dependency.

A guard test fails if the `--` form reappears anywhere in the tree.

Net -50 lines of production code.

* test(wsl): drop remaining escaped-dollar assertions

* fix(wsl): cover the --exec migration's blind spots

An audit of every wsl.exe invocation found sites the first pass missed,
including two it actively broke:

- config/scripts/wsl-git-shell-benchmark.mjs imported
  escapeWslShCommandForWindows, which no longer exists, so the script
  threw on startup. Its wslShellArgs helper also still used `--`; the
  file already had an --exec helper, so route both call sites there.
- classifySubprocessCommand unwrapped `wsl.exe <...> -- <binary>` by
  breaking on `--` alone. With every Orca spawn now on --exec it never
  found the guest binary and bucketed all WSL subprocesses as plain
  "wsl", losing the git/gh/glab breakdown. Break on either separator,
  since foreign wsl.exe processes still use `--`.

CliSkillRuntimeSetup builds its setup command as a template literal
rather than an argv array, so no array-shaped search could see it. Its
decoder accepts both separators so commands persisted before this
change still decode.

The guard now scans config/ and tests/ as well as src/, and checks the
command-string spelling alongside the argv one — the two shapes that
have each shipped a regression. It skips comment lines so prose about
the old form stays allowed, and asserts it scanned a plausible file
count so a bad root cannot make it vacuous.

* fix(wsl): restore the guard's multi-line sensitivity

The guard matched line by line, so `'--',\s*'bash'` could not span a
newline -- and every argv array in this repo is formatted one element
per line, which is exactly the shape it exists to catch. Measured
against the pre-migration tree it caught 17 files before and 9 fewer
after. It now strips comment lines and matches the rejoined text, with
a case that pins the multi-line shape so this cannot silently return.

The program list is wider than shells now, which surfaced a false
positive: tmux takes a `--` separator followed by a program too
(`split-window ... -- cat`). Matching is scoped to files that mention
WSL rather than narrowing the list back.

Also:
- Replaced the `sed` regression case, which was vacuous. A backreference
  contains no `$`, so it returned `bac` under both separators and would
  have passed without the fix. The block claimed every case proved the
  bug. Swapped in a positional argument and a shell local, both measured
  to differ -- the positional is the shape `wslUncDirectoryExists` uses,
  where `--` blanked `$1` so every existing directory probed as missing.
- windows-shell-args.test.ts derived its expected argv from
  buildWslExecArgs, the helper under test, so six assertions would still
  pass if it regressed to `--`. Spelled the expectation out.
- Dropped two comments citing the removed `--` behavior as rationale.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-18 02:12:21 -07:00
Neil 9367169888 refactor(tests): split every oversized test file off the max-lines suppression list (#14728)
* refactor(tests): split oversized test files off the max-lines suppression list

Every `*.test.ts`/`*.spec.ts` that carried an `eslint/oxlint-disable max-lines`
directive is now split into focused, behavior-scoped suites that fit the 800-line
test budget, with shared setup extracted into co-located `*-test-harness.ts` /
`*-test-fixtures.ts` modules (300-line budget). 83 files became ~930; the largest
output is 797 effective lines. `orca-runtime.test.ts` is intentionally untouched.

Test bodies were moved by scripted line-range slicing rather than retyped, so
assertions are byte-identical. The only permitted body edits were mechanical
rebinding where a shared value moved into a harness (e.g. `tmpHome` ->
`homes.tmpHome`).

Registries that enumerate test files were updated in lockstep:
- config/max-lines-baseline.txt: pruned 341 -> 258 entries (all 83 removed).
- config/reliability-gates.jsonc: 33 gates repointed at the split files, with
  assertionRefs split per file where a gate's coverage now spans several.
- .github/workflows/pr.yml: the real-zsh lane now lists the 4 split files that
  actually exercise zsh, so they keep running in the dedicated shell lane.

Also renamed agent-hooks `server-test-fixtures.ts` to `server.test-fixtures.ts`
so the global-fetch call-site audit keeps skipping it, and added `.js` extensions
to the CLI suites' dynamic harness imports (node16 resolution) to unbreak
`build:cli`.

Verification: full suite 52,449 passing vs 52,448 at baseline with zero
assertions lost; `pnpm lint`, `pnpm typecheck`, and `pnpm build:cli` all exit 0;
the terminal-pane e2e spec runs 31/31 headless.

* refactor(tests): split hook-idle arbitration suite that oxfmt pushed over budget

The pre-commit oxfmt pass reflowed pty-connection-hook-idle-arbitration.test.ts
to 811 effective lines, 11 over the test budget. Split the hook-completion side
effect and replacement-agent veto cases into their own suite; both files now sit
well under the cap and the 15 tests are unchanged.

* test: port upstream test changes into the split files after rebase

Rebasing onto main surfaced 27 tests that main had added to files this branch
deleted, plus edits to tests that had already moved. Taking the deletion side of
those modify/delete conflicts would have dropped that coverage silently, so each
upstream change is ported into the split file that now owns the behavior — for
example main's six orchestration mailbox tests land across orchestration-runs,
-send, and -check.

Also repoints `orchestration.notification-mailbox-consistency`, a gate main added
after this branch's gate remap, at those same three split files, and re-prunes
the max-lines baseline against main's (257 entries).

Verified: all 27 upstream test titles present; full suite 52,761 passing with the
only diff vs baseline being 12 tests main itself removed and 3 that moved from
skipped to passing; lint and typecheck exit 0.

* fix(test): flush pending continuations before tearing down terminal test globals

CI shard 5/16 failed on both Node 24 and 26 with `ReferenceError: window is not
defined` from pty-connection.ts, surfacing through
pty-connection-daemon-snapshot-replay.test.ts.

The reattach/settle chains `await` a real promise and then touch `window.api`.
Under fake timers those continuations cannot run, so they only become schedulable
once restoreTerminalTestGlobals() switches back to real timers — which previously
happened immediately before `delete globalThis.window`, so a late continuation
threw and failed the whole file. Flush async ticks in that window instead.

This is latent in the source rather than new: the pre-split 25k-line file kept
running other tests after these, which gave the chains time to settle before
teardown. Splitting the file moved teardown directly behind them.

* fix(test): keep an inert window after terminal test teardown instead of deleting it

The async-tick flush was not enough: the reattach/settle chain can resolve after
teardown regardless of how long we drain, so CI shard 5/16 still failed with
`ReferenceError: window is not defined` from pty-connection.ts.

A real renderer never loses `window`, so deleting it was the artificial part.
Swap in an inert proxy whose properties resolve to callables and whose calls
resolve to undefined, making a late `window.api.pty.*` call a harmless no-op.
The next test replaces it wholesale via installTerminalTestGlobals(), and no test
asserts that `window` is absent.
2026-08-15 00:54:20 -07:00
Brennan Benson b614908ccc fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270) (#14458)
* fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270)

The Codex launch preflight carried a bare command name ('orca' / 'orca-dev') in
ORCA_CODEX_LAUNCH_PREFLIGHT. The codex() wrapper that invokes it is emitted after
the user's profile scripts are sourced, and those routinely rewrite PATH, so the
name was resolved against a PATH Orca neither controls nor can predict.

Resolve and verify the shipped CLI's absolute path instead, and return null when
no path verifies so the preflight is skipped rather than run against an
unidentified program.

* test(codex): align bundled launcher fixture across CI hosts
2026-08-14 13:09:50 -07:00
Langning ZhangandJinwoo-H 3246b73add Forward Unix launcher signals to orca serve supervisor (#14071)
* fix(cli): forward Unix launcher signals to serve

* test(cli): retry incomplete listener state writes

* test(cli): cover both Unix launcher termination signals

* test(cli): cover macOS launcher signal oracle

* refactor(cli): keep Unix launcher exec atomic

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-08-13 20:05:49 -07:00
Neil 991a3fe963 chore(lint): update oxlint to 1.77 and enable no-op cleanup rules (#13901)
Enable eleven oxlint rules that simplify code without changing behavior, and fix
every existing violation. Each candidate was gated on measured cost rather than
assumption, so rules that regressed runtime performance or type checking were
dropped instead of suppressed.

typescript/no-redundant-type-constituents is the largest addition: 113 sites, no
autofix. Dead constituents are deleted. Where the redundant literal existed to
document intent (`string | 'all'`), it is preserved as `(string & {})`, which
keeps the autocomplete hint the original code was reaching for instead of
flattening it away. The rule also caught a broken import —
remote-shared-control-retirement-probe.ts pulled RuntimeStatus from
src/shared/types, which does not export it, so the type silently degraded to
`any`; no tsconfig covers that file, so tsc never saw it.

oxlint stays at 1.77.0 rather than 1.78.0 because .npmrc sets
minimum-release-age=4320 and 1.78.0 is younger than that window.

Rules evaluated and rejected, with what disqualified each:
- prefer-string-raw: String.raw is a runtime call, not a literal (184x slower)
- prefer-string-replace-all: 26% slower
- text-encoding-identifier-case: ~5% slower, reproducible
- prefer-spread: [...str] is 110% slower than split('') and differs on surrogates
- no-implicit-coercion: `!!x` narrows types and `Boolean(x)` does not (22 tsc errors)
- prefer-arrow-callback: arrows are not constructible, breaking `new` on mocks
- object-shorthand: rewrites source text asserted by a tracked reliability gate
- switch-case-braces: pushes ten files past max-lines, which cannot be suppressed
- no-useless-switch-case: drops `case undefined:` that switch-exhaustiveness-check needs
- arrow-body-style: 115 violations have no fix, and it breaks max-lines
- newline-after-import: false-positives on the leading-semicolon ASI idiom

electron-vite-output-contract asserted on the literal
Object.prototype.hasOwnProperty.call text; retarget it to Object.hasOwn, which
rejects inherited keys identically.
2026-08-11 18:19:43 -07:00
BingZandBrennan Benson d6e1d84235 fix(wsl): forward native CLI arguments losslessly (#12582)
* fix(cli): preserve WSL --deps quotes and parse task ids strictly

PowerShell 5.1 native splat was stripping ASCII double quotes on the WSL
bridge, so non-empty JSON --deps arrays failed while [] still worked.
Pre-escape quotes before launching orca.exe, and recover quote-stripped
task-id arrays while rejecting non-task-id and malformed CSV input (#12188).

* fix(orchestration): narrow WSL deps recovery

* fix(wsl): forward native CLI arguments losslessly

* ci(windows): exercise WSL PowerShell argv boundary

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-11 13:49:12 -07:00
OrcaWinandBrennan Benson e05c7f1625 fix(wsl): preserve forwarded flags in PowerShell 5.1 (#13426)
* fix(wsl): prevent PowerShell flag prefix binding

* chore(wsl): remove root-level reproduction report

* fix(wsl): preserve bridge-first launcher upgrades

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-11 13:20:16 -07:00
OrcaWin 8c975b9d15 fix(terminal): avoid Windows PATH registry subprocesses (#13485) 2026-08-10 19:55:29 -07:00
50594c55a9 Stop the Windows Orca CLI from crashing when the environment carries both PATH and Path (#12218)
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment

The packaged Windows `orca.exe` launcher read
`ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the
case-sensitive process block into a case-insensitive dictionary via `.Add`.
An inherited block carrying both `PATH` and `Path` threw
`ArgumentException: Item has already been added. Key in dictionary: 'PATH'`,
so every `orca` invocation exited 1 before Electron started
(native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67).

The launcher now mutates its own environment with
`Environment.SetEnvironmentVariable` and never touches either
`ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment
block and the child inherits the live one verbatim.

Orca was also minting the duplicate itself. `applyTerminalAttributionEnv`
read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows
PTY that inherited `Path` got a second spelling; which one the child resolved
was non-deterministic. `createLaunchEnv` did the same and, because its read
always missed on Windows, shipped Agent Teams terminals a `PATH` containing
only the tmux shim dir.

`resolvePathEnvKey` (extracted from the existing precedent in
windows-environment-path.ts) now drives every PATH read and write in the PTY
env pipeline, and attribution collapses Windows onto the single OS-resolved
spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior
is unchanged and a case-sensitive POSIX `Path` variable is never touched.

Closes #12046

* test(windows): track the launcher's own-environment marker

The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND
off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the
old dictionary writes and failed.

Co-authored-by: Orca <help@stably.ai>

* fix(windows): follow the host block's PATH spelling on sparse daemon env patches

Resolving a path-less Windows env to `Path` handed the daemon's own
`{...process.env, ...opts.env}` merge both spellings when the host block spelt
`PATH`. Fall back to the host block's own key, and collapse again inside the
daemon since that merge happens after attribution.

Co-authored-by: Orca <help@stably.ai>

* fix(windows): resolve the live PATH spelling by block order, not casing

Win32 resolves a duplicated variable by taking the first case-insensitive
match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first
preference targeted the shadowed spelling on the reporter's own
`["PATH","Path"]` block. Drop the attribution-side collapse with it: it
deleted the other spelling's value, and deleting the live key promotes
the shadowed one, so an env that stripped down to empty lost both.

* chore: drop unrelated merge formatting

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 20:45:23 -07:00
Siddiqui QamarandOrcaWin f3c824bc28 fix(terminal): expand environment variables in Windows PATH (#11987)
* fix(terminal): expand variables in Windows PATH

* fix(terminal): preserve expanded Windows PATH at spawn

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 01:00:58 -07:00
OrcaWinandOrcaWin dde72f85de fix(windows): separate updater from orchestration migration (#11405)
* fix(windows): separate updater from orchestration migration

* fix(terminal): attest adopted reveal identity

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 15:23:06 -07:00
OrcaWinandOrcaWin 363e478909 fix(orchestration): preserve active workers across updates (#11271)
* fix(orchestration): preserve active workers across updates

* test(ssh): model absent legacy adoption

* test(orchestration): align compatibility contracts

* fix(windows): escape updater PowerShell booleans

* fix(windows): restore stock uninstall process check

* fix(orchestration): keep recovery off renderer startup barrier

* fix(orchestration): harden legacy recovery migration

* fix(orchestration): close recovery review gaps

* fix(orchestration): complete legacy worker cutover recovery

* fix(orchestration): preserve legacy workers across updates

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 11:31:35 -07:00
Neil badf91101b fix(quality): enforce performance-safe lint baseline (#11074)
* fix(quality): clear safe existing lint findings

* fix(quality): keep lint cleanup allocation-free

* fix(quality): enforce performance-safe baseline

* test(terminal): drain deferred confirmation cleanup
2026-07-27 20:54:02 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
NeilandOrca 190de8223e refactor(comments): slim verbose comments in main integrations (git/providers/…) (#9543)
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.

Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.

Area: main — git, source-control, providers & integrations. 40 files changed, 1432 insertions(+), 4473 deletions(-).

Co-authored-by: Orca <help@stably.ai>
2026-07-20 03:18:28 -07:00
Jinwoo Hong 808299cd1f fix(cli): avoid Windows PATH status timeout (#9483) 2026-07-19 21:59:22 -04:00
Brennan Benson 31f643ca42 Add version-matched skill guides to the CLI (#8624)
* Add version-matched bundled skill guides

* Clarify skill freshness rollout PRs

* Add canonical skills show alias

* fix(skills): address guide review feedback

* fix(skills): make guide commands cross-platform

* fix(skills): apply the ORCA convention to the emulator guides

Review follow-up: the emulator guides still instructed literal
`orca emulator ...` in sh fences with no Linux disambiguation, so on
unmanaged Linux they could launch the GNOME screen reader — the exact
failure the executable-selection preamble prevents. Both emulator
guides now carry the preamble and ORCA placeholder across fences,
tables, and prose, and the cross-platform safety test covers all four
converted guides. Also replaces computer-use's "unless a block names a
shell" carve-out, which contradicted its own POSIX example, with the
unconditional placeholder rule.
2026-07-14 02:17:55 -07:00
Jinjing 302b97029a P2 windows cli hardening (#8638)
* fix(cli): harden Windows launcher transports

* Fix csc.exe compile failures on space-bearing Windows install paths

- Legacy csc.exe mangles absolute paths containing spaces, so the
  compile step now cd's into the bin directory and passes bare
  file names for /out and the source file instead of full paths
2026-07-13 18:58:21 -07:00
f90cd6ebc9 fix(cli): preserve WSL cwd through the Windows bridge (#6965) (#7640)
* fix(cli): preserve WSL cwd through the Windows bridge (#6965)

# Conflicts:
#	src/cli/index.test.ts
#	src/cli/index.ts

* fix(cli): preserve bridge exit codes (#6965)

* fix(cli): harden WSL cwd bridge compatibility

* chore(cli): align cwd tests with main

* fix(cli): repair deleted WSL cwd before path conversion

* chore: preserve main formatting after merge

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-13 15:44:32 -07:00
Jinjing 34a9df7795 Fix wsl cli rc4 migration (#8471)
* fix(cli): migrate managed WSL launchers to native exe

* refactor(cli): reconcile managed WSL registrations

* fix(cli): harden WSL registration reconciliation

* fix(cli): serialize and harden WSL CLI registration lifecycle

- Cap per-distro concurrent repairs and add a filesystem lock so two
  Orca installs can't race the same distro's launcher/bridge files
- Skip re-adopting a legacy managed launcher when its bridge is user-
  owned, avoiding a doomed install retried on every startup
- Track per-distro reconciliation against launcher target + app
  version so unchanged registrations aren't re-probed each boot
- Replace command install with a single atomic rename instead of a
  move-based backup, closing a window where no orca-ide exists
- Remove the registry invalidate-on-persist-failure path now that
  writes are atomic and repair is disk-authoritative
- Add brief negative caching to WSL distro discovery to bound
  wsl.exe respawns on persistently broken hosts
- Extract keyed-promise-queue as a shared serialization primitive
2026-07-12 23:19:15 -07:00
Neil ee82d66a35 fix(cli): preserve multiline arguments on Windows (#8374)
* fix(cli): preserve multiline Windows arguments

* test(cli): run Windows launcher regression in CI

* fix(cli): support Windows Framework C# compiler
2026-07-12 02:13:41 -07:00
NeilandOrca 42cf64cdc9 fix(linux): stop Orca terminals from launching the GNOME Orca screen reader via bare orca (#8347)
On Linux the CLI installs as orca-ide so it never shadows /usr/bin/orca
(GNOME's screen reader), but agent-facing surfaces (orca-cli skill,
dispatch preambles, CLI hints) all invoke bare `orca` — so on stock
Ubuntu an agent inside an Orca terminal launched the screen reader,
which started speaking (#7904).

Fix: prepend a userData-scoped shim dir (bare `orca` -> bundled
orca-ide launcher, or the stable AppImage) to the PATH of every
packaged-Linux managed PTY, mirroring the existing dev-mode cli/bin
prepend. The user's own shells — and their real screen reader command —
stay untouched. Also flip the orca-cli skill probe to prefer orca-ide
so agents outside Orca terminals never execute the screen reader.

Fixes #7904

Co-authored-by: Orca <help@stably.ai>
2026-07-12 00:38:35 -07:00
NeilandOrca 11e537616c Surface a friendly error when Windows blocks the user PATH write (#6412) (#6588)
* Surface a friendly error when Windows blocks the user PATH write

When HKCU\Environment is locked (Group Policy / restricted ACL / EDR-managed
environment), the PowerShell SetEnvironmentVariable write rejects with a raw
'Command failed: powershell ...' error (often mojibake-localized) that the CLI
Settings toast displayed verbatim. Detect the permission failure via the Latin
PowerShell error-envelope tokens and rethrow an actionable message naming the
exact folder to add to PATH manually, chaining the original error on cause.

Fixes #6412

Co-authored-by: Orca <help@stably.ai>

* Tighten Windows PATH permission detection

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-11 13:43:31 -07:00
NeilandOrca e33b2006f4 Remove stale max-lines lint disables from files under the limit (#7548)
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.

Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.

Co-authored-by: Orca <help@stably.ai>
2026-07-06 02:12:32 -07:00
5124b0d0ad fix(serve): install orca CLI in headless serve so orca claude-teams resolves (takeover of #5766) (#7267)
* fix(serve): install the orca CLI in headless serve mode

In headless serve mode the orca CLI command (~/.local/bin/orca on Linux,
/usr/local/bin/orca on macOS) is never created, so the in-app Claude Team
launcher (orca claude-teams) opened by 'Start new agent' fails with
command-not-found. CliInstaller.install() is only invoked from renderer/desktop
flows (onboarding, Settings 'Install CLI', feature tips) via the cli:install IPC;
headless serve has no renderer. Run the existing idempotent installer on the
serve startup path, best-effort so a failure never blocks serve startup.

Fixes #5763.

* fix(serve): install a bare `orca` dispatcher on Linux for Claude Team

On Linux CliInstaller installs the CLI as `orca-ide` (to avoid shadowing GNOME
Orca's /usr/bin/orca), not bare `orca`. But the Claude Team launcher (`orca
claude-teams`), composed client-side by both the desktop renderer and the
mobile app and written verbatim to the host PTY, invokes bare `orca` — which
is unresolved in the initial managed terminal (no agent-teams shim on PATH
yet). The serve-mode CLI install therefore fixed macOS but not Linux.

Add a bare-`orca` dispatcher in the serve branch (host-side, Linux only) that
execs the bundled CLI wrapper. ~/.local/bin is hardcoded ahead of /usr/bin on
the managed-terminal PATH, so it resolves. Plain file (not a managed symlink)
so CliInstaller.removeLegacyLinuxCommandIfManaged leaves it untouched.

* Restore ai-vault files to origin/main (drop stale pre-0.52 oxfmt merge artifact)

Co-authored-by: Orca <help@stably.ai>

* Harden serve CLI install: AppImage-safe dispatcher, no-prompt macOS, skip Windows, conflict guard

- Gate CliInstaller.install() to macOS+Linux; Windows would only mutate the
  persistent user-registry PATH without helping the current serve's terminals.
- Inject a non-prompting privilegedRunner so headless macOS never pops an
  osascript admin dialog when /usr/local/bin isn't writable (skips instead).
- Linux dispatcher: exec the stable $APPIMAGE via buildAppImageCliWrapper when
  running from an AppImage (resourcesPath is an ephemeral FUSE mount); skip when
  the bundled orca-ide launcher is absent; never clobber a user-owned ~/.local/bin/orca.
- Single-quote the exec target; add coverage for all new branches.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Daniel Rudaev <daniel@d1dx.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-03 14:12:33 -07:00
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00
Jinjing 6729f683f6 Fix Orca CLI path resolution and improve registration UI (#6484)
* fix Orca CLI path resolution and improve registration UI

- Hydrate the shell PATH before running installer actions on macOS to
  match the user's terminal instead of launchd.
- Resolve the active macOS command path dynamically against shell PATH
  candidates, ignoring broken symlinks and non-executable files.
- Detect and report conflicts with unmanaged executables shadowing the
  configured command path.
- Add "Preparing..." and "Registering..." loading states to the settings
  and setup panels during preflight checks.

* Fix Orca CLI detection and installation path conflict checks

- Allow detecting a managed Orca command located later in PATH when the
  default path entry is missing or non-executable.
- Avoid raising installation conflicts for unmanaged commands located
  after the default install path, as they will be safely shadowed.
- Translate terminal setup and Orca CLI registration strings for
  Spanish, Japanese, Korean, and Chinese locales.

* Only open terminal when pre-install checks succeed

Prevent the terminal from opening and setting the terminal command if
onBeforeOpenTerminal or refreshPreInstallNotice throws an error.
2026-06-27 02:05:23 -07:00
08ba730d8e feat(devin): managed hooks, sleeping resume, AI Vault (#5380)
* Revert "fix(terminal): add proportional scroll fallback for sidebar resize" (#937)

* fix(sidebar): smoothly animate off-screen worktree reveal on click (#1302)

Clicking a worktree card whose row lies outside the sidebar viewport
caused an instant jump when scrolling it into view. Switching
`scrollToIndex` to `behavior: 'smooth'` turns that minimum-distance
scroll into an animated slide while keeping `align: 'auto'` so visible
cards still no-op (no re-centering).

Co-authored-by: Orca <help@stably.ai>

* Avoid local scrollback serialization on shutdown (#1821)

* Fix PR refresh coordinator test arguments (#2545)

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.36-rc.6

* release: v1.4.36-rc.6

* release: v1.4.36-rc.6

* ci: gate release-cut to the canonical repo so it skips forks (#4815)

The cut job checks out main, bumps package.json's version, and
fast-forwards main. On a fork with Actions enabled, the scheduled RC
cut runs against the fork's main and diverges it on the version line
every slot, so that contributor's PRs back to upstream conflict on
package.json even when their change never touches it.

Gate the job to github.repository == 'stablyai/orca' so it (and the
jobs that depend on it) no-op on forks. Canonical scheduled and manual
cuts are unaffected.

* feat(hooks): install Devin managed status hooks

* feat(devin): address hook review, resume, and UI polish

- Parse Devin config.json as JSONC; warn on read_config_from overlap
- Windows hook command uses forward slashes; APPDATA fallback
- Add devin to sleeping-agent resume and UI registries (plan 003/004)
- Add hook-service and hook-config-json tests

Closes follow-up for plans 002–004 on feat/add-devin-agent.

* feat(devin): scan ATIF transcripts for AI Vault

Register devin in AI_VAULT_AGENTS, discover ~/.local/share/devin/cli/transcripts
(or DEVIN_HOME), parse ATIF JSON sessions, and build devin --resume commands.

* docs(devin): clarify stdin-after-start vs bracketed paste

* fix(devin): use JSONC for remote install, add partial+APPDATA tests

- installRemote: replace readHooksJsonRemote (JSON.parse) with
  readTextFileRemote + parseJsonc for JSONC compatibility on SSH
- Add partial status test (some hooks missing → state:'partial')
- Add Windows APPDATA config path test with fallback

* fix(devin): address CodeRabbit review — sessionId fallback, parseJsonc errors, comment, i18n

* Fix Devin integration edge cases

Co-authored-by: Orca <help@stably.ai>

* Package Devin JSONC parser dependency

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-06-17 20:49:29 -07:00
0ec3882cb8 Add project Windows runtime selection (#5519)
* Add project Windows runtime selection

* Fix project Windows runtime selection

Co-authored-by: Orca <help@stably.ai>

* fix: preserve WSL shell variables

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <neil@stably.ai>
2026-06-17 16:08:14 -07:00
Jinwoo HongandOrca 7ea359bd60 Add Claude Agent Teams native pane launcher (#4892)
* Add Claude Agent Teams native pane launcher

Co-authored-by: Orca <help@stably.ai>

* Fix Agent Teams CI coverage checks

Co-authored-by: Orca <help@stably.ai>

* Fix Claude Agent Teams split direction mapping

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-08 16:11:32 -04:00
Jinwoo Hong 3017efe00b Fix WSL account and skill setup issues (#4412) 2026-06-01 20:01:03 -04:00
Neil df9a254393 Heal stale generated Orca CLI launchers
Detect generated Orca Unix launcher files as stale CLI registrations and replace them during install, while keeping arbitrary regular files protected as conflicts. This removes the stale /usr/local/bin/orca blocker found during memory profiling.
2026-06-01 14:56:27 -07:00