@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
package/package.json
CHANGED
|
@@ -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/`,
|
|
49
|
-
directory; a missing root is silently skipped. The `.claude/lib/bash/` root carries
|
|
50
|
-
destination-portable bash library published by push-down,
|
|
51
|
-
same format, lint, test, and
|
|
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
|
|
66
|
-
`.claude/lib/bash/` — so the Claude bash library
|
|
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
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|