@danmoisan/drm-copilot-mcp 1.0.26 → 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 +542 -41
- package/package.json +1 -1
- 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/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/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 +61 -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 +8 -0
- package/resources/claude-customizations/pack-manifests/core.json +19 -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 +16 -1
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
7
7
|
command runs. Regex-matches gh pr merge with a --merge flag against
|
|
8
|
-
CLAUDE_TOOL_INPUT.command and, when matched, allows the merge only when one of
|
|
8
|
+
CLAUDE_TOOL_INPUT.command and, when matched, allows the merge only when one of three
|
|
9
9
|
checkpoint-only conditions holds:
|
|
10
10
|
|
|
11
11
|
1. Child-feature path: artifacts/orchestration/orchestrator-state.json exists,
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
2. Epic-integration path: artifacts/orchestration/epic-orchestrator-state.json exists,
|
|
15
15
|
epic_merge_pr.ci_gate.conclusion == "success", and, when the command names an
|
|
16
16
|
explicit PR number, that number matches epic_merge_pr.pr_number.
|
|
17
|
+
3. Parallel path: artifacts/orchestration/parallel-orchestrator-state.json exists,
|
|
18
|
+
route_id == "parallel", and the command's explicit PR number matches an items[]
|
|
19
|
+
entry whose merge_status == "ci_green". A parallel run always names an explicit PR
|
|
20
|
+
number (each item merges from its own isolated worktree), so a bare command with no
|
|
21
|
+
PR number cannot satisfy this branch.
|
|
17
22
|
|
|
18
23
|
Otherwise the command is denied with reason EPIC_MERGE_GATE_BLOCKED. A missing or
|
|
19
|
-
unreadable checkpoint in
|
|
20
|
-
orchestration never sets epic_mode
|
|
21
|
-
|
|
24
|
+
unreadable checkpoint in any branch fails closed (denies); standalone (non-epic,
|
|
25
|
+
non-parallel) orchestration never sets epic_mode, populates epic_merge_pr, or writes a
|
|
26
|
+
parallel checkpoint with route_id == "parallel", so it is structurally prevented from
|
|
27
|
+
invoking gh pr merge --merge at all.
|
|
22
28
|
|
|
23
29
|
Design decision: this gate trusts the on-disk checkpoint rather than shelling out live
|
|
24
30
|
to gh pr view for a real-time head-SHA check, matching the same non-adversarial,
|
|
@@ -35,6 +41,7 @@ param()
|
|
|
35
41
|
|
|
36
42
|
$script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
37
43
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
44
|
+
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
38
45
|
|
|
39
46
|
function Get-ChildOrchestratorCheckpointContent {
|
|
40
47
|
<#
|
|
@@ -72,6 +79,24 @@ function Get-EpicOrchestratorCheckpointContent {
|
|
|
72
79
|
return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
function Get-ParallelOrchestratorCheckpointContent {
|
|
83
|
+
<#
|
|
84
|
+
.SYNOPSIS
|
|
85
|
+
Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
|
|
86
|
+
this function (read seam).
|
|
87
|
+
.OUTPUTS
|
|
88
|
+
System.String or $null
|
|
89
|
+
#>
|
|
90
|
+
[CmdletBinding()]
|
|
91
|
+
[OutputType([string])]
|
|
92
|
+
param()
|
|
93
|
+
|
|
94
|
+
if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
|
|
95
|
+
return $null
|
|
96
|
+
}
|
|
97
|
+
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
function ConvertFrom-EpicMergeGateJson {
|
|
76
101
|
<#
|
|
77
102
|
.SYNOPSIS
|
|
@@ -113,9 +138,20 @@ function Get-EpicMergeGateCommandPrNumber {
|
|
|
113
138
|
[string] $CommandText
|
|
114
139
|
)
|
|
115
140
|
|
|
141
|
+
# Original form: the PR number appears immediately after "merge"
|
|
142
|
+
# (e.g. "gh pr merge 410 --merge"). Preserved verbatim so epic-path outcomes
|
|
143
|
+
# for the forms the epic path uses are unchanged.
|
|
116
144
|
if ($CommandText -match '(?i)\bgh\s+pr\s+merge\s+(\d+)\b') {
|
|
117
145
|
return [int]$Matches[1]
|
|
118
146
|
}
|
|
147
|
+
# Broadened, additive form: the parallel command places the flag before the
|
|
148
|
+
# number (e.g. "gh pr merge --merge 410"). Once "gh pr merge" is confirmed,
|
|
149
|
+
# capture the first standalone run of digits that is not preceded by "-" or a
|
|
150
|
+
# word character, so a flag token such as "--merge" is not treated as a number
|
|
151
|
+
# and a bare "gh pr merge --merge" still yields $null.
|
|
152
|
+
if ($CommandText -match '(?i)\bgh\s+pr\s+merge\b' -and $CommandText -match '(?<![-\w])(\d+)\b') {
|
|
153
|
+
return [int]$Matches[1]
|
|
154
|
+
}
|
|
119
155
|
return $null
|
|
120
156
|
}
|
|
121
157
|
|
|
@@ -206,6 +242,69 @@ function Test-EpicCheckpointAllowsMerge {
|
|
|
206
242
|
return $true
|
|
207
243
|
}
|
|
208
244
|
|
|
245
|
+
function Test-ParallelCheckpointAllowsMerge {
|
|
246
|
+
<#
|
|
247
|
+
.SYNOPSIS
|
|
248
|
+
Decision logic for the parallel-orchestrator checkpoint path (branch 3).
|
|
249
|
+
.PARAMETER Checkpoint
|
|
250
|
+
Parsed parallel-orchestrator checkpoint, or $null when absent/unreadable.
|
|
251
|
+
.PARAMETER CommandPrNumber
|
|
252
|
+
The explicit PR number parsed from the command, or $null when the command
|
|
253
|
+
does not name one. A parallel run always names an explicit PR number because
|
|
254
|
+
each item merges from its own isolated worktree, so a $null value denies.
|
|
255
|
+
.OUTPUTS
|
|
256
|
+
System.Boolean
|
|
257
|
+
#>
|
|
258
|
+
[CmdletBinding()]
|
|
259
|
+
[OutputType([bool])]
|
|
260
|
+
param(
|
|
261
|
+
[AllowNull()]
|
|
262
|
+
$Checkpoint,
|
|
263
|
+
|
|
264
|
+
[AllowNull()]
|
|
265
|
+
[Nullable[int]] $CommandPrNumber
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if ($null -eq $Checkpoint) {
|
|
269
|
+
return $false
|
|
270
|
+
}
|
|
271
|
+
$props = @($Checkpoint.PSObject.Properties.Name)
|
|
272
|
+
if ($props -notcontains 'route_id' -or ([string]$Checkpoint.route_id) -ne 'parallel') {
|
|
273
|
+
return $false
|
|
274
|
+
}
|
|
275
|
+
# A parallel merge always names an explicit PR number; without one the target
|
|
276
|
+
# item cannot be identified, so fail closed.
|
|
277
|
+
if ($null -eq $CommandPrNumber) {
|
|
278
|
+
return $false
|
|
279
|
+
}
|
|
280
|
+
if ($props -notcontains 'items' -or $null -eq $Checkpoint.items) {
|
|
281
|
+
return $false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
285
|
+
if ($null -eq $item) {
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
289
|
+
if ($itemProps -notcontains 'pr_number') {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
$itemPrNumber = 0
|
|
293
|
+
if (-not [int]::TryParse([string]$item.pr_number, [ref] $itemPrNumber)) {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
if ($itemPrNumber -ne $CommandPrNumber) {
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
if ($itemProps -notcontains 'merge_status') {
|
|
300
|
+
return $false
|
|
301
|
+
}
|
|
302
|
+
return ([string]$item.merge_status) -eq 'ci_green'
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return $false
|
|
306
|
+
}
|
|
307
|
+
|
|
209
308
|
function Get-EpicMergeGateAllowDecision {
|
|
210
309
|
[CmdletBinding()]
|
|
211
310
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -284,7 +383,12 @@ function Invoke-EpicMergeGateDecision {
|
|
|
284
383
|
return Get-EpicMergeGateAllowDecision
|
|
285
384
|
}
|
|
286
385
|
|
|
287
|
-
|
|
386
|
+
$parallelCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ParallelOrchestratorCheckpointContent)
|
|
387
|
+
if (Test-ParallelCheckpointAllowsMerge -Checkpoint $parallelCheckpoint -CommandPrNumber $commandPrNumber) {
|
|
388
|
+
return Get-EpicMergeGateAllowDecision
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: gh pr merge --merge requires either a per-feature checkpoint with epic_mode == true and step9_status == "passed", an epic checkpoint with epic_merge_pr.ci_gate.conclusion == "success" and a matching pr_number, or a parallel-orchestrator checkpoint with route_id == "parallel" whose target item (matched by pr_number) has merge_status == "ci_green". No checkpoint satisfied this gate.'
|
|
288
392
|
}
|
|
289
393
|
|
|
290
394
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
@@ -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
|