@danmoisan/drm-copilot-mcp 1.1.14 → 1.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/package.json +1 -1
  2. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
  3. package/resources/claude-customizations/.claude/rules/shell.md +8 -7
  4. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
  5. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
  6. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
  7. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
  8. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
  9. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
  10. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
  11. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
  12. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
  13. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
  14. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
  15. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
  16. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
  17. package/resources/claude-customizations/pack-manifests/core.json +11 -0
  18. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  19. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +5 -0
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env bash
2
+ # cleanup_worktrees_actions_lib.sh: sourceable function library for the mutating
3
+ # ("apply mode") half of the cleanup-worktrees tool. Provides consolidation onto the
4
+ # documentationandmemories branch (dedicated worktree creation, cherry-picking with
5
+ # conflict/empty handling, abort cleanup), the post-merge verification gate, and the
6
+ # deletion mechanics (base-branch refusal, same-process re-verification, no-force
7
+ # worktree removal, branch deletion) plus the apply-mode driver.
8
+ #
9
+ # Sourcing contract: this library depends on functions defined in
10
+ # .claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh (cleanup_wt_git, parse_worktree_list,
11
+ # classify_ancestry, classify_branch, run_report). The wrapper
12
+ # .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh sources that library first; the bats suites do
13
+ # the same. This file defines functions only and runs nothing at source time.
14
+ # git commands that legitimately return non-zero (cherry-pick on conflict, worktree
15
+ # remove on a dirty tree, merge-base --is-ancestor) are captured with `|| rc=$?` so
16
+ # an intended non-zero exit does not abort under set -euo pipefail. All consolidation
17
+ # git commands target the dedicated worktree via `git -C`, never the caller's tree.
18
+ #
19
+ # Guarded-read invariant: every git-backed read whose output or exit code is
20
+ # authoritative is captured in the parent shell (`out=$(cmd) || rc=$?`, iterated over
21
+ # `<<<"$out"`), never via `mapfile`/`< <(...)`. consolidation_worktree_path fails
22
+ # (non-zero, empty stdout) on a parse_worktree_list hard failure or an empty main path,
23
+ # so the malformed `-wt/documentationandmemories` derivation is impossible; both
24
+ # consumers (create_consolidation_worktree, cleanup_consolidation_on_abort) guard the
25
+ # capture. run_apply captures parse_worktree_list, enumerate_branches, and each
26
+ # classify_branch with rc up front and returns non-zero on any hard failure without
27
+ # mutating. reverify_delete_eligible and remove_worktree_safe capture their git-backed
28
+ # reads and fail closed (BLOCKED-REVERIFY / BLOCKED-DIRTY). Accepted best-effort sites
29
+ # (Design Decision 9), intentionally left as `|| true` because every failure direction
30
+ # is already fail-closed: `git fetch origin main` in verify_consolidation_merged (a
31
+ # stale main can only block deletion, never fabricate ancestry), `cherry-pick
32
+ # --skip`/`--abort` in cherry_pick_candidates (a failed skip/abort surfaces on the next
33
+ # pick as rc=1), the CHERRY_PICK_HEAD and ref-exists probes, and
34
+ # `vout=$(verify_consolidation_merged) || true` in run_apply (only the exact token
35
+ # MERGED_CLEAN unlocks deletion).
36
+ #
37
+ # Opt-in disposable-dirt clearing hook: when CLEANUP_WT_CLEAR_DISPOSABLE is 1,
38
+ # delete_candidate answers a BLOCKED-DIRTY removal by calling clear_disposable_dirt
39
+ # (.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh), then re-verifying delete eligibility in
40
+ # process, then retrying the SAME unforced removal. remove_worktree_safe is unmodified
41
+ # by that hook: it gains no force flag, no new argument, and no new call site, so the
42
+ # never-force-remove invariant holds on every path. The hook is off by default, runs in
43
+ # apply mode only, and refuses the clear when any per-file verdict for the worktree is
44
+ # UNIQUE, including the fail-closed UNIQUE assigned when a classification read errors.
45
+
46
+ CLEANUP_WT_CONSOLIDATION_BRANCH="documentationandmemories"
47
+
48
+ consolidation_worktree_path() {
49
+ # Echo the consolidation worktree path. CLEANUP_WT_CONSOLIDATION_PATH overrides;
50
+ # otherwise the path is <main-worktree-path>-wt/documentationandmemories, where the
51
+ # main worktree path is the first `git worktree list --porcelain` stanza.
52
+ #
53
+ # A parse_worktree_list hard failure aborts with git's exit code and no stdout; an
54
+ # empty main worktree path aborts with return 1 and no stdout. Either way the
55
+ # malformed `-wt/documentationandmemories` derivation from an empty main path is
56
+ # impossible.
57
+ local override=${CLEANUP_WT_CONSOLIDATION_PATH:-}
58
+ if [[ -n $override ]]; then
59
+ printf '%s\n' "$override"
60
+ return 0
61
+ fi
62
+ local out rc=0 first_record main_wt=""
63
+ # Guarded parent-shell capture instead of `mapfile < <(...)`, which would swallow a
64
+ # git hard failure and yield a malformed path.
65
+ out=$(parse_worktree_list) || rc=$?
66
+ if ((rc != 0)); then
67
+ printf 'cleanup-worktrees: cannot derive consolidation path: git worktree list failed (rc=%s)\n' "$rc" >&2
68
+ return "$rc"
69
+ fi
70
+ first_record=${out%%$'\n'*}
71
+ main_wt=${first_record%%|*}
72
+ if [[ -z $main_wt ]]; then
73
+ printf 'cleanup-worktrees: cannot derive consolidation path: empty main worktree path\n' >&2
74
+ return 1
75
+ fi
76
+ printf '%s-wt/%s\n' "$main_wt" "$CLEANUP_WT_CONSOLIDATION_BRANCH"
77
+ }
78
+
79
+ create_consolidation_worktree() {
80
+ # Create the documentationandmemories branch in a dedicated worktree off main.
81
+ #
82
+ # Precondition: refs/heads/documentationandmemories must NOT already exist. If it
83
+ # does (a prior aborted run), stop and report to stderr rather than reuse silently.
84
+ # Otherwise run `git worktree add <path> -b documentationandmemories main` with the
85
+ # path derived by consolidation_worktree_path.
86
+ #
87
+ # Returns 0 on success (emits ACTION|worktree-add|<path>|OK); non-zero on a
88
+ # consolidation-path derivation failure, the pre-existing-branch guard, or a failed
89
+ # worktree add.
90
+ local path rc=0 prc=0
91
+ # Guarded capture: a consolidation-path derivation failure aborts before the ref
92
+ # probe and before any git mutation.
93
+ path=$(consolidation_worktree_path) || prc=$?
94
+ if ((prc != 0)); then
95
+ return "$prc"
96
+ fi
97
+ if cleanup_wt_git rev-parse --verify --quiet \
98
+ "refs/heads/$CLEANUP_WT_CONSOLIDATION_BRANCH" >/dev/null 2>&1; then
99
+ printf 'cleanup-worktrees: refs/heads/%s already exists; refusing to reuse a prior consolidation branch. Remove it or resume the prior run.\n' \
100
+ "$CLEANUP_WT_CONSOLIDATION_BRANCH" >&2
101
+ return 1
102
+ fi
103
+ cleanup_wt_git worktree add "$path" -b "$CLEANUP_WT_CONSOLIDATION_BRANCH" main || rc=$?
104
+ if ((rc != 0)); then
105
+ printf 'ACTION|worktree-add|%s|FAILED\n' "$path"
106
+ return "$rc"
107
+ fi
108
+ printf 'ACTION|worktree-add|%s|OK\n' "$path"
109
+ return 0
110
+ }
111
+
112
+ cherry_pick_candidates() {
113
+ # Cherry-pick each UNIQUE candidate onto the consolidation worktree.
114
+ #
115
+ # Reads COMMIT|<branch>|<sha>|UNIQUE|... records from stdin. Source branches are
116
+ # processed in LC_ALL=C order and commits oldest-first within each branch; that
117
+ # ordering is inherited from run_report's COMMIT emission (enumerate_branches is
118
+ # LC_ALL=C sorted and select_cherry_pick_candidates emits oldest-first). One
119
+ # `git -C <wt> cherry-pick -x <sha>` invocation per commit (never a multi-SHA
120
+ # invocation): -x records provenance and default cherry-pick preserves authorship.
121
+ # Every git command targets the consolidation worktree via -C, never the caller's
122
+ # worktree. Conflict/empty handling is implemented in this function (see below).
123
+ #
124
+ # Conflict handling (Planner Decision 4): on a non-zero cherry-pick whose result is
125
+ # a conflict (CHERRY_PICK_HEAD present), run `git -C <wt> cherry-pick --abort`,
126
+ # record the commit as CONFLICT, skip the remaining commits of that source branch
127
+ # (intra-branch dependency safety), and continue with the next branch. On a "now
128
+ # empty" result, run `git -C <wt> cherry-pick --skip` and reclassify the commit as
129
+ # droppable (CONTENT_ON_MAIN). Conflicts are never auto-resolved; --allow-empty and
130
+ # --keep-redundant-commits are never used.
131
+ #
132
+ # Args: $1 = consolidation worktree path.
133
+ local wt="$1" line branch sha prc cp_out rc=0 skip_branch=""
134
+ while IFS= read -r line; do
135
+ [[ $line == COMMIT\|* ]] || continue
136
+ IFS='|' read -r _ branch sha _ <<<"$line"
137
+ # A conflict on an earlier commit of this branch skips its remaining commits.
138
+ if [[ -n $skip_branch && $branch == "$skip_branch" ]]; then
139
+ printf 'ACTION|cherry-pick|%s|SKIPPED-BRANCH\n' "$sha"
140
+ continue
141
+ fi
142
+ prc=0
143
+ # Capture combined output for reliable "now empty"/conflict detection (real
144
+ # git emits those diagnostics on stderr). The recording stub multiplexes its
145
+ # `stub-git:` argv log onto the same streams, so re-surface those lines to
146
+ # stderr for argv observation; this is a no-op under real git.
147
+ cp_out=$(cleanup_wt_git -C "$wt" cherry-pick -x "$sha" 2>&1) || prc=$?
148
+ if [[ $cp_out == *"stub-git: "* ]]; then
149
+ printf '%s\n' "$cp_out" | grep '^stub-git: ' >&2 || true
150
+ fi
151
+ if ((prc == 0)); then
152
+ printf 'ACTION|cherry-pick|%s|OK\n' "$sha"
153
+ continue
154
+ fi
155
+ # "Now empty" means the content already landed on main: skip and reclassify.
156
+ if printf '%s' "$cp_out" | grep -qi 'now empty'; then
157
+ cleanup_wt_git -C "$wt" cherry-pick --skip >/dev/null || true
158
+ printf 'COMMIT|%s|%s|CONTENT_ON_MAIN|reclassified-empty\n' "$branch" "$sha"
159
+ printf 'ACTION|cherry-pick-skip|%s|OK\n' "$sha"
160
+ continue
161
+ fi
162
+ # Otherwise a conflict: abort (never auto-resolve), record, skip the branch.
163
+ if cleanup_wt_git -C "$wt" rev-parse --verify --quiet CHERRY_PICK_HEAD >/dev/null 2>&1; then
164
+ cleanup_wt_git -C "$wt" cherry-pick --abort >/dev/null || true
165
+ fi
166
+ printf 'COMMIT|%s|%s|CONFLICT|\n' "$branch" "$sha"
167
+ printf 'ACTION|cherry-pick|%s|CONFLICT\n' "$sha"
168
+ skip_branch="$branch"
169
+ rc=1
170
+ done
171
+ return "$rc"
172
+ }
173
+
174
+ cleanup_consolidation_on_abort() {
175
+ # Tear down a partially-built consolidation on a hard failure: remove the
176
+ # consolidation worktree and delete the documentationandmemories branch, reporting
177
+ # each action. No silent leftovers are permitted. The forced-removal flag is never
178
+ # used (that policy is global to this library); a failed removal is reported.
179
+ #
180
+ # Returns 0 (best-effort cleanup; each step's result is reported via ACTION lines).
181
+ # When the consolidation path cannot be derived (a parse_worktree_list hard failure),
182
+ # the worktree removal is skipped and reported as ACTION|worktree-remove||FAILED (path
183
+ # unknown), and the branch deletion is still attempted (best-effort contract).
184
+ local path wrc=0 brc=0 prc=0
185
+ # Guarded capture: on a path-derivation failure, skip the removal (path unknown) and
186
+ # continue to the branch-deletion step.
187
+ path=$(consolidation_worktree_path) || prc=$?
188
+ if ((prc != 0)); then
189
+ printf 'ACTION|worktree-remove||FAILED\n'
190
+ else
191
+ cleanup_wt_git worktree remove "$path" >/dev/null || wrc=$?
192
+ if ((wrc == 0)); then
193
+ printf 'ACTION|worktree-remove|%s|OK\n' "$path"
194
+ else
195
+ printf 'ACTION|worktree-remove|%s|FAILED\n' "$path"
196
+ fi
197
+ fi
198
+ cleanup_wt_git branch -D "$CLEANUP_WT_CONSOLIDATION_BRANCH" >/dev/null || brc=$?
199
+ if ((brc == 0)); then
200
+ printf 'ACTION|branch-delete|%s|OK\n' "$CLEANUP_WT_CONSOLIDATION_BRANCH"
201
+ else
202
+ printf 'ACTION|branch-delete|%s|FAILED\n' "$CLEANUP_WT_CONSOLIDATION_BRANCH"
203
+ fi
204
+ return 0
205
+ }
206
+
207
+ verify_consolidation_merged() {
208
+ # Post-merge gate: after `git fetch origin main`, is the consolidation branch an
209
+ # ancestor of main? Uses the same 0/1/>1 merge-base --is-ancestor handling as
210
+ # classify_ancestry. Exit 0 is the ONLY state that unlocks deletion of branches
211
+ # whose unique content was consolidated. Echoes MERGED_CLEAN / NOT_ANCESTOR /
212
+ # ANCESTRY_ERROR and returns 0 / 1 / 2 respectively.
213
+ local mrc=0
214
+ # Tip-equality pre-check, ahead of the best-effort fetch so no network call is made
215
+ # in the blocked case. A consolidation branch created at main and not yet committed
216
+ # to has a tip identical to main's; merge-base --is-ancestor answers 0 for that
217
+ # shape, which would wrongly present a zero-commit branch as delete-eligible. An
218
+ # empty or unresolvable tip on either side is a HARD FAILURE, never an equality
219
+ # match: two empty strings compare equal, so treating them as equality would block a
220
+ # genuinely merged branch. A commit-counting guard was rejected for this check: the
221
+ # count is also zero after the consolidation PR merges with a merge commit, which
222
+ # would permanently block the documented post-merge cleanup step.
223
+ local cons_tip main_tip ctrc=0 mtrc=0
224
+ cons_tip=$(cleanup_wt_git rev-parse "$CLEANUP_WT_CONSOLIDATION_BRANCH") || ctrc=$?
225
+ main_tip=$(cleanup_wt_git rev-parse main) || mtrc=$?
226
+ if ((ctrc != 0)) || ((mtrc != 0)) || [[ -z $cons_tip || -z $main_tip ]]; then
227
+ printf 'ANCESTRY_ERROR\n'
228
+ return 2
229
+ fi
230
+ if [[ $cons_tip == "$main_tip" ]]; then
231
+ printf 'NOT_ANCESTOR\n'
232
+ return 1
233
+ fi
234
+ cleanup_wt_git fetch origin main >/dev/null 2>&1 || true
235
+ cleanup_wt_git merge-base --is-ancestor "$CLEANUP_WT_CONSOLIDATION_BRANCH" main >/dev/null 2>&1 || mrc=$?
236
+ if ((mrc == 0)); then
237
+ printf 'MERGED_CLEAN\n'
238
+ return 0
239
+ elif ((mrc == 1)); then
240
+ printf 'NOT_ANCESTOR\n'
241
+ return 1
242
+ fi
243
+ printf 'ANCESTRY_ERROR\n'
244
+ return 2
245
+ }
246
+
247
+ reverify_delete_eligible() {
248
+ # Same-process re-verification immediately before a destructive action.
249
+ #
250
+ # Re-runs the classification ladder for <name> (which re-checks ancestry against
251
+ # main, and the content-neutral/equivalence verdicts) and confirms the FRESH state
252
+ # is still delete-eligible: MERGED_CLEAN | MERGED_CONTENT_NEUTRAL | MERGED_EQUIVALENT.
253
+ # Any mismatch (the branch drifted, main moved, or a recorded verdict no longer
254
+ # holds) emits ACTION|delete|<name>|BLOCKED-REVERIFY and returns 1; otherwise 0.
255
+ #
256
+ # Args: $1 = branch name; $2 = recorded state (advisory, for callers' context).
257
+ local name="$1" line state="" out crc=0
258
+ # Guarded parent-shell capture: a classify_branch hard failure (non-zero rc) maps
259
+ # explicitly to BLOCKED-REVERIFY/return 1, in addition to the state-token allowlist.
260
+ out=$(classify_branch "$name") || crc=$?
261
+ if ((crc != 0)); then
262
+ printf 'ACTION|delete|%s|BLOCKED-REVERIFY\n' "$name"
263
+ return 1
264
+ fi
265
+ while IFS= read -r line; do
266
+ [[ $line == BRANCH\|* ]] || continue
267
+ IFS='|' read -r _ _ state <<<"$line"
268
+ break
269
+ done <<<"$out"
270
+ case "$state" in
271
+ MERGED_CLEAN | MERGED_CONTENT_NEUTRAL | MERGED_EQUIVALENT)
272
+ return 0
273
+ ;;
274
+ *)
275
+ printf 'ACTION|delete|%s|BLOCKED-REVERIFY\n' "$name"
276
+ return 1
277
+ ;;
278
+ esac
279
+ }
280
+
281
+ remove_worktree_safe() {
282
+ # Remove a worktree without the forced-removal flag. A dirty worktree (modified
283
+ # tracked or any untracked files) makes `git worktree remove` fail; that blocks
284
+ # removal — the worktree's `git -C <path> status --porcelain` output is reported as
285
+ # DIRTY| lines and the worktree is left in place. An empty path means the branch
286
+ # has no worktree: skip silently and return 0. The main worktree is never passed in.
287
+ #
288
+ # Args: $1 = worktree path (may be empty).
289
+ # Returns 0 on successful removal or no-worktree; 1 when a dirty worktree blocks it.
290
+ local path="$1" rc=0 line sout srrc=0
291
+ [[ -z $path ]] && return 0
292
+ cleanup_wt_git worktree remove "$path" >/dev/null || rc=$?
293
+ if ((rc == 0)); then
294
+ printf 'ACTION|worktree-remove|%s|OK\n' "$path"
295
+ return 0
296
+ fi
297
+ # Guarded parent-shell capture of the diagnostic status read: a hard failure emits no
298
+ # DIRTY lines (the removal failure is already reported) but still blocks removal.
299
+ sout=$(cleanup_wt_git -C "$path" status --porcelain) || srrc=$?
300
+ if ((srrc == 0)) && [[ -n $sout ]]; then
301
+ while IFS= read -r line; do
302
+ [[ -z $line ]] && continue
303
+ printf 'DIRTY|%s|%s\n' "$path" "$line"
304
+ done <<<"$sout"
305
+ fi
306
+ printf 'ACTION|worktree-remove|%s|BLOCKED-DIRTY\n' "$path"
307
+ return 1
308
+ }
309
+
310
+ delete_branch() {
311
+ # Delete a branch with `git branch -D` (not -d). -d's merge check is HEAD-relative
312
+ # and this tool runs off-main, so -d would misfire; safety comes from the caller's
313
+ # preceding reverify_delete_eligible re-check against main. Emits an ACTION line.
314
+ #
315
+ # Args: $1 = branch name.
316
+ local name="$1" rc=0
317
+ cleanup_wt_git branch -D "$name" >/dev/null || rc=$?
318
+ if ((rc == 0)); then
319
+ printf 'ACTION|branch-delete|%s|OK\n' "$name"
320
+ else
321
+ printf 'ACTION|branch-delete|%s|FAILED\n' "$name"
322
+ fi
323
+ return "$rc"
324
+ }
325
+
326
+ delete_candidate() {
327
+ # Delete one candidate in the fixed order:
328
+ # 0. refuse the base branch CLEANUP_WT_BASE_BRANCH: emit
329
+ # ACTION|delete|<name>|BLOCKED-PROTECTED-BASE and return 1 before any other
330
+ # step runs. The constant is defined in
331
+ # .claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh, which every caller sources
332
+ # first.
333
+ # 1. reverify_delete_eligible (same-process ancestry/equivalence re-check),
334
+ # 2. remove_worktree_safe (only when the candidate has a worktree),
335
+ # 3. delete_branch (git branch -D).
336
+ # Any step's failure stops the sequence for that candidate (a dirty worktree, for
337
+ # example, blocks the branch deletion because git refuses to delete a branch still
338
+ # checked out in a worktree). Prunable registrations are report-only; this tool
339
+ # never executes worktree pruning.
340
+ #
341
+ # Args: $1 = branch name, $2 = worktree path (may be empty), $3 = recorded state.
342
+ local name="$1" wt_path="$2" state="$3"
343
+ # Refuse the base branch before re-verification, worktree removal, or branch
344
+ # deletion. run_apply never reaches this path for the base, because compute_protected
345
+ # already classifies it PROTECTED_CURRENT; this is the backstop for any other caller.
346
+ if [[ $name == "$CLEANUP_WT_BASE_BRANCH" ]]; then
347
+ printf 'ACTION|delete|%s|BLOCKED-PROTECTED-BASE\n' "$name"
348
+ return 1
349
+ fi
350
+ reverify_delete_eligible "$name" "$state" || return 1
351
+ if [[ -n $wt_path ]]; then
352
+ # Opt-in clear-and-retry, off unless CLEANUP_WT_CLEAR_DISPOSABLE is 1. A
353
+ # BLOCKED-DIRTY removal is retried once, and only after clear_disposable_dirt has
354
+ # emptied a worktree whose every per-file verdict is disposable and
355
+ # reverify_delete_eligible has re-confirmed eligibility in process. The retry calls
356
+ # the same unforced remove_worktree_safe, so no force flag and no new removal call
357
+ # site is introduced. Any step's failure stops the sequence and returns 1.
358
+ if ! remove_worktree_safe "$wt_path"; then
359
+ ((CLEANUP_WT_CLEAR_DISPOSABLE == 1)) || return 1
360
+ clear_disposable_dirt "$wt_path" || return 1
361
+ reverify_delete_eligible "$name" "$state" || return 1
362
+ remove_worktree_safe "$wt_path" || return 1
363
+ fi
364
+ fi
365
+ delete_branch "$name"
366
+ }
367
+
368
+ run_apply() {
369
+ # Apply-mode driver. Emits the report (WORKTREE and per-branch BRANCH/COMMIT lines)
370
+ # and then performs deletion for delete-eligible states only. The eligible-state
371
+ # gate is a single explicit allowlist: MERGED_CLEAN | MERGED_CONTENT_NEUTRAL |
372
+ # MERGED_EQUIVALENT. NOT_MERGED, HAS_UNIQUE_RESIDUALS, PROTECTED_CURRENT, and
373
+ # ANCESTRY_ERROR never trigger a destructive action. The main worktree and the base
374
+ # branch CLEANUP_WT_BASE_BRANCH are never candidates: compute_protected protects the
375
+ # base by name in every checkout topology, so classify_branch resolves it to
376
+ # PROTECTED_CURRENT, and delete_candidate refuses it as a backstop. Deletion of the
377
+ # consolidation branch (whose unique content was consolidated) is gated on
378
+ # verify_consolidation_merged() returning MERGED_CLEAN. Returns non-zero if any
379
+ # candidate's deletion failed or was blocked, or when the worktree listing, branch
380
+ # enumeration, or any branch classification hard-fails (in which case no mutation is
381
+ # performed for that failure).
382
+ local rc=0 name record wpath wbranch wflags cb_out state
383
+ local wlout wlrc=0 ebout ebrc=0
384
+ # Guarded parent-shell captures up front, before check_main_freshness and any output:
385
+ # a hard failure of the worktree listing or branch enumeration aborts apply with git's
386
+ # exit code and performs no mutation.
387
+ wlout=$(parse_worktree_list) || wlrc=$?
388
+ if ((wlrc != 0)); then
389
+ return "$wlrc"
390
+ fi
391
+ ebout=$(enumerate_branches) || ebrc=$?
392
+ if ((ebrc != 0)); then
393
+ return "$ebrc"
394
+ fi
395
+ check_main_freshness
396
+ local -A wt_of=()
397
+ while IFS= read -r record; do
398
+ [[ -z $record ]] && continue
399
+ IFS='|' read -r wpath _ wbranch wflags <<<"$record"
400
+ # A detached registration is handled by apply_detached_worktrees instead: it
401
+ # emits that registration's single WORKTREE record and owns its removal decision.
402
+ is_detached_candidate "$wflags" && continue
403
+ printf 'WORKTREE|%s|%s|%s\n' "$wpath" "$wbranch" "$wflags"
404
+ [[ -n $wbranch && $wbranch != DETACHED ]] && wt_of[$wbranch]=$wpath
405
+ done <<<"$wlout"
406
+ apply_detached_worktrees "$wlout" || rc=1
407
+ # Consolidation merge gate: unlock the consolidation branch's own deletion only
408
+ # when documentationandmemories is merged into main.
409
+ local consolidation_ok=1 vout
410
+ if cleanup_wt_git rev-parse --verify --quiet \
411
+ "refs/heads/$CLEANUP_WT_CONSOLIDATION_BRANCH" >/dev/null 2>&1; then
412
+ vout=$(verify_consolidation_merged) || true
413
+ [[ $vout == MERGED_CLEAN ]] && consolidation_ok=0
414
+ fi
415
+ # One shared classification pass for every branch, identical to the one report mode
416
+ # runs, so the two modes can never disagree about a branch's state. A non-zero driver
417
+ # return means at least one branch hard-failed; that propagates to rc here and the
418
+ # per-branch allowlist check below still gates every individual deletion.
419
+ local crc=0 all_out
420
+ all_out=$(classify_all_branches) || crc=$?
421
+ if ((crc != 0)); then
422
+ rc=1
423
+ fi
424
+ while read -r name _; do
425
+ [[ -z $name ]] && continue
426
+ if [[ $name == "$CLEANUP_WT_CONSOLIDATION_BRANCH" ]] && ((consolidation_ok != 0)); then
427
+ printf 'ACTION|delete|%s|BLOCKED-CONSOLIDATION-UNMERGED\n' "$name"
428
+ continue
429
+ fi
430
+ # Extract this branch's own lines from the shared driver's combined output. The
431
+ # name is the second pipe-delimited field of each of the three per-branch record
432
+ # types, so the filter is name-scoped and cannot pick up a sibling's records.
433
+ cb_out=$(printf '%s\n' "$all_out" | awk -F'|' -v n="$name" \
434
+ '($1 == "BRANCH" || $1 == "CHILD_OF" || $1 == "COMMIT") && $2 == n')
435
+ printf '%s\n' "$cb_out"
436
+ state=$(printf '%s\n' "$cb_out" | awk -F'|' '/^BRANCH\|/{print $3; exit}')
437
+ if [[ $state == "ANCESTRY_ERROR" ]]; then
438
+ # A branch classification hard failure never triggers deletion (its state is an
439
+ # error state, not on the allowlist) and propagates a non-zero driver rc.
440
+ rc=1
441
+ continue
442
+ fi
443
+ case "$state" in
444
+ MERGED_CLEAN | MERGED_CONTENT_NEUTRAL | MERGED_EQUIVALENT)
445
+ delete_candidate "$name" "${wt_of[$name]:-}" "$state" || rc=1
446
+ ;;
447
+ *) : ;;
448
+ esac
449
+ done <<<"$ebout"
450
+ return "$rc"
451
+ }