@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,496 @@
1
+ #!/usr/bin/env bash
2
+ # cleanup_worktrees_lib.sh: sourceable function library for the cleanup-worktrees
3
+ # tool. Provides the full classification ladder (ancestry -> content-neutral ->
4
+ # cherry-equivalence -> rename-aware blob fallback -> unique-residual selection) and
5
+ # the report driver. The git-binary test seam (cleanup_wt_git), branch/worktree
6
+ # enumeration (enumerate_branches, parse_worktree_list), path normalization
7
+ # (normalize_wt_path), the current-worktree/branch protection set (compute_protected),
8
+ # and the main-freshness warning (check_main_freshness) now live in the sibling file
9
+ # cleanup_worktrees_enumerate_lib.sh, which MUST be sourced before this file.
10
+ # Consolidation, deletion, and the CLI live in cleanup_worktrees_actions_lib.sh and
11
+ # cleanup-worktrees.sh. Splitting the library keeps every file within the 500-line cap.
12
+ #
13
+ # Sourcing contract: this library defines functions only; it never runs work at
14
+ # source time, so the wrapper and the bats suites can source it without side effects.
15
+ #
16
+ # It depends on cleanup_worktrees_enumerate_lib.sh (source that first). All git
17
+ # commands go through cleanup_wt_git so tests can stub the git binary via
18
+ # CLEANUP_WT_GIT_BIN.
19
+ #
20
+ # Git exit-code capture rule: a git command with an EXPECTED non-zero exit in the
21
+ # ladder (merge-base --is-ancestor, diff --quiet) is captured at the function-body
22
+ # level with `|| rc=$?` and mapped to a verdict token. A git command whose output is
23
+ # ITERATED or whose exit code is authoritative (cherry, diff-tree, ls-tree, rev-list)
24
+ # is captured in the PARENT shell (`out=$(cmd) || rc=$?`, then iterated over
25
+ # `<<<"$out"`) so a non-zero exit is observed rather than lost inside a process
26
+ # substitution or a pipeline. Internal hard-error verdict tokens (echo-verdict
27
+ # contract, the callee returns 0; classify_branch maps each to the report state):
28
+ # CHERRY_ERROR - non-zero `git cherry` exit (classify_cherry_equivalent).
29
+ # DIFF_TREE_ERROR - non-zero `git diff-tree` exit while probing a `+` commit's diff
30
+ # for the empty-residual case (classify_cherry_equivalent).
31
+ # RESIDUAL_ERROR - non-zero exit of the name-status `git diff-tree` read or the
32
+ # D-rung `git ls-tree` probe (classify_residual_commit).
33
+ # MINUS_PRESENT - a `- <sha>` cherry line was seen (partial-merge signal); read by
34
+ # classify_branch from the captured verdict instead of a second
35
+ # `git cherry` invocation whose exit code would be discarded.
36
+ # A hard failure of any enumeration/protection/cherry/diff-tree/ls-tree/rev-list read
37
+ # maps to the BRANCH|<name>|ANCESTRY_ERROR report state and a non-zero return. A hard
38
+ # git failure never resolves to a MERGED_* verdict.
39
+ #
40
+ # Report line contract (LC_ALL=C ordered, pipe-delimited, one record per line):
41
+ # BRANCH|<name>|<state>
42
+ # COMMIT|<branch>|<sha>|<state>|<paths-csv>|<author>|<author-date>
43
+ # WORKTREE|<path>|<branch-or-DETACHED>|<flags>
44
+ # WORKTREE|<path>|DETACHED|<state>|<flags> (detached registration; replaces the above)
45
+ # WARN|main-divergence|<local-sha>|<origin-sha>
46
+ # DIRTY|<worktree-path>|<status-porcelain-line>
47
+ # DIRTFILE|<worktree-path>|<verdict>|<detail>|<status-code>|<file-path>
48
+ # DIRTSUM|<worktree-path>|<aggregate>|<detail> (aggregate gates --clear-disposable)
49
+ # ACTION|<verb>|<target>|<result> (apply mode only; emitted by the actions lib)
50
+ # ORPHAN_DIR|<path>|<size> advisory, read-only records defined in
51
+ # STALE_REF|<refname> cleanup_worktrees_report_records_lib.sh; none of
52
+ # CHILD_OF|<branch>|<ancestor> these four unlocks a destructive action.
53
+ # WARN|registration-lost|<path>
54
+ # Branch states: NOT_MERGED | MERGED_CLEAN | MERGED_CONTENT_NEUTRAL |
55
+ # MERGED_EQUIVALENT | HAS_UNIQUE_RESIDUALS | PROTECTED_CURRENT; ANCESTRY_ERROR is a
56
+ # hard failure. Per-commit states: EQUIVALENT | CONTENT_ON_MAIN | EMPTY | UNIQUE |
57
+ # CONFLICT.
58
+
59
+ classify_ancestry() {
60
+ # First ladder rung: is <tip> an ancestor of main?
61
+ #
62
+ # Wraps `git merge-base --is-ancestor <tip> main`, capturing the exit code with
63
+ # `|| rc=$?` so set -e does not abort on the expected non-zero. Echoes exactly one
64
+ # verdict token and always returns 0:
65
+ # exit 0 -> MERGED_CLEAN (delete-eligible; zero residual commits)
66
+ # exit 1 -> NOT_ANCESTOR (continue the ladder)
67
+ # exit > 1 -> ANCESTRY_ERROR (hard failure for the branch; never "not merged")
68
+ #
69
+ # Args: $1 = branch tip (name or sha).
70
+ local tip="$1" rc=0
71
+ cleanup_wt_git merge-base --is-ancestor "$tip" main >/dev/null 2>&1 || rc=$?
72
+ if ((rc == 0)); then
73
+ printf 'MERGED_CLEAN\n'
74
+ elif ((rc == 1)); then
75
+ printf 'NOT_ANCESTOR\n'
76
+ else
77
+ printf 'ANCESTRY_ERROR\n'
78
+ fi
79
+ return 0
80
+ }
81
+
82
+ classify_content_neutral() {
83
+ # Second ladder rung: does the branch add no net content versus main?
84
+ #
85
+ # Runs `git diff --quiet main...<branch>` (three-dot: merge-base(main,branch) ->
86
+ # branch tip) with `|| rc=$?`. This short-circuit runs BEFORE any per-commit
87
+ # `git cherry` analysis and catches revert-pairs (a commit and its later revert
88
+ # on the same branch net to nothing). Echoes exactly one verdict token and always
89
+ # returns 0:
90
+ # exit 0 -> MERGED_CONTENT_NEUTRAL (delete-eligible)
91
+ # exit 1 -> NOT_NEUTRAL (continue the ladder)
92
+ # exit > 1 -> CONTENT_NEUTRAL_ERROR (hard failure for the branch)
93
+ #
94
+ # Args: $1 = branch name.
95
+ local branch="$1" rc=0
96
+ cleanup_wt_git diff --quiet "main...$branch" >/dev/null 2>&1 || rc=$?
97
+ if ((rc == 0)); then
98
+ printf 'MERGED_CONTENT_NEUTRAL\n'
99
+ elif ((rc == 1)); then
100
+ printf 'NOT_NEUTRAL\n'
101
+ else
102
+ printf 'CONTENT_NEUTRAL_ERROR\n'
103
+ fi
104
+ return 0
105
+ }
106
+
107
+ classify_cherry_equivalent() {
108
+ # Third ladder rung: patch-id equivalence via `git cherry main <branch>`.
109
+ #
110
+ # `git cherry` emits `- <sha>` when an equivalent patch already exists on main and
111
+ # `+ <sha>` when none does. A `+` commit whose diff is empty (verified with
112
+ # `git diff-tree --no-commit-id -r <sha>` producing no output) is also counted as
113
+ # equivalent (droppable empty commit). When every residual is equivalent the branch
114
+ # is content-equivalent-merged.
115
+ #
116
+ # Output contract (one token per line; always returns 0):
117
+ # - if all residuals are equivalent: a single line `MERGED_EQUIVALENT`
118
+ # - otherwise: a `MINUS_PRESENT` line first (only when any `- <sha>` cherry line
119
+ # was seen), then one `RESIDUAL <sha>` line per remaining `+` commit for the
120
+ # blob-level tier (classify_residual_commit). classify_branch reads MINUS_PRESENT
121
+ # from this captured verdict instead of re-invoking `git cherry`.
122
+ # - on a hard `git cherry` failure (non-zero exit): a single line `CHERRY_ERROR`.
123
+ # `git cherry` has no expected non-zero in this ladder, so any non-zero exit is
124
+ # a hard failure; the caller (classify_branch) maps CHERRY_ERROR to the
125
+ # ANCESTRY_ERROR report state and never to MERGED_EQUIVALENT.
126
+ # - on a hard `git diff-tree` failure while probing a `+` commit's diff (non-zero
127
+ # exit): a single line `DIFF_TREE_ERROR`. diff-tree has no expected non-zero
128
+ # here, so any non-zero exit is a hard failure; the caller maps DIFF_TREE_ERROR
129
+ # to ANCESTRY_ERROR and never to MERGED_EQUIVALENT. An empty diff-tree output
130
+ # with exit 0 remains the legitimate droppable empty-commit case.
131
+ #
132
+ # Args: $1 = branch name.
133
+ local branch="$1" rc=0 line marker sha out minus_seen=0
134
+ local -a residuals=()
135
+ # Guarded parent-shell capture: a non-zero `git cherry` exit is observed here and
136
+ # reported as CHERRY_ERROR before any residual processing, so an empty/failed cherry
137
+ # result cannot be mistaken for "all residuals equivalent".
138
+ out=$(cleanup_wt_git cherry main "$branch") || rc=$?
139
+ if ((rc != 0)); then
140
+ printf 'CHERRY_ERROR\n'
141
+ return 0
142
+ fi
143
+ while IFS= read -r line || [[ -n $line ]]; do
144
+ [[ -z $line ]] && continue
145
+ marker=${line%% *}
146
+ sha=${line#* }
147
+ # `-` lines are patch-id equivalent: nothing to carry forward, but record that a
148
+ # partial-merge signal exists so classify_branch need not re-invoke `git cherry`.
149
+ if [[ $marker == "-" ]]; then
150
+ minus_seen=1
151
+ continue
152
+ fi
153
+ if [[ $marker == "+" ]]; then
154
+ local dt="" dtrc=0
155
+ # Guarded parent-shell capture: a non-zero diff-tree exit is a hard failure
156
+ # reported as DIFF_TREE_ERROR, never mistaken for an empty (droppable) diff.
157
+ dt=$(cleanup_wt_git diff-tree --no-commit-id -r "$sha") || dtrc=$?
158
+ if ((dtrc != 0)); then
159
+ printf 'DIFF_TREE_ERROR\n'
160
+ return 0
161
+ fi
162
+ # An empty-diff residual commit is droppable; treat as equivalent.
163
+ [[ -z $dt ]] && continue
164
+ residuals+=("$sha")
165
+ fi
166
+ done <<<"$out"
167
+ if ((${#residuals[@]} == 0)); then
168
+ printf 'MERGED_EQUIVALENT\n'
169
+ else
170
+ ((minus_seen == 1)) && printf 'MINUS_PRESENT\n'
171
+ for sha in "${residuals[@]}"; do
172
+ printf 'RESIDUAL %s\n' "$sha"
173
+ done
174
+ fi
175
+ return 0
176
+ }
177
+
178
+ _blob_equal() {
179
+ # Return 0 iff <ref-a>:<path> and <ref-b>:<path> resolve to the same blob OID.
180
+ #
181
+ # A rev-parse failure for either side (e.g. the path is absent on that ref) is
182
+ # treated as "not equal" (return 1) so an added/removed path is never mistaken for
183
+ # equivalent content.
184
+ #
185
+ # Args: $1 = ref-a, $2 = ref-b, $3 = path.
186
+ local ref_a="$1" ref_b="$2" path="$3" oid_a oid_b
187
+ oid_a=$(cleanup_wt_git rev-parse "$ref_a:$path" 2>/dev/null) || return 1
188
+ oid_b=$(cleanup_wt_git rev-parse "$ref_b:$path" 2>/dev/null) || return 1
189
+ [[ -n $oid_a && $oid_a == "$oid_b" ]]
190
+ }
191
+
192
+ classify_residual_commit() {
193
+ # Fourth ladder rung: rename-aware blob-OID comparison for one `+` commit.
194
+ #
195
+ # Enumerates touched paths via
196
+ # git diff-tree --no-commit-id --name-status -r -M <sha>
197
+ # and decides per path whether the branch's content already exists on main:
198
+ # A / M : compare blob OIDs of <branch>:<path> vs main:<path>; differ or absent
199
+ # on main -> unique.
200
+ # D : probe `git ls-tree main -- <path>` (guarded capture). Non-empty output
201
+ # means the path is still present on main -> the branch's deletion is
202
+ # unique work; empty output (exit 0) means the path is also absent on main
203
+ # -> droppable. A non-zero ls-tree exit is a hard failure -> RESIDUAL_ERROR.
204
+ # Rnnn : compare blob OIDs at the NEW path (rename target).
205
+ # All touched paths equivalent -> CONTENT_ON_MAIN; any unique path -> UNIQUE with
206
+ # the comma-separated unique path list.
207
+ #
208
+ # Output contract (single line; always returns 0):
209
+ # CONTENT_ON_MAIN
210
+ # UNIQUE|<path1,path2,...>
211
+ # RESIDUAL_ERROR (hard git failure of the name-status diff-tree read or the
212
+ # D-rung ls-tree probe; never resolved to CONTENT_ON_MAIN/UNIQUE)
213
+ #
214
+ # Args: $1 = branch name, $2 = commit sha.
215
+ local branch="$1" sha="$2" status p1 p2 relpath out dtrc=0
216
+ local -a unique_paths=()
217
+ # Guarded parent-shell capture: a non-zero name-status diff-tree exit is a hard
218
+ # failure reported as RESIDUAL_ERROR before any path processing, never resolved to
219
+ # CONTENT_ON_MAIN or UNIQUE.
220
+ out=$(cleanup_wt_git diff-tree --no-commit-id --name-status -r -M "$sha") || dtrc=$?
221
+ if ((dtrc != 0)); then
222
+ printf 'RESIDUAL_ERROR\n'
223
+ return 0
224
+ fi
225
+ while IFS=$'\t' read -r status p1 p2 || [[ -n $status ]]; do
226
+ [[ -z $status ]] && continue
227
+ case "$status" in
228
+ A | M)
229
+ relpath="$p1"
230
+ _blob_equal "$branch" main "$relpath" || unique_paths+=("$relpath")
231
+ ;;
232
+ D)
233
+ relpath="$p1"
234
+ # Guarded ls-tree probe: a hard git failure (RESIDUAL_ERROR) is distinct from a
235
+ # path legitimately absent on main (droppable). rev-parse's exit code cannot
236
+ # make this distinction, so use ls-tree's stdout: non-empty -> present on main
237
+ # (the deletion is unique work); empty with exit 0 -> absent (droppable).
238
+ local lsout="" lsrc=0
239
+ lsout=$(cleanup_wt_git ls-tree main -- "$relpath") || lsrc=$?
240
+ if ((lsrc != 0)); then
241
+ printf 'RESIDUAL_ERROR\n'
242
+ return 0
243
+ fi
244
+ [[ -n $lsout ]] && unique_paths+=("$relpath")
245
+ ;;
246
+ R*)
247
+ relpath="$p2"
248
+ _blob_equal "$branch" main "$relpath" || unique_paths+=("$relpath")
249
+ ;;
250
+ esac
251
+ done <<<"$out"
252
+ if ((${#unique_paths[@]} == 0)); then
253
+ printf 'CONTENT_ON_MAIN\n'
254
+ else
255
+ local IFS=,
256
+ printf 'UNIQUE|%s\n' "${unique_paths[*]}"
257
+ fi
258
+ return 0
259
+ }
260
+
261
+ select_cherry_pick_candidates() {
262
+ # Emit one COMMIT record per UNIQUE residual commit, oldest-first.
263
+ #
264
+ # The non-equivalent `+` residual set comes from classify_cherry_equivalent; the
265
+ # oldest-first order and per-commit author/author-date come from
266
+ # git rev-list --reverse --no-merges --format='%H|%an|%aI' main..<branch>
267
+ # (`--reverse` = application order for cherry-picking; `--no-merges` excludes merge
268
+ # commits; the `commit <sha>` header lines from --format are dropped). Emits, per
269
+ # UNIQUE residual: COMMIT|<branch>|<sha>|UNIQUE|<paths-csv>|<author>|<author-date>.
270
+ #
271
+ # Args: $1 = branch name.
272
+ local branch="$1" line sha author date verdict paths restline rc=0
273
+ local ceout cerc=0 rlout
274
+ local -A is_residual=()
275
+ # Defensive re-check of the cherry verdict: a CHERRY_ERROR (or a non-zero return)
276
+ # must abort with a non-zero status rather than proceed with an empty residual map.
277
+ ceout=$(classify_cherry_equivalent "$branch") || cerc=$?
278
+ if ((cerc != 0)) || [[ $ceout == "CHERRY_ERROR" || $ceout == "DIFF_TREE_ERROR" ]]; then
279
+ return 2
280
+ fi
281
+ while IFS= read -r line; do
282
+ [[ $line == RESIDUAL\ * ]] || continue
283
+ is_residual[${line#RESIDUAL }]=1
284
+ done <<<"$ceout"
285
+ # Guarded parent-shell capture: a rev-list hard failure returns git's exit code
286
+ # with no COMMIT line emitted, so a candidate is never silently fabricated or
287
+ # dropped as a success.
288
+ rlout=$(cleanup_wt_git rev-list --reverse --no-merges --format='%H|%an|%aI' "main..$branch") || rc=$?
289
+ if ((rc != 0)); then
290
+ printf 'cleanup-worktrees: git rev-list failed for %s (rc=%s)\n' "$branch" "$rc" >&2
291
+ return "$rc"
292
+ fi
293
+ while IFS= read -r line || [[ -n $line ]]; do
294
+ [[ $line == commit\ * ]] && continue
295
+ [[ -z $line ]] && continue
296
+ sha=${line%%|*}
297
+ restline=${line#*|}
298
+ author=${restline%%|*}
299
+ date=${restline#*|}
300
+ [[ -n ${is_residual[$sha]:-} ]] || continue
301
+ verdict=$(classify_residual_commit "$branch" "$sha")
302
+ if [[ $verdict == "RESIDUAL_ERROR" ]]; then
303
+ # A residual hard error must abort candidate selection with a non-zero status
304
+ # rather than silently drop or fabricate a COMMIT record.
305
+ return 2
306
+ fi
307
+ if [[ $verdict == UNIQUE\|* ]]; then
308
+ paths=${verdict#UNIQUE|}
309
+ printf 'COMMIT|%s|%s|UNIQUE|%s|%s|%s\n' "$branch" "$sha" "$paths" "$author" "$date"
310
+ fi
311
+ done <<<"$rlout"
312
+ return "$rc"
313
+ }
314
+
315
+ classify_branch() {
316
+ # Orchestrate the full classification ladder for one branch and emit its pinned
317
+ # report lines (BRANCH first, then COMMIT records for unique residuals oldest-first).
318
+ # No commit-message text is ever consulted. Ladder order (spec):
319
+ # 1. PROTECTED_CURRENT exclusion (branch-name OR worktree-path match; main worktree
320
+ # and base branch CLEANUP_WT_BASE_BRANCH always protected).
321
+ # 2. ancestry -> MERGED_CLEAN, or ANCESTRY_ERROR (hard fail).
322
+ # 3. content-neutral short-circuit -> MERGED_CONTENT_NEUTRAL.
323
+ # 4. cherry patch-id equivalence -> MERGED_EQUIVALENT when all residuals equiv.
324
+ # 5. rename-aware blob fallback: residuals all CONTENT_ON_MAIN -> MERGED_EQUIVALENT.
325
+ # 6. remaining unique residuals -> HAS_UNIQUE_RESIDUALS when the branch was
326
+ # partially incorporated on main (a cherry `-` line or a CONTENT_ON_MAIN
327
+ # residual), else NOT_MERGED (purely unmerged code).
328
+ #
329
+ # Args: $1 = branch name. Returns 0 normally; 2 on any hard error surfaced by the
330
+ # enumeration/protection reads (worktree-list, protected-set), the cherry rung
331
+ # (CHERRY_ERROR), the empty-residual diff-tree probe (DIFF_TREE_ERROR), the
332
+ # name-status/ls-tree residual reads (RESIDUAL_ERROR), or rev-list candidate
333
+ # selection. A hard git failure always maps to the ANCESTRY_ERROR report state and a
334
+ # non-zero return, never a MERGED verdict.
335
+ local name="$1"
336
+ local -A prot_branch=() prot_path=()
337
+ local pline cpout cprc=0
338
+ # Guarded parent-shell capture of the protected set: a compute_protected hard
339
+ # failure (a git worktree-list failure underneath) must map to ANCESTRY_ERROR, not
340
+ # a weakened/empty protected set that could let a real branch reach a MERGED verdict.
341
+ cpout=$(compute_protected) || cprc=$?
342
+ if ((cprc != 0)); then
343
+ printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
344
+ return 2
345
+ fi
346
+ while IFS= read -r pline; do
347
+ case "$pline" in
348
+ protected-branch\|*) prot_branch[${pline#protected-branch|}]=1 ;;
349
+ protected-path\|*) prot_path[${pline#protected-path|}]=1 ;;
350
+ esac
351
+ done <<<"$cpout"
352
+ # Locate this branch's worktree path (normalized), if any, from a guarded capture
353
+ # of the worktree list; a hard failure here likewise maps to ANCESTRY_ERROR.
354
+ local wt_norm="" record wpath wbranch wlout wlrc=0
355
+ wlout=$(parse_worktree_list) || wlrc=$?
356
+ if ((wlrc != 0)); then
357
+ printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
358
+ return 2
359
+ fi
360
+ while IFS= read -r record; do
361
+ [[ -z $record ]] && continue
362
+ IFS='|' read -r wpath _ wbranch _ <<<"$record"
363
+ if [[ $wbranch == "$name" ]]; then
364
+ wt_norm=$(normalize_wt_path "$wpath")
365
+ break
366
+ fi
367
+ done <<<"$wlout"
368
+ if [[ -n ${prot_branch[$name]:-} ]] || { [[ -n $wt_norm && -n ${prot_path[$wt_norm]:-} ]]; }; then
369
+ printf 'BRANCH|%s|PROTECTED_CURRENT\n' "$name"
370
+ return 0
371
+ fi
372
+ local v
373
+ v=$(classify_ancestry "$name")
374
+ case "$v" in
375
+ MERGED_CLEAN)
376
+ printf 'BRANCH|%s|MERGED_CLEAN\n' "$name"
377
+ return 0
378
+ ;;
379
+ ANCESTRY_ERROR)
380
+ printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
381
+ return 2
382
+ ;;
383
+ esac
384
+ v=$(classify_content_neutral "$name")
385
+ case "$v" in
386
+ MERGED_CONTENT_NEUTRAL)
387
+ printf 'BRANCH|%s|MERGED_CONTENT_NEUTRAL\n' "$name"
388
+ return 0
389
+ ;;
390
+ CONTENT_NEUTRAL_ERROR)
391
+ printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
392
+ return 2
393
+ ;;
394
+ esac
395
+ local ce
396
+ ce=$(classify_cherry_equivalent "$name")
397
+ if [[ $ce == "CHERRY_ERROR" || $ce == "DIFF_TREE_ERROR" ]]; then
398
+ # Hard `git cherry` or `git diff-tree` failure: same mapping as
399
+ # CONTENT_NEUTRAL_ERROR (ANCESTRY_ERROR report state, hard-fail return 2).
400
+ printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
401
+ return 2
402
+ fi
403
+ if [[ $ce == "MERGED_EQUIVALENT" ]]; then
404
+ printf 'BRANCH|%s|MERGED_EQUIVALENT\n' "$name"
405
+ return 0
406
+ fi
407
+ # Residual `+` commits exist; resolve each via the blob tier.
408
+ local sha verdict unique_count=0 content_count=0
409
+ while IFS= read -r pline; do
410
+ [[ $pline == RESIDUAL\ * ]] || continue
411
+ sha=${pline#RESIDUAL }
412
+ verdict=$(classify_residual_commit "$name" "$sha")
413
+ if [[ $verdict == "RESIDUAL_ERROR" ]]; then
414
+ # Hard failure of the name-status diff-tree read or the D-rung ls-tree probe:
415
+ # map to ANCESTRY_ERROR before any state line is emitted.
416
+ printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
417
+ return 2
418
+ fi
419
+ if [[ $verdict == UNIQUE\|* ]]; then
420
+ unique_count=$((unique_count + 1))
421
+ elif [[ $verdict == "CONTENT_ON_MAIN" ]]; then
422
+ content_count=$((content_count + 1))
423
+ fi
424
+ done < <(printf '%s\n' "$ce")
425
+ if ((unique_count == 0)); then
426
+ # Every residual was content-on-main; the branch is content-equivalent-merged.
427
+ printf 'BRANCH|%s|MERGED_EQUIVALENT\n' "$name"
428
+ return 0
429
+ fi
430
+ # Partial-merge signal: was any part of this branch already incorporated on main?
431
+ # Derived from the already-captured cherry verdict (MINUS_PRESENT token) instead of a
432
+ # second `git cherry` invocation whose exit code would be discarded in a pipeline.
433
+ local minus_present=0
434
+ [[ $ce == *MINUS_PRESENT* ]] && minus_present=1
435
+ local state="NOT_MERGED"
436
+ if ((minus_present == 1)) || ((content_count > 0)); then
437
+ state="HAS_UNIQUE_RESIDUALS"
438
+ fi
439
+ printf 'BRANCH|%s|%s\n' "$name" "$state"
440
+ if [[ $state == "HAS_UNIQUE_RESIDUALS" ]]; then
441
+ # Propagate a rev-list/cherry hard failure: the BRANCH line already names a
442
+ # non-delete-eligible state, and the non-zero return signals the hard error.
443
+ local scrc=0
444
+ select_cherry_pick_candidates "$name" || scrc=$?
445
+ if ((scrc != 0)); then
446
+ return 2
447
+ fi
448
+ fi
449
+ return 0
450
+ }
451
+
452
+ run_report() {
453
+ # Report-mode driver: emit the deterministic report with no mutation of any kind.
454
+ # Emission order: WARN (freshness), then the pre-branch advisory scans via the single
455
+ # run_report_scans call, which emits STALE_REF, ORPHAN_DIR, and WARN|registration-lost
456
+ # in that order from ONE filesystem scan (none depends on per-branch classification,
457
+ # so all three are colocated with the freshness warning), then WORKTREE registrations,
458
+ # then the per-branch BRANCH/CHILD_OF/COMMIT lines in enumerate_branches' LC_ALL=C
459
+ # order. Those come from classify_all_branches, the shared driver apply mode also
460
+ # uses, so both modes derive one identical classification. Returns the maximum return
461
+ # code observed. A worktree-list OR enumerate-branches hard failure aborts the report
462
+ # before any line is emitted and returns git's non-zero exit code, so a git failure
463
+ # never resolves to a partial, misleading report.
464
+ local rc=0 crc record wpath wbranch wflags wlout wlrc=0 ebrc=0
465
+ # Guarded parent-shell captures up front, before any output: a hard failure of either
466
+ # read aborts the report before any WARN/WORKTREE/BRANCH line. The branch list itself
467
+ # is re-read by classify_all_branches; this probe exists only for the early abort.
468
+ wlout=$(parse_worktree_list) || wlrc=$?
469
+ if ((wlrc != 0)); then
470
+ return "$wlrc"
471
+ fi
472
+ enumerate_branches >/dev/null || ebrc=$?
473
+ if ((ebrc != 0)); then
474
+ return "$ebrc"
475
+ fi
476
+ check_main_freshness
477
+ run_report_scans || rc=$?
478
+ while IFS= read -r record; do
479
+ [[ -z $record ]] && continue
480
+ IFS='|' read -r wpath _ wbranch wflags <<<"$record"
481
+ # A detached registration is emitted by report_detached_worktrees instead.
482
+ is_detached_candidate "$wflags" && continue
483
+ printf 'WORKTREE|%s|%s|%s\n' "$wpath" "$wbranch" "$wflags"
484
+ # Dirt verdicts for a candidate registration only; main and bare are never candidates.
485
+ if [[ ,$wflags, != *,main,* && ,$wflags, != *,bare,* ]]; then
486
+ classify_worktree_dirt "$wpath" || rc=$?
487
+ fi
488
+ done <<<"$wlout"
489
+ report_detached_worktrees "$wlout" || rc=$?
490
+ crc=0
491
+ classify_all_branches || crc=$?
492
+ if ((crc > rc)); then
493
+ rc=$crc
494
+ fi
495
+ return "$rc"
496
+ }