From e1fe12322ccf55b63017a579fc72ed5bb9b2783b Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 15 Apr 2026 14:58:43 -0400 Subject: [PATCH] fix(worktree): add pre-merge deletion guard to quick.md; fix backup handling on conflict (#2275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps in the orchestrator file-protection block (#1756, #2040): 1. quick.md never received the pre-merge deletion guard added to execute-phase.md in #2040. Added the same DELETIONS check: if the worktree branch deletes any tracked .planning/ files, block the merge with a clear message rather than silently losing those files. 2. Both workflows deleted STATE_BACKUP and ROADMAP_BACKUP on merge conflict — destroying the recovery files at exactly the moment they were needed. Changed conflict handler to: preserve both backup paths, print restore instructions, and break (halt) instead of continue (silently advancing to the next worktree). 3. Neither workflow used --no-ff. Without it a fast-forward merge produces no merge commit, so HEAD~1 in the resurrection check points to the worktree's parent rather than main's pre-merge HEAD. Added --no-ff to both git merge calls so HEAD~1 is always reliable. Closes #2208 Co-authored-by: Claude Sonnet 4.6 --- get-shit-done/workflows/execute-phase.md | 10 ++++++---- get-shit-done/workflows/quick.md | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index c79f1c807..9b077a5da 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -614,11 +614,13 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT continue fi - # Merge the worktree branch into the current branch - git merge "$WT_BRANCH" --no-edit -m "chore: merge executor worktree ($WT_BRANCH)" 2>&1 || { + # Merge the worktree branch into the current branch (--no-ff ensures a merge commit so HEAD~1 is reliable) + git merge "$WT_BRANCH" --no-ff --no-edit -m "chore: merge executor worktree ($WT_BRANCH)" 2>&1 || { echo "⚠ Merge conflict from worktree $WT_BRANCH — resolve manually" - rm -f "$STATE_BACKUP" "$ROADMAP_BACKUP" - continue + echo " STATE.md backup: $STATE_BACKUP" + echo " ROADMAP.md backup: $ROADMAP_BACKUP" + echo " Restore with: cp \$STATE_BACKUP .planning/STATE.md && cp \$ROADMAP_BACKUP .planning/ROADMAP.md" + break } # Restore orchestrator-owned files (main always wins) diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index d71f55ea2..02ca6ba69 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -628,10 +628,21 @@ After executor returns: # Snapshot files on main to detect resurrections PRE_MERGE_FILES=$(git ls-files .planning/) - git merge "$WT_BRANCH" --no-edit -m "chore: merge quick task worktree ($WT_BRANCH)" 2>&1 || { - echo "⚠ Merge conflict — resolve manually" + # Pre-merge deletion guard: block merges that delete tracked .planning/ files + DELETIONS=$(git diff --diff-filter=D --name-only HEAD..."$WT_BRANCH" 2>/dev/null || true) + if [ -n "$DELETIONS" ]; then + echo "BLOCKED: Worktree branch $WT_BRANCH contains file deletions: $DELETIONS" + echo "Review these deletions before merging. If intentional, remove this guard and re-run." rm -f "$STATE_BACKUP" "$ROADMAP_BACKUP" continue + fi + + git merge "$WT_BRANCH" --no-ff --no-edit -m "chore: merge quick task worktree ($WT_BRANCH)" 2>&1 || { + echo "⚠ Merge conflict from worktree $WT_BRANCH — resolve manually" + echo " STATE.md backup: $STATE_BACKUP" + echo " ROADMAP.md backup: $ROADMAP_BACKUP" + echo " Restore with: cp \$STATE_BACKUP .planning/STATE.md && cp \$ROADMAP_BACKUP .planning/ROADMAP.md" + break } # Restore orchestrator-owned files @@ -639,7 +650,7 @@ After executor returns: if [ -s "$ROADMAP_BACKUP" ]; then cp "$ROADMAP_BACKUP" .planning/ROADMAP.md; fi rm -f "$STATE_BACKUP" "$ROADMAP_BACKUP" - # Remove files deleted on main but re-added by worktree + # Remove files deleted on main but re-added by worktree (--no-ff guarantees a merge commit so HEAD~1 is reliable) DELETED_FILES=$(git diff --diff-filter=A --name-only HEAD~1 -- .planning/ 2>/dev/null || true) for RESURRECTED in $DELETED_FILES; do if ! echo "$PRE_MERGE_FILES" | grep -qxF "$RESURRECTED"; then