Commit Graph
218 Commits
Author SHA1 Message Date
Brennan Benson 8a07bbd8cf fix(orchestration): enforce nested worker depth instead of an accidental fence (#16668)
* fix(orchestration): enforce nested worker depth instead of an accidental fence

Orca documented that "dispatched workers cannot spawn their own sub-workers
(worker-start is coordinator-fenced)". No such check existed. What existed was a
single Run-binding check in the workerStart RPC: a worker's terminal is not bound
to a Run, so worker-start happened to fail. The rule was emergent, asserted by no
test, and written in no doc — and it leaked. A worker could run-create its own
Run, task-create, and worker-start: now bound, the check passed.

Replace it with a real, configurable depth cap.

Depth is derived from the caller's own active Dispatch rather than from Run
binding, which is what dissolves the run-create bypass: creating a Run does not
stop you being a worker. Enforcement lives in a single dispatch-row writer that
owns all three INSERTs that mint a live worker — the generic claim, the supervised
worker-start path (including every retry), and the remote attachment. Two of those
were missed by earlier drafts of this change, so `creator` and `maxDepth` are
required parameters: a new spawn path cannot compile without deciding, and a
boundary test refuses the SQL anywhere else.

Schema v30 adds depth to dispatch_contexts and remote_dispatch_attachments,
NOT NULL DEFAULT 1 and backfilled to 1 so an unstamped or pre-upgrade row fails
closed rather than reading as a root coordinator. The attachment pane indexes
widen to the five states in which a remote worker may still be running:
loss of contact is not evidence of process death, so an unverifiable worker still
counts as a nesting parent.

Also adds the caller-evidence assertion that workerStart was the only Run-scoped
verb to skip, so a declared --from cannot name another terminal's pane and inherit
its depth.

Default is 1, so behaviour is unchanged unless the new setting is raised. Two
limitations are deliberate and documented rather than papered over: this is a
guardrail and not a security boundary, since a caller whose launch evidence is
unverifiable (any ordinary restored terminal) can declare another handle; and it
is enforced at supervised dispatch creation, so a settled worker whose process is
still alive counts as a root again.

* fix(orchestration): share caller resolution and pin worker gaps

* refactor(orchestration): make the caller resolver's pane contract explicit

Overloads so requireStablePane callers get a non-null string instead of casting,
and rename the attestation opt-out to say what it means: the caller asserts it
itself. A flag called assertEvidence:false reads as "attestation optional",
which is the hole this helper exists to close.

* fix(orchestration): propagate dispatch depth to federated workers

* chore(cli): refresh bundled orchestration guide
2026-08-26 13:22:09 -07:00
Jinjing cda2280d63 Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo

Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.

* Filter automation create projects by destination host

Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.

* Add runtime storage authority support for automations

- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata

* Replace child_process.execFile with runProcess for external automations

- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)

* Unify desktop automation CRUD onto the local runtime RPC surface

The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).

The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.

External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).

* Remove automation ghost SSH tombstone scanning

This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.

* Refuse orphan automations at dispatch time, not migration time

Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.

* Show all automations in flat table with unified filter menu

- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components

* Add automation owner fencing and destination validation

- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers

* Route automation recovery actions to the origin host

When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.

* Remove external manager scope limitation notices

Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.

* Persist only store-derived automation contexts, not client-perspective o

Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
2026-08-26 09:50:12 -07:00
Jinwoo HongandJinwoo-H a9781a4118 STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai>
2026-08-25 15:36:51 -07:00
Neil 2b1b094aa8 fix(cli): pair every resolved CLI with its runtime, and ratchet it (#16383)
Follow-up to #16365, which paired 8 spawn sites by hand. Hand-pairing is how
the class got introduced, so close it structurally instead.

cliPath is now required on CodexAppServerInvocation, `null` only for the
guest-side wsl.exe launcher where a host path pairs nothing. Optional let a
native builder omit it and silently fall back to pairing against a cmd.exe
wrapper with no type error. Every production site already passed it; only
test fixtures needed updating, which is the type doing its job.

Four more sites now pair. codex-state-db-backfill-recovery spawns the same
`codex app-server` subcommand #16365 fixed elsewhere. cli/handlers/account
was the worst case: addAgentNodePaths prepends the *newest* version-manager
bin, which is not necessarily where the CLI being launched lives, so it
actively created the mismatch — pairing now runs last so the CLI's own node
wins. commit-message-text-generation and skills/skill-update-run spawn
resolved binaries with inherited env.

cli/handlers/skills had grown its own buildNpxPath: a weaker local copy that
prepended unconditionally, ignored the Windows `Path` key, and special-cased
a '.' dirname. Deleted in favor of the shared helper, which checks the
sibling node actually exists — the behavior change one test had pinned.

The ratchet is the point: any file that resolves a CLI and spawns must
reference withCliRuntimeOnPath, with a shrink-only allowlist. It caught
skill-update-run, which I had missed. Its first draft required a call paren
and so let dependency-injected resolvers (`resolveCommand: resolveCodexCommand`)
through — verified by removing a pairing and watching it stay green, then
widened until it failed. A second assertion fails on a stale allowlist entry
so an exemption cannot outlive its reason.

external-editor-launch stays allowlisted: it launches a GUI editor, not a
Node CLI whose ABI matters.
2026-08-24 23:12:37 -07:00
Neil 09048c63d4 feat(orcad): add headless browser providers (#16193)
* feat(orcad): add headless browser providers

* fix(orcad): merge the duplicate runtime-browser type import
2026-08-24 21:11:45 -07:00
Neil 0b66daffcc refactor(cli): split orchestration handlers (#16139) 2026-08-24 19:51:40 -07:00
Brennan Benson 31562c5b27 fix(windows): attach interactive login children to console input
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.
2026-08-24 18:12:42 -07:00
erish 5bcbafff53 docs(cli): document worktree rm branch cleanup (#16167)
Document that Git worktree removal may also delete the checked-out local branch, while clarifying that --force does not force branch deletion and that Orca retains branches whose changes cannot be proven merged.
2026-08-23 17:32:36 -07:00
Neiland2sumtech 445c390170 fix(cli): allow an empty --value for storage set commands (#15863)
Co-authored-by: 2sumtech <2sumtech@gmail.com>
2026-08-22 01:37:59 -07:00
Brennan Benson 3fca1d1648 fix(linear): unbound list-issues by default, surface truncation, bind cursor workspace (#15824)
Fixes STA-5076.

list-issues capped at 50 by default and hard-clamped at 250, with hasMore buried
under result.meta and no stderr warning for --json, so a page that stopped early
read as a complete answer. Omitting --limit now walks Linear's pages until they
run out (meta.limit is null), and --limit <n> is the only cap, paging past
Linear's 250-per-request maximum to reach it. result.truncated sits next to
result.issues and is set only when a cap actually held results back; human output
prints "truncated: showing N".

The read still has to fit the CLI's 60s RPC budget, so a 20s wall-clock deadline
and a 200-page ceiling stop the walk early and report truncated with a
continuation cursor rather than failing the command.

Also:
- issued --cursor values bind the resolved workspace, so call -> nextCursor ->
  call works without --workspace; raw Linear cursors still need one and now carry
  nextSteps
- issued cursors whose payload smuggles back `all` or an empty workspace are
  rejected at decode, since either would widen the read past the bound workspace
- JSON issue rows carry priorityLabel (none/urgent/high/medium/low), matching
  orca linear priority set
- truncated and priorityLabel are optional on the wire, so a host that predates
  either is not read as "complete"; readers fall back to meta.hasMore
- the truncation line prints the rows actually rendered, so a remote result with
  no meta.returned cannot print "showing undefined"
2026-08-21 14:28:55 -07:00
Neil ef096d539d fix(terminal): refuse a cursor on a screen read, and correct the source docs (#15563)
Review follow-up on #15380.

The RPC accepted `cursor` and `screen` together. The CLI refuses the pair, but
terminal.read is reachable without it, and honoring both answered with rendered
lines carrying the stream's pagination metadata — two frames of reference in one
payload, which is the confusion `source` exists to remove. The guard beside it,
withVisibleSnapshotFallback, already declines to substitute rendered lines when
a cursor is present; the screen path now agrees, at the RPC boundary where every
remote caller passes. Nothing could previously send both, since `screen` did not
exist, so rejecting breaks no existing caller.

The command notes and the runtime comment both still described the fallback as
`source: stream`, left over from renaming that value to `screen-unavailable`
during implementation. The spec text is surfaced through `orca help` and the
agent-context schema, so a caller following it would test for a value the code
never emits. Both now describe all four states, including that an absent source
means the host predates the field.
2026-08-19 22:42:39 -07:00
Neil 9d1dfc314f fix(cli): resolve host names across both kinds, and stop ssh: answering empty (#15449)
* fix(cli): resolve host names across both kinds, and stop ssh: answering empty

`--host ssh:<id>` was never validated. An unknown target filtered to nothing and
returned ok:true with an empty list — the same silent wrong-machine answer that
unknown `runtime:` ids gave before they were rejected. And because SSH target
ids are machine-generated (`ssh-<timestamp>-<random>`) while the name anyone
actually knows is the label, this fired on the ordinary spelling rather than a
rare typo: every human-typed SSH name missed.

The two kinds of remote machine are also reached on different axes. A paired
Orca server is a connection (`--environment <name>`); an SSH target is a machine
the connected host reaches (`--host ssh:<id>`). A caller only knows "the machine
called X", so naming X on the wrong axis was the common failure and produced
either an empty answer or a dead-end "unknown environment".

Now: `ssh:` resolves labels as well as ids and rejects an unknown target with the
known ones listed; `runtime:` accepts the environment name as well as its id,
matching --environment, and canonicalizes to the id so stored host ids still
compare; and when a name misses on one axis but exists on the other, the error
says which and gives the exact flag. Candidates ride along in error.data so an
agent can recover without parsing prose.

`orca host list` is the discovery surface that was missing entirely — nothing in
the CLI listed SSH targets, so a caller told to use one had nowhere to look. It
prints this machine, the SSH targets registered on the connected host, and the
paired servers, each with the selector to use.

* fix(cli): give --environment the same cross-kind hint, and validate the ssh host on setup-create

Two gaps a follow-up survey found in the first pass.

`--environment openclaw` still dead-ended with a bare "Unknown environment"
while an SSH target by that name sat right there — the inverse of the case just
fixed, and the direction the report actually hit. The store's own error cannot
carry the hint: translateStoreError forwards code and message and drops data. So
the selector is resolved before the client is built, where the payload survives.
Only the explicit flag is asserted eagerly; an ambient ORCA_ENVIRONMENT stays
lazy, because failing local-only commands over stale background config would be
a regression.

`project setup-create` records independent metadata and, unlike the other setup
paths, is not covered by the runtime's ssh rejection — so an unknown target
persisted a row pointing at a machine that does not exist. It now resolves the
host. `local` and `runtime:` still pass through untouched: this is also the
provisioning path, where a runtime host legitimately may not exist yet when its
metadata is written.

`setup-existing-folder` and `setup-clone` deliberately keep the unresolved id.
The runtime rejects every ssh host for those operations regardless of whether it
exists, so resolving first would answer "no such target" and imply the command
would have worked with the right id.

* fix(cli): refuse an ambiguous host name instead of resolving the first match

Name lookup took the first match while the environment store itself refuses an
ambiguous name rather than guessing. That put the guess back, in the selector
whose entire purpose is to stop a command reaching a machine the caller did not
choose — and it applied to both spellings: two SSH targets sharing a label, and
two paired servers sharing a name.

Both now resolve to nothing and report every candidate with its id, so the
caller picks. An exact id still resolves past a colliding name, since an id is
never ambiguous.

Also pins the property that makes accepting a name safe at all: `runtime:<id>`
is a persisted token that lands in ProjectHostSetup.hostId and is embedded in
generated setup ids, so the name is canonicalized to the id before anything
downstream sees it. A test now asserts a name never reaches the wire.

* fix(cli): fall back to the older ssh listing so an old host is not read as having no targets

Hosts predating ssh.listTargetSummaries still answer ssh.listTargets, and both
are served by the same summariser. Swallowing the method_not_found made such a
host indistinguishable from one with no SSH targets registered, which would
reject a target id that is valid there — a new-client/old-host regression on a
path that previously passed the id through unvalidated.
2026-08-19 17:20:21 -07:00
Neil a61b39a9a6 fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792) (#15376)
* fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792)

Two independent frame-of-reference bugs, both from code describing one machine
while labelled as another.

#15366 — projectHostSetup.* persisted the caller's host id verbatim. Those
`runtime:<environment-id>` ids are minted by the calling client's own pairing
store, so they name a machine only relative to that client. A client sending
one is addressing this runtime, and runtimes do not proxy these calls onward,
so the host it names is us. Storing the client's spelling made one machine look
like a different host to every other client, hid its rows from them, and
defeated the (projectId, hostId) duplicate check — two laptops paired to one
server each created their own setup for the same checkout. Re-spell it as
`local` at the RPC boundary. Rows written earlier keep their old stamp; readers
already project `local` back to `runtime:<their-id>`, so the client-visible
model is unchanged and no ids are rewritten.

STA-4792 defect 4 — `status --environment <name>` hardcoded app.running:false
to mean "no desktop on THIS machine" while every other field in the same object
described the target, including a desktopWindowStatus echoed straight from it.
The result contradicted itself and read as "that run was headless" when the
remote GUI was up. `app` now describes the target, keyed off the one window
status that requires a live renderer, and the result names its own subject so
the frame can't be misread again. The remote pid is not knowable, so it stays
null.

STA-4792 defect 2 gets a regression test rather than a fix: routing already
made the client remote, which is what stops a Windows destination being joined
to the local cwd. The test pins the exact reported invocation.

* fix(status): share the remote app projection with the SSH host passthrough, and name the version gap on project host setup

Two review follow-ups.

The SSH host passthrough answered `app.running: true` unconditionally for the
Orca host a caller reached over SSH, claiming a desktop app even for a headless
`serve`. That is the same defect as the paired-server path, one transport over,
so the projection moved to shared and both now answer the question the same way.

`--host runtime:<id>` routes project commands to a paired server, which means a
client can reach a server that predates project host setup without meaning to.
That answered a raw `method_not_found`, which reads as an Orca bug rather than a
version gap; the CLI now names it the way the desktop already does.

Reverted a third change: making the persistence duplicate check treat `local`
and `runtime:*` as one machine. That assumption holds at the RPC boundary, where
a `runtime:` host means the runtime being addressed, but not in the store, which
also records independent provisioning metadata for machines that are not itself.
An existing test covers exactly that, and it was right. The duplicate
convergence therefore stays bounded to rows written after the normalization.
2026-08-19 17:12:17 -07:00
Neil 3ffab9a6b3 feat(terminal): read the rendered screen with terminal read --screen (STA-4792) (#15380)
* feat(terminal): read the rendered screen with `terminal read --screen` (STA-4792)

`terminal read` returns accumulated pty output with escape sequences stripped.
That is the right answer for "what happened over time" and the wrong one for
"what is on screen": any program that repaints a line comes back as stacked
fragments, so one `clear` typed key by key reads as `cclclecleaclear`, and a
prompt that draws a space by moving the cursor loses it. Nothing in the output
said which question had been answered, so it was used as rendering evidence and
produced false conclusions.

The runtime already knew how to render — it replays the byte stream through a
headless emulator — but only as a fallback for blank reads, alternate screen,
and never-attached ptys. A normal attached terminal never reached it. `--screen`
asks for it directly.

Every read now reports its source, which also surfaces the pre-existing
snapshot fallback that until now swapped rendered lines into an ordinary read
with no indication. `screen-unavailable` distinguishes "asked for a screen,
none could be rendered, here is the stream" from a stream the caller asked for,
and an absent source means the host predates the field. Because an older host
strips the unknown param and answers with its ordinary read, `--screen` against
one fails with that explanation rather than passing the stream off as a screen.

`--screen` and `--cursor` are mutually exclusive: a screen is the current frame
and has nothing behind it to page.

* refactor(terminal): stamp the screen source where rendered lines enter the read

Inferring it from tail array identity worked but made a load-bearing contract
out of reference equality; any later path spreading the read would silently
mislabel. Rendered lines only enter through one builder, so it stamps there and
anything still unlabelled is the stream.
2026-08-18 22:51:37 -07:00
Jinwoo Hong 9b5538d786 fix(runtime): scope create-with-activate navigation to the requesting client (STA-2802) (#15407) 2026-08-18 21:37:35 -07:00
Jinwoo Hong 79be5b7fde feat(orchestration): report a worker blocked on a human prompt (STA-4513, STA-3714) (#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.
2026-08-18 14:19:20 -07:00
Neil 4cc7e7859a fix(cli): route --host runtime:<id> to that server instead of answering locally (#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.
2026-08-18 14:12:24 -07:00
Brennan Benson 2a760e310b fix(computer): report unasserted accessibility actions (#15028)
* fix(computer): report unasserted accessibility actions

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

* Fix merged tab search test fixture
2026-08-18 11:29:26 -07:00
Brennan Benson fc8b92e507 docs(computer): explain screenshot file requirements (#15054)
* docs(computer): clarify screenshot output requirements

* fix(cli): do not advertise an unshipped --probe flag

The capabilities help line referenced --probe, which does not exist yet;
it ships in a later change. Advertising it here would be false until then.

* fix(cli): align computer-use screenshot guidance

* docs(computer): document inline screenshot fallback

* docs(computer): keep screenshot summary accurate

* docs(computer): keep screenshot guidance general
2026-08-18 01:18:56 -07:00
Brennan BensonandQA 64de8dd637 fix(workspaces): delete on the confirmed host, and make both hosts' rows selectable (STA-4343) (#15013)
* fix(workspaces): host-qualified workspace deletion (STA-4343, STA-4448)

Squashed integration of PR #14606 + the codex review-loop output, replayed
onto current main. Granular history preserved on brennanb2025/sta-4343-review-full.

Fixes the regression from #13413: a workspace id is repoId::path with no host
component, so the same repo at the same path on two hosts published one id for
two workspaces, and deletion routed by that id landed on whichever host routing
preferred - usually the ACTIVE one, not the row the user confirmed.

- removeWorktree takes a REQUIRED host-qualified WorktreeRemovalTarget; omitting
  the host is a type error. All destructive callers migrated.
- Projections dedup on (host, id), so two hosts render as two selectable rows
  while the createWorktree/fetchWorktrees race duplicate still collapses.
- Ephemeral VM cleanup is host-scoped. It matched on bare workspaceId, so the
  host-scoped delete path destroyed the SURVIVING host's VM and its unpushed
  filesystem - a leak fix that had become data destruction.
- Selection, keyboard routing, lineage grouping and Space rows carry host
  identity end to end; fixing the executor dedupe alone would have turned
  one-row intent into deleting both hosts.

Files split to stay under max-lines rather than raising any cap.

* refactor: split files that crossed max-lines

The review-loop commits used --no-verify, so the pre-commit hook never
enforced the caps. Extracted cohesive units rather than raising any limit:
renderer teardown, delete-with-toast, pinned-group rows, host-scope helpers,
workspace-kind predicates, filter actions, kanban drag selection, the
renderer removal result type, and the native-chat persistence tests.

* refactor(workspaces): extract cleanup deletion-phase selector

Clears the last max-lines violation and the import-type side effect the
changed-code gate flagged.

* refactor(sidebar): track the delete-dialog extraction modules

* fix(workspaces): preserve host identity across remaining surfaces

* fix(sidebar): re-carry host through the rewritten palette result model

#15170 replaced PaletteSearchResult while this PR was open. Re-applied the
host qualification on top of the new model instead of taking either side:
results carry worktreeHostId again, and the board filter keys its matched
set on host identity rather than the bare id.

Known gap, documented in the board test rather than deleted: searchWorktrees
resolves evidence through a `documents` map keyed by BARE worktree id, so two
same-id host rows collapse before this code sees them. Closing that belongs
with the palette work.

* test(cmd-j): pin the palette collision gap instead of asserting the old model

The palette collision test asserted two host-qualified rows, which #15170's
rewrite made unreachable: item ids are bare again and worktreeMap is id-keyed.

Rewritten to assert what holds — activation always names a host — and to pin
the defect it exposes: two same-id rows render on ONE command value, so React
sees duplicate keys and a click on the first row activates the second row's
host. That reproduces on main, so it is pre-existing, not from this PR. Pinned
rather than deleted so fixing it must update this test.

---------

Co-authored-by: QA <qa@local>
2026-08-17 15:57:07 -07:00
Jinwoo Hong 0bedeea642 fix(orchestration): expose unsupervised dispatch lanes (#15105) 2026-08-17 13:53:26 -07:00
Jinwoo Hong be07b43a2b fix(orchestration): enforce honest recipient routing (#14964) 2026-08-17 01:50:17 -07:00
SebastianandBrennan Benson 04e7f5c805 fix(cli): relativize absolute POSIX paths against UNC worktree roots in WSL (#11406)
* chore: ignore worktrees directory

* fix(cli): relativize absolute POSIX paths against UNC worktree roots in WSL

* fix(cli): prevent double-prefixing UNC paths in WSL path normalization

* fix(cli): gate the WSL path rewrite on a UNC worktree root

WSL_DISTRO_NAME is also set for a plain Linux CLI inside the distro, where
worktree roots are POSIX; prefixing there stranded every absolute path.
Rewrite only when the root really is a WSL UNC path, and cover the legacy
wsl$ alias, cross-distro paths, and the non-WSL case.

* test(cli): pin WSL_DISTRO_NAME absent for the whole file-path suite

Contributors run this suite inside WSL, where the inherited distro name
would flip the rewrite on for every POSIX-root case.

* fix(cli): never rewrite a Linux path that contains a backslash

Backslash is a legal Linux filename character but a separator once the
path reads as UNC, so `a\b.ts` relativized to `a/b.ts` — a different
file. Such a path has no UNC spelling; let it fail the match instead.

* test(cli): pin the WSL rewrite's negative space

Sibling-prefix roots, Linux-tail case sensitivity, and Windows
drive-letter workspaces all passed only by construction.

* test(cli): pin the distro-case fold from the CLI side

The negative-only case passed identically with the fold broken.

* fix(cli): name the WSL distro from the invocation cwd when the env is absent

WSL_DISTRO_NAME only reaches the CLI if interop forwards it across the
PowerShell bridge, which nothing in the launcher guarantees. ORCA_CLI_CWD
is set explicitly and its UNC form already names the distro.

* test(cli): match the launcher's real cwd spelling and fix an over-claim

wslpath -w emits a backslash UNC path; the fallback test now uses that
shape. The aliasing test's comment described a state the || guard makes
unreachable.

* refactor(cli): spell the WSL rewrite with the shared toWindowsWslPath helper

src/shared/wsl-paths.ts already owns "absolute Linux path in a known distro
-> its Windows form" and has five production callers; the handler hand-rolled
a fourth copy of the UNC template. Behavior is identical under the UNC-root
guard, and passing distro as a real argument makes the null check a compile
error rather than an untested branch.

* test(cli): drop a WSL case that pins the guard shape and kills no mutant

Deleting the distro null check left the case green — it asserts the same
passthrough as 'does not rewrite when the CLI is not running under WSL'. The
check is now enforced by the compiler instead.

* chore: keep WSL path fix scoped

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-17 00:52:38 -07:00
Brennan Benson 7afce2ea41 fix(ssh): stop reporting a confirmed kill when the SSH provider is gone (#14977)
* fix(ssh): stop reporting a confirmed kill when the SSH provider is gone

A detached relay PTY is designed to outlive the provider that addressed it
(it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider
is no longer registered" is lost contact, never evidence the remote process
stopped. Both stop primitives in the PTY controller returned `true` from
that branch, and every caller downstream reported the fabricated success:
the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped,
and — because the stop "succeeded" — the unstopped-PTY gate never ran, so
worktree removal walked straight past a live remote agent.

`kill`/`stopAndWait` now still tombstone the local lease but report an
unconfirmed stop and record why, using the three-verdict vocabulary the
worktree teardown gate already spoke (`live` / `unverifiable` / `exited`),
promoted out of that module into `src/shared/pty-liveness-verdict.ts`.
The close receipt, the CLI wording, worker-stop and the removal gate all
read that verdict instead of inferring an exit from silence.

The same rule fixes the mirror-image defect: the aggregate inventory only
enumerates registered providers, so a dropped relay clears `connected` for
every remote PTY at once. The sweep now separates the provider answering
"absent" (an exit) from no provider being able to answer (lost contact), so
worker-stop stops claiming `exited` from a disconnect.

The `connected` wire field is unchanged in meaning and shape.

* fix(orchestration): apply the same honesty to the federation stop path

The federation host runs its own copy of the worker observation and stop
logic, with the same two defects: `inspectRemoteAttachment` read a dropped
relay's `connected: false` as `exited`, and `federationStop` settled the
dispatch as stopped from a close it never confirmed — relaying a fabricated
success all the way home to the coordinator.

Two guards also had to move so the honest verdict does not become a new
refusal. `federationRead` gated on `status !== 'running'`, which would have
rejected a connected terminal the moment a stop lost contact with it; it now
gates on `status === 'exited'`, which is equivalent for every pre-existing
status given the two guards beside it. Local `workerStop` likewise still
attempts the close when the verdict is `unverifiable` — losing contact is a
reason to report the outcome honestly, never a reason to stop trying.

The show observations now carry the reason alongside the status, so a bare
`unverifiable` is actionable. Both are new optional fields.

* fix(ssh): preserve unconfirmed stop verdicts across consumers

* fix(ssh): use canonical live verdict wording

* fix(ssh): refuse wrong-host teardown verification

* test(orchestration): confirm worker release teardown

* fix(orchestration): negotiate honest worker stop receipts

* fix(agent-teams): fence uncertain teammate respawns

* fix(ssh): avoid duplicate missing-provider teardown

* fix(orchestration): preserve archives across release retries

* fix(ssh): preserve verdicts across synthetic kill exits

* fix(ssh): preserve liveness evidence across teardown

* fix(agent-teams): replace panes only after confirmed stop

* fix(ssh): distinguish host exits from relay loss

* fix(ssh): narrow concurrent inventory verdicts

* fix(orchestration): serve archives after uncertain release

* fix(orchestration): expose unverifiable read liveness

* test(ssh): align liveness assertions with verdicts

* fix(ssh): preserve host scope across inventory failures
2026-08-17 00:11:19 -07:00
Brennan Benson 8ca4ed945e feat(terminal): report execution host and listing scope in terminal list (#14973)
* feat(terminal): report execution host and listing scope in terminal list

`orca terminal list` returned rows with no host identity and no statement
of what the listing covered, so a scoped listing that saw nothing read as
"nothing exists anywhere" — an agent reported a live remote worker dead.

Each row now carries an optional `executionHostId` derived from the PTY id
(SSH and paired-runtime ids embed their owner), and the result carries an
optional `hostScope` naming the hosts covered and the known hosts skipped.
Both are surfaced in `--json` and in the human-readable CLI output, where
an absent field renders as `unknown` rather than `local`.

Both row builders route through one resolver, so the rule lives in one place.

* fix(terminal): preserve unverifiable host scope

* fix(terminal): fail closed on unverifiable hosts

* test(terminal): name unverifiable scope explicitly

* perf(terminal): keep graph hydration host scans narrow

* fix(terminal): reject blank foreign host owners

* fix(terminal): validate inferred inventory hosts

* fix(terminal): preserve paired folder host scope

* fix(terminal): keep inventory host inference typed

* fix(terminal): disclose paired folder hosts
2026-08-16 22:13:03 -07:00
Jinwoo Hong fa9b20cb41 feat(skills): reland private bundle sharing safely (#14934) 2026-08-16 13:45:54 -07:00
Jinjing 763b1febeb Revert "feat(skills): add private bundle sharing (#14401)" (#14913)
This reverts commit 757fae28d7.
2026-08-16 10:39:57 -07:00
Jinwoo HongandE2E Test 757fae28d7 feat(skills): add private bundle sharing (#14401)
Co-authored-by: E2E Test <e2e@test.local>
2026-08-16 02:36:18 -07:00
Jinwoo Hong d2ffe1f362 fix(terminal): settle CLI prompts for Claude and Codex (#14608) 2026-08-15 15:45:17 -07:00
Neil 9367169888 refactor(tests): split every oversized test file off the max-lines suppression list (#14728)
* refactor(tests): split oversized test files off the max-lines suppression list

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

A real renderer never loses `window`, so deleting it was the artificial part.
Swap in an inert proxy whose properties resolve to callables and whose calls
resolve to undefined, making a late `window.api.pty.*` call a harmless no-op.
The next test replaces it wholesale via installTerminalTestGlobals(), and no test
asserts that `window` is absent.
2026-08-15 00:54:20 -07:00
Brennan Benson 66dfdc456f feat(computer-use): support macOS middle click and stop the silent left-click fallback (#14721)
* feat(computer-use): support macOS middle click and gate the AX click path

`--mouse-button middle` already validated end-to-end through the CLI, the
zod schema, and the provider validator, and both the Windows and Linux
providers honored it. Only the macOS provider rejected it outright with
"middle-click is not yet supported", so the flag was a dead end on the one
platform that has no fallback.

Two changes:

- Add `.middle` to the macOS button mapping. macOS has no dedicated middle
  event family, so it rides `otherMouseDown`/`otherMouseUp` with the button
  number carried by `mouseButton: .center`; that constructor argument is
  honored for exactly the `otherMouse*` types, so no extra field write is
  needed.
- Validate the requested button before the accessibility fast path, and skip
  that path for buttons it cannot express. Previously the raw string was read
  unvalidated, and `performClickAction` only special-cased `right`, so
  `click --mouse-button middle --element-index N` (no modifiers, count 1) fell
  through to `AXPress` — a left click — and reported success with
  `path: "accessibility"`. Any unrecognized button string did the same. This
  matches guards the Windows and Linux providers already had.

The button enum moves into `OrcaComputerUseMacOSCore` so it is unit-testable;
`main.swift` keeps only the CoreGraphics mapping.

Also documents `--mouse-button` in the computer-use skill guide, which never
mentioned the flag, so agents on Windows and Linux had no way to discover it.

* test(computer-use): cover macOS middle click in the real-desktop e2e suite

* test(computer-use): prove macOS middle-click delivery
2026-08-15 00:41:45 -07:00
Brennan Benson 78d5920446 fix(orchestration-cli): point dropped mutations at --retry-request (#14586)
* fix(orchestration-cli): guide dropped mutations to idempotent retry

* test(orchestration-cli): preserve read-only drop message

* fix(orchestration): harden mutation replay identity

* fix(orchestration): preserve replay across remints

* fix(orchestration): defer local mutation identity
2026-08-14 18:11:12 -07:00
Jinwoo Hong 500b72d8ef fix(vm): harden provisioned root ownership and cleanup (#14477)
* fix(vm): verify provisioned root ownership

* test(vm): retry transient removal menu

* test(vm): stabilize provisioned root teardown

* fix(vm): clarify recipe-owned cleanup

* fix(vm): pin provisioned root source commit

* fix(vm): make runtime cleanup user-cancellable
2026-08-14 19:04:55 -04:00
Brennan Benson 83e2123582 Add global worktree visibility source defaults (#14276)
* Add global external worktree visibility defaults

* Expand global worktree visibility source defaults

* Fix host-scoped visibility settings races

* Fix global worktree visibility integration

* Enable source visibility defaults on mobile

* Polish external worktree settings navigation

* Clarify inherited worktree visibility settings

* feat(sidebar): replace the inherited-visibility switch with a Show/Hide picker

Each source row now shows a two-segment Show / Hide control preselected to the
global setting, and explains itself only where the project actually disagrees:
an "Overriding global setting: <value>" card names the value being ignored.
Picking the segment global already holds drops the override instead of pinning
a duplicate, so the same control both overrides and reverts, retiring the
separate "Use global" link. The dialog footer now lists every inheritable
source with its global value.

* fix(sidebar): preserve reset for matching visibility overrides
2026-08-14 12:15:58 -07:00
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00
Neil 583ab1601b refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.

Move each domain into its own folder and drop the now-redundant prefix:

    src/shared/github-pr-types.ts    -> src/shared/github/pull-request-types.ts
    src/shared/worktree-id.ts        -> src/shared/worktree/id.ts
    src/shared/linear-links.ts       -> src/shared/linear/links.ts

This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.

Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.

Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.

Two things `tsc` cannot catch, handled explicitly:

- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
  entry is REPOINTED to the new path rather than pruned. Pruning would drop the
  bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
  (`mobile/node_modules` is empty). Instead every relative specifier in the repo
  was resolved against the filesystem: 174 unresolved before this change and 174
  after — identical, so nothing broke in mobile either.

The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
2026-08-13 20:44:16 -07:00
Brennan Benson 537864a248 Fix Codex hook trust before manual shell launches (#14326)
* fix codex hook trust before shell launch

* fix packaged cli preflight dependency

* fix codex shell preflight safety

* fix Codex shell preflight settings and startup safety
2026-08-13 17:02:28 -07:00
Jinwoo Hong 77b37d85e2 feat(vm): create workspaces from provisioned SSH roots (#14359)
* feat(vm): use recipe-provisioned SSH roots

* fix(vm): preserve ordinary create failure timing

* test(vm): prepare provisioned root SSH fixture

* ci(vm): enable SSH setup for provisioned root E2E
2026-08-13 16:23:22 -07:00
erishandJinwoo-H 1f4b731f7c fix(skills): use exported recipe id in environment guide (#14280)
* fix(skills): use exported recipe id in environment guide

* fix(skills): keep recipe-derived Vercel names valid

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-08-13 14:18:57 -07:00
Brennan Benson cbca291aa7 fix(orchestration): preserve direct user authority after worker_done (#14192)
* fix(orchestration): preserve direct user authority

* test(orchestration): assert settled dispatch boundaries
2026-08-13 12:00:04 -07:00
Neil 4882eeb8ac rm git shim: neutralize stale wrappers without a host gate (#14255)
* Revert "fix terminal attribution shim removal edge cases (#14187)"

This reverts 585dd6d3a9. Re-landed in the next commit without the host capability gate. Nothing shipped with it, so no migration constraint.

* rm git shim: neutralize stale wrappers without a host gate

Re-lands the cleanup half of #14187: pass-through tombstones for retained wrapper paths, env/PATH scrubbing at every spawn owner, and the retired setting drop.

Only writes tombstones when the legacy directory already exists, so a clean install no longer has it created. Leaves out the terminal.attribution-removed.v1 capability gate: the tombstone neutralizes each host locally, so refusing terminal create/split against older hosts denied service without adding cleanup.

* rm git shim: surface neutralization failures and fix rollback marker

Readiness review follow-ups: warn on each failed attempt and on give-up (was silent and undiagnosable); write a VERSION marker distinct from the retired shim's '7' so a rolled-back build rewrites its own wrappers; clear a captured ORCA_REAL_* path that no longer exists so the cmd wrapper's where.exe fallback can run; stop a locked temp file masking the real error. Adds retry-exhaustion coverage.

* rm git shim: pin the cmd fallback order and correct the give-up count

Round-2 review follow-ups: string-pin that a stale ORCA_REAL_* is cleared before the where.exe fallback, and count the initial attempt in the give-up warning so it agrees with the per-attempt line.

* rm git shim: keep the split-failure toast

The revert took a toast that #14187 added alongside the gate but which stands on its own: without it a failed remote split only reaches the console and the pane silently never appears. Also pins attempt ordinals in the retry-exhaustion test.
2026-08-13 03:01:45 -07:00
Jinwoo Hong 9cfa00d665 Fix federation terminal settlement retries and legacy admission (#14105) 2026-08-13 02:31:56 -07:00
Neil 585dd6d3a9 fix terminal attribution shim removal edge cases (#14187)
* fix(terminal): fully retire attribution shim

* fix(terminal): harden shim tombstone path lookup
2026-08-12 23:22:48 -07:00
Jinwoo Hong d349f9a972 Use provider-neutral Opus alias in orchestration guide (#14119) 2026-08-12 16:03:29 -07:00
Jinwoo Hong a868b090e0 fix: connect mobile emulator in folder workspaces (#14009) 2026-08-12 13:02:23 -07:00
Jinwoo Hong 7319d59a10 Make worker completion and cleanup authoritative (#13927)
* fix: require authoritative worker completion verdicts

* Harden federated settlement replay

* fix(orchestration): reconcile dead retained workers

* docs: record SSH worker release coverage

* test(e2e): exercise worker settlement and release CLI

* docs: register combined orchestration CLI oracle

* test(orchestration): pin pre-ack attachment state
2026-08-11 23:06:12 -07:00
Jinwoo HongandJinwoo-H 45c1cb979a fix(orchestration): release context-only dispatches (#13376)
* fix(orchestration): release context-only dispatches

Refs #13005

* test(orchestration): align PTY readiness timeout

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-09 14:49:36 -07:00
JinjingandOrca 3ec48a74d5 Gate artifact publishing behind off-by-default capability (#13368)
* fix(artifacts): gate agent artifact publishing behind an off-by-default capability

Public artifact sharing was reachable by any agent through `orca artifacts
share`: the Artifacts settings toggle only controlled sidebar visibility, and
nothing in the main process checked a capability before minting a public URL.

Add `artifactSharingEnabled` (default off) and enforce it in
ArtifactCloudService.share/update — before auth, network, or the share-record
write — so the CLI, relay-forwarded remote CLI, and IPC paths are all denied.
The denial carries a stable `artifact_sharing_disabled` code plus next steps
through the RPC error allowlist, so the CLI prints actionable guidance.

list, unshare, and delete stay ungated: turning publishing off must not strand
already-published links. The capability is absent from the `settings.update`
RPC schema, so an agent cannot grant it to itself — only the desktop UI can.

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

* fix(artifacts): gate agent artifact publishing behind an off-by-default

Publishing is blocked until enabled in Settings → Artifacts. CLI preflights the capability before reading files to avoid unnecessary uploads. RPC surface rejects capability grants so callers cannot self-grant. UI shows opt-in workflow and recovery path when publishing is off. Web clients mirror the host's setting read-only.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 13:19:08 -07:00
Jinwoo Hong c991bb27d3 Add account-backed artifact sharing (#13012) 2026-08-07 23:02:29 -07:00
Wooseong KimandJinwoo-H f057cbc85f fix(serve): recognize CLI-form serve args on the Electron process (#12818)
* fix(serve): recognize CLI-form serve args on the Electron process

When the binary is launched as `… serve --port …` without the CLI rewrite
that injects `--serve`, normalize argv so isServeMode, headless GPU flags,
and serve option parsing all engage.

Preserves existing `--serve*` flag behavior for the CLI-spawned path.

Fixes #12677

* fix(serve): treat only CLI subcommand position as serve

Parse bare `serve` as the first positional token after flags/values so an
option value named `serve` cannot enable headless mode.

Addresses CodeRabbit on #12818.

* fix(serve): keep CLI redirects ahead of the serve argv rewrite

Rewriting argv before maybeRedirectAppImageCliLaunch replaced the `serve`
positional with `--serve`, so the redirect's command-name lookup saw a port
number and bailed — dropping AppImage serve launches out of the CLI path.

Also translate `--port=6768` (the CLI accepts it, getServeOptions only reads
the next token) and the mixed `--serve --port` form, so a security-shaped flag
like `--no-pairing` can no longer read as accepted while pairing stays on.
Map lookups replace `in` on object literals, which turned a stray `serve
toString` positional into a function spliced onto argv.

* fix(serve): close the CLI-form serve gaps found in review

second-instance: shouldActivateDesktopForSecondInstance matched only `--serve`,
so a duplicate `<binary> serve --port …` — the ExecStart shape documented in
docs/reference/headless-linux-server.md — promoted the live headless server to a
desktop window, un-fixing #11935 on exactly the launch shape this PR legitimizes.

findServeSubcommandIndex consumed a flag's value unconditionally while the
rewrite consumed it only when the next token was not flag-shaped. The two could
disagree and swallow the `serve` token, leaving `--serve` uninjected: #12677
again in a new shape (`--port --port serve`, `--port -- serve`). Both scans now
share one definition of value consumption.

`<binary> serve --help` / `serve help` bound a network-exposed runtime server
with pairing on and printed nothing; the AppImage redirect already routes those
three tokens to the CLI, so refuse them here too.

`--no-pairing=false` translated to `--serve-no-pairing` with the value dropped,
disabling pairing for an operator who asked for the opposite. The CLI reads its
serve booleans as `flags.get(name) === true`, so a boolean is now translated only
in its bare form and the `=` form rides through as the CLI treats it.

Tests: spec-derived parity between src/cli/specs/serve.ts and the rewrite,
covering both ends of the contract (serveOrcaApp and getServeOptions); a
source-text lock on the index.ts redirect/rewrite ordering, which reverted
silently green before; an exhaustive self-consistency property test; and the
real GUI launch argv shapes that must never enter serve mode.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 23:56:34 -07:00