* fix(worktree-watcher): surface external push -u through the git-common watch
An external-shell 'git push -u' writes only the common .git/config (plus
refs/remotes/<remote>/<branch>), both invisible to the git-common event
filter, so the Checks panel stayed on 'No upstream configured' until the
renderer safety poll. Classify the common config and remote-tracking refs
as status-tier signals, poll config alongside the other primary-checkout
metadata files, and keep FETCH_HEAD/reflog/ref-lock churn ignored.
* fix(worktree-watcher): refresh after subsequent pushes
* perf(main): gate idle git-common polling
Co-authored-by: Orca <help@stably.ai>
* fix(main): enumerate git-common worktrees every tick to catch coarse-FS add/remove
The worktrees-dir readdir was gated on the dir's mtime:ctime:ino:size signature, so on
a coarse-mtime/FAT filesystem a same-granule add+remove (all four fields collide) went
undetected until the ~30s index backstop. A single readdir of a small dir is negligible
next to the per-entry structural stats that already run every tick, so always enumerate —
the listing is the authoritative add/remove signal. The expensive per-entry index read
stays gated on each entry's own dir signature; onFullScan now reflects the ungated
index-metadata backstop fan-out (the real periodic cost) rather than the readdir.
Co-authored-by: Orca <help@stably.ai>
* fix(main): don't fabricate worktree deletions on a transient git-common readdir failure
Follow-up to always-enumerating the worktrees dir: the readdir catch-all treated ANY
error as an authoritative empty listing, so a transient failure (EIO/ESTALE/EMFILE,
network/SSH hiccup) emitted a false delete for every linked worktree (and a false create
next tick) — and enumerating every tick widened that exposure. Only ENOENT (dir truly
absent) now yields an empty listing; other errors retain the known entries so per-entry
stats still run and a real removal surfaces as that entry's own stat miss. Adds a
regression test (readdir → ENOTDIR) asserting no false delete.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(main): park worktree pollers while hidden
The per-repo worktree metadata pollers ran 24/7 with no window-visibility gate
(~50 fs.stat/sec at 20 repos on macOS while the window is hidden). Park the poll
timers while the window is hidden and resume losslessly (immediate fresh-vs-retained
snapshot diff) on reveal, via an injected WorktreePollerWindowVisibility mirroring
the ssh-port-scanner pattern.
Only the poll timers are gated; the darwin native worktrees/ fsevents watch and its
teardown/re-arm lifecycle stay always-on (push-based, ~0 idle cost; #8732 race).
A window is parked only once it has actually been shown and is now hidden: a live
never-shown window (ORCA_E2E_HEADLESS keeps one) and null/destroyed windows stay
always-visible, so a windowless/headless host never permanently parks the poller.
Co-authored-by: Orca <help@stably.ai>
* fix(main): keep start-to-start poller cadence after the interval→timeout change
Parking the pollers moved them from setInterval to a one-shot setTimeout chain, but
scheduling the next tick a full pollIntervalMs AFTER each scan completed turned the
cadence into gap-after-completion — every visible refresh landed ~one scan-duration
late per tick. Measure from tick start and schedule max(0, interval - elapsed) so the
cadence is start-to-start like the original interval, while keeping the one-shot chain
that park/resume needs.
Co-authored-by: Orca <help@stably.ai>
* fix(main): clamp poller reschedule delay to [0, pollIntervalMs]
Date.now() is not monotonic: a backward wall-clock jump (NTP correction) during a scan
makes (now - startedAt) negative, so the start-to-start delay pollIntervalMs - elapsed
would exceed one interval by the adjustment — suppressing visible metadata refreshes for
minutes/hours, unlike the former setInterval. Cap the computed delay at pollIntervalMs
(upper) as well as 0 (lower) in all three pollers.
Co-authored-by: Orca <help@stably.ai>
* refactor(main): drop redundant notifyTimer guards to stay under max-lines
The visibility-parking change pushed worktree-base-directory-watcher.ts to 303 code
lines (limit 300). clearTimeout tolerates null/undefined, so the two truthy guards
around it are redundant — remove them (coalescing null→undefined for the type). No
behavior change; back under the limit without an eslint-disable.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Prevent index churn from refreshing worktrees
* Cover IPC contract in worktree reliability gate
* Refresh background worktree heads without re-entering structural fanout
External commits, amends, and soft resets in non-active worktrees now reach
store rows through spawn-free Git metadata reads diffed in the watcher's
existing debounce, emitted only on real head moves. HEAD reflog appends become
status-only triggers, config.worktree becomes structural for sparse-flag
freshness, and the non-darwin poller gains a periodic ungated index re-stat
so in-place rewrites on coarse-mtime filesystems cannot be missed forever.
* Reject unsafe symref paths and validate object ids in the head reader
Ref content comes from repo files an attacker can craft. Backslash segments
traverse on Windows where join treats them as separators, and colons are
forbidden in Git ref names; both now fail isSafeRefName before any path is
built. Resolved values are additionally emitted only when they match a hex
SHA-1/SHA-256 object id, so no file content can leak through the identity
event even in principle.
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <>