* feat(workspace): reland set project location from the create-worktree host picker
Relands #14868 (reverted by #14912) with a fix for the regression that caused the
revert: setting a project location could change the path before Orca used it.
The retarget-after-setup path read the raw store record to find a just-created
setup, because the memoized picker options had not refreshed yet:
useAppStore.getState().projectHostSetups.find(
(candidate) => candidate.id === setupId && candidate.setupState === 'ready'
)
That hand-rolls a second selection path that skips every rule the option builder
applies — repo eligibility, ephemeral-VM and runtime-owned SSH host exclusion,
and the one-setup-per-host dedupe whose own comment notes that
resolveWorkspaceCreationTarget takes the first project+host match and ignores the
rest. So the composer could be retargeted at a setup other than the canonical one
for that host, pointing creation at a different location than the one chosen.
Resolves through buildProjectHostSetupOptions against fresh store state instead,
so the fallback and the steady-state picker agree by construction.
STA-4547
* fix(workspace): sanitize the clone prefill and drop an abandoned set-location
Review follow-ups on this PR.
The "Clone from URL" prefill seeded the field with the verbatim `git remote` URL,
which can embed a PAT (`https://x-access-token:ghp_...@github.com/...`). The
clone then runs on the *target* host, writing that token into its .git/config —
a credential the user never typed into this flow, now readable by anyone on a
shared host. Strip it with the same sanitizer `getProvisionedRootRecipeRepoUrl`
already applies to the ephemeral-VM recipe URL. Extracted to
resolveProjectCloneUrlPrefill so the rule is directly testable.
The dialog also stays dismissable while a submit is in flight, and an SSH clone
is unbounded. A clone the user backed out of minutes earlier still called
onReady, silently moving the run target and resetting start-from under a form
they had since pointed at another host. Drop the result if the dialog went away.
* fix(workspace): re-arm the abandoned guard on mount
StrictMode runs mount/cleanup/mount, so latching `abandoned` on the first
cleanup left it true for the rest of the session and permanently suppressed
onReady — the app wraps its root in StrictMode. Reset it on mount.
Async cwd validation dedupes by exact path but had no concurrency bound, and a
dead UNC share answers `stat` in ~21s while holding one of libuv's 4 default fs
threads. Four distinct paths on one unreachable server therefore starved every
other async fs read in the daemon — including the cold-restore history replay
running alongside them — which moves the head-of-line stall #14848 removed from
the event loop into the thread pool.
Adds a per-route lane of 2, reusing PrioritySemaphore and matching the per-distro
lane in rate-limits/auth-filesystem-operation.ts. Keyed by the host that has to
answer (WSL distro, or the `\\server` prefix) so many dead subdirectories of one
share fold into one lane. Local-disk paths bypass the lane entirely: a global cap
would queue a healthy local spawn behind a dead share.
Moves PrioritySemaphore to src/shared. It has no imports, and reaching into
src/main/daemon from src/main/providers inverted the dependency direction that
already runs daemon -> providers.
Note this bounds pool occupancy, which cancellation cannot: an aborted `stat`
still holds its libuv thread until the OS returns.
STA-4543
* refactor: split GitHubItemDialog.tsx under 400 lines
No intentional behavior change.
* refactor: group github-item-dialog into lifecycle folders
Reorganize the 50 flat files under src/renderer/src/components/
github-item-dialog/ into six lifecycle folders:
load-item-details/ shared types, both caches, fetch/settle, state badge
open-dialog/ dialog shell, headers, body, tabs, link copy
discuss-item/ conversation tab, comments, composer, timeline
edit-item-fields/ GH edit section, labels, assignees, status
inspect-pull-request/ combined diff viewer, checks tab
land-pull-request/ PR actions, merge menu, reviewers
No intentional behavior change. All 50 files moved verbatim; the only
edits are relative-import specifiers (sibling paths plus a depth bump
for ../../../../shared) and the hardcoded module paths in the two
source-boundary tests.
Import graph stays acyclic: zero mutual folder pairs, no file importing
4+ sibling folders, no dest file importing the public barrel, and no
per-folder index barrels.
* refactor: split item references and improve diff-viewer remount logic
- Break down full `GitHubWorkItem` props into discrete `itemId`, `itemNumber`, and
`itemRepoId` in mutation and action functions to prevent over-memoization of callbacks
and improve dependency clarity.
- Extract `getPRFilesCombinedDiffSignature()` and use it as a component key to safely
remount the diff viewer when the PR revision changes, replacing generationRef tracking.
- Add `getKeyedCheckAnnotations()` and `getKeyedCheckJobs()` to generate stable,
collision-resistant keys for check arrays that may contain duplicates.
- Consolidate interpreter timeouts into a single `SPAWNED_INTERPRETER_TIMEOUT_MS` constant
and apply it via describe options rather than per-test values.
* refactor: improve github-item-dialog repo context and i18n coverage
- Add repoId prop to ConversationTab for explicit repo context override
- Internationalize UI strings in diff viewer and PR action components
- Improve error handling with cache rollback and guard cleanup on sync failure
- Enhance cache key validation for cross-window invalidation by repoPath
- Add repository access validation before rendering diff viewer
- Fix cross-platform issues: skip symlink test on Windows, normalize CRLF in test assertions
* Refactor check button i18n key and update text
- Replace hash-based key with semantic name for maintainability
- Change button label to "Open in browser" for broader context
A dying main process never delivers process-gone, so a crash loop never reached
the only prune call site, and Crashpad's own pass runs in the handler child
after a delayed first sweep. Prune on startup instead of behind the coalescing
timer the loop outruns, and cap dump count alongside the byte budget.
Signature parsing stayed on the main event loop after a crash. Reject on ptype
before the whole-buffer scan, and bound the backward prefix search that could
otherwise walk the entire dump only to discard the result past 96 bytes.
Also keeps dumps already claimed by a persisted report from being pruned out
from under the report's minidumpPath.
STA-4544
* fix(daemon): bound cwd validation and attach-only requests on a dead share
#14848 made cwd validation async so one dead share could not freeze every
terminal, but left two ways for a single unreachable path to strand work.
Canceling a create did not abort the probe: `isCanceled` is only polled between
spawn steps, so `fs.stat` on a dead SMB/NFS/UNC path ran to completion. The
create stayed in flight, later creates for that session queued behind it, and
shutdown/idle waited on it. Carry an AbortSignal next to the existing poll so a
canceled caller abandons the probe, and make the per-session queue wait abortable
so a canceled queued request stops waiting too. The shared probe is left running
for whoever still wants it, and the dedupe entry is evicted on a hard cap so a
never-settling mount cannot poison that path across sessions forever.
Attach-only requests registered no cancellable preparation, so the daemon could
not match the client's cancel and the client had already cleared its timer —
neither side bounded the request. Register the preparation for every
createOrAttach and run spawn preflight only when it is not attach-only, and give
the client a bounded grace window when the daemon reports an unmatched cancel.
The cancellation reason now wins over a racing daemon rejection: callers key
recovery off `client_disconnected`, and letting the race pick would roll back
terminals it should keep (#7718).
Splits working-directory validation out of local-pty-utils to stay under the
line cap.
STA-4541
* fix(daemon): keep a hung cwd probe to one thread and one cancel identity
Review follow-ups on this PR.
The 30s dedupe eviction retired an entry whose probe was still running. `fs.stat`
is uninterruptible, so that freed no libuv thread — it only let the next caller
pin another. A few retries against one dead mount exhaust the default pool of 4
and stall every other async fs read in the daemon, which is the cross-session
freeze #14848 set out to remove. Drop the timer and clear on settle only: the
AbortSignal added here already lets callers escape a shared hung probe, so
sharing one no longer strands them, and a mount whose stat eventually returns
still re-probes on the next call.
The abort also introduced a second cancellation identity on the wire.
WorkingDirectoryValidationAbortedError propagated out of createPtySubprocess as
the request's error, and the client's mapping only recognizes the attach-cancel
message — so a canceled create could reach the rollback branch that closes a
terminal the user still has (#7718). Translate it at the daemon boundary so the
wire carries one identity.
Backspacing over the last radical of a Cangjie composition empties the IME's
marked text without reaching compositionend, and the vendored xterm
CompositionHelper only dropped the overlay's `active` class there. The box
stayed painted with whatever glyph it last held (#11951).
Clear on the state rather than on the key, as native terminals do: an empty
`compositionupdate` now hides the overlay instead of only ever showing it, and
a key the IME swallows re-derives the preedit from the textarea once it settles
so a composition emptied with no composition event at all is cancelled too.
* fix(crash-reporting): decode POSIX wait statuses for display and record Windows session-end reasons
Chromium on POSIX hands render/child-process-gone the raw waitpid() status, so
crash reports read "Exit code: 61696" where exit status 241 is meant (field:
61696=exit 241, 9=SIGKILL, 133=SIGTRAP+core, darwin crashed 5=SIGTRAP). Decode
at the display layer only: the stored exitCode stays raw, Windows codes and
launch-failed launch-error codes render unchanged, and the process-gone span
gains a crash.exit_code_decoded attribute.
Also durably record a system_session_end breadcrumb (with WindowSessionEndEvent
reasons) when Windows session-end fires, so bundles can tell OS shutdown from a
user task-kill in killed/exit-1 sweeps.
* test(crash-reporting): pin the exit(0) no-suffix rendering
Adversarial mutation review: removing the exit-0 suppression in
formatCrashReportExitCode survived the suite — nothing pinned that a clean
exit(0) renders without an '(exit status 0)' suffix.
* test(crash-reporting): decode-attribute tests use synchronous child kills
Renderer killed events gain a 250ms sibling-kill settle once the
correlation branch lands, which (a) defers the span past the test's
platform stub so the decode gate reads the real host platform, and (b)
adds a deferred span that breaks the exact sink assertion. The decode
gate is source-agnostic, and a non-recoverable child kill persists
synchronously on every branch of the stack, so coverage is unchanged and
the platform stub is deterministic on any CI host.
* fix(crash-reporting): keep session-end reasons type-safe
* fix(crash-reporting): keep a pre-gone process-metrics sample so the crashed process's working set survives its crash report
* feat(crash-reporting): renderer peak/private bytes and gone-time system memory in crash details
* fix(crash-reporting): macOS system-memory fields and an era-invariant pin for peak/private metrics
* test(crash-reporting): kill five mutation survivors in the pre-gone sampler coverage
Adversarial review found these mutations survived the suite:
- dropping the immediate sample at startPreGoneProcessMetricsSampling()
- removing the double-start idempotence guard
- widening renderer peak/private aggregation to all buckets
- a failed sweep erasing the previous good sample
- the recorder hardcoding 'renderer' instead of event.processType
Each now has a binding assertion; also documents that the
crashed-process-absent flag is bucket-level only.
* fix(crash-reporting): prove crasher absence by vanished pid, not bucket count alone
The absent flag was bucket-level, so any surviving same-type process (a
webview guest, the dashboard popout, another utility) silently cleared it
— and webviewTag guests make multi-renderer sessions the norm. The pre-gone
sample now keeps per-process pid/bucket/workingSet identities; a sampled
same-bucket pid missing from the live set proves absence and reports the
vanished process's own working set (processMetricsVanished*), so the
crasher's size is no longer summed with surviving guests.
Also: split gone-time system memory into its own module (max-lines), pin
peak/private aggregation as a true max, clamp garbage negative working
sets and backwards clocks, pin live-metrics precedence over incoming
detail keys, and verify the sampler timer is unref'd by behavior.
* fix(crash-reporting): bucket-aware vanished-pid check with consume-once attribution
Loop-3 hardening of the vanished-pid logic:
- Live pids now carry their bucket: a recycled pid living on as a different
process type still reads as a vanished sampled process (the bare pid set
misread the crasher as alive).
- Vanished pids are attributed once. In a crash loop with no sweep between
deaths, record #2 confidently inherited the FIRST crasher's pid and
working set (dedupe window is only 2s, so both records ship); it now
degrades to the honest bucket-count arm instead.
- An ambiguous multi-process VanishedWorkingSetMB sum is bounded by
VanishedLargestWorkingSetMB so no single-process reading of the sum
survives triage.
- Killer tests for the remaining mutation survivors: unreadable gone-time
metrics prove nothing (flag/vanished stay off), pid-less sampled metrics
never vanish, fractional-MB rounding, negative system-memory clamp, and
full per-family precedence over colliding incoming detail keys.
* fix(crash-reporting): flag consumed and blind-era vanished attribution instead of going silent
Loop-4 hardening of the consume-once attribution:
- processMetricsVanishedAlreadyReportedCount: a record whose vanished pids
were consumed by a prior report now says so, instead of being
indistinguishable from "nothing vanished" while its PreGone mirrors still
show the prior crasher's era.
- processMetricsVanishedAmbiguousWithEarlierCrash: consume-once only consumed
when the gone-time read succeeded; a crash recorded blind (getAppMetrics
threw) left its pid unconsumed, so the next record in the same era
confidently emitted THAT crash's pid and working set as its own. Blind
buckets now taint the era until a fresh sweep.
- Pin two behaviors that were correct but unpinned: a failed sweep must not
clear attribution state, and an ambiguous vanished pair is consumed too.
- Document gone-time system memory reading healthier than at kill time, and
the bound on the attribution set.
- Split the suppressed-breadcrumb builder into its own module (max-lines).
* fix(crash-reporting): extend vanished ambiguity to consumed eras and pin two unpinned behaviors
Loop-5 findings:
- processMetricsVanishedAmbiguousWithEarlierCrash fired only for the
blind era; a partially-consumed era has the same shape (an earlier
crash's unsampled respawn is as plausible a crasher as the newly
vanished pid), yet emitted a confident VanishedPid with no flag.
- Pin the > largest tie-break (first-enumerated wins) instead of
re-accepting it as an equivalent mutant every loop.
- The suppressed-breadcrumb type field had zero coverage after the
module split — removing the whole block passed the suite.
* refactor(crash-reporting): cut per-pid vanished attribution, keep the stateless absence proof
Five review loops found defects in the same subsystem: the per-pid
vanished attribution outputs (consume-once set, blind-era taint,
consumed-era ambiguity). The absence proof they fed does not need any
of it — a sampled same-bucket pid missing from the live enumeration
(including cross-bucket pid recycle) is stateless and idempotent, so
it stays true for every record of a crash loop with zero module-level
attribution state.
Dropped: processMetricsVanished{Count,WorkingSetMB,Pid,
LargestWorkingSetMB,AlreadyReportedCount,AmbiguousWithEarlierCrash},
attributedVanishedPids, metricsBlindCrashBuckets, and the tests that
existed only to defend them.
Kept and still pinned: the 60s pre-gone sampler and its lifecycle,
PreGone* mirrors + SampleAgeMs, renderer peak/private, gone-time
system memory, the recorder processType binding, the live/PreGone era
invariant, and the browser-pane case (webview guests keep the renderer
bucket alive) that motivated the PR — now asserted via the absence
flag plus PreGone mirrors alone. Documented the two honest limits:
PreGone values are sample-time (up-to-60s understatement, bounded by
AgeMs and lifetime peaks), and same-bucket pid recycle inside the
sweep window is a false negative for the absence proof.
* test(crash-reporting): pin PreGoneLargest to the crasher's own size, not the bucket's running sum
Loop-6 mutation battery found one survivor in the cut's re-anchored
suite: mutating Largest to carry the bucket's running sum survived every
test, because no fixture put a same-bucket sibling BEFORE the largest
process. That is the summing-bug family loop 2 found live. The
webview-guest test now enumerates the guest first and asserts
PreGoneLargest{Pid,Type,WorkingSetMB} carry the crasher's individual
4380, alongside the 4680 bucket total.
Also restores the false-positive caveat the cut's comment dropped: a
legitimately closed sampled process can trip the absence flag if the
crasher's row somehow survives the live enumeration (pre-existing,
unchanged by the cut).
* fix(crash-reporting): mark pre-gone attribution ambiguous
PreGone mirrors are whole-app snapshots, so a larger surviving Tab can own Largest and renderer-wide peak/private fields. Emit an explicit ambiguity boundary, prove the counterexample, and use Electron's pid plus creationTime identity to catch same-bucket PID reuse without adding stateful attribution.
* fix(crash-reporting): give the parking census a retained breadcrumb slot without starving memory highwaters
* test(crash-reporting): pin retained slot starvation
* fix(terminal): keep unresolved snapshot-capability verdicts re-askable
A daemon that could not answer the snapshot-capability probe within the
~91s startup ladder had its ptys settled 'no authoritative snapshot'
permanently: settled ids were excluded from every later synchronization,
and the only refresh callers run during renderer startup. Because the
eviction-exemption predicate treats absent/unknown capability as exempt,
one slow daemon start converted every local pty into a permanently
eviction-exempt tab — the hidden-worktree retention budget could free
nothing for the rest of the session (its own degenerate-case log:
'retention force-park freed no panes').
Three changes, none touching the safety direction (unknown still means
exempt, panes stay mounted):
- The retry ladder now decays to a slow 5-minute re-ask instead of a
permanent verdict, so a recovered daemon is consulted again without any
event wiring; the synchronization loop's existing timer carries it.
- The ongoing collector now gathers the same fields the startup refresh
does (layouts, pending reconnect ids), keyed on the sorted id set —
synchronization prunes cached verdicts outside its collected ids, so
the narrower collector could evict valid split-leaf answers back into
the exempt-by-default state.
- The degenerate-case log now carries per-route exemption counts
(fail-open / foreign-worktree / capability-unknown / split-pane) and a
matching crash breadcrumb, so field bundles can say which route
dominates instead of a fourth investigation.
Guard: exemption flips are a pin-unreachable input to the rendered park
verdict, so capability verdicts must only change between commits — the
new react185 harness force-parks a worktree, lets pane mounts write
layouts/titles, flips capability repeatedly, and asserts every flip
settles far from React's nested-update limit.
Gates (previously red): a recovered resolver is re-consulted and the
exemption clears; retained mounted tabs stay bounded by the retention
limit as hidden worktrees accumulate after daemon recovery.
Scope honesty: this bounds an unbounded-by-design retention residual; it
is NOT the fix for the unexplained multi-GB field OOM cluster, whose
retainer remains unidentified (field sessions crash with <= 1 mounted
manager).
* test(terminal): pin the widened capability collector and breadcrumb route buckets
Review found two unpinned pieces of the re-settlement fix: reverting the
ongoing collector to the narrow field set (dropping split-leaf layout and
pending-reconnect ptys, which the sync would then prune back to exempt
unknown) survived every existing test, and the force-park breadcrumb's
route counters had no coverage at all. Both mutations now fail.
* perf(terminal): memoize the capability collect key on its four store maps
The widened collector ran collect+sort+join on every Terminal render and
the memo-dep completeness was unpinned. Memoize the key on the four map
identities (behavior-identical; the string-keyed second memo still damps
layout-only churn) and pin the layouts dep with a staleness test — a
missing dep silently drops new split-leaf ptys from the sync.
* fix(terminal): keep a superseded capability pass's re-ask chain alive
Review loop 2: a synchronization pass cancelled by a newer generation
returned null, which ends the caller's timer chain — but the winner (the
startup refresh) ignores its own return value, so unknowns it leaves
behind had no scheduler left; recovery then waited on an id-set change.
Return 0 instead: the superseded chain re-checks immediately and the
early-outs collapse the re-check when nothing is pending.
Also pin the collect-key memo's remaining deps at runtime (pending
reconnect, tabs, pty ids) — exhaustive-deps is only a warn here, and
loop 1's staleness test covered the layouts dep alone.
* test(terminal): pin the same-identity re-ask path and closed-pty retry prune
Review loop 3: mutation testing showed the chain's own re-ask path was
unpinned — collapsing the first early-out to identity-only (returning null
whenever the live-set reference is unchanged) passed every test, yet it
kills the timer chain after one backoff: the hook refires with the SAME
memoized array, so that mutation is the settled-forever bug in a worse
shape. Pin it at both levels: a module test drives two passes over one
array identity, and a hook test drives the real timer chain through a
backoff refire with no id churn.
Also pin the closed-pty retry prune (its removal survived the battery):
the empty-set pass must return null — a leaked entry keeps a phantom
5-minute timer for the session — and a reappeared id must restart the 1s
ladder rather than resume the decayed cadence.
* fix(terminal): apply recovered snapshot capabilities
* test(terminal): remove stale lint suppression
* test(terminal): clean up capability prefetch hook
* fix(crash-reporting): attribute coalesced repeats exactly once
Two ways one burst's suppressed repeats were misattributed in forensics,
found across two adversarial review loops on the sibling-correlation work
but pre-dating it (the coalesce machinery shipped in #8800/#5818/#10729):
- Double-claim: a crash report filed mid-window snapshots the ring, folding
the suppressed repeats into the emitted crumb — but the next emit still
re-claimed those repeats in its suppressedSinceLast, reporting one burst
twice across two crumbs.
- Mirror erasure: a fold resolving onto a re-emitted crumb overwrote the
count that crumb was born carrying, deleting the previous window's repeats.
Track what each crumb has claimed (carried at emit, resolved by folds) so
every repeat is attributed exactly once. And never resolve into an evicted
crumb: when a storm pushes the burst crumb out of the 30-entry ring — or
past the retained-slot snapshot budget — mid-window, folding there would
mark the repeats claimed by evidence no snapshot can see and the burst
would vanish from the record entirely; drop the handle so the next emit
claims them instead.
Semantic note for trace-mirroring consumers: the returned
suppressedSinceLast is now net of already-folded repeats, so exactly-once
holds over the union of trace spans and report snapshots rather than
within the trace stream alone.
* fix(crash-reporting): preserve orphaned repeat debt on cleanup
* fix(crash-reporting): preserve data-less repeat debt
* fix(crash-reporting): make coalescing window monotonic
Orca had to guess which shell would parse a queued command line, then emit
syntax for it. Guessing is unreliable for a remote or WSL host, and every
dialect-dependent function is a place to get it wrong.
Replace the guess. Everything emitted for a Unix shell is now built to be
correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed:
- quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between
single-quoted runs. Both families read that identically, unlike the sh '\''
idiom, which fish silently halves and which makes a trailing backslash a
hard syntax error.
- clearEnvCommand emits a self-contained fish/sh branch. It deliberately does
NOT call a helper defined by Orca's shell wrappers: Orca wraps only zsh, bash
and fish, so an `sh`/`dash`/`ksh` login shell launches unwrapped — and the
same text is copied to the clipboard and pasted into shells Orca never
spawned. In both, a helper would be `command not found`, which is the exact
failure this exists to avoid. Two guarded statements rather than `A && B ||
C`, because fish's `set -e` returns non-zero for an already-unset variable
and would fall through to the sh branch; a trailing `true` pins the status,
since this is the last statement of a launch line and the prompt renders it.
- One tokenizer for Unix. The input is a settings string the shell never
parses, so parsing it per-shell only made the same setting mean different
things in different workspaces.
AgentStartupShell loses its 'fish' and 'unix' members, and the three
login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with
them.
Per-worktree shell history now actually works:
- zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before
any wrapper Orca controls, so the injected value was already gone — and with
ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The
intended path rides ORCA_HISTFILE and is restored after user config.
Fixes#11044.
- fish keeps history in its own data dir keyed by session name, since it
ignores HISTFILE and has no custom-directory knob. Files are deleted rather
than truncated, a symlinked ~/.local/share no longer disables cleanup, and a
GC sweep reclaims orphans whose meta.json is gone. The sweep refuses an empty
live-worktree set (indistinguishable from a store that failed to hydrate) and
skips files younger than GC_MIN_AGE_MS, mirroring the tree GC's guard against
the live-set snapshot race.
Verified against real shells rather than asserted as strings:
startup-shell-portability.live-shell.test.ts runs 194 assertions across
sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a
real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting
corpus was replayed byte-exact on Linux, where /bin/sh is dash.
* fix(terminal): preserve Option-composed ASCII input
* fix(terminal): preserve Option keyboard protocol semantics
* fix(terminal): complete Option keyboard event encoding
* fix(terminal): harden Option input encoding
* fix(terminal): close keyboard protocol fallback gaps
* test(terminal): prove Option-composed ASCII reaches the pty end to end
The Option-compose fix had unit coverage only. This drives a live Electron
pane whose kitty flags are armed by the application's own CSI > 1 u and
asserts the bytes at the pty boundary: composed `@` and Shift-layer `\`
arrive as text, configured Option-as-Alt still reports the layout-resolved
chord, and a non-ASCII glyph still reaches the app as its alt hotkey.
Restoring the pre-fix policy fails exactly the two composed-text scenarios.
Also records the ASCII rule's rationale where the rule lives, not only in a
test comment.
* refactor(terminal): drop the unread Option layers from the layout snapshot
The native helper computed an Option and Option+Shift character for every
key, shipped both over IPC, validated them in the parser and cached them in
the renderer — but no production caller ever asked for them. Only the base
and Shift layers are read, and Shift is the one the web layout map cannot
supply, which is why the helper exists at all.
Removing them halves the helper's UCKeyTranslate work per key and drops the
option parameter that six signatures were threading through for nobody.
* Preserve OpenCode session across command completion
- Add session start events and launch token tracking to establish session boundaries
- Defer retiring launch authority until OpenCode process actually exits, not just when a command finishes
- Fence previous tokens after restarts to prevent status updates from stale sessions
- Maps SessionStart as a session boundary for proper turn/state management
* Emit SessionStart only from OpenCode, not mimo-code
Restrict SessionStart lifecycle events to OpenCode exclusively. Mimo-code no longer emits SessionStart, as it should rely on OpenCode for session boundary signals. This prevents duplicate lifecycle events that could interfere with pane authority tracking and session state management. Also tighten foreground process result validation to reject stale results after title observation changes, fixing a race where a delayed foreground read from a previous cycle would incorrectly retire authority.
* feat(workspace): set project location from the create-worktree host picker
Hosts that still say "Project location not set" now get an inline Set location action. It opens a nested dialog over Create worktree so the in-progress form stays put.
* fix(workspace): replace unset-location status copy with a button
Drop the redundant "Project location not set" caption and show a Set project location action with a hover tooltip instead.
* refactor(workspace): tighten the set-project-location dialog
- reuse CreateProjectParentBrowser instead of a second host-filesystem browse view
- drop ProjectLocationBrowseTarget; parseExecutionHostId already models it
- single setLocation path in RunTargetCombobox (row, button, Enter)
- memoize the default clone URL instead of scanning on every store update
- fix missing required props in the new composer-card test
* fix(workspace): close the correctness gaps in set-project-location
- Escape in the host browser now backs out to the form instead of dismissing
the dialog and discarding the half-filled path/clone URL. Radix dismisses
from a document-capture listener, so only preventDefault can stop it.
- Drop the stopPropagation guards: window-capture (the composer's Escape
handler) already ran by then, so they never protected it — the nestedDialogOpen
gate does. They did silently kill RemoteFileBrowser's own key handling.
- Hide Set project location for host-local repo:<id> projects (folder projects,
git repos with no remote). Linking on another host matches by project identity,
which those have none of, so the call could only ever toast an error.
- Drop a standalone placeholder setup once a repo projection covers the same
project+host, restoring the (projectId, hostId) uniqueness invariant. Setting a
location on a host with a pending setup was leaving a ghost that sorts first and
reads back as 'not set up'.
- Existing-folder submit label matched a catalog string reading 'Importing...'
* test(composer): follow the renamed local in the host-retarget source assertion
* fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal
createOrAttach validated the working directory synchronously on the daemon's
only thread. Measured on Windows 11 + Ubuntu-24.04:
existsSync on an unreachable UNC share 21,022 ms
wsl.exe probe, cold distro 1,266 ms
wsl.exe probe, warm distro 59 ms
existsSync/statSync on healthy \\wsl.localhost 4 ms / 1 ms
A single unreachable share therefore blocks the whole RPC loop past the
client's 30s request ceiling, so every other terminal stalls behind it and
reports `DaemonProtocolError: Request createOrAttach timed out after 30000ms`.
The main process already validates asynchronously and passes prevalidatedCwd
(ipc/pty.ts); the daemon never got the same treatment.
Add validateWorkingDirectoryAsync (one stat, not exists-then-stat, so an
unreachable share is not paid for twice) and await it from the daemon spawn
preflights. spawnSubprocess now returns SubprocessHandle | Promise<...>, which
existing sync stubs still satisfy.
Deliberately not bounding the stat with a timeout: the 30s ceiling comes from
blocking the shared loop, not from the duration. A timeout cannot tell "slow
share" from "gone share", so it would fail spawns that succeed today at 3-8s
on a cold VPN mount, and trade an accurate "working directory does not exist"
for a guess.
The new await opened a race: it sits between the "already exists?" check and
the sessions.set that publishes the session, so two concurrent creates for one
session id both spawned. Gate creation per session id; distinct ids still spawn
in parallel.
STA-4470
* fix(daemon): fence async spawn lifecycle
* fix(worktrees): address review feedback on the worktrees slice split
Follow-ups to #14643. All of these predate the split (the constructs moved
verbatim from worktrees.ts), so they are behavior fixes, not refactor fallout.
- fetchAllWorktrees: route runtime-scope-forbidden errors through the toast in
the hydration path, matching the fast path
- updateWorktreeMeta / ensureHostedReviewPushTarget: use
trySettingsForWorktreeOwner so an ambiguous owner skips push-target resolution
instead of throwing past the { ok, error } contract
- updateWorktreeMeta: compare linkedPR with ?? null so an unknown worktree does
not trigger a spurious hosted-review refresh (matches the four sibling providers)
- persistWorktreeMeta: localize the two runtime-capability messages
- buildWorktreeRenameState: fix a doc comment that contradicted the code
- setWorktreesPinnedAndReveal: skip updateWorktreesMeta on an empty map
- pruneLastVisitedTimestamps: treat an empty worktreesByRepo list as unhydrated,
and clear activeWorkspaceKey alongside a stale activeWorktreeId
- forceDeletePreservedBranch: fail closed when several retained cleanups match
and no host was given, rather than routing to the active runtime
- purgeOrphanedRuntimeSshProjects: drop blank target ids so a repo with no
connectionId cannot match
- markWorktreesDeleting: make the skip guard phase-aware so queued rows are
promoted to deleting
- worktree purge: validate right-sidebar tabs with normalizeRightSidebarRoute so
pr-checks and plugin panels are not silently dropped
* test(worktrees): add edge case tests from review feedback
Add defensive tests covering phase-aware deletion state transitions, tab
preservation across worktree purges, graceful handling of ambiguous owner
scenarios, and stale workspace cleanup. These catch edge cases that could
lead to silent failures or invalid state transitions.
* fix(worktrees): preserve workspace key pointing at live worktree
Replace overly broad scope check with direct equality check, so pruning
only drops the stale worktree's own workspace key while preserving keys
pointing to other live worktrees or folder workspaces.
* fix(worktrees): clear a legacy unprefixed active workspace key on prune
pruneLastVisitedTimestamps only dropped the `worktree:`-prefixed derived key,
so sessions predating the prefix kept a phantom workspace selected. Match the
purge path, which already treats a bare worktree id as a workspace key.
* chore(i18n): re-key the two runtime-capability strings to the canonical scheme
The keys added when persistWorktreeMeta was localized were hand-written. Derive
them the way localize-renderer-strings.mjs does — module path plus
sha1(path:text) — so a drift audit against the generator does not flag them.
* fix(worktrees): stop updateWorktreeLineage rejecting past its never-rejects contract
The sidebar's "Remove parent link" (WorktreeContextMenu.handleRemoveParentLink)
awaits updateWorktreeLineage in a bare `void Promise.all(...)` with no catch, so
the action must always resolve. Two paths broke that:
- settingsForWorktreeOwner ran outside the try and throws on an ambiguous owner.
Skip with warnAmbiguousOwnerOnce instead, matching the ensure-push-target fix.
- the recovery refresh inside the catch has no internal try/catch, so a failing
lineage RPC rejected out of the handler meant to absorb the failure.
assignWorktreeParent keeps rethrowing (both its callers catch and toast), but its
recovery refresh is now best-effort too so it can't mask the original cause.
Also: localize the new ambiguous-host preserved-branch throw (it reaches a toast
description; the throw above it stays verbatim because it mirrors a main-process
message), and drop blank SSH target ids once so both purge lookups agree.
* fix(worktrees): surface un-nest failures instead of silencing them
Supersedes the approach in 789edeaa7f. That commit fixed the unhandled rejection
in WorktreeContextMenu.handleRemoveParentLink by making updateWorktreeLineage
never reject, which had two costs:
- it made the .catch in use-lineage-drop-commit unreachable, so a drag-unnest
failure went from an error toast to a silent no-op; and
- trySettingsForWorktreeOwner returns null for BOTH 'ambiguous' and 'missing'
route resolutions (worktree-operation-route.ts collapses the three-way result),
so the skip also swallowed unresolvable-route cases while logging "identity is
ambiguous", which is the wrong cause for a legacy row on a multi-runtime setup.
Fix the caller instead: updateWorktreeLineage keeps rejecting, and both call
sites go through a shared unnestWorktrees() that catches and toasts. The
best-effort recovery refresh from 789edeaa7f is kept — that part was correct and
stops a failing refresh masking the original error on the assign path.
unnestWorktrees also de-duplicates the identical console.error/toast pair the two
callers had, and reuses the existing failedUnnestWorkspace key (already
translated in all five locales), so no new string ships.
Follow-ups to the module extraction (#14252):
- Guard the lineage companion maps in worktree meta GC, and clear them
alongside a corrupt worktreeMeta so stale rows can't re-attach.
- Repair a renamed worktree's stale lineage.worktreeId and flag the save.
- Key the git-username cache by execution host + path so the same checkout
path on local/SSH/runtime hosts can't cross-hydrate usernames.
- Drop undefined keys before the automation update spread; a Partial with
an explicit undefined blanked stored values.
- Normalize pane identities in every workspaceSessionsByHostId partition,
not just the legacy blob, and use the merged leaf maps for lease and
acknowledgement remapping.
- Reject duplicate preferred leaf ids so two panes can't collapse onto one
pty/buffer/scrollback key.
- Let an explicit rightSidebarExplorerView outrank the legacy search-tab
fallback; the legacy migration now runs on the raw payload at load.
- Record why the mobile pairing migration leaves its sources in place.
- Trim persisted folderPath, normalize synthesized worktree visibility
preferences, validate notification settings field-by-field, strip
undefined optional keys before the SSH lease merge, reuse `now` for an
automation's updatedAt, and rename the pane alias registrar.
* fix(editor): address review feedback on the split editor slice
- Localize the conflict-placeholder guidance string (en/es/ja/ko/zh).
- Filter target-worktree tabs by migrated tab id so an owner transition
cannot leave two tabs sharing one id.
- Resolve a pending editor reveal by fileId first; the oldFilePath scan
could pick another worktree's rekey.
- Return before opening a workspace editor item when conflict metadata
is missing, so no tab is created for a file that never entered openFiles.
- Skip a persisted open file whose resolved id is already used; the
session schema allows repeated (path, worktree, runtime) tuples.
* fix(editor): remove migrated tab ids from sibling groups
When tabs migrate to a target group during editor owner transition, the
same tab IDs can be left in sibling groups, causing state corruption.
Strip these IDs from all sibling groups to ensure each tab ID exists
only once across the editor layout.
* feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK
40% of renderer deaths report exit 0x80000003 (STATUS_BREAKPOINT) — a Chromium
CHECK/DCHECK — and we captured only the exit code, so the cause was structurally
unknowable. Nothing in the tree wired crashReporter at all.
Start Crashpad pre-whenReady and lift the text signature out of the dump:
Chromium stores the fatal log line in the LOG_FATAL annotation, so the check
name, file and line are recoverable with no symbols and no minidump_stackwalk.
Upload stays off. The existing transport is a user-initiated 4 MiB text bundle;
raw dumps are multi-MB binary carrying process memory. Dumps stay on disk and
only the signature rides the existing crash-report flow.
- minidump-stream-reader: bounds-checked view; a truncated dump degrades
- minidump-crashpad-annotations: allowlisted keys (switch-N carries command lines)
- minidump-crash-signature: LOG_FATAL -> file/line, exception, faulting module
- crashpad-capture: polls for the dump, which races process-gone delivery
STA-4469
* refactor(crash-reporting): claim dumps once and match them to the dead process
A newer dump from a different process could be paired to the wrong report, and
two reports in one crash burst could both claim the same file. Match the dump's
own ptype against the process Electron said died, and claim each dump once.
Also let the fatal line use the stack-length budget: a CHECK message truncated
at 240 chars can lose the condition, which is the diagnosis.
Fix the child-type test to match the classifier: GPU exits are recoverable
churn and never become reports, so they must not burn a dump poll either.
STA-4469
* fix(crash-reporting): recover real Electron CHECK logs
Electron 43 Windows dumps carry the Chromium CHECK line in captured memory but omit the claimed LOG_FATAL annotation. Recover only bounded Chromium-formatted fatal/CHECK lines, and prune raw dumps after crash events so suppressed child crash storms stay within the 128 MiB budget.
* fix(crash-reporting): satisfy not-found lint
* perf: coalesce git upstream status reads
* fix(git): repair upstream lease key imports and guard its field list
The read owner imported the shared/types barrel deleted by #14447, and its
push-target key hand-enumerated fields, so a new GitPushTarget field would
silently share a lease between two different targets. The destructure now
fails to compile if a field is added. Lease tests moved into their own file
after #14728 split ssh-git-provider.test.ts.
* test(git): enforce native upstream coalescing in CI
The 10-caller benchmark only runs under ORCA_GIT_UPSTREAM_COALESCING_BENCH_JSON,
so nothing in CI failed when the native/WSL lease was bypassed. Route status.ts
through invalidateGitUpstreamStatusReads so the export has a production caller.
* ci: color added/deleted LoC counts in PR summary
* ci: use GitHub color-swatch dots for added/deleted LoC counts
* ci: color LoC counts with LaTeX textsf
* ci: bold LoC counts; render zero in white
* ci: render LoC counts large bold sans-serif
* ci: use bold math font for LoC counts
* ci: color only the + and - signs on LoC counts
* refactor: split editor.ts under 400 lines
Move editor slice types, file-id/tab helpers, and action factories under
src/renderer/src/store/slices/editor/. The source file is now a public
barrel. No intentional behavior change.
* refactor: split editor-chrome-slice into state modules
- Move EditorDraftState, ExplorerDirState, and RightSidebarState type definitions into their respective action files
- Simplify state creator return types from Pick<EditorSlice, ...> to specific state types
- Improve modularity by colocating types with implementations
* refactor(persistence): extract modules to half persistence.ts
* refactor(persistence): tighten the extracted operations seam
Review follow-ups on the module extraction, all behavior-neutral.
The extracted operations read and mutate the Store's state object in place, but
every seam typed it as a bare PersistedState, so nothing at the boundary said a
caller must pass the live reference — a future caller handing over a clone would
have its writes silently dropped. Name that contract: StoreOwnedPersistedState
carries it to every operations interface and every mutating free function.
normalizePersistedPaneIdentityState and backfillFolderScopeConnectionIds stay on
PersistedState; they build a fresh state rather than mutating the Store's.
The six *PersistenceOperations wrappers were constructed per delegate call. They
are stateless today, so this was inert, but any future instance state would be
lost between calls. Memoize them, and mark state and gitUsernameCache readonly
so the compiler enforces the single-assignment invariant memoizing them relies
on.
Also: restore flushSshPtyConsumerRecovery, whose inlining left its rationale
duplicated at both call sites; document that migrateWorktreeIdentity's boolean
gates the caller's save, since the extracted function kept no docs of its own;
and merge a duplicate shared/types import that was failing lint under
--deny-warnings.
* delete plan doc
* refactor(persistence): add error recovery and improve field cleanup
- Rollback failed migrations to prevent corrupted state that blocks retry
- Gracefully skip malformed entries in normalization instead of aborting
- Strip retired fields to prevent orphaned state and sync issues
* refactor(persistence): drop the redundant persistence- filename prefix
The extracted modules already live in src/main/persistence/, so name
them after the domain they own. Point leftover shared/types imports
at the real type modules while touching those files.
* refactor(persistence): optimize lookups and fix unsanitized updates
- Use Maps instead of repeated array searches for O(1) lookups
- Apply sanitized updates instead of raw input in ui-state-update
- Compare fields directly rather than JSON strings to avoid false dirty states from persisted key ordering differences
* refactor(persistence): group modules into lifecycle folders
Move the 42 flat persistence modules into six folders named for what the
module does, and lift the Store class out of the barrel so persistence.ts
becomes an 8-line public surface.
Bodies are unchanged: every moved file diffs clean against HEAD once import
blocks are excluded. Only import specifiers were rewritten, by resolving each
one to an absolute path and mapping it through the move map.
Store keeps its existing max-lines suppression; its baseline entry is repathed
rather than re-added. Its 119-method public API sets a ~525-line floor, so it
cannot meet the 400-line cap without breaking the API for 153 importers.
* Sanitize worktree visibility sources and preferences on hydration
Ensure invalid or corrupted data from disk (untracked whitespace,
relative paths, bogus preference values) is cleaned during load
rather than corrupting the in-memory store.
`source-control-dropdown-items.ts` was 524 counted lines behind an
`eslint-disable max-lines`. It splits along the seams the resolver already had:
- `source-control-dropdown-item-types` — the row union, consumed by CommitArea,
the composer and the action dispatcher without pulling in the state machine.
- `source-control-dropdown-labels` — count/label/title wording.
- `source-control-dropdown-action-context` — the branch, upstream and review
facts every row reads, derived once so rows cannot disagree about them.
- `source-control-dropdown-commit-items` / `-remote-items` / `-review-items` —
the three row groups, each keeping its own disabled-reason ladder intact.
`resolveDropdownItems` is now just the entry order plus the conflict-abort and
hosted-review-busy passes.
Verified output-identical to the pre-split resolver: a differential harness ran
both implementations over 16,380 generated input combinations (every upstream
shape × PR state × conflict operation × blocked reason × staged count ×
provider) and compared entries deeply. The harness was scaffolding and is not
committed.
Splits the nine oversized modules in the daemon/provider/runtime domain into
focused per-concern files and drops their max-lines baseline entries.
- daemon: `Session` decomposes into an output plane (emulator, pending-output
buffer, client fan-out), a producer-pause controller, a shell-ready barrier and
a termination controller; `DaemonClient` into socket connect, hello handshake,
ndjson readers, pending-request settlement, listener registry and notify
settlement; `daemon-health` into pid-file parsing, process identity,
stale-kill, TCC attribution and bundle staleness; `shell-ready` into the marker
constant and the bash/zsh rcfile generators.
- providers: local-pty shell-ready wrapper generation, wrapper root, startup
command and bash rcfile split out of local-pty-shell-ready.
- runtime: `Coordinator` sheds DAG convergence, decision gates, escalation
triage, the runtime contract, the stale-base flag and task dispatch; the
files/git/github rpc modules split into per-domain method groups.
Behavior-preserving: the extracted units keep their original construction order,
guards and timer lifetimes, and every RPC method name is still registered.
Test `vi.mock` surfaces were re-partitioned to follow the moved symbols.
The two tab-group hooks, the pane manager, worktree activation, and the terminal
pane context menu each carried a file-level `eslint-disable max-lines` and ran
461-745 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all five suppressions and prunes their entries
(341 -> 335).
Pure move, no behavior change. useTabDragSplit is cut into gesture lifecycle,
hover preview and drop commit; useTabGroupWorkspaceModel into item projections
plus the tab-close, close-scope, activation and creation command sets; the pane
manager into host, tree mutations, pane creation, drag wiring, reparent frame
tracking, layout sweeps and rendering diagnostics.
react-hooks exhaustive-deps stays at zero warnings, matching HEAD. Dependency
additions are only stable identifiers -- refs and callbacks that became
parameters -- and no `.current` dereference was added to any dependency array.
Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the three remaining failures are pre-existing load flakes in untouched files,
each green when re-run serially), no new runtime import cycles among 1020
modules, no barrel files, and no lint suppression added anywhere.
The four editor modules, the diff-comment decorator and the file-type icon table
each carried a file-level `eslint-disable max-lines` and ran 319-806 counted
lines against 300/400-line budgets. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all six suppressions and prunes their entries (341 -> 334).
Pure move, no behavior change. MonacoEditor is cut along its own seams -- mount,
input bindings, markdown annotations, decorations, content sync, view-state
persistence and reveal scheduling -- with the markdown overlay becoming its own
component. useEditorPanelContentState splits into file and diff content loaders
plus the active-tab load and reload triggers.
When the mount module came in at 370 counted lines, over the 300 ceiling, it was
split again into its parameter types and its input bindings rather than carrying
a suppression.
Hook usage is identical to HEAD across all three React split families: the same
counts of every hook type between each original and its extracted modules, so no
hook was added, dropped, or converted to a plain function. react-hooks
exhaustive-deps stays at zero warnings, matching HEAD.
Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the four remaining failures are pre-existing load flakes in untouched files,
each green when re-run serially), no new runtime import cycles among 884
modules, and no lint suppression added anywhere.
The six right-sidebar modules and the remote file browser each carried a
file-level `eslint-disable max-lines` and ran 347-797 counted lines against
300/400-line budgets. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all seven
suppressions and prunes their entries (341 -> 333).
Pure move, no behavior change.
Two renderer-specific hazards were found and fixed rather than shipped.
First, effect and ref LIFETIME. FileExplorer's `if (!worktreePath) return` sits
above the files pane, so moving the worktree-reset effect into that pane made its
guard ref `lastResetWorktreePathRef` die on any render where worktreePath was
transiently null (workspace-list refresh, store rehydrate, remote worktree
reload). On remount the guard read null, so the reset fired even when returning
to the SAME worktree -- wiping dirCache, collapsing every expanded directory,
clearing the name filter and undo history, and forcing a full re-read over SSH.
The tree-load effects now live in a hook called from FileExplorer above the early
return, and the pane is purely presentational with zero hooks. That also restores
the original parent-effect ordering, which had shifted because React flushes
child effects before parent effects.
Second, extracting a hook silently degrades dependency analysis: `setX` setters
that the linter knew were stable when created locally become opaque parameters,
producing 8 new react-hooks/exhaustive-deps warnings where src/renderer had zero.
Those are fixed by listing the genuinely stable identifiers (useState setters and
ref OBJECTS). No `.current` dereference was added to any dependency array, since
that would change callback identity as the ref mutates.
Verified: oxlint clean with exhaustive-deps back to zero, ratchet passes,
typecheck clean, full unit suite green on the first pass, no new runtime import
cycles, no lint suppression added, and hook usage identical to HEAD across both
split families.
The three usage scanners and their stores, plus the renderer usage-overview
model, each carried a file-level `eslint-disable max-lines` and had grown to
338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all seven suppressions and prunes their entries
(341 -> 334).
Each file is cut along the seams it already had -- and that several of the
suppression comments named out loud: filesystem discovery / record parsing /
attribution / aggregation for the scanners, and pricing policy / scope filters /
rollups / session rows / automation attribution for the stores.
Pure move, no behavior change. Code is relocated verbatim; the only edits are
import plumbing and, where a private class method became a free function, the
mechanical `this.state` -> `state` parameter threading. Every converted call
site passes `this.state` at call time and the automation path takes a live
`getState: () => this.state` getter, so no state is snapshotted. No barrel
exports: each new module owns real logic and importers point at the owner.
Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no import cycles among the 64 affected modules, and a
statement-level diff of every split confirms the moves are verbatim.
The four agent hook services, the main hooks module, and the two relay modules
each carried a file-level `eslint-disable max-lines` and ran 365-628 counted
lines against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all seven suppressions and prunes their entries (341 -> 334).
Pure move, no behavior change. Each hook service splits into its managed script
source, its config/bundle serialization, and its remote-install path, keeping the
per-agent integrations independent: copilot, amp, antigravity and hermes each
retain their own getManagedScript rather than sharing one, because each emits a
different script body for a different agent. Merging them by name would have
been a behavior change, not a refactor.
For antigravity the suppression's stated rationale -- that local install, Windows
wrapper generation, status cleanup, and SSH remote install must share one event
list and managed-command matcher so stale-hook cleanup cannot drift by platform
-- is now enforced structurally instead: both install paths call
buildInstalledConfig + createAntigravityManagedCommandMatcher over the single
ANTIGRAVITY_EVENTS catalog, with the graph a strict DAG.
Also registers the six new antigravity/ and copilot/ modules in
config/tsconfig.cli.json. That project uses a curated `include` list rather than
a glob, so an unlisted module fails `tsc -p config/tsconfig.tc.cli.json` with
TS6307 even though the entire unit suite passes.
Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, green when
re-run serially), no new runtime import cycles, and no lint suppression added.