Skip to content

feat(worktree): name worktrees wedged in an abandoned merge - #4499

Merged
BunsDev merged 7 commits into
mainfrom
fix/cave-97svy-wedged-worktree-detection
Aug 10, 2026
Merged

feat(worktree): name worktrees wedged in an abandoned merge#4499
BunsDev merged 7 commits into
mainfrom
fix/cave-97svy-wedged-worktree-detection

Conversation

@BunsDev

@BunsDev BunsDev commented Aug 10, 2026

Copy link
Copy Markdown
Member

Closes cave-97svy.

The signal

A worktree left mid-merge blocked its branch for three days and was reported repeatedly as a persistent blocker. Every session that found it did the right thing — saw 137 uncommitted paths, read them as another session's in-flight work, and backed off rather than clobber a colleague.

Backing off was correct each time. The missing information was whether anyone was still there.

Root cause

Nothing in src/lib or scripts references MERGE_HEAD, rebase-merge, or CHERRY_PICK_HEAD — verified by grep before this change. So a paused git operation has no name, no age, and no remedy anywhere in the tooling:

  • the lifecycle patrol classifies on dirty/retention/PR state,
  • the auto-lock hook locks anything dirty,
  • the retention-push hook pushes unpushed commits,
  • the status dashboard printed 137 dirty.

All four are satisfied by a wedged worktree. It looks exactly like live work, forever.

scripts/worktree-status.mjs was also documented in neither AGENTS.md nor CLAUDE.md, so the one tool positioned to answer this was not something a session would think to run.

The fix

A WEDGED verdict on the local dashboard that detects merge / rebase / cherry-pick / revert / bisect markers, dates the stall from the marker mtime, lists the unresolved paths, and — the part that unblocks a decision — compares every dirty path's mtime against the moment the operation stalled:

  • nothing tracked touched since → no hand resolution exists; the tree is raw merge output both parents reproduce, and abort is lossless
  • something tracked touched since → a human is mid-resolution; the report says do NOT abort and marks the abort command "only with the owner's say-so"
  • unreadable tree → fails closed to the same warning, since "nobody touched it" is the fact the abort decision leans on

Retention of both merge parents is reported alongside (network-free, via remote-tracking refs and tags), so an abort is only called lossless when nothing would be stranded; otherwise the archive-tag command prints instead.

WEDGED outranks every verdict including PRIMARY, and is never SAFE-RETIRE — so neither --prune nor wt:retire-on-exit can remove a tree holding unfinished work.

Verification

Against the real wedged worktree, the tool independently reproduces the manual forensics that took ~20 minutes:

🟠 WEDGED — docs/cave-zs85n-chat-sidebar-attention
   merge paused 2d ago (2026-08-07T23:50:31.967Z), marker MERGE_HEAD
   unresolved paths (5):
     src/app/api/chat/send/chat-send-capabilities.ts
     ...
   ✓ no tracked file touched since the merge stalled (1 untracked file(s) added since) —
      the tree is raw merge output with no hand resolution to lose.
      Both sides are reachable from a remote-tracking ref or tag, so an abort strands nothing.
   finish it:  cd '…' && git commit
   or drop it: cd '…' && git merge --abort

node --test scripts/worktree-status.test.mjs — 11/11 pass (6 pre-existing, 5 new covering: mid-merge is WEDGED not DIRTY; abandoned reports no hand resolution; hand-resolved warns against aborting; a paused rebase reports rebase-shaped remedies and survives the detached HEAD it creates; --prune never offers a WEDGED tree however merged it looks).

Docs added to CLAUDE.md (full section) and AGENTS.md (pointer at the "is this someone's live work?" decision point).

A worktree paused mid-merge presents to every tool here as nothing more
than "N dirty", which is indistinguishable from a session editing right
now. Nothing in src/lib or scripts read MERGE_HEAD, rebase-merge, or
CHERRY_PICK_HEAD, so the state had no name and no age.

The cost is not hypothetical: an abandoned merge on
docs/cave-zs85n-chat-sidebar-attention stalled 2026-08-07 and survived
until 2026-08-10 because each session that found its 137 dirty paths read
them as a colleague's in-flight work and correctly declined to clobber
them. Backing off was the right call every time; the missing information
was whether anyone was actually still there.

Add a WEDGED verdict to the local worktree dashboard that detects the
paused operation, dates it from the marker's mtime, lists the unresolved
paths, and answers the question backing off leaves open: compare every
dirty path's mtime against the moment the operation stalled. Nothing
touched since means no hand resolution exists to lose, so the tree is
raw merge output the two parents reproduce; anything touched since means
a human is mid-resolution and the report says do NOT abort. An unreadable
tree fails closed to the same warning, because "nobody touched it" is the
fact an abort decision leans on.

Retention is reported alongside, network-free, so an abort is only ever
called lossless when both sides are reachable from a remote-tracking ref
or tag; otherwise the archive-tag command prints instead. WEDGED outranks
every other verdict and is never SAFE-RETIRE, so neither --prune nor
wt:retire-on-exit can remove a tree with unfinished work in it.

Verified against the real wedged worktree: the tool independently
reproduces the manual forensics — 5 conflicts, zero hand resolution, both
parents retained, abort lossless.
Copilot AI lite review requested due to automatic review settings August 10, 2026 11:08
@BunsDev BunsDev self-assigned this Aug 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a new WEDGED verdict to worktree-status so unfinished git operations (merge/rebase/cherry-pick/revert/bisect) are detected, aged, and given actionable remediation guidance—preventing abandoned paused operations from being mistaken as “someone’s live dirty work”.

Changes:

  • Detect in-progress git operations via per-worktree git-dir markers and surface them as WEDGED (with age, conflicted paths, and “touched since stall” analysis).
  • Update human output ordering/formatting and add a detailed wedge remedy block (including retention hints).
  • Add node test coverage for WEDGED behavior (merge + rebase scenarios) and document pnpm wt:status usage in agent docs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
scripts/worktree-status.mjs Implements WEDGED detection, sorting precedence, and human remedy output.
scripts/worktree-status.test.mjs Adds fixtures/tests for merge- and rebase-based wedged worktrees and --prune behavior.
CLAUDE.md Documents pnpm wt:status and explains the WEDGED verdict semantics.
AGENTS.md Adds a workflow pointer to run pnpm wt:status before assuming dirtiness is live work.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/worktree-status.mjs Outdated
Comment on lines +430 to +434
process.stdout.write(` finish it: cd ${shq(row.path)} && git ${w.op === "rebase" ? "rebase --continue" : "commit"}\n`);
process.stdout.write(
` or drop it: cd ${shq(row.path)} && git ${w.op === "bisect" ? "bisect reset" : `${w.op} --abort`}` +
`${handResolved ? " ← only with the owner's say-so" : ""}\n`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 403f200.

Comment on lines +174 to +185
const scan = (res) => {
if (!res.ok) return { count: 0, newest: null, readable: false };
let count = 0;
let newest = null;
for (const rel of res.out.split("\0").filter(Boolean)) {
const at = mtimeMs(path.join(wtPath, rel));
if (at === null || at <= cutoff) continue;
count += 1;
if (!newest || at > newest.atMs) newest = { path: rel, atMs: at, at: new Date(at).toISOString() };
}
return { count, newest, readable: true };
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 403f200.

Comment on lines +121 to +128
const OPERATION_MARKERS = [
{ marker: "rebase-merge", op: "rebase" },
{ marker: "rebase-apply", op: "rebase" },
{ marker: "MERGE_HEAD", op: "merge" },
{ marker: "CHERRY_PICK_HEAD", op: "cherry-pick" },
{ marker: "REVERT_HEAD", op: "revert" },
{ marker: "BISECT_LOG", op: "bisect" },
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 403f200.

Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
@BunsDev

BunsDev commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Triage: this PR is not broken — it's blocked by a break on main.

The only failing check is iOS build:

apps/ios/CovenCave/CovenCave/Views/ConnectionView.swift:390:9: error: switch must be exhaustive

This branch does not touch apps/ios at all (git diff --stat origin/main...HEAD -- apps/ios/ is empty). AppModel.DiscoveryOutcome gained a credentialFailure(String) case on main without ConnectionView being updated.

Fix is up as #4510, verified with a real local xcodebuild: main fails, the fix builds. Once that lands, rebase or re-run and this should go green.

Context for why it appeared here at all: the iOS job no longer runs on pull requests (it moved to release.yml), so it only surfaced because this PR's CI came from a workflow_dispatch recovery run, which builds unconditionally.

@BunsDev
BunsDev merged commit 185420d into main Aug 10, 2026
1 check passed
@BunsDev
BunsDev deleted the fix/cave-97svy-wedged-worktree-detection branch August 10, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants