@danmoisan/drm-copilot-mcp 1.0.23 → 1.0.26

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 (43) hide show
  1. package/out/mcp-server.js +504 -174
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +5 -3
  4. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +11 -4
  5. package/resources/claude-customizations/.claude/agents/parallel-planner.md +5 -2
  6. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +28 -8
  7. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +28 -8
  8. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +117 -46
  9. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +115 -3
  10. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +312 -0
  11. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +392 -0
  12. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +500 -0
  13. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +58 -67
  14. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +383 -0
  15. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +297 -0
  16. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +298 -0
  17. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +232 -43
  18. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +416 -0
  19. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +366 -0
  20. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +408 -0
  21. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +428 -0
  22. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +377 -0
  23. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +166 -0
  24. package/resources/claude-customizations/.claude/rules/general-unit-test.md +1 -1
  25. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +28 -3
  26. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  27. package/resources/claude-customizations/.claude/rules/quality-tiers.md +3 -3
  28. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -4
  29. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +10 -5
  30. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +108 -34
  31. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +71 -9
  32. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +7 -3
  33. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +1 -1
  34. package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +4 -4
  35. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
  36. package/resources/claude-customizations/config/blast-radius.json +1 -3
  37. package/resources/claude-customizations/pack-manifests/core.json +12 -0
  38. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +1 -1
  39. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +3 -3
  40. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +3 -3
  41. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
  42. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  43. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.0.23",
3
+ "version": "1.0.26",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -109,12 +109,14 @@ Coverage metrics are mandatory for every language that has changed files in the
109
109
 
110
110
  Coverage thresholds follow the uniform tier rule (Authoritative Decision #2) defined in `.claude/rules/quality-tiers.md`:
111
111
 
112
- - **New code files** (files added in this feature, not previously existing): line coverage >= 85%, branch coverage >= 75%.
113
- - **Modified files** (files that existed before and were changed): line coverage >= 85%, branch coverage >= 75%, and no regression on changed lines relative to baseline.
114
- - **Repo-wide per language**: line coverage >= 85%, branch coverage >= 75%.
112
+ - **New code files** (files added in this feature, not previously existing): line coverage >= 85%, branch coverage >= 75% for branch-capable languages.
113
+ - **Modified files** (files that existed before and were changed): line coverage >= 85%, branch coverage >= 75% for branch-capable languages, and no regression on changed lines relative to baseline.
114
+ - **Repo-wide per language**: line coverage >= 85%, branch coverage >= 75% for branch-capable languages.
115
115
 
116
116
  Tier-specific lower thresholds are not used.
117
117
 
118
+ The branch threshold applies only to branch-capable languages — TypeScript, Python, and C#. PowerShell is a coverage language and is fully subject to the line threshold and the no-regression requirement, but Pester measures command (instruction) coverage and line coverage only, so no branch percentage exists to evaluate and no branch threshold applies to it (see `.claude/rules/powershell.md`). Do not record FAIL for an absent PowerShell branch figure.
119
+
118
120
  ### Verification Procedure
119
121
 
120
122
  For each language that has changed files in the feature branch:
@@ -187,10 +187,17 @@ Read `cohorts[] { index, generation, item_keys[] }` and schedule from it exactly
187
187
 
188
188
  Two scheduling rules govern every launch:
189
189
 
190
- 1. **Cohort barrier.** Cohort `N+1` branches from `main` only after every cohort-`N` item is
191
- `merged` or `worktree_removed`. `current_cohort` increments only on durable confirmation via
192
- `git` and `gh` commands, never from in-memory notifications. A blocked item is neither `merged`
193
- nor `worktree_removed`, so a blocked item holds the barrier.
190
+ 1. **Cohort barrier (per-edge).** An item may start only when every conflicting neighbour
191
+ (`conflict_edges[]`) that sits in a strictly prior current-generation cohort has `merge_status`
192
+ of `merged` or `worktree_removed`. `ci_green` does not satisfy the barrier. Same-cohort and
193
+ later-cohort neighbours do not hold an item back, and an item with no conflicting prior-cohort
194
+ neighbour may start regardless of other cohorts' progress. Evaluate the predicate only against
195
+ durable state confirmed by `git` and `gh` commands, never from in-memory notifications. A
196
+ blocked item is neither `merged` nor `worktree_removed`, so it holds only its own conflicting
197
+ later-cohort neighbours and, transitively, the tail of its own conflict component; unrelated
198
+ lanes keep advancing. `current_cohort` is a PROGRESS INDICATOR — the lowest current-generation
199
+ cohort index still holding a non-terminal, non-withdrawn item, updated only on the same durable
200
+ confirmation — and gates nothing.
194
201
  2. **`max_concurrency` slot filling.** `max_concurrency` caps the number of simultaneously in-flight
195
202
  items independently of cohort size. Fill slots in ascending item-key (`issue_num`) order, and
196
203
  refill each freed slot with the next unstarted item of the current cohort in the same ascending
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: parallel-planner
3
3
  model: opus
4
- description: Planning half of the parallel orchestration surface. It performs item intake over issue numbers and potential-entry paths, drives per-item preparation (promotion, research, spec/user-story, atomic plan, preflight clearance) through concurrent preparation-mode Agent(orchestrator) delegations, computes and validates each item's blast radius, seeds the generation-0 cohort table, writes the parallel run manifest and the planner checkpoint, and emits the parallel-orchestrator kickoff prompt artifact. Performs no atomic execution, PR authoring, or CI monitoring.
4
+ description: Planning half of the parallel orchestration surface. It performs item intake over issue numbers and potential-entry paths, drives per-item preparation (promotion, research, spec/user-story, atomic plan, preflight clearance) through preparation-mode Agent(orchestrator) delegations launched in bounded waves of at most max_concurrency, computes and validates each item's blast radius, seeds the generation-0 cohort table, writes the parallel run manifest and the planner checkpoint, and emits the parallel-orchestrator kickoff prompt artifact. Performs no atomic execution, PR authoring, or CI monitoring.
5
5
  tools:
6
6
  - "Agent(orchestrator)"
7
7
  - Read
@@ -90,7 +90,10 @@ On every invocation:
90
90
  ## Delegation Model
91
91
 
92
92
  You delegate exclusively through `Agent(orchestrator)`, one delegation per item, each carrying the
93
- preparation-mode kickoff line defined in the `parallel-plan` skill. Each child `orchestrator` runs
93
+ preparation-mode kickoff line defined in the `parallel-plan` skill. Delegations are launched in
94
+ BOUNDED WAVES of at most `max_concurrency`, computed with
95
+ `bash .claude/lib/bash/compute-concurrency-batches.sh`, with wave *k+1* launched only after every
96
+ child of wave *k* has terminated. Never launch every item's preparation at once. Each child `orchestrator` runs
94
97
  promotion, research, feature documents, atomic planning, and preflight clearance under
95
98
  `route_id: preparation`, commits and pushes its own branch, then stops before any execution. You do
96
99
  not delegate directly to `atomic-planner`, `atomic-executor`, `task-researcher`, or `prd-feature`;
@@ -25,8 +25,10 @@
25
25
  interprets the CLI's exit code and captured output.
26
26
 
27
27
  .NOTES
28
- Compatible with PowerShell 7+. Read-only validation gate; the validator
29
- subprocess is the only external process invoked.
28
+ Requires PowerShell 7.4+ (the shared validation module uses
29
+ `Test-Json -SchemaFile` Draft 2020-12 support). Read-only validation gate that
30
+ invokes NO external process: validation runs in-process through
31
+ `.claude/lib/discovery-validation/DiscoveryValidation.psm1` (issue #475).
30
32
  #>
31
33
  [CmdletBinding()]
32
34
  param()
@@ -34,11 +36,23 @@ param()
34
36
  function Invoke-DiscoveryValidatorExe {
35
37
  <#
36
38
  .SYNOPSIS
37
- Wrapper around the discovery-artifact validator CLI. Mockable seam.
39
+ Wrapper around the discovery-artifact validator. Mockable seam.
38
40
  .DESCRIPTION
39
- Invokes `python -m scripts.dev_tools.validate_discovery_artifacts` with
40
- the supplied arguments and captures both stdout and stderr. Tests mock
41
- this function directly; production code must never mock `python`.
41
+ Delegates to the portable PowerShell implementation in
42
+ `.claude/lib/discovery-validation/DiscoveryValidation.psm1`, keeping this
43
+ function's name, its `-ValidatorArgs <string[]>` parameter, and its
44
+ `@{ ExitCode; Output }` return shape unchanged so existing mocks and
45
+ `Should -Invoke` assertions continue to bind.
46
+
47
+ This no longer invokes a Python interpreter (issue #475). The `.claude/**`
48
+ payload ships to destinations with no guaranteed Python, Poetry, or
49
+ `scripts/dev_tools`, where the previous `python -m ...` call failed
50
+ obscurely or blocked every operation.
51
+
52
+ Success is SILENT by contract: a passing validation returns `ExitCode = 0`
53
+ with an EMPTY `Output`. The caller denies on a non-zero exit code OR on
54
+ non-empty output, so any success chatter here would deny a passing
55
+ validation (defect D-2).
42
56
  #>
43
57
  [CmdletBinding()]
44
58
  [OutputType([hashtable])]
@@ -47,8 +61,14 @@ function Invoke-DiscoveryValidatorExe {
47
61
  [string[]] $ValidatorArgs
48
62
  )
49
63
 
50
- $output = & python -m scripts.dev_tools.validate_discovery_artifacts @ValidatorArgs 2>&1
51
- return @{ ExitCode = $LASTEXITCODE; Output = ($output | Out-String).Trim() }
64
+ $modulePath = Join-Path -Path $PSScriptRoot `
65
+ -ChildPath '../lib/discovery-validation/DiscoveryValidation.psm1'
66
+ if (-not (Test-Path -LiteralPath $modulePath -PathType Leaf)) {
67
+ return @{ ExitCode = 1; Output = "Discovery-validation module not found: $modulePath" }
68
+ }
69
+
70
+ Import-Module -Name $modulePath -Force -ErrorAction Stop
71
+ return Invoke-DiscoveryArtifactValidation -ValidatorArgs $ValidatorArgs
52
72
  }
53
73
 
54
74
  function Get-DiscoveryArtifactType {
@@ -28,8 +28,10 @@
28
28
  captured output.
29
29
 
30
30
  .NOTES
31
- Compatible with PowerShell 7+. Read-only validation gate; the validator
32
- subprocess is the only external process invoked.
31
+ Requires PowerShell 7.4+ (the shared validation module uses
32
+ `Test-Json -SchemaFile` Draft 2020-12 support). Read-only validation gate that
33
+ invokes NO external process: validation runs in-process through
34
+ `.claude/lib/discovery-validation/DiscoveryValidation.psm1` (issue #475).
33
35
  #>
34
36
  [CmdletBinding()]
35
37
  param()
@@ -37,11 +39,23 @@ param()
37
39
  function Invoke-DiscoveryValidatorExe {
38
40
  <#
39
41
  .SYNOPSIS
40
- Wrapper around the discovery-artifact validator CLI. Mockable seam.
42
+ Wrapper around the discovery-artifact validator. Mockable seam.
41
43
  .DESCRIPTION
42
- Invokes `python -m scripts.dev_tools.validate_discovery_artifacts` with
43
- the supplied arguments and captures both stdout and stderr. Tests mock
44
- this function directly; production code must never mock `python`.
44
+ Delegates to the portable PowerShell implementation in
45
+ `.claude/lib/discovery-validation/DiscoveryValidation.psm1`, keeping this
46
+ function's name, its `-ValidatorArgs <string[]>` parameter, and its
47
+ `@{ ExitCode; Output }` return shape unchanged so existing mocks and
48
+ `Should -Invoke` assertions continue to bind.
49
+
50
+ This no longer invokes a Python interpreter (issue #475). The `.claude/**`
51
+ payload ships to destinations with no guaranteed Python, Poetry, or
52
+ `scripts/dev_tools`, where the previous `python -m ...` call failed
53
+ obscurely or blocked every operation.
54
+
55
+ Success is SILENT by contract: a passing validation returns `ExitCode = 0`
56
+ with an EMPTY `Output`. The caller denies on a non-zero exit code OR on
57
+ non-empty output, so any success chatter here would deny a passing
58
+ validation (defect D-2).
45
59
  #>
46
60
  [CmdletBinding()]
47
61
  [OutputType([hashtable])]
@@ -50,8 +64,14 @@ function Invoke-DiscoveryValidatorExe {
50
64
  [string[]] $ValidatorArgs
51
65
  )
52
66
 
53
- $output = & python -m scripts.dev_tools.validate_discovery_artifacts @ValidatorArgs 2>&1
54
- return @{ ExitCode = $LASTEXITCODE; Output = ($output | Out-String).Trim() }
67
+ $modulePath = Join-Path -Path $PSScriptRoot `
68
+ -ChildPath '../lib/discovery-validation/DiscoveryValidation.psm1'
69
+ if (-not (Test-Path -LiteralPath $modulePath -PathType Leaf)) {
70
+ return @{ ExitCode = 1; Output = "Discovery-validation module not found: $modulePath" }
71
+ }
72
+
73
+ Import-Module -Name $modulePath -Force -ErrorAction Stop
74
+ return Invoke-DiscoveryArtifactValidation -ValidatorArgs $ValidatorArgs
55
75
  }
56
76
 
57
77
  function Get-DiscoveryArtifactType {
@@ -149,31 +149,94 @@ function Test-HumanInteractionShape {
149
149
  return @{ Ok = $true; Message = $null }
150
150
  }
151
151
 
152
+ function Test-OrchestratorCheckpointStructure {
153
+ <#
154
+ .SYNOPSIS
155
+ Type-scoped structural check for the epic and parallel checkpoint types.
156
+ .DESCRIPTION
157
+ PD-3 implementation. The Python reference exposes no validation surface for
158
+ `epic-orchestrator-state` or `parallel-orchestrator-state` under this hook's
159
+ flag pair: argparse rejects the pair and exits 2 without running a single
160
+ check, so parity is UNDEFINED in this region. Rather than inherit an
161
+ undefined behavior, this hook defines it: the checkpoint must exist, parse
162
+ as JSON, and have an object root. That is the largest assertion that holds
163
+ for every checkpoint type without importing a schema this hook does not own.
164
+
165
+ Deliberately NOT applied here:
166
+ - the standard-checkpoint REQUIRED_STATE_KEYS presence block, whose key
167
+ set belongs to the standard checkpoint and would produce false
168
+ ROUTING_CONTRACT_BLOCKED verdicts against a well-formed epic or
169
+ parallel checkpoint (defect D-1),
170
+ - the model-routing gate, whose receipts live on the standard checkpoint.
171
+
172
+ The check fails closed: a missing file, unreadable content, invalid JSON, or
173
+ a non-object root all yield ExitCode 1 with the load error as Output.
174
+ .PARAMETER CheckpointPath
175
+ The path to the checkpoint JSON file.
176
+ .OUTPUTS
177
+ System.Collections.Hashtable with keys ExitCode (int, 0 or 1) and Output (string).
178
+ #>
179
+ [CmdletBinding()]
180
+ [OutputType([hashtable])]
181
+ param(
182
+ [Parameter(Mandatory = $true)]
183
+ [string] $CheckpointPath
184
+ )
185
+
186
+ # Get-OrchestratorStateCheckpoint already implements exactly the three
187
+ # structural conditions (exists, parses, object root) and reports each as a
188
+ # fail-closed Error string, so the structural leg reuses it rather than
189
+ # duplicating the load contract.
190
+ $loaded = Get-OrchestratorStateCheckpoint -CheckpointPath $CheckpointPath
191
+ if (-not $loaded.Ok) {
192
+ return @{ ExitCode = 1; Output = $loaded.Error }
193
+ }
194
+
195
+ return @{ ExitCode = 0; Output = '' }
196
+ }
197
+
152
198
  function Invoke-RoutingContractValidation {
153
199
  <#
154
200
  .SYNOPSIS
155
- Runs the authoritative Python routing-contract validator against the
156
- on-disk checkpoint and reports whether it emitted errors.
201
+ Runs the portable routing-contract validation against the on-disk
202
+ checkpoint and reports whether it emitted errors.
157
203
  .DESCRIPTION
158
- Invokes the validator through an injectable subprocess scriptblock seam.
159
- The default Invoker runs the authoritative Python CLI:
160
- python -m scripts.dev_tools.validate_orchestration_artifacts \
161
- <ArtifactType> <CheckpointPath> --require-complete
162
- Tests inject a mock scriptblock so no Python process runs. The function
163
- does not reimplement routing logic; it delegates to the Python validator.
164
- ArtifactType defaults to 'orchestrator-state' so the default invocation
165
- string is unchanged for every existing caller of this hook.
204
+ Invokes the validation through an injectable scriptblock seam. As of issue
205
+ #475 the default Invoker names no interpreter and starts no subprocess: the
206
+ portable PowerShell path is the ONLY path, so the hook behaves identically
207
+ in this repository and in every destination that received only the
208
+ pushed-down `.claude` pack. The former capability-detection probe and the
209
+ interpreter-subprocess leg it guarded are both gone.
210
+
211
+ The default Invoker dispatches on ArtifactType:
212
+
213
+ orchestrator-state
214
+ the COMPLETE-PARITY completion validation
215
+ (Test-OrchestratorStateCompletionReadiness), a row-by-row port of the
216
+ Python call surface `--require-complete --require-model-routing`.
217
+
218
+ epic-orchestrator-state, parallel-orchestrator-state
219
+ the type-scoped structural check
220
+ (Test-OrchestratorCheckpointStructure): exists, parses as JSON,
221
+ object root. PD-3: this is DEFINED behavior in a region where Python
222
+ parity is UNDEFINED (argparse exit 2, zero checks run). It is a
223
+ design decision, not a deferral. The standard-checkpoint
224
+ REQUIRED_STATE_KEYS block and the model-routing gate are deliberately
225
+ not applied, which is the D-1 fix.
226
+
227
+ anything else
228
+ fail closed, naming the unsupported type. An unrecognized type must
229
+ never read as a clean pass.
230
+
231
+ ArtifactType defaults to 'orchestrator-state' so every existing caller of
232
+ this hook keeps its current behavior.
166
233
 
167
234
  Returns a hashtable with keys:
168
- - HasErrors: $true only when the validator reported a non-zero exit
235
+ - HasErrors: $true only when the validation reported a non-zero exit
169
236
  code; $false when it exited 0. The exit code is the sole
170
- discriminator, because the validator prints its success
171
- line to stdout on a clean pass and the default Invoker
172
- captures with 2>&1, so output text is present on success.
173
- - ErrorText: the validator's combined captured output text, carried
174
- through unchanged: the error lines on a failure, and the
175
- success line (Python CLI) or empty (portable fallback)
176
- on a clean pass.
237
+ discriminator.
238
+ - ErrorText: the validation's output text, carried through unchanged:
239
+ the error lines on a failure, empty on a clean pass.
177
240
  #>
178
241
  [CmdletBinding()]
179
242
  [OutputType([hashtable])]
@@ -187,29 +250,36 @@ function Invoke-RoutingContractValidation {
187
250
  [Parameter(Mandatory = $false)]
188
251
  [scriptblock] $Invoker = {
189
252
  param($Path, $Type)
190
- # Capability detection: use the authoritative Python CLI when
191
- # scripts.dev_tools is importable (drm-copilot); otherwise fall back to
192
- # the portable PowerShell completion module that travels with the
193
- # pushed-down pack. The portable path performs the presence-level
194
- # required-once-delegated existence gate and still fails closed.
195
- if (Test-PythonOrchestratorValidatorAvailable) {
196
- $output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
197
- $Type $Path --require-complete --require-model-routing 2>&1
198
- [pscustomobject]@{
199
- ExitCode = $LASTEXITCODE
200
- Output = ($output | Out-String)
253
+ switch ($Type) {
254
+ 'orchestrator-state' {
255
+ # Import the portable completion module only when its function is not
256
+ # already available, so a repeated call (or a test that pre-imports and
257
+ # mocks the function) does not reload the module and reset the seam.
258
+ if (-not (Get-Command -Name Test-OrchestratorStateCompletionReadiness -ErrorAction SilentlyContinue)) {
259
+ Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force
260
+ }
261
+ $portable = Test-OrchestratorStateCompletionReadiness -CheckpointPath $Path
262
+ [pscustomobject]@{
263
+ ExitCode = $portable.ExitCode
264
+ Output = $portable.Output
265
+ }
201
266
  }
202
- } else {
203
- # Import the portable completion module only when its function is not
204
- # already available, so a repeated call (or a test that pre-imports and
205
- # mocks the function) does not reload the module and reset the seam.
206
- if (-not (Get-Command -Name Test-OrchestratorStateCompletionReadiness -ErrorAction SilentlyContinue)) {
207
- Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force
267
+ { $_ -in @('epic-orchestrator-state', 'parallel-orchestrator-state') } {
268
+ # PD-3: defined fail-closed structural behavior in an
269
+ # undefined-parity region. See Test-OrchestratorCheckpointStructure.
270
+ $structural = Test-OrchestratorCheckpointStructure -CheckpointPath $Path
271
+ [pscustomobject]@{
272
+ ExitCode = $structural.ExitCode
273
+ Output = $structural.Output
274
+ }
208
275
  }
209
- $portable = Test-OrchestratorStateCompletionReadiness -CheckpointPath $Path
210
- [pscustomobject]@{
211
- ExitCode = $portable.ExitCode
212
- Output = $portable.Output
276
+ default {
277
+ # Fail closed on an unwired type: an unrecognized artifact type is
278
+ # not a clean pass.
279
+ [pscustomobject]@{
280
+ ExitCode = 1
281
+ Output = "orchestrator hook: unsupported artifact type '$Type'; no validation surface is wired for it. Supported types: orchestrator-state, epic-orchestrator-state, parallel-orchestrator-state."
282
+ }
213
283
  }
214
284
  }
215
285
  }
@@ -225,10 +295,10 @@ function Invoke-RoutingContractValidation {
225
295
  $outputText = ([string]$result.Output).Trim()
226
296
  }
227
297
 
228
- # The exit code is the complete failure discriminator: the validator prints every
229
- # error to stderr and returns non-zero, and prints its success line to stdout and
230
- # returns 0. Because the default invoker captures with 2>&1, the success line lands
231
- # in $outputText on a clean pass, so output text must not influence this decision.
298
+ # The exit code is the complete failure discriminator: every dispatch leg
299
+ # returns a non-zero ExitCode with its error text on failure and ExitCode 0
300
+ # with empty Output on a clean pass, so output text must not influence this
301
+ # decision.
232
302
  $hasErrors = ($exitCode -ne 0)
233
303
  return @{ HasErrors = $hasErrors; ErrorText = $outputText }
234
304
  }
@@ -313,9 +383,10 @@ function Invoke-OrchestratorOutputValidation {
313
383
  return @{ Ok = $false; Message = $hiResult.Message }
314
384
  }
315
385
 
316
- # Delegate to the authoritative Python routing-contract validator. The
317
- # optional RoutingInvoker seam lets tests inject a mock; the default seam
318
- # produces the real subprocess call.
386
+ # Delegate to the portable routing-contract validation, dispatched on
387
+ # ArtifactType. The optional RoutingInvoker seam lets tests inject a mock; the
388
+ # default seam runs the in-process PowerShell validation and starts no
389
+ # subprocess.
319
390
  $routingArgs = @{ CheckpointPath = $CheckpointPath; ArtifactType = $ArtifactType }
320
391
  if ($PSBoundParameters.ContainsKey('RoutingInvoker') -and $null -ne $RoutingInvoker) {
321
392
  $routingArgs['Invoker'] = $RoutingInvoker
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # parallel-manifest-validate.sh: sourceable bash port of
3
3
  # scripts/dev_tools/parallel_manifest_contract.py. Validates a parallel-run
4
- # manifest document against invariants M1 through M7 and exposes the two
4
+ # manifest document against invariants M1 through M8 and exposes the two
5
5
  # default-resolving accessors that every consumer uses instead of reading
6
6
  # `mode` and `max_concurrency` directly.
7
7
  #
@@ -9,7 +9,8 @@
9
9
  # frontmatter checks short-circuit with a single error; then run identity
10
10
  # (M2 parallel, M3 mode, M4 max_concurrency, M5 created_at) in schema field
11
11
  # order; then the M7 prohibited-key scan, deep results in document order
12
- # followed by the top-level results; then the M6 items collection.
12
+ # followed by the top-level results; then the M6 items collection; then the
13
+ # key-gated M8 expected_conflict_components assertion.
13
14
  #
14
15
  # Every error string begins with the literal prefix `Parallel manifest` and
15
16
  # ends with a period. The Python module remains the repository authority.
@@ -43,7 +44,7 @@ PM_DEFAULT_MAX_CONCURRENCY=4
43
44
 
44
45
  # Inclusive bounds on a present `max_concurrency` (invariant M4, A7).
45
46
  PM_MIN_CONCURRENCY=1
46
- PM_MAX_CONCURRENCY=8
47
+ PM_MAX_CONCURRENCY=32
47
48
 
48
49
  # Keys the manifest rejects at any nesting level (invariant M7).
49
50
  PM_DEEP_PROHIBITED_KEYS="depends_on"
@@ -54,6 +55,11 @@ PM_TOP_LEVEL_PROHIBITED_KEYS="integration_branch"
54
55
  # Detail recorded when the parser refuses an out-of-subset construct.
55
56
  PM_SUBSET_DETAIL=""
56
57
 
58
+ # Issue numbers already claimed by an earlier expected_conflict_components
59
+ # entry (invariant M8). Threaded across components so cross-component duplicate
60
+ # membership is decided in one pass; reset by pm_validate_text.
61
+ PM_CLAIMED=""
62
+
57
63
  pm_parse_manifest() {
58
64
  # Parse manifest text into the shared node table (invariant M1).
59
65
  #
@@ -128,6 +134,108 @@ pm_validate_identity() {
128
134
  fi
129
135
  }
130
136
 
137
+ pm_declared_issue_nums() {
138
+ # Echo the space-separated issue_num values the items collection declares.
139
+ #
140
+ # Supplies the resolution target for invariant M8 without re-running the M6
141
+ # item validation: a malformed entry is reported once by M6 and simply
142
+ # contributes no resolvable key here.
143
+ local declared="" count index entry_path issue_type issue_value
144
+ [[ $(yp_type_of "items") == seq ]] || return 0
145
+ count=$(yp_count_of "items")
146
+ # Accept only entries whose primary key is already well formed; admitting a
147
+ # malformed one would produce a second, confusing M8 error for one defect.
148
+ for ((index = 0; index < count; index++)); do
149
+ entry_path="items[${index}]"
150
+ [[ $(yp_type_of "$entry_path") == map ]] || continue
151
+ issue_type=$(yp_type_of "${entry_path}.issue_num")
152
+ issue_value=$(yp_value_of "${entry_path}.issue_num")
153
+ pc_is_positive_integer "$issue_type" "$issue_value" || continue
154
+ declared="$declared $issue_value"
155
+ done
156
+ printf '%s' "$declared"
157
+ }
158
+
159
+ pm_validate_component_members() {
160
+ # Validate one component's members list against invariant M8.
161
+ #
162
+ # Args: $1 = node path of the members list, $2 = component-scoped context
163
+ # prefix, $3 = space-separated declared issue_num values. Appends every
164
+ # accepted key to PM_CLAIMED so the caller's running set enforces the
165
+ # no-duplicate-membership rule across components.
166
+ local path="$1" ctx="$2" declared="$3"
167
+ local members_type count=0 index slot entry_path member_type member_value
168
+ members_type=$(yp_type_of "$path")
169
+ if [[ $members_type == seq ]]; then
170
+ count=$(yp_count_of "$path")
171
+ fi
172
+ # A missing key, a scalar, and an empty list are one violation: the
173
+ # component asserts no membership and therefore carries no information.
174
+ if [[ $members_type != seq ]] || ((count == 0)); then
175
+ pc_error_add "$ctx members must be a non-empty list of positive integers."
176
+ return 0
177
+ fi
178
+
179
+ # Three successive gates per member; the first failure ends that member's
180
+ # checks, because an unusable value cannot be resolved and an unresolved key
181
+ # cannot meaningfully duplicate another.
182
+ for ((index = 0; index < count; index++)); do
183
+ entry_path="${path}[${index}]"
184
+ slot="$ctx members[${index}]"
185
+ member_type=$(yp_type_of "$entry_path")
186
+ member_value=$(yp_value_of "$entry_path")
187
+ if ! pc_is_positive_integer "$member_type" "$member_value"; then
188
+ pc_error_add "$slot must be a positive integer; found: $(pi_repr_at "$entry_path")."
189
+ continue
190
+ fi
191
+ if ! pc_contains_word "$declared" "$member_value"; then
192
+ pc_error_add "$slot does not resolve to an items[] issue_num; found: $member_value."
193
+ continue
194
+ fi
195
+ if pc_contains_word "$PM_CLAIMED" "$member_value"; then
196
+ pc_error_add "$slot repeats issue_num $member_value, already claimed by an earlier component."
197
+ continue
198
+ fi
199
+ PM_CLAIMED="$PM_CLAIMED $member_value"
200
+ done
201
+ }
202
+
203
+ pm_validate_expected_components() {
204
+ # Validate the optional expected_conflict_components key (invariant M8).
205
+ #
206
+ # Key gated: absence is the overwhelmingly common shape and must cost the
207
+ # caller nothing, so the whole invariant is skipped rather than defaulted
208
+ # and a pre-M8 manifest's error list is unchanged.
209
+ local root="expected_conflict_components"
210
+ yp_has "$root" || return 0
211
+ if [[ $(yp_type_of "$root") != seq ]]; then
212
+ pc_error_add "$PM_CONTEXT $root must be a list."
213
+ return 0
214
+ fi
215
+
216
+ local declared total position comp_path ctx name_type name_value
217
+ declared=$(pm_declared_issue_nums)
218
+ total=$(yp_count_of "$root")
219
+ for ((position = 0; position < total; position++)); do
220
+ comp_path="${root}[${position}]"
221
+ ctx="$PM_CONTEXT ${root}[${position}]"
222
+ if [[ $(yp_type_of "$comp_path") != map ]]; then
223
+ pc_error_add "$ctx must be an object."
224
+ continue
225
+ fi
226
+ # Field order follows the documented authoring order: the optional
227
+ # diagnostic label first, then the required membership list.
228
+ if yp_has "${comp_path}.name"; then
229
+ name_type=$(yp_type_of "${comp_path}.name")
230
+ name_value=$(yp_value_of "${comp_path}.name")
231
+ if ! pc_is_non_empty_string "$name_type" "$name_value"; then
232
+ pc_error_add "$ctx name must be a non-empty string."
233
+ fi
234
+ fi
235
+ pm_validate_component_members "${comp_path}.members" "$ctx" "$declared"
236
+ done
237
+ }
238
+
131
239
  pm_validate_text() {
132
240
  # Validate a manifest document against invariants M1 to M7.
133
241
  #
@@ -139,6 +247,7 @@ pm_validate_text() {
139
247
  local text="$1" parse_status=0
140
248
  pc_errors_reset
141
249
  PM_SUBSET_DETAIL=""
250
+ PM_CLAIMED=""
142
251
  pm_parse_manifest "$text" || parse_status=$?
143
252
  # An out-of-subset refusal propagates so the caller can distinguish it from
144
253
  # a validation verdict; an M1 failure returns the single-element error list
@@ -156,6 +265,9 @@ pm_validate_text() {
156
265
  # The manifest carries `kind` on every item, unlike the orchestrator
157
266
  # checkpoint, so the shared item validator is asked to require it (S1).
158
267
  pi_validate_items "items" "$PM_CONTEXT" 1
268
+ # M8 runs last because its membership check resolves against the same items
269
+ # collection the previous call validated.
270
+ pm_validate_expected_components
159
271
  return 0
160
272
  }
161
273