[skip-track-community] merge: resolve conflicts from main into feat/cloudflare-tunnel-pairing-option by braitoli · Pull Request #10 · Pierre-Mike/orca · GitHub
Skip to content

[skip-track-community] merge: resolve conflicts from main into feat/cloudflare-tunnel-pairing-option - #10

Closed
braitoli wants to merge 657 commits into
Pierre-Mike:feat/cloudflare-tunnel-pairing-optionfrom
braitoli:feat/cloudflare-tunnel-pairing-option
Closed

braitoli wants to merge 657 commits into
Pierre-Mike:feat/cloudflare-tunnel-pairing-optionfrom
braitoli:feat/cloudflare-tunnel-pairing-option

Conversation

@braitoli

Copy link
Copy Markdown

Conflict resolution

This rebases Pierre-Mike:feat/cloudflare-tunnel-pairing-option (the branch from stablyai#14040) onto current main to clear the conflicts reported there.

Conflicts were additive — both branches added sibling keys to auto.components.settings in the 5 i18n catalogs without modifying the same keys:

Branch Keys added
HEAD (main) GlobalWorktreeVisibilitySourcesSetting (all locales), EphemeralVmCleanupStopDialog + shortcutDefinitionCatalog (en), automations (ko)
This PR CloudflareTunnelFields (all 5 locales)

Resolution: 3-way merge with current-branch key order preserved, main-only keys appended at end of auto.components.settings. en.json, es.json, ja.json, ko.json, zh.json were re-derived from HEAD, main, and merge base 346e59c879.

Verification (locally on macOS, Node 24)

  • pnpm typecheck:web / typecheck:node / typecheck:cli — pass
  • pnpm exec vitest run on the 4 PR-touched test files — 29/29 pass
  • pnpm exec oxlint on all 12 PR-touched files — pass
  • pnpm verify:localization-catalog / coverage / extraction — pass
  • pnpm check:reliability-gates — pass
  • pnpm check:max-lines-ratchet — pass

Diff scope vs base

This PR adds a single merge commit on top of the 2 PR commits (d4e186c333, ad48e9d698). The 5 locale files are the only files with changes vs the PR base; everything else from the original PR is preserved unchanged.

nwparker and others added 30 commits August 18, 2026 01:49
* 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>
The runners have no font covering Hangul, Han or Kana, so any spec that
asserts how CJK text renders is measuring tofu rather than the glyph.

That is not hypothetical. An end-of-row preedit spec added for stablyai#12729
measured the composition overlay at 1.02 cells against an 8.43px grid
and failed its "wider than one cell" assertion. The overlay is
shrink-to-fit with no width of its own, so it tracks the glyph's advance
rather than the two cells the grid reserves for a wide character. With
no Korean font that advance is one cell, and the assertion cannot
distinguish a real result from a missing font - which is exactly the
question that spec exists to answer.

fonts-noto-cjk covers all three scripts and is added to both jobs that
execute specs, the sharded suite and the changed-spec job, plus the ssh
docker lane so the three stay consistent.

This does not make any spec pass on its own. It makes the CJK ones
mean something.
* fix(wsl): read machine output from a fenced login shell

Orca runs WSL reads through the distro's *interactive* login shell so
PATH matches the user's own terminal (nvm, mise and asdf only install
into rc files interactive shells read). An interactive shell also runs
the distro's rc/motd, and stock Ubuntu 24.04 writes its "run a command
as administrator" hint to stdout -- no user customization required.

Every caller parsing that stream was reading the banner as data:

  statPath  -> "To run a command as administrator...\n\ndirectory"
  readPath  -> banner prepended to the contents of every file read
  preflight -> banner prepended to `gh --version` / auth output

`.trim()` cannot recover any of these, so a WSL worktree's file
explorer sees no valid entry types and file reads return junk.

Three call sites had independently grown their own marker to survive
this (`__ORCA_AGENT_PATH__`, `ORCA_WSL_GIT_READ_ENV_V1`, and a
`>/dev/null` fd dance), which is the tell that it belongs in one place.

Fence the payload once, in the shared builder, and hand callers a
reader that returns just their bytes. The fence carries a per-call
nonce so `cat`-ing a file that happens to quote a marker is not
truncated. Exit status is preserved, so the ENOENT mapping still works.

wsl-git-read-environment drops its bespoke marker and parsing.

* test(wsl): fence the login-shell path-lookup boundary test

It asserted a raw interactive login-shell read matched an absolute path,
so the distro rc banner made it fail on any stock Ubuntu. It is part of
the shell-contracts CI gate, where it skips on Linux and hid the break.

* docs(wsl): record the guest command-execution contract

Both failure modes are silent - the command runs, exits 0, and returns
the wrong bytes - so the rules need to live somewhere a reader will
find them before writing the next wsl.exe call site.

* fix(codex): fence the WSL Codex identity probe

buildWslCodexBinaryStamp reads the login shell's stdout positionally --
path before the first newline, version after -- through an interactive
login shell. On a stock Ubuntu the rc banner lands ahead of the payload,
so the first newline falls inside the banner and the stamp becomes
path="To run a command as administrator..." with the rest as version.
Both halves are non-empty, so nothing throws: the stamp is silently
wrong, and an unstable stamp reads as "the Codex binary changed" and
reissues the trust grant.

The identity script ends in `exec`, so it never writes a closing fence;
the reader returns everything after the opening one, which is exactly
this case.

buildWslCodexIdentityArgs becomes buildWslCodexIdentityProbe and returns
the reader with the argv so the two cannot drift apart. The other three
WSL Codex commands are deliberately left unfenced: availability is
exit-code only, and app-server/login hand stdout to a long-running
program.

* fix(wsl): harden the capture fence after review

- readStdout now takes the LAST opening fence, matching the lastIndexOf
  the wsl-git-read-environment marker used deliberately: a login shell
  can echo the command text before running it, repeating the fence.
- local-worktree-filesystem throws instead of falling back to raw stdout
  when the fence is missing. The fallback silently reinstated the bug
  being fixed -- statPath would return the banner as a file type and
  readPath would return banner+contents, with no signal. Preflight keeps
  its fallback; its matchers scan the whole blob and tolerate a prefix.
- The exit-status test asserted only that the script CONTAINS `exit $?`,
  which is true for any input and never executed those lines. It now
  runs a real distro and asserts status 2 reaches the caller, which is
  what statPath's ENOENT mapping depends on.
- Corrected the doc: a sed backreference has no `$`, so `--` never
  rewrote it. Replaced with the positional and shell-local cases that
  were measured to differ.

* fix(wsl): stop running a login shell for filesystem reads

statPath/readPath/rm run coreutils at standard paths and shell builtins.
They need nothing from the user's PATH, so there was never a reason to
start a login shell -- and starting one is what put the distro's rc/motd
on the stdout these callers parse.

Fencing that output treated the symptom. Using a plain `sh -c` removes
the cause: no profile, no rc, no banner, by construction. The fence and
its missing-fence error go away with it.

The fence stays where it is actually needed: the three places that must
run the user's shell to resolve their PATH (the preflight CLI probe, the
WSL git environment probe, and the Codex identity probe).

Net -12 lines.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…e cursor (stablyai#15242)

* test(terminal): pin that the CJK block is the preedit overlay, not the cursor

A report described the cursor sitting on a wide character's first cell
and hiding its right half, with cursor style and opacity settings
ignored. Neither defect reproduces.

Replaying the reporter's own captured byte stream leaves the cursor at
column 11, exactly where the application asked, with correct wide and
continuation cells. A block cursor also cannot hide half a glyph: it
inverts the cell and the syllable renders inside the cursor span.

The black block is the IME preedit overlay. macOS 2-set Korean keeps the
trailing syllable composing until a terminator, so it sits in an opaque
absolutely-positioned box over the grid rather than in the buffer. That
box took stock upstream colours, black on white. It explains what no
cursor theory can: the block appears at the composing cursor cell, no
cursor option reaches it, it is identical with GPU acceleration off
since it is a DOM node above both renderers, Latin never triggers it
because Latin opens no composition, and Enter clears it because Enter
commits the composition.

Already fixed by the overlay theming in stablyai#15014, which landed a day after
the reported release, so the fix ships in the next one.

Tests only, no production change. Two pin the negative results so the
cursor explanation cannot be re-derived, and one pins the actual
mechanism at end of row, beside the existing mid-line arm.

Separately confirmed and not fixed here: the WebGL renderer drops the
cursor colour's alpha, so terminal cursor opacity genuinely does nothing
for a block cursor, which is the default style on the default renderer.
That is in the webgl addon rather than in xterm or in our code.

Refs stablyai#12729

* test(terminal): make the cursor precedence assertion real and measure the overlay

Review of the first pass found one assertion that could not fail. It set
options.cursorStyle and then read decPrivateModes.cursorStyle, which are
separate fields with separate storage, so it pinned that writing one does
not clobber the other. Deleting the precedence expression from both
renderers left it green.

It now asserts the rendered cursor class: the option style renders, a
DECSCUSR overrides it, and the reset hands control back. That fails if
the precedence is removed.

The overlay's rendered width is the one measurement in the report that
argues against our explanation, and no test here could reach it, because
the unit environment performs no layout. Adds an end-of-row browser arm
beside the existing mid-line one, asserting a single composing Hangul
syllable spans about two cells. That settles whether the block the
reporter measured at one cell can be this overlay.

Also scopes two DOM queries to the test container rather than the
document, and attaches the render listener before writing so a missed
render fails instead of hanging to timeout.

Records in the file header what it does not establish: composing the
opacity into the theme is not the same as it reaching the screen, since
the webgl renderer drops the cursor colour's alpha for a block cursor.

Refs stablyai#12729
* fix(git): fence buffered WSL login-shell reads

Two git paths force the login shell unconditionally and buffer its whole
stdout, so the distro's rc banner lands in front of the payload:

- gitExecFileAsyncBuffer backs `git show :<path>` blob reads and hands
  the bytes straight to the diff/blob viewer, so the banner is prepended
  to displayed file content.
- buildNetworkSshPolicyEnv probes `core.sshCommand` and treats any
  non-empty answer as a user-configured wrapper. A banner reads as
  configured, so the code skips the `ssh -o BatchMode=yes` fallback and
  silently disarms the guard that keeps non-interactive SSH from
  hanging on a prompt.

Fencing is opt-in per call site rather than applied to the login-shell
branch as a whole: streaming consumers (`git grep`, `ls-files -z`) parse
records as they arrive, so an opening marker would be glued onto their
first record. Only these two, both buffered by construction, opt in.

Blob content can be binary, so the payload is sliced out of the raw
bytes; decoding to find the fence would corrupt it. The markers are
exposed on the captured command because the shared module is bundled for
the renderer and cannot reference Buffer.

Note this path is not a rare fallback: `preferWslDirectGit` is only set
by gitStatusReadOptionsForWorktree, so every other WSL-routed git call
takes the login shell on every invocation.

* test(git): use findLast for the ssh-policy call lookup

Satisfies the code-quality rule that flags filter-then-index.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(git): run WSL git reads without a shell

WSL-routed git ran through the distro user's interactive login shell for
one reason: to inherit their PATH. That shell also runs the distro's
rc/motd and writes it to the stdout callers parse, which is why stablyai#10917
reports a shell banner breaking GitHub source detection.

A shell-free route already existed (`--exec /usr/bin/env PATH=... git`,
added for status reads in stablyai#13207) but it was opt-in, and only
gitStatusReadOptionsForWorktree opted in. Every other read -- remote
get-url, config --get, log, show, rev-parse -- took the login shell on
every call, so the reported parse never benefited.

Classify reads at the resolver instead of at each caller. A read needs
nothing the login shell provides, so it takes the direct route without
the caller asking. Writes and network operations stay on the login
shell: they can depend on credential helpers and ssh-agent that the
user's profile sets up.

Subcommands that both read and write (config, remote, branch,
submodule) require an explicit read flag before they qualify, so
`config --get` goes direct while `config user.email x` does not.

`git show` blob reads stop forcing the login shell and go direct too.
The fence stays on that path: the login shell is still the fallback when
the environment probe is cold or rejected, and a banner there would
become file content.

Behavior note: the first WSL read per distro now also warms the
environment probe in the background, so a cold read spawns one extra
wsl.exe. Subsequent reads start no shell at all.

* fix(git): keep queried WSL remote reads on login shell

* style: format WSL runner test

* fix(git): match WSL read markers positionally

The conditional read markers were matched anywhere after the subcommand,
so a positional argument that happened to share a marker's name routed a
write shell-free: `worktree remove list` read as a listing, and
`submodule foreach status` as a status query -- the latter can run
arbitrary commands, including network ones.

Split the two kinds of marker apart. `config`/`branch` are flag-marked
and still match anywhere; `remote`/`worktree`/`submodule` are
action-marked and must match the first non-flag argument. The queried
`remote show` rule and the `symbolic-ref` arity rule are unchanged.

Neither case is reachable today -- Orca issues no `submodule foreach`,
and the worktree paths are its own CLI, not git argv -- but the loose
match was the shape of the defect, not those two instances.

* test(git): pin read routing behind global options

Writes hidden behind -c/-C/--git-dir must not reach the shell-free
route, and an unparsed global form must fall back to the login shell
rather than guess at the subcommand. Both directions fail safe.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…ive feature channel (stablyai#15258)

* fix(shell): repair worktree HISTFILE in plain zsh panes via one positive feature channel

A plain zsh pane — no startup command, no agent overlay — was never wrapped, so
Orca's HISTFILE repair never ran in it. macOS `/etc/zshrc` assigns
`HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history` with no check-before-set and runs
before any file Orca controls, so per-worktree history was a silent no-op for
every ordinary pane on the primary platform.

Wrapping those panes needs a way to say which wrapper features a shell should
turn on. That channel is one exported variable, ORCA_SHELL_FEATURES, carrying a
comma-separated positive allowlist from a closed set (history, markers, ready,
identity, overlay). The wrapper .zshenv reads it into a plain, non-exported
array and unsets it in its first executable lines, before the user's own
.zshenv — so the selection survives .zshenv -> .zprofile -> .zshrc -> .zlogin in
this process but physically cannot reach a child. There is no negative or
suppression variable anywhere; an absent or inherited value can only ever mean
fewer features. ORCA_HISTFILE is consumed and destroyed the same way, which
removes the root cause of stablyai#11146 instead of patching it.

All order-sensitive wrapper work now lives in one `__orca_shell_epilogue`
defined in .zshenv and invoked exactly once, from .zshrc for a non-login shell
and .zlogin for a login shell, with each feature an independent guard.

Selection is a pure function of spawn env and launch intent, so a pane wrapped
only for history gets no OSC 133 and is observably identical to the unwrapped
pane it used to be.

Generation is now fail-closed: wrapper files are written to a temp name and
renamed, every required path is verified non-empty, and ZDOTDIR is only set when
that holds. Previously a failed write still pointed ZDOTDIR at an empty dir and
the user silently lost their entire zsh config.

Orca also recognised only its own `*/shell-ready/zsh` dir shape when deciding
what the user's ZDOTDIR was, so being launched from any other terminal that had
hijacked ZDOTDIR captured that as the user's config dir. Ownership is now
established positively — a stamped marker file, or Orca's own dir shape for
wrappers written by older builds — and an inherited ZDOTDIR holding no zsh
startup file is ignored. No vendor is detected by name.

* fix(shell): make the zsh epilogue option-proof and stop history widening relay wrapping

Review follow-ups on the feature-channel PR.

- `emulate -L zsh` as the epilogue's first statement. It runs after the user's
  own config, so `setopt no_unset` made the precmd_functions append a fatal
  error that returned from the whole function (no ready widget, ZDOTDIR left at
  Orca's wrapper dir), and `setopt ksh_arrays` made the 1-based feature
  subscript drop whichever feature is listed first.
- The `/etc/zshrc` HISTFILE repair is no longer behind the `history` guard: it
  undoes damage Orca's own ZDOTDIR caused, so it must also run for a shell that
  re-enters the wrapper after the allowlist was consumed.
- The relay keeps its own wrapping gate. Its .zshenv resolves the user's config
  dir from a ZDOTDIR Orca has already overwritten, so wrapping a remote pane
  just for `history` cost a relocated-ZDOTDIR user their whole shell config.
- A failed primary spawn no longer leaks the primary shell's launch env
  (wrapper ZDOTDIR + feature channel) into an unwrapped fallback pane.

* fix(shell): drop the relay wrapping gate and stop HISTFILE inheriting across Orca instances

The relay-specific gate added last round rested on a false premise:
main's hasOverlayRestoreEnv already included ORCA_REMOTE_CLI_BIN_DIR, and
ssh-pty-spawn-env sets that on every SSH pane whose session has a CLI
bridge — so ordinary remote zsh panes were already wrapped. The gate only
bit where remoteCliBridgeEnv is null (a host too old to report its
platform), where it silently dropped that pane's worktree history. All
three transports now share the features.length rule.

HISTFILE stays exported, so a newly wrapped pane handed the worktree
history path to every child, including a nested Orca whose panes then all
hit injectHistoryEnv's check-before-set and appended into the launching
worktree's file. Same class as the fish_history fix in stablyai#15195: recognise a
path Orca minted and drop it before the check, on the desktop, daemon and
relay injection paths and both history-disabled branches.

Also: run the epilogue from the wrapper .zshrc when zsh is in sh/ksh
emulation, since sourcehome() then reads $HOME/.zlogin and the wrapper's
.zlogin never runs; track fallback launch-env keys per attempt rather than
once from the primary; and guard the cross-file epilogue call so a wrapper
dir shared by two builds degrades quietly.

* fix(shell): make every wrapper file self-sufficient and retire the deleted marker vars from tests

- .zprofile/.zshrc/.zlogin each define __orca_resolve_user_config_dir. They
  called it on line 2 while only .zshenv defined it, so a wrapper dir written by
  two concurrently installed builds printed three "command not found" and
  skipped the user's entire zsh config. New live-shell test covers it.
- Retarget every remaining ORCA_SHELL_READY_MARKER/ORCA_SHELL_STARTUP_IDENTITY
  reference onto ORCA_SHELL_FEATURES, or delete it where the key is now dead.
- isOrcaMintedHistFile requires a leading '/', so a relative path of the same
  shape stays the user's.
- Drop an unused no-control-regex disable, and register the two real-zsh suites
  in the dedicated shell-contracts lane.

* fix(shell): stop the zsh wrapper colliding on REPLY and degrade under sh emulation

Widening wrapping from overlay/startup panes to every zsh pane turned three
latent wrapper defects into user-visible ones.

- The config-dir resolver used `REPLY`, zsh's shared scratch global, as its
  out-parameter. `typeset -r REPLY` in a user config made the wrapper's first
  executable assignment fatal, `typeset -i REPLY` silently resolved every path
  to 0; both left HISTFILE inside Orca's wrapper dir. It now writes an
  Orca-private `_orca_resolved_config_dir`, declared `typeset -g` so the
  rename introduces no `warn_create_global` noise. A new rule test fails on any
  generated wrapper file that writes a global outside Orca's namespace.

- The daemon dropped an inherited HISTFILE but never an inherited
  ORCA_HISTFILE, which now both wraps a pane the client scoped nothing for and
  re-exports another worktree's history path. The relay had the same gap on its
  isolation-off and revive paths. Both now mirror the desktop.

- A user .zshenv or .zprofile ending in `emulate sh` makes zsh ignore ZDOTDIR,
  so no later wrapper file is read and the epilogue never runs. Nothing can
  repair HISTFILE from there, so the wrapper now detects the emulation and
  hands the pane back unwrapped instead of leaving history somewhere invisible.

Also `typeset -g __orca_in_command` so the OSC 133 preexec hook prints no
warning under `setopt warn_create_global`.
…ablyai#15303)

build-relay.mjs writes each relay's marker as out/relay/<platform>/.version,
and upload-artifact excludes dotfiles unless include-hidden-files is set. The
markers were therefore stripped from e2e-build-out, so every consumer that
actually starts a relay failed with:

  Orca's local relay build is missing its version marker at
  out/relay/linux-x64/.version

This stayed latent because the sharded e2e lane never sets ORCA_E2E_SSH_DOCKER,
so its SSH specs skip instead of touching the relay. The changed-specs lane does
set it whenever a changed spec needs Docker SSH, which is why the failure only
appears on PRs that touch SSH-adjacent code.
…as spawned into (STA-2069) (stablyai#14615)" (stablyai#15295)

Reverts stablyai#14615. Its premise does not reproduce, it does not reach the failure that does, and the correction it installs can misattribute status on a path that worked before.

1. PREMISE FALSE. stablyai#14615 asserts Claude Code >= 2.1.206 hosts TUI sessions under a shared daemon. On 2.1.233 `claude daemon status` reports "not running" with 69 live interactive sessions, and every client is a direct child of its own pane's shell. Measured across the fleet: 68 distinct pane keys, zero collisions. Foreground attribution was never broken.

2. DOES NOT FIX THE REAL BUG. The failure in stablyai#9236 is real but scoped to BACKGROUNDED sessions, whose workers inherit the dispatching pane's whole ORCA_* set. stablyai#14615 mints a binding only for launches Orca constructs, so a typed `claude --bg` produces none. Fixed properly in stablyai#15304.

3. INTRODUCES A MISATTRIBUTION. Bindings are removed only on PTY death, and a user who exits Claude keeps the pane's PTY. Resuming that session in another pane does not rebind (`--resume` is a session selector, so the pin declines), and resolveBoundPaneOverride then rewrites paneKey and tabId onto the ORIGINAL pane despite a correct posted key. Demonstrated with a failing test against main; causation isolated to resolveBoundPaneOverride.

Kept stablyai#14706's observations.rebind() in the conflicting hunk — it postdates stablyai#14615 and is not part of this revert.
* fix(computer): report unasserted accessibility actions

* fix(computer): fail closed on missing action metadata

* Fix merged tab search test fixture
…truction order (stablyai#15079)

PtyHandler registered a no-op pty.ackData handler and SshPtyConsumerSessionAdapter
registered the real sourceCredit.acknowledge for the same method. onNotification is a
single slot, so only the adapter's survived — and only because relay.ts constructs it
second. Reversing those two lines would have made every credit-mode delivery wedge
permanently once the 256KB window emptied, with no error and no log.

Delete the dead no-op, and make onNotification throw on a duplicate registration the way
registerPtyDataPublicationAdmission already does for the admission slot. pty.ackData was
the only double-registered method in the tree, so nothing else changes behavior.

The existing test asserted only that pty.ackData appeared in the handler map, which stays
true when the real handler is shadowed. It now asserts PtyHandler does not own the method.
…tablyai#14948)

* fix(github): scope GHES host-auth cache to the executing connection

The gh auth answer cached for a connection-backed repo is probed without
the repository cwd, so it describes that connection's runtime — not the
local host. Keying only on repoPath+wslDistro let a local repo and an
SSH-hosted repo at the same path share one entry, and whichever resolved
first decided "is this GHES host authenticated" for both.

Include the connection identity in the runtime cache key. Local and WSL
keys are unchanged.

* fix(github): fence GHES auth cache across SSH reconnects

* fix(github): fence origin cache across SSH reconnects

* fix(github): fence repository identity cache on reconnect
…yai#15359)

`files` is an all-negation list, so electron-builder's default `**/*` packs
anything without an explicit `!` entry. out/electron-dev holds `pnpm dev`'s
per-branch Electron.app copies (~270MB each), so packaging on a machine that
has run dev bundled them all. CI never creates the directory, so releases were
never affected.
…yai#15187)

* fix(sidebar): clear the delete state when a removal is refused

Callers mark rows deleting up front for immediate sidebar feedback, but a
refusal in beginHostQualifiedRemoval returns before removeWorktree's
try/catch — the only other place that clears the flag. The failure toast
auto-dismisses after 10s, so the workspace was left on a 'Deleting…'
spinner indefinitely with no explanation still on screen.

Also removes workspace-cleanup-removal-host-guard.ts: stablyai#14731 refused a
colliding cleanup removal, stablyai#15013 replaced refusing with routing to the
confirmed host, and nothing imports the module any more — not even its
own tests.

* test(sidebar): pin the two states that actually reach the removal refusal

The existing case used an empty store, which proved the clear but not that a
user can land there. Cover a stale folder-workspace id and a hostless row under
an ambiguous legacy runtime — both resolve to no route from populated state.

---------

Co-authored-by: QA <qa@local>
…y unreadable (STA-4737) (stablyai#15287)

* fix(codex): stop overwriting and deleting Codex files that were merely unreadable (STA-4737)

Three modules shared by the host and WSL Codex lanes decided a file was absent
from a read that had only failed, and then wrote over it or removed it.

- `codex-config-mirror`: `existsSync` on the RUNTIME config.toml returned false
  for a locked file exactly as for an absent one, so the mirror took the
  "seed a fresh runtime config" branch and replaced the user's config wholesale.
- `config-settings-promotion`: an unreadable ~/.codex/config.toml counted as
  having no promoted settings, and the write path then rebuilt the user's
  canonical Codex config from Orca's runtime copy.
- `codex-home-paths`: both delete branches in `linkSystemCodexResource` remove
  Orca's mirrored copy because the system resource "is not there". `existsSync`
  and `systemResourceIsRegularFile`'s `catch { return false }` both reported
  that for a source nobody could read, so one denied read on ~/.codex/AGENTS.md
  removed the managed copy on the next launch.

`src/shared/definitive-filesystem-absence.ts` now owns the one errno allowlist —
ENOENT and ENOTDIR, with every other code including unrecognised ones treated as
indeterminate — and `host-codex-managed-home-ownership.ts` drops its private
copy rather than letting the two drift. `codex-path-observation.ts` builds the
three-valued observation on top of it.

The resource sync's two `existsSync`/`statSync` probes collapse into one
resolved stat, which answers reachability and regular-file-ness together and
closes the window between them.

`config-settings-promotion.ts` crossed its max-lines budget, so the write-target
resolution moves to its own module rather than taking a lint exemption.

Deliberately not here: the hook-service trust writes that run after a refused
mirror, and the promotion write target's own classification, which is
unreachable because it always resolves to the same file the read above already
refused. Both are noted in comments rather than half-built.

* fix(codex): preserve resource copies on indeterminate reads
…ng locally (stablyai#15364)

* fix(cli): route --host runtime:<id> to that server instead of answering locally

`--host` was only ever a local filter over whatever runtime the CLI happened
to connect to, so `--host runtime:<id>` silently answered for (and mutated)
the local machine. A real environment id and a made-up one were
indistinguishable: both returned ok:true with an empty list and the local
runtimeId in _meta, and `project setup-clone --host runtime:<id>` cloned into
the caller's own machine.

Resolve the flag before the client is built: unparseable host ids and runtime
ids that no paired environment owns are rejected, and a known runtime id
selects that environment as the connection (conflicting with --pairing-code or
a different --environment is an error). Once routed, a host filter also
accepts the runtime's own `local`-stamped rows, since both spellings name the
machine we are now talking to.

* fix(cli): close --host routing gaps found in review

- Conflict-check an ambient ORCA_ENVIRONMENT, not just the --environment flag.
  `ORCA_ENVIRONMENT=staging orca ... --host runtime:<prod-id>` silently routed
  to prod while the flag spelling errored. An ambient pairing code still loses
  to the explicit flag, because it cannot be resolved to an id to compare.
- Attach the known environment ids to the unknown-id error as `error.data`, so
  a --json consumer can retry without parsing prose, and say outright that
  runtime:<id> matches ids only and never environment names.
- Fix four command examples that documented `--host runtime:gpu`. `gpu` is an
  environment name, so every one of them would now be rejected; use an id.
- Cover the routed connection on `worktree create` and `automations create`
  (the mutating paths), the `--environment X --host local` filter-only case,
  and assert error.code/error.data rather than only substrings.

* test(cli): pin execution-host-flag to the deferred error-class import

index.ts now loads execution-host-flag.ts on every invocation, making it the
sixth module on the --help path. It imports RuntimeClientError from
./runtime/types today, but nothing enforced that; switching it to the barrel
would silently drag zod/ws/tweetnacl back onto --help, which is exactly what
this guard exists to prevent. Verified the assertion fails when the import is
flipped to the barrel.
…troy (STA-4735) (stablyai#15289)

`snapshotCodexRuntimeHookTrustProvenance` rebuilt `.orca-hook-trust-provenance.json`
from the current `config.toml` on every install and refresh, including when the
existing record could not be read. That record is the only thing separating a
trust entry Orca wrote from one the user approved inside Codex, so rewriting it
after a failed read stamps the approval as Orca-written — and
`promoteCodexRuntimeHookApprovalsToSystem`, which runs earlier in the same pass
and had already bailed on the same unreadable file, then skips it on every
later pass too. One denied read, permanent loss.

Only the unreadable case is preserved. A malformed or absent record is still
rebuilt, because resetting those IS the intent; conflating the two would wedge a
user on a corrupt file forever.

`fileContentsEqual` returned `false` from a bare `catch`, so "I could not read
this" reached `writeRuntimeAuth` as "these differ" and sent it to the
unconditional write below — replacing a refresh token Codex may have rotated a
moment earlier with Orca's stale copy. It now reports the difference only when
the bytes were actually compared, and the caller refuses instead.

`fileContentsMatchExpected`'s `!existsSync` has the same collapse but is left
alone deliberately: the write it guards is `writeFileAtomicallyIfUnchanged`,
whose rename-and-compare re-checks the real file and refuses on its own, so
classifying there would add a guard no test can drive. Noted in a comment.

The three `writeRuntimeAuthAtPath` call sites have the same overwrite shape but
are all on the WSL lane, which STA-4606 restructures; refusing there without its
lane bookkeeping would set a baseline for a write that never happened.
…d account (STA-4734) (stablyai#15279)

* fix(codex): stop a locked auth.json from deleting a just-authenticated account (STA-4734)

A successful Codex login could be reported as a failure, and the rollback then
deleted the managed home holding the credentials it had just written.

Three reads on the add path answered "no credentials" from a read that had
merely failed:

- `service.ts` decided the login verdict with `existsSync(authJsonPath)`, which
  returns false for EPERM/EBUSY as readily as for ENOENT. The post-auth tree
  kill only arms after the watcher has already observed new credential bytes, so
  an unreadable file there is a lock, not a failed login.
- `loadOAuthCredentials` read auth.json bare, so a denial surfaced as a generic
  error indistinguishable from a real one.
- `importCodexAuthFromHome` turned an unreadable source file into "No Codex
  credentials found ... run `codex login` first" — advice to redo a login that
  had already succeeded.

Each now classifies: only a definitive ENOENT/ENOTDIR means absent, everything
else is `ManagedCodexHomeTemporarilyUnavailableError`.

Classifying alone is inert, because the add path's rollback deletes the home for
any error. `removeManagedHomeUnlessUnproven` holds the rollback back on an
unproven failure — a kept home is a recoverable leak, a deleted one is permanent
data loss. The mutation check for this is in the test: with the typed error but
no rollback guard, the home is still destroyed.

`isDefinitiveAbsence` is exported so the second credential lane reuses the host
predicate instead of keeping its own copy of the errno allowlist.

* refactor(codex): move the definitive-absence predicate to src/shared

Why: STA-4737 needs the same predicate from src/main/codex, which the CLI
tsconfig project does not admit imports from codex-accounts into. Both branches
now carry byte-identical copies so they merge cleanly in either order.
…513, STA-3714) (stablyai#15261)

* feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714)

A lane parked on an approval, trust, or permission prompt looked exactly like a
lane that was thinking or inside a long tool call. On origin/main, driving a real
cursor-agent through Orca:

  surface                        running `sleep 60`   awaiting approval
  worktree ps agents[].state     working              working
  terminal show / list           no such field        no such field
  terminal wait --for tui-idle   satisfied: true      satisfied: true
  worker-show                    no agent state       no agent state

The runtime already fuses hook state, OSC title, and matched prompt text into a
`permission` verdict inside getTerminalAgentStatus — it was reachable only from the
renderer, and it was blind to cursor-agent approvals. Two gaps, one boundary.

Exposure: getTerminalInteractiveWait publishes that same fusion, minus the async
foreground probe, as `agentWait` on `terminal show` and on `worker-show`'s
observation. It carries the evidence that proved the wait (hook, prompt-text, or
title) so a coordinator can weigh it. Null means no proof; a missing field means
the host predates it — absence is never read as "not waiting".

Detection: cursor-agent's hook set has no approval event and beforeShellExecution
fires identically for auto-allowed commands, so its rendered menu is the only
authority. Matched on the key-bound choices rather than the prose, requiring two,
and self-clearing when the follow-up input line returns. Its live spinner title is
exempted from the staleness rule that clears startup modals, because cursor keeps
spinning while it waits.

Falls out of routing it through the shared verdict: `dispatch --inject` into a
cursor pane on an approval now refuses with agent_prompt_blocked instead of typing
the preamble into the dialog.

Fixtures are captured verbatim from cursor-agent 2026.08.11-e8db854 driven through
Orca; the same case matrix was replayed live against a built runtime.

terminal list stays untouched: its rows would each need a full tail scan, and
STA-4694 owns the one-call-per-run aggregate.

* fix(orchestration): only call a Cursor approval live while it owns the screen

Independent review found the approval detector trusted one dismissal string, so
any later output that did not contain cursor's follow-up line left the menu
reading as a live wait. Reproduced: a tail of the real menu followed by two lines
of ordinary output returned agent-approval-prompt, which fails tui-idle and
refuses prompt injection on a healthy lane.

Replaced with the structural property the string was standing in for: a live
dialog owns the bottom of the screen, so the last choice may sit at most one line
above the end of the retained tail. That tolerates a status footer or a partial
line mid-redraw without admitting scrollback, and it drops the vendor prose.

Being bottom-of-screen is also the dating this reason needed, so it no longer
requires waitBlockedAt. A tail restored from terminal history carries none, and a
lane parked on a prompt emits no bytes — so before this, an Orca restart made
exactly the lane both issues are about go quiet for good. The startup modals keep
the timestamp rule: their text lingers in scrollback with nothing to say whether
it was answered.

Also from review:
- worker-show and federationShow reuse the verdict showTerminal already computed
  rather than rescanning the tail, so the two can no longer disagree.
- The worker-show test now drives a real runtime, real PTY tail, and the real
  detector; it previously mocked getTerminalInteractiveWait, so it would have
  passed with detection permanently returning null.
- The guard claim is now asserted against the guard: a blocked pane rejects both
  assertTerminalAgentSendable and sendTerminalAgentPrompt, and a working pane
  still passes.
- Added a non-local (connectionId) pane case, since the verdict is derived from
  retained tail and title state on every host.

* fix(agent-status): stop a hook wait from outliving its agent

A third reviewer caught that the hook branch proved agent ownership from the pane
title alone, while the shared verdict it claimed to reuse also probes the
foreground process. A shell that takes a pane back usually sets something like
`user@host: ~/repo`, which no title rule recognizes, and a hook row stays fresh
for AGENT_STATUS_STALE_AFTER_MS — so a dead agent could be reported as waiting on
a human for half an hour.

Hook evidence now goes through getTerminalAgentStatus, which is the only thing
that can answer whether an agent still owns this PTY. The two prompt branches skip
it: a matched prompt is on the pane's screen now, so it proves itself. That makes
the probe cost fall exactly where correctness needs it, and getTerminalInteractiveWait
async, which only showTerminal had to absorb.

Also trims the comments the same reviewer flagged as longer than the repo's rule.

* test(agent-status): pin that a dead pane stops reporting a human wait

A fourth reviewer noted the approval menu sits at the bottom of a dead pane's tail
forever, and that no test covered process exit with no trailing output. The
snapshot already refuses an exited pane, and worker-show gates agentWait on proven
identity — this pins both so neither can drift into reporting a worker that needs
intervention as one that needs an answer.

* fix(orchestration): never report an unchecked worker as not waiting

Automated review caught that the three worker paths which return before the wait
is ever evaluated — unattached, missing, and identity_changed — then had their
undefined coerced to null by the emitters. A worker whose process was replaced was
reported as `agentWait: null`, which reads as "Orca looked and nobody is waiting"
when Orca never looked. That is the false negative this field exists to remove.

The field is now emitted only when it was evaluated, so a present null is a claim
about the pane and an absent one means nobody looked — because the host predates
the field, or the worker's identity could not be verified. The CLI and the
worker-show note say that rather than blaming an old host.

Covered on the context-only path, where the regression test fails against the
previous behavior; the supervised and federated emitters take the identical
one-line change.

Also trims the two test-file headers to one statement of purpose.

* fix(agent-status): tighten the Cursor menu match and stop guessing on unknowns

Fourth review round, three findings, each reproduced before acting.

Matching each choice marker with an independent lastIndexOf let text outside the
menu carry the anchor. An agent narrating "next time I'll suggest Run Everything"
after the menu was answered pulled the match down to the bottom of the screen and
revived it. The match is now confined to the last lines of the tail, and a choice
is a line that ends in the key that picks it — prose writes the same words but not
the same shape.

The one line of slack under the dialog went with it. It was a guess; every capture
of a live dialog ends on its last choice, and one line is exactly enough room for
that narration. A redraw caught mid-flight now reads as no wait until the next
poll, which is the safe way to be wrong.

The hook branch awaited a foreground probe that reaches a PTY controller which may
be a remote host, so a wedged probe stalled every caller of showTerminal — a path
that never probed before. It is bounded now, and a timeout leaves the wait
unevaluated rather than claiming there is none.

Which is the same distinction the previous commit only fixed one level up:
getTerminalInteractiveWait itself turned an unreadable pane into `null`, so
showTerminal published "looked, nobody waiting" for a pane it could not read. It
returns undefined there, showTerminal omits the key, and worker-show's text output
prints unknown rather than rendering it the same as none.

* fix(agent-status): bound the wedged probe's cost and stop matching prose keys

Fifth review round. No correctness defects in the shipped behaviour this time; two
robustness holes and the documentation of the contract.

The bounded probe abandoned the wait but not the request, so a coordinator watching
a wedged remote host added one live probe on every poll. It is single-flighted per
PTY now, the way the leaf-absence probe already is.

The trailing-key rule that separates a menu row from the agent narrating a choice
was written as a character class, and any lowercase run up to twelve characters
satisfied it — "…suggest Run Everything (as before)" passed. Spelled out as key
names instead, which also lets the glyph forms of those keys through.

The contract wording said an absent agentWait meant an old host or an unverifiable
identity. It also covers an unreadable pane and a probe that did not answer, and a
reader diagnosing an old peer from that would be wrong. Corrected on the type, the
worker-show note, and in docs/reference/remote-wire-compatibility.md, which had no
entry for a field whose absent and null states mean different things.

Also strengthens the worker-show agreement test, which compared the terminal and
observation payloads without asserting either held the expected wait, so it passed
when both were absent.
…king to probe zsh emulation (STA-4682) (stablyai#15361)

* fix(shell): carry a WSL pane's shell across relay revive and stop forking to probe zsh emulation (STA-4682)

Two residuals left behind by stablyai#15236 and stablyai#15258.

Relay revive called resolveDefaultShell() and ignored the entry's shell
override, so a restarted relay handed the user a PowerShell pane where a
WSL one had been -- and with it the host default shell's history rather
than the worktree-scoped one stablyai#15236 injects on spawn. The override and
the requested distro are now serialized (optional fields; state from an
older relay still revives the default shell) and re-resolved through the
same allowlist a fresh spawn uses, so revive re-launches the same shell
with the same guest-visible HISTFILE and the same WSLENV carrier. A bad
override degrades that one pane instead of failing the whole batch.

The wrapper's three `$(emulate)` probes each fork a zsh carrying
everything the user's config has loaded, and since wrapping widened to
every zsh pane every pane pays for them. Each probe now sits behind a
fork-free option test that is true whenever `emulate sh`/`emulate ksh`
has run; the exact probe still runs when that test passes, so no answer
changes. Measured on zsh 5.9 / macOS: 9.97 ms/run unwrapped, 14.20 ms/run
wrapped, 12.27 ms/run wrapped after -- about half the wrapper's cost.

* fix(shell): harden the revive override path and silence the option probe on an exotic zsh

Readiness-review follow-ups on this branch.

- `[[ -o <name> ]]` prints "no such option" to stderr and returns false rather
  than aborting, so a zsh lacking one of the three Bourne option names would put
  that text in the user's pane. All three predate every supported zsh, so
  `2>/dev/null` is belt and braces -- but the belt costs nothing.
- Bound the WSL distro name revive replays into `wsl.exe -d <name>`. It is the
  one field this branch newly routes from untrusted serialized state into argv,
  and reviveEntry's stated job is to re-apply fresh-spawn bounds.
- Skip a pane whose overridden shell can no longer spawn instead of letting the
  throw escape the revive loop and cost every later entry its state. Skipping,
  not falling back to the host default shell: substituting a different shell is
  the defect the override exists to fix, and the args and history env are built
  for the shell that is gone. The worktree-removal fence throws before this,
  outside reviveEntry, so it stays a hard failure.
… answer (stablyai#15351)

A Claude pane parked on a permission prompt published `done` to mobile and
paired clients — the state that retires the card — while the user was still
being asked.

The title layer cannot express permission for Claude or OpenCode: a title reads
`permission` only from a vendor glyph or a synthesized `<Agent> - action
required` label, and SYNTHETIC_AGENT_TITLE_PROFILES has no entry for Claude
(OpenCode sets synthesizeTerminalTitle: false). So `titleConfirmsState` is
unreachable for them and the hook's only protection was a timestamp comparison.

That comparison loses because the two title clocks advance on different events:
lastAgentStatusRichInvalidatedAtEpochMs moves only when the title's status class
changes, while lastOscTitleEpochMs moves on every write. Measured against a real
claude 2.1.234 (two runs, identical structure): the title settles working->idle
~12ms after PreToolUse, PermissionRequest fires ~39ms later, then one more
same-class repaint lands ~123ms after the hook. That last repaint pushes title
evidence past a hook that is still current. The title then goes silent for the
whole prompt (31s and 85s in the two runs), so the wrong verdict is frozen for
as long as the user takes to answer.

Treat a fresh `waiting`/`blocked` under an idle title as non-renewable. Scoped
to idle on purpose: idle is the absence of activity evidence, whereas a
`working` title or a null/shell/identity-only one contradicts the hook and must
still retire the row and its stale question (stablyai#11761). Bounded by the stale
window so an agent killed while parked still decays.

Both state names matter: Claude's PermissionRequest normalizes to `waiting`, not
`blocked`, so a guard written against `blocked` alone passes its own tests and
misses every Claude permission prompt.
nwparker and others added 23 commits August 24, 2026 15:48
Merged after clean CI, Windows packaging verification, and readiness review.
Merged after clean CI, Windows PTY IPC validation, and readiness review.
)

* feat(agent-status): add the pane agent identity resolver

Four ladders answer "which agent is in this pane" independently — the tab icon, the
open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they
disagree. Two consult the terminal title before the launch record, so a string Orca parsed
outranks a fact Orca owns.

resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering:

1. Evidence is ranked by how directly it observes the process; a display title is last.
2. Each observation carries the runId of the agent run it describes. Evidence from a superseded
   run is INELIGIBLE, not merely outranked.

Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming
B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) —
identical signals, opposite correct answers. Run ids make them different facts: in the bug both
belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as
a test asserting the two produce opposite answers from the same evidence.

Missing run ids are treated as eligible. Absence means "this peer does not publish them", not
"this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so
pane-scoped consumers cannot inherit another pane's agent.

No consumer imports this yet; each migrates separately with its own evidence.

Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the
run filter fails 3.

* fix(agent-status): close three resolver contract holes found in review

**Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the
first match, so two live hooks naming different agents were settled by input position — the exact
property this resolver exists to remove. The original order-independence test only used DISTINCT
sources, so it never exercised it. Conflicting same-class evidence now returns null with
`ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two
hooks disagree is worse than saying nothing.

**A bare numeric runId collided across authority restarts.** `incarnation` is a total order only
within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per
authority instance, so a restarted host counting from its own floor would report `1` and match an
unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT
authority is treated as incomparable — kept, like an absent key — rather than as stale.

**Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse
unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker
evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed
string even by reordering its inputs. Display surfaces omit it and are unaffected.

Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was
lost when this branch was rebased.

Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping
the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used
`incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses
differing incarnations.

The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here:
it needs an observation timestamp the evidence type does not yet carry. Recorded rather than
silently dropped.
…ai#16282)

* wip(workspace-cleanup): PR 3 blockers-become-labels, recovered from a dead worker

Uncommitted work recovered from a worker that died with 'Agent process stop was
requested but never confirmed'. Committed as-is to preserve it; NOT verified yet.

* Fix workspace cleanup review regressions

* fix(workspace-cleanup): drop filter chips for the safety fields this PR removes

The cleanup dialog crashed on every open. My merge of stablyai#15300 brought in the
applied-filter chips, which read `safety.tiers` and `safety.selectableOnly` --
the exact fields this PR deletes. Electron QA caught it; the chip derivation runs
on open, so it threw before anything rendered.

Removed the two chip branches, their formatter entries, and the now-dead 'tier'
chip-kind label. The test that swept every chip keeps its breadth by using
`safety.dismissed`, which survives.

Worth noting what did not catch this: typecheck flagged only the test file, not
the source, because the QA agent had already patched the source locally without
committing. A merge that compiles can still remove a field a caller reads at
runtime, and only opening the dialog proved it.
…tablyai#16312)

* fix(sidebar): stop reporting an interrupted agent as done (STA-5357)

An interrupted turn rendered on the worktree card and the tab glyph as `done` —
visually identical to a clean completion. A cancelled or dead turn read as
finished work, and with several agents it contributed no signal at all, so a
sibling that merely finished could hide it entirely.

`interrupted` is a flag clamped onto `done` at parse time, never its own state,
so the activity summary never even received it: its Pick was
{state, workingMode}, and the `done` branch swallowed it into hasLiveDone.

Adds the flag end to end — summary, both activity hooks, the resolver, the card
glyph and the tab badge — and slots it below permission and above working, which
is where SUMMARY_STATE_ORDER already ranked it for the text summary. The card and
the expanded agent list now agree.

The tab-bar test that asserted the old behavior is flipped rather than deleted:
it explicitly pinned `done` to mirror the card, and that premise is gone.

* fix(sidebar): rank interrupted below every live state

Corrects the precedence from the previous commit. Interrupted means the user
pressed Esc or Ctrl+C — a deliberate act, so they already know. It must be
DISTINGUISHABLE from done (that is the bug) without being LOUDER than anything
live.

Moved below done in all three ladders — resolveWorktreeStatus, the tab attention
badge, and SUMMARY_STATE_ORDER, which had also ranked it above working. That
matches smart-attention, which already classes an interrupted done as 4 (idle),
below both done and working; the card, the tab, the summary text and the sort now
agree instead of two of them disagreeing.

Tests re-pinned to the corrected order, including one of my own from the previous
commit that asserted a finished sibling must not mask an interrupted pane — it
should, and now does.

* fix(status): preserve interrupted outcomes in aggregates
* fix(popover): use non-passive wheel listener

* fix(popover): preserve wheel handler cancellation and ref cleanup

* fix(popover): bridge descendant wheel cancellation
Verified on native Windows awin at the exact PR head with Electron CDP/Playwright: the Claude sign-in console is visible, cancellation after console launch restores Add Account state, and the login process/PID/temp cleanup completes.
…lyai#16039)

* Fix deleted remote worktree reappearing due to host ID mismatch

Paired clients and servers may use different spellings for execution hosts
(e.g., client 'runtime:env-1' vs server 'local'). Resolve these spellings
before worktree.rm and worktree.forceDeleteBranch to prevent failures and
orphaned worktrees.

* Validate runtime kind before comparing environment IDs

Ensure parsed host IDs are actually runtime environments before
accessing their environmentId property. Fixes incorrect host ID
matching during worktree cleanup that caused deleted remote
worktrees to reappear.

* Use hostId directly when same-ID surviving host exists

When a surviving host has the same ID as the deleted worktree's
original host, use the hostId directly instead of qualifying it
through the runtime call host. This prevents worktrees from
reappearing due to host ID mismatch.

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…put (stablyai#16325)

* test(terminal): pin park/reveal re-subscribe on a shared multiplexer

Investigating STA-5098. Parking a mirrored remote tab closes its stream
while a sibling tab keeps the multiplexer alive, so the reveal
re-subscribes on an instance that already retired a stream.

This came back green, which exonerates the multiplexer as the cause of
STA-5098 — the wedge is above it. Kept as a contract guard; the header
says explicitly that it is not coverage for that ticket.

* fix(terminal): stop stale multiplex stream handles from swallowing input

A stream handle whose record was dropped (park close, or a reconnect that
clears the stream table) kept reporting success: sendFrame gates only on
socket readiness, never on stream membership. The host drops those frames
for an unknown stream id, so a revealed cold-parked remote pane looked
connected while the PTY never saw a byte and never painted (STA-5098).

Reporting success also defeated the transport's own recovery — it re-sends
input over terminal.send when the stream refuses it, which never ran.

Guard the three public senders on stream membership, the check close() and
setOutputPaused already use, and drain input queued behind a viewport claim
on every stream install rather than only a still-pending claim.

Withdraws the parked-reveal re-subscribe test: its fake host answered
Subscribe with an immediate snapshot, so it could not fail.

* chore(terminal): tighten the stale-stream comments
…73) (stablyai#16345)

* test(pty): pin the renderer-liveness guard STA-2373 relies on (STA-5373)

STA-5373 reported that stablyai#15927 dropped the `webContents.isDestroyed()` half of
the guard stablyai#10065 added for STA-2373, and that the app therefore dies when a
daemon death fans out to every pane. The guard is present on main: stablyai#15172
restored it at both senders when it split the PTY monolith, and a per-file
count across v1.4.188..HEAD shows only relocation (pty.ts:4 ->
write-input.ts:2 + bind-listeners.ts:2). The reported 4 -> 0 was scoped to
`src/main/ipc/pty.ts`, now a 39-line barrel. No production fix is needed.

What was real is that nothing pinned the guard — it survived stablyai#15927 only
because stablyai#15172 happened to re-expand it — and the shared PTY test fake made
that invisible: its `webContents` had no `isDestroyed` at all, so both guards
passed vacuously in every suite. That is also why they are written defensively
as `typeof ... === 'function' && ...`.

- Give the fake an `isDestroyed` mock, re-stubbed to `false` each beforeEach
  rather than left `undefined`, so "alive" is stated rather than accidental.
- Cover both senders red/green: the daemon-death fan-out
  (bind-listeners.ts:37) and the per-write reporter (write-input.ts:57).
  Verified red against a window-only guard and green with the real one.

The per-write case needs a chunked write. A single-chunk write is already
fenced by `isPtyWriteEventFromMainWindow`, which rejects the event once the
WebContents is gone; only the multi-chunk path yields a macrotask mid-write,
letting the renderer die after the sender check passes. That is the sole route
to this sender with a dead WebContents, and what makes its own guard
load-bearing.

Also deletes `src/main/ipc/pty-renderer-surface.ts`: zero importers repo-wide,
and its `isRendererGone` is exactly the weakened predicate. Its comment claims
the headless path "now passes null", but register-headless-runtime.ts:26 still
fakes `{ isDestroyed: () => true }` — stablyai#15172 rolled that back too. Adopting it
would reintroduce STA-5373 for real.

Verified: 17 tests across the two pty write suites; full src/main/ipc run 3156
pass (4 pre-existing @parcel/watcher failures in filesystem-watcher-real and
worktree-base-directory-poller, unrelated and failing identically on a
pristine tree); pnpm typecheck clean; oxlint clean.

* docs(pty): correct headless-path comments the stablyai#15172 rebase left stale

stablyai#15927 made `registerPtyHandlers` accept `BrowserWindow | null` so the headless
path could pass null instead of faking a window. stablyai#15172 reverted that signature
while splitting the PTY monolith, but the comments describing it survived, so
three of them now document an API that does not exist.

- orcad-entry.ts: the module docstring claimed orcad installs its controller via
  `registerPtyHandlers(null, …)`. It uses `registerHeadlessPtyRuntime`, and null
  is not accepted. It also claimed desktop surfaces are "declared rather than
  faked" — true except for the renderer window, which is still faked.
- register-headless-runtime.ts: record why the fake is safe rather than leaving
  `isDestroyed: () => true` looking arbitrary. It is load-bearing: every
  renderer-liveness guard reads it and skips, so no send is attempted.

Also gives the fake a `webContents.isDestroyed`. The real guards check both, and
a missing method reads as "alive" — the same gap this PR fixes in the test fake.
No behavior change: the window-level check already short-circuits.

Verified: 45 tests across the liveness-guard, startup-barrier, management and
kill/exit suites; pnpm typecheck clean; oxlint clean.
…tablyai#16314)

* fix(startup): stop the PATH seed pinning nvm to its newest install

patchPackagedProcessPath prepends the newest nvm version dir to
process.env.PATH, then hydrateShellPath probes the login shell with that
same env. nvm's startup `use` honors whatever node is already on PATH
instead of the user's `default` alias, so the probe returns a PATH pinned
to the newest install and every terminal pane inherits it.

A user whose newest nvm node is a bare install then loses every global CLI
(codex, claude, gemini, vercel...) inside Orca while they still resolve in
Ghostty/Terminal, which start from the bare GUI PATH and fall through to
`default`.

Probe with the PATH the process launched with. Windows already gets this
through WindowsShellPathOwnership.

* test(startup): pin the platform in the probe-env test

shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed
for anyone running the suite on Windows. Matches the convention in
hydrate-shell-path.windows.test.ts.

Also narrows the win32 exemption comment: WindowsShellPathOwnership
snapshots its baseline after the seeds land, so it does not unwind them.
The exemption holds because Windows keys PATH as `Path` and no Windows
seed pins a node version.

* fix(startup): give win32 the same probe insulation, keyed by Path

The previous commit exempted win32 on the stated grounds that no Windows
seed pins a node version. That is wrong: getVersionManagerDirectories
calls getNvmVersionDirectories on every platform, so a Git Bash user whose
nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir
seeded on Windows too, and the -ilc Git Bash probe inherits it.

Record the PATH key alongside the value and overwrite that entry in place,
so Windows never carries both `Path` and `PATH` — which was the only real
reason to skip win32.

* fix(startup): snapshot the launch PATH at module load, log probe failures

Three loose ends from the review, folded in rather than deferred.

The probe's clean PATH was handed over by an explicit recordLaunchPath call
from the seeding site, so the invariant lived across three files and a
refactor that moved the seed call would silently re-pin nvm. Snapshot PATH
during module init instead: that runs while the import graph is evaluated,
strictly before any statement in main's body, so it cannot observe the
seeds and there is no call ordering left to break. All seven importers are
static, so no lazy import can defeat it. A test asserts the probe ignores
later process.env mutation, and fails if the live read is reintroduced.

A failed startup probe leaves the seeded newest-nvm dir in front and said
nothing, so the population whose rc files blow the 5s budget hit the
original symptom with no diagnosable trace. Log the failureReason.

The probe is an interactive login shell, so rc files that exec into a
multiplexer or start a heavy prompt can outrun that budget with no way to
opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path.

* fix(startup): drop the other-cased PATH key from the Windows probe env

Caught by running the suite on a real Windows machine, not a mocked
platform. The spread of process.env is a plain, case-sensitive object,
while Windows resolves env names case-insensitively. Writing the captured
`Path` back onto it left the seeded value still live under `PATH`, so the
probe shell could read either one — the exact duplicate-key hazard the
win32 branch was supposed to prevent.

Drop any other-cased variant of the key before writing.

* fix(startup): preserve launch PATH across app restarts
Resolved conflicts in 5 i18n locale files (en/es/ja/ko/zh). All conflicts
were additive: both branches added sibling keys to auto.components.settings
without modifying the same keys.

3-way merge applied with current branch order preserved; main-only keys
appended at end of auto.components.settings. Verified zero mismatches.
@braitoli
braitoli changed the base branch from feat/cloudflare-tunnel-pairing-option to main August 25, 2026 04:22
@braitoli
braitoli changed the base branch from main to feat/cloudflare-tunnel-pairing-option August 25, 2026 04:22
@braitoli
braitoli deleted the branch Pierre-Mike:feat/cloudflare-tunnel-pairing-option August 25, 2026 04:26
@braitoli braitoli closed this Aug 25, 2026
@braitoli
braitoli deleted the feat/cloudflare-tunnel-pairing-option branch August 25, 2026 04:26
@braitoli
braitoli restored the feat/cloudflare-tunnel-pairing-option branch August 25, 2026 04:27
@braitoli braitoli changed the title merge: resolve conflicts from main into feat/cloudflare-tunnel-pairing-option [skip-track-community] merge: resolve conflicts from main into feat/cloudflare-tunnel-pairing-option Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.