* feat(native-chat): hover cards for GitHub PR and issue links
Links to the chat's own repo (origin or upstream) open a card with the
state, title, author, update time, and +/- or comment count. Bare #123
references in agent replies now link too. Cards use a light one-call
lookup; older hosts ignore the flag and answer with the full lookup.
* test(github): expect the preview options argument in runtime owner/repo lookups
* fix(native-chat): route GitHub link cards by the chat's own host and retry failed repo lookups
The repo lookup took the first worktree with the chat's id, so a twin on
another host could answer, and the preview cache keyed by repo id alone.
A failed slug lookup also left cards off until the chat remounted.
* fix(wsl): Source Control's PR push-target and Create PR checks never start a stopped distro
Showing Source Control resolved the linked PR's push target
(worktrees:resolvePrBase) and its Create PR eligibility
(hostedReview:getCreationEligibility); both read the repo's git config over
\\wsl.localhost and ran git and gh in the distro, starting a stopped one
(E9). Both now refuse a stopped distro unless the user is creating or
launching from the PR, or pressed Create PR (optional wakeWslDistro), in
the IPC handlers and their runtime twins.
* fix(wsl): gate the GitLab MR push-target lookup the same way
Source Control's push-target ensure also resolves a linked GitLab MR
(worktrees:resolveMrBase / worktree.resolveMrBase), which read the repo and
ran glab and git in a stopped distro. It now refuses unless the user is
creating from the MR, which passes wakeWslDistro.
* test(runtime): let the mocked running gate cover guardWslUncPathAccess too
Runtime specs fake win32 with no wsl.exe and mock blockedWslDistroForPath
to running; the real guard calls its own module's check, so the PR-base,
MR-base and eligibility gates refused there. The guard mock now asks the
mocked check, keeping per-test overrides working.
GitHub serves a renamed repository under its old name, so branch discovery
finds the PR through a stale origin URL, then rejects it because the PR
reports the new head repository name. Accept the PR when the queried
repository is origin itself and GitHub reports it as a same-repository PR,
and share that same-repository check with the cached-number lookup path.
Fixes#35687
Co-authored-by: Neil <neil@stably.ai>
Cwd-less gh/glab calls whose host CLI is missing fell back to the default
WSL distro with no running check, so background work (rate limits, auth
status, Settings, task-source probes) booted a stopped distro. The
fallback now asks the background exec gate first: stopped or
unverifiable refuses with the distro-stopped error, running and user
acts fall back as before.
* Send ordinary PR reviewer updates directly to the API
* test(github): trim redundant cases
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* Reuse work-item page totals for matching count queries
* Verify page totals avoid later count requests
* Read numbered PR pages with reusable cursors
* Combine short cursor prefixes with the requested page
* Use advanced search for grouped PR cursor queries
* Validate combined quota changes against the final PR base
* Verify forced and credential-scoped PR pages bypass saved cursors
* Batch uncached PR reads and reuse equivalent PR totals
* Match Boolean PR totals to the advanced list predicate
* Keep malformed PR count groups on the scoped legacy search
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* Read sidebar PR and stack metadata in one GraphQL query
* Preserve direct PR error recovery without duplicate GraphQL reads
* Keep combined sidebar queries out of merge preflight
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
gh pr list already scopes --search with repo:OWNER/REPO type:pr. The
extra is:pr Orca added made private-repo PR lists come back empty when
gh is authenticated with a GitHub App user token (ghu_).
Fixes#34536
* fix(github): count gh pr and issue commands against the GraphQL rate limit
gh serves pr/issue/repo view-style subcommands over GraphQL, but the breaker
filed every non-api command under core and only matched REST's
"API rate limit exceeded" wording, so GraphQL exhaustion never tripped it.
Map those subcommands to the graphql bucket, match GraphQL's
"API rate limit already exceeded", and guard project-view issue close/reopen
against the GraphQL quota.
Fixes#28767
* test(github): drop redundant Date.now() args in graphql breaker test
Keeps the flaky-test-pattern ratchet at its baseline.
---------
Co-authored-by: Neil <neil@stably.ai>
* fix(wsl): background repo probes, PR refresh and worktree scans never start a stopped distro (E9)
Any Windows touch of a \\wsl.localhost path, or Git/gh run for it, boots a
stopped distro. These background paths ran without asking the stopped-distro
gate: the worktree scan refresh (Git-admin fingerprint reads plus git worktree
list), the remote-identity and username enrichment sweeps, the startup
upstream backfill, the quiet-period ref maintenance, and queued PR refreshes.
Each now asks blockedWslDistroForPath first and skips, keeping known data,
until the distro runs; a host path never asks the gate.
* fix(wsl): a user act's wake allowance ends with the act
The allowance lived in an AsyncLocalStorage store, so any timer, listener or
queue created during a user act (opening a workspace, a wake read, the first
git status) kept it forever and could start the distro later, after a
terminate. The scope now carries an active flag cleared when the act settles.
* fix(wsl): skill discovery and automatic refreshPRNow never start a stopped distro (E9)
- Skill discovery (mounted by passive UI) leaves out roots on a stopped or
unverifiable distro, and skips plugin discovery for such a cwd, instead of
reading \\wsl.localhost; they are not reported missing, so the next scan
picks them up.
- refreshPRNow skips automatic reasons (visible, active, post-push) on a
blocked distro like the drainer does; only a manual refresh may start it.
- Ref maintenance's opt-out probe takes the same sync wslDistroForPath
early-out as the other paths.
- Tests: stopped-with-no-cache worktree scans answer from stored rows, and
'unverifiable' is treated as blocked.
* fix(wsl): skill discovery for a WSL target never starts a stopped distro (E9)
Passive UI discovers skills for projects whose agents run in WSL. The target
resolution probed the distro home (wsl.exe) and the scan ran in the distro,
so either booted a stopped one. Discovery now refuses a stopped or
unverifiable WSL target before the home probe and answers no skills and no
sources, so nothing reads as missing and the next scan retries. Delete and
share keep resolving without the gate.
* fix(wsl): a settled user act marks its distro woken; host skill scans never await the gate
- runAsWslUserAct (the gate's wrapper) notes the distro woken inside the act
when it settles, success or throw, so a follow-up is never answered by a
'stopped' read taken before the act.
- Skill discovery takes sync early-outs: a host-only root set and a host cwd
never await the WSL gate.
* fix(wsl): Share discovers as a user act; a stopped distro's skills read as unknown, not absent
- skills:prepareShare (and the skills.share RPC) discover as a user act on the
target's distro, so an explicit Share on a stopped distro finds its skills.
- A blocked WSL target answers an 'unavailable' source per requested kind, and
roots (or a workspace plugin folder) on a stopped distro are reported
'unavailable' instead of dropped, so the installed-skill verdict says the
status may be incomplete rather than offering Install.
* test(wsl): use a fixed fetchedAt in the stopped-distro PR refresh test
* fix(wsl): Share scans stopped distros' skill folders, with or without a target
The desktop Share dialog sends no target, so the user-act scope had no distro
and the gated scan reported a stopped distro's repo folders unavailable; Share
then failed with skill-share-source-not-found. discoverSkills takes
gateStoppedWslDistros: false for both share paths (IPC and RPC), skipping the
stopped-root split and the plugin-cwd check; passive discovery keeps the gate.
Gated and ungated scans of one target no longer share a coalesced answer.
GitHub issue close/reopen swallowed every gh failure mentioning "already"
(e.g. "API rate limit already exceeded"), and GitLab MR close/reopen did the
same for glab. gh exits 0 when the issue is already in the target state, so
every gh failure is now reported. For glab, only its fixed no-op refusals
("this merge request has been closed" / "this merge request is already open")
count as success; "has already been merged" and all other errors surface.
Fixes#28766
Co-authored-by: Neil <neil@stably.ai>
* fix(i18n): localize GitHub project view errors and states
Main now tags Orca-authored project-view failures with an optional
reason code plus host, rate-limit numbers and diagnostic detail; the
English message is unchanged for mobile, older clients and logs. The
renderer maps type/reason to translated copy in the error state, picker,
item dialog and mutation toasts, falling back to the English message for
older hosts and GitHub's own text. Also wraps the layout labels,
unsupported-layout, not-found/too-large copy, picker partial-failure
summaries and the gh sign-in help.
Closes#34589
* test: expect input_too_large reason on oversized project refs
* fix: make project view error copy switches exhaustive
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Main now tags each Orca-authored PR/MR create failure with a reason code
(the English error is unchanged for mobile, older clients and logs), and
the renderer maps the reason to translated copy in the Source Control
notice, the Checks panel and the pre-create blocked notice. Sign-in steps
are single-sourced across providers.
Fixes#33321
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
On repos whose base branch requires a merge queue and that disable
auto-merge, gh pr merge --auto always calls enablePullRequestAutoMerge,
which GitHub rejects. Call the enqueuePullRequest mutation directly in
that case and report enqueue failures as merge-queue errors.
Fixes#19638
Co-authored-by: Neil <neil@stably.ai>
Issue create (including the empty-body fallback and follow-up PATCH),
issue body edits, and issue comments passed the body as
`--raw-field body=...` on argv. Windows caps the command line at
32,767 chars, so bodies with pasted inline images failed with
`spawn ENAMETOOLONG` before reaching GitHub.
These calls now use `gh api -X <METHOD> <endpoint> --input -` with a
JSON payload on stdin. ghExecFileWithScopeAsync also now forwards
`options.stdin` to the child process; it was previously dropped.
Fixes#15832
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* fix(worktrees): never leave the create-worktree dialog stuck on a stalled preflight
Bound the same-repo PR branch fetch with the review-head fetch timeout, and
hand a slow composer submit off to a staged sidebar row so the dialog closes.
* fix(worktrees): harden create-worktree handoff from adversarial review
Dedupe handed-off submits against linked creations resolved during preflight,
bound the staged sidebar row with a deadline, offer Reopen on handed-off
failures, and bound the GitLab MR branch fetch.
* test(worktrees): expect the bounded PR/MR branch fetch timeout
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* chore(lint): remove disable directives that suppress nothing
Each was reported unused by the oxlint config that owns its rule (root,
casting, react-doctor, anti-slop, native-plugins), including 17 file-level
react-doctor blanket disables whose rule no longer fires. An unused
react-hooks disable was masking a react/purity finding in
useAgentBucketCounts (compiler rules skip functions with hook
suppressions); suppress that one explicitly where the code explains it.
* test(tab-bar): use typed tab/file fixtures instead of @ts-expect-error partials
* chore(lint): fix misc suppressed lint violations instead of disabling
- consistent-type-imports: the "vi.importActual requires inline import()"
rationale was wrong; use typed vi.mock(import(...), importOriginal) or a
type-only namespace import.
- no-control-regex: source-control commit/push failure summaries, shell PATH
hydration, and ipynb ANSI parsing reuse the shared ansi-escape-sequences
stripper and control-character pattern instead of private copies.
- no-explicit-any / no-useless-spread / no-this-alias: typed `this` in
bridge test mocks, drop blanket file-level disables, drop a this-alias.
* refactor(main): replace avoidable type assertions with checked narrowing
17 production cast suppressions in main/wsl-guest removed: untrusted IPC
and RPC payloads (Jira issue update, WSL guest plugin install, handshake
type) now narrow through isRecord/type guards and build typed results;
redundant casts dropped; hand-rolled object checks and errno casts use the
existing isRecord / hasErrorCode helpers.
* refactor(renderer,mobile): replace avoidable type assertions with checked types
13 production cast suppressions removed: RPC failures read through the
`ok` discriminant instead of casting to RpcFailure; react-native-web host
refs narrowed with instanceof HTMLElement; redundant reply/diff casts
become checked annotations; the TUI agent param uses z.custom(isTuiAgent)
like its sibling schemas; the agent-status slice cast that hid missing
actions is gone so the creator's return type checks the whole slice.
* chore(lint): drop dead tiptap wrapper; scope anti-slop mocking rule to test support
- safe-react-node-view-renderer patched handleSelectionUpdate, which
@tiptap/react 3.31.3 no longer has, so it always returned early. Use the
stock ReactNodeViewRenderer.
- The anti-slop test override only matched *.test.{ts,tsx}, forcing
file-level no-module-mocking disables in test harnesses and fixtures.
Match *-test-harness, *-test-fixture(s), .test-fixture and *.test.mjs,
then drop those disables and the now-redundant per-file exemptions.
* test: replace repeated cast suppressions with typed fixtures (main/shared/e2e)
Net -119 cast directives. Typed fixtures replace recurring casts:
unusedSshConnection/unusedOrcadHostTransport placeholders, a typed
readPersistedState harness read (exposed a test treating optional
httpProxyUrl as always set), real Response objects in fetch spies, an
updater main-window fixture, a CDP webContents adapter, one shared
releaseModuleExport for cross-version tests, and requireOverlayDir in
place of `as string` on possibly-undefined overlay paths.
* fix(runtime): validate loaded device rows and host scope instead of casting
Two cast suppressions claimed checks that did not happen:
- device-registry-file-read: "normalizeLoadedDeviceRow re-validates every
field" was false; deviceId, token, name and timestamps were never
checked. Rows without an identity are now dropped and the file is set
aside through the existing corrupt-file path (registry reads as
doubtful, so no revocation verdicts); a non-string token is stripped
so the row stays listed but cannot authenticate.
- client-ui: "only optionality differs" was false; any string reached
persisted workspaceHostScope. Narrow it with isWorkspaceHostScope and
ignore unknown scopes (wire schema unchanged for mixed versions). The
remaining cast now states what it actually covers.
* test(runtime,ipc): drop redundant cast suppressions; typed runtime store fixture
-98 cast directives across runtime/ipc tests: 61 casts that typecheck
without the assertion; a typed vi.fn for scoped invalidation; a
partialRuntimeStore fixture in place of `store as never` for
OrcaRuntimeService; typed worker terminal/authority builders in the
incarnation-fallback suite (the old stub lacked runtimeId, ptyId,
worktreeId and launchTokenHash) plus removal of stacked duplicate
directives there.
* chore(lint): typed vi.mock imports and explicit listener snapshot in shared-bucket tests
Remaining files from the misc-rule pass that overlapped other buckets:
codex-accounts mocks use the typed vi.mock(import(...)) form instead of
a consistent-type-imports disable; the daemon late-exit fixture copies
its listener set with Array.from instead of a no-useless-spread disable.
* refactor(github,mobile): narrow routing settings type; drop remaining avoidable casts
- work-item-routing cast a one-field object to AppState['settings'] under
a claim that readers only consume activeRuntimeEnvironmentId. Return the
existing RuntimeFocusSettings pick instead and narrow the cache-key
chain's parameters, so the compiler enforces that claim.
- mobile file preview reads refusals through the RpcResponse `ok`
discriminant rather than a cast.
- terminal document host-notify accepts `unknown` errors, removing the
message-bridge cast.
- native chat readability drops a redundant repo-list cast.
* test(renderer): replace repeated cast suppressions with typed fixtures
-140 cast directives (and ~150 unsuppressed casts of the same shapes):
- mockPanePtyArgs owns the connectPanePty fixture casts, so a wrong
argument order or fixture is now a type error; drop binding casts for
members PanePtyBinding already declares.
- Seed AppState tab rows with the typed makeTerminalTab; this exposed a
split direction 'row' that is not a TerminalPaneSplitDirection.
- Partial GlobalSettings use getDefaultSettings overrides; Jira-only
stores use createTestStore; runtime environment rows/statuses come from
a typed paired-runtime-environment fixture.
* chore(lint): drop no-module-mocking disable now covered by the test-fixture glob
* test(e2e): fix all 221 grandfathered e2e type errors; baseline to zero
The e2e typecheck baseline grandfathered 221 errors in 119 files. About a
third were tests drifted from the current API, several of which would
fail or mislead at runtime:
- terminal-shortcuts called a nonexistent xterm buffer method;
- the daemon-lifecycle fake lacked terminateOwnedTree, hidden by importing
its type from a module that does not export it;
- arguments in the wrong position (listProcesses(hostId, opts), a dropped
pty.write inputKind, an extra toWebTerminalSurfaceTabId argument);
- removed enum values ('drawer', 'idle', 'editor',
'session-insecure-origin') and a launch_source the telemetry schema
rejects;
- missing required fields (turnScope, childWorkOpen, providerIdentity,
reviewLookupOutcome, runtimeServiceAccount behind an `as RelayConfig`);
- dead reads (tab.customTitle, bucket.targets, changedPixels).
Also lifts variables that narrowed to `never` after poll-callback
assignment, and adds a typed xterm render-dimensions accessor. Includes
the cross-version suites' shared releaseModuleExport adoption.
* chore(typecheck): gate the now-clean e2e typecheck in local lint
The e2e baseline is empty, so the ratchet now rejects any tests/ type
error. CI already runs it via run-ratchets; add it to pnpm lint's
explicit ratchet list and replace the stale 'not yet clean' note.
* test(mobile): typecheck every mobile test; baseline 105 files -> 0
mobile/tests-typecheck-baseline.txt grandfathered 105 test files (531
errors) whose types CI never checked. Contract changes had landed without
these tests following, and type pins in them silently checked nothing.
- Root cause: salvagedOptional (src/shared/zod-salvage.ts) cast away
zod's optional marker, so every reply type built on it claimed
salvaged fields were always present. Return a real ZodOptional;
runtime is unchanged (the outer optional short-circuits undefined).
- Production now takes the port it uses: RpcOperationSender instead of
RpcClient for send-only callers, a narrow fetch signature, and only the
request fields push-receive reads.
- findAllByHostType/findByHostType replace `'Text' as never` lookups;
stubRpcClient gives tests a typed RpcClient.
- Real test bugs: settings-host-client-lifecycle "relay" hosts were
direct hosts (relayHostId no longer exists); two toEqualTypeOf pins
could never hold; a recovery hook ran with an undefined scope key;
a create-PR test cast to a never-imported type; "no URL" assertions
passed on null; plus stale required props, args and enum values.
* test(e2e): parse workerStart result instead of casting it
* test(runtime): typed partial runtime stores with production-shaped fixture data
Route every partial store passed to OrcaRuntimeService through
partialRuntimeStore with typed row builders, removing 60 cast directives
that hid fixture data production never produces:
- paired-client-navigation-test-harness keyed setWorktreeMeta writes by
`[object Object]` (it took an id where the store takes a target), so
reads never saw them;
- invalid worktree lineage origin/capture and incomplete workspace lineage;
- getWorkspaceSession returning null where the store returns the default
session; getGitHubCache returning undefined;
- orcad migration states missing migrationId/manifest/receipt, with a
toEqual pinned to that incomplete shape;
- partial repos, metadata, folders, project groups and notification rows.
* refactor(renderer): resolve suppressed React hook lint instead of disabling it
109 of 187 react-hooks/react-doctor suppressions fixed:
- ~95 exhaustive-deps disables only hid stable refs and setters passed
through controller objects; list them (identity never changes, so
nothing re-runs), plus a few real stale closures (tabId/worktreeId).
- Effects keyed on one value that read the latest props now use
useEffectEvent (WebConnect, skill freshness, Bitbucket dialog seeding,
Landing preflight, automation catalog, structured-session recovery,
file explorer).
- The conditional layout-owner hook is selected once at module load from
the build constant instead of a rules-of-hooks disable that also hid
the whole controller from compiler and react-doctor rules.
- A hook disable was masking a react/purity Date.now() in the palette's
recent-tabs memo; sample it when the snapshot is captured.
- Three identical debounced-search effects share
useTaskSearchDebouncedApply; layout snapshot merge helpers move out to
stay under max-lines.
Remaining suppressions are mount-once resources, invalidation epochs,
deliberately keyed fields, and test harnesses that mock the dispatcher.
* chore(ratchet): exempt test harnesses and fixtures from the focus-read ratchet
The ratchet skipped *.test-fixture.ts but scanned *-test-harness,
*-test-fixture(s) and *.test-support files. Those seed settings with
getDefaultSettings (which the ratchet discovers as a focus reader) and
never route host work, so 17 baseline entries were false positives, and
replacing a test's `settings: {...} as never` with typed defaults tripped
it. Match the repo's test-support names and prune (which also drops one
stale entry), and rename the frame-ordering harness to *-test-harness like
its siblings. A blanket "-harness" match would also exempt
e2e-terminal-pty-harness, which production imports, so the names stay
explicit.
* test: merge duplicate shared/constants imports
* revert(file-explorer): restore original tree load effects pending SSH e2e investigation
* test(runtime): drop RuntimeStore cast now that the fixture is typed
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(github): reject cached same-branch PRs with foreign heads
* fix(github): keep same-repo cached PRs behind renamed remote URLs
A stale origin URL after a repository rename or transfer made the
foreign-head check reject the checkout's own same-repo PR and clear its
cache. Treat a PR whose head matches its canonical base URL as local.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(github): PR conflict previews fetch the base branch through the fetch controller
The conflict summary on a PR card spawned its own `git fetch origin <base>`,
outside RuntimeRemoteFetchController: it refetched a base a worktree create
had just refreshed (and vice versa), ran git's auto-maintenance in the
background, and never armed the idle loose-ref sweep.
It now asks the runtime's controller for the exact-base refresh creates
already use (same argv, per-remote queue, dedupe and freshness), waiting at
most the old 10 s before falling back to the local ref. The runtime registers
its controller at construction, since the IPC and poller paths have no
runtime handle.
Also corrects the controller comment that claimed every fetch skips
auto-maintenance: full fetches keep it on purpose (#8039).
* test(github): read the mocked fetch's WSL option without a type assertion
* test(github): give the conflicting fallback-lookup test a git mock for its conflict summary
* test(github): check WSL routing without duplicating fetch policy
* fix(git): bound preview fetch ownership without cancelling create
* Preserve repository identity after preview cancellation
* feat(ci): ratchet raw repo connectionId host reads; one drop-in resolver
Codex review 6 class fix (#4/#6/#7): host decisions read off a repo row's
legacy connectionId miss rows that name SSH only as executionHostId.
- getRepoRoutingConnectionId (shared/execution-host.ts): drop-in for a raw
repo.connectionId routing check that also finds executionHostId-only SSH.
- check-repo-host-read-ratchet: per-file count of raw repo connectionId
reads in src/main, src/shared, src/cli; counts may only shrink.
- Migrated 66 cheap reads: runtime GitHub/GitLab commands, PR/MR head ref
fetches, preserved-branch cleanup, ref queries, base-ref IPC, worktree
preparation, trash sweep, scan cache TTL, usage metadata.
* fix(ipc): PR base IPC routes executionHostId-only SSH rows; ratchet is a stated lower bound
Review follow-up (Opus on #31096):
- worktrees:resolvePrBase decided git/gh routing on the raw connectionId while
its head-ref fetches already used the resolver; one resolved connectionId
now drives all of it (test fails on main).
- getRepoRoutingConnectionId's contract is stated exactly: never local when the
raw field is set; on a contradictory ssh row executionHostId wins.
- The ratchet regex no longer matches report.connectionId; header and docs
say it is a lower bound.
- Regression tests for a runtime GitHub and GitLab command site.
* test(ci): pin the repo host-read ratchet step to the static phase
* fix(ci): repo host-read ratchet skips verbatim field copies into projected rows
A copy of connectionId into a projected row decides nothing; migrating it to
the resolver would change what the projection publishes. The sidebar feed
catalog's copy (added after the baseline) and two older projections no longer
count; baseline pruned to 91.
* The workspace you're on disappearing lands you where a sidebar delete does, whatever removed it
When the active workspace left the list for any reason other than a
successful delete from one of Orca's delete flows (a delete that partly
failed, git worktree remove, the orca CLI, another client, a workspace
opened while its own delete ran), Orca showed the empty "Select a
workspace" screen. The successor was chosen only by per-flow committers
prepared before each delete.
One App-lifetime store subscriber now remembers the last listed active
row and, once the selection is empty and that row has left the list,
focuses the same successor a sidebar delete picks: the most recently
visited other workspace in the project, else its main workspace, on that
row's own host. A selection the user cleared stays cleared; a process
exit's clear is marked automatic. The per-flow committers are removed.
Tracked in STA-8872.
* test: drop type assertions from the removal successor test
* fix: preserve selection intent across workspace removal
GitHub orgs using an SSH certificate authority get an SCP remote whose user is
org-<id> rather than git. The three GitHub remote parsers only matched a
literal git@ user, so those repos were classified not-github and lost PR,
issue and check support.
Co-authored-by: Yi-111-a <yi-111-a@users.noreply.github.com>
* fix(worktrees): skip Git sweeps for missing repo folders
* fix(worktrees): skip missing roots in headless scans
* fix(worktrees): check missing roots at Git listing boundary
* test(git): treat branch removal fixture as existing repo
* test(git): keep mocked worktree roots present for Git cases
* test(git): await host preflight in scan sharing fixtures
* revert(worktrees): drop the listing pre-check for missing repo folders
Reverts 82ccf05a4d, 13854fd3dc, c19604012a, 3f1b7ef64c, a868225608
and 962d9218c2. The reader-level stat ran in the wrong WSL distro, added
a wsl.exe spawn per listing, and only covered git worktree list. The next
commits replace it with a gate at the shared Git runner.
* fix(git): stop spawning Git in deleted native project folders
A registered project whose folder was deleted spawned a failing Git
process (spawn git ENOENT) on every sweep: git worktree list and the
git remote probes. The shared async Git runner now remembers, in memory,
a native cwd once a spawn ENOENT is followed by a definitive lstat
absence. Later calls for that cwd re-check with lstat before admission
and, while it is still absent, reject with the same spawn ENOENT shape
without starting a process. The entry is dropped as soon as the folder
exists again. WSL routes and UNC paths are never recorded, so their
behavior is unchanged.
The fenced-output readers move unchanged to captured-git-output.ts so
git-exec-file.ts stays under the file-length limit.
* fix(github): skip queued PR refreshes for removed projects
A PR-status refresh queued before its project was removed still ran
and probed Git (git remote) against the removed path. The drainer now
re-checks registration for each entry before any Git or GitHub call,
using the same predicate the refresh IPC entry points use, so every
removal path (sidebar, CLI, phone, closed window) and the rate-limit
re-queue are covered. The skip mirrors the existing not-visible skip,
so there is no new skip reason on the wire.
* fix(git,github): skip missing folders without a span; pure refresh registration
Round-2 review fixes.
- The known-missing-folder check now runs before withGitSpan in both
async runners, so a skipped call starts no process and writes no
git.exec span. The command is resolved lazily, only for a cwd already
recorded, and the git.known_missing_cwd attribute is gone.
- The gate uses stat, which follows symlinks the way spawn's chdir does,
so a symlinked project root whose target is gone is recorded too.
- The Set is re-checked after the stat, so concurrent first failures
record and warn once.
- The PR-refresh drain check is now a pure predicate: is a project still
registered at the candidate's path on its execution host. It reads the
store each time and no longer resolves runtime Git options, which could
throw inside the drain loop. Matching by path and host, not repo id,
keeps a re-added project's coalesced refresh from being dropped.
* refactor(git,github): one wrapper per Git runner; drainer holds its check
Tripwire re-plan cuts, no behavior change except one Windows edge.
- The missing-folder gate is a single wrapper around each exported async
runner (gitExecFileAsync outside the fetch-head lock, and
gitExecFileAsyncBuffer). It decides "native" from the call's inputs
(cwd set, not UNC, and no WSL distro on Windows) for both recording and
checking, so the early command resolution and the second filter are
gone. The span bodies are back to main's code.
- A Windows WSL project's linked worktree that runs host Git on a drive
path is no longer recorded; before, it was recorded but never gated.
- The synthesized error is built from constants (spawn git ENOENT, empty
stdout and stderr). Nothing reads spawnargs, and the buffer runner's
only caller reads code and message.
- The drainer holds the registration check the way the event publisher
holds its outcome observer, and the registration skip shares the
existing not-visible skip.
* fix(github): resolve SSH host aliases for account binding
An origin like git@github-personal:owner/repo kept the alias as the
required gh host, so the Settings account list matched nothing.
* fix(github): keep github.com filter for resolved aliases and resolve in the gh runtime
Real canonical keys carry github.com, so an alias resolving to github.com
must filter to github.com like a direct remote. Resolve via the runtime-scoped
resolveSshConfigHostname (WSL distro aware) and keep Enterprise hosts exact.
* fix(github): expand SSH aliases only for SSH-transport remotes
* fix(github): resolve the SSH alias with its original case
---------
Co-authored-by: orchardprior <342342873+orchardprior@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
* fix(git): keep merge-tree's throwaway trees out of the real object store
The PR conflict summary and the squash-merged branch cleanup both run
`git merge-tree --write-tree` only to read its output, but every divergent
run left unreachable loose objects behind. Run it with a per-call scratch
object directory (inside objects/, alternates pointing at the real store)
that is deleted afterwards, on native, WSL and SSH relay hosts alike.
* fix(git): keep partial-clone downloads and clean up stranded scratch stores
- A partial clone fetches missing blobs during merge-tree; those packs landed in the
scratch store and were deleted, so every check re-downloaded them. Move fetched packs
(index last) into the real store before removing the scratch dir.
- Sweep this app's own scratch dirs older than an hour, once per objects dir per process,
so a crash or failed delete cannot strand one forever in repos where gc never runs.
- Remove with Windows transient-lock retries; the shared core now owns all filesystem
work and host adapters only resolve the objects dir.
* fix(git): quote Windows alternates paths containing ';' and log scratch cleanup failures
* fix(git): sweep a merge-tree scratch dir only when its owner is provably gone
Another Orca process on the same repo (a second instance, dev and release
builds) could delete a scratch object dir still in use once its mtime passed
an hour. Each scratch dir now records its owner (pid, hostname, platform,
Linux pid namespace); the sweep removes it only when that owner shares this
pid space and has exited, or, with no checkable owner, at Git's own two-week
tmp_ expiry.
Also:
- A fetched pack whose later rename fails is rolled back, so the real store
never keeps an index-less pack; a pack already installed is left alone.
- WSL Git gets the /mnt/<drive> spelling of a drive-path common dir (Git
< 2.31 prints it relative, resolved against the C:\ repo path).
- conflict-summary tests no longer create scratch dirs in the cwd.
* fix(git): cap every merge-tree scratch dir at two weeks, live-looking owner included
A reused pid held by a long-lived process could keep a leftover scratch dir
forever in a repo where git gc never runs. The two-week age (git gc's own
tmp_ expiry) now removes any scratch dir; a younger one is still swept only
when its owner shares this pid space and has exited.
* fix(git): sweep merge-tree scratch dirs by age alone, off the check's path
- The stale sweep now removes any scratch dir at least two weeks old, Git's
own expiry for its objects/tmp_* dirs; no merge-tree runs that long. The
owner file and pid-space probe are gone, and with them the cases where a
sweeper misjudged another process's pid.
- The sweep no longer delays the user's check: it starts without being
awaited and can only touch dirs far older than the current run's.
- A fetched pack's .keep never moves into the real store, so a transient one
from an aborted lazy fetch cannot stop gc from repacking that pack.
- Tests: the legacy merge-tree fallback gets the scratch env; the local
quarantine's WSL decisions (distro from a UNC path, drvfs spelling,
host-Git linked-worktree skip, WSLENV hand-off).
* fix(git): let quarantined merge-tree see exactly the object stores Git sees without it
The scratch object store must not change what merge-tree can read, or the
conflict list and the squash-merged branch cleanup differ from main:
- An inherited GIT_ALTERNATE_OBJECT_DIRECTORIES (from Orca's or the SSH
relay's environment) was replaced; it is now kept and the real store
appended after it, as Git's own temporary object stores do.
- An inherited GIT_OBJECT_DIRECTORY is the store Git reads and writes, so the
command now runs unquarantined. On the WSL route a Windows value only
counts if WSLENV forwards it, and then the quarantine is skipped too.
- The real store's own info/alternates entries are listed before it, so an
alternates chain is linked at the same depth as without the quarantine
(a 6-deep chain no longer hits Git's nesting limit). An alternates file
that cannot be read or holds a quoted entry runs unquarantined.
* fix(git): run merge-tree unquarantined when the repo's store has its own alternates
Mirroring Git's alternates resolution could not match it in every shape: a
path back to the real store (mutual alternates, or an inherited alternates
value naming the repo itself) still linked its chain one level deeper, and a
NUL byte in info/alternates broke the spawn. When objects/info/alternates
exists, or cannot be checked, the command now runs exactly as before. Repos
cloned with --reference or --shared keep writing throwaway objects, as they
always have. Inherited alternates are still appended to, and an inherited
GIT_OBJECT_DIRECTORY still skips the quarantine.
* refactor(git): drop the stale-folder sweep and pack-move rollback from merge-tree's scratch store
A scratch folder left by a crash is a tmp_* dir inside objects/, which
Git's own gc prunes after gc.pruneExpire, as it does Git's own leftovers,
so Orca's 14-day sweep and its test hooks go.
Packs a partial clone fetched into the scratch store are moved in Git's
migration order: pack- files only, index last, existing names left in
place, stopping at the first failed rename with no rollback. A failed
move is logged and never stops the scratch dir's removal or the result.
Adds a real-Git test that a partial clone lists its conflicts offline
once an earlier check downloaded the contents.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(git): quarantine merge-tree only once the host is known to support --write-tree
Git before 2.35 cannot prune a leftover scratch folder under objects/tmp_*, so
a crash in the moment one exists makes every later `git gc` warn and stops
automatic cleanup. On Git before 2.38 merge-tree --write-tree is rejected, yet
the first check per host per session still made and removed a folder around it.
Branch cleanup and the PR conflict list now use the scratch object store only
when that host's capability cache already knows merge-tree --write-tree works.
Until then the command runs exactly as it does on main: on a Git that
rejects it no folder is ever created, and every Git that does get one is 2.38+,
whose gc prunes it. Also reword two comments in the quarantine module.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(git): learn merge-tree support from a conflicting branch-cleanup check
Branch cleanup quarantines merge-tree only once the host's capability cache
knows --write-tree works. A conflicting merge-tree exits 1, so the check threw
and support was never recorded; on a host whose only checks conflict (likely
over SSH, where the relay has no conflict summary to teach its cache) every
check kept writing loose objects into the real store.
A failure whose stdout starts with a tree ID proves --write-tree ran (old Git
that rejects the option prints nothing there), so it now resolves to the same
"not merged" result and lets the cache record support. Every other failure is
rethrown unchanged. Also shorten two comments and soften a third.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(git): put WSL Git's scratch object store on its command line
A WSL login profile that set GIT_ALTERNATE_OBJECT_DIRECTORIES replaced the
WSLENV-forwarded value, so a quarantined merge-tree lost the real store and
failed where main succeeds. The WSL runner now passes both variables as
command-line assignments after the profile, appending to its alternates.
* test(git): drop quarantine tests the real-Git and compatibility tests already pin
The cold-capability gating, scratch-folder lifecycle and WSL path spelling
are covered end to end by merge-tree-object-quarantine-real-git.test.ts, the
real-binary compatibility contract (Git 2.25/2.38/2.49) and the
localGitObjectsDirectory tests, so the unit copies only mirrored them.
---------
Co-authored-by: Claude <noreply@anthropic.com>