@danmoisan/drm-copilot-mcp 1.0.24 → 1.0.27

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 (68) hide show
  1. package/out/mcp-server.js +1045 -214
  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/enforce-epic-merge-gate.ps1 +109 -5
  8. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +390 -0
  9. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +28 -8
  10. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +117 -46
  11. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +115 -3
  12. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
  13. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
  14. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
  15. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
  17. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +312 -0
  18. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +392 -0
  19. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +500 -0
  20. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
  21. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
  22. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
  23. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
  24. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +58 -67
  25. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +383 -0
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +297 -0
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +298 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +232 -43
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +416 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +366 -0
  31. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +408 -0
  32. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +428 -0
  33. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +377 -0
  34. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +166 -0
  35. package/resources/claude-customizations/.claude/rules/general-unit-test.md +1 -1
  36. package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
  37. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +88 -3
  38. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
  39. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  40. package/resources/claude-customizations/.claude/rules/quality-tiers.md +3 -3
  41. package/resources/claude-customizations/.claude/settings.json +5 -0
  42. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
  43. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  44. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
  45. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -4
  46. package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
  47. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
  48. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
  49. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
  50. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
  51. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
  52. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
  53. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
  54. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
  55. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
  56. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +10 -5
  57. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +116 -41
  58. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +95 -13
  59. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +7 -3
  60. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +1 -1
  61. package/resources/claude-customizations/config/blast-radius.json +9 -3
  62. package/resources/claude-customizations/pack-manifests/core.json +31 -1
  63. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  64. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +1 -1
  65. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +3 -3
  66. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  67. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  68. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +37 -1
@@ -0,0 +1,390 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pre-tool-use hook for Claude Code that blocks Mermaid diagrams with named structural defects.
4
+
5
+ .DESCRIPTION
6
+ This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
+ operation. It reads the tool input from the CLAUDE_TOOL_INPUT environment variable
8
+ (JSON with 'file_path' plus 'content' for Write, or 'old_string'/'new_string' for
9
+ Edit) and applies two independent gates:
10
+
11
+ 1. Syntax gate. On a Write of a '.mmd'/'.mermaid' file, the whole file is one
12
+ diagram. On a Write of a Markdown file, every fenced ```mermaid block is a
13
+ diagram. Each diagram is validated by .claude/lib/mermaid/MermaidValidation.psm1
14
+ and a defect of a checked class produces a deny naming the class and the line.
15
+ 2. Managed-diagram gate. A '.mmd'/'.mermaid' file whose ON-DISK frontmatter carries
16
+ 'id:' is connected to the Mermaid Chart sync workflow and must not be hand
17
+ edited. This is a property of the target file rather than of the payload, so it
18
+ applies to Edit as well as Write, and the opt-out marker never suppresses it.
19
+
20
+ The gate's contract is "rejects the named defect classes", never "proves validity".
21
+ Blocking a valid diagram is worse than missing an invalid one, so the hook declines
22
+ to judge rather than rejecting whenever it cannot classify content confidently:
23
+
24
+ - empty, absent, or unparseable CLAUDE_TOOL_INPUT: allow;
25
+ - missing 'file_path', or a path outside the '.mmd'/'.mermaid'/Markdown scope: allow;
26
+ - the validation module absent from disk: allow;
27
+ - an Edit payload (the syntax check needs the whole file, which an
28
+ old_string/new_string fragment cannot supply): allow;
29
+ - a Markdown file carrying no ```mermaid fence: allow;
30
+ - a ```mermaid fence nested inside another open fence, which is documentation
31
+ showing example Mermaid rather than a diagram: skip that block;
32
+ - a fence immediately preceded by '<!-- mermaid-validator: ignore -->': skip that
33
+ block, and only that block.
34
+
35
+ DELIBERATE DIVERGENCE FROM enforce-evidence-locations.ps1: that hook throws on
36
+ malformed CLAUDE_TOOL_INPUT JSON and its entry point exits 1. This hook allows
37
+ instead. The difference is intentional and must not be "fixed" into a hard failure:
38
+ a content gate that hard-fails on input it cannot parse converts an unparseable
39
+ payload into a blocked write, which is the false-positive failure mode this feature
40
+ exists to avoid. The evidence-location hook gates a path, which is always parseable
41
+ when present; this hook gates content, which is not.
42
+
43
+ The extension scope check runs before any content scan, so a write outside the
44
+ Mermaid scope pays only the JSON parse.
45
+
46
+ .NOTES
47
+ Compatible with PowerShell 7+.
48
+ This script must not modify any state; it is a read-only validation gate.
49
+ It emits compact hookSpecificOutput JSON on stdout and exits 0 in every case,
50
+ never a non-zero exit and never the {"decision":"block"} shape.
51
+ It invokes no Python and starts no subprocess.
52
+ #>
53
+ [CmdletBinding()]
54
+ param()
55
+
56
+ Set-StrictMode -Version Latest
57
+
58
+ $script:MermaidModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../lib/mermaid/MermaidValidation.psm1'
59
+ $script:MermaidSkillPointer = 'See .claude/skills/mermaid-diagram/SKILL.md.'
60
+ $script:MermaidSyncPointer = 'Change it through the Mermaid Chart sync workflow in VS Code (Mermaid Chart extension: Sync Diagram with Mermaid, then Review Mermaid Sync) and pull the synced result instead of hand-editing. See .claude/rules/mermaid.md.'
61
+
62
+ function Import-MermaidValidationModule {
63
+ <#
64
+ .SYNOPSIS
65
+ Imports the validation module, returning $false when it is absent.
66
+ .DESCRIPTION
67
+ A consumer repository that receives this hook without the library must not be
68
+ bricked, so a missing module fails open rather than throwing.
69
+ #>
70
+ [CmdletBinding()]
71
+ [OutputType([bool])]
72
+ param()
73
+
74
+ if (-not (Test-Path -LiteralPath $script:MermaidModulePath -PathType Leaf)) { return $false }
75
+
76
+ try {
77
+ Import-Module -Name $script:MermaidModulePath -Force -ErrorAction Stop
78
+ } catch {
79
+ return $false
80
+ }
81
+
82
+ return $true
83
+ }
84
+
85
+ function Get-MermaidOnDiskContent {
86
+ <#
87
+ .SYNOPSIS
88
+ Reads the current on-disk content of a target file, or $null when unavailable.
89
+ .DESCRIPTION
90
+ The named wrapper seam for the managed-diagram gate. Pester mocks this function
91
+ rather than the filesystem, so no test needs a temporary file.
92
+ #>
93
+ [CmdletBinding()]
94
+ [OutputType([string])]
95
+ param(
96
+ [Parameter(Mandatory)]
97
+ [AllowEmptyString()]
98
+ [string] $Path
99
+ )
100
+
101
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
102
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
103
+
104
+ try {
105
+ return [string](Get-Content -LiteralPath $Path -Raw -ErrorAction Stop)
106
+ } catch {
107
+ return $null
108
+ }
109
+ }
110
+
111
+ function Get-MermaidToolInputField {
112
+ <#
113
+ .SYNOPSIS
114
+ Reads one field from the parsed tool input, or $null when it is absent.
115
+ #>
116
+ [CmdletBinding()]
117
+ param(
118
+ [AllowNull()]
119
+ $InputObject,
120
+
121
+ [Parameter(Mandatory)]
122
+ [string] $Name
123
+ )
124
+
125
+ if ($null -eq $InputObject) { return $null }
126
+
127
+ $property = $InputObject.PSObject.Properties[$Name]
128
+ if ($null -eq $property) { return $null }
129
+
130
+ return $property.Value
131
+ }
132
+
133
+ function Test-MermaidDiagramFilePath {
134
+ <#
135
+ .SYNOPSIS
136
+ Returns $true when the path names a standalone Mermaid diagram file.
137
+ #>
138
+ [CmdletBinding()]
139
+ [OutputType([bool])]
140
+ param(
141
+ [Parameter(Mandatory)]
142
+ [AllowEmptyString()]
143
+ [string] $FilePath
144
+ )
145
+
146
+ $normalized = $FilePath -replace '\\', '/'
147
+ return [bool]($normalized -imatch '\.(mmd|mermaid)$')
148
+ }
149
+
150
+ function Test-MermaidMarkdownFilePath {
151
+ <#
152
+ .SYNOPSIS
153
+ Returns $true when the path names a Markdown file that may carry a fence.
154
+ #>
155
+ [CmdletBinding()]
156
+ [OutputType([bool])]
157
+ param(
158
+ [Parameter(Mandatory)]
159
+ [AllowEmptyString()]
160
+ [string] $FilePath
161
+ )
162
+
163
+ $normalized = $FilePath -replace '\\', '/'
164
+ return [bool]($normalized -imatch '\.(md|markdown)$')
165
+ }
166
+
167
+ function Get-MermaidAllowDecision {
168
+ <#
169
+ .SYNOPSIS
170
+ Builds the explicit-allow decision.
171
+ #>
172
+ [CmdletBinding()]
173
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
174
+ param()
175
+
176
+ return [ordered]@{
177
+ hookSpecificOutput = [ordered]@{
178
+ hookEventName = 'PreToolUse'
179
+ permissionDecision = 'allow'
180
+ }
181
+ }
182
+ }
183
+
184
+ function Get-MermaidDenyDecision {
185
+ <#
186
+ .SYNOPSIS
187
+ Builds a deny decision carrying the supplied token-prefixed reason.
188
+ #>
189
+ [CmdletBinding()]
190
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
191
+ param(
192
+ [Parameter(Mandatory)]
193
+ [string] $Reason
194
+ )
195
+
196
+ return [ordered]@{
197
+ hookSpecificOutput = [ordered]@{
198
+ hookEventName = 'PreToolUse'
199
+ permissionDecision = 'deny'
200
+ permissionDecisionReason = $Reason
201
+ }
202
+ }
203
+ }
204
+
205
+ function Get-MermaidValidationBlockedReason {
206
+ <#
207
+ .SYNOPSIS
208
+ Formats the syntax-deny reason from a structured validation result.
209
+ .DESCRIPTION
210
+ The reason names the defect class, the line number, and the corrective pointer,
211
+ because a deny a reader cannot act on is indistinguishable from a broken gate.
212
+ #>
213
+ [CmdletBinding()]
214
+ [OutputType([string])]
215
+ param(
216
+ [Parameter(Mandatory)]
217
+ [string] $FilePath,
218
+
219
+ [Parameter(Mandatory)]
220
+ $Result,
221
+
222
+ [AllowEmptyString()]
223
+ [string] $Location = ''
224
+ )
225
+
226
+ $finding = @($Result.Findings)[0]
227
+ $declared = if ([string]::IsNullOrWhiteSpace([string]$Result.DiagramType)) { 'no diagram type' } else { "'$($Result.DiagramType)'" }
228
+ $where = if ([string]::IsNullOrWhiteSpace($Location)) { '' } else { " ($Location)" }
229
+
230
+ return "MERMAID_VALIDATION_BLOCKED: '$FilePath'$where declares $declared and has a Mermaid syntax defect: $($finding.Class) at line $($finding.Line): $($finding.Message). $script:MermaidSkillPointer"
231
+ }
232
+
233
+ function Get-MermaidManagedDiagramBlockedReason {
234
+ <#
235
+ .SYNOPSIS
236
+ Formats the managed-diagram deny reason.
237
+ #>
238
+ [CmdletBinding()]
239
+ [OutputType([string])]
240
+ param(
241
+ [Parameter(Mandatory)]
242
+ [string] $FilePath
243
+ )
244
+
245
+ return "MERMAID_MANAGED_DIAGRAM_BLOCKED: '$FilePath' is a Mermaid Chart-managed diagram: its on-disk frontmatter carries an 'id:' marker, so a hand-edit would be overwritten by the next sync. $script:MermaidSyncPointer"
246
+ }
247
+
248
+ function Get-MermaidMarkdownBlockDecision {
249
+ <#
250
+ .SYNOPSIS
251
+ Validates every eligible fenced block of a Markdown payload.
252
+ .DESCRIPTION
253
+ Returns a deny decision for the first block carrying a defect, or $null when
254
+ every block is either accepted or skipped. A nested block is documentation
255
+ showing example Mermaid; an opted-out block carries the documented marker.
256
+ #>
257
+ [CmdletBinding()]
258
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
259
+ param(
260
+ [Parameter(Mandatory)]
261
+ [string] $FilePath,
262
+
263
+ [Parameter(Mandatory)]
264
+ [AllowEmptyString()]
265
+ [string] $Content
266
+ )
267
+
268
+ foreach ($block in @(Get-MermaidFenceBlock -Content $Content)) {
269
+ if ($block.IsNested -or $block.IsOptedOut) { continue }
270
+
271
+ $result = Test-MermaidDiagram -Content $block.Content -LineOffset ($block.BodyStartLine - 1)
272
+ if ($result.Verdict -ne 'Invalid') { continue }
273
+
274
+ $location = "the mermaid fence opening at line $($block.StartLine)"
275
+ return Get-MermaidDenyDecision -Reason (Get-MermaidValidationBlockedReason -FilePath $FilePath -Result $result -Location $location)
276
+ }
277
+
278
+ return $null
279
+ }
280
+
281
+ function Invoke-MermaidValidationDecision {
282
+ <#
283
+ .SYNOPSIS
284
+ Parses the Claude Code tool-input JSON and returns the allow-or-deny decision.
285
+ .DESCRIPTION
286
+ The pure decision function, separated from the thin entry point so Pester
287
+ exercises the logic directly. Returns $null when the call is none of this hook's
288
+ business (out of scope, unparseable, or a Markdown file with no fence), which the
289
+ entry point treats as a silent allow.
290
+ .PARAMETER ToolInputRaw
291
+ The raw JSON string from $env:CLAUDE_TOOL_INPUT.
292
+ #>
293
+ [CmdletBinding()]
294
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
295
+ param(
296
+ [AllowEmptyString()]
297
+ [AllowNull()]
298
+ [string] $ToolInputRaw
299
+ )
300
+
301
+ if ([string]::IsNullOrWhiteSpace($ToolInputRaw)) { return $null }
302
+
303
+ try {
304
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
305
+ } catch {
306
+ # Fail open. See the DELIBERATE DIVERGENCE note in this file's header before
307
+ # changing this to a throw.
308
+ return $null
309
+ }
310
+
311
+ $filePath = [string](Get-MermaidToolInputField -InputObject $toolInput -Name 'file_path')
312
+ if ([string]::IsNullOrWhiteSpace($filePath)) { return $null }
313
+
314
+ # Scope check first: a write outside the Mermaid scope pays only the JSON parse.
315
+ $isDiagramFile = Test-MermaidDiagramFilePath -FilePath $filePath
316
+ $isMarkdownFile = Test-MermaidMarkdownFilePath -FilePath $filePath
317
+ if (-not ($isDiagramFile -or $isMarkdownFile)) { return $null }
318
+
319
+ if (-not (Import-MermaidValidationModule)) { return $null }
320
+
321
+ # Managed-diagram gate: a property of the target file, so Edit is covered without
322
+ # reconstructing the post-edit content, and the opt-out marker cannot suppress it.
323
+ if ($isDiagramFile) {
324
+ $onDisk = Get-MermaidOnDiskContent -Path $filePath
325
+ if (-not [string]::IsNullOrWhiteSpace([string]$onDisk) -and (Test-MermaidManagedDiagram -Content ([string]$onDisk))) {
326
+ return Get-MermaidDenyDecision -Reason (Get-MermaidManagedDiagramBlockedReason -FilePath $filePath)
327
+ }
328
+ }
329
+
330
+ $content = Get-MermaidToolInputField -InputObject $toolInput -Name 'content'
331
+ if ($null -eq $content) {
332
+ # Edit payload: old_string/new_string is a fragment, not the resulting file, so
333
+ # the syntax check cannot run. The next Write catches a regression.
334
+ return Get-MermaidAllowDecision
335
+ }
336
+
337
+ if ($isDiagramFile) {
338
+ $result = Test-MermaidDiagram -Content ([string]$content)
339
+ if ($result.Verdict -eq 'Invalid') {
340
+ return Get-MermaidDenyDecision -Reason (Get-MermaidValidationBlockedReason -FilePath $filePath -Result $result)
341
+ }
342
+
343
+ return Get-MermaidAllowDecision
344
+ }
345
+
346
+ $blocks = @(Get-MermaidFenceBlock -Content ([string]$content))
347
+ if ($blocks.Count -eq 0) { return $null }
348
+
349
+ $decision = Get-MermaidMarkdownBlockDecision -FilePath $filePath -Content ([string]$content)
350
+ if ($null -ne $decision) { return $decision }
351
+
352
+ return Get-MermaidAllowDecision
353
+ }
354
+
355
+ function Invoke-MermaidValidationEntryPoint {
356
+ <#
357
+ .SYNOPSIS
358
+ Thin entry point: writes the decision JSON to stdout and nothing else.
359
+ .DESCRIPTION
360
+ The function emits only the JSON, and the caller exits 0 unconditionally. An
361
+ entry point that also returned a status code would place that code in the same
362
+ output stream as the JSON, so the caller would consume the JSON instead of
363
+ printing it and the decision would never reach Claude Code.
364
+ .PARAMETER ToolInputRaw
365
+ The raw JSON string from $env:CLAUDE_TOOL_INPUT.
366
+ #>
367
+ [CmdletBinding()]
368
+ [OutputType([string])]
369
+ param(
370
+ [AllowEmptyString()]
371
+ [AllowNull()]
372
+ [string] $ToolInputRaw = $env:CLAUDE_TOOL_INPUT
373
+ )
374
+
375
+ $decision = Invoke-MermaidValidationDecision -ToolInputRaw $ToolInputRaw
376
+ if ($null -eq $decision) { return }
377
+
378
+ $decision | ConvertTo-Json -Compress -Depth 5
379
+ }
380
+
381
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
382
+ if ($MyInvocation.InvocationName -eq '.') {
383
+ return
384
+ }
385
+
386
+ Invoke-MermaidValidationEntryPoint -ToolInputRaw $env:CLAUDE_TOOL_INPUT
387
+
388
+ # Exit 0 on allow and on deny alike: the decision travels in the JSON, never in the
389
+ # exit code.
390
+ exit 0
@@ -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