@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.
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- package/resources/claude-customizations/.claude/rules/shell.md +8 -7
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- 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
|
+
}
|