Repository navigation
fix(agent-manager): report worktree problems accurately and recover from them - #14137
Conversation
…rom them Agent Manager could tell a user "Git is not installed or not found in PATH" while git was installed and working, show a worktree as having no changes when the status check had actually failed, and keep polling worktrees whose folder was long gone. One unresponsive worktree was enough to slow down status updates for every other row. Three causes, all of them the same mistake in different places: a failure was reported as a fact about something else. - A failed process launch reports ENOENT whether the program or the working directory is missing, and the code read that as "git is missing". - A failed or timed-out status check returned zero counts, which is indistinguishable from a clean worktree. - A timed-out GitHub CLI call was recorded as a success, which reset the backoff that was supposed to stop retrying it. Nothing reconciled the three views of a worktree either (the row, git's own registration, the folder on disk), so stale rows accumulated and were polled forever, and a timed-out `gh pr view` was retried on every cycle. Approach: name each state and never guess between them. A worktree is healthy, restorable, gone, not-a-worktree, or unmeasurable, and each state gets the message and the recovery action that actually applies. Startup reconciles the three views and repairs metadata only; deleting files is always a user's click. Repeated failures park one worktree instead of the whole panel, and git/gh calls get budgets sized to the work they do. Both clients implement the same states, wording, and recovery actions.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge The single outstanding finding from the previous review is resolved in Files Reviewed (2 files)
Previous Review Summaries (12 snapshots, latest commit b6c9e99)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit b6c9e99)Status: 1 Issue Found | Recommendation: Address before merge Incremental review of Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (4 files)
Fix these issues in Kilo Cloud Previous review (commit 68ef839)Status: 2 Issues Found | Recommendation: Address before merge Incremental review of Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (6 files)
Fix these issues in Kilo Cloud Previous review (commit 664be7d)Status: No Issues Found | Recommendation: Merge Incremental review of Files Reviewed (5 files)
Context (no action requested): the three renamed keys ( Previous review (commit dead6df)Status: 5 Issues Found | Recommendation: Address before merge Overview
Incremental review of Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (12 files)
Fix these issues in Kilo Cloud Previous review (commit 6624db6)Status: No Issues Found | Recommendation: Merge Incremental review of Files Reviewed (3 files)
Previous review (commit 1459d81)Status: 1 Issue Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)SUGGESTION
Files Reviewed (8 files)
Incremental review of Previous review (commit 9e6bdf2)Status: 3 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)SUGGESTION
Files Reviewed (4 files)
Incremental review of Previous review (commit 8100652)Status: 2 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)SUGGESTION
Incremental review of Files Reviewed (52 files)
Previous review (commit aabe219)Status: No Issues Found | Recommendation: Merge Incremental review of Files Reviewed (3 files)
Previous review (commit 23bc385)Status: No Issues Found | Recommendation: Merge The previous Files Reviewed (3 files)
Previous review (commit 4075601)Status: 1 Issue Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Files Reviewed (51 files)
Note: this incremental pass is against Previous review (commit 91040bc)Status: 17 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)CRITICAL
WARNING
[Snapshot truncated.] Additional previous summary content was truncated to keep this comment within platform limits. Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0 Review guidance: REVIEW.md from base branch |
Follow-up to the worktree-health PR, fixing the cases where an indeterminate answer was still treated as a definite one. - `removeOrphanDirectory` failed open: an unanswerable `git worktree list` skipped the live-worktree guard and recursively deleted the path. It now refuses. - `applyPresence` used `some` to sync branches, so only the first drifted worktree was synced per presence tick. - The reconcile drop loop re-reads the session count: `prune` is awaited after classification, and a session attached in that window would have been deleted with the row. - `unavailable` is no longer treated as broken. It paused stats/PR polling for the rest of the session, rendered as a warning badge, and offered the entry-dropping removal actions for a worktree that is probably fine. A presence probe now also re-schedules a reconcile while the last report is degraded. - Git and gh timeouts are classified from the killed/signal shape rather than from message text, so a watchdog kill no longer surfaces as a raw "Command failed" string. - A PR strategy ladder that only timed out now reports the failure instead of falling through to "no PR", which cleared the quarantine and rendered a wedged `gh` as a clean worktree. - The PR loop counts rejections that are not attributable to already quarantined worktrees, instead of comparing two unrelated totals. - JetBrains: `stop()` cancels the stats/dirty jobs, an empty list from a failed `git worktree list` no longer clears every badge, a TIMEOUT no longer consumes the one-shot gh notification, the diagnostics clipboard write moved to the EDT, `git fetch`/`worktree add` got their own wider budget, and a timed-out review-thread query no longer reads as "every conversation settled". - Added the three missing `agentManager.setup.error.*` keys in all 21 dictionaries; they were rendering as literal keys.
CI's Linux (ext4) `fs.readdir` order is not alphabetical the way it tends to be on macOS (APFS), so `reconcileWorktrees`' orphan list came back in a different order than the "reports untracked directories as orphans without deleting them" test expected, failing only on Linux CI. Sort worktreeDirs() so both the orphan report and the deterministic slug-suffix scan get a stable, environment-independent order.
KiloWorktreeService.stats/dirty caught every exception and returned a plain WorktreeStatsListDto()/WorktreeDirtyListDto(), whose unavailable default is false. A thrown RPC failure (or a stop()-triggered CancellationException, unlike open()) therefore looked identical to a successful "no worktrees" answer one layer up, so WorktreeStatusService.merge's drop=!dto.unavailable cleared every stats/dirty badge on exactly the failure this flag was added to guard against. Rethrow CancellationException before the generic catch, and report unavailable=true from it, matching the pattern open() already uses. Covered by "an RPC exception keeps every badge instead of clearing them", which drives a thrown FakeWorktreeRpcApi.stats/dirty through the real KiloWorktreeService catch path and WorktreeStatusService.merge.
|
@kilocode-bot can you investigate with multiple subagents all previous changes that where recently done to gh, worktree, staleness and worktree pooling. Check if any of those changes conflict or introduce bugs in combination with this change. |
Cross-cutting review: this PR against recent worktree / gh / pooling workInvestigation only, no code changes. I read the full diff of this PR on both clients, then walked the last six weeks of history for adjacent work (gh usage, worktree creation/removal/pooling, staleness, backoff, polling) and looked for double-handling, conflicting backoff strategies, paths still assuming the old semantics, and cross-client divergence. Related recent changes
FindingsF1 -- High -- the batched PR path bypasses everything this PR adds. Since 81b9c21 a full sync resolves every worktree through seed() in pr/am-pr-seed.ts, and fetchOne only falls back to fetchPRForBranch for worktrees the batch left unresolved. That batch path uses its own budget of 20000 ms (PRStatusPoller.host(), "gh: (args, cwd) =: this.gh(args, { cwd, timeout: 20_000 })"), contradicting BUDGET.gh = 10000 in command-budget.ts. It never classifies a timeout (isTimeout is not used) and never records a quarantine failure, because chunk() at am-pr-seed.ts:105-113 logs and returns and the fetchAll .catch swallows the rest. It also ignores isUnhealthy and quarantine.blocked entirely: one() at am-pr-seed.ts:71-85 gates only on existsSync(wt.path), so an unregistered or quarantined worktree still gets a "git rev-parse HEAD", is still put into the chunk, and items[0].cwd (possibly that same worktree) becomes the cwd of the whole "gh api graphql" call. Net effect: the headline symptom, a hanging gh costing a full timeout on every poll with no explanation, is still reachable on what is now the primary code path. The hardened ladder only runs as a fallback. F2 -- High -- the diff targets a pre-batch fetchAll and needs a rebase. The hunk replacing "const ok = results.every(...)" sits in a function that has since gained the seed step. The rewrite itself survives a rebase (index alignment holds, settled() at PRStatusPoller.ts:861-877 writes results[i] by input index), but please re-check one interaction: empty() now calls quarantine.clear(worktreeId), and a batch-derived "seeds.set(item.id, null)" at am-pr-seed.ts:129 reaches empty(), so a batch answer clears a quarantine the fallback ladder deliberately keeps armed. F3 -- Medium -- reconcile can report live pool slots as orphaned directories. reconcileWorktrees takes its registered() snapshot first, then performs per-row exists/branchExists awaits, and only afterwards lists dirs(). Neither call holds the git lock. WorktreePool.fill/build (worktree-pool.ts:135-174) creates new slot directories under the same .kilo/worktrees/ while holding that lock, and WorktreeManager re-warms on a timer after every claim (WorktreeManager.ts:291). A slot created inside that window is not claimed, is absent from the stale registered set, and has a .git file, so it is classified as an orphan of kind "broken", shown in OrphanNotice, and counted in the new "Agent Manager Worktree Health" telemetry. Clicking Clean up leftover folders then fails with "Refusing to remove a live worktree" from removeOrphanDirectory. The fail-closed re-check does its job so nothing is lost, but the user sees a phantom problem and an error. This is not only a startup race: reconcileProject also runs from the presence-driven HealthScheduler and after every recovery action, concurrently with pool warm-ups. Fixes to consider: list dirs() before registered(), re-read registration for candidate orphans only, or skip directories whose pool metadata says pooled (worktreeInfo already reads that metadata). F4 -- Medium -- recovery does not clear the PR quarantine. restoreWorktree in worktree-recovery.ts clears ctx.stale and re-reconciles, but nothing clears the PRStatusPoller quarantine for that id, so after a successful restore the worktree can stay parked for up to QUARANTINE_MAX (30 minutes) even though the user just fixed it. That is the same "badges stop updating and never recover" experience this PR targets. PRStatusPoller.refresh(id) already clears the quarantine, so call it (and invalidate the stats cache) from the recovery paths. F5 -- Medium -- the orphan cleanup wording is wrong for "broken" orphans, and there is now a two-click path to deleting real files. reconcile distinguishes "broken" (still has a .git file, an ex-worktree that can hold uncommitted work) from "leftover" (a bare directory), but healthPayload sends only orphans.map(o =: o.path) and agentManager.orphans.confirm reads "Delete these folders permanently? Nothing here is tracked by git." For a broken orphan that is not true. Combined with the new flow (unregistered row, then Remove keep sessions, which turns the directory into an orphan, then Clean up leftover folders) this is two clicks from permanently deleting a checkout with uncommitted changes. Recommend sending kind, listing broken entries separately with different wording, or excluding them from bulk cleanup. F6 -- Medium -- the new removal action for unregistered can dead-end. removeStaleLifecycleWorktree now accepts a worktree because the health report says it is broken, which includes unregistered, a state where the directory still exists. The untouched PTY cleanup failure branch at provider-lifecycle.ts:362-377 only bypasses cleanup when lstat reports ENOENT, so for an existing directory it posts "Failed to stop terminals before removing the stale worktree" and returns. If acquirePtyCleanup throws for an unregistered worktree, the only action the UI offers fails with a message unrelated to the actual problem. F7 -- Medium -- JetBrains: the poll-driven "git worktree prune" is not under the per-repo mutation lock, and this PR widens its blast radius. reconcile() now backs list() as well as stats() and dirty(), and it runs "git worktree prune -v" whenever staleWorktrees is non-empty. create, import, remove, rename and adopt all serialise through lock(base, op) at KiloWorktreeRpcApiImpl.kt:451-463, reconcile does not. staleWorktrees excludes trash-doomed paths but not a worktree mid "git worktree add", whose registration exists before the checkout completes, so the most frequent poll can now prune an in-flight add. The VS Code side deliberately wraps its prune in withGitLock (WorktreeManager.pruneWorktrees), so the two clients now disagree on the safety rule. Recommend taking the same mutex, or skipping the prune while it is held. F8 -- Medium -- JetBrains: "git status" was moved onto the 5s probe budget. PROBES in GitComparison.kt includes "status", so the working-tree scan behind dirty() drops from 30s to GIT_PROBE_TIMEOUT_MS. A status is a content scan, not a metadata query, so on large or cold worktrees this converts previously successful measurements into unavailable rows. The same list includes "worktree", so the reconcile prune runs on 5s even though the GIT_WRITE_TIMEOUT_MS docstring claims worktree prune uses the write budget. F9 -- Low/Medium -- VS Code paths still on the old semantics, in files the PR did not update. WorktreeManager.fetchPRInfo (WorktreeManager.ts:1284-1307) keeps a hardcoded 30000 gh timeout and calls classifyPRError(msg) with no isTimeout, so a timed-out PR import is still reported as an unexplained failure, while JetBrains got an explicit GhAvailability.TIMEOUT guard for the equivalent importPr flow in this same PR. WorktreeManager.hasWork (WorktreeManager.ts:327-331) still fails safe to "no work" on any git error, the same "a failure reported as a fact about something else" pattern removed elsewhere (impact limited to auto branch naming). prepareWorktreePath (WorktreeManager.ts:519-528) and WorktreeStateManager.findWorktreeByPath (161-163) still compare with the lexical normalizePath rather than the new pathKey, so the macOS symlink and case-folding class of bug fixed in reconcile survives in those comparisons. F10 -- Low -- Quarantine.blocked() mutates, and a diagnostics run consumes the retry token. blocked() sets entry.until = 0 once the window has elapsed, PRStatusPoller.paused() calls it for every worktree, and paused() is what the new diagnostics command reads, so generating a report silently releases expired quarantines. Harmless today, but a report-generating getter should not have side effects. Consider a non-mutating peek(). F11 -- Low -- cross-client state semantics still differ. The description says both clients implement the same states, the same wording and the same recovery actions. On JetBrains there is no absent-restorable, absent-gone or unregistered classification, no Restore, no Remove keep sessions, and orphans are report-only, while rows whose directory is missing are still silently dropped by live(managedWorktrees(all).filter { Files.isDirectory(...) }). Only unavailable plus reason is genuinely shared. Suggest softening the claim and tracking the JetBrains gap as a follow-up. F12 -- Low -- failure containment is only half implemented on the VS Code stats path. Quarantine is wired into PRStatusPoller only. GitStatsPoller gained the isUnhealthy filter, but unavailable is (correctly) excluded from broken, so an unmeasurable worktree keeps getting a full status plus diff fan-out every cycle on the new shorter budgets, and stats failures still fall back to prior with no per-worktree backoff (GitStatsPoller.ts:264-266). Worth saying explicitly: removeOrphanDirectory failing closed on an unanswerable worktree list, the re-read of the session count before drop, pathKey canonicalisation, formatLog, and the unavailable-is-not-broken distinction are all the right calls, and the reconcile and pool logic is safe in steady state. The findings above are windows and adjacent paths, not the core design. Recommendation
Built for marius-kilocode by Kilo |
|
Investigation complete — full findings are posted here: #14137 (comment) Key points for @marius-kilocode:
Recommendation: address F1/F2 before merge (it's the exact bug this PR is meant to fix, just on the other code path); F3/F5 should at least get a follow-up issue; F7/F8/F11 are tracked as JetBrains gaps. Full report has file/line refs and all 12 findings. |
…tree A worktree deleted while a status poll is in flight fails before gh runs: the platform reports `Cannot start a process, the working directory '...' does not exist`. That message is captured as the command's stderr, and richRefusal() matched its bare "does not exist" as a rejected --json field name, so PrResolver latched `rich = false` (and, one call later, `threads = false`) for the life of the backend. One resolver serves every checkout, so a single deleted worktree stripped reviewDecision, statusCheckRollup, mergeable, and the review-conversation count from every row in the IDE. Rows kept their PR number and silently lost their approved, checks, and conversation badges until the IDE was restarted — observed in the wild for 17h, across an approval that never appeared. badDir() already recognises both shapes of this race, so richRefusal() consults it before classifying anything: a directory that vanished says nothing about what this gh can answer.
Twelve findings from a review of this PR against the recent gh, worktree, staleness, and pooling work. The theme is that the hardening landed on the paths this PR touched, while adjacent paths kept the old semantics. VS Code: - The batched GraphQL lookup became the primary PR path after 81b9c21, and it bypassed everything this PR added: a 20s budget instead of BUDGET.gh, no timeout classification, and no quarantine or health gate, so a parked worktree still cost a git process per full sync and could become the working directory of the whole `gh api graphql` call. It now runs on the shared budgets, skips parked worktrees, and reports a timeout as a timeout instead of retrying it as an unsupported field. - Orphan reporting re-checks git registration before naming a directory, so a worktree the pool created mid-pass is no longer offered for deletion, and an unanswerable re-check reports nothing. - Restoring a worktree clears its PR polling backoff, instead of leaving the worktree the user just repaired parked for up to half an hour. - The orphan kind reaches the webview: a directory that still holds a checkout is marked and gets a confirmation that says it may hold uncommitted work, rather than "nothing here is tracked by git". - Removing a stale entry no longer dead-ends when terminals cannot be stopped. Nothing on that path deletes files, so for an unregistered worktree the only action offered used to fail with an unrelated error. - PR import runs gh on BUDGET.gh and reports a hang as a timeout. - findWorktreeByPath and prepareWorktreePath compare with pathKey, so a symlinked parent or a case variant cannot hide a registration. - Quarantine gained a non-mutating peek, so generating a diagnostics report no longer releases the quarantines it is reporting. - The stats loop gained the same per-worktree backoff as the PR loop; an explicit refresh still measures a parked worktree. JetBrains: - A poll prunes stale worktree metadata only when no mutation holds the repository lock, and never waits for it. `git worktree add` registers a worktree before its checkout is written, so pruning underneath one could delete the metadata of a worktree being created. - `git status` is off the 5s probe budget: it scans the working tree, so on a large or cold worktree the probe budget reported unavailable for a measurement that would have succeeded. The sync prune uses the write budget its docstring already claimed.
|
Addressed all twelve findings in Checks: F1 — the batched path bypassed everything
Covered by "leaves a parked worktree out of the batch entirely" (asserts no git call in the parked directory and that the gh cwd is the healthy one) and "does not retry the batch after a timeout, even when the output reads like a refused field". F2 — rebase and the
|
# Conflicts: # packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx
The merge left AgentManagerApp.tsx 11 lines over its 2800-line lint cap: main had grown it to 2799 and this branch adds the health props and the error toast. Rather than raise the cap, both failure-reporting branches move to failure-toast.ts — the PR error code and the plain error message are the same concern, and neither needs anything from the component but the toast, the translator, and the project on screen.
…poll prune Two review findings on the previous commit. The stats backoff had no way out that a user could reach: the bypass its comment pointed at, `snapshot(true)`, is only called from tests, and the recovery path cleared the PR quarantine alone. A worktree parked for a reason the reconcile cannot name — locked index, permissions, unmounted volume — therefore kept its previous badges until the half-open probe at the end of a window that reaches 30 minutes, and the diagnostics report could not say why, because it read only the PR loop. GitStatsPoller gained `revive` (called by the recovery actions) and `paused`, and the report now merges the parked ids from both loops. The poll-driven `git worktree prune` ran on the 180s write budget while holding the repository's mutation lock, so one wedged prune could park a user-initiated create for three minutes. It only rewrites $GIT_DIR bookkeeping, so it now runs on the probe budget: five seconds is the longest a mutation can wait for a poll, and a prune that misses it is skipped — the entry is still stale on the next poll. Also folds the doctor's output-channel writes into the host object it is already given, so the report path is one seam instead of a callback that each caller reimplements.
|
Merged latest The merge also put Both review findings on the previous commit are addressed in Post-merge verification: One note for whoever runs this locally: after the merge, |
The extraction dropped the early return the inline branch had: a `agentManager.prError` for a background project used to stop there, and after the move it fell through to `projectLive.apply(msg)`. Harmless only because that router has no branch for the message today. `reportFailure` now answers "stale" for it — the same contract `routeReview` already uses in this handler — and the call site returns on it, so the control flow is in the code rather than implied by a boolean nobody read.
- Recover automatically when a stale remote-tracking ref blocks a pull request fetch (the exact directory/file ref conflict reported against PR imports): detect it, run `git remote prune origin` once, retry. - Translate known fetch failures (timeout, ref conflict, missing remote ref) into actionable text instead of raw git stderr; unknown failures keep a short diagnostic snippet while full stderr stays in the log. - Reject a pull request URL that belongs to a different repository before spawning gh, both inline in the New Worktree dialog and in the backend import guard, naming both repositories in the message. - listBranches now reports the checkout's origin owner/repo so the dialog can validate without an extra RPC round trip.
Group the worktree row menu into: rename; open in browser; diff actions (show diff against base, show uncommitted changes); copy actions (branch name, branch path, PR reference); setup script actions; delete. Moves Copy PR Reference out of the diff group and into the copy group, and shortens several labels (Show Diff Against Base, Show Uncommitted Changes, Show Worktree Setup, Copy PR Reference).
- Strip refs/heads/ as well as refs/remotes/origin/ when naming the branch that blocks a fetch: the cross-repo pull-ref fetch and the closing `branch --force` both write refs/heads/, so those conflicts were rendered as raw ref paths. - Require a host boundary before github.com so a host that merely ends in it (notgithub.com) is no longer parsed as GitHub, and accept the ssh://…:22/ port form that previously returned null. - Resolve the origin slug when the worktree controller is created, not only on reload, so the dialog's cross-repo check is armed on the chat-dock and worktree-editor entry paths too. - Name the origin remote as a possible cause in the cross-repo message, since the comparison is by remote URL and an origin left stale by a GitHub rename still reaches the same repository. - Replace the case-insensitivity test that fell through to real gh and `git fetch` calls with hermetic coverage of the comparison itself.
The prune recovery matched only git's "exists; cannot create" detail line, which the Linux CI image does not print: the fetch reported just the summary naming `git remote prune` as the fix, so the conflict went undetected and the retry never ran. Match that summary too, and fall back to unnamed wording when git did not name the blocking ref instead of reporting the imported branch as its own blocker. Also allow github.com subdomains when parsing remotes: requiring a scheme or userinfo boundary dropped www.github.com, and a null slug reads as "cannot tell", which silently disabled the cross-repo guard for such a checkout. Splits the origin-slug controller test so reload()'s own assignment stays covered now that construction also resolves it.
The subdomain dot was unanchored, so a path segment ending in
.github.com matched too: parseRepoSlug("https://gitlab.com/team/
x.github.com/owner/repo.git") returned owner/repo. That turns the
cross-repo guard's "cannot tell, skip" into an active comparison
against a slug the checkout never published. Allow subdomain labels
only between a scheme/userinfo boundary and the host.
Why
Agent Manager told me "Git is not installed or not found in PATH" on a machine where git was installed and working fine. Chasing that turned up a cluster of related problems, all of which make Agent Manager look broken while giving the user no way to find out what actually happened:
ghcall hung..kilo/worktrees/piled up invisibly (on my checkout: 60 folders, 8 real worktrees, 24 rows in state, ~15 of them pointing at folders that no longer existed).What was actually wrong
Three variations of the same mistake: a failure was reported as a fact about something else.
ENOENTwhether the program or the working directory is missing. The code assumed the program.On top of that, nothing ever reconciled the three views of a worktree — the row in the UI, git's own registration, and the folder on disk — so drift between them accumulated silently and was polled forever.
How we approached it
Name each state and never guess between them. A worktree is healthy, restorable, gone, not-a-worktree, or unmeasurable. Each state gets the message and the recovery action that actually applies, and "we could not check" is never rendered as "it is fine" or "it is broken".
Self-heal metadata, never delete files on its own. On startup the three views are reconciled: stale git metadata is pruned, and a row is dropped automatically only when its folder, its branch, and its sessions are all gone — nothing can be lost. Everything else is offered as an explicit action.
Contain failures instead of spreading them. A worktree that keeps failing is parked on its own for a while rather than degrading the whole panel. Git and GitHub CLI calls get time budgets sized to the work they do, instead of one long budget shared by everything from a version check to a full diff.
Make the answer findable. Both clients get a diagnostics report with the same sections, so a user can see what git and gh actually did instead of guessing from a log.
Both clients share the parts that decide whether a failure is reported honestly: the same distinction between "broken" and "we could not check", the same wording for it, and the same diagnostics report. The recovery actions are not at parity yet — restore, "Remove, keep sessions", and orphan cleanup are VS Code only, and JetBrains reports orphans without offering to remove them. The two sections below list what each client actually needed; the underlying bugs were similar in kind but different in detail, because the two clients have independent worktree implementations.
Fixed in the VS Code extension
The false "git is missing" message. Worktree errors are now classified with the context the message alone cannot carry, so that text only appears when a git version probe genuinely failed to launch. A launch failure in a folder that no longer exists says the worktree is missing; a broken worktree registration says so; a timeout says it timed out.
Ghost rows that never went away. Startup now reconciles rows, git registrations, and folders on disk. Stale git metadata is pruned, and rows are dropped only when nothing can be lost. A dead code path that would have deleted worktree rows and their conversations on sight is gone.
Recovery actions on the row itself, instead of a dead end:
One broken worktree no longer degrades the rest. Repeated failures park a single worktree (with backoff, cleared by any success or an explicit refresh) instead of feeding the loop-wide backoff that slowed every other row.
PR lookups no longer stall on a hanging GitHub CLI. The lookup form that hangs is demoted to a short-budget fallback, and timeouts are reported as timeouts rather than as a missing
ghinstall.A startup that opened dozens of git processes at once. Discovering worktrees used to launch one git process per folder in parallel — the same storm that makes every command look slow. It now takes a single snapshot from git and probes with a bounded number of processes.
Failures that were invisible. Errors sent to the panel were silently dropped and never shown; the log helper printed
{}for every error it was given, losing the message and the stack. Both are fixed.Also: an installed
git-lfswas ignored until the window was reloaded (a negative check was cached forever), and new "Agent Manager: Show Worktree Diagnostics" command. All new strings are translated in 21 languages.Fixed in the JetBrains plugin
Every git command timing out at 30 seconds — including
git --version. The real cause was on our side, not git's: status polls could stack up on a slow repository, each one fanning out its own set of git processes, until trivial commands could not get through. Polls are now one-at-a-time with a single shared process budget for the whole repository, and commands get budgets sized to their work (a metadata lookup no longer waits as long as a full diff).Git comparison failed (exit=-1)with no explanation. A timeout and a failed process launch both collapsed into the same meaningless message with empty output. A timeout now says it timed out; a real failure reports its exit code and error.Worktree badges silently disappearing. A failed status check returned zeros, which the UI rendered as a clean worktree. Failures are now marked as unmeasured, and rows keep their previous values with an explanatory tooltip instead of quietly reading as "no changes".
PR badges that stopped updating and never recovered. A timed-out
ghcall was classified as success, which reset the very backoff meant to slow it down — so every poll kept paying the full timeout. Timeouts are now their own state: they slow retries down, explain themselves in the banner, and do not raise a notification the user cannot act on. The PR lookup also names the branch first instead of using the form that hangs.Review and CI badges disappearing across the whole IDE. Deleting a worktree while its status was being checked was enough to lose the approved badge, the checks badge, and the conversation count on every row until the IDE restarted. A worktree deleted mid-poll fails before
ghruns, and the platform's "working directory does not exist" message was read asghrejecting the--jsonfield names — a verdict the resolver latches for the whole backend, because aghrelease that cannot answer those fields will never answer them. It now distinguishes a directory that vanished from a field that was refused. Found in a real log: one deleted worktree at 14:17 hid an approval landing 17 hours later.Rows and status disagreeing about what exists. The worktree list and the status polls used to reconcile differently, so a row could exist that nothing would ever report status for. They now share one reconciliation, which also reports leftover folders that git does not track (reported only — nothing is deleted).
Also: new Copy report action in Advanced settings, producing the same diagnostics report as VS Code.
Notes for reviewers
git worktree listreports canonical paths, so on macOS every worktree under a symlinked path (e.g./var→/private/var) compared as unregistered. Path comparison now canonicalizes.Verification
bun run typecheck,bun run lint,bun run knip,bun run compile, 5708 unit tests pass./gradlew typecheck test— BUILD SUCCESSFUL (backend + frontend)kilocode_changemarkers, markdown tables, workflow allowlist, source linksghfield downgrade