Commit Graph
7143 Commits
Author SHA1 Message Date
Jinjing 8dd5afb996 Fix rebase race by fetching to private ref before rebasing
`git pull --rebase` is vulnerable to concurrent fetches modifying remote-tracking refs during execution. Fetch to a temporary private ref (refs/orca/rebase/*) first, then rebase from that stable ref to avoid the race condition.
2026-08-24 11:35:13 -07:00
Jinjing aa32871a61 Improve cmd j ranking with recency (#16281)
* Track container-only tokens and tab focus for cmd+j ranking

Previously ranked by whether any container-only matches existed (boolean);
now counts tokens matching only containers for finer-grained ranking. Tab
focus recency is now tracked explicitly so recent refocuses rank above
stale worktree activity. Preserves worktree grouping by input order while
applying focused-group MRU within each block.

* fix(cmd-j): preserve duplicate recent tab occurrences

* fix(cmd-j): preserve host scope during worktree purge

* fix(cmd-j): scope repo purge for exact-id host twins

* fix(cmd-j): scope ssh visit recency to local to survive restarts

Boot hydration loads only local + runtime:* partitions, so routing
ssh-qualified recency to ssh partitions strands it across restarts.

- Keep ssh-qualified visit timestamps in local partition
- Route runtime-qualified keys to their partition
- Remove groupId from recent tab occurrence base (unstable on regroup)
- Collapse bare and host-qualified timestamps, preserving max
- Simplify repo pruning host-match logic
- Add robustness: optional chaining, helper function

* Scope focused tab recency by worktree to fix Cmd+J ranking

Tab ids can be duplicated across worktrees; scoping recency keys to per-worktree prevents one worktree's MRU position from overwriting another's in Cmd+J. Scope worktree order blocks to (hostId, worktreeId) to keep same-id worktrees on different hosts separate.

Also fix recency preservation during partial identity migrations and prune orphaned host keys on removal.
2026-08-24 11:23:16 -07:00
Brennan Benson 94f231737d fix(agent-status): retire panes whose agent process is gone (STA-4612) (#15212)
* fix(agent-status): retire panes whose agent process is gone (STA-4612)

Agent status can hold `working` on a pane where no work is outstanding, and
nothing closes the gap. A pane's Claude state is a join of a lead turn and three
latches — the subagent roster, the background-task gate and the session-cron gate
— and each is set by a hook and cleared only by another hook. Claude Code emits
no terminating hook on `/exit`, `/clear`, Ctrl+C, crash, SIGKILL or terminal
close, so every one of those latches is a claim with no owner and no expiry. The
join is also materialised at ingest time and persisted, so a stale `working`
survives restart and blocks hibernation, which requires `done`.

Registering `SessionEnd` is not the fix: it covers roughly a third of exit paths
(measured on 2.1.231/2.1.233; upstream anthropics/claude-code#17885 and #6428 are
both closed as not planned). Nor is a TTL — `AGENT_STATUS_STALE_AFTER_MS` only
decays the sidebar dot at read time while the stored row stays non-terminal.

So the backstop is built from evidence Orca already owns.

A session id that changes means the conversation was replaced. On the first hook
of the new session — whatever that hook is — the previous session's own claims
are void: its session crons and its one-shot subagents. Deliberately not voided:
the background-task gate (a background shell is an OS process that survives
`/clear`, and the previous inventory is positive evidence it was running), and
`confirmedTeammate` rows (persistent in-process teammates a lead swap cannot
end). The lead record is left to the incoming event's own fold.

A certified process exit retires the pane. Orca already does this on every
attributable PTY exit — `clearProviderPtyState` resolves the pane key and calls
`clearPaneState` — but that resolution depends on the spawn-time `ptyPaneKey`
mapping, which a restored or reattached PTY may never rebuild. Those panes keep
their row and latches for good. `onPtyExit` knows the keys teardown could not
resolve, so it reconciles them from its own records. The certificate is
`exitCode >= 0 || hostExitConfirmed || providerExitObserved`: a synthetic `-1`
from a failed stop is not a death (the PTY can have survived it), while a real
exit can also report `-1`, so neither the code nor the SSH surface predicate is
sufficient alone. `providerExitObserved` is additive and separate from
`hostExitConfirmed`, which also drives the liveness verdict and the SSH surface
decision.

A confirmed shell foreground is the `/exit` case: the agent died, the shell
lived. That already dropped the row, but through `agentStatus:drop`, which by its
own contract preserves a live pane's caches — so every latch survived and the
next event resolved the pane back to `working`. It now routes through the
reconciler instead, gated on a per-pane accepted-status generation rather than
row identity: the confirming process read can take seconds, and `updatedAt`
cannot order two writes inside one millisecond (the store deliberately admits
equal timestamps).

Cold start generalises the same way. The startup sweep required a restored
subagent roster, so a stranded lead row, background-task gate or cron gate — the
shapes with no child event left to reap them — were never candidates.

Hibernation needs no change: with the above, those rows become genuinely `done`
and the lockout resolves through the front door. A `restoredUnconfirmed` bypass
in the planner would let it reclaim the heap of an agent that may be working.

Not included: folding `background_tasks` from a child-attributed `SubagentStop`.
Writing its test surfaced #11838's deliberate assertion that child inventories
are not authoritative for lead-owned background work, and the listener says the
same — "background_tasks is trusted only where unambiguous". An empty list on a
`SubagentStop` does not prove the lead's shell ended, so the fold would have
cleared a gate on evidence that establishes nothing.

STA-4119's live-side question — whether a genuinely live background shell should
hold the lead row after the lead turn ends — is untouched. This change extends
gate-clearing to zero new triggers.

* fix(agent-status): make the confirmed-shell reconcile survive its own drop

The /exit leg never fired. `settleDeferredCommandFinishedStatusDrop` runs the
paired drop before the reconcile, and `dropAgentStatus` cleared the per-pane
accepted-status counter the reconcile's guard then read — so the guard compared
a live anchor against a zeroed counter and skipped itself on every pane that had
a status row, which is every pane worth reconciling. The existing test passed
only because it used a pane with no row, where the drop early-returns and both
sides read 0.

Stop keying the guard on a counter a sibling teardown path can reset: the
ordinal is now stamped on the row itself, derived from the row it replaces, so
there is no side table to clear and a batched burst lands the same ordinals as
the equivalent sequential writes. A removed row means "nothing reported", which
is exactly what the paired drop leaves behind.

Also:
- Keep the `providerSessionOnly` resume identity that the paired dismissal mints
  when the shell outlived the agent; a certified PTY exit still takes it, since
  there is no pane left to resume into.
- De-vacuum two guard tests. The confirmed-teammate pin never anchored a session
  owner, so the void it claimed to survive never ran; the unavailable-inspection
  pin asserted before the confirm ladder settled. Both now fail when their guard
  is removed.
- Derive `hasLiveClaimsForPaneKey` from a predicate that lives beside
  `clearPaneCacheState`, so a new latch cannot be added to the teardown and
  silently missed by the claim check.
- Drop the unreachable compact-`trigger` clauses; SessionStart is the whole guard.
- Cover the connectionId arm of the exit certificate, where a provider-observed
  death and a preserved SSH surface are deliberately independent.

* fix(agent-status): keep agent-status-types under its line cap

main already sits exactly at the 300-line max-lines cap for this file, so the single
`acceptedStatusSeq` field this branch adds pushed it to 301 once main's observation
facet merged in.

Declared the field as a mixin beside the observation facet instead. Both are per-write
facets mixed into `AgentStatusEntry` rather than fields a reporter supplies, so they
belong together — and the capped file loses a line rather than gaining one, since it
already imports from that module. No lint suppression.

* fix(agent-status): collapse the entry facets into one intersection

The previous attempt still tripped max-lines: two mixins on one intersection wrap
across two lines under oxfmt, so removing the field line bought nothing.

Expose a single AgentStatusRowFacets that already includes the observation facet, so
the entry intersects one short name on one line. The payload keeps intersecting the
observation facet alone — it must not carry the renderer-local ordinal.

Verified by formatting first and then linting, which is the order that catches this.

* fix(agent-status): retire resume authority with dead panes
2026-08-24 11:15:11 -07:00
Brennan Benson 8d08d0078d fix(codex): stop an unreadable legacy hooks.json from clearing managed trust (#16147)
`cleanupLegacySystemManagedHooks` reads `~/.codex/hooks.json` and, when it finds
no hooks, removes Orca's managed trust entries from the system config.toml and
deletes that home's grant-ledger record.

Since the STA-4823 read classification landed, `readHooksJsonWithRaw` reports a
genuine absence as `{ raw: null, config: {} }` and a failed read as
`{ raw: null, config: null }`. Both still fell into the same branch, so a read
that merely failed discarded hook approvals the user had already given — and the
ledger record that would have let a later pass notice.

Only the definitive-absence answer may reach the removal now.

Found while reviewing #15417 and not covered by it: that PR fixed the classifier
and this is a consumer of it that still collapsed the two answers.

The sibling `assertHooksJsonGeneration` guard in codex-real-home-hook-install.ts
has the same `existsSync ? read : null` shape and is deliberately NOT changed
here. Measured, a permission denial leaves existsSync true and throws from the
read, so that path already fails closed; a guard there could not be made to fail
in a test and would be unprovable code.
2026-08-24 10:47:54 -07:00
Brennan Benson a70fe63993 fix(workspace-cleanup): read git for a pinned workspace before deleting it (#15370)
`shouldReadWorkspaceCleanupGitEvidence` refused to read git whenever `blockers`
included 'pinned' -- in a clause `forceGitCheck` could not override. So the
confirm-time forced read, which exists precisely to decide whether a removal
needs force, never ran for pinned rows.

This is reachable today, not only after the planned verdict removal. 'pinned' is
not a queue blocker (queue blockers are main-worktree, folder-repo, dismissed),
so a pinned idle workspace is hand-selectable from the row checkbox right now.
Its git evidence stays `clean: null, checkedAt: null`, and
`shouldForceWorkspaceCleanupRemoval` returns true whenever git is unknown -- so
it force-deletes with no evidence ever obtained.

Moved 'pinned' into the cost-skip clause so `forceGitCheck` overrides it:
broad scans still skip it, targeted preflights do not. main-worktree and
folder-repo stay unconditional because both are refused before removal, so
reading git for them is pure cost.

Broad-scan performance is unchanged, and there is now a test pinning that: it
passes before and after. The preflight test fails against the unfixed source.
2026-08-24 10:46:30 -07:00
JinjingandBrennan Benson 7b9529da22 Add keyboard shortcut for workspace deletion (#16271)
* Add keyboard shortcut for workspace deletion

Default Mod+Shift+Backspace (⌘⇧⌫ on Mac) lets users delete the hovered
worktree or folder workspace immediately. The shortcut targets the
sidebar hover state rather than requiring focus, and avoids terminal
pane D-based split shortcuts on all platforms.

Co-authored-by: Brennan Benson <brennankbenson@gmail.com>

* Omit delete shortcut from disabled Delete Worktree for primary checkout

- Remove shortcut badge from the disabled "Delete Worktree" action when it cannot be executed
- Only show shortcut in multi-context delete actions where the command is available
- Extract host identity parsing into reusable helper function to prevent inline string manipulation
- Fix folder workspace deletion to use correct host-qualified identity comparison

* Document host extraction safety for destructive worktree ops

Unqualified identities must stay undefined rather than defaulting to
'local'. Destructive operations depend on correct host identification.
Added tests and JSDoc to clarify this safety-critical behavior.

* fix test

---------

Co-authored-by: Brennan Benson <brennankbenson@gmail.com>
2026-08-24 10:12:38 -07:00
Jinwoo Hong c618ec7393 test(reliability): protect recent P0 regression invariants (#16163) 2026-08-24 09:38:46 -07:00
Neil afd76a4df9 fix(terminal): preserve synchronized frames on reveal (#16026) 2026-08-24 00:01:53 -07:00
Neil 95633a7883 Fix stale task-source flashes in new workspace input (#16145)
* fix(new-workspace): prevent stale GitHub URL selection

* fix(new-workspace): guard all task URL transitions

* test(e2e): make task URL frame proof runner-safe

* fix(new-workspace): guard Enter during task URL lookup
2026-08-23 22:18:39 -07:00
Jinwoo Hong 4ee41fede2 fix(automations): reveal full prompt from detail view (#16067) 2026-08-23 19:59:30 -07:00
Brennan Benson 55258f34ad test(agent-status): characterize title-derived agent identity before the resolver change (#16144)
* test(agent-status): characterize title-derived agent identity before the resolver change

getAgentLabel is an ordered first-match-wins scan of substring predicates over a display
title, so chain position rather than evidence strength decides identity. Pin the current
answers — including the wrong ones — so the resolver change lands as a reviewable diff of
assertions instead of silent behavior drift.

Eight of the nineteen assertions record defects. Five are minimized from real recorded pane
titles: four Grok panes that read as Codex and one that reads as Gemini CLI, in every case
because a foreign agent name in free-form task text is checked before the `- <agent>` owner
suffix that actually names the pane. The suite also pins the pairwise property behind them —
both orderings of a name pair resolve to the same agent, which is the tell that the title
carries no signal distinguishing them.

Also pinned as correct so the resolver does not regress them: hyphenated worktree names
(`review-14600-codex`) stay unclassified, and a Claude glyph still wins over foreign task text.

Verified non-vacuous: applying PR #15535's narrowing to isGeminiTerminalTitle flips exactly
four assertions, one of them a real corpus title, and the suite is green again on revert.

No production code changes.

* test(agent-status): re-pin the four assertions #15535 changed

#15535 landed the Antigravity narrowing, so four characterized answers moved. Re-pinned
against the new main rather than deleted, and the two that are now correct say why they are
correct — a targeted exception cleared the path, not a structural fix.

Added the general form as a new defect case: the same Grok pane without the word
"Antigravity" in its task text still reads as Gemini CLI, because only that one pair has an
exception. That is the case the resolver has to answer without a per-competitor clause.

* test(agent-status): clarify characterization precedence
2026-08-23 19:07:38 -07:00
Brennan Benson 4bc9e6b00c fix(workspace-cleanup): stop a wheel tick setting a filter, and land every filter patch (#15298)
A profile carried `activity.idleMinDays = 20` that the user never set, hiding 253
of 799 workspaces on open. Chromium mutates a *focused* number input on every wheel
tick, and before #14629 the facet panel could not scroll, so the natural response --
cursor into the panel, spin the wheel -- walked the threshold up and persisted it.

Two fixes:

- `FacetNumberField` renders `type="text" inputMode="numeric"`. A wheel cannot
  mutate a text input, and the parser already takes strings. `preventDefault` on a
  focused number input would also work, but it blocks the wheel's default action --
  which includes scrolling the nearest scrollable ancestor -- and would re-break the
  panel scrolling #14629 just fixed, in exactly the reported gesture. All five
  numeric facets share this one field.
- `patchFilters` closed over the render's `browse` snapshot, so two patches in one
  tick dropped one. It now writes through a functional update against the latest
  store state. `toggleSortField` and `clearFilters` had the same defect.

Both tests were confirmed to fail against the unfixed source before being kept.
2026-08-23 18:39:48 -07:00
Neil d9aa09cc8c fix(runtime): retire terminal snapshot probes before waiter timeout (#16149)
* refactor(renderer): use terminal paste sanitizer directly

* fix(runtime): order visible probe timeout settlement

* fix(runtime): skip unorderable visible probes
2026-08-23 18:38:52 -07:00
Brennan Benson 41ddcbde06 fix(workspace-cleanup): stop pre-selecting workspaces for deletion (#15152)
* fix(workspace-cleanup): stop pre-selecting workspaces for deletion

'Ready' is Orca's verdict about the user's own work. The dialog already
refuses to display the tier as a workspace fact, then pre-checked rows based
on it anyway — acting on the verdict more loudly than showing it would. Open
with an empty selection and let the user decide.

Removing auto-select unmasks two selection defects it was hiding, both fixed
here because they become reachable the moment select-all is the primary path:

- The header checkbox compared a canQueue-scoped selected count against a
  canSelect-scoped selectable count, so hand-picking review-tier rows flipped
  it to fully-checked and the next click cleared the entire selection.
- A filter change silently dropped selected rows; auto-select used to refill
  them instantly, so the loss was invisible. It is now reported.

Also drops the scan toast's 'N cleanup suggestions' clause, which published
the same verdict outside the dialog, and labels select-all with the count it
actually takes (the deletable subset, not every matched row).

* chore(i18n): sync the catalog for the cleanup selection strings

* fix(workspace-cleanup): preserve explicit selections
2026-08-23 18:35:27 -07:00
Brennan Benson 4c44116197 fix(workspace-cleanup): name the local context instead of totalling it (#15070)
* fix(workspace-cleanup): name the local context instead of totalling it

The delete confirmation showed 'Context: 2', a sum of five unrelated things
(terminal tabs, clean editor tabs, browser tabs, diff notes, finished agents)
rendered as plain text with no icon or tooltip. A reader cannot tell what the
number counts, which is the one thing that screen exists to tell them.

Reuse the breakdown the expanded row already renders ('Terminal tabs: 1,
Browser tabs: 1') so the confirmation names what deleting would discard. No new
strings: the per-kind labels already exist and were already translated.

* fix(workspace-cleanup): keep context labels legible
2026-08-23 18:35:05 -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
Jinwoo Hong 3d74f5fe03 test(remote): preserve HTML inventory RPC failures (STA-5210) (#16056) 2026-08-23 16:09:49 -07:00
Jinwoo Hong 6989c92b16 fix(skills): recover upload admission lifecycle (#16057) 2026-08-23 16:01:24 -07:00
Neil 0753f0a8dc fix(crash-reporting): stamp React #185 boundary attribution as unreliable (#16153) 2026-08-23 15:56:27 -07:00
Neil 0926c25854 refactor: reuse canonical regex escaping (#16150) 2026-08-23 15:45:53 -07:00
Neil a7a4f5d987 style(secrets): trim backend probe comments (#16136) 2026-08-23 15:42:37 -07:00
Brennan Benson ab3b1d07cd reland(opencode): session continuity without the command-finished deferral (STA-4557) (#15350)
* reland(opencode): session continuity without the command-finished deferral (STA-4557)

Relands #14866 (reverted in #14943) minus its `orca-runtime.ts` change, which
is what caused the revert.

## Why the original runtime change was wrong

`retirePtyAgentLaunchAuthorityAfterCommandFinished` deferred launch-authority
retirement behind an async foreground read, on the premise that OpenCode emits
`command-finished` while still in the foreground. Raw PTY capture disproves it:
OpenCode emits no OSC 133 of its own, and Orca's shell wrappers emit exactly one
`133;D` per pane — at OpenCode's exit — under both zsh and bash. The event being
deferred past only ever fires at exit, which is exactly when authority should be
retired. Both call sites stay on the synchronous `retirePtyAgentLaunchAuthority`.

## Why the deferral was unsafe

`confirmPtyAgentExit` uses the same async-foreground pattern four lines away, but
its early return means "don't record an exit" — conservative. The deferral copied
that shape into a site where the early return means "don't revoke a secret". Same
code, inverted consequence: every guard failed open, so a stale or racing read
silently kept a finished session's authority alive, and the pane's persisted
`launchTokenHash` was never scrubbed — so it rehydrated as `restored` authority
after an app restart.

## Why the deferral's guards could not have worked

`ORCA_AGENT_LAUNCH_TOKEN` lives in the PTY environment, so every process started
in that shell inherits it — both sessions in a reused pane post the same token. A
pane-lifetime bearer secret cannot be a session identity baseline, by
construction, and `incarnationId` tracks the PTY, not the agent. The only field
that separates sessions is the provider `sessionID`.

## What lands

- Status/session-boundary work from #14866: opencode emits `SessionStart` for
  root sessions (mimo-code does not), launch-token fencing, and `SessionStart`
  as an opencode turn boundary.
- The two `server.ts` fixes from #14941: re-fence a still-authorized pane on a
  tokened `SessionStart`, and restore mimo-code's explicit-prompt restart
  boundary (mimo emits no `SessionStart`, so opencode-only stranded its panes).
  #14941's re-poll hunk is dropped along with the code it patched.
- Five regression tests in `opencode-finished-session-authority.test.ts`. They
  pass here and all five go red if the deferral is re-added.

* chore: drop incidental reformatting of files unrelated to this PR
2026-08-23 15:24:25 -07:00
Neil b89b58bf77 fix(renderer): keep navigation responsive under terminal load (#16138) 2026-08-23 15:07:36 -07:00
Neil f0c718ecd5 refactor(renderer): use canonical attachment labels (#16135) 2026-08-23 14:46:22 -07:00
Neil 9814813b2e refactor(skills): centralize observation matching (#16134) 2026-08-23 14:45:57 -07:00
Brennan Benson da57e10dd9 fix(agent-status): stop an Antigravity pane reading as Gemini CLI (#15535)
Antigravity's models are named "Gemini <n.n> <Name>" — the real `agy models`
output is already parsed in commit-message-agent-spec.test.ts — so an agy pane's
own title carries a whole `gemini` token. getAgentLabel checks Gemini CLI before
Antigravity, first match wins, so the model name won and the pane read as Gemini
CLI. Measured on '⠋ agy · Gemini 3.7 Flash · high': geminiGlyphs false,
geminiToken true, agyToken true, label 'Gemini CLI'. Even
'Antigravity · Gemini 3.7 Flash' resolved to Gemini CLI.

This surfaced as the tab bar and the sidebar disagreeing about the same pane,
because the two reach different copies of the chain and apply different
precedence to its result.

Defer only the token path: if a title carries an agy/antigravity token, the
bare-`gemini` branch declines. The four Gemini OSC glyphs stay decisive, and agy
emits none of them. Same shape as the existing isPiAgentTitle veto directly
above, which exists because substring matching made paths like 'gemini-project'
masquerade as Gemini CLI.

Narrowing the token rather than reordering the chain, deliberately: a real
recorded pane title from local terminal history is
'STA-4011 Linux Antigravity Commit Messages - grok' — a Grok pane whose task
text contains the token Antigravity. It resolves correctly only because grok is
checked before antigravity, so hoisting the Antigravity branch would break it.
That title ships as a regression case.

Both copies of the chain are fixed; the sidebar reaches one and the tab the
other, so fixing one alone would only move the disagreement.
2026-08-23 14:40:42 -07:00
Neil 8af02d658c Preserve code editor selections across tab switches (#16132)
* Preserve editor selections across tab switches

* Defer editor selection caching to tab lifecycle
2026-08-23 14:25:03 -07:00
Jinjing 7a72f341f7 Split pty-connection.ts into focused modules under 400 lines (#15166)
* refactor: split pty-connection.ts under 400 lines

* rm design doc

* refactor(pty-connection): extract reattach payload handlers as factories

- Replace bindApplyReattachPayload with createReattachPayloadHandlers factory that returns handlers instead of mutating session directly, enabling better composability and testing
- Extract waitForUserInitiatedSshConnect as standalone function for reuse across deferred session attach flows
- Create ReattachPayloadSession type to document and isolate required session capabilities
- Add test coverage for overlapping reattach payload attempts
- Clean up comments to remove redundant prefixes (session.pane → pane, session.transport → transport)

* fix(pty-connection): correct sequencing and state bugs in spawn and reat

- Fix terminal tail slice to take prefix instead of suffix, preserving escape
  sequence markers needed by next scan
- Clear pending pane serializer when direct SSH retry PTY is unclaimed
- Initialize interrupt status baseline to undefined so first input advances
  sequence counter
- Bump reattach generation only after confirming current attempt owns the stream,
  preventing superseded results from canceling in-flight prepaint

* fix(pty-connection): correct sequencing and state bugs in spawn and reat

- Fix terminal tail slice to take prefix instead of suffix, preserving escape
  sequence markers needed by next scan
- Clear pending pane serializer when direct SSH retry PTY is unclaimed
- Initialize interrupt status baseline to undefined so first input advances
  sequence counter
- Bump reattach generation only after confirming current attempt owns the stream,
  preventing superseded results from canceling in-flight prepaint

* fix(test): increase poll iterations to prevent Node 26 test leakage

Increase event loop turns from 40 to 200 in the timer settlement loop.
Node 26's libuv poll phase can briefly starve when concurrent workers
transform tests, causing cleanup to leak into the next test. The higher
iteration count ensures async operations complete before returning.

* fix(foreground-output-budgets): use >= for budget window boundary check

At the exact window boundary, the budget should roll over. Change the
comparison from > to >= so the window resets when now equals
windowStart + FOREGROUND_BUDGET_WINDOW_MS, not just after. Add tests
to verify budget rejection and rollover behavior.

* refactor(pty-connection): add status observations and routing improvemen

- Track agent status observations with origin and transition metadata
- Separate interactive redraw input timing from general terminal input
- Restore pane authority on bind and reattach
- Refine routing trust and confirmation state handling
- Invoke queued startup callbacks when PTY is bound
- Resolve Windows shell overrides with user settings

* refactor: extract resolveLaunchAgentCandidate helper

Consolidate duplicated launch-agent resolution logic into a shared helper to prevent future divergence between paneExpectsLaunchAgent and resolveExpectedLaunchTuiAgent.

* refactor(pty-connection): use model snapshot for direct SSH reconnects

Direct SSH reconnects now restore from the full SSH model snapshot (complete scrollback) when dimensions are compatible, instead of the bounded relay tail. Falls back gracefully when incompatible or alternate-screen was exited.

* refactor(pty): retry unverifiable SSH reattaches via preserved bindings

Preserve deferred SSH session IDs longer when they serve as the only retry binding,
allowing the system to attempt recovery through direct SSH retries or PTY remounts
when reattach fails in an unverifiable way. Simplify reconnect model restoration
by removing the conditional model snapshot probe and using relay replay directly.

* test: poll terminal readiness in expectSingleOwningPty

Retry the terminal list assertion with polling to account for timing
delays in PTY state reporting from the runtime.
2026-08-23 13:55:20 -07:00
Brennan Benson 677718c4a5 fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823) (#15417)
* fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823)

Six shared files were rebuilt, erased or reported healthy after a read that had
only failed. Batch A of the STA-4606 split: every one of these is reachable and
testable on the host lane, so none of them wait on the WSL work.

- `config-toml-trust.ts` upsertHookTrustEntries: `existsSync` reported a locked
  config.toml as absent, so the base content became '' and the upsert rewrote
  the file from the trust entries alone — a trust-only stub, with the user's
  model, provider, MCP servers, approvals and comments gone. It refuses now;
  every hook-service caller already turns that into "trust entries could not be
  written. Run /hooks in Codex to approve."
- `codex-trust-grant-ledger.ts`: an unreadable ledger degraded to empty and the
  next write persisted a file holding only the home being written, dropping
  every other home's grants. The write paths refuse; the read path still
  degrades, and a corrupt ledger is still rebuilt.
- `codex-pane-account-registry.ts`: an unreadable registry erased every pane's
  attribution AND cached that erasure, so it survived the file recovering. The
  failure is no longer cached, and both write sites refuse rather than persist a
  registry derived from an empty stand-in.
- `hooks-json-read.ts`: the read arm already separated "no hooks" from "could
  not read", but the `existsSync` arm in front of it returned a valid empty
  config for a file that could not be opened. One read now classifies both.
- `config-settings-baseline.ts`: absent, unparseable and unreadable all collapsed
  into `null`, so the snapshot rebuilt a baseline it could not read — recording
  an in-Codex edit as Orca's own write, after which promotion skips it forever.
- `config-sync-stall.ts`: an unreadable runtime config read as absent and the
  status reported `synced` while the mirror was refusing. It reports
  `managed-home-unavailable`, the existing reason for exactly this, rather than
  borrowing a source-side one and blaming the wrong path.

Absent and malformed still rebuild throughout — resetting corrupt state is the
intent, and conflating it with unreadable would wedge a user on a broken file.

* fix(codex): close shared state read-denial gaps

* fix(codex): recover oversized settings baselines

* fix(codex): name the stalled managed config

* fix(codex): preserve hooks after failed source reads

* test(codex): correct what the denyExistence rig actually models

MEASURED on both platforms: a file-permission denial leaves existsSync TRUE
and fails only the content read — chmod 000 gives EACCES on macOS, icacls
/deny (R) gives EPERM errno -4048 on Windows, with stat/lstat succeeding in
both. The rig's docblock claimed this mode modelled that denial. It does not.

What it models is the UNC / \\wsl$ transport, where an unreachable distro
reports errno UNKNOWN at every level and existsSync folds it to false.

The distinction decides what the D29 guard is worth: under a permission denial
the pre-fix code already failed safe, because existsSync was true so it took
the read branch and threw. Only a transport that lies about existence reaches
the rebuild-from-empty path. No behaviour change; the comment was wrong, not
the code.

* test(codex): exercise live baseline read denial

* fix(codex): retry pane attribution writes

* fix(codex): retry reconciliation registry writes

* fix(codex): report unreadable sync baselines
2026-08-23 13:52:18 -07:00
Brennan Benson d1127b9939 fix(agent-hooks): revive a retired pane on each provider's own new-turn event (#15344)
* fix(agent-hooks): revive a retired pane on each provider's own new-turn event

The un-retirement gate matched two raw event-name literals, UserPromptSubmit and
SessionStart. Only 5 of 18 hook sources name their turn boundary that way, so for
the rest a reused pane stayed rowless forever: the user starts a new turn and no
row ever appears in the sidebar or dashboard.

Measured, not estimated — 10 providers fail the new test on main: gemini
(BeforeAgent), antigravity (PreInvocation), amp (agent.start), cursor
(beforeSubmitPrompt), pi/omp/prime-agent (before_agent_start), grok
(user_prompt_submit), copilot (sessionStart, seen raw), hermes (pre_llm_call).

The gate was also wrong in the other direction: opencode has no turn boundary at
all, yet a literal SessionStart revived its pane. Both directions are covered.

The correct per-provider classifier, isNewTurnEvent, was already imported into
this file and already used 160 lines below — #14706 added that call specifically
so consumers would stop re-deriving boundaries from literals, and left this one.

Keeps the literal check when the remote envelope omits source: that field is
optional, an older relay does not send it, and requiring it would have left every
source-less remote pane retired forever.

* fix(agent-hooks): tell an absent source apart from an unrecognized one

Review of the first commit found that `isAgentHookSource(envelope.source) ?
envelope.source : undefined` collapses two different wire conditions into one:
an older relay that omits `source`, and a NEWER host relaying a provider this
build does not know. They need opposite answers.

Case two is the normal upgrade order — hosts and clients update independently —
and its boundary event will not be UserPromptSubmit/SessionStart, because 13 of
the 18 providers we already ship are named something else. So the legacy-literal
fallback stranded that provider's panes permanently: the exact defect this branch
fixes, silently reintroduced for traffic our own wire doctrine calls normal.

Pass the raw wire value so the gate can distinguish them, and fail OPEN on an
unrecognized provider. The costs are asymmetric: a stranded pane is invisible,
permanent, and has no user-facing recovery, while a spurious revive produces a
row that decays after AGENT_STATUS_STALE_AFTER_MS.

Also from review:
- Cover the source-less branches, which had no coverage at all — including a test
  pinning what the legacy shim CANNOT do, so nobody later widens the literal list
  to "fix" it.
- Use the source as agentType rather than always 'claude'; 15 of 16 rows were
  describing a state that cannot occur.
- Reword the opencode comment: no plugin in that family emits these literals, so
  this closes a hole rather than removing behavior.

Checked and not changed: the local HTTP path 404s an unresolvable source, so the
fallback is unreachable there and masks nothing.

* test(agent-hooks): stop pinning opencode's fence behavior on a synthetic event

The negative case asserted that an opencode pane stays retired after a literal
SessionStart. That rests on a false premise: origin/main's opencode plugin never
emits SessionStart at all — zero occurrences in opencode/hook-service.ts, and the
shared family source is what mimo-code uses too. So the assertion pinned an event
no plugin sends, and it would have become actively wrong the moment a pending
change gives opencode a real SessionStart, silently re-breaking the rowless
reused-pane case that change exists to fix.

Assert against mimo-code and command-code instead. Both genuinely have no
boundary event in any planned state, so the case tests what it claims to.

* fix(agent-hooks): reject malformed relay sources at retired fence

Only a non-empty unknown string can identify a future provider. Keep null, blank, numeric, and object source values behind the retired-pane fence instead of treating malformed wire data as a new turn.
2026-08-23 12:54:13 -07:00
Isaque Coelhoandm4air 853afdf80e fix(status-bar): remove pet menu reserved space (#13067)
* fix(status-bar): remove pet menu reserved space

* test(status-bar): add pet segment layout validation tests

- Unit test guards against pr-[6.5rem] padding reintroduction
- E2E test measures trailing overhang instead of total width delta
  for more accurate layout validation
- Extract enableExperimentalPet helper for test clarity

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-23 12:21:15 -07:00
Jinjing e50cc309c3 fix(runtime): prevent restored workers from appearing idle while busy (#15998)
* fix(runtime): classify tui-idle from the visible screen only

The adopted-PTY tui-idle probe added in #15569 read the provider snapshot as
`scrollbackAnsi + data`, and the Codex readiness classifier matches the startup
banner. For a daemon-hosted adopted worker — where the retained tail stays empty
forever — every wait re-probed and could resolve `satisfied: true` off banner
history while Codex was actively working, turning a loud timeout into a silent
false ready.

- probe now requests and parses the visible grid, never scrollback
- retirement of a timed-out provider acquisition is checked before the
  re-acquire branch, so a wider row request can no longer resurrect a hung
  provider
- probe builds its result before clearing the poll interval, so a stale handle
  cannot leave the waiter with neither poll nor probe

Fixture follow-ups from the same review:
- resume legs pin the captured `launchConfig.agentCommand` to the fake instead
  of bare `codex`, which resolved the machine's real Codex off PATH
- the command override is quoted for the Windows shell the runtime will actually
  use, and specs pin that shell alongside the override
- fake agents acknowledge a bare submit after a short grace, so an unbracketed
  delivery path fails with a diagnosable ACK instead of a suite timeout

Refs STA-4907, STA-4885

* test: assert tui-idle probes serialize visible grid only

- Verify idle timeout probes exclude scrollback from serialization
- Add test case for Git Bash shell path quoting with apostrophes
- Simplify verbose test helper comments

* test: improve fake agent paste protocol validation

Refactor paste end detection to properly track both begin and end markers,
validate bracketed paste protocol (RFC 2544) through chronological event
sequencing, and emit correct error messages for protocol violations. This
ensures reliable detection of when pastes complete even when delivered
across multiple chunks, and correctly distinguishes between bracketed and
unbracketed paste modes.

* fix(runtime): reject provider snapshots when live output advances

Provider snapshots become stale when live output is received after the
snapshot is requested. Reject snapshots where the current output sequence
exceeds the snapshot sequence, preventing callers from consuming outdated
terminal state. Add tests verifying stale frame rejection.
2026-08-23 09:45:52 -07:00
Neil c3a1694b1d perf(preflight): read the WSL mount table once, and make launch agree with detection (#16053)
* perf(preflight): read the WSL mount table once per shell, not once per CLI

The prelude is embedded in the lookup script, and the caller wraps that in
`for cmd in <every agent>`, so the unconditional assignment forked awk once per
probed CLI -- 36 of them inside the distro against a 10s detection budget. The
comment claimed it was read once outside the loop; it was not.

`${x+set}` rather than `[ -n ... ]`: a host with no Windows mounts yields the
empty string, which must still count as read.

Pinned by counting real awk forks through /bin/sh with a stub on PATH, because
nothing covered this expression at all -- a wrong-field mutation shipped green.
Verified to bind: the unconditional form counts 4 for 4 commands.

* fix(wsl): make launch resolve the same binary detection reported

Agent detection skips Windows mounts during the PATH walk; the Codex WSL
command builder and the WSL branch of isCommandOnPath did not. So Orca could
report the guest codex as installed and then launch the Windows one sitting
ahead of it on PATH, or disagree with itself between preflight and detection
about the same distro.

Both now pass the same option.

Verified on a real Windows host against a real WSL2 distro, with a Windows
binary planted ahead of a guest one on PATH:

  plain `command -v orcaprobe` -> /mnt/c/Users/neil/orca-agree/orcaprobe
  this lookup                  -> /home/neil/.orca-agree/bin/orcaprobe

That host reports /mnt/c as 9p, which the mount expression matches, so the
fstype list is confirmed against hardware rather than fixtures.

* test(preflight): prove the memoised mount list applies past the first command

Counting awk forks with a stub that reports no mounts cannot see what the
hoist trades correctness for. A mutant that empties `_orca_win_mounts` inside
the walk keeps the fork count at 1 and keeps every existing test green, while
every agent after the first stops skipping /mnt.

This runs two commands behind a stubbed Windows mount and asserts both resolve
to the guest binary. Verified against that exact mutant.

Credit: review counsel.
2026-08-23 02:58:40 -07:00
NeilandMelih b2902cb61e fix(agent-resume): restore Kimi Code sessions after restart (#15883)
Co-authored-by: Melih <mberatsanli@gmail.com>
2026-08-23 02:26:52 -07:00
OrcaWinandOrcaWin 4c984d4c1b Fix Windows Git Bash console-capacity failures (#16045)
* fix(terminal): retain failed local console panes

* fix(terminal): preserve failed pane restart context

* fix(terminal): scope capacity recovery to PTY binding

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-23 01:24:37 -07:00
Neil e26f849682 fix(wsl): budget the whole command line, not just the script (#16032)
* fix(wsl): budget the whole command line, not just the script

The argv/stdin threshold measured `script.length`, but the cap applies to the
finished command line -- which also carries `PATH=<login PATH>` and `HOME=`.
A login PATH is itself a few KB.

That produced a perverse band: with a long enough PATH, a 7,999-char hook was
placed on argv and CreateProcess refused it, while the SAME hook at 8,001 chars
flipped to stdin and ran. Size decided how a hook behaved, in the wrong
direction, and the failure looked like "your setup hook failed" with nothing
pointing at length.

Now the argv form is built, measured, and only used if the whole line fits;
otherwise the script goes to stdin as before. The count over-estimates slightly
(it charges quoting for every argument) because over-counting is the safe
direction for a cap.

The regression test uses a 7,000-char script -- deliberately under any
script-only threshold -- with a 27KB PATH, and asserts it lands on stdin. My
first attempt used 7,999 + `echo `, which is 8,004 and flipped under the old
rule too, so it passed either way and proved nothing.

Credit: Grok.

* fix(wsl): charge quoting and measure the line that is actually spawned

Two under-counts the review found in the estimator I added.

The doc comment claimed it over-counts. It did not: libuv escapes every `"`
and doubles a backslash run before a quote, so a quote-dense script costs more
than its length. And `wsl.exe` plus `-d <distro> --exec` are prepended AFTER
the measurement, so ~45 characters of the budget were never counted.

Together those put a quote-heavy ~26KB script on argv and over the real 32767
ceiling -- where the old script-only rule would have sent it to stdin and it
would have run. A narrower band than the one this PR removes, but the same
shape of bug, so worth closing before merge rather than after.

Now charges one character per `"` or backslash and measures the full spawn
line. New test: 26,000 quote characters must land on stdin; verified to fail
with the quoting charge removed.
2026-08-23 00:57:34 -07:00
Neil 92315c4178 fix(preflight): do not count a Windows binary reached through interop as a WSL install (#16028)
* fix(preflight): do not count a Windows binary reached through interop as a WSL install

WSL appends the Windows PATH to the guest PATH by default, so on a distro with
no guest `claude`, `command -v claude` resolves to
`/mnt/c/Users/me/.../claude.exe`. That path is POSIX-absolute, so the existing
absolute-path check accepted it and preflight reported the agent as installed
in the distro.

That is worse than reporting it absent. Absent tells the user to install it; a
false positive launches a Windows executable inside a Linux session, where it
sees Windows paths, no guest $HOME and none of the distro's config -- and the
failure surfaces later, somewhere less obvious.

Rejects `/mnt/<drive>/` and any `.exe`, case-insensitively. A genuine guest
install is unaffected.

* fix(preflight): skip Windows mounts during the PATH walk, not after it

The review caught this and it is the more important half of the fix.

Rejecting the interop path in TypeScript happens after the guest walk has
already stopped on it: the lookup breaks at the first executable, and the
version-manager fallback dirs are APPENDED, so they sit behind the Windows
entries WSL appends. A user with claude in nvm AND on the Windows PATH
therefore went from a false positive to "not installed" -- the exact #9725
population the fallback dirs exist to serve. Worse than the bug being fixed.

The lookup now takes `skipWindowsMountDirs` and skips those PATH components
mid-walk, so the guest binary behind the shadow is still found. Matched by
mount metadata from /proc/mounts (drvfs/9p/virtiofs), not by a `/mnt` name:
the automount root is configurable, and `/mnt` is an ordinary directory on a
Linux box. That also closes the custom-root hole the reviewers found in the
name-based predicate.

The TypeScript check stays as a secondary net for a mount the guest does not
report, with a comment saying why it must never be the thing that decides.

Proven with a real /bin/sh: a Windows `claude` ahead of an nvm `claude` on
PATH now resolves to the nvm one.

Credit: review counsel, and community PR #12794 (spfcraze), which proposed
this shape first.

* fix(preflight): let the mount table be the only word on what is a Windows path

The name-based check could veto a path the walk had deliberately kept. /mnt/d
is a perfectly ordinary Linux mount, so a guest binary there was resolved
correctly by the walk and then discarded by its name -- the #9725 false
negative, reintroduced by the belt-and-braces net I added "just in case". And
if awk were missing, the name rule became the only rule, which is precisely
the failure it was supposed to backstop.

The walk skips components the guest itself reports as drvfs/9p/virtiofs. That
is authoritative. Without a mount table we now degrade to main's behaviour (the
old false positive) rather than inventing a new false negative.

Net: one predicate, three fixtures and an import deleted.
2026-08-23 00:57:05 -07:00
Neil 0e9f02baf3 fix(settings): an empty agent detection must not erase the saved default (#16043)
Three bugs in one screen, reported in #15256 with a diff of the user's
orca-data.json showing defaultTuiAgent going from "claude" to null.

1. The Auto pill's handler writes null, and it was rendered as the ACTIVE
   choice whenever the stored agent was merely not detected right now. So the
   pill that already looked selected was destructive: one click erased the
   setting, and a later successful detection did not bring it back. Auto is now
   active only when null is actually stored. Detection is a transient fact; the
   stored value is not, and this control reports the stored value.

2. With zero agents detected there were no agent pills at all, so the stored
   choice was both invisible and unrecoverable -- nothing to click to put it
   back. The stored agent is now always offered, labelled as saved but not
   currently detected.

3. Refresh lived inside the Installed section, which only renders when at least
   one agent was found, so the only retry control vanished in exactly the state
   that needs it. An empty result now renders its own Refresh.

This matters more now than when it was filed: #16028 makes WSL detection
legitimately return an empty set on machines where the only agent was a Windows
binary reached through interop, so the empty path is about to get more traffic.

Each of the three tests was verified to fail with its own fix reverted.
2026-08-23 00:56:36 -07:00
Neil 7a739c6bf5 fix(secrets): probe the Linux-only storage backend defensively (#16046)
`safeStorage.getSelectedStorageBackend` is `@platform linux`, so it is genuinely
undefined on macOS and Windows — confirmed against the installed Electron 43,
where it reads `undefined` on darwin and `function` on Linux. The shipped code
called it behind a `process.platform === 'linux'` check, so it never threw, but
the guard was the only thing standing between that call and a startup TypeError.

The platform check now lives with the probe, alongside a typeof check and a
try/catch, and an unreadable or unknown backend reports no gap — claiming one we
cannot prove would be its own kind of lie.

The gap this closes is in the tests, not just the code: every suite here mocks
safeStorage with the method present, so the suite could stay green while the
shipped app threw. The new case deletes the member from the live mock rather than
re-mocking, because the module already holds that object and a later vi.doMock is
inert — the first version of this test passed against the unguarded code, which
is the failure mode it exists to catch. Verified against the expression currently
on main: three cases fail.
2026-08-23 00:38:28 -07:00
Neil 1375c57b16 fix(secrets): report the protection gap on change, not on every launch (#16044)
The gap warning fired every startup with no way to stop it. It usually needs a
keyring installed and unlocked to fix, so repeating it every launch is nagging
the user cannot act on and will learn to ignore.

It now reports when the answer changes: once when the gap starts being true,
again if it becomes true for a different reason, and once when it is fixed —
because silence after a "your secrets are not protected" warning would leave the
user assuming that is still the case.

State lives beside the profile data file, which is why the call moved out of the
port bootstrap: that state has nowhere to live until the profile exists. A
corrupt state file re-reports rather than trusting it, and a failed write logs
instead of failing startup, since re-reporting next launch is the safe direction.

ORCA_ALWAYS_REPORT_SECRET_PROTECTION=1 forces a re-report for support without
disturbing the stored state.
2026-08-22 23:56:01 -07:00
Neilandhwantage 202d74a8a4 fix(git): enable Windows long paths for worktree creation (local, sparse, and SSH hosts) (#15866)
Co-authored-by: hwantage <hwantagexsw2@gmail.com>
2026-08-22 23:00:31 -07:00
Neilandvam af2e825626 fix(worktree): stop warning about a stale local base branch that does not exist yet (#15331) (#15871)
Co-authored-by: vam <a@a.com>
2026-08-22 23:00:27 -07:00
Neilandkriptoburak 8d1557c225 fix(skills): discover Hermes home skills (#15862)
Co-authored-by: kriptoburak <kriptoburak@users.noreply.github.com>
2026-08-22 23:00:23 -07:00
Brennan Benson 9ea2636b38 fix(remote): keep editor focus after unfocused HTML side preview (STA-5001) (#15716)
Opening a remote HTML preview to the side created an empty split and made it
the active group. The next host session-tab snapshot still had the terminal
active, so the client treated that empty group as a terminal focus change.

Do not activate the empty split for unfocused remote previews, and when a
reserved preview group is still empty, keep the sibling editor as the
visible tab instead of following the host terminal.
2026-08-22 22:37:40 -07:00
Neil 0cb076f03c fix(dictation): address visualizer review feedback (#16035) 2026-08-22 22:34:38 -07:00
Neil 838f5bfb75 fix(secrets): tell Linux users when their secrets are only obfuscated (#16033)
On Linux with no keyring, Electron falls back to the `basic_text` backend, which
"encrypts" with a hardcoded password. `isEncryptionAvailable()` returns true for
it, so Orca reported those secrets as sealed. They are not.

The obvious fix — returning false for basic_text — is wrong and would have been a
credential regression: `decryptWithStatus()` skips decryption entirely when
encryption is unavailable, so every already-stored secret would read back empty.
Sealing genuinely works on basic_text and must keep working.

So capability and trust are now separate questions. `isEncryptionAvailable()`
still answers "can this host seal and unseal", and `describeProtectionGap()`
(renamed from `describeUnavailable`) answers "is my data actually protected",
covering both no-sealing and weak-sealing.

That method had no production caller — the port documented a promise nothing
kept. `reportSecretProtectionGap()` now reads it at startup. A user-visible
surface is follow-up; this at least stops the silence.

Adds a bootstrap wiring guard over all nine host port installs. The no-op
defaults are correct for a renderer-less host and silently wrong for the desktop,
and a dropped or reordered install fails no existing test. Verified in both
directions: it fails when an install is removed, and when one moves after the
runtime is constructed.
2026-08-22 22:30:11 -07:00
erish b851a5e13f fix(github): add GHES avatar fallback to PR and task views (#13981)
* fix(pr-page): route remaining user avatars through GitHubUserAvatar

On a private-mode GitHub Enterprise instance the stored avatar URL 302s to
/login, and the renderer's default Electron session carries no cookie, so the
image never loads. #8784 added GitHubUserAvatar for exactly this — it degrades
to an initials placeholder via onError — but three call sites in
PullRequestPage kept a bare <img>: the reviewer picker, the comment author,
and the @ mention suggestions.

Each only guarded on avatarUrl being absent, so on GHE the URL is present, the
placeholder branch never runs, and a broken image is left on screen. The
authorAvatarUrl type comment already documents the intended contract ("falls
back to the login URL and finally an initials placeholder").

GitHubUserAvatar was already imported in this file for the PR author, so this
makes all five avatars in the page consistent. Note the three switched slots
now carry the shared border/bg styling, matching the two that already did.

Add a boundary test that fails if any avatar is rendered through a bare <img>
again.

Fixes #13976

* fix(task-page): route GitHub avatar cells through GitHubUserAvatar too

Auditing the rest of the GHE avatar path turned up the same bare <img> in
TaskPage: GitHubAssigneeAvatar, GHAssigneesCell and PRReviewCell. Fixing only
the PR page would leave half of #13976 in place.

GitHubAssigneeAvatar is the clearest case — ReviewChipAvatar directly above it
already renders through GitHubUserAvatar, so two adjacent functions disagreed
on how a GitHub user avatar is drawn. Its border also moves from
border-border/40 to /50, matching the neighbour.

Linear member avatars in this file are left alone; they use their own provider
path and are out of scope here.

Move the regression assertions into the existing repro-8784 file rather than a
new boundary test — that file already guards PullRequestPage and TaskPage
together, so it is where this belongs. The PR-page check matches the <img>
pattern instead of specific field names, so a rename or a newly added avatar
slot cannot slip past it; the TaskPage check is scoped per function to avoid
catching the Linear cells.

* test(github): scope the avatar guard per call site and cover TaskPage names

Addresses review feedback on the regression guard.

The field-name regex missed aliases and resolver expressions, and the PR-page
assertion did not require GitHubUserAvatar in each migrated slot — deleting all
three would have passed. Reject any bare <img> within the component scope
instead, which is safe now that every assertion is scoped to one function.

Drive all six slots from one table so each gets its own named case, and extend
the display-name contract to TaskPage, which previously went unchecked. The
ConversationTab entry carries displayName: null because PRComment has no
display-name field.

Reverting the fix now fails 11 cases instead of 3.
2026-08-22 22:11:55 -07:00
Jinjing 36dfcc3955 fix(terminal): arm the restore baseline only on a snapshot that painted content (#16009)
The restored-snapshot baseline permanently drops every delivery chunk at or
below the snapshot's seq, on the model's claim that those bytes are already
painted. reconcileChunkAgainstRestoredSnapshot recovers when the baseline
under-reports (a gap ahead re-restores; a rawLength mismatch re-restores) but
has no path for a baseline that over-reports: those chunks return
drop-duplicate forever, and an idle shell never re-sends them.

Gate arming on whether the snapshot painted printable cells. A snapshot
claiming seq > 0 while painting nothing cannot be the rendering of the output
it claims to cover, so the claim is disproven and the redelivery is the only
remaining copy.

Also pins disposeHeadlessTerminal's two-part write ordering, which was
previously unpinned and silently reversible.

Refs STA-5179
2026-08-22 22:01:45 -07:00
Neil 03fcfdfb92 feat(orcad): boot the Orca runtime on plain Node (#15968)
* refactor(host): resolve the app root through the port in fork-reachable modules

`parcel-watcher-entry-path.ts` and `session-scanner-service-entry-path.ts` read the
app root via `require('electron').app` inside a try/catch that already returns null
when Electron is absent. They were therefore correct under plain Node at runtime and
only failed the *static* text check — which is real, not pedantic: the comment in
`ports/port-scan-command-client.ts:19` records that the plain-node-entry-guard fails
on that literal text, try/catch or not.

`hasAppEnvironment() ? getAppEnvironment() : null` gives the identical "no app root
here" answer without the text. That restores `hasAppEnvironment`, which an earlier
commit in this stack deleted as unused — it now has the caller it was waiting for.

Ratchet baseline 27 → 25.

Verified: 74 files / 458 tests; `pnpm typecheck` clean; `oxlint` clean.

* feat(orcad): boot the Orca runtime on plain Node

Closes the last two Electron couplings and makes `orcad` a working artifact:
a 4.43 MB Node bundle that boots, pairs, registers a repo, creates a real git
worktree and round-trips a PTY — with zero `require("electron")`.

Ratchet 2 -> 0, so `config/runtime-electron-baseline.txt` is now empty and its
test asserts exactly that: any reachable electron import is a regression.

- speech: inject the service factories, so importing ModelManager for its type
  no longer drags Electron's streaming net.request into the graph
- filesystem-watcher: add a WorktreeWatcherRemoval port. Every entry in those
  maps arrives through an ipcMain handler carrying a renderer sender, so a host
  with no renderer has nothing to close, restore or forget — the inert default
  is what the desktop code does against empty maps, not a stub hiding work
- user-data-path / profile-storage-paths: resolve userData through
  AppEnvironment. These surfaced only once orcad pulled the store in

Both host ports now anchor to a realm-global symbol. `vi.resetModules()` gives
the re-imported graph a fresh module copy, so a binding installed before the
reset silently read back as uninstalled.

The acceptance smoke drives both hosts through one code path (`--target
orcad|electron`) and seeds its own git repo, so it is hermetic and asserts the
same contract of each. Wired into PR CI.

* test(smoke): remove the seeded workspace container, not just the worktree

* test(smoke): surface the server's stderr when it dies before ready

* fix(smoke): build node-pty for Node before booting orcad in CI

* fix(smoke): drive the CLI built from this checkout, not one on PATH

* docs(ratchet): say the baseline must stay empty, not merely shrink

* build(orcad): externalize only the native modules actually in the graph
2026-08-22 21:47:46 -07:00
Neil dee48498b2 feat(dictation): add sound-reactive grape visualizer (#16017)
* feat(dictation): add sound-reactive grape visualizer

* perf: scope dictation meter updates
2026-08-22 21:40:47 -07:00