@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.
- package/out/mcp-server.js +1045 -214
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/feature-review.md +5 -3
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +11 -4
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +5 -2
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +28 -8
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +109 -5
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +390 -0
- package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +28 -8
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +117 -46
- package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +115 -3
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +312 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +58 -67
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +383 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +297 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +232 -43
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +416 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +366 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +408 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +428 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +377 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +166 -0
- package/resources/claude-customizations/.claude/rules/general-unit-test.md +1 -1
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +88 -3
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
- package/resources/claude-customizations/.claude/rules/quality-tiers.md +3 -3
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +10 -5
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +116 -41
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +95 -13
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +7 -3
- package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +1 -1
- package/resources/claude-customizations/config/blast-radius.json +9 -3
- package/resources/claude-customizations/pack-manifests/core.json +31 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +3 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- 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
|
-
|
|
32
|
-
|
|
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
|
|
42
|
+
Wrapper around the discovery-artifact validator. Mockable seam.
|
|
41
43
|
.DESCRIPTION
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
$
|
|
54
|
-
|
|
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
|
|
156
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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:
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
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
|
|
317
|
-
# optional RoutingInvoker seam lets tests inject a mock; the
|
|
318
|
-
#
|
|
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
|