@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -0,0 +1,330 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Parses `gh pr checks` JSON into the orchestrator `ci_gate` object defined by
4
+ Step S9 (CI Green Gate) of .claude/skills/orchestrate/SKILL.md.
5
+
6
+ .DESCRIPTION
7
+ Consumes the JSON array emitted by
8
+ `gh pr checks --required --json bucket,name,state,link,workflow` and produces
9
+ the `ci_gate` object the orchestrator writes to its checkpoint. The object
10
+ carries the five contract fields: head_sha, pr_pipeline_run_id,
11
+ pr_pipeline_run_url, conclusion, and verified_at.
12
+
13
+ This script is the single source of truth for `ci_gate.conclusion`
14
+ derivation. It does NOT invoke `gh`. The orchestrator runs `gh` and passes
15
+ the resulting JSON to this script via the -ChecksJson parameter or the
16
+ pipeline, keeping all network/executable I/O outside this derivation step so
17
+ the logic is deterministically testable.
18
+
19
+ Conclusion derivation (per the documented bucket mapping):
20
+ - Any required check with bucket 'fail' or 'cancel' -> conclusion 'failure'
21
+ - Else any required check with bucket 'pending' -> conclusion 'pending'
22
+ - Else (all checks 'pass' or 'skipping', or empty set) -> conclusion 'success'
23
+ An empty required-check set is treated as 'success' (vacuously satisfied: no
24
+ required check can fail or be in progress). An unrecognized bucket value is a
25
+ fail-fast error so the parser never silently passes an enum it does not
26
+ understand.
27
+
28
+ The verified_at timestamp is produced through an injectable clock delegate
29
+ (-NowProvider) so tests can assert an exact deterministic value without
30
+ reading the wall clock.
31
+
32
+ .PARAMETER ChecksJson
33
+ The JSON array string emitted by `gh pr checks --required --json ...`. Each
34
+ element is expected to expose at least a `bucket` property. Accepts pipeline
35
+ / stdin input so the orchestrator can pipe the `gh` output directly.
36
+
37
+ .PARAMETER HeadSha
38
+ The PR head SHA the required checks were observed against. Recorded verbatim
39
+ as ci_gate.head_sha.
40
+
41
+ .PARAMETER PrPipelineRunId
42
+ Optional GitHub Actions run id for the PR Pipeline. Recorded verbatim as
43
+ ci_gate.pr_pipeline_run_id (may be empty when not supplied).
44
+
45
+ .PARAMETER PrPipelineRunUrl
46
+ Optional URL of the PR Pipeline run. Recorded verbatim as
47
+ ci_gate.pr_pipeline_run_url (may be empty when not supplied).
48
+
49
+ .PARAMETER NowProvider
50
+ Optional clock delegate (ScriptBlock) returning the value used for
51
+ ci_gate.verified_at. Defaults to a UTC Get-Date formatted as ISO-8601. Tests
52
+ inject a fixed delegate to make verified_at deterministic.
53
+
54
+ .PARAMETER AsJson
55
+ When set, emits the ci_gate object as a JSON string instead of a PowerShell
56
+ object. The default (object) output is convenient for in-process callers; the
57
+ JSON output is convenient for checkpoint serialization.
58
+
59
+ .EXAMPLE
60
+ gh pr checks --required --json bucket,name,state,link,workflow |
61
+ ./.claude/lib/ci-gate/Invoke-CiGateParser.ps1 -HeadSha $sha
62
+
63
+ .OUTPUTS
64
+ A PSCustomObject (or JSON string when -AsJson is set) with properties
65
+ head_sha, pr_pipeline_run_id, pr_pipeline_run_url, conclusion, verified_at.
66
+ #>
67
+
68
+ [CmdletBinding()]
69
+ param(
70
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
71
+ [string]$ChecksJson,
72
+
73
+ [Parameter(Mandatory = $true)]
74
+ [ValidateNotNullOrEmpty()]
75
+ [string]$HeadSha,
76
+
77
+ [Parameter(Mandatory = $false)]
78
+ [string]$PrPipelineRunId = '',
79
+
80
+ [Parameter(Mandatory = $false)]
81
+ [string]$PrPipelineRunUrl = '',
82
+
83
+ [Parameter(Mandatory = $false)]
84
+ [ValidateNotNull()]
85
+ [ScriptBlock]$NowProvider = { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') },
86
+
87
+ [Parameter(Mandatory = $false)]
88
+ [switch]$AsJson
89
+ )
90
+
91
+ # Function definitions live in begin so the script is a single explicit
92
+ # begin/process unit. This is required because the script-level -ChecksJson
93
+ # parameter accepts pipeline input, which forces a named process block; once any
94
+ # named block is used, all top-level statements must reside inside named blocks.
95
+ begin {
96
+ function Get-CiGateConclusion {
97
+ <#
98
+ .SYNOPSIS
99
+ Pure derivation of ci_gate.conclusion from a parsed required-check set.
100
+
101
+ .DESCRIPTION
102
+ Implements the documented bucket precedence with no I/O and no external
103
+ calls, so it is deterministically unit-testable in isolation:
104
+ 1. Any 'fail' or 'cancel' -> 'failure'.
105
+ 2. Else any 'pending' -> 'pending'.
106
+ 3. Else -> 'success'.
107
+ 'skipping' is non-blocking: it neither forces failure nor pending and
108
+ contributes to 'success'. An empty set yields 'success' (vacuous). Any
109
+ bucket value outside the known enum throws a fail-fast error naming the
110
+ unrecognized value, preventing a silent pass on an unknown state.
111
+
112
+ .PARAMETER Checks
113
+ The parsed required-check collection (each element exposing a `bucket`
114
+ property). May be $null or empty, which yields 'success'.
115
+
116
+ .OUTPUTS
117
+ One of the strings 'success', 'failure', or 'pending'.
118
+
119
+ .NOTES
120
+ Pure function: no filesystem, network, or `gh` access.
121
+ #>
122
+ [CmdletBinding()]
123
+ [OutputType([string])]
124
+ param(
125
+ [Parameter(Mandatory = $false)]
126
+ [AllowNull()]
127
+ [object[]]$Checks
128
+ )
129
+
130
+ # Empty or null required-check set is vacuously satisfied: there is no check
131
+ # that can fail or be in progress, so the gate concludes 'success'.
132
+ if ($null -eq $Checks -or $Checks.Count -eq 0) {
133
+ return 'success'
134
+ }
135
+
136
+ # Track whether any check is still in progress so we can defer the 'pending'
137
+ # decision until after confirming nothing failed (failure outranks pending).
138
+ $anyPending = $false
139
+
140
+ # Inspect each required check once, classifying by its bucket value. Failure
141
+ # short-circuits immediately because it is the highest-precedence outcome.
142
+ foreach ($check in $Checks) {
143
+ # A check element must expose a bucket; absence is an undefined state and
144
+ # is treated as a fail-fast error rather than silently ignored.
145
+ if ($null -eq $check -or -not ($check.PSObject.Properties.Name -contains 'bucket')) {
146
+ throw "Invoke-CiGateParser: required check is missing a 'bucket' property; cannot derive conclusion."
147
+ }
148
+
149
+ $bucket = [string]$check.bucket
150
+
151
+ # Routing table for the bucket enum. Ordering inside the switch does not
152
+ # change semantics because failure short-circuits via return; pending is
153
+ # only recorded (not returned) so a later failure can still take priority.
154
+ switch ($bucket) {
155
+ 'fail' { return 'failure' } # a failed required check is terminal-non-success
156
+ 'cancel' { return 'failure' } # a cancelled required check is treated conservatively as failure
157
+ 'pending' { $anyPending = $true } # in-progress; defer until failures are ruled out
158
+ 'pass' { } # passing check contributes to success; no state change
159
+ 'skipping' { } # skipped check is non-blocking; neither failure nor pending
160
+ default {
161
+ # Unknown bucket: fail fast and name the value so the unsupported
162
+ # enum is visible rather than silently mapped to a pass.
163
+ throw "Invoke-CiGateParser: unrecognized check bucket '$bucket'; refusing to derive a conclusion for an unknown state."
164
+ }
165
+ }
166
+ }
167
+
168
+ # No failure or cancel was seen. If any check was pending the gate is still
169
+ # in progress; otherwise every check was pass/skipping and the gate succeeds.
170
+ if ($anyPending) {
171
+ return 'pending'
172
+ }
173
+
174
+ return 'success'
175
+ }
176
+
177
+ function ConvertTo-CiGateObject {
178
+ <#
179
+ .SYNOPSIS
180
+ Thin constructor for the ci_gate object from already-derived inputs.
181
+
182
+ .DESCRIPTION
183
+ Assembles the five contract fields into a PSCustomObject. Exists as a
184
+ small helper so the wrapper body stays readable and so the field set is
185
+ defined in exactly one place. Performs no derivation and no I/O.
186
+
187
+ .PARAMETER HeadSha
188
+ Recorded as head_sha.
189
+ .PARAMETER PrPipelineRunId
190
+ Recorded as pr_pipeline_run_id.
191
+ .PARAMETER PrPipelineRunUrl
192
+ Recorded as pr_pipeline_run_url.
193
+ .PARAMETER Conclusion
194
+ The already-derived conclusion ('success'/'failure'/'pending').
195
+ .PARAMETER VerifiedAt
196
+ The already-resolved ISO-8601 timestamp string.
197
+
198
+ .OUTPUTS
199
+ A PSCustomObject with the five ci_gate properties.
200
+ #>
201
+ [CmdletBinding()]
202
+ [OutputType([pscustomobject])]
203
+ param(
204
+ [Parameter(Mandatory = $true)][string]$HeadSha,
205
+ [Parameter(Mandatory = $true)][AllowEmptyString()][string]$PrPipelineRunId,
206
+ [Parameter(Mandatory = $true)][AllowEmptyString()][string]$PrPipelineRunUrl,
207
+ [Parameter(Mandatory = $true)][string]$Conclusion,
208
+ [Parameter(Mandatory = $true)][string]$VerifiedAt
209
+ )
210
+
211
+ return [pscustomobject]@{
212
+ head_sha = $HeadSha
213
+ pr_pipeline_run_id = $PrPipelineRunId
214
+ pr_pipeline_run_url = $PrPipelineRunUrl
215
+ conclusion = $Conclusion
216
+ verified_at = $VerifiedAt
217
+ }
218
+ }
219
+
220
+ function Invoke-CiGateParser {
221
+ <#
222
+ .SYNOPSIS
223
+ Wrapper that parses the checks JSON, derives the conclusion, and returns
224
+ the ci_gate object.
225
+
226
+ .DESCRIPTION
227
+ Thin orchestration over the pure helpers: it parses -ChecksJson (failing
228
+ fast on malformed JSON), normalizes the result into an array, delegates
229
+ conclusion derivation to Get-CiGateConclusion, resolves verified_at via
230
+ the injected clock delegate, and assembles the object via
231
+ ConvertTo-CiGateObject. The wrapper exists to separate the impure JSON-parsing
232
+ and clock concerns from the pure derivation logic.
233
+
234
+ .PARAMETER ChecksJson
235
+ See script-level parameter of the same name.
236
+ .PARAMETER HeadSha
237
+ See script-level parameter of the same name.
238
+ .PARAMETER PrPipelineRunId
239
+ See script-level parameter of the same name.
240
+ .PARAMETER PrPipelineRunUrl
241
+ See script-level parameter of the same name.
242
+ .PARAMETER NowProvider
243
+ See script-level parameter of the same name.
244
+ .PARAMETER AsJson
245
+ See script-level parameter of the same name.
246
+
247
+ .OUTPUTS
248
+ PSCustomObject, or JSON string when -AsJson is set.
249
+
250
+ .NOTES
251
+ Throws on malformed JSON and on unrecognized bucket values (fail-fast).
252
+ #>
253
+ [CmdletBinding()]
254
+ param(
255
+ [Parameter(Mandatory = $true)]
256
+ [string]$ChecksJson,
257
+
258
+ [Parameter(Mandatory = $true)]
259
+ [ValidateNotNullOrEmpty()]
260
+ [string]$HeadSha,
261
+
262
+ [Parameter(Mandatory = $false)]
263
+ [string]$PrPipelineRunId = '',
264
+
265
+ [Parameter(Mandatory = $false)]
266
+ [string]$PrPipelineRunUrl = '',
267
+
268
+ [Parameter(Mandatory = $false)]
269
+ [ValidateNotNull()]
270
+ [ScriptBlock]$NowProvider = { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') },
271
+
272
+ [Parameter(Mandatory = $false)]
273
+ [switch]$AsJson
274
+ )
275
+
276
+ # Parse the gh JSON. ConvertFrom-Json throws on malformed input; wrap it so
277
+ # the failure surfaces as an explicit, parser-attributed error rather than a
278
+ # raw deserialization message, satisfying fail-fast-and-explicitly policy.
279
+ try {
280
+ $parsed = $ChecksJson | ConvertFrom-Json -ErrorAction Stop
281
+ }
282
+ catch {
283
+ throw "Invoke-CiGateParser: malformed checks JSON could not be parsed: $($_.Exception.Message)"
284
+ }
285
+
286
+ # Normalize to an array. ConvertFrom-Json yields a single object for a
287
+ # one-element payload and $null for the literal 'null'; @() coercion makes the
288
+ # downstream Count/foreach logic uniform for the empty, single, and many cases.
289
+ $checks = @($parsed)
290
+
291
+ $conclusion = Get-CiGateConclusion -Checks $checks
292
+
293
+ # Resolve the timestamp through the injected delegate so tests can pin an
294
+ # exact value; the default delegate reads UTC wall-clock time.
295
+ $verifiedAt = [string](& $NowProvider)
296
+
297
+ $ciGate = ConvertTo-CiGateObject `
298
+ -HeadSha $HeadSha `
299
+ -PrPipelineRunId $PrPipelineRunId `
300
+ -PrPipelineRunUrl $PrPipelineRunUrl `
301
+ -Conclusion $conclusion `
302
+ -VerifiedAt $verifiedAt
303
+
304
+ # Emit either the object (default, for in-process callers) or its JSON
305
+ # serialization (for direct checkpoint writing) based on the caller's choice.
306
+ if ($AsJson) {
307
+ return ($ciGate | ConvertTo-Json -Depth 5)
308
+ }
309
+
310
+ return $ciGate
311
+ }
312
+ }
313
+
314
+ # Entry point. The script-level -ChecksJson parameter accepts pipeline input, so
315
+ # the entry-point execution lives in a process block: each piped JSON payload is
316
+ # handled as it arrives. The parser runs only when the file is executed directly;
317
+ # when dot-sourced for testing, $MyInvocation.InvocationName is '.', so the guard
318
+ # suppresses execution and lets tests invoke the functions with controlled inputs.
319
+ process {
320
+ if ($MyInvocation.InvocationName -ne '.') {
321
+ Invoke-CiGateParser `
322
+ -ChecksJson $ChecksJson `
323
+ -HeadSha $HeadSha `
324
+ -PrPipelineRunId $PrPipelineRunId `
325
+ -PrPipelineRunUrl $PrPipelineRunUrl `
326
+ -NowProvider $NowProvider `
327
+ -AsJson:$AsJson
328
+ }
329
+ }
330
+
@@ -45,10 +45,11 @@ single pass. Do not substitute the VS Code task wrappers for the native command
45
45
 
46
46
  ## Discovery Contract
47
47
 
48
- - Search roots: `tools/`, `scripts/`, and `.claude/lib/bash/`, relative to the current working
49
- directory; a missing root is silently skipped. The `.claude/lib/bash/` root carries the
50
- destination-portable bash library published by push-down, so those scripts are held to the
51
- same format, lint, test, and coverage standards as `tools/` and `scripts/`.
48
+ - Search roots: `tools/`, `scripts/`, `.claude/lib/bash/`, and `.claude/skills/`, relative to the
49
+ current working directory; a missing root is silently skipped. The `.claude/lib/bash/` root carries
50
+ the destination-portable bash library published by push-down, and the `.claude/skills/` root carries
51
+ scripts bundled inside a skill folder, so those scripts are held to the same format, lint, test, and
52
+ coverage standards as `tools/` and `scripts/`.
52
53
  - A file is a shell script when its suffix (lowercased) is `.sh` or its first line is a
53
54
  shebang whose resolved interpreter is `bash` or `sh` (including `env` and `env -S`/`-flag`
54
55
  forms; the shebang is lowercased before parsing, so `#!/usr/bin/env BASH` qualifies).
@@ -62,9 +63,9 @@ single pass. Do not substitute the VS Code task wrappers for the native command
62
63
  - Coverage is measured with kcov, which emits a single merged Cobertura report `cov.xml` under
63
64
  `artifacts/pester/kcov` (or `SHELL_QC_KCOV_OUT_DIR`). The run prints
64
65
  `Bash coverage (lines): NN.N%`.
65
- - The kcov include pattern covers all three discovery roots — `tools/`, `scripts/`, and
66
- `.claude/lib/bash/` — so the Claude bash library is measured, not merely discovered. The
67
- `tests/` tree remains excluded.
66
+ - The kcov include pattern covers all four discovery roots — `tools/`, `scripts/`,
67
+ `.claude/lib/bash/`, and `.claude/skills/` — so the Claude bash library and skill-bundled scripts are
68
+ measured, not merely discovered. The `tests/` tree remains excluded.
68
69
  - kcov reports **line coverage only**. The uniform line-coverage threshold (>= 85% per
69
70
  `.claude/rules/quality-tiers.md`) applies. Branch coverage is not measurable by kcov for
70
71
  bash; there is no bash branch-coverage gate.
@@ -6,7 +6,7 @@ allowed-tools:
6
6
  - Grep
7
7
  - Glob
8
8
  - Agent
9
- - "Bash(bash scripts/bash/cleanup-worktrees.sh *)"
9
+ - "Bash(bash .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh *)"
10
10
  - "Bash(git fetch *)"
11
11
  - "Bash(git merge-base *)"
12
12
  - "Bash(git push *)"
@@ -27,9 +27,9 @@ allowed-tools:
27
27
 
28
28
  Drive the end-to-end cleanup of stale git worktrees and branches after their work has
29
29
  merged into `main`. The deterministic classification, consolidation staging, and
30
- deletion mechanics live in `scripts/bash/cleanup-worktrees.sh` (wrapping
31
- `scripts/bash/cleanup_worktrees_lib.sh` and
32
- `scripts/bash/cleanup_worktrees_actions_lib.sh`). This skill owns the editorial and
30
+ deletion mechanics live in `.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh` (wrapping
31
+ `.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh` and
32
+ `.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh`). This skill owns the editorial and
33
33
  orchestration layer: deciding whether flagged unique content is genuinely
34
34
  documentation/memory material, driving consolidation onto a single
35
35
  `documentationandmemories` branch, delegating PR creation to `Agent(pr-author)`, and
@@ -145,7 +145,7 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
145
145
 
146
146
  ## End-to-End Workflow
147
147
 
148
- 1. **Detect and report (dry run).** Run `bash scripts/bash/cleanup-worktrees.sh`
148
+ 1. **Detect and report (dry run).** Run `bash .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh`
149
149
  (report mode is the default and mutates nothing). It verifies local `main` against
150
150
  `origin/main` (emitting `WARN|main-divergence` on drift), enumerates branches and
151
151
  worktrees, and prints one `BRANCH|` line per branch plus `COMMIT|...|UNIQUE|...`
@@ -203,7 +203,7 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
203
203
  consolidated commit is now reachable from `main`; that is the only state that unlocks
204
204
  deletion of branches whose unique content was consolidated.
205
205
 
206
- 6. **Run the apply-mode deletion.** Run `bash scripts/bash/cleanup-worktrees.sh --apply`.
206
+ 6. **Run the apply-mode deletion.** Run `bash .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh --apply`.
207
207
  It re-verifies each candidate's ancestry/equivalence in-process, removes worktrees
208
208
  (without force; a dirty worktree is reported via `DIRTY|` lines and skipped), then
209
209
  deletes branches with `git branch -D`. The now-merged `documentationandmemories`
@@ -241,7 +241,7 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
241
241
 
242
242
  When report mode classifies every candidate as `MERGED_CLEAN` or `MERGED_EQUIVALENT`
243
243
  with an empty cherry-pick-candidate list, skip steps 3-5 entirely: proceed directly from
244
- the report to `bash scripts/bash/cleanup-worktrees.sh --apply`. Cleanup completes in a
244
+ the report to `bash .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh --apply`. Cleanup completes in a
245
245
  single session with no PR.
246
246
 
247
247
  ## Dirty Worktree Triage Procedure
@@ -464,7 +464,7 @@ These records serve the consolidation consumer and are never read by either gate
464
464
  "branch_state": "HAS_UNIQUE_RESIDUALS",
465
465
  "removal_disposition": "SAFE_TO_DELETE",
466
466
  "verdict": "ALREADY_SOLVED_ELSEWHERE",
467
- "evidence": "Unique residual commit 3f9a1c2 records the cleanup-worktrees ancestry error; main already fixes it at scripts/bash/cleanup_worktrees_lib.sh:214-231 under issue #612."
467
+ "evidence": "Unique residual commit 3f9a1c2 records the cleanup-worktrees ancestry error; main already fixes it at .claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh:214-231 under issue #612."
468
468
  }
469
469
  ],
470
470
  "preserved_files": [
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env bash
2
+ # cleanup-worktrees.sh: thin CLI wrapper for the cleanup-merged-worktrees tool. It
3
+ # classifies local branches/worktrees against main and, in apply mode only, deletes
4
+ # delete-eligible candidates and drives consolidation. All logic lives in the two
5
+ # sourceable libraries so the functions are unit-testable from bats by sourcing them
6
+ # directly; this wrapper only resolves paths, prints usage, and dispatches.
7
+ set -euo pipefail
8
+
9
+ # Resolve this script's own directory so the libraries source regardless of cwd.
10
+ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
11
+ # The library paths are resolved at runtime from SCRIPT_DIR, so shellcheck cannot load
12
+ # them as static inputs without -x; SC1091 is the expected, benign result. The
13
+ # enumeration/protection library is sourced first because cleanup_worktrees_lib.sh's
14
+ # classification functions call cleanup_wt_git, parse_worktree_list, compute_protected,
15
+ # and normalize_wt_path defined there.
16
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh
17
+ # shellcheck disable=SC1091
18
+ source "$SCRIPT_DIR/cleanup_worktrees_enumerate_lib.sh"
19
+ # The report-record library depends on the enumeration library above and is called BY
20
+ # run_report/run_apply in the two libraries sourced below, so it is sourced between them.
21
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh
22
+ # shellcheck disable=SC1091
23
+ source "$SCRIPT_DIR/cleanup_worktrees_report_records_lib.sh"
24
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh
25
+ # shellcheck disable=SC1091
26
+ source "$SCRIPT_DIR/cleanup_worktrees_lib.sh"
27
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh
28
+ # shellcheck disable=SC1091
29
+ source "$SCRIPT_DIR/cleanup_worktrees_actions_lib.sh"
30
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh
31
+ # shellcheck disable=SC1091
32
+ source "$SCRIPT_DIR/cleanup_worktrees_detached_lib.sh"
33
+ # Neither of the three libraries below requires being sourced last, and neither claims to
34
+ # be. Each library's source-time work is limited to function definitions plus constant and
35
+ # environment-default variable assignments; none of them calls a function while being
36
+ # sourced, and no function name is defined by more than one library, so no later source can
37
+ # shadow an earlier definition. Bash resolves a function name when the call runs, not when
38
+ # the caller is defined, so the cross-library calls resolve whatever the order: the
39
+ # disposable-dirt classifier is called BY run_report and BY delete_candidate in the
40
+ # libraries above, and the preserve library calls cleanup_wt_git from the enumeration
41
+ # library and consolidation_worktree_path from the actions library. The order here is
42
+ # therefore grouping for readability only.
43
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh
44
+ # shellcheck disable=SC1091
45
+ source "$SCRIPT_DIR/cleanup_worktrees_dirt_lib.sh"
46
+ # The line-ending and index group is placed immediately before the preserve library, whose
47
+ # read-only planning phase calls the functions it defines.
48
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh
49
+ # shellcheck disable=SC1091
50
+ source "$SCRIPT_DIR/cleanup_worktrees_preserve_eol_lib.sh"
51
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh
52
+ # shellcheck disable=SC1091
53
+ source "$SCRIPT_DIR/cleanup_worktrees_preserve_lib.sh"
54
+
55
+ usage() {
56
+ # Print the wrapper usage/help text.
57
+ cat <<'EOF'
58
+ Usage: cleanup-worktrees.sh [command]
59
+
60
+ Commands:
61
+ (no args) | report Dry-run report (default). Classifies every local branch and
62
+ worktree against main and prints a deterministic, machine-
63
+ parseable report. Performs NO mutation of any kind.
64
+ --apply | apply Apply mode. Deletes delete-eligible candidates (MERGED_CLEAN,
65
+ MERGED_CONTENT_NEUTRAL, MERGED_EQUIVALENT) after same-process
66
+ re-verification, removing worktrees without force and deleting
67
+ branches with -D. Never acts on NOT_MERGED,
68
+ HAS_UNIQUE_RESIDUALS, or PROTECTED_CURRENT candidates.
69
+ preserve | --preserve
70
+ Stage the manifest's preserved_files[] findings onto the
71
+ consolidation branch. Takes no operand; the manifest path comes
72
+ from CLEANUP_WT_MANIFEST_PATH. Copies each named source file
73
+ verbatim, carries its MEMORY.md index line across using the
74
+ destination file's own re-derived line-ending convention, and
75
+ refuses the whole pass when the incoming bytes carry a
76
+ host-identifying token. Exit 0 clean, 1 something was skipped or
77
+ an index was created, 3 host-token hard stop, 127 a required tool
78
+ could not be resolved.
79
+ --help | -h | help Print this help and exit 0.
80
+
81
+ Flags:
82
+ --clear-disposable Apply mode only, opt-in, and destructive. When a worktree removal
83
+ is blocked because the worktree is dirty, and every per-file
84
+ verdict for that worktree is disposable, the working tree is
85
+ cleared with reset --hard and clean -fd and the SAME unforced
86
+ removal is retried once after a fresh in-process re-verification.
87
+ Refuses the clear for the whole worktree when any per-file verdict
88
+ is UNIQUE, including the fail-closed UNIQUE assigned when a
89
+ classification read errors. Ignored files are never cleared. This
90
+ flag may be given before or after the apply argument; supplying it
91
+ without apply mode is a usage error and exits 2.
92
+
93
+ Report lines (pipe-delimited, LC_ALL=C ordered): BRANCH|<name>|<state>;
94
+ COMMIT|<branch>|<sha>|<state>|<paths>|<author>|<date>;
95
+ WORKTREE|<path>|<branch>|<flags> for a branch-backed worktree registration;
96
+ WORKTREE|<path>|DETACHED|<state>|<flags> for a detached-HEAD worktree registration,
97
+ which is classified on its own HEAD SHA and whose fifth field preserves the porcelain
98
+ locked and prunable markers; WARN|main-divergence|<local>|<origin>;
99
+ DIRTY|<path>|<status>; ACTION|<verb>|<target>|<result> (apply mode);
100
+ DIRTFILE|<worktree>|<verdict>|<detail>|<xy>|<path>, one per dirty entry in porcelain order;
101
+ DIRTSUM|<worktree>|<aggregate>|<detail>, exactly one per dirty worktree. The verdicts are
102
+ DISPOSABLE_BUILD_ARTIFACT, DISPOSABLE_SESSION_ARTIFACT, CONTENT_ON_MAIN, CONTENT_IN_HISTORY,
103
+ STAGED_TREE_IS_COMMIT, and UNIQUE; the aggregate is ALL_DISPOSABLE or HAS_UNIQUE. Both are
104
+ read-only records and neither unlocks a destructive action on its own.
105
+
106
+ PRESERVE|<worktree-path>|<source-path>|<verdict> (preserve mode), a manifest
107
+ preserved_files[] finding staged onto the consolidation branch, whose per-file outcome is
108
+ reported by the companion ACTION|preserve-stage|... record.
109
+
110
+ Advisory, read-only records (report and apply mode; none unlocks a destructive action):
111
+ ORPHAN_DIR|<path>|<size> for a scanned directory with no .git pointer file and no
112
+ worktree registration, where <size> is best-effort and may be the literal unknown;
113
+ STALE_REF|<refname> for a refs/remotes/<name>/* ref whose <name> is not a configured
114
+ remote, in full ref form; CHILD_OF|<branch>|<ancestor> emitted alongside a branch's own
115
+ BRANCH| line when it is a git ancestor of a branch that resolved exactly NOT_MERGED;
116
+ WARN|registration-lost|<path> for a worktree whose .git gitdir pointer no longer
117
+ resolves. Deleting any of these is a manual, individually confirmed action.
118
+
119
+ Branch and detached-worktree states: MERGED_CLEAN, MERGED_CONTENT_NEUTRAL,
120
+ MERGED_EQUIVALENT, NOT_MERGED, HAS_UNIQUE_RESIDUALS, PROTECTED_CURRENT, and
121
+ ANCESTRY_ERROR. The first three are the delete-eligible allowlist; ANCESTRY_ERROR is a
122
+ hard git failure and never unlocks a destructive action.
123
+
124
+ PROTECTED_CURRENT also covers the base branch main, which is protected by name in every
125
+ checkout topology: main is never deleted and a worktree checked out on main is never
126
+ removed. As a second guard, apply mode refuses a deletion request for main with the
127
+ action result BLOCKED-PROTECTED-BASE.
128
+
129
+ In apply mode a blocked detached removal sets a non-zero exit status. The blocked results
130
+ are BLOCKED-DIRTY, BLOCKED-LOCKED, and BLOCKED-REVERIFY. A checkout holding dirty or
131
+ locked detached worktrees therefore exits non-zero from --apply where it previously
132
+ exited 0.
133
+
134
+ Environment overrides:
135
+ CLEANUP_WT_GIT_BIN Path to the git binary; an empty or nonexistent value
136
+ is treated as missing (falls back to PATH git). This is
137
+ the test-stub seam.
138
+ CLEANUP_WT_SCAN_BIN Path to the filesystem-scan helper; an empty or
139
+ nonexistent value is treated as missing (falls back to
140
+ the bundled cleanup_worktrees_scan_helper.sh). This is
141
+ the filesystem-scan test-stub seam.
142
+ CLEANUP_WT_ORPHAN_ROOTS Colon-separated override for the worktree-tracking roots
143
+ scanned for ORPHAN_DIR and WARN|registration-lost.
144
+ CLEANUP_WT_STUB_SCENARIO Scenario directory consumed by the checked-in git and
145
+ scan stubs (tests only).
146
+ CLEANUP_WT_CONSOLIDATION_PATH Override the derived consolidation worktree path
147
+ (<main-worktree-path>-wt/documentationandmemories).
148
+ CLEANUP_WT_MANIFEST_PATH Path to the cleanup-worktrees manifest read by preserve
149
+ mode. Defaults to
150
+ artifacts/orchestration/cleanup-worktrees-manifest.json.
151
+ CLEANUP_WT_JQ_BIN Path to the jq binary used to read that manifest; an empty
152
+ or nonexistent value is treated as missing (falls back to
153
+ PATH jq). No default. This is the jq test-stub seam.
154
+ EOF
155
+ }
156
+
157
+ main() {
158
+ # Dispatch on the first argument. No args or `report` runs the dry-run report;
159
+ # `--apply`/`apply` runs apply mode; `preserve`/`--preserve` stages the manifest's
160
+ # preserved_files[] findings onto the consolidation branch and takes no operand,
161
+ # reading its manifest path from CLEANUP_WT_MANIFEST_PATH; `--help`/`-h`/`help`
162
+ # prints usage and exits 0;
163
+ # anything else prints usage to stderr and exits 2 (usage-error parity with
164
+ # shell-qc.sh). Subcommand return codes are captured so an intermediate failure is
165
+ # never masked before the final exit.
166
+ local exit_code=0
167
+ # --clear-disposable pre-pass, run before the dispatch below so the flag may be given
168
+ # on either side of the mode argument. The flag is stripped from the argument list and
169
+ # the remaining first argument must select apply mode: the flag is opt-in and
170
+ # destructive, so pairing it with report mode (or with no mode at all) is a usage error
171
+ # rather than a silently ignored flag. Every case arm below is unchanged.
172
+ local -a args=()
173
+ local clear_flag=0 arg
174
+ for arg in "$@"; do
175
+ if [[ $arg == "--clear-disposable" ]]; then
176
+ clear_flag=1
177
+ continue
178
+ fi
179
+ args+=("$arg")
180
+ done
181
+ if ((clear_flag == 1)); then
182
+ case "${args[0]:-}" in
183
+ --apply | apply) ;;
184
+ *)
185
+ usage >&2
186
+ return 2
187
+ ;;
188
+ esac
189
+ # Read across a source boundary shellcheck does not follow:
190
+ # cleanup_worktrees_dirt_lib.sh gates clear_disposable_dirt on this variable and
191
+ # cleanup_worktrees_actions_lib.sh gates delete_candidate's clear-and-retry block
192
+ # on it, so SC2034 reports it unused here. The suppression below is line-scoped,
193
+ # not file-scoped, so SC2034 stays live for every other variable in this file.
194
+ # The assignment's effect is pinned end to end by the wrapper-driven test pair at
195
+ # the foot of tests/shell/test_cleanup_worktrees_dirt_clear.bats.
196
+ # shellcheck disable=SC2034
197
+ CLEANUP_WT_CLEAR_DISPOSABLE=1
198
+ fi
199
+ if ((${#args[@]} > 0)); then
200
+ set -- "${args[@]}"
201
+ else
202
+ set --
203
+ fi
204
+ local command=${1:-}
205
+ case "$command" in
206
+ "" | report)
207
+ run_report || exit_code=$?
208
+ ;;
209
+ --apply | apply)
210
+ run_apply || exit_code=$?
211
+ ;;
212
+ preserve | --preserve)
213
+ run_preserve || exit_code=$?
214
+ ;;
215
+ --help | -h | help)
216
+ usage
217
+ return 0
218
+ ;;
219
+ *)
220
+ usage >&2
221
+ return 2
222
+ ;;
223
+ esac
224
+ return "$exit_code"
225
+ }
226
+
227
+ # Guard so the file can be sourced without executing main. main's return code is
228
+ # captured and re-exited explicitly as the final statement, so intermediate
229
+ # `|| rc=$?` captures inside the libraries cannot mask a real failure.
230
+ if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
231
+ rc=0
232
+ main "$@" || rc=$?
233
+ exit "$rc"
234
+ fi