@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.22

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 (31) hide show
  1. package/out/mcp-server.js +1624 -190
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
  4. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
  5. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
  6. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +238 -0
  8. package/resources/claude-customizations/.claude/agents/parallel-planner.md +149 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
  10. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
  15. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
  17. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
  18. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
  19. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
  20. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
  21. package/resources/claude-customizations/.claude/settings.json +25 -0
  22. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +148 -0
  23. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
  24. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +960 -0
  25. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +420 -0
  26. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
  27. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
  28. package/resources/claude-customizations/pack-manifests/core.json +19 -1
  29. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  30. package/resources/config/orchestration-routing.json +22 -0
  31. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
@@ -0,0 +1,366 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Blast-radius record normalization and validation rules V1, V2, and V3.
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell port of the rule half of
7
+ scripts/dev_tools/_blast_radius_validation.py (validate_blast_radius,
8
+ _coverage_findings, _shared_surface_findings, _over_breadth_findings and the
9
+ RadiusFinding record), plus the construction-time invariants the BlastRadius
10
+ dataclass in scripts/dev_tools/compute_blast_radius.py enforces in
11
+ __post_init__ and from_dict.
12
+
13
+ The Python module remains the authoritative reference implementation. This
14
+ module is one half of a two-language mirror; it never imports validator
15
+ logic from outside this library. Every function is pure: no filesystem,
16
+ subprocess, network, or wall-clock access, and no input is mutated.
17
+
18
+ Parity notes for maintainers:
19
+ - V1 and V2 are Blocking, V3 Advisory with at most one finding. Findings
20
+ are sorted by rule then subject, at most one per rule per subject.
21
+ - The V2 touched-surface set is the union of the radius's own concrete
22
+ paths and the plan's concrete paths, and enumeration is exact-path
23
+ membership. Glob coverage in either paths or shared_surfaces is
24
+ deliberately insufficient, so a surface reachable only through a wildcard
25
+ still produces a finding. That is the fail-closed reading.
26
+ - V3 applies the threshold by multiplication rather than division so the
27
+ boundary is exact: a radius sitting exactly at the fraction does not
28
+ trigger, and both languages compute the identical IEEE-754 comparison.
29
+ - tracked_file_count must be an integer, and a boolean is rejected
30
+ explicitly because the Python reference rejects it.
31
+ - Finding message text is a contract literal shared with the Python
32
+ reference and the cross-language fixture corpus; do not reword it.
33
+ #>
34
+
35
+ Set-StrictMode -Version Latest
36
+
37
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force
38
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
39
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force
40
+
41
+ # Finding vocabulary. These strings are contract literals consumed by the
42
+ # downstream parallel schema and planner features.
43
+ $script:RuleCoverage = 'V1'
44
+ $script:RuleSharedSurface = 'V2'
45
+ $script:RuleOverBreadth = 'V3'
46
+ $script:SeverityBlocking = 'Blocking'
47
+ $script:SeverityAdvisory = 'Advisory'
48
+ $script:FindingRule = @('V1', 'V2', 'V3')
49
+ $script:FindingSeverity = @('Blocking', 'Advisory')
50
+
51
+ # The single subject the over-breadth rule reports against; the rule is about the
52
+ # whole paths level rather than any one entry.
53
+ $script:OverBreadthSubject = 'blast_radius.paths'
54
+
55
+ # Integer types a caller may supply for the tracked-file count. Boolean is
56
+ # excluded explicitly because Python treats it as an integer.
57
+ $script:IntegerTypeName = @('System.Int32', 'System.Int64', 'System.Int16', 'System.Byte')
58
+
59
+ # Serialized key set, in the order of the parallel manifest schema. Downstream
60
+ # features depend on these strings verbatim.
61
+ $script:RadiusKey = @('paths', 'modules', 'shared_surfaces', 'contracts', 'source', 'computed_at')
62
+
63
+ # Confidence sources: derived seeds cohorts provisionally, declared is
64
+ # planner-computed and authoritative for scheduling, and observed comes from an
65
+ # actual diff during drift correction.
66
+ $script:RadiusSource = @('derived', 'declared', 'observed')
67
+
68
+
69
+ function ConvertTo-NormalizedBlastRadius {
70
+ <#
71
+ .SYNOPSIS
72
+ Validate a blast-radius record and normalize every collection it carries.
73
+
74
+ .DESCRIPTION
75
+ Port of the BlastRadius dataclass construction path: the exact key-set
76
+ check from from_dict plus the field guards, source-vocabulary check, and
77
+ sorted, deduplicated collections from __post_init__. An exact key-set
78
+ check is deliberate: a missing key would silently narrow a radius and an
79
+ unexpected key would silently drop data, and both failure modes
80
+ under-report contention. PowerShell hashtable keys are matched case
81
+ insensitively, which is the one place this port is more permissive than
82
+ Python; a case-variant key is malformed input in both languages.
83
+
84
+ .PARAMETER Radius
85
+ A radius record carrying exactly the keys paths, modules,
86
+ shared_surfaces, contracts, source, and computed_at.
87
+
88
+ .OUTPUTS
89
+ System.Collections.Hashtable. A new record with the same key set, every
90
+ collection deduplicated and ordinally sorted.
91
+ #>
92
+ [CmdletBinding()]
93
+ [OutputType([hashtable])]
94
+ param(
95
+ [Parameter(Mandatory = $true)]
96
+ [AllowNull()]
97
+ [object] $Radius
98
+ )
99
+
100
+ $mapping = Get-RequiredMapping -Value $Radius -FieldName 'blast radius'
101
+
102
+ $missing = @($script:RadiusKey | Where-Object { -not $mapping.ContainsKey($_) })
103
+ if ($missing.Count -gt 0) {
104
+ throw "blast radius record is missing keys $($missing -join ', ')."
105
+ }
106
+ $unexpected = [string[]]@(Get-OrdinalSortedEntry -Entry ([string[]]@(
107
+ $mapping.Keys | Where-Object { $script:RadiusKey -notcontains $_ })))
108
+ if ($unexpected.Count -gt 0) {
109
+ throw "blast radius record has unexpected keys $($unexpected -join ', ')."
110
+ }
111
+
112
+ $source = Get-RequiredText -Value $mapping['source'] -FieldName 'source'
113
+ if ($script:RadiusSource -cnotcontains $source) {
114
+ throw "source must be one of $($script:RadiusSource -join ', ')."
115
+ }
116
+
117
+ return @{
118
+ paths = @(Get-RequiredStringList -Value $mapping['paths'] -FieldName 'paths')
119
+ modules = @(Get-RequiredStringList -Value $mapping['modules'] -FieldName 'modules')
120
+ shared_surfaces = @(Get-RequiredStringList -Value $mapping['shared_surfaces'] -FieldName 'shared_surfaces')
121
+ contracts = @(Get-RequiredStringList -Value $mapping['contracts'] -FieldName 'contracts')
122
+ source = $source
123
+ computed_at = Get-RequiredText -Value $mapping['computed_at'] -FieldName 'computed_at'
124
+ }
125
+ }
126
+
127
+
128
+ # Port of the RadiusFinding dataclass construction path: reject any finding
129
+ # outside the frozen rule and severity vocabulary, then return the record.
130
+ function Get-RadiusFinding {
131
+ [CmdletBinding()]
132
+ [OutputType([hashtable])]
133
+ param(
134
+ [Parameter(Mandatory = $true)]
135
+ [string] $Rule,
136
+ [Parameter(Mandatory = $true)]
137
+ [string] $Severity,
138
+ [Parameter(Mandatory = $true)]
139
+ [string] $Subject,
140
+ [Parameter(Mandatory = $true)]
141
+ [string] $Message
142
+ )
143
+
144
+ if ($script:FindingRule -cnotcontains $Rule) {
145
+ throw "RadiusFinding rule must be one of $($script:FindingRule -join ', ')."
146
+ }
147
+ if ($script:FindingSeverity -cnotcontains $Severity) {
148
+ throw "RadiusFinding severity must be one of $($script:FindingSeverity -join ', ')."
149
+ }
150
+
151
+ return @{
152
+ rule = Get-RequiredText -Value $Rule -FieldName 'RadiusFinding.rule'
153
+ severity = Get-RequiredText -Value $Severity -FieldName 'RadiusFinding.severity'
154
+ subject = Get-RequiredText -Value $Subject -FieldName 'RadiusFinding.subject'
155
+ message = Get-RequiredText -Value $Message -FieldName 'RadiusFinding.message'
156
+ }
157
+ }
158
+
159
+ # Port of _coverage_findings. Coverage is subsumption, not equality: an exact
160
+ # entry, a listed directory, or a glob in the radius all cover a plan path.
161
+ function Get-CoverageFinding {
162
+ [CmdletBinding()]
163
+ [OutputType([System.Object[]])]
164
+ param(
165
+ [Parameter(Mandatory = $true)]
166
+ [hashtable] $Radius,
167
+ [Parameter(Mandatory = $true)]
168
+ [AllowEmptyCollection()]
169
+ [AllowEmptyString()]
170
+ [string[]] $PlanConcretePath
171
+ )
172
+
173
+ $finding = [System.Collections.Generic.List[hashtable]]::new()
174
+ foreach ($path in $PlanConcretePath) {
175
+ if (-not (Test-PathSubsumed -Path $path -CoveringPath ([string[]]@($Radius['paths'])))) {
176
+ $finding.Add((Get-RadiusFinding -Rule $script:RuleCoverage `
177
+ -Severity $script:SeverityBlocking `
178
+ -Subject $path `
179
+ -Message "Plan path $path is not subsumed by blast_radius.paths."))
180
+ }
181
+ }
182
+
183
+ return @($finding.ToArray())
184
+ }
185
+
186
+ # Port of _shared_surface_findings. The touched-surface source is the union of
187
+ # the radius's own concrete paths and the plan's concrete paths, so a radius that
188
+ # covers a surface only by glob is still caught.
189
+ function Get-SharedSurfaceFinding {
190
+ [CmdletBinding()]
191
+ [OutputType([System.Object[]])]
192
+ param(
193
+ [Parameter(Mandatory = $true)]
194
+ [hashtable] $Radius,
195
+ [Parameter(Mandatory = $true)]
196
+ [AllowEmptyCollection()]
197
+ [AllowEmptyString()]
198
+ [string[]] $PlanConcretePath,
199
+ [Parameter(Mandatory = $true)]
200
+ [AllowNull()]
201
+ [object] $Config
202
+ )
203
+
204
+ $touchedSource = [System.Collections.Generic.List[string]]::new()
205
+ $touchedSource.AddRange([string[]]@(Get-ConcreteEntry -Entry ([string[]]@($Radius['paths']))))
206
+ $touchedSource.AddRange($PlanConcretePath)
207
+
208
+ $declared = [System.Collections.Generic.HashSet[string]]::new(
209
+ [string[]]@($Radius['shared_surfaces']), [StringComparer]::Ordinal)
210
+
211
+ $finding = [System.Collections.Generic.List[hashtable]]::new()
212
+ foreach ($surface in @(Resolve-BlastRadiusSharedSurface -ConcretePath $touchedSource.ToArray() -Config $Config)) {
213
+ if (-not $declared.Contains($surface)) {
214
+ $finding.Add((Get-RadiusFinding -Rule $script:RuleSharedSurface `
215
+ -Severity $script:SeverityBlocking `
216
+ -Subject $surface `
217
+ -Message ("Shared surface $surface is touched but is not " +
218
+ 'enumerated in blast_radius.shared_surfaces.')))
219
+ }
220
+ }
221
+
222
+ return @($finding.ToArray())
223
+ }
224
+
225
+ # Port of _over_breadth_findings. An over-broad radius is safe but serializes the
226
+ # batch, so the rule only reports and emits at most one Advisory finding.
227
+ function Get-OverBreadthFinding {
228
+ [CmdletBinding()]
229
+ [OutputType([System.Object[]])]
230
+ param(
231
+ [Parameter(Mandatory = $true)]
232
+ [hashtable] $Radius,
233
+ [Parameter(Mandatory = $true)]
234
+ [AllowNull()]
235
+ [object] $Config,
236
+ [Parameter(Mandatory = $true)]
237
+ [AllowNull()]
238
+ [object] $TrackedFileCount
239
+ )
240
+
241
+ if ($TrackedFileCount -is [bool] -or $null -eq $TrackedFileCount -or
242
+ $script:IntegerTypeName -notcontains $TrackedFileCount.GetType().FullName) {
243
+ throw 'tracked_file_count must be an integer.'
244
+ }
245
+ $count = [long]$TrackedFileCount
246
+ if ($count -le 0) {
247
+ throw 'tracked_file_count must be a positive integer.'
248
+ }
249
+
250
+ $threshold = Get-ConfigOverBreadthFraction -Config $Config
251
+ $covered = @(Get-ConcreteEntry -Entry ([string[]]@($Radius['paths']))).Count
252
+ if ($covered -le ($threshold * $count)) {
253
+ return @()
254
+ }
255
+
256
+ return @(
257
+ Get-RadiusFinding -Rule $script:RuleOverBreadth `
258
+ -Severity $script:SeverityAdvisory `
259
+ -Subject $script:OverBreadthSubject `
260
+ -Message ("Radius covers $covered of $count tracked files, " +
261
+ 'which exceeds the configured over-breadth fraction.')
262
+ )
263
+ }
264
+
265
+ # Port of the findings.sort(key=(rule, subject)) call. A stable insertion sort is
266
+ # used because Python's sort is stable and because ordinal comparison must not be
267
+ # delegated to the culture-sensitive Sort-Object cmdlet.
268
+ function Get-SortedRadiusFinding {
269
+ [CmdletBinding()]
270
+ [OutputType([System.Object[]])]
271
+ param(
272
+ [Parameter(Mandatory = $true)]
273
+ [AllowEmptyCollection()]
274
+ [hashtable[]] $Finding
275
+ )
276
+
277
+ $sorted = [System.Collections.Generic.List[hashtable]]::new()
278
+ foreach ($candidate in $Finding) {
279
+ $position = $sorted.Count
280
+ while ($position -gt 0) {
281
+ $previous = $sorted[$position - 1]
282
+ $ruleOrder = [string]::CompareOrdinal([string]$previous['rule'], [string]$candidate['rule'])
283
+ $order = if ($ruleOrder -ne 0) {
284
+ $ruleOrder
285
+ } else {
286
+ [string]::CompareOrdinal([string]$previous['subject'], [string]$candidate['subject'])
287
+ }
288
+ if ($order -le 0) {
289
+ break
290
+ }
291
+ $position -= 1
292
+ }
293
+ $sorted.Insert($position, $candidate)
294
+ }
295
+
296
+ return @($sorted.ToArray())
297
+ }
298
+
299
+ function Test-BlastRadius {
300
+ <#
301
+ .SYNOPSIS
302
+ Apply validation rules V1, V2, and V3 to a radius against its plan.
303
+
304
+ .DESCRIPTION
305
+ Port of validate_blast_radius. The radius is normalized on entry, which
306
+ reproduces the sorted, deduplicated invariant the Python BlastRadius
307
+ dataclass guarantees by construction and makes the result independent of
308
+ the order the caller happened to serialize its collections in.
309
+
310
+ .PARAMETER Radius
311
+ Radius record under validation, carrying exactly the keys paths,
312
+ modules, shared_surfaces, contracts, source, and computed_at.
313
+
314
+ .PARAMETER PlanText
315
+ Approved atomic-plan text the radius claims to cover; may be empty.
316
+
317
+ .PARAMETER Config
318
+ Parsed config/blast-radius.json.
319
+
320
+ .PARAMETER TrackedFileCount
321
+ Files tracked in the repository, a caller input so the library performs
322
+ no subprocess call. Must be a positive integer.
323
+
324
+ .OUTPUTS
325
+ System.Object[]. Finding hashtables with keys rule, severity, subject,
326
+ and message, sorted by rule then subject. An empty array means the radius
327
+ is valid.
328
+ #>
329
+ [CmdletBinding()]
330
+ [OutputType([System.Object[]])]
331
+ param(
332
+ [Parameter(Mandatory = $true)]
333
+ [AllowNull()]
334
+ [object] $Radius,
335
+ [Parameter(Mandatory = $true)]
336
+ [AllowEmptyString()]
337
+ [string] $PlanText,
338
+ [Parameter(Mandatory = $true)]
339
+ [AllowNull()]
340
+ [object] $Config,
341
+ [Parameter(Mandatory = $true)]
342
+ [AllowNull()]
343
+ [object] $TrackedFileCount
344
+ )
345
+
346
+ $normalized = ConvertTo-NormalizedBlastRadius -Radius $Radius
347
+ [void](Get-RequiredText -Value $PlanText -FieldName 'plan_text' -AllowEmpty)
348
+ # The root-surface set comes from the same -Config value that V1 and V2 use
349
+ # below to resolve modules and shared surfaces, and from the same reader
350
+ # Get-BlastRadius calls. That shared source is what keeps a derived radius
351
+ # passing V1 and V2 against its own plan (issue #452).
352
+ $planPath = [string[]]@(Get-PlanPaths -PlanText $PlanText `
353
+ -RootSurface ([string[]]@(Get-ConfigRootSurface -Config $Config)))
354
+ $planConcrete = [string[]]@(Get-ConcreteEntry -Entry $planPath)
355
+
356
+ $finding = [System.Collections.Generic.List[hashtable]]::new()
357
+ $finding.AddRange([hashtable[]]@(Get-CoverageFinding -Radius $normalized -PlanConcretePath $planConcrete))
358
+ $finding.AddRange([hashtable[]]@(
359
+ Get-SharedSurfaceFinding -Radius $normalized -PlanConcretePath $planConcrete -Config $Config))
360
+ $finding.AddRange([hashtable[]]@(
361
+ Get-OverBreadthFinding -Radius $normalized -Config $Config -TrackedFileCount $TrackedFileCount))
362
+
363
+ return @(Get-SortedRadiusFinding -Finding $finding.ToArray())
364
+ }
365
+
366
+ Export-ModuleMember -Function ConvertTo-NormalizedBlastRadius, Test-BlastRadius
@@ -0,0 +1,184 @@
1
+ # Parallel Orchestration Artifact Invariants
2
+
3
+ This rule governs the three artifacts of the `parallel` orchestration surface: the parallel-run manifest at `docs/features/parallel/<slug>/parallel.md`, the parallel-orchestrator checkpoint at `artifacts/orchestration/parallel-orchestrator-state.json`, and the parallel-planner checkpoint at `artifacts/orchestration/parallel-planner-state.json`. It records the invariants those artifacts must satisfy as numbered prose so that downstream features consume a fixed schema and add behavior only.
4
+
5
+ The `parallel` surface schedules thematically unrelated items concurrently by computed blast-radius contention rather than by a human-authored dependency graph. There is no `depends_on` field anywhere, `issue_num` is the primary key for every item reference, and there is no integration branch: each item opens its own pull request against `main`.
6
+
7
+ ## Foreign Schema Warning (do not copy verbatim)
8
+
9
+ The prohibition recorded in `.claude/rules/orchestrator-state.md` is restated here for the parallel artifacts. A hardened snapshot from another repository contains a JSON Schema for the orchestrator-state artifact whose `$id` references a foreign origin (`drmoisan.github.io/mix-calculator/`). That schema MUST NOT be copied verbatim into this repository, and it MUST NOT be adapted into a parallel-artifact schema: its `$id`, its top-level required-field set, and its cycle-level `additionalProperties: false` do not match this repository's checkpoint contract.
10
+
11
+ No JSON Schema file is authored, imported, or read for the parallel manifest or for either parallel checkpoint. The invariants above are expressed as prose in this file and enforced by validator logic. A schema whose `$id` is repo-local is not the disqualified foreign artifact, but the repository's enforcement mechanism remains prose-and-validator-logic regardless.
12
+
13
+ ## Scope and Backward Compatibility
14
+
15
+ These invariants apply only to the three parallel artifacts named above. They do not apply to, and do not change, the epic artifacts (`artifacts/orchestration/epic-orchestrator-state.json`, `artifacts/orchestration/epic-planner-state.json`) or the standard orchestrator-state checkpoint governed by `.claude/rules/orchestrator-state.md`. The parallel validators are additive: the existing epic validators, their helper modules, their TypeScript cores, and their tests are unmodified.
16
+
17
+ Every validator named below returns a list of error strings, never mutates its input, returns a single-element list for text that is not parseable, and rejects a non-object (non-mapping) root. Error strings use the literal context-prefixed style with the prefixes `Parallel checkpoint`, `Parallel planner checkpoint`, and `Parallel manifest`.
18
+
19
+ ## Invariants (parallel orchestrator checkpoint)
20
+
21
+ Enforced by `validate_parallel_orchestrator_state_text(text, *, require_complete=False)` in `scripts/dev_tools/validate_parallel_orchestrator_state.py`. Invariants 1 through 19 are enforced unconditionally; invariants 20 and 21 are enforced only under `require_complete`.
22
+
23
+ 1. **Required keys.** The checkpoint must carry `objective`, `completed_steps`, `next_step`, `last_updated`, `route_id`, `parallel_slug`, `parallel_manifest_path`, `parallel_status_doc_path`, `mode`, `max_concurrency`, `current_cohort`, `recolor_generation`, `cohorts`, `items`, `conflict_edges`, `mutations`, and `drift_events`. One error is emitted per missing key.
24
+
25
+ 2. **Route identity.** `route_id` must be exactly `'parallel'`.
26
+
27
+ 3. **Mode enum.** `mode` must be `closed` or `open`.
28
+
29
+ 4. **Bounded concurrency.** `max_concurrency` must be an integer from 1 through 8, and must not be a boolean.
30
+
31
+ 5. **Item uniqueness and shape.** Each `items[]` entry must be an object whose `issue_num` is a positive integer unique across items and whose `feature_folder` is a non-empty string.
32
+
33
+ 6. **Item state enum.** Each item's `state` must be one of the eight item-state values `proposed | admitted | prepared | scheduled | in_flight | merged | withdrawn | blocked`.
34
+
35
+ 7. **Merge-status enum.** Each item's `merge_status`, when present, must be one of the eight merge-status values `not_started | worktree_created | pr_open | ci_green | merged | worktree_removed | blocked_drift | blocked_ci_loop_limit`. An absent `merge_status` is treated as `not_started` and contributes zero errors.
36
+
37
+ 8. **State/merge-status consistency.** An item whose `merge_status` is `merged` or `worktree_removed` must have `state == 'merged'`. An item whose `merge_status` is `blocked_drift` or `blocked_ci_loop_limit` must have `state == 'blocked'`.
38
+
39
+ 9. **Blast-radius shape.** Each item's `blast_radius` must be an object carrying `paths`, `modules`, `shared_surfaces`, and `contracts` as lists of non-empty strings, a `source` in `{derived, declared, observed}`, and a non-empty `computed_at` string. `modules`, `shared_surfaces`, and `contracts` may be empty lists.
40
+
41
+ 10. **Prohibited dependency edges.** No object anywhere in the checkpoint may carry a `depends_on` key, and no top-level `depends_on` key may be present. Ordering is expressed only as blast-radius overlap. Presence is an explicit rejection, not mere absence.
42
+
43
+ 11. **Prohibited integration-branch fields.** The checkpoint must not carry `integration_branch` or `epic_merge_pr` at any level. Each parallel item opens its own pull request against `main`, so there is no integration branch and no final integration pull request.
44
+
45
+ 12. **Cohort shape and resolution.** Each `cohorts[]` entry must be an object with a non-negative integer `index`, a non-negative integer `generation` that is `<= recolor_generation`, and an `item_keys` list in which every entry resolves to an `items[].issue_num`.
46
+
47
+ 13. **Current-generation cohort uniqueness.** Among the `cohorts[]` entries whose `generation` equals `recolor_generation`, `index` values must be unique and every non-withdrawn item must appear in exactly one such cohort's `item_keys`. An item that appears in no current-generation cohort is permitted only in state `withdrawn`, `merged`, or `blocked`. The strictness is "exactly one", following the pinning model in which recoloring is a pure function over the unstarted subgraph and therefore implies full coverage.
48
+
49
+ 14. **Current-cohort bound.** `current_cohort` must be a non-negative integer. When any current-generation cohort exists, `current_cohort` must not exceed the maximum current-generation `index`.
50
+
51
+ 15. **Conflict-edge shape.** Each `conflict_edges[]` entry must be an object whose `a` and `b` resolve to distinct `items[].issue_num` values with `a < b` (numeric normalization, so edge identity is canonical and recomputation is deterministic), and whose `reason` is in `{path_overlap, module_overlap, shared_surface_overlap, contract_dependency}`. A self-edge, an unresolved endpoint, an unnormalized pair, a duplicate `(a, b)` pair, or an out-of-enum reason is a malformed edge.
52
+
53
+ 16. **Mutation shape.** Each `mutations[]` entry must satisfy the mutation table: `op` in `{add, remove, close, requeue}`; `item_key` resolving to an `items[].issue_num` for `add`, `remove`, and `requeue`, and null for `close` (a run-level operation); a non-empty `at`; `prior_state` and `new_state` either null or in the item-state enum, with `prior_state` null for `add` and `close` and `new_state` null for `close`; and `recolor_generation` a non-negative integer that is `<=` the top-level `recolor_generation`. Transition legality — which state may follow which — is downstream behavior, not schema; this validator checks shape, enum membership, and the null rules only.
54
+
55
+ 17. **In-flight removal requires a disposition.** A `mutations[]` entry with `op == 'remove'` and `prior_state == 'in_flight'` must carry `disposition` exactly `'detach'` or `'abandon'`. A `disposition` on any other entry must be null.
56
+
57
+ 18. **Drift-event shape.** Each `drift_events[]` entry must carry an `item_key` that resolves to an `items[].issue_num`, `declared` and `observed` as lists of non-empty strings, an `escaped_paths` list that is non-empty and holds non-empty strings (an event with zero escaped paths is not a drift event), a non-empty `at`, and an `action` in `{raised_blocking_finding, halted_later_started_item}`.
58
+
59
+ 19. **Receipt arrays.** `delegation_receipts`, `skill_receipts`, and `mcp_call_receipts`, when present, must each be a list. Absent receipt arrays contribute zero errors. Per-receipt content validation follows the loose tolerance of the standard checkpoint validators.
60
+
61
+ 20. **Completion gate, closed mode.** Under `require_complete` with `mode == 'closed'`, every item whose `state` is not `withdrawn` must have `merge_status` in `{merged, worktree_removed}`.
62
+
63
+ 21. **Completion gate, open mode.** Under `require_complete` with `mode == 'open'`, the checkpoint must additionally record a `mutations[]` entry with `op == 'close'` (the run-close record). Invariant 20's per-item condition applies in open mode as well.
64
+
65
+ When `require_complete` is not passed, invariants 20 and 21 contribute zero errors and the validation result is byte-identical to a plain call.
66
+
67
+ ## Invariants (parallel planner checkpoint)
68
+
69
+ Enforced by `validate_parallel_planner_state_text(text, *, require_ready_for_execution=False)` in `scripts/dev_tools/validate_parallel_planner_state.py`. P1 through P4 are enforced unconditionally; P6 through P9 are enforced only under `require_ready_for_execution`.
70
+
71
+ - **P1 — Required keys.** The checkpoint must carry `objective`, `parallel_slug`, `parallel_manifest_path`, `mode`, `max_concurrency`, `items`, `cohorts`, `conflict_edges`, `recolor_generation`, `completed_steps`, `next_step`, and `last_updated`. One error is emitted per missing key. `kickoff_prompt_path` is optional outside the ready gate.
72
+
73
+ - **P2 — Route-consistent identity.** `parallel_slug` and `parallel_manifest_path` must be non-empty strings; `mode` and `max_concurrency` satisfy orchestrator invariants 3 and 4.
74
+
75
+ - **P3 — Item shape.** Each `items[]` entry must carry `issue_num`, `feature_folder`, `kind`, `state`, `blast_radius`, `preparation_status`, `research_path`, `plan_path`, and `preflight_status`. `issue_num` must be a positive integer unique across items; `kind` must be in `{feature, bug}`; `state` must be in the item-state enum; `blast_radius` must satisfy orchestrator invariant 9; `complexity_band`, when present, must be in `{C1, C2, C3, C4}`. The prohibited-key rejections of orchestrator invariants 10 and 11 apply.
76
+
77
+ - **P4 — Cohort and edge shape.** `cohorts[]`, `conflict_edges[]`, and `recolor_generation` satisfy orchestrator invariants 12 through 15.
78
+
79
+ - **P5 — Deterministic recoloring seam (deliberately absent).** This feature does not recompute the cohort coloring. Recomputation parity against the cohort-computation module is the planner-surface feature's check (the analogue of the epic planner's wave-number cross-check). The omission is recorded here explicitly so a later reader does not mistake it for an oversight. There is no P5 check in the validator.
80
+
81
+ - **P6 — Ready gate, cardinality.** Under `require_ready_for_execution`, `items` must contain at least two entries.
82
+
83
+ - **P7 — Ready gate, preparation.** Under `require_ready_for_execution`, each item must have `preparation_status == 'prepared'`, `preflight_status == 'PREFLIGHT: ALL CLEAR'`, non-empty `research_path` and `plan_path`, and `blast_radius.source == 'declared'`. Only the planner-computed radius is authoritative for scheduling.
84
+
85
+ - **P8 — Ready gate, sentinel.** Under `require_ready_for_execution`, `next_step` must be exactly `'PARALLEL_EXECUTION_READY'`.
86
+
87
+ - **P9 — Ready gate, kickoff path.** Under `require_ready_for_execution`, `kickoff_prompt_path` must be exactly `artifacts/orchestration/parallel-kickoff-<parallel_slug>.md`.
88
+
89
+ The planner checkpoint carries no `epic_worthiness` analogue and no `NON_EPIC_RECOMMENDED` branch; the parallel surface has no worthiness verdict, and scale assessment happens before parallel planning is invoked. When `require_ready_for_execution` is not passed, P6 through P9 contribute zero errors.
90
+
91
+ ## Invariants (parallel run manifest)
92
+
93
+ Enforced by `validate_parallel_manifest_text(text)` in `scripts/dev_tools/parallel_manifest_contract.py`, with the default-resolving accessors `manifest_mode(mapping)` and `manifest_max_concurrency(mapping)`. Manifest validation is a library call; it is deliberately not a third MCP `artifact_type`.
94
+
95
+ - **M1 — Frontmatter block.** The document must open with a `---` YAML frontmatter block terminated by `---`, parseable by `yaml.safe_load` into a mapping. Extraction is tolerant of LF, CRLF, and CR line endings. A missing, unterminated, unparseable, or non-mapping frontmatter block is malformed.
96
+
97
+ - **M2 — Slug.** `parallel` must be a non-empty string.
98
+
99
+ - **M3 — Mode default.** `mode`, when present, must be `closed` or `open`. When absent it defaults to `closed`: the accessor `manifest_mode(mapping)` returns the default and the validator emits no error for absence.
100
+
101
+ - **M4 — Concurrency default.** `max_concurrency`, when present, must be an integer from 1 through 8. When absent it defaults to `4`: the accessor `manifest_max_concurrency(mapping)` returns the default and the validator emits no error for absence.
102
+
103
+ - **M5 — Created-at.** `created_at` must be a non-empty string.
104
+
105
+ - **M6 — Items.** `items` must be a list. An empty list is valid at authoring time. Each entry must be an object carrying `issue_num` (positive integer, unique across items), `feature_folder` (non-empty string), `kind` in `{feature, bug}`, `state` in the item-state enum, and `blast_radius` in the shape of orchestrator invariant 9.
106
+
107
+ - **M7 — Prohibited keys.** No `depends_on` key may appear at any level, and no `integration_branch` key may appear at top level. Presence is an explicit rejection.
108
+
109
+ ## Cache Doctrine — the checkpoint is not the source of truth
110
+
111
+ The parallel-orchestrator checkpoint is a CACHE of durable state, not the source of truth. Every field it records is re-derivable from the repository and from GitHub:
112
+
113
+ - `git worktree list --porcelain` — worktree existence and path (`items[].worktree_path`, `worktree_created_at`, `worktree_removed_at`).
114
+ - `git branch` — branch existence and name (`items[].branch_name`).
115
+ - `gh pr view --json state,mergedAt,headRefOid` — pull-request state, merge time, and merge commit (`items[].pr_number`, `pr_url`, `merge_status`, `merged_at`, `merge_commit_sha`).
116
+
117
+ No downstream feature may treat the checkpoint as authoritative. When the checkpoint disagrees with those three commands, the commands win and the checkpoint is rewritten from them. The validators in this rule check the checkpoint's structural shape only; they never assert that the cached values agree with the repository, because that reconciliation is a runtime concern of the orchestrator surface, not a schema concern.
118
+
119
+ ## Omitted Epic Schema Fields (S8)
120
+
121
+ There is no integration branch for a parallel run: each item opens its own pull request against `main`. The parallel schema therefore carries no integration-branch and no final-integration-pull-request fields. The disposition of every relevant epic field is fixed as follows.
122
+
123
+ | Epic field | Disposition in the parallel schema |
124
+ | --- | --- |
125
+ | `integration_branch` (top level) | OMITTED — no integration branch; its presence is a prohibited-key violation (invariant 11, M7) |
126
+ | `epic_merge_pr` / `epic_merge_pr.merge_commit_sha` | OMITTED — no final integration pull request; the completion gate checks per-item terminal states instead (invariants 20-21) |
127
+ | `features[].depends_on` | OMITTED everywhere — ordering is derived from blast-radius overlap; presence is a prohibited-key violation (invariant 10, P3, M7) |
128
+ | `waves[]`, `features[].wave_number`, `current_wave` | REPLACED by `cohorts[]`, `current_cohort`, and `recolor_generation` |
129
+ | `max_parallel_features` | REPLACED by `max_concurrency` |
130
+ | `epic_feature_folder` | REPLACED by `parallel_slug`, plus `parallel_manifest_path` and `parallel_status_doc_path` |
131
+ | merge-status values `merge_conflict`, `blocked_conflict_loop_limit` | REPLACED by `blocked_drift` and `blocked_ci_loop_limit` — the fan-in merge-conflict path does not exist; drift and per-item CI loops are the parallel failure modes |
132
+ | planner `epic_worthiness`, `NON_EPIC_RECOMMENDED` branch | OMITTED — the parallel planner contract carries no worthiness verdict |
133
+
134
+ Per-item `merge_commit_sha` is retained; only the run-level merge-pull-request block is omitted.
135
+
136
+ ## Concurrency Bound (A7)
137
+
138
+ `max_concurrency` is bounded at 1 through 8 inclusive and defaults to `4` when absent from the manifest. The design document sets only the default of 4; the upper bound of 8 is adopted here for symmetry with the epic surface, whose `max_parallel_features` is validated as `1..8`. The bound is recorded in this rule file so that downstream features do not re-litigate it. Booleans are rejected even though `True` and `False` are integers in Python.
139
+
140
+ The bound is enforced in three places with the same semantics: orchestrator invariant 4, planner invariant P2, and manifest invariant M4.
141
+
142
+ ## Drift-Event Recording Rule (A8)
143
+
144
+ `drift_events[].action` is the two-member enum `{raised_blocking_finding, halted_later_started_item}`. The recording rule is: one event per drift occurrence, carrying the STRONGEST action taken. `halted_later_started_item` subsumes `raised_blocking_finding`, so an occurrence that halted a later-started item records exactly one event with `action == 'halted_later_started_item'` and does not additionally record a `raised_blocking_finding` event for the same occurrence.
145
+
146
+ The drift-detection feature consumes this enum and this rule without extending either.
147
+
148
+ ## Enum Ownership (F6/F7/F8 consume, never extend)
149
+
150
+ All nine enums of the parallel surface are owned by the schema-and-validator feature (F3) and are fixed by this rule file:
151
+
152
+ | Enum | Members |
153
+ | --- | --- |
154
+ | `mode` | `closed`, `open` (default `closed`) |
155
+ | item `state` | `proposed`, `admitted`, `prepared`, `scheduled`, `in_flight`, `merged`, `withdrawn`, `blocked` |
156
+ | `merge_status` | `not_started`, `worktree_created`, `pr_open`, `ci_green`, `merged`, `worktree_removed`, `blocked_drift`, `blocked_ci_loop_limit` |
157
+ | `blast_radius.source` | `derived`, `declared`, `observed` |
158
+ | `items[].kind` | `feature`, `bug` |
159
+ | `conflict_edges[].reason` | `path_overlap`, `module_overlap`, `shared_surface_overlap`, `contract_dependency` |
160
+ | `mutations[].op` | `add`, `remove`, `close`, `requeue` |
161
+ | `mutations[].disposition` | `detach`, `abandon`, or null |
162
+ | `drift_events[].action` | `raised_blocking_finding`, `halted_later_started_item` |
163
+
164
+ 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.
165
+
166
+ ## F7 Seam
167
+
168
+ 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.
169
+
170
+ ## F3 Scope Boundary — kickoff contract deferred to F4
171
+
172
+ F3 deliberately excludes the kickoff-prompt contract module `scripts/dev_tools/parallel_kickoff_contract.py` and the `parallel-kickoff` `artifact_type`. Both are F4's scope, and F3 neither creates the module nor registers the artifact type on the CLI or MCP surfaces. The MCP surface grows by exactly two `artifact_type` values: `parallel-orchestrator-state` and `parallel-planner-state`.
173
+
174
+ F3's `require_ready_for_execution` gate is STRUCTURAL ONLY. It enforces the kickoff-PATH invariant (P9: `kickoff_prompt_path` must equal `artifacts/orchestration/parallel-kickoff-<parallel_slug>.md`) and does not parse or cross-check kickoff CONTENT. The deeper readiness-integrity machinery of the epic surface — git-integrity checks, launch-evidence binding, and kickoff-contract cross-checks — is left to F4, which may layer repository-aware checks behind an additional keyword without changing the schema. F3 likewise does not recompute the cohort coloring (planner invariant P5).
175
+
176
+ ## Enforcement
177
+
178
+ - `scripts/dev_tools/validate_parallel_orchestrator_state.py`, with the helper modules `scripts/dev_tools/_parallel_state_common.py`, `scripts/dev_tools/_parallel_state_structures.py`, and `scripts/dev_tools/_parallel_state_records.py`, appends one error per violated orchestrator invariant. The completion-gate invariants 20 and 21 run only when the caller passes `require_complete=True`.
179
+ - `scripts/dev_tools/validate_parallel_planner_state.py` appends one error per violated planner invariant. The ready-gate invariants P6 through P9 run only when the caller passes `require_ready_for_execution=True`.
180
+ - `scripts/dev_tools/parallel_manifest_contract.py` appends one error per violated manifest invariant and exposes the default-resolving accessors. Manifest validation is a library call, not an MCP artifact type.
181
+ - `scripts/dev_tools/validate_orchestration_artifacts.py` registers the CLI subparsers `parallel-orchestrator-state` (with `--require-complete`) and `parallel-planner-state` (with `--require-ready-for-execution`). An unknown artifact type continues to fail with `Unsupported artifact type: {type}`.
182
+ - The TypeScript parity port at `extensions/drm-copilot/src/lib/validate/parallel-state-shared.ts`, `parallel-state-structures.ts`, `parallel-state-records.ts`, `parallel-orchestrator-state-core.ts`, and `parallel-planner-state-core.ts` reproduces the same invariants and is dispatched from `extensions/drm-copilot/src/lib/validate/orchestration-artifacts.ts` for both new `artifact_type` values. Verified scope: 96 of 96 error strings matched across 43 constructed documents, for JSON-representable values that round-trip through both runtimes' native types. Three divergence classes are known outside that verified scope: (1) **`pythonRepr` quote selection** — `parallel-state-shared.ts:112-132` always single-quotes, while Python's `repr` switches to double quotes when the value contains a single quote (recorded repo-wide at `docs/features/potential/2026-08-07-python-repr-quote-selection-divergence.md`); (2) **integral floats** — `JSON.parse` erases Python's `int`/`float` distinction, so an integral float value produces a different Python-side error count than the TypeScript side; (3) **boolean/integer equality** — `parallel-state-structures.ts:228` uses `===`, so a boolean value is not selected the way Python's `True == 1` equality selects it, producing differing error counts.
183
+ - Enforcement is therefore Python validator logic, plus the TypeScript parity port, plus this prose file. It is NEVER an imported JSON Schema. No schema file is read at validation time.
184
+ - The `parallel` route entry lives in `config/orchestration-routing.json` with `requires_pr_gate: false` (there is no run-level pull request to gate; each child's own route checkpoint enforces its per-item pull-request gate) and is mirrored byte-for-byte in `extensions/drm-copilot/resources/config/orchestration-routing.json`.
@@ -114,6 +114,14 @@
114
114
  {
115
115
  "type": "command",
116
116
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-worktree-removal-gate.ps1"
117
+ },
118
+ {
119
+ "type": "command",
120
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-parallel-worktree-removal-gate.ps1"
121
+ },
122
+ {
123
+ "type": "command",
124
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-parallel-abandon-gate.ps1"
117
125
  }
118
126
  ]
119
127
  },
@@ -184,6 +192,14 @@
184
192
  {
185
193
  "type": "command",
186
194
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-invocation-origin.ps1"
195
+ },
196
+ {
197
+ "type": "command",
198
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-parallel-cohort-barrier.ps1"
199
+ },
200
+ {
201
+ "type": "command",
202
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-parallel-drift-gate.ps1"
187
203
  }
188
204
  ]
189
205
  }
@@ -246,6 +262,15 @@
246
262
  "command": "pwsh -NoProfile -File .claude/hooks/validate-orchestrator-output.ps1 -CheckpointPath artifacts/orchestration/epic-orchestrator-state.json -ArtifactType epic-orchestrator-state"
247
263
  }
248
264
  ]
265
+ },
266
+ {
267
+ "matcher": "parallel-orchestrator",
268
+ "hooks": [
269
+ {
270
+ "type": "command",
271
+ "command": "pwsh -NoProfile -File .claude/hooks/validate-orchestrator-output.ps1 -CheckpointPath artifacts/orchestration/parallel-orchestrator-state.json -ArtifactType parallel-orchestrator-state"
272
+ }
273
+ ]
249
274
  }
250
275
  ]
251
276
  },