@danmoisan/drm-copilot-mcp 1.1.12 → 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 (38) hide show
  1. package/out/mcp-server.js +30 -78
  2. package/package.json +2 -2
  3. package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
  4. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  6. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
  7. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
  8. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
  9. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
  10. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
  11. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
  12. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
  13. package/resources/claude-customizations/.claude/rules/shell.md +8 -7
  14. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
  15. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
  16. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
  17. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
  18. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
  19. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
  20. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
  21. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
  22. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
  23. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
  24. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
  25. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
  26. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +11 -0
  27. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
  28. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
  29. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
  30. package/resources/claude-customizations/config/blast-radius.json +14 -1
  31. package/resources/claude-customizations/pack-manifests/core.json +13 -0
  32. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  33. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  37. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  38. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -0
@@ -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
+
@@ -209,6 +209,8 @@ All nine enums of the parallel surface are owned by the schema-and-validator fea
209
209
 
210
210
  The wave-4 features — F6 (mutation protocol), F7 (enforcement hooks), and F8 (drift detection) — CONSUME these member sets and NEVER extend them. A wave-4 feature that needs a new member must amend this rule file and the validators at spec review, not add the member at implementation time. This constraint exists because the wave-4 features are prepared concurrently and would otherwise add fields to the same files at the same time.
211
211
 
212
+ Integration-cost scheduling (issue #722) adds no `conflict_edges[].reason` member; the four members above are unchanged. The scheduling data travels in tolerated-not-validated fields: an edge may carry the extra fields `hard`, `cost`, and `benefit`, and the planner and orchestrator checkpoints may carry a `tolerated_overlaps` list. No validator reads these fields, and the Python validators and the TypeScript validator port accept them with zero errors.
213
+
212
214
  ## F7 Seam
213
215
 
214
216
  The retrospective cohort-ordering invariant `PARALLEL_COHORT_BARRIER_VIOLATION` (design section 9, Layer 2) is F7's explicitly assigned addition to the orchestrator validator. It is NOT implemented here. The entry point of `scripts/dev_tools/validate_parallel_orchestrator_state.py` contains a clearly delimited, appendable helper-invocation block, marked with explicit begin and end comments that name F7 and the invariant token, so that F7's edit is one appended helper call with no reflow of existing code. The TypeScript core `extensions/drm-copilot/src/lib/validate/parallel-orchestrator-state-core.ts` carries the matching comment-delimited seam. Existing helper calls sit outside the block.
@@ -241,6 +243,11 @@ plan-side extraction so V1 and V2 stay self-consistent against a radius derived
241
243
  The key is optional and fail-closed: a truth table that omits it excludes nothing and reproduces
242
244
  pre-change behaviour exactly.
243
245
 
246
+ The Copilot instructions file `.github/copilot-instructions.md` is a mandate read as well (issue
247
+ #722): the policy reading order begins with it, so a plan that cites copilot-instructions.md is
248
+ reporting that it read the tone and communication policy. Both committed copies of
249
+ `config/blast-radius.json` list it in `mandate_reads`.
250
+
244
251
  Three constraints bound the mandate-read exclusion:
245
252
 
246
253
  1. **The planner remains obliged to enumerate a genuine write explicitly.** An exclusion describes
@@ -391,6 +398,144 @@ weakens the relation below the path level: two items editing the same file still
391
398
  A candidate module belongs in the map when it names a subsystem an item could plausibly not touch.
392
399
  A candidate that matches the majority of work items belongs nowhere.
393
400
 
401
+ ### Integration-cost scheduling (issue #722)
402
+
403
+ Detection and scheduling are separate steps. The detection relation (`conflicts` in Python,
404
+ `Test-BlastRadiusConflict` in PowerShell) is unchanged and still answers one question: do two radii
405
+ contend? Scheduling decides whether a detected contention must serialize the two items. The entry
406
+ points are `schedule_conflict_edges` in `scripts/dev_tools/_blast_radius_scheduling.py` and
407
+ Get-BlastRadiusConflictEdge in `.claude/lib/blast-radius/BlastRadiusScheduling.psm1`; both return
408
+ the edges and the tolerated overlaps of a set of items. Planners call the entry point instead of
409
+ applying the detection relation to every pair by hand.
410
+
411
+ This is an operator-directed configured policy change (issue #722, 2026-09-27), not ad hoc
412
+ narrowing. The only sanctioned mechanisms for relaxing contention are the configured
413
+ `conflict_tolerance` key and configured write-intent extraction. Planners still never hand-narrow a
414
+ radius: editing a declared radius to suppress an edge remains prohibited.
415
+
416
+ **Configuration.** `config/blast-radius.json` carries an optional `conflict_tolerance` object with
417
+ tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only, and
418
+ module, each an integer of at least 1), `band_durations` (C1 through C4, each an integer of at least
419
+ 1), `default_band` (one of C1 through C4), and append_only_paths (a list of exact entries and `**`
420
+ globs). The reader fails fast with an error naming the key for any other shape; booleans are
421
+ rejected wherever an integer is required. An absent key means strict scheduling, identical to
422
+ tolerance_percent 0.
423
+
424
+ **Edge rule.** For each unordered pair of items:
425
+
426
+ 1. Run the unchanged detection relation. No conflict means no edge and no tolerated overlap.
427
+ 2. **Hard classes.** A reason list containing `shared_surface_overlap` or `contract_dependency`
428
+ makes the pair an edge at every tolerance.
429
+ 3. **Cost** (integer, computed only for a detected conflict). Re-enumerate the overlapping entry
430
+ pairs with the same mergeable-path exclusion and the same entry-overlap primitive the relation
431
+ uses, then sum the append_only weight for a concrete overlapping path that matches an
432
+ append_only_paths entry, the same_file weight for two equal concrete entries, the
433
+ possible_overlap weight for a glob or directory-prefix overlap, and the module weight times the
434
+ number of shared modules. The append_only_paths check runs before the same_file check (append-only
435
+ precedence), so an append-only file stays low cost even when both items name it. Mergeable paths
436
+ contribute 0 because they are excluded before enumeration.
437
+ 4. **Benefit** (integer, pairwise). The smaller of the two items' band durations; a missing band uses
438
+ `default_band`. Running the pair concurrently saves at most one cohort step of the shorter item.
439
+ 5. **Rule.** The pair is an edge if and only if it conflicts and either it is hard or
440
+ `cost * 100 > benefit * tolerance_percent`.
441
+ 6. **Recorded reason.** The first member of the relation's reason list in canonical kind order.
442
+
443
+ **Strict-identity proof.** Every weight and every band duration is validated as an integer of at
444
+ least 1. A detected conflict that is not hard therefore has a cost of at least 1, so at
445
+ tolerance_percent 0 the inequality `cost * 100 > 0` always holds and the edge set equals the
446
+ detected-conflict set exactly; cohort coloring of that set is identical to strict scheduling. At
447
+ every tolerance an edge requires a conflict, so every edge implies a conflict and the edge set is
448
+ always a subset of the detected-conflict set. Raising tolerance_percent never adds an edge, and the
449
+ decision for (a, b) equals the decision for (b, a).
450
+
451
+ **Soft-overlap handling.** A detected, non-hard pair within tolerance is a tolerated overlap, not an
452
+ edge. The planner records it in a tolerated_overlaps list on the planner checkpoint (and the
453
+ orchestrator carries it), each entry holding `a`, `b`, `reasons`, `cost`, and `benefit`. Tolerated
454
+ pairs run in the same or adjacent cohorts without a barrier. The later-merging item of a tolerated
455
+ pair merges `origin/main` under the existing per-item merge-conflict handling and re-passes CI before
456
+ it merges.
457
+
458
+ **Drift behaviour.** When an item's observed radius replaces its declared radius, drift detection
459
+ evaluates each in-flight peer pair through the same edge rule, using the items' complexity bands from
460
+ the checkpoint (`default_band` when absent), through the helper module
461
+ `scripts/dev_tools/_parallel_drift_scheduling.py`. A pair is newly conflicting only when the rule
462
+ yields an edge and the pair is not already a recorded edge. A tolerated pair whose observed overlap
463
+ stays within tolerance halts neither item; one whose observed overlap exceeds tolerance, or becomes
464
+ hard, is reported. An unevaluable peer radius still counts as an edge (fail closed). At
465
+ tolerance_percent 0 the drift output equals strict drift output.
466
+
467
+ ### Write-intent extraction (issue #722)
468
+
469
+ The extractor harvests every inline-code token of a plan and spec, so a path an item only reads,
470
+ mentions as a glob, or quotes inside a command becomes a radius path and can create a false edge.
471
+ Write-intent extraction keeps only tokens that state a write. It is the second sanctioned mechanism,
472
+ beside `conflict_tolerance`, for relaxing contention; like it, it is an operator-directed configured
473
+ policy change, and planners still never hand-narrow a radius.
474
+
475
+ **Configuration.** `config/blast-radius.json` carries two optional keys. `write_intent_extraction`
476
+ is a boolean; when it is true, rules W1 through W6 below apply. `path_roots` is a list of non-empty
477
+ first path segments that enables rule W4. Both readers fail fast with an error naming the key for any
478
+ other shape. An absent or false `write_intent_extraction` reproduces pre-change extraction exactly
479
+ (fail closed), and an empty or absent `path_roots` disables W4. The self-hosted copy sets
480
+ `path_roots` to this repository's tracked top-level directories. The implementations are
481
+ `scripts/dev_tools/_blast_radius_write_intent.py` and
482
+ `.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1`.
483
+
484
+ **Rules.** Each rule is a pure, statically decidable function of the text and the truth table.
485
+
486
+ - **W1 glob mention.** Drop any harvested token containing a wildcard character. The feature-folder
487
+ glob is added after filtering and is never dropped.
488
+ - **W2 command span.** Drop every token of an inline span whose whitespace split yields more than
489
+ one word.
490
+ - **W3 read task.** Drop the tokens of a task attribution window (a task line plus the following
491
+ non-task lines up to the next ATX heading, as defined in `.claude/rules/plan-acceptance-gates.md`)
492
+ whose title's first word, after an optional bold label, is in the read-verb set (Read, Verify,
493
+ Confirm, Inspect, Review, Baseline) and whose title contains no member of the write-verb set (Fix,
494
+ Write, Update, Edit, Add, Create, Delete, Remove, Rename, Author, Append, Replace). A write verb
495
+ anywhere in the title overrides the read verb.
496
+ - **W4 root anchoring.** Drop a concrete token whose first segment, after stripping a leading `./`,
497
+ is not in `path_roots` and is not a configured root surface.
498
+ - **W5 spec paths.** The spec contributes contracts only, not paths. Contract harvesting from the
499
+ spec applies W1 and W2.
500
+ - **W6 placeholder stem.** Drop a concrete token whose final-component stem is in the placeholder
501
+ set (the single ASCII letters, and foo, bar, baz, example, sample, placeholder), compared
502
+ case-insensitively.
503
+
504
+ The read-verb, write-verb, and placeholder-stem sets are code constants pinned equal across Python
505
+ and PowerShell by a parity test. Shared surfaces are still resolved from the surviving concrete
506
+ paths, so a shared surface cited only in a read task, a command span, or the spec produces no hard
507
+ edge, while a shared surface an item writes in a write task survives and stays hard.
508
+
509
+ **One extractor for derivation and validation.** W1, W4, and W6 are token-level and also apply when
510
+ `normalize_declared_radius` and Get-NormalizedDeclaredRadius re-filter a recorded radius; an entry
511
+ that starts with `docs/features/`, ends with `/**`, and carries no other wildcard (the feature-folder
512
+ glob) is always kept. W2, W3, and W5 need line context and apply only to derivation and to the
513
+ plan-side extraction of validation. `derive_blast_radius` and `validate_blast_radius` (and their
514
+ PowerShell ports) select the same extractor from the same flag, so a derived radius still passes V1
515
+ and V2 against its own plan in both modes.
516
+
517
+ #### Known false negatives
518
+
519
+ Write-intent extraction can drop a genuine write in these cases:
520
+
521
+ 1. a genuine write stated only as a glob (W1);
522
+ 2. a genuine write stated only inside a command span (W2);
523
+ 3. a write named only in spec prose (W5);
524
+ 4. a read-verb task that also writes without a write verb in its title (W3);
525
+ 5. a new top-level directory not yet in `path_roots` (W4);
526
+ 6. a genuine file whose stem is in the placeholder set (W6).
527
+
528
+ Three mitigations bound these cases:
529
+
530
+ 1. **The planner remains obliged to enumerate a genuine write explicitly.** When an item's plan will
531
+ write a path that one of the rules drops, the planner appends that exact path to the declared
532
+ radius after normalization, as for the mandate-read exclusion.
533
+ 2. **Execution-time escaped-path detection.** `detect_escaped_paths` compares the declared radius
534
+ against the paths a diff actually touched, so a dropped write is caught against observed evidence
535
+ rather than against prose.
536
+ 3. **The fail-closed absent key.** A truth table without `write_intent_extraction`, or with it set
537
+ to false, reproduces pre-change extraction exactly, so an operator can disable the rules at once.
538
+
394
539
  ### The published truth table is not a copy of this one (issue #500)
395
540
 
396
541
  The push-down publishes a second truth table into a destination workspace at
@@ -430,8 +575,11 @@ non-vacuous input.
430
575
  subset, not a copy of the self-hosted sets.** They were authored narrow when the bundled copy was
431
576
  created and were never a copy that fell behind, so the correct gate is portable-set equality against
432
577
  a declared constant plus a subset relation against the self-hosted list — never byte-equality with
433
- the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, and `mergeable_paths`
434
- are byte-equal across the two copies.
578
+ the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, `mergeable_paths`,
579
+ `conflict_tolerance`, and `write_intent_extraction` are byte-equal across the two copies.
580
+ `path_roots` is a Class 2 key: its self-hosted value is this repository's tracked top-level
581
+ directory list, and its bundled value is an empty list, because the bundle cannot know a
582
+ destination's top-level directories and an empty list disables root anchoring there.
435
583
 
436
584
  The reason the two key groups take different relations is an asymmetry between surfaces and modules.
437
585
  An over-matching MODULE glob costs concurrency on every pair of items it touches, because a module
@@ -85,7 +85,7 @@ The window size is fixed at four adjacent non-blank lines. Blank lines are remov
85
85
 
86
86
  ### The shared measurement behind G7, G8, G8b, and G9
87
87
 
88
- The four severities below were fixed by one corpus measurement recorded in `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`. The pre-declared decision rule was written and committed **before** the driver existed and before any count was taken, so the ordering is verifiable from git history rather than only asserted in prose. Its form matches the G5 precedent: the shipped severity is Blocking if and only if the total finding count for that rule is greater than zero **and** the recorded false-positive count for that rule is zero; otherwise Warning.
88
+ The four severities below were fixed by one corpus measurement recorded in `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`. The pre-declared decision rule was written and committed **before** the driver existed and before any count was taken, so the ordering is verifiable from git history rather than only asserted in prose. Its form matches the G5 precedent: the shipped severity is Blocking if and only if the total finding count for that rule is greater than zero **and** the recorded false-positive count for that rule is zero; otherwise Warning.
89
89
 
90
90
  The false-positive definition the rule consumes is likewise pre-declared: a finding is a false positive when the acceptance condition it reports is in fact falsifiable — when the plan states an observation sufficient to distinguish a passing run from a failing one, by a mechanism the rule's predicate does not recognise. A finding is a true positive when the acceptance condition it reports genuinely cannot fail, or can only be satisfied vacuously.
91
91
 
@@ -108,7 +108,7 @@ G7's finding count of 466 satisfies the first conjunct, so the rule was not deci
108
108
 
109
109
  Both classes are addressable by a later feature — Class 1 by adding a check-flag exclusion to the `prettier-write` entry, Class 2 by admitting a tree-observation span as an alternative to a marker — but neither was changed here, because narrowing a predicate after reading its measurement would invalidate the measurement that decided its severity. A later feature that narrows either class must re-take the measurement the same way.
110
110
 
111
- Counts, and every one of the 22 false positives named by plan path, task identifier, and offending span: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
111
+ Counts, and every one of the 22 false positives named by plan path, task identifier, and offending span: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
112
112
 
113
113
  ### G8 — ships as a Warning because the measurement recorded 7 false positives
114
114
 
@@ -118,7 +118,7 @@ G8's finding count of 82 satisfies the first conjunct. It fails the second: 7 fi
118
118
 
119
119
  **Class 2, one finding — the unmerged-path filter.** `--diff-filter=U` selects conflicted paths during a merge or rebase. In that state the worktree-against-index comparison is the correct one and does not become vacuous on commit, because the conflict is precisely what is not yet committed.
120
120
 
121
- Counts, and every one of the 7 false positives named by plan path, task identifier, and offending span: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
121
+ Counts, and every one of the 7 false positives named by plan path, task identifier, and offending span: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
122
122
 
123
123
  ### G8b — ships as a Warning unconditionally, by the pre-declared rule
124
124
 
@@ -126,7 +126,7 @@ G8b is exempt from the two-conjunct rule and **cannot reach the Blocking channel
126
126
 
127
127
  The reason is that G8b carries the highest false-positive surface of the set, and a false-positive count taken over one corpus does not bound the false-positive surface of a predicate. Promoting a rule on the strength of a clean run over one corpus would convert an accident of that corpus into a gate. The 19 findings were nonetheless examined individually and all 19 were retained as true positives, across three sub-classes: a placeholder ref operand, which resolves to a real ref at run time and still cannot make the diff see an untracked path; an artifact-field label inside the span, which is cosmetic and leaves the underlying acceptance condition as the finding describes it; and a pathspec written without the `--` separator, where the predicate reads the pathspec as a ref operand. That last sub-class is a true positive for G8b and is simultaneously recorded in the measurement as a **G8 false negative**: the same span should also have been reported by G8, and was not.
128
128
 
129
- Counts, the zero false-positive record, and the three sub-classes with their plan paths and offending spans: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
129
+ Counts, the zero false-positive record, and the three sub-classes with their plan paths and offending spans: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
130
130
 
131
131
  ### G9 — ships as a Warning because the measurement recorded 4 false positives
132
132
 
@@ -134,7 +134,7 @@ G9's finding count of 8 satisfies the first conjunct. It fails the second: 4 of
134
134
 
135
135
  The remaining 4 findings are true positives, and each is the exact defect the rule was written for: the acceptance condition demands a numeric coverage percentage, the command supplies no terminal reporter, the project `addopts` supplies only an LCOV reporter, and the number the acceptance demands is therefore never printed.
136
136
 
137
- Counts, and every one of the 4 false positives named by plan path, task identifier, offending span, and the reason its acceptance condition is falsifiable: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
137
+ Counts, and every one of the 4 false positives named by plan path, task identifier, offending span, and the reason its acceptance condition is falsifiable: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
138
138
 
139
139
  ## The Write-Mode Register
140
140
 
@@ -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": [