* fix(runtime): stop broadcasting terminalSideEffects to clients without consumers
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): keep mobile subscribers counted for side-effect availability
Excluding phones from the consumer-availability count added a new flip edge
(last desktop client leaving a phone-attached host), and the flip's tracker
rebuild cancels armed stale-working-title timers — stranding a 'working'
spinner on the phone. Availability counts all subscribers again; the
broadcast fix stays in the per-listener fan-out skip, now applied inside the
delivery callback so live-Set unsubscribe semantics and allocation-free
iteration are preserved.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): separate mobile title tracking from side-effect scans
---------
Co-authored-by: Orca <help@stably.ai>
* fix(persistence): tolerate null worktreeMeta entries in linked-item normalization
Co-authored-by: Orca <help@stably.ai>
* fix(persistence): drop null worktreeMeta entries instead of retaining them
gcStaleWorktreeMeta never reaps a timestamp-less key, so a skipped null meta
would persist forever and reach unguarded consumers (reassignSshTargetId,
push-target cleanup, disconnected-SSH listing, usage metadata).
Co-authored-by: Orca <help@stably.ai>
* fix(persistence): drop every corrupt worktreeMeta shape with its lineage rows
Widen the guard past null so scalars/arrays can't survive load into a
Record<string, WorktreeMeta> that consumers trust, self-heal a whole-map
"worktreeMeta": null instead of leaning on gcStaleWorktreeMeta running next,
and clear the two lineage companions the way gcStaleWorktreeMeta and
removeWorktreeMeta already do.
Co-authored-by: Orca <help@stably.ai>
* fix(persistence): persist whole-map metadata repairs
---------
Co-authored-by: Orca <help@stably.ai>
* fix(windows): make the GPU fallback actually remove the GPU child, and stop WSL latching absent
Three Windows crash/regression fixes from shipped 1.4.156/1.4.158/1.4.159 crash reports.
GPU fallback (cluster D, 14 reports, exit 0x80000003 STATUS_BREAKPOINT):
the software-rendering fallback called disableHardwareAcceleration() plus
--disable-gpu, neither of which removes the GPU child process — Chromium still
spawns it to host Viz and merely drops the backend to software GL. Measured on
Windows 11 / Electron 43.1.0: gpuProcessCount stays 1. So a GPU process being
killed by a bad driver or an injected DLL kept dying after the fallback engaged,
on every launch, for the life of that build (the marker is sticky per version).
The crash tails show exactly this: gpu_fallback_applied followed by another GPU
crash 1.3s later. --in-process-gpu is the only switch that drops the child count
to 0; --disable-software-rasterizer is deliberately excluded because it also
kills SwiftShader, which would drop every terminal to the DOM renderer.
WSL distro list: a successful-but-empty `wsl --list --quiet` was cached for the
process lifetime. `wsl --install` reports zero distros while one is still
provisioning, so an early probe latched "no WSL" until restart — WSL appeared
during setup and then vanished from the terminal picker. Empty results now
re-probe on an exponential window (15s doubling to a 5min cap) while staying
readable, so a missing distro is still visible to isKnownMissingDistro.
WSL availability: isWslAvailable() latched false on any failure via a bare catch,
so one slow wsl.exe activation disabled WSL for the whole session. Failures are
now classified — a numeric exit status or ENOENT is answer-shaped and holds for
10min, anything else (timeout, spawn failure) retries after 45s — and both back
off per consecutive failure, mirroring isPwshAvailable.
Windows-only: every changed path is behind an existing process.platform check,
so macOS and Linux behaviour is unchanged.
* fix(windows): drop a stale WSL availability failure once a distro list succeeds
The distro-list and availability caches expire independently, and
getWslRepairReason checks availability first. So a definitive availability
failure (numeric exit status or ENOENT) held for 10-30min would keep reporting
`wsl-unavailable` even after `wsl --list --quiet` successfully returned a
distro — i.e. over a WSL that demonstrably just answered. That is the same
latch class this branch fixes, surviving in the gap between the two caches.
A non-empty distro list proves wsl.exe ran, so drop the negative availability
cache and let the next call re-probe. Scoped to non-empty lists only: those are
cached for the process lifetime, so this cannot re-spawn the blocking 5s probe
more than once. An empty list keeps its failure cache, since it re-probes on a
15s-to-5min schedule and would otherwise pay the blocking probe far too often.
* fix(windows): harden GPU safe mode and WSL recovery
* fix(wsl): make capability refresh cleanup explicit
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(ssh): key the PTY model-migration fence by app pty id
Co-authored-by: Orca <help@stably.ai>
* test(ssh): pin the post-recovery checkpoint rekey
The finishSourceRecovery app-id rekey had no coverage: reverting it left every
suite green while reconnects silently resumed from the stale migration-era
checkpoint.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(browser): scope Cmd/Ctrl+F find to the focused split (#11348)
The browser pane's renderer-path Find handler is a window-global
capture-phase keydown listener, but it armed on `isActive` (the active
tab within its own group) rather than on whether its split holds focus.
In a terminal+browser split, the browser was therefore `isActive` even
while the terminal held keyboard focus, so it swallowed Cmd/Ctrl+F and
opened find-in-page in the browser instead of find-in-terminal.
Thread a focused-split signal (`isFocused`) from BrowserPaneOverlayLayer
— derived from `activeGroupIdByWorktree` — down to the Find handler and
gate the listener on it. This mirrors how terminal leaves already gate
global shortcuts via `focusedGroupId` in TabGroupSplitLayout. Floating
browser panels omit the prop and fall back to `isActive`, preserving
their behavior. The IPC path (webview guest focused) is unchanged; it
only fires when the guest genuinely has focus.
Not platform-specific: the chord resolves through `keybindingMatchesAction`
(Mod -> metaKey on macOS, ctrlKey elsewhere), so the same path is fixed on
macOS, Linux, and Windows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(browser): preserve Find before split focus settles
* fix(browser): handle stale focused split IDs
* fix(browser): route guest Find to source page
* test(browser): wait for split address bar
* test(browser): focus split before Find routing
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(checks): rank successful checks above skipped and neutral
Checks were ordered with `skipped` (4) and `neutral` (3) ahead of
`success` (5), so a PR with a long tail of skipped jobs pushed every
passing check below the fold — you scroll past a wall of "Skipped" to
find out whether anything actually ran.
Rank the no-signal conclusions last (`success` 3, `neutral` 4, `skipped`
5) and pull the order out of its three duplicated copies
(checks-panel-content, PullRequestPage, GitHubItemDialog) into
`src/shared/pr-check-severity-order.ts`. Unknown conclusions now sink to
the bottom instead of silently ranking as `neutral`.
* fix(checks): look up check ranks through a Map, not an object literal
An object-literal rank table resolves `constructor`, `toString`, and
`__proto__` off Object.prototype, so those keys returned a function
instead of falling through to UNKNOWN_CHECK_RANK — the comparator then
subtracted functions, went NaN, and left the list in arbitrary order.
Conclusions come from provider payloads, so keep the lookup on a Map and
cover prototype property names in the test.
* test(checks): cover provider-neutral ordering states
* fix(checks): preserve actionable provider states
* fix(checks): preserve unresolved provider rollups
* fix(checks): keep unknown GitLab rollups neutral
* fix: preserve neutral review check summaries
* fix: complete provider-neutral check ordering remediation
* fix: use provider-neutral mobile review status input
* fix: hydrate GitLab mobile review status
* fix: type mobile GitLab review hydration
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(skills): stop OS sidecars marking an untouched skill as modified
Package identity compared a live user directory against a tree read from a
clean checkout, so anything the OS deposited counted as drift. One Finder
visit writes .DS_Store, which sorts before SKILL.md and misaligns the
index-aligned snapshot comparison — the copy became 'unrecognized', was
reported as "may be modified... Remove it", and left out of the update.
Running the update could not clear it either: the updater compares its lock
to the source and never reads disk, so it correctly reports "up to date"
and writes nothing.
Ignore OS-authored names on both sides of the comparison. The generator
half is not hypothetical: a stray sidecar in a working tree made the
committed artifacts read as stale, failing lint for that developer.
Scoped to OS-authored names only. Tolerating unexpected files in general
would let an injected payload ride along beside a clean SKILL.md; these are
safe because an official SKILL.md never references them, so no agent can be
routed into one. Mode bits are deliberately untouched — that would weaken
identity for real scripts.
* fix(skills): keep guarding a directory or link wearing an OS metadata name
The name-only skip dropped any entry matching an OS metadata name, so a
directory named .DS_Store or ._scripts took its whole subtree out of
identity and a symlink wearing one stopped tripping the link guard — a
skill hiding either read as pristine. The OS writes these as plain files
only, so the entry type decides, still ahead of the case-fold map.
Also compares both walkers over the same fixture: an asymmetric skip is
worse than none, since one side would bake in content the other can
never observe.
* chore: ignore the OS metadata names skill identity already skips
Both skill-identity walkers ignore these names, but .gitignore covered only
.DS_Store and Thumbs.db — so a stray ._SKILL.md showed as untracked and
`git add -A` could commit it. That is the one way the two walkers can
disagree: the disk walker skips such a file while the git-tree producer
(collectGitPackageFiles, used by the unreferenced --rebuild-from-tags path)
does not, so a committed sidecar would make released history and observation
describe different content.
Ignoring them keeps that asymmetry unreachable rather than adding a second
skip to the released-history path, which is load-bearing and provably never
sees one today: no committed sidecar exists on any ref.
Nothing tracked matches the new patterns.
* chore: correct the skill-identity ignore comment
The previous wording claimed these names cannot be committed, which
overstates what .gitignore provides: `git add -f` and `git apply --index`
both bypass it, so a cherry-pick, rebase or fork branch already carrying a
sidecar is unaffected. That clause was load-bearing — it was the stated
reason for leaving the released-history producer unhardened — so it should
not read as a structural guarantee.
Also fixes the producer count (three, not two: two disk walkers plus the
git-tree producer, which does not skip) and says plain file, since the skip
is isFile()-gated so a directory or link wearing the name is still walked.
types.ts sat 9 effective lines under the 300 max-lines cap, which forced
#10065 to type onWriteUnavailable via a cast at the pty.ts call site.
Split into pty-provider-contract.ts, filesystem-provider-contract.ts, and
git-provider-contract.ts; types.ts stays a barrel owning IProviderRegistry
and re-exporting every prior symbol, so no caller import changes.
Largest new file is 105 effective lines.
With headroom back, onWriteUnavailable is now declared on IPtyProvider
(optional — only respawnable endpoints like the daemon adapter signal it)
and the pty.ts cast is gone.
* fix(pty): do not create unused Pi/OMP home dirs on bare shells
Bare terminals used to materialize ~/.pi/agent and ~/.omp/agent (and install
managed extensions) for possible later shell-launched agents. Users who never
use those agents still saw the directories recreated after deletion.
Only create the default agent home when launching that agent explicitly, or
when the home already exists. Bare-shell OMP status still uses the userData
fallback so typed `omp` keeps the shell wrapper extension.
Closes#10196
* fix(relay): OMP bare-shell status fallback without ~/.omp
CodeRabbit: relay materializePi returned null on bare shells with a missing
OMP home, so SSH PTYs never set ORCA_OMP_STATUS_EXTENSION. Local already
wrote a userData-managed status extension in that case.
Write the status file under ~/.orca-relay/omp-managed-status-extension and
return MaterializePiResult so relay.ts can export ORCA_OMP_STATUS_EXTENSION
without ORCA_OMP_SOURCE_AGENT_DIR or creating ~/.omp. Also fix the local
withOrcaManagedExtensionMarker typo on the bare-shell path.
* fix(pty): only materialize Pi home for Pi launches
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* feat(cli): add `orca account add` / `account list` for headless hosts
The desktop "Add account" UI is disabled when the renderer drives a remote
runtime (isRemoteAccountScope === kind:'environment'), so a headless server
reached from a remote desktop/web client has no way to register managed
Claude accounts. Add a host-local CLI path that reuses the existing capture
logic:
- ClaudeAccountService.addAccountFromConfigDir(): register a managed account by
capturing credentials from an already-authenticated CLAUDE_CONFIG_DIR instead
of spawning the interactive browser login (extracted persist/rollback helpers
shared with the existing add flow)
- RPC accounts.addClaudeFromConfigDir, bridged via OrcaRuntime; rejected for
mobile device tokens (host-local only)
- `orca account add` runs `claude login` in the user's own terminal into a temp
CLAUDE_CONFIG_DIR, then registers it via the local runtime; `orca account list`
lists managed accounts
Switching (select) already works from a remote client; only adding was blocked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): support Codex in `orca account add` / `account list`
Mirror the Claude headless-account CLI for Codex:
- CodexAccountService.addAccountFromHome(): register a managed Codex account by
importing auth.json from an already-authenticated CODEX_HOME, reusing a shared
persist helper extracted from doAddAccount (no interactive login spawned here)
- RPC accounts.addCodexFromHome + OrcaRuntime.addCodexAccountFromHome bridge,
rejected for mobile device tokens (host-local only)
- `orca account add --agent claude|codex` (default claude); `orca account list`
now renders both Claude and Codex managed-account blocks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: cover headless account-add capture paths (Claude + Codex)
- ClaudeAccountService.addAccountFromConfigDir: registers a managed account by
capturing an authenticated CLAUDE_CONFIG_DIR; rejects and rolls back when the
dir has no .credentials.json
- CodexAccountService.addAccountFromHome: imports auth.json from an
authenticated CODEX_HOME into a managed account; rejects when auth.json is
missing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review on headless account-add flows
- CLI login spawn uses a shell on Windows so `.cmd` agent shims resolve without
ENOENT (args are fixed literals, no injection risk)
- Claude capture skips the `.credentials.json` precheck on macOS, where creds
live in the Keychain and captureAuthFromConfigDir reads them
- Claude add rollback is best-effort: a failed rematerialization no longer skips
managed-auth cleanup or masks the original add error
- Codex persist restores the prior account/selection if a post-write sync or
rate-limit refresh fails, so a failure can't leave a dangling managed account
- Codex sync passes the account's selection target (correct runtime for WSL)
- Add JSDoc to the new public service methods and CLI functions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): harden headless account capture
* fix(cli): correct account command flag surface and interrupt cleanup
- `account` commands no longer accept or advertise the browser `--page`
flag; `supportsBrowserPageFlag` allow-listed them by omission, so
`orca account list --page x` was silently accepted and `--help`
rendered a browser-only option
- account specs declare GLOBAL_FLAGS, so `--help`/`--json` render in the
Options block like every other command
- `--agent` on `account add` documents the account provider instead of
the terminal TUI-agent meaning inherited from the shared flag table
- a SIGINT/SIGTERM during the interactive login now removes the temp
login dir (and restores the macOS Keychain item) before exiting 130;
Node terminates without unwinding `finally`, which stranded live OAuth
credentials on disk
* perf(cli): stop `account list` forcing a provider usage refresh
`accounts.list` awaited refreshAccountsForMobile(), which runs
fetchAll({ force: true }) — bypassing both the poll throttle and the
per-provider Retry-After gate — then O(N) serial per-account round
trips. `orca account list` renders only emails and the active ids, so
all of that work was discarded. The RPC now takes `refreshUsage`
(default true, so mobile and web keep the forced lane) and the CLI opts
out. Older hosts declare `params: null` and ignore the field, so a newer
CLI degrades to the previous behavior rather than failing.
Also documents on `account list` that `--environment` does not retarget
it, matching the host-local behavior of shouldIgnoreRemoteSelection.
* fix(cli): survive repeated and hangup signals during account add
withInterruptCleanup latched cleanup behind a boolean, so a second signal
got an already-resolved promise and its process.exit fired while the first
cleanup was still inside a Keychain call (3s each) — the temp dir's OAuth
credentials and the swapped macOS Keychain item both survived. Memoize the
cleanup promise so every signal awaits the same run, and register with
`on` instead of `once` so a second Ctrl-C cannot fall through to Node's
terminate-immediately default mid-cleanup.
Handle SIGHUP too. This flow exists for headless/SSH hosts, where the most
likely interrupt is the connection dropping, which hangs up the login's
terminal and previously ran no cleanup at all.
Warn when the interrupt lands after sign-in completed: the runtime finishes
the add independently of this process, so exiting 130 silently would tell
the user it was cancelled when the account may exist.
Reject a valueless `--agent`; the parser turns it into boolean true, which
silently ran a full OAuth login for Claude when the user asked for another
provider.
Also lock two behaviors the refactor changed but left uncovered: a WSL Codex
add must sync the WSL runtime lane rather than the default host lane, and
rename the account-spec help test to describe the Options block it actually
asserts rather than the usage string it never reads.
* fix(build): bundle the main modules the account CLI imports
electron-vite cleans out/main and emits only its declared entries, and
`build:desktop` runs it after `build:cli`, so the tsc-emitted copies of
`claude-accounts/keychain`, `codex-cli/command` and `win32-utils` were
deleted before packaging. Both `orca account add` and `orca account list`
then died at require time with "Cannot find module
'../../main/claude-accounts/keychain'" — reproduced against a real
`--serve` host. `agent-hooks/managed-agent-hook-controls` already carried
an entry for exactly this reason; these three were missing.
Adds a parity test so any future CLI import of a `src/main` module fails
in CI rather than at a user's shell after packaging.
* test: cover the desktop add-path behavior this PR changes
Both changes ride in the persist/rollback helpers the existing GUI add
flow shares with the new headless path, and neither had coverage:
- Claude: rollbackAddAccount now guards forceMaterializeCurrentSelection-
ForRollback, so a rejecting rematerialization no longer replaces the
real add error nor skips safeRemoveManagedAuth. Asserts the original
error surfaces and the throwaway auth dir is gone.
- Codex: the desktop add now passes the account's selection target to
syncForCurrentSelection, matching reauthenticate and select. Asserts
the host target alongside the existing WSL assertion.
Both fail when the corresponding change is reverted.
* fix(cli): close the remaining account-add interrupt and preflight gaps
The round-1 interrupt fix detached the signal handlers before running the
finally-path cleanup, so the very window it was meant to protect — the two
serial 3s `security` calls plus rmSync on the success/error path — was
still covered only by Node's terminate-immediately default. Both review
lanes reproduced it independently. Await cleanup first, detach in a nested
finally, and stop a cleanup failure from replacing the error that actually
explains why the add failed.
Do not burn the interactive login when the runtime is unreachable. The
RuntimeClient is lazily constructed and the first call was the registration
RPC itself, so "Requires the Orca runtime to be running" was discovered
only after the user completed a full OAuth round trip. Preflight with the
now-cheap `accounts.list { refreshUsage: false }`.
Reject `--environment` / `--pairing-code` on `account add`.
shouldIgnoreRemoteSelection pins account commands to the local runtime, so
`orca account add --environment homelab` silently registered the account on
the laptop instead of the headless host it names.
Survive a daemon that cannot spawn `claude`. `allowFailure` is honored in
onClose but not onError, and unlike the GUI flow nothing has run `claude` in
the daemon before this point — so a launchd/systemd daemon with a minimal
PATH hard-failed an add the user had already signed in for, even though
identity resolves fine from the config dir's oauthAccount.
Also align the `--agent` help description with the global flag column.
* fix(cli): reject runtime selectors on `account list` too
`orca account list --environment homelab` was accepted and silently
listed the LOCAL machine's accounts, because shouldIgnoreRemoteSelection
pins account commands to the local runtime. Documenting that in --help
does not reach someone who already typed the flag, and answering with the
wrong host's accounts is the specific wrong answer they would act on.
`account add` already errors; this makes the new command group internally
consistent. The other groups in shouldIgnoreRemoteSelection keep their
existing silent-ignore behavior — changing those is not this PR's job.
* test: harden account-add signal tests and cover cleanup failure
- Identify the handler under test by set difference instead of
`process.listeners(sig).at(-1)`. Vitest installs its own once-wrapped
SIGINT teardown, so the positional lookup could grab the wrong listener;
the helper also asserts exactly one new listener was added.
- Mock rmSync while keeping the real implementation by default, so the
temp-dir assertions elsewhere stay honest.
- Cover that a cleanup failure in the `finally` does not replace the error
explaining why the add failed. Fails when that guard is removed.
Completes the review loop's final round; the loop died on an API error
before it could commit this, and its `import()` type annotation would
have failed oxlint.
* fix(cli): harden interactive account add
* test(cli): make account cancellation coverage portable
* fix(cli): preserve merged skills runtime modules
---------
Co-authored-by: Dominik <marketing@gavaplast.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(native-chat): mirror multi-line launch drafts into the chat composer
seedNativeChatLaunchDraftForAgentTab rejected any text containing a newline,
so every Linear launch ("Linked Linear issue: X\n<url>") and any GitHub launch
with a typed note was invisible in chat. The rejection existed because the send
path pre-cleared the TUI with a single Ctrl+U, which cannot clear a buffer with
embedded newlines.
Orca injects the draft itself, so when the composer still holds exactly what was
injected the buffer already IS the message: the send becomes the submit key
alone — no clear, no paste, nothing that can concatenate, and multi-line submits
as one turn for free. Only the edited case needs real buffer replacement, and
that now clears every line and verifies against the agent's rendered input line
instead of firing blind.
Measured on real PTYs against Claude Code and codex (both agree exactly):
clearing N logical lines costs 2N-1 Ctrl+U. See src/shared/agent-tui-input-clear.ts
for the law, the sequences that do NOT work, and why an upper bound is safe.
* fix(native-chat): send the mobile clear burst as its own write
Live QA caught the bundled form failing: a multi-line burst prefixed onto the
body in the SAME terminal.send reached the agent as LITERAL Ctrl+U characters,
so the parked draft survived and the message arrived as
draft + 21x \x15 + body. Sending the burst as its own non-submitting write —
the shape the image paste has always used — clears as intended.
The body write's own single-Ctrl+U prefix is dropped once that dedicated clear
ran, for the same reason: a Ctrl+U immediately followed by body text in one
write lands as a literal control character and headed the received message.
Re-verified live end to end: received prompt is exactly the draft, one turn,
zero control characters.
* test(native-chat): invert the multi-line Linear launch-draft mirror expectation
The Linear work-item launch seeds `Linked Linear issue: ENG-42\n<url>\n`.
This test pinned the pre-relaxation rule (multi-line drafts withheld), which
the send path no longer needs now that it submits the TUI buffer in place or
clears every line first — so it asserted the exact behavior the fix removes.
Assert the seeded payload instead of absence, so the test fails if the mirror
regresses to single-line-only.
* fix(native-chat): preserve launch draft send contents
* fix(native-chat): preserve confirmed send queue ordering
* fix(native-chat): preserve send pacing after renderer stalls
* test(native-chat): align activation with multiline draft mirroring
* fix(native-chat): clear launch drafts from any cursor
* fix(native-chat): retire mobile-consumed launch drafts
* test(mobile): stabilize QR capacity boundary fixture
* fix(relay): refuse silent fallback when pairing invite fails
When Orca Relay pairing fails, don't silently degrade to a LAN-only QR under the Relay label. Instead, surface structured failure information so the UI can clearly inform the user and offer recovery options.
* fix issues
* fix(sidebar): stop background workspace creation from scrolling the sidebar
Creating a workspace in the background still spawns its terminals, and the
renderer treated "no presentation stated" as "point the user at this
terminal" -- revealing (scrolling to) the owning workspace.
Split adoption from surfacing with an explicit surfaceOwner flag: background
worktree creates and worker dispatch adopt their tabs silently, while
`orca terminal create` keeps its discoverability reveal.
* fix(sidebar): keep split-mode setup panes silent, tighten surfaceOwner
Review catch: with setupScriptLaunchMode split-vertical/horizontal the Setup
terminal goes through splitTerminal, whose reveal payload had no surfaceOwner,
so a background create still scrolled the sidebar in that configuration.
Also narrow surfaceOwner to `false` so "surface it" can only be expressed by
omitting the key, and fold the repeated conditional spreads into ownerSurfacing.
* fix(worktrees): stop terminals after external deletion
* fix(worktrees): request teardown per caller and revalidate uncached
Two defects let the original fix silently strand PTYs:
- teardown rode the scan's coalescing promise, so any caller that joined an
in-flight scan purged its renderer state without ever asking for a sweep;
it now runs per caller against its own known-id snapshot, deduped on the
request it actually produces so fan-out still shares one host sweep.
- the runtime's authoritative recheck was served from the 30s worktree-scan
cache, which can still list a directory git already dropped. The renderer
purges either way, so a stale miss leaked those processes permanently.
Co-authored-by: Orca <help@stably.ai>
* perf(worktrees): enumerate the host once per teardown sweep
An agent cleaning up N workspaces made killAllProcessesForWorktree issue one
full provider enumeration per missing worktree: O(N) relay round-trips carrying
O(N^2) rows. At 30 worktrees over an 80ms-RTT SSH link that is 30 scans and
~1.3s of stalled teardown; it scales linearly from there.
Share one point-in-time process list across the sweep — every worktree in it is
already known-missing, so a single snapshot answers all of them. A failed scan
is never shared: it falls back to a per-caller scan so one transient relay error
cannot suppress the sweep for the whole batch. Pinned requirePhysicalStop:false
since that path re-lists after shutdown and must not read a pre-shutdown snapshot.
Co-authored-by: Orca <help@stably.ai>
* test(worktrees): pin the disconnected-SSH no-teardown invariant
main's new directSshAuthority gate bails before any refresh when an SSH target
is not connected. That is exactly the #10562 safety rule — "host unreachable"
must never be read as "worktree deleted" — so pin it: a disconnected target
issues no teardown RPC and keeps its renderer state.
Co-authored-by: Orca <help@stably.ai>
* fix(worktrees): keep selector grammar intact when scoping by connection
resolveRepoSelectorForConnection matched the selector as a bare repo id, so an
explicit connection identity silently changed the grammar: `path:` and `name:`
selectors resolved to repo_not_found on that path alone, losing the whole sweep.
A connection identity should only *narrow* the candidate set.
Extract the selector matching both paths now share, and stop re-resolving an
already-resolved repo: teardown rescanned via `id:<repo.id>`, which throws
selector_ambiguous when an id is duplicated across hosts even though the
caller's own selector was unambiguous.
Reported as a P2 by Greptile (as redundant work); it is load-bearing.
Co-authored-by: Orca <help@stably.ai>
* fix(worktrees): keep the shared snapshot out of provider internals
The snapshot proxy passed itself as the Reflect.get receiver, so prototype
methods invoked through it ran with `this` bound to the proxy. A provider whose
own shutdown() re-read state via `this.listProcesses()` would then silently get
this sweep's cached snapshot instead of the live host — batching leaking past
the calls it was built for.
Bind non-listProcesses members to the target so only the sweep's own calls share
the snapshot. No shipped provider does this today; the point is that adding one
must not quietly change teardown semantics.
Raised by Greptile as an undocumented implicit constraint; closed structurally
rather than by comment.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(feedback): attach images to feedback submissions
Users pasting screenshots into the feedback dialog were silently dropped:
the textarea had no paste handler, the IPC payload had no image field, and
the endpoint had nowhere to put one. Reports arrived saying "images
attached" with nothing attached, which is why feedback-sourced tickets
never have a screenshot to work from.
Adds paste, drag-drop, and a file picker with thumbnail previews (up to 4
images, 8 MB each, png/jpeg/webp/gif). Rejected files raise a toast rather
than disappearing — silent loss is the bug being fixed.
Images ride the existing multipart lane, which previously activated only
for crash diagnostic bundles. Crash submissions still drop images; that
lane already carries bundles and the server rejects them there.
When the server reports imagesDelivered: false the dialog says the
feedback sent but the images did not, instead of a blanket success. A 2xx
without the field counts as delivered so this keeps working against a
server that predates the field.
Requires the marketing-site half to deploy first.
* copy(feedback): shorten attachment hint to 'Attach up to 4 screenshots'
* fix(feedback): make dropped screenshots actually attach
Three defects that discarded a user's image without telling them — the exact
failure this feature exists to fix.
Drag-and-drop never worked. `DataTransfer.files` is empty until the drop
lands, so the dragenter guard always saw zero files and the highlight never
armed. Worse, preload consumes native file drops on document capture with
`stopPropagation()` and routes the paths to the editor, so React's `onDrop`
never ran at all: dropping a screenshot on the dialog opened it in an editor
behind the modal. The drop is now claimed one phase earlier on window capture
and scoped to the dialog element, and the highlight keys off the drag types
the OS advertises — matching useComposerFileDragOver and useSidebarProjectDrop.
`crypto.randomUUID()` is undefined in non-secure browser contexts (the LAN web
client over plain HTTP), so building draft ids with it rejected the read and
dropped every image in the batch with no message and an unhandled rejection.
Use createBrowserUuid, the repo's fallback for exactly this.
`readFeedbackImageFiles` had no rejection handler, so any read failure (file
removed after picking, permission error) silently lost the whole batch.
Also: capacity was checked against a ref mirroring committed state, so two
pastes landing during an in-flight read both saw room for four and the main
process then rejected the entire submission; in-flight batches now count
against capacity. And the non-en catalogs still carried the pre-amendment
English copy for the attachment hint.
* fix(feedback): close the prototype-chain hole in the image allow-list
`contentType in FEEDBACK_IMAGE_EXTENSIONS` walks the prototype chain, so
"constructor", "__proto__", "toString", "valueOf" and "hasOwnProperty" all
cleared the allow-list. feedbackImageFilename then indexed the same object and
named the upload after the inherited value — "feedback-image-1.function
Object() { [native code] }" — and the part went out with that content type.
Only reachable by invoking feedback:submit directly (the renderer screens
types with Array.includes), which is exactly the threat model this function's
own doc comment claims to cover. Object.hasOwn matches the 54 other uses in
the repo and is identical for the four real types.
The inherited values carry no quotes or CRLF, so this was a bypassed allow-list
and a malformed upload, not multipart header injection.
Adds unit coverage for the module, which had none, plus an IPC-level case; all
six new assertions fail against `in`.
* fix(feedback): accept the drag on dragover so the drop can fire
The window-capture drop interception only fires if something first
preventDefaults `dragover`. In Electron that comes free from preload's
document-capture handler, but the same renderer is served to browsers as
web-index.html, where `installWebPreloadApi` builds `window.api` in JS and
installs no drag listeners at all. Nothing else in the renderer
preventDefaults dragover for a native file drag.
So on the web client the dialog is not a valid drop target: `drop` never
fires and the browser falls back to its default action for a file dropped
on a page — it navigates the tab to the file, taking the user's typed
feedback with it. The new types-based dragenter guard makes this worse
than before, because the highlight now arms and invites the drop that the
old `files`-based guard could never light up.
Mirrors useSidebarProjectDrop.onDragOver, which the drop rework already
claimed to match. In Electron it is a harmless duplicate of the
preventDefault preload already applied.
* fix(feedback): revoke batch previews when a read rejects partway
readFeedbackImageFiles creates the object URL for each accepted file as it
goes. If a later file in the same batch fails `arrayBuffer()` — the
removed-after-picking case the new rejection handler was added for — the
whole promise rejects and the already-built drafts are never returned, so
nothing ever revokes their previews.
Each leaked URL pins its blob for the life of the renderer, up to three at
8 MB. Release them before rethrowing; the caller's rejection handler is
unaffected.
* fix(feedback): cancel non-image drops the dialog already accepted
dragover advertises copy for every native file drag over the dialog, but
drop only cancelled for images. On the web client an uncancelled drop
navigates the tab to the file, taking the typed feedback with it.
* fix(feedback): stop image validation from aborting crash reports
buildSubmitBody drops images on the crash lane, but validation ran
unconditionally, so a crash submission carrying an invalid image would
have failed outright over attachments that were never going to be sent —
losing a crash report the user needs delivered. Gate validation the same
way body construction is gated.
Not reachable today (the IPC handler forces submissionType 'feedback' and
internal crash callers pass no images), but the two gates disagreeing is a
trap for the next caller. Raised by CodeRabbit.
Also documents why the image lane deliberately skips the 5xx retry the
text lane performs: replaying up to 32 MiB on a flaky link costs more than
it saves, and the dialog preserves the draft and thumbnails on failure.
* fix(feedback): stop mutating the image-count ref during render
React Doctor fails CI on "Ref mutated during render": the count was
assigned in the component body, where React can discard or replay work
that never commits.
Read the committed count from the callback closure instead of a ref.
Syncing the ref in an effect (the suggested fix) would reintroduce the
race a previous commit removed — right after an add, the ref is stale-low
until the effect flushes, so a paste in that window over-accepts and the
main process rejects the whole submission. The closure value is always the
committed count, and pendingImageReadsRef still covers in-flight reads.
Costs a re-registration of the drop listeners per attach, which is the
same teardown the hook already does when the dialog opens or closes.
* fix(feedback): stop an unsupported pasted image from eating co-pasted text
The paste handler consumed the event whenever the clipboard held any
image/* file, but only the four allow-listed types can actually attach.
Pasting text alongside an SVG or BMP therefore lost the text and attached
nothing — a silent loss of the user's own input, in the dialog where they
are mid-sentence.
Consume the paste only when something is attachable. Unsupported types
still route through readFeedbackImageFiles for their rejection toast, so
nothing is dropped silently; the difference is that the default paste is
left alone when we have nothing to offer in exchange.
Extraction deliberately stays broad. Narrowing it there (as suggested by
review) would skip handleAddFiles entirely, and a file paste into a
textarea does nothing visible — the image would vanish with no feedback.
The drop path is untouched: it must keep cancelling every native file drop
or the browser navigates the tab to the file.
* fix(feedback): stop the dialog accepting more than the endpoint will take
The endpoint rejects reports over 5000 characters with a 400, which the
dialog surfaces as a generic "Failed to submit feedback. Please try again."
Nothing said length was the problem, so retrying could not help — the draft
survived but the user had no way to know what to change.
Cap the textarea at the same 5000 and show a counter once 500 characters
remain, so the limit is visible before it bites rather than after. The
counter stays hidden until then; an always-on count reads as a word limit
to hit.
Extracted rather than inlined: the dialog is already past the 300-line mark
React Doctor warns on.
* fix(feedback): prevent silent attachment loss
* fix(feedback): improve attachment failure feedback
* fix(feedback): bound attachment response parsing
* fix(feedback): surface response body timeouts
* fix(feedback): harden image delivery
* fix(feedback): bound image preview resources
* fix(feedback): honor atomic image delivery response
Production’s single-message feedback endpoint uploads text and images atomically, then returns 202 {"ok":true} without an imagesDelivered field. Treating that omission as false warned users that every successful production attachment had failed.
Treat a settled successful JSON response with ok: true and no image field as delivered. Explicit imagesDelivered: false still surfaces partial delivery, while malformed, oversized, aborted, and stalled bodies remain unconfirmed or fail through the existing response bound and timeout path.
Prevent setup prompt inspection, caching, dismissal, saves, telemetry, and settings navigation from leaking across local, direct SSH, and runtime-relayed hosts.
* Link Jira issues from workspace create dialog
Add Jira issue linking to workspace creation, matching existing GitHub and Linear workflows. Users can paste Jira issue URLs in the smart name field to auto-populate workspace names and link the issue to the created workspace/worktree.
Linked Jira issues appear on workspace cards via the new 'jira-issue' card property. Implements cancellable searches and summary reads to prevent stalled requests from blocking the shared Jira pool. Persists paired issue + source context metadata with validation of provider/site identity.
Fixes git-username rate-limit handling to reject malformed JSON responses so garbage never becomes branch prefixes.
* feat(jira): link issues during workspace creation
- Display linked Jira issues on worktree cards
- Fetch issue summaries and timestamps via Jira API
- Gate Jira linking behind runtime capability check
- Preserve user-typed names during async lookups
* Enforce git check-ref-format rules in login validation
Extend isBranchSafeHostedLogin to reject usernames that git rejects as
invalid branch components: trailing dots, consecutive dots, and .lock
suffix. Prevents invalid branch names from login usernames.
* Enforce filesystem filename cap for branch-safe logins
Loose refs store logins as single filenames, so the real constraint is the
255-byte filesystem cap, not git check-ref-format rules. This allows longer
provider-agnostic logins while staying platform-safe.
* fix(runtime): avoid immediate WebSocket heartbeat sweep
Defer the first heartbeat sweep until the interval tick.
The immediate sweep can close a newly accepted WebSocket before the E2EE handshake completes on Linux ARM64.
* test(runtime): update heartbeat expectations for deferred sweep
* docs(runtime): update heartbeat initialization comment
Clarified comment regarding socket pinging during heartbeat.
* fix(runtime): arm heartbeat after socket listeners
* test(runtime): pin shared heartbeat cadence
* chore(runtime): preserve reliability gate formatting
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Allow setup when the selected project exists only on another host by carrying its validated provider identity with the request instead of reverse-parsing project IDs. Preserve host-qualified provider identity and reject mismatched payloads before linking.
Make linking atomic for local and runtime imports, including clone setup: roll back only newly registered repos and invalidate the same caches as canonical removal. Cover local, runtime, host-qualified identity, mismatch, clone rollback, and renderer routing paths.
Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
The payload prune had only its miss side covered, so the bound could be raised
or lowered by a refactor without anything failing. Both directions are now
pinned: a skill is found through 2 intermediate directories below a package and
missed at 3.
Raising the bound spends the entry budget on vendor payload — the cost that made
ordinary caches collapse and pin every skill amber (#10865). Missing a deeper
copy costs only a Details row, since a plugin-cache placement is not convergeable
by any update command. Recording the tradeoff on the constant so the next person
to touch it knows which direction is the safe one.
No behavior change.
Closes#11454
* fix(native-chat): make the launch-draft mirror reachable
Seed the chat-composer copy of unsent launch context on every originating
draft path, then let those launches open in chat by default.
Three paths delivered a draft to the TUI without mirroring it into chat:
folder-workspace create, the local argv-prefill branch of launchAgentInNewTab,
and the web-host equivalent. The first was invisible; the other two were hidden
only because draft launches were forced into terminal view.
The view-mode decision now gates on the same predicate as seeding
(canMirrorLaunchDraftToNativeChat), so a draft can never open in chat with a
composer chat would refuse to fill.
* fix(native-chat): gate draft view mode on argv-prefill launches too
The draft view-mode gate read `startup.draftPrompt`, which only the
post-ready-paste delivery sets. An argv-prefill launch carries its draft
inside `launchCommand`, so the gate never saw one and the tab opened in
chat unconditionally — a multi-line draft was correctly not seeded yet
still opened chat, leaving an empty composer beside a filled TUI input.
Adds `launchDraftText` to the activation startup payload as a view-mode-only
field, deliberately distinct from `draftPrompt` so it cannot double-deliver
the draft through pty-connection's bracketed paste, and sets it at all four
originating producers.
* fix(native-chat): reconcile backend draft launch tabs
* fix(worktrees): prevent deletion from blocking Orca
* test(worktrees): loosen async history-delete event-loop bound for CI
The main-thread safety check failed on a loaded runner when a single
timer gap hit ~48ms under the prior 30ms threshold. Keep the bound well
below a recursive sync-rm stall without treating CI jitter as a block.
* test(worktrees): measure history-delete critical path, not timer gaps
setInterval gaps during async rm of thousands of files still flake under
CI scheduling. deleteWorktreeHistoryDir is sync and must only rename, so
assert that critical-path wall time stays well below a recursive walk.
* fix(worktrees): prevent deletion from blocking Orca
Add timeout-based draining of watcher closes so SSH round-trip delays don't
indefinitely block the worktree removal path. Also: order durable temp-file
sweeps ahead of writes to reclaim orphans before accumulation, skip own-process
temps to avoid deleting live writes, swallow persistence errors so disk failures
don't cascade to query callers, and measure history-deletion progress by loop
turns rather than timer gaps to detect blocking on CI runners.
* fix(worktrees): prevent deletion from blocking Orca
Worktree deletion can now proceed even if filesystem watchers or history cleanup operations hang, preventing Orca from freezing. Changes:
- Fence install slots with tokens instead of counters so removals can abandon wedged installs without corrupting later removals
- Timeout-bound watcher unsubscribe operations with a shared drain budget
- Move JSON serialization of large usage caches from queue-time to write-time to avoid blocking main thread
- Async tombstone + schedule history tree deletion instead of blocking recursive rmSync during GC, preventing main-thread stalls ~10s after startup
* Extract usage cache writer into reusable durable snapshot class
Consolidates serialized durable-write and generation-veto logic from
three usage stores into UsageCacheSnapshotWriter. Eliminates duplication,
centralizes multi-MB JSON serialization on the main thread via write-queue
serialization, and vetoes superseded snapshots to avoid wasted rewrites.
* fix(worktrees): prevent deletion from blocking Orca
Worktree deletion used to recursively delete large session trees (hundreds
of MB) on the critical path, stalling the event loop. Instead, rename trees
into a `.pending-delete` tombstone queue and reclaim them asynchronously
off the removal's critical path.
Extracted host tree removal into a reusable helper (`removeHostTree`) that
centralizes Windows retry logic. Added usage-cache flush on quit to prevent
data loss when scans complete right before shutdown. Improved watcher
removal deadline management with reserved tail slices for the final
unsubscribe, and added retry logic for tombstone removals that fail once
under transient Windows locking.
* fix(history): retry failed session tree removals
Tombstoned session trees whose removal fails transiently (e.g., EBUSY
under Windows AV) are now re-queued in-process with bounded exponential
backoff instead of sitting until the next HistoryManager construction.
Prevents a single stuck tree from blocking the entire Orca process.
Move native macOS helper process ownership into Electron main while preserving the sidecar as the authenticated socket peer. Add fixed lifecycle IPC, bounded claim and release handling, confirmed-exit tracking, sidecar and helper force-kill escalation, cleanup across failure paths, and focused lifecycle coverage.
* fix(hibernation): reap restored subagent rows with no live agent process
A pane whose Claude session had a subagent in flight can be locked out of
agent hibernation for good. A PTY that dies while Orca is down never runs
the teardown that clears pane state, so hydrate rebuilds a subagent roster
that nothing can retire: the existing reap needs the parent to emit a
complete `background_tasks` inventory, and a parent that went idle before
the restart never emits one. The restored row keeps gating the pane
'working', and hibernation only accepts 'done'.
Observed locally: six panes parked at SubagentStop in state 'working' for
17 to 145 hours, each still holding a working child row.
Adds a second reap path. Hydrate seeds are marked `restoredFromSnapshot`,
cleared by any live lifecycle event or an id-exact running inventory entry.
One post-restore sweep drops the rows still unconfirmed when the pane's PTY
is absent from the live local inventory, then re-derives the child-gated
'working' to 'done'.
The scan is local-only by construction: panes with a relay connection id are
skipped and SSH-scoped PTY ids resolve as live, since a remote agent runs on
the far host and could never appear in a local listing. An unreadable
inventory is not evidence that anything exited, so it is a no-op. Panes that
have reported to this runtime are left alone.
`stateStartedAt` and `stateHistory` are untouched, so a draft typed while
the pane was working still blocks hibernation.
* fix(hibernation): prove local ownership before restored reap
* fix(hibernation): require authoritative restored PTY absence
* fix(hibernation): probe restored PTY liveness authoritatively
* fix(hibernation): restart idle window after restored reap
* fix(hibernation): type restored reconciliation timing
* fix(hibernation): respect worktree host ownership
* fix(hibernation): preserve same-id restored PTY rebinds
* fix(hibernation): fence batched restored PTY probes
Settings → Orchestration → Agent coverage decided whether each detected agent had the orchestration skill by matching a skill's single `rootPath` against a hand-maintained table of path segments. Skill discovery dedups by canonical file path, so when one provider home is a symlink onto another agent's skills directory — which `npx skills add --global` does — the two collapse into one row and the absorbed roots survive only in `rootPaths`, which the old matcher ignored. With `~/.grok/skills` symlinked onto `~/.claude/skills`, Claude read Ready and Grok read Missing even though Grok loads the same files.
Now classifies every entry in `rootPaths` and resolves each root through the `SkillDiscoverySource` the scanner already returns, accepting it when `sourceKind` is not `repo` and `owner` is either the agent's owner or `null`. Deletes `ORCHESTRATION_SKILL_LOCATIONS` and `ORCHESTRATION_SKILL_LOCATION_IDS_BY_AGENT` — a renderer-side mirror of `buildSkillDiscoverySources` that had to be hand-edited for every new provider root, which is what caused this class of bug.
No discovery roots change here; those landed in #8510.
Fixed during review:
- Restored the required `{ kind: 'local' }` argument to `useDetectedAgents`. Dropping it failed typecheck (TS2554) and pinned `detectedIds` to null, leaving the widget on "Checking installed agents and skill paths…" forever. It also reverted the remote-host scoping from #9790.
- Stopped a duplicate repo root from shadowing an owning home root. `SkillDiscoverySource.path` is not unique: when the scan cwd is the home directory, `~/.claude/skills` is scanned as both a home root and a repo root, and a path-keyed Map is last-write-wins — so every agent read Missing while the panel above said Installed. Guaranteed in Settings on a WSL project.
- Carried OMP coverage in the owner-based shape so #6422's coverage-table hunks could be dropped on merge.
Behavior note: an orchestration skill inside an enabled Claude plugin now counts for Claude. That is a deliberate consequence of trusting the scanner's ownership data, matches how the Codex plugin cache was already treated, and is pinned by a test.
Refs #8256
Co-authored-by: sdhilip200 <49802211+sdhilip200@users.noreply.github.com>
* test(skills): execute the plugin-cache entry limit instead of reasoning about it
The entry bound that ended a plugin-cache scan was never reached by any test.
Every existing `entry-limit` case builds a synthetic issue object at the display
layer, so both emission sites in scanKnownPluginSkillCandidates — the dirent read
loop and the declared-skill-root resolve — were verified by reading the code. The
real bound is 16,384 dirents, and the declared-root guard only fires when the count
lands in a one-entry window below it, which is why neither was ever fixtured.
Makes the entry bound injectable the way the candidate bound already was, folding
both into a `PluginSkillScanBounds` bag so a fourth positional number is not needed,
and adds:
- a case that truncates the dirent read and asserts the scan drops both real skills
and reports `entry-limit` at the root;
- a case that truncates while resolving declared roots, asserting the declared root
that exists was still walked so the guard under test is the resolve one;
- an inventorySkillFreshness case at the production 16,384 bound asserting a
truncated scan produces zero fabricated placements — the #10918 regression.
No bound value or scan behavior changes.
Refs #10918.
* docs(skills): state the declared-root guard's real window in the bounds comment
* test(skills): pin entry-limit to the scan root, not the crossing directory
Both entry-budget fixtures crossed the bound in the same directory they named,
so swapping recordIssue(rootPath) for recordIssue(directory) at either guard
passed the whole suite — the dialog would surface a nested path and no test
would say so. Cross the budget below the root instead.
* test(skills): assert the declared-root guard's threshold, not just its firing
The declared-root entry guard's ±1 boundary was left unasserted on the claim
that it is unkillable: admitting one more declared root was said to always cost
a dirent the read-loop guard then catches identically. It does not. A declared
root that does not exist reads no dirent, and when it is the last one the loop
simply ends — so the scan completes and reports nothing.
Restates both budgets against the fixture's exact entry count: one short of it,
where only the last root's resolve can cross, and exactly at it, where nothing
should be reported. `>` -> `>=` and `>` -> `> max + 1` now each fail a test.
* test(skills): assert the entry bound stops the walk, not just what it reports
Dropping `limitReached = true` at the dirent guard survived every case: the
already-read entries of the crossing directory are still descended, and the
scan reports a depth-limit for a path it never reached. A nine-level chain
whose deepest directory sits at the depth bound and crosses the budget on the
second of its two children kills that, with no symlink and no Windows skip.
* test(skills): derive the walk-stop fixture from the depth bound it depends on
The walk-stop case detects a dropped `limitReached` only because its chain is
exactly MAXIMUM_PLUGIN_SCAN_DEPTH deep, so the entries already read when the
entry bound trips are rejected on depth if the walk keeps going. That coupling
was a hardcoded 9. Raising the depth bound left the test passing while it
stopped killing the mutant it is the only cover for — verified by A/B: at
depth 12 the hardcoded fixture lets the mutant survive, the derived one does
not. Other tests in the file fail loudly on that change, which is exactly what
makes the silent one dangerous.
Exports the bound the way the file already exports its four siblings for the
same reason. No behavior change.