@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0
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 +542 -42
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- 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/hook-payload/HookPayload.psm1 +494 -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/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- 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/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-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +16 -2
- package/resources/claude-customizations/pack-manifests/core.json +22 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- 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 +31 -1
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
spec.md, or user-story.md are not yet present in that same folder.
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
|
-
Invoked by the Claude Code PreToolUse hook on Write or Edit operations.
|
|
8
|
-
|
|
7
|
+
Invoked by the Claude Code PreToolUse hook on Write or Edit operations. Acquires
|
|
8
|
+
the hook payload through the shared reader and reads file_path from the envelope's
|
|
9
|
+
nested tool_input. When the
|
|
9
10
|
target file_path matches a feature-folder plan.md path under
|
|
10
11
|
docs/features/(active|archive)/<folder>/plan.md, the script verifies that
|
|
11
12
|
each of issue.md, spec.md, and user-story.md exists in the same folder.
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
[CmdletBinding()]
|
|
25
26
|
param()
|
|
26
27
|
|
|
28
|
+
|
|
29
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
27
30
|
function Get-FeatureFolderFileExistence {
|
|
28
31
|
<#
|
|
29
32
|
.SYNOPSIS
|
|
@@ -87,28 +90,34 @@ function Test-IsFeaturePlanPath {
|
|
|
87
90
|
function Invoke-FeatureFolderOrderDecision {
|
|
88
91
|
<#
|
|
89
92
|
.SYNOPSIS
|
|
90
|
-
Parses
|
|
93
|
+
Parses the PreToolUse envelope and produces an allow-or-block decision.
|
|
91
94
|
.PARAMETER ToolInputRaw
|
|
92
|
-
|
|
95
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload. An envelope
|
|
96
|
+
anomaly fails closed as a deny; a well-formed tool_input carrying no file_path
|
|
97
|
+
remains an allow.
|
|
93
98
|
#>
|
|
94
99
|
[CmdletBinding()]
|
|
95
100
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
96
101
|
param(
|
|
102
|
+
[AllowNull()]
|
|
103
|
+
[AllowEmptyString()]
|
|
97
104
|
[string] $ToolInputRaw
|
|
98
105
|
)
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
108
|
+
if (-not $payload.IsValid) {
|
|
109
|
+
return [ordered]@{
|
|
110
|
+
hookSpecificOutput = [ordered]@{
|
|
111
|
+
hookEventName = 'PreToolUse'
|
|
112
|
+
permissionDecision = 'deny'
|
|
113
|
+
permissionDecisionReason = 'FEATURE_FOLDER_ORDER_BLOCKED: payload anomaly - ' +
|
|
114
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
115
|
+
'. The gate fails closed on an envelope it cannot read.'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
$filePath = $
|
|
120
|
+
$filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
|
|
112
121
|
if (-not $filePath) {
|
|
113
122
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
114
123
|
}
|
|
@@ -134,19 +143,55 @@ function Invoke-FeatureFolderOrderDecision {
|
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
function Invoke-FeatureFolderOrderEntryPoint {
|
|
147
|
+
<#
|
|
148
|
+
.SYNOPSIS
|
|
149
|
+
Runs the hook decision and returns the process exit code.
|
|
150
|
+
.DESCRIPTION
|
|
151
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
152
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
153
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
154
|
+
decision by the time control reaches here. The function does not call exit;
|
|
155
|
+
the thin tail converts the returned code into a process exit.
|
|
156
|
+
.PARAMETER ToolInputRaw
|
|
157
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
158
|
+
.PARAMETER ReadPayload
|
|
159
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
160
|
+
case without touching a console.
|
|
161
|
+
.OUTPUTS
|
|
162
|
+
System.Int32
|
|
163
|
+
#>
|
|
164
|
+
[CmdletBinding()]
|
|
165
|
+
[OutputType([int])]
|
|
166
|
+
param(
|
|
167
|
+
[AllowNull()]
|
|
168
|
+
[AllowEmptyString()]
|
|
169
|
+
[string] $ToolInputRaw,
|
|
170
|
+
|
|
171
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
175
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
$decision = Invoke-FeatureFolderOrderDecision -ToolInputRaw $ToolInputRaw
|
|
179
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
180
|
+
|
|
181
|
+
return 0
|
|
182
|
+
}
|
|
183
|
+
|
|
137
184
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
138
185
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
139
186
|
return
|
|
140
187
|
}
|
|
141
188
|
|
|
142
|
-
|
|
143
|
-
|
|
189
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
190
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
191
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
192
|
+
$entryPointResult = @(Invoke-FeatureFolderOrderEntryPoint)
|
|
193
|
+
if ($entryPointResult.Count -gt 1) {
|
|
194
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
144
195
|
}
|
|
145
|
-
catch {
|
|
146
|
-
Write-Error $_
|
|
147
|
-
exit 1
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
151
196
|
|
|
152
|
-
exit
|
|
197
|
+
exit ([int]$entryPointResult[-1])
|
|
@@ -0,0 +1,402 @@
|
|
|
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 acquires the hook payload through the shared reader and reads the
|
|
8
|
+
envelope's nested tool_input ('file_path' plus 'content' for Write, or
|
|
9
|
+
'old_string'/'new_string' for Edit), then 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
|
+
- missing 'file_path' inside a well-formed tool_input, or a path outside the
|
|
25
|
+
'.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
|
+
ENVELOPE VERSUS CONTENT (revised by issue #501). The hook now distinguishes two
|
|
36
|
+
kinds of unreadability that the original note conflated. An ENVELOPE-level anomaly
|
|
37
|
+
-- empty payload on every transport, unparseable envelope JSON, or a payload with
|
|
38
|
+
no usable tool_input -- means the PreToolUse contract itself drifted, which is the
|
|
39
|
+
exact defect #501 fixed and which must fail loudly rather than silently allow; the
|
|
40
|
+
hook therefore denies. A CONTENT-level judgement remains permissive, and that half
|
|
41
|
+
of the original note stands unchanged and must not be "fixed" into a hard failure:
|
|
42
|
+
an Edit fragment, a Markdown file with no fence, an absent validation module, and a
|
|
43
|
+
diagram the classifier cannot classify all still allow, because blocking a valid
|
|
44
|
+
diagram is worse than missing an invalid one. The prior blanket allow-on-unparseable
|
|
45
|
+
made the gate inert against the payload the harness actually sends.
|
|
46
|
+
|
|
47
|
+
The extension scope check runs before any content scan, so a write outside the
|
|
48
|
+
Mermaid scope pays only the JSON parse.
|
|
49
|
+
|
|
50
|
+
.NOTES
|
|
51
|
+
Compatible with PowerShell 7+.
|
|
52
|
+
This script must not modify any state; it is a read-only validation gate.
|
|
53
|
+
It emits compact hookSpecificOutput JSON on stdout and exits 0 in every case,
|
|
54
|
+
never a non-zero exit and never the {"decision":"block"} shape.
|
|
55
|
+
It invokes no Python and starts no subprocess.
|
|
56
|
+
#>
|
|
57
|
+
[CmdletBinding()]
|
|
58
|
+
param()
|
|
59
|
+
|
|
60
|
+
Set-StrictMode -Version Latest
|
|
61
|
+
|
|
62
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
63
|
+
|
|
64
|
+
$script:MermaidModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../lib/mermaid/MermaidValidation.psm1'
|
|
65
|
+
$script:MermaidSkillPointer = 'See .claude/skills/mermaid-diagram/SKILL.md.'
|
|
66
|
+
$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.'
|
|
67
|
+
|
|
68
|
+
function Import-MermaidValidationModule {
|
|
69
|
+
<#
|
|
70
|
+
.SYNOPSIS
|
|
71
|
+
Imports the validation module, returning $false when it is absent.
|
|
72
|
+
.DESCRIPTION
|
|
73
|
+
A consumer repository that receives this hook without the library must not be
|
|
74
|
+
bricked, so a missing module fails open rather than throwing.
|
|
75
|
+
#>
|
|
76
|
+
[CmdletBinding()]
|
|
77
|
+
[OutputType([bool])]
|
|
78
|
+
param()
|
|
79
|
+
|
|
80
|
+
if (-not (Test-Path -LiteralPath $script:MermaidModulePath -PathType Leaf)) { return $false }
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
Import-Module -Name $script:MermaidModulePath -Force -ErrorAction Stop
|
|
84
|
+
} catch {
|
|
85
|
+
return $false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return $true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Get-MermaidOnDiskContent {
|
|
92
|
+
<#
|
|
93
|
+
.SYNOPSIS
|
|
94
|
+
Reads the current on-disk content of a target file, or $null when unavailable.
|
|
95
|
+
.DESCRIPTION
|
|
96
|
+
The named wrapper seam for the managed-diagram gate. Pester mocks this function
|
|
97
|
+
rather than the filesystem, so no test needs a temporary file.
|
|
98
|
+
#>
|
|
99
|
+
[CmdletBinding()]
|
|
100
|
+
[OutputType([string])]
|
|
101
|
+
param(
|
|
102
|
+
[Parameter(Mandatory)]
|
|
103
|
+
[AllowEmptyString()]
|
|
104
|
+
[string] $Path
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
|
|
108
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
return [string](Get-Content -LiteralPath $Path -Raw -ErrorAction Stop)
|
|
112
|
+
} catch {
|
|
113
|
+
return $null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function Get-MermaidToolInputField {
|
|
118
|
+
<#
|
|
119
|
+
.SYNOPSIS
|
|
120
|
+
Reads one field from the parsed tool input, or $null when it is absent.
|
|
121
|
+
#>
|
|
122
|
+
[CmdletBinding()]
|
|
123
|
+
param(
|
|
124
|
+
[AllowNull()]
|
|
125
|
+
$InputObject,
|
|
126
|
+
|
|
127
|
+
[Parameter(Mandatory)]
|
|
128
|
+
[string] $Name
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if ($null -eq $InputObject) { return $null }
|
|
132
|
+
|
|
133
|
+
$property = $InputObject.PSObject.Properties[$Name]
|
|
134
|
+
if ($null -eq $property) { return $null }
|
|
135
|
+
|
|
136
|
+
return $property.Value
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Test-MermaidDiagramFilePath {
|
|
140
|
+
<#
|
|
141
|
+
.SYNOPSIS
|
|
142
|
+
Returns $true when the path names a standalone Mermaid diagram file.
|
|
143
|
+
#>
|
|
144
|
+
[CmdletBinding()]
|
|
145
|
+
[OutputType([bool])]
|
|
146
|
+
param(
|
|
147
|
+
[Parameter(Mandatory)]
|
|
148
|
+
[AllowEmptyString()]
|
|
149
|
+
[string] $FilePath
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
$normalized = $FilePath -replace '\\', '/'
|
|
153
|
+
return [bool]($normalized -imatch '\.(mmd|mermaid)$')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function Test-MermaidMarkdownFilePath {
|
|
157
|
+
<#
|
|
158
|
+
.SYNOPSIS
|
|
159
|
+
Returns $true when the path names a Markdown file that may carry a fence.
|
|
160
|
+
#>
|
|
161
|
+
[CmdletBinding()]
|
|
162
|
+
[OutputType([bool])]
|
|
163
|
+
param(
|
|
164
|
+
[Parameter(Mandatory)]
|
|
165
|
+
[AllowEmptyString()]
|
|
166
|
+
[string] $FilePath
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
$normalized = $FilePath -replace '\\', '/'
|
|
170
|
+
return [bool]($normalized -imatch '\.(md|markdown)$')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function Get-MermaidAllowDecision {
|
|
174
|
+
<#
|
|
175
|
+
.SYNOPSIS
|
|
176
|
+
Builds the explicit-allow decision.
|
|
177
|
+
#>
|
|
178
|
+
[CmdletBinding()]
|
|
179
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
180
|
+
param()
|
|
181
|
+
|
|
182
|
+
return [ordered]@{
|
|
183
|
+
hookSpecificOutput = [ordered]@{
|
|
184
|
+
hookEventName = 'PreToolUse'
|
|
185
|
+
permissionDecision = 'allow'
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function Get-MermaidDenyDecision {
|
|
191
|
+
<#
|
|
192
|
+
.SYNOPSIS
|
|
193
|
+
Builds a deny decision carrying the supplied token-prefixed reason.
|
|
194
|
+
#>
|
|
195
|
+
[CmdletBinding()]
|
|
196
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
197
|
+
param(
|
|
198
|
+
[Parameter(Mandatory)]
|
|
199
|
+
[string] $Reason
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return [ordered]@{
|
|
203
|
+
hookSpecificOutput = [ordered]@{
|
|
204
|
+
hookEventName = 'PreToolUse'
|
|
205
|
+
permissionDecision = 'deny'
|
|
206
|
+
permissionDecisionReason = $Reason
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function Get-MermaidValidationBlockedReason {
|
|
212
|
+
<#
|
|
213
|
+
.SYNOPSIS
|
|
214
|
+
Formats the syntax-deny reason from a structured validation result.
|
|
215
|
+
.DESCRIPTION
|
|
216
|
+
The reason names the defect class, the line number, and the corrective pointer,
|
|
217
|
+
because a deny a reader cannot act on is indistinguishable from a broken gate.
|
|
218
|
+
#>
|
|
219
|
+
[CmdletBinding()]
|
|
220
|
+
[OutputType([string])]
|
|
221
|
+
param(
|
|
222
|
+
[Parameter(Mandatory)]
|
|
223
|
+
[string] $FilePath,
|
|
224
|
+
|
|
225
|
+
[Parameter(Mandatory)]
|
|
226
|
+
$Result,
|
|
227
|
+
|
|
228
|
+
[AllowEmptyString()]
|
|
229
|
+
[string] $Location = ''
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
$finding = @($Result.Findings)[0]
|
|
233
|
+
$declared = if ([string]::IsNullOrWhiteSpace([string]$Result.DiagramType)) { 'no diagram type' } else { "'$($Result.DiagramType)'" }
|
|
234
|
+
$where = if ([string]::IsNullOrWhiteSpace($Location)) { '' } else { " ($Location)" }
|
|
235
|
+
|
|
236
|
+
return "MERMAID_VALIDATION_BLOCKED: '$FilePath'$where declares $declared and has a Mermaid syntax defect: $($finding.Class) at line $($finding.Line): $($finding.Message). $script:MermaidSkillPointer"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function Get-MermaidManagedDiagramBlockedReason {
|
|
240
|
+
<#
|
|
241
|
+
.SYNOPSIS
|
|
242
|
+
Formats the managed-diagram deny reason.
|
|
243
|
+
#>
|
|
244
|
+
[CmdletBinding()]
|
|
245
|
+
[OutputType([string])]
|
|
246
|
+
param(
|
|
247
|
+
[Parameter(Mandatory)]
|
|
248
|
+
[string] $FilePath
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
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"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function Get-MermaidMarkdownBlockDecision {
|
|
255
|
+
<#
|
|
256
|
+
.SYNOPSIS
|
|
257
|
+
Validates every eligible fenced block of a Markdown payload.
|
|
258
|
+
.DESCRIPTION
|
|
259
|
+
Returns a deny decision for the first block carrying a defect, or $null when
|
|
260
|
+
every block is either accepted or skipped. A nested block is documentation
|
|
261
|
+
showing example Mermaid; an opted-out block carries the documented marker.
|
|
262
|
+
#>
|
|
263
|
+
[CmdletBinding()]
|
|
264
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
265
|
+
param(
|
|
266
|
+
[Parameter(Mandatory)]
|
|
267
|
+
[string] $FilePath,
|
|
268
|
+
|
|
269
|
+
[Parameter(Mandatory)]
|
|
270
|
+
[AllowEmptyString()]
|
|
271
|
+
[string] $Content
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
foreach ($block in @(Get-MermaidFenceBlock -Content $Content)) {
|
|
275
|
+
if ($block.IsNested -or $block.IsOptedOut) { continue }
|
|
276
|
+
|
|
277
|
+
$result = Test-MermaidDiagram -Content $block.Content -LineOffset ($block.BodyStartLine - 1)
|
|
278
|
+
if ($result.Verdict -ne 'Invalid') { continue }
|
|
279
|
+
|
|
280
|
+
$location = "the mermaid fence opening at line $($block.StartLine)"
|
|
281
|
+
return Get-MermaidDenyDecision -Reason (Get-MermaidValidationBlockedReason -FilePath $FilePath -Result $result -Location $location)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return $null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function Invoke-MermaidValidationDecision {
|
|
288
|
+
<#
|
|
289
|
+
.SYNOPSIS
|
|
290
|
+
Parses the Claude Code tool-input JSON and returns the allow-or-deny decision.
|
|
291
|
+
.DESCRIPTION
|
|
292
|
+
The pure decision function, separated from the thin entry point so Pester
|
|
293
|
+
exercises the logic directly. Returns $null when the call is none of this hook's
|
|
294
|
+
business (out of scope, unparseable, or a Markdown file with no fence), which the
|
|
295
|
+
entry point treats as a silent allow.
|
|
296
|
+
.PARAMETER ToolInputRaw
|
|
297
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
298
|
+
#>
|
|
299
|
+
[CmdletBinding()]
|
|
300
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
301
|
+
param(
|
|
302
|
+
[AllowEmptyString()]
|
|
303
|
+
[AllowNull()]
|
|
304
|
+
[string] $ToolInputRaw
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Envelope-level anomaly: fail closed. See the ENVELOPE VERSUS CONTENT note in
|
|
308
|
+
# this file's header before changing this back to a silent allow.
|
|
309
|
+
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
310
|
+
if (-not $envelope.IsValid) {
|
|
311
|
+
return Get-MermaidDenyDecision -Reason (
|
|
312
|
+
'MERMAID_VALIDATION_BLOCKED: payload anomaly - ' +
|
|
313
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
|
|
314
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
$toolInput = $envelope.Value
|
|
318
|
+
|
|
319
|
+
$filePath = [string](Get-MermaidToolInputField -InputObject $toolInput -Name 'file_path')
|
|
320
|
+
if ([string]::IsNullOrWhiteSpace($filePath)) { return $null }
|
|
321
|
+
|
|
322
|
+
# Scope check first: a write outside the Mermaid scope pays only the JSON parse.
|
|
323
|
+
$isDiagramFile = Test-MermaidDiagramFilePath -FilePath $filePath
|
|
324
|
+
$isMarkdownFile = Test-MermaidMarkdownFilePath -FilePath $filePath
|
|
325
|
+
if (-not ($isDiagramFile -or $isMarkdownFile)) { return $null }
|
|
326
|
+
|
|
327
|
+
if (-not (Import-MermaidValidationModule)) { return $null }
|
|
328
|
+
|
|
329
|
+
# Managed-diagram gate: a property of the target file, so Edit is covered without
|
|
330
|
+
# reconstructing the post-edit content, and the opt-out marker cannot suppress it.
|
|
331
|
+
if ($isDiagramFile) {
|
|
332
|
+
$onDisk = Get-MermaidOnDiskContent -Path $filePath
|
|
333
|
+
if (-not [string]::IsNullOrWhiteSpace([string]$onDisk) -and (Test-MermaidManagedDiagram -Content ([string]$onDisk))) {
|
|
334
|
+
return Get-MermaidDenyDecision -Reason (Get-MermaidManagedDiagramBlockedReason -FilePath $filePath)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
$content = Get-MermaidToolInputField -InputObject $toolInput -Name 'content'
|
|
339
|
+
if ($null -eq $content) {
|
|
340
|
+
# Edit payload: old_string/new_string is a fragment, not the resulting file, so
|
|
341
|
+
# the syntax check cannot run. The next Write catches a regression.
|
|
342
|
+
return Get-MermaidAllowDecision
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if ($isDiagramFile) {
|
|
346
|
+
$result = Test-MermaidDiagram -Content ([string]$content)
|
|
347
|
+
if ($result.Verdict -eq 'Invalid') {
|
|
348
|
+
return Get-MermaidDenyDecision -Reason (Get-MermaidValidationBlockedReason -FilePath $filePath -Result $result)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return Get-MermaidAllowDecision
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
$blocks = @(Get-MermaidFenceBlock -Content ([string]$content))
|
|
355
|
+
if ($blocks.Count -eq 0) { return $null }
|
|
356
|
+
|
|
357
|
+
$decision = Get-MermaidMarkdownBlockDecision -FilePath $filePath -Content ([string]$content)
|
|
358
|
+
if ($null -ne $decision) { return $decision }
|
|
359
|
+
|
|
360
|
+
return Get-MermaidAllowDecision
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function Invoke-MermaidValidationEntryPoint {
|
|
364
|
+
<#
|
|
365
|
+
.SYNOPSIS
|
|
366
|
+
Thin entry point: writes the decision JSON to stdout and nothing else.
|
|
367
|
+
.DESCRIPTION
|
|
368
|
+
The function emits only the JSON, and the caller exits 0 unconditionally. An
|
|
369
|
+
entry point that also returned a status code would place that code in the same
|
|
370
|
+
output stream as the JSON, so the caller would consume the JSON instead of
|
|
371
|
+
printing it and the decision would never reach Claude Code.
|
|
372
|
+
.PARAMETER ToolInputRaw
|
|
373
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
374
|
+
#>
|
|
375
|
+
[CmdletBinding()]
|
|
376
|
+
[OutputType([string])]
|
|
377
|
+
param(
|
|
378
|
+
[AllowEmptyString()]
|
|
379
|
+
[AllowNull()]
|
|
380
|
+
[string] $ToolInputRaw
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
384
|
+
$ToolInputRaw = Read-ClaudeHookRawPayload
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
$decision = Invoke-MermaidValidationDecision -ToolInputRaw $ToolInputRaw
|
|
388
|
+
if ($null -eq $decision) { return }
|
|
389
|
+
|
|
390
|
+
$decision | ConvertTo-Json -Compress -Depth 5
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
394
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Invoke-MermaidValidationEntryPoint -ToolInputRaw (Read-ClaudeHookRawPayload)
|
|
399
|
+
|
|
400
|
+
# Exit 0 on allow and on deny alike: the decision travels in the JSON, never in the
|
|
401
|
+
# exit code.
|
|
402
|
+
exit 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
7
|
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
-
tool input JSON from the
|
|
8
|
+
tool input JSON from the the envelope's nested tool_input environment variable and the
|
|
9
9
|
orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
|
|
10
10
|
|
|
11
11
|
The hook enforces presence only: it cannot read the delegate's chosen
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
[CmdletBinding()]
|
|
33
33
|
param()
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
35
37
|
function Get-ModelRoutingCheckpoint {
|
|
36
38
|
<#
|
|
37
39
|
.SYNOPSIS
|
|
@@ -118,7 +120,7 @@ function Test-ModelRoutingReceiptPresent {
|
|
|
118
120
|
function Invoke-ModelRoutingReceiptDecision {
|
|
119
121
|
<#
|
|
120
122
|
.SYNOPSIS
|
|
121
|
-
Parses
|
|
123
|
+
Parses the envelope's nested tool_input and returns an allow-or-block decision object.
|
|
122
124
|
#>
|
|
123
125
|
[CmdletBinding()]
|
|
124
126
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -128,21 +130,23 @@ function Invoke-ModelRoutingReceiptDecision {
|
|
|
128
130
|
|
|
129
131
|
$allow = [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
130
132
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
# Envelope-level anomalies fail closed (issue #501): an unreadable envelope means
|
|
134
|
+
# the PreToolUse contract drifted, which is exactly the condition that made every
|
|
135
|
+
# gate in this runtime inert, so it must deny rather than pass through.
|
|
136
|
+
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
137
|
+
if (-not $envelope.IsValid) {
|
|
138
|
+
return [ordered]@{
|
|
139
|
+
hookSpecificOutput = [ordered]@{
|
|
140
|
+
hookEventName = 'PreToolUse'
|
|
141
|
+
permissionDecision = 'deny'
|
|
142
|
+
permissionDecisionReason = 'MODEL_ROUTING_RECEIPT_BLOCKED: payload anomaly - ' +
|
|
143
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
|
|
144
|
+
'. The gate fails closed on an envelope it cannot read.'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
$subagent =
|
|
149
|
+
$subagent = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'subagent_type'
|
|
146
150
|
|
|
147
151
|
# Only the gated Agent-tool delegates are receipt-checked; any other
|
|
148
152
|
# subagent_type (including orchestrator) passes through.
|
|
@@ -169,13 +173,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
169
173
|
return
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
$decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
Write-Error $_
|
|
177
|
-
exit 1
|
|
178
|
-
}
|
|
176
|
+
$decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
|
|
179
177
|
|
|
180
178
|
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
181
179
|
|