Add safe worktree inventory, pruning, and revival - #4742
Conversation
- Add worktree inventory, safety validation, pruning, and reaping - Expose worktree management through server RPC and settings UI - Restore missing worktrees when threads resume
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| }); | ||
| // A pruned (or manually deleted) worktree is recreated from the thread's | ||
| // branch before the provider session starts in it. | ||
| if (thread.worktreePath !== null && thread.branch !== null && project) { |
There was a problem hiding this comment.
🟠 High Layers/ProviderCommandReactor.ts:502
When a thread's worktree directory is manually deleted while a provider session is still active, ensureSessionForThread revives the worktree but then reuses the existing provider session because the path string is unchanged. The provider process keeps its original cwd handle pointing at the deleted directory, so the turn runs against a stale/invalid cwd instead of the revived worktree. The cwdChanged check on line 568 only compares path strings (effectiveCwd === activeSession?.cwd), so it stays false even though the underlying directory was recreated. Track the revived result from worktreeService.reviveWorktree and force a session restart when it is true, so the provider process is respawned with the fresh directory handle.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 502:
When a thread's worktree directory is manually deleted while a provider session is still active, `ensureSessionForThread` revives the worktree but then reuses the existing provider session because the path string is unchanged. The provider process keeps its original cwd handle pointing at the deleted directory, so the turn runs against a stale/invalid cwd instead of the revived worktree. The `cwdChanged` check on line 568 only compares path strings (`effectiveCwd === activeSession?.cwd`), so it stays false even though the underlying directory was recreated. Track the `revived` result from `worktreeService.reviveWorktree` and force a session restart when it is true, so the provider process is respawned with the fresh directory handle.
| const reviveWorktree: WorktreeService["Service"]["reviveWorktree"] = Effect.fn( | ||
| "WorktreeService.reviveWorktree", | ||
| )(function* (input) { | ||
| const exists = yield* fs.exists(input.worktreePath).pipe(Effect.orElseSucceed(() => false)); |
There was a problem hiding this comment.
🟡 Medium vcs/WorktreeService.ts:531
reviveWorktree returns { revived: false } whenever input.worktreePath exists on the filesystem, even when that path is an empty directory, a stale worktree registration, or an unrelated directory left behind by a pruned or manually deleted worktree. The caller treats { revived: false } as "the worktree is ready to use," so it continues with a path that has no valid Git worktree, causing the provider session to fail or run in the wrong directory instead of recreating the worktree.
The fs.exists check at line 531 only confirms the path exists on disk — it does not verify the path is a registered Git worktree pointing at the expected branch. Consider checking the Git worktree registration (e.g. via git worktree list) or verifying the .git worktree metadata before treating existence as "already revived."
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/WorktreeService.ts around line 531:
`reviveWorktree` returns `{ revived: false }` whenever `input.worktreePath` exists on the filesystem, even when that path is an empty directory, a stale worktree registration, or an unrelated directory left behind by a pruned or manually deleted worktree. The caller treats `{ revived: false }` as "the worktree is ready to use," so it continues with a path that has no valid Git worktree, causing the provider session to fail or run in the wrong directory instead of recreating the worktree.
The `fs.exists` check at line 531 only confirms the path exists on disk — it does not verify the path is a registered Git worktree pointing at the expected branch. Consider checking the Git worktree registration (e.g. via `git worktree list`) or verifying the `.git` worktree metadata before treating existence as "already revived."
| } else if (aheadOfUpstreamCount > 0) { | ||
| blockers.push("unpushed"); | ||
| } | ||
| } else if (sync !== undefined && sync.upstreamGone) { |
There was a problem hiding this comment.
🟡 Medium vcs/WorktreeService.ts:386
When sync.upstreamGone is true, the prune-safety check treats the branch as fully pushed and skips all commit-counting, so any local commits made after the remote ref was deleted are never detected. A clean, inactive worktree with unpushed local-only commits is marked safeToPrune and gets auto-removed, bypassing the advertised no-unpushed-commits safeguard.
The else if (sync !== undefined && sync.upstreamGone) branch returns no blocker without comparing the branch against defaultRef. Consider falling through to the aheadOfDefaultCount check (or another rev-list comparison) so post-deletion local commits still trigger unpushed.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/WorktreeService.ts around line 386:
When `sync.upstreamGone` is `true`, the prune-safety check treats the branch as fully pushed and skips **all** commit-counting, so any local commits made *after* the remote ref was deleted are never detected. A clean, inactive worktree with unpushed local-only commits is marked `safeToPrune` and gets auto-removed, bypassing the advertised no-unpushed-commits safeguard.
The `else if (sync !== undefined && sync.upstreamGone)` branch returns no blocker without comparing the branch against `defaultRef`. Consider falling through to the `aheadOfDefaultCount` check (or another rev-list comparison) so post-deletion local commits still trigger `unpushed`.
| concurrency: { | ||
| mode: "serial", | ||
| key: ({ environmentId }) => JSON.stringify([environmentId, "worktrees.prune"]), |
There was a problem hiding this comment.
🟡 Medium state/vcs.ts:218
pruneWorktrees uses the concurrency key [environmentId, "worktrees.prune"], so the shared vcsCommandScheduler only serializes prune requests against other prune requests. Other VCS commands use [environmentId, input.cwd], so a prune can still run concurrently with createWorktree, removeWorktree, switchRef, etc. in the same environment. The comment says prune should serialize per environment, but the key namespace is disjoint from every other command's key, so there is no actual mutual exclusion. To serialize prune against all other VCS commands in the same environment, the key must overlap with theirs — for example [environmentId].
- key: ({ environmentId }) => JSON.stringify([environmentId, "worktrees.prune"]),
+ key: ({ environmentId }) => JSON.stringify([environmentId]),🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/client-runtime/src/state/vcs.ts around lines 218-220:
`pruneWorktrees` uses the concurrency key `[environmentId, "worktrees.prune"]`, so the shared `vcsCommandScheduler` only serializes prune requests against other prune requests. Other VCS commands use `[environmentId, input.cwd]`, so a prune can still run concurrently with `createWorktree`, `removeWorktree`, `switchRef`, etc. in the same environment. The comment says prune should serialize per environment, but the key namespace is disjoint from every other command's key, so there is no actual mutual exclusion. To serialize prune against all other VCS commands in the same environment, the key must overlap with theirs — for example `[environmentId]`.
| const seen = new Set<string>(); | ||
| const worktrees: WorktreeInfo[] = []; | ||
| for (const record of perProject.flat()) { | ||
| if (seen.has(record.path)) continue; | ||
| seen.add(record.path); | ||
| worktrees.push(record); | ||
| } |
There was a problem hiding this comment.
🟠 High vcs/WorktreeService.ts:449
The deduplication loop in listWorktrees keeps the first WorktreeInfo per path and silently discards all later records for the same path. When two projects share a repository and the second project has an active thread on that worktree, the retained first record can lack the active_thread blocker and be marked safeToPrune: true. pruneWorktrees then removes a worktree that an active thread is still using. The fix is to merge thread references and blockers from later records into the kept record instead of dropping them.
| const seen = new Set<string>(); | |
| const worktrees: WorktreeInfo[] = []; | |
| for (const record of perProject.flat()) { | |
| if (seen.has(record.path)) continue; | |
| seen.add(record.path); | |
| worktrees.push(record); | |
| } | |
| const seen = new Map<string, WorktreeInfo>(); | |
| const worktrees: WorktreeInfo[] = []; | |
| for (const record of perProject.flat()) { | |
| const existing = seen.get(record.path); | |
| if (existing === undefined) { | |
| seen.set(record.path, record); | |
| worktrees.push(record); | |
| } else { | |
| existing.threads = [...existing.threads, ...record.threads]; | |
| existing.orphaned = existing.orphaned && record.orphaned; | |
| existing.pruneBlockers = [ | |
| ...new Set([...existing.pruneBlockers, ...record.pruneBlockers]), | |
| ] as WorktreePruneBlocker[]; | |
| existing.safeToPrune = existing.pruneBlockers.length === 0; | |
| } | |
| } |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/WorktreeService.ts around lines 449-455:
The deduplication loop in `listWorktrees` keeps the first `WorktreeInfo` per path and silently discards all later records for the same path. When two projects share a repository and the second project has an active thread on that worktree, the retained first record can lack the `active_thread` blocker and be marked `safeToPrune: true`. `pruneWorktrees` then removes a worktree that an active thread is still using. The fix is to merge thread references and blockers from later records into the kept record instead of dropping them.
| if (thread === null || thread.worktreePath === null) { | ||
| return; | ||
| } | ||
| const worktreePath = pathService.resolve(thread.worktreePath); |
There was a problem hiding this comment.
🟡 Medium Layers/ThreadDeletionReactor.ts:78
pruneOrphanedWorktree resolves the thread's stored path with pathService.resolve, but WorktreeService.listWorktrees returns paths canonicalized via fs.realPath. When the stored path uses a non-canonical spelling (e.g. /var/... on macOS where /private/var is the real path), worktrees.find((worktree) => worktree.path === worktreePath) never matches, so the immediate orphan cleanup is silently skipped and the worktree is left behind. Consider canonicalizing worktreePath with fs.realPath (or the equivalent the service uses) before the comparison.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts around line 78:
`pruneOrphanedWorktree` resolves the thread's stored path with `pathService.resolve`, but `WorktreeService.listWorktrees` returns paths canonicalized via `fs.realPath`. When the stored path uses a non-canonical spelling (e.g. `/var/...` on macOS where `/private/var` is the real path), `worktrees.find((worktree) => worktree.path === worktreePath)` never matches, so the immediate orphan cleanup is silently skipped and the worktree is left behind. Consider canonicalizing `worktreePath` with `fs.realPath` (or the equivalent the service uses) before the comparison.
| const { worktrees } = yield* worktreeService.listWorktrees({ | ||
| projectId: thread.projectId, | ||
| }); | ||
| const record = worktrees.find((worktree) => worktree.path === worktreePath); |
There was a problem hiding this comment.
Orphan prune path mismatch
High Severity
Immediate orphan worktree cleanup in ThreadDeletionReactor fails to prune eligible worktrees. The path comparison between the stored worktreePath (resolved) and inventory paths (canonicalized with realPath) can mismatch, especially with symlinks or macOS /var paths, preventing removal.
Reviewed by Cursor Bugbot for commit de7f960. Configure here.
| blockers.push("status_unavailable"); | ||
| } else if (aheadOfDefaultCount > 0) { | ||
| blockers.push("unpushed"); | ||
| } |
There was a problem hiding this comment.
Upstream gone skips unpushed check
High Severity
When a branch’s upstream is marked gone, prune safety treats it as fully synced and does not apply the already-computed aheadOfDefaultCount. Worktrees whose branches still have commits not on the default ref can be marked safeToPrune and removed by auto-prune, bulk prune, or orphan cleanup.
Reviewed by Cursor Bugbot for commit de7f960. Configure here.
| const localApi = readLocalApi(); | ||
| let shouldDeleteWorktree = false; | ||
| if (canDeleteWorktree && localApi) { | ||
| if (canDeleteWorktree && localApi && !serverPrunesOrphan) { |
There was a problem hiding this comment.
Orphan setting skips force cleanup
Medium Severity
When deleteOrphanedImmediately is on for the primary environment, the desktop worktree confirmation and client removeWorktree path are skipped entirely. The server only removes orphans that are safeToPrune, so dirty or unpushed orphan worktrees are neither auto-pruned nor offered for force deletion after thread delete.
Reviewed by Cursor Bugbot for commit de7f960. Configure here.
ApprovabilityVerdict: Needs human review 6 blocking correctness issues found. This PR introduces a substantial new worktree management feature with new services, UI, contracts, and integration into core orchestration. Multiple unresolved high-severity review comments identify bugs that could lead to data loss (worktrees with unpushed commits incorrectly marked safe to prune) and require attention before merge. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Effect service conventions review. One clear error-handling violation (wrapper drops cause and rebuilds its message from cause.message), plus a curried error-constructor helper that the conventions call out. Details inline.
Posted via Macroscope — Effect Service Conventions
| new ProviderAdapterRequestError({ | ||
| provider: providerErrorLabel(preferredProvider), | ||
| method: "thread.turn.start", | ||
| detail: `Could not restore the thread's worktree: ${error.message}`, | ||
| }), |
There was a problem hiding this comment.
This wrapper drops the underlying error and rebuilds detail from error.message. Per the error conventions, preserve the immediate failure as cause and derive the wrapper's message/detail from its own structural attributes rather than copying cause.message. ProviderAdapterRequestError already accepts an optional cause.
| new ProviderAdapterRequestError({ | |
| provider: providerErrorLabel(preferredProvider), | |
| method: "thread.turn.start", | |
| detail: `Could not restore the thread's worktree: ${error.message}`, | |
| }), | |
| new ProviderAdapterRequestError({ | |
| provider: providerErrorLabel(preferredProvider), | |
| method: "thread.turn.start", | |
| detail: `Could not restore the thread's worktree at ${thread.worktreePath}.`, | |
| cause: error, |
Posted via Macroscope — Effect Service Conventions
| const toGitManagerError = (operation: string, cwd: string, detail: string) => (cause: unknown) => | ||
| new GitManagerError({ operation, cwd, detail, cause }); |
There was a problem hiding this comment.
toGitManagerError is a curried helper whose only behavior is (...args) => new GitManagerError({ ...args }), used with mapError. The conventions ask that the error be constructed at the failure boundary so its attributes and cause stay visible, rather than through a generic constructor alias. Consider inlining new GitManagerError({ operation, cwd, detail, cause }) at each mapError site.
Posted via Macroscope — Effect Service Conventions
|
Closing in favor of #2829 (orchestration V2). #2829 deletes the V1 orchestration layer this PR builds on — This is not a judgement on the change itself. Several of these are real gaps we still want fixed; the base just moved out from under them. Once #2829 merges, please rebase onto |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Bugbot Autofix is ON, but a cloud agent failed to start.
Reviewed by Cursor Bugbot for commit de7f960. Configure here.
| const primaryEnvironmentId = usePrimaryEnvironmentId(); | ||
| const deleteOrphanedWorktreesImmediately = usePrimarySettings( | ||
| (settings) => settings.worktrees.deleteOrphanedImmediately, | ||
| ); |
There was a problem hiding this comment.
Wrong environment orphan policy
Medium Severity
Thread delete skips the desktop “delete worktree too?” prompt only when deleteOrphanedImmediately comes from primary server settings and the thread’s environment is the primary one. Deletes on other connected servers ignore that server’s worktree policy and may show the dialog even when that server auto-prunes orphans.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit de7f960. Configure here.


What Changed
Why
Worktrees accumulated indefinitely, especially for web users and settled threads, because cleanup was limited to a desktop-only deletion flow. This change provides visibility and controlled cleanup while preserving branches and checkpoint references so pruned worktrees can be recreated safely when needed.
UI Changes
Added a Worktrees settings page showing managed worktree status, linked threads, pruning eligibility, attention blockers, filters, bulk pruning, row-level deletion, and global auto-prune policies.
Checklist
Agent Transcript
Expand
Assistant
Here's what the codebase actually has today, then a proposed feature set. The short version of the current state: there is almost no worktree management — which matches your 186-worktree situation.
Current state (from a codebase sweep):
worktree_pathstring onprojection_threads(ProjectionThreads.ts:41). Nothing records creation time, last use, or dirty state.useThreadActions.ts:253-397) — and it's desktop-only (needslocalApi), so web users always leak worktrees. Server-sidethread.deletenever touches the worktree.git worktree pruneis never called anywhere;git worktree listis only used to annotate the branch list.settled_override/settled_at, decider atdecider.ts:448-507) — so as you said, settling means worktrees now live forever.refs/t3/checkpoints/...) live in the shared ref store, not per-worktree, so deleting a worktree does not lose checkpoints — which is what makes revival viable.Proposed features
1. Worktree inventory (the foundation). A server-side service that scans
git worktree list --porcelainper project and joins against thread rows to produce, per worktree: linked threads + their settled state, dirty/clean, ahead/unpushed counts, PR state (merged?), last activity (last thread activity or dir mtime), and orphaned (no thread references it at all — your existing 186 will be full of these from deleted threads). I'd derive this on demand with a short cache rather than adding a persisted table — worktrees are filesystem truth and a table will drift. The status pieces already exist (localStatus/remoteStatusinGitManager.ts).2. Worktrees settings page (your observability ask). Follows the appearance-settings pattern exactly (route file + panel in
SettingsPanels.tsx+ nav entry inSettingsSidebarNav.tsx). List with badges (dirty, unpushed, merged, orphaned, settled-only), per-row delete, and a "prune all safe" bulk action.3. A "safe to prune" classification — this is the crux. A worktree is auto-prunable when: no active (unsettled) thread references it, the working tree is clean, and the branch is merged or fully pushed. Everything else needs explicit user action — with one big enhancement below.
4. Safety snapshot before deletion. Before removing a worktree (even a dirty one, if the user forces it), commit the dirty state to a backup ref like
refs/t3/worktree-backup/<branch>. This makes every prune lossless and makes auto-pruning much less scary — you can loosen the policy because deletion is reversible.5. Auto-prune sweep + policy. A periodic server loop on the
ProviderSessionReaperpattern (Effect.forkScoped+Schedule.spaced, registered inserverRuntimeStartup.ts) that applies a configurable policy — Zortos's "delete after N days of inactivity" (say default 14, options like 7/14/30/never), evaluated only against safe-to-prune worktrees whose threads are all settled or deleted. Settling doesn't prune immediately; it just starts the clock. Also runsgit worktree prunefor stale admin entries.6. Revival on unsettle. The thread keeps its
branch+worktreePathmetadata even after prune. When a thread unsettles (explicitly, or via the existing auto-unsettle-on-activity in the decider) and its path is gone,bootstrap.prepareWorktreerecreates the worktree at the same path from the still-existing branch, and restores the backup ref if one exists. Since branches and checkpoint refs survive pruning, this should be genuinely lossless.7. Move orphan cleanup server-side. Replace the desktop-only dialog mechanism with a server-side removal path so web gets it too (keep the dialog as UX, but the server does the work).
8.
t3 worktrees list|pruneCLI. Net-new (nothing exists inapps/server/src/cli/), but cheap once the inventory service exists — and it's the practical tool for digging out of the current 186-worktree hole headlessly. Worth peeking at worktrunk per Max's suggestion for list-view/status ideas; the safety model above is roughly what it does.Open decisions for you
t3code/*branches (plus their checkpoint refs). I'd prune worktrees only in v1 — branches are cheap, and keeping them is what makes revival trivial. Merged-branch deletion could be an opt-in second tier later.My suggested build order: inventory service → settings page (immediate observability win + manual bulk-prune for your 186) → safety snapshot + server-side removal → auto-prune sweep → revival → CLI. The first two alone solve the acute pain.
User
We don't need this. If we only auto-prune safe ones and allow the user to force, it's on them that we cannot restore it later. Should just be clear that it is an unsafe action if they choose to delete it in a dirty state?
If we do this it should be super minimal in v1
Sure, lets start with worktrees v1 and then we can add more advanced options later
never touch dirty worktrees. We can have some UI to emphasize that they have worktrees that "needs action" in the worktrees setting to show like "This worktree has not had activity for N days but is dirty so we wont delete it" kinda thing
Global for now. Per-project settings override is a larger thing we need to support application-wide
Should be configurable I think. User can choose (in settings) to delete "orphan" worktrees immediately or just keep it on the normal schedule
Assistant
Plan is written up at
.plans/worktree-management-v1.mdwith all your decisions folded in. The v1 shape:git worktree list --porcelainper project, joins thread rows, computes dirty/unpushed/merged/orphaned/lastActivity and asafeToPruneflag. Exposed asvcsListWorktrees+vcsPruneWorktreesnext to the existing worktree RPCs, with the server re-validating safety before every removal rather than trusting the client's classification.ProviderSessionReaper-pattern loop; only ever removes safe worktrees past the inactivity window (default 14d, options 7/14/30/off), never dirty ones. Uses non-forcedgit worktree removeso git itself is a last-line safety net, and finishes withgit worktree prune. The "delete orphans immediately" toggle is handled here too: when on,thread.deleteorphaning a worktree removes it server-side with no dialog — which also fixes the current gap where that cleanup is desktop-only.branch/worktreePathafter prune;bootstrap.prepareWorktreerecreates the worktree from the still-existing branch on unsettle/turn-start when the path is missing. Branches and checkpoint refs both survive v1 pruning, so this is lossless.t3 worktrees list/prune --dry-runas a stretch, cut first if needed; the settings page's bulk prune already covers digging out of the 186.One open implementation question I flagged in the doc: the sweep runs server-side, but our existing policy-ish settings (like
sidebarAutoSettleAfterDays) are client-side only — so the prune policy needs a server-readable home. I'd resolve that at implementation time (reuse a server-persisted settings mechanism if one exists, otherwise a small settings store + RPC).Build order: inventory → settings page → sweep/policy → revival → CLI. Steps 1–2 alone give you observability and manual cleanup. Say the word and I'll start on the inventory service.
User
the page probably also needs some filter functionality
Assistant
Added to the plan doc. The filter spec for the settings page is now:
t3code-ab12cd34means nothing to a human and "which worktree belongs to that auth-refactor thread" is the actual question.Anything else you want to shape before I start on the inventory service?
Note
Medium Risk
Touches turn-start paths and git worktree removal with policy-driven auto-prune; mistakes could remove worktrees or fail sessions, but server-side safety checks and non-force prune limit blast radius.
Overview
Introduces managed worktree lifecycle so thread worktrees no longer accumulate without visibility or server-side cleanup.
Server: New
WorktreeServicebuilds inventory fromgit worktree listplus thread projections, classifies safe-to-prune (no active thread, clean tree, no unpushed commits), and implementspruneWorktrees(re-validates on the server, non-force remove,git worktree prune) andreviveWorktree(recreate directory from surviving branch).WorktreeReaperruns periodic sweeps fromworktreesserver settings (autoPruneAfterDays,deleteOrphanedImmediately).ProviderCommandReactorcalls revival before turn start when the path is missing.ThreadDeletionReactorcan auto-prune orphaned safe worktrees when the immediate-orphan setting is on. WS exposesvcsListWorktrees/vcsPruneWorktrees; contracts add worktree types andServerSettings.worktrees.Web: New Settings → Worktrees page lists worktrees with filters, needs-attention grouping, bulk safe prune, row delete (safe vs force), and policy controls. Thread delete skips the desktop orphan dialog when the server handles immediate orphan pruning on the primary environment.
Also adds a v1 design plan doc and Git-backed
WorktreeServicetests; integration tests mockWorktreeServicefor revival.Reviewed by Cursor Bugbot for commit de7f960. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add worktree inventory, pruning, and revival with a settings UI and background reaper
WorktreeServiceexposinglistWorktrees,pruneWorktrees, andreviveWorktreewith safety checks (dirty files, unpushed commits, orphaned detection) across all managed projects.WorktreeReaperas a background service that periodically prunes safe/orphaned worktrees according to configurableautoPruneAfterDaysanddeleteOrphanedImmediatelyserver settings.vcs.listWorktrees,vcs.pruneWorktrees) with authorization scopes; pruning triggers git status refreshes for affected workspaces.WorktreesSettingsPanelat/settings/worktreeswith a filterable table, bulk safe-prune, per-row delete/force-delete, and an auto-prune configuration section.thread.turn.start,ProviderCommandReactornow attempts to revive a pruned worktree before starting the provider session; on thread deletion,ThreadDeletionReactormay immediately prune the orphaned worktree when settings permit.📊 Macroscope summarized de7f960. 17 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.