@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
|
@@ -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
|
-
|
|
8
|
+
the envelope's 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,
|
|
@@ -33,8 +39,11 @@
|
|
|
33
39
|
[CmdletBinding()]
|
|
34
40
|
param()
|
|
35
41
|
|
|
42
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
43
|
+
|
|
36
44
|
$script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
37
45
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
46
|
+
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
38
47
|
|
|
39
48
|
function Get-ChildOrchestratorCheckpointContent {
|
|
40
49
|
<#
|
|
@@ -72,6 +81,24 @@ function Get-EpicOrchestratorCheckpointContent {
|
|
|
72
81
|
return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
function Get-ParallelOrchestratorCheckpointContent {
|
|
85
|
+
<#
|
|
86
|
+
.SYNOPSIS
|
|
87
|
+
Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
|
|
88
|
+
this function (read seam).
|
|
89
|
+
.OUTPUTS
|
|
90
|
+
System.String or $null
|
|
91
|
+
#>
|
|
92
|
+
[CmdletBinding()]
|
|
93
|
+
[OutputType([string])]
|
|
94
|
+
param()
|
|
95
|
+
|
|
96
|
+
if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
|
|
97
|
+
return $null
|
|
98
|
+
}
|
|
99
|
+
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
function ConvertFrom-EpicMergeGateJson {
|
|
76
103
|
<#
|
|
77
104
|
.SYNOPSIS
|
|
@@ -113,9 +140,20 @@ function Get-EpicMergeGateCommandPrNumber {
|
|
|
113
140
|
[string] $CommandText
|
|
114
141
|
)
|
|
115
142
|
|
|
143
|
+
# Original form: the PR number appears immediately after "merge"
|
|
144
|
+
# (e.g. "gh pr merge 410 --merge"). Preserved verbatim so epic-path outcomes
|
|
145
|
+
# for the forms the epic path uses are unchanged.
|
|
116
146
|
if ($CommandText -match '(?i)\bgh\s+pr\s+merge\s+(\d+)\b') {
|
|
117
147
|
return [int]$Matches[1]
|
|
118
148
|
}
|
|
149
|
+
# Broadened, additive form: the parallel command places the flag before the
|
|
150
|
+
# number (e.g. "gh pr merge --merge 410"). Once "gh pr merge" is confirmed,
|
|
151
|
+
# capture the first standalone run of digits that is not preceded by "-" or a
|
|
152
|
+
# word character, so a flag token such as "--merge" is not treated as a number
|
|
153
|
+
# and a bare "gh pr merge --merge" still yields $null.
|
|
154
|
+
if ($CommandText -match '(?i)\bgh\s+pr\s+merge\b' -and $CommandText -match '(?<![-\w])(\d+)\b') {
|
|
155
|
+
return [int]$Matches[1]
|
|
156
|
+
}
|
|
119
157
|
return $null
|
|
120
158
|
}
|
|
121
159
|
|
|
@@ -206,6 +244,69 @@ function Test-EpicCheckpointAllowsMerge {
|
|
|
206
244
|
return $true
|
|
207
245
|
}
|
|
208
246
|
|
|
247
|
+
function Test-ParallelCheckpointAllowsMerge {
|
|
248
|
+
<#
|
|
249
|
+
.SYNOPSIS
|
|
250
|
+
Decision logic for the parallel-orchestrator checkpoint path (branch 3).
|
|
251
|
+
.PARAMETER Checkpoint
|
|
252
|
+
Parsed parallel-orchestrator checkpoint, or $null when absent/unreadable.
|
|
253
|
+
.PARAMETER CommandPrNumber
|
|
254
|
+
The explicit PR number parsed from the command, or $null when the command
|
|
255
|
+
does not name one. A parallel run always names an explicit PR number because
|
|
256
|
+
each item merges from its own isolated worktree, so a $null value denies.
|
|
257
|
+
.OUTPUTS
|
|
258
|
+
System.Boolean
|
|
259
|
+
#>
|
|
260
|
+
[CmdletBinding()]
|
|
261
|
+
[OutputType([bool])]
|
|
262
|
+
param(
|
|
263
|
+
[AllowNull()]
|
|
264
|
+
$Checkpoint,
|
|
265
|
+
|
|
266
|
+
[AllowNull()]
|
|
267
|
+
[Nullable[int]] $CommandPrNumber
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if ($null -eq $Checkpoint) {
|
|
271
|
+
return $false
|
|
272
|
+
}
|
|
273
|
+
$props = @($Checkpoint.PSObject.Properties.Name)
|
|
274
|
+
if ($props -notcontains 'route_id' -or ([string]$Checkpoint.route_id) -ne 'parallel') {
|
|
275
|
+
return $false
|
|
276
|
+
}
|
|
277
|
+
# A parallel merge always names an explicit PR number; without one the target
|
|
278
|
+
# item cannot be identified, so fail closed.
|
|
279
|
+
if ($null -eq $CommandPrNumber) {
|
|
280
|
+
return $false
|
|
281
|
+
}
|
|
282
|
+
if ($props -notcontains 'items' -or $null -eq $Checkpoint.items) {
|
|
283
|
+
return $false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
287
|
+
if ($null -eq $item) {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
291
|
+
if ($itemProps -notcontains 'pr_number') {
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
$itemPrNumber = 0
|
|
295
|
+
if (-not [int]::TryParse([string]$item.pr_number, [ref] $itemPrNumber)) {
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
if ($itemPrNumber -ne $CommandPrNumber) {
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
if ($itemProps -notcontains 'merge_status') {
|
|
302
|
+
return $false
|
|
303
|
+
}
|
|
304
|
+
return ([string]$item.merge_status) -eq 'ci_green'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return $false
|
|
308
|
+
}
|
|
309
|
+
|
|
209
310
|
function Get-EpicMergeGateAllowDecision {
|
|
210
311
|
[CmdletBinding()]
|
|
211
312
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -239,29 +340,34 @@ function Get-EpicMergeGateBlockDecision {
|
|
|
239
340
|
function Invoke-EpicMergeGateDecision {
|
|
240
341
|
<#
|
|
241
342
|
.SYNOPSIS
|
|
242
|
-
Parses
|
|
343
|
+
Parses the PreToolUse envelope and returns an allow-or-block decision.
|
|
344
|
+
.DESCRIPTION
|
|
345
|
+
Envelope-level anomalies (empty payload, unparseable JSON, missing or
|
|
346
|
+
malformed tool_input) fail closed as a deny; the legacy flat root shape is
|
|
347
|
+
one such anomaly. Property-level absence of command inside a well-formed
|
|
348
|
+
tool_input remains an allow, because that is this gate's scope filter.
|
|
243
349
|
.PARAMETER ToolInputRaw
|
|
244
|
-
The raw JSON
|
|
350
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
245
351
|
.OUTPUTS
|
|
246
352
|
System.Collections.Specialized.OrderedDictionary
|
|
247
353
|
#>
|
|
248
354
|
[CmdletBinding()]
|
|
249
355
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
250
356
|
param(
|
|
357
|
+
[AllowNull()]
|
|
358
|
+
[AllowEmptyString()]
|
|
251
359
|
[string] $ToolInputRaw
|
|
252
360
|
)
|
|
253
361
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
} catch {
|
|
261
|
-
throw "enforce-epic-merge-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
362
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
363
|
+
if (-not $payload.IsValid) {
|
|
364
|
+
return Get-EpicMergeGateBlockDecision -Reason (
|
|
365
|
+
'EPIC_MERGE_GATE_BLOCKED: payload anomaly - ' +
|
|
366
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
367
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
262
368
|
}
|
|
263
369
|
|
|
264
|
-
$commandText = $
|
|
370
|
+
$commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
|
|
265
371
|
if (-not $commandText) {
|
|
266
372
|
return Get-EpicMergeGateAllowDecision
|
|
267
373
|
}
|
|
@@ -284,7 +390,50 @@ function Invoke-EpicMergeGateDecision {
|
|
|
284
390
|
return Get-EpicMergeGateAllowDecision
|
|
285
391
|
}
|
|
286
392
|
|
|
287
|
-
|
|
393
|
+
$parallelCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ParallelOrchestratorCheckpointContent)
|
|
394
|
+
if (Test-ParallelCheckpointAllowsMerge -Checkpoint $parallelCheckpoint -CommandPrNumber $commandPrNumber) {
|
|
395
|
+
return Get-EpicMergeGateAllowDecision
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
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.'
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function Invoke-EpicMergeGateEntryPoint {
|
|
402
|
+
<#
|
|
403
|
+
.SYNOPSIS
|
|
404
|
+
Runs the merge-gate decision and returns the process exit code.
|
|
405
|
+
.DESCRIPTION
|
|
406
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
407
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
408
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
409
|
+
decision by the time control reaches here. The function does not call exit;
|
|
410
|
+
the thin tail converts the returned code into a process exit.
|
|
411
|
+
.PARAMETER ToolInputRaw
|
|
412
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
413
|
+
.PARAMETER ReadPayload
|
|
414
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
415
|
+
case without touching a console.
|
|
416
|
+
.OUTPUTS
|
|
417
|
+
System.Int32
|
|
418
|
+
#>
|
|
419
|
+
[CmdletBinding()]
|
|
420
|
+
[OutputType([int])]
|
|
421
|
+
param(
|
|
422
|
+
[AllowNull()]
|
|
423
|
+
[AllowEmptyString()]
|
|
424
|
+
[string] $ToolInputRaw,
|
|
425
|
+
|
|
426
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
430
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
$decision = Invoke-EpicMergeGateDecision -ToolInputRaw $ToolInputRaw
|
|
434
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
435
|
+
|
|
436
|
+
return 0
|
|
288
437
|
}
|
|
289
438
|
|
|
290
439
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
@@ -292,13 +441,12 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
292
441
|
return
|
|
293
442
|
}
|
|
294
443
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
444
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
445
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
446
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
447
|
+
$entryPointResult = @(Invoke-EpicMergeGateEntryPoint)
|
|
448
|
+
if ($entryPointResult.Count -gt 1) {
|
|
449
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
300
450
|
}
|
|
301
451
|
|
|
302
|
-
$
|
|
303
|
-
|
|
304
|
-
exit 0
|
|
452
|
+
exit ([int]$entryPointResult[-1])
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Invoked by the Claude Code PreToolUse hook on the "Agent" matcher before any Agent
|
|
7
|
-
(Task) call runs. Activates only when
|
|
7
|
+
(Task) call runs. Activates only when the envelope's nested tool_input.subagent_type == "orchestrator"
|
|
8
8
|
and the serialized prompt contains the epic-mode kickoff marker "Epic mode: true".
|
|
9
9
|
|
|
10
10
|
Resolution and decision procedure:
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
[CmdletBinding()]
|
|
34
34
|
param()
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
36
38
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
37
39
|
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
38
40
|
$script:EpicModeMarker = 'Epic mode: true'
|
|
@@ -232,7 +234,7 @@ function Get-EpicWaveBarrierBlockDecision {
|
|
|
232
234
|
function Invoke-EpicWaveBarrierDecision {
|
|
233
235
|
<#
|
|
234
236
|
.SYNOPSIS
|
|
235
|
-
Parses
|
|
237
|
+
Parses the envelope's nested tool_input and returns an allow-or-block decision.
|
|
236
238
|
.PARAMETER ToolInputRaw
|
|
237
239
|
The raw JSON tool payload supplied by Claude Code.
|
|
238
240
|
.OUTPUTS
|
|
@@ -244,22 +246,20 @@ function Invoke-EpicWaveBarrierDecision {
|
|
|
244
246
|
[string] $ToolInputRaw
|
|
245
247
|
)
|
|
246
248
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
} catch {
|
|
254
|
-
throw "enforce-epic-wave-barrier hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
249
|
+
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
250
|
+
if (-not $envelope.IsValid) {
|
|
251
|
+
return Get-EpicWaveBarrierBlockDecision -Reason (
|
|
252
|
+
'EPIC_WAVE_BARRIER_BLOCKED: payload anomaly - ' +
|
|
253
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
|
|
254
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
$subagent = $
|
|
257
|
+
$subagent = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'subagent_type'
|
|
258
258
|
if (-not $subagent -or $subagent -ne 'orchestrator') {
|
|
259
259
|
return Get-EpicWaveBarrierAllowDecision
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
$prompt =
|
|
262
|
+
$prompt = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'prompt'
|
|
263
263
|
if (-not $prompt -or $prompt -notlike "*$script:EpicModeMarker*") {
|
|
264
264
|
return Get-EpicWaveBarrierAllowDecision
|
|
265
265
|
}
|
|
@@ -287,18 +287,47 @@ function Invoke-EpicWaveBarrierDecision {
|
|
|
287
287
|
return Get-EpicWaveBarrierBlockDecision -Reason "EPIC_WAVE_BARRIER_BLOCKED: '$featureFolder' cannot start until every dependency in its depends_on list is durably confirmed merged or worktree_removed in the epic checkpoint. The checkpoint was unreadable, the feature record was not found, or a dependency is not yet safe."
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
function Invoke-EpicWaveBarrierEntryPoint {
|
|
291
|
+
<#
|
|
292
|
+
.SYNOPSIS
|
|
293
|
+
Runs the hook decision and returns the process exit code.
|
|
294
|
+
.DESCRIPTION
|
|
295
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
296
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
297
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
298
|
+
decision by the time control reaches here. The function does not call exit;
|
|
299
|
+
the thin tail converts the returned code into a process exit.
|
|
300
|
+
.PARAMETER ToolInputRaw
|
|
301
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
302
|
+
.PARAMETER ReadPayload
|
|
303
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
304
|
+
case without touching a console.
|
|
305
|
+
.OUTPUTS
|
|
306
|
+
System.Int32
|
|
307
|
+
#>
|
|
308
|
+
[CmdletBinding()]
|
|
309
|
+
[OutputType([int])]
|
|
310
|
+
param(
|
|
311
|
+
[AllowNull()]
|
|
312
|
+
[AllowEmptyString()]
|
|
313
|
+
[string] $ToolInputRaw,
|
|
314
|
+
|
|
315
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
319
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
$decision = Invoke-EpicWaveBarrierDecision -ToolInputRaw $ToolInputRaw
|
|
323
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
324
|
+
|
|
325
|
+
return 0
|
|
326
|
+
}
|
|
327
|
+
|
|
290
328
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
291
329
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
292
330
|
return
|
|
293
331
|
}
|
|
294
332
|
|
|
295
|
-
|
|
296
|
-
$decision = Invoke-EpicWaveBarrierDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
297
|
-
} catch {
|
|
298
|
-
Write-Error $_
|
|
299
|
-
exit 1
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
303
|
-
|
|
304
|
-
exit 0
|
|
333
|
+
exit (Invoke-EpicWaveBarrierEntryPoint)
|
package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
7
|
-
command runs. Regex-matches git worktree remove against
|
|
7
|
+
command runs. Regex-matches git worktree remove against the envelope's tool_input.command,
|
|
8
8
|
extracts the target worktree path argument, reads
|
|
9
9
|
artifacts/orchestration/epic-orchestrator-state.json, and finds the features[] record
|
|
10
10
|
whose worktree_path matches. Allows removal only when that record's merge_status is
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
[CmdletBinding()]
|
|
22
22
|
param()
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
24
26
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
25
27
|
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
26
28
|
|
|
@@ -169,7 +171,7 @@ function Get-EpicWorktreeGateBlockDecision {
|
|
|
169
171
|
function Invoke-EpicWorktreeRemovalGateDecision {
|
|
170
172
|
<#
|
|
171
173
|
.SYNOPSIS
|
|
172
|
-
Parses
|
|
174
|
+
Parses the PreToolUse envelope and returns an allow-or-block decision.
|
|
173
175
|
.PARAMETER ToolInputRaw
|
|
174
176
|
The raw JSON tool payload supplied by Claude Code.
|
|
175
177
|
.OUTPUTS
|
|
@@ -181,17 +183,15 @@ function Invoke-EpicWorktreeRemovalGateDecision {
|
|
|
181
183
|
[string] $ToolInputRaw
|
|
182
184
|
)
|
|
183
185
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
} catch {
|
|
191
|
-
throw "enforce-epic-worktree-removal-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
186
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
187
|
+
if (-not $payload.IsValid) {
|
|
188
|
+
return Get-EpicWorktreeGateBlockDecision -Reason (
|
|
189
|
+
'EPIC_WORKTREE_REMOVAL_BLOCKED: payload anomaly - ' +
|
|
190
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
191
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
$commandText = $
|
|
194
|
+
$commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
|
|
195
195
|
if (-not $commandText) {
|
|
196
196
|
return Get-EpicWorktreeGateAllowDecision
|
|
197
197
|
}
|
|
@@ -220,18 +220,55 @@ function Invoke-EpicWorktreeRemovalGateDecision {
|
|
|
220
220
|
return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching epic checkpoint features[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal."
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
function Invoke-EpicWorktreeRemovalGateEntryPoint {
|
|
224
|
+
<#
|
|
225
|
+
.SYNOPSIS
|
|
226
|
+
Runs the hook decision and returns the process exit code.
|
|
227
|
+
.DESCRIPTION
|
|
228
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
229
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
230
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
231
|
+
decision by the time control reaches here. The function does not call exit;
|
|
232
|
+
the thin tail converts the returned code into a process exit.
|
|
233
|
+
.PARAMETER ToolInputRaw
|
|
234
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
235
|
+
.PARAMETER ReadPayload
|
|
236
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
237
|
+
case without touching a console.
|
|
238
|
+
.OUTPUTS
|
|
239
|
+
System.Int32
|
|
240
|
+
#>
|
|
241
|
+
[CmdletBinding()]
|
|
242
|
+
[OutputType([int])]
|
|
243
|
+
param(
|
|
244
|
+
[AllowNull()]
|
|
245
|
+
[AllowEmptyString()]
|
|
246
|
+
[string] $ToolInputRaw,
|
|
247
|
+
|
|
248
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
252
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
$decision = Invoke-EpicWorktreeRemovalGateDecision -ToolInputRaw $ToolInputRaw
|
|
256
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
257
|
+
|
|
258
|
+
return 0
|
|
259
|
+
}
|
|
260
|
+
|
|
223
261
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
224
262
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
225
263
|
return
|
|
226
264
|
}
|
|
227
265
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
266
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
267
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
268
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
269
|
+
$entryPointResult = @(Invoke-EpicWorktreeRemovalGateEntryPoint)
|
|
270
|
+
if ($entryPointResult.Count -gt 1) {
|
|
271
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
233
272
|
}
|
|
234
273
|
|
|
235
|
-
$
|
|
236
|
-
|
|
237
|
-
exit 0
|
|
274
|
+
exit ([int]$entryPointResult[-1])
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
|
|
7
|
-
operation. It
|
|
8
|
-
|
|
9
|
-
a non-canonical evidence location.
|
|
7
|
+
operation. It acquires the hook payload through the shared reader and reads
|
|
8
|
+
file_path from the envelope's nested tool_input, then rejects the operation when
|
|
9
|
+
the target path is a non-canonical evidence location.
|
|
10
10
|
|
|
11
11
|
Forbidden path prefixes (case-sensitive, normalized to forward-slash):
|
|
12
12
|
- artifacts/baselines/
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
response to stdout with hookSpecificOutput.permissionDecision = 'deny' and exits with
|
|
32
32
|
code 0 so Claude Code surfaces the reason. For allowed paths, a PreToolUse response
|
|
33
33
|
with permissionDecision = 'allow' is written to stdout and the script exits 0. On hard
|
|
34
|
-
failure
|
|
34
|
+
failure the script still exits 0 with a deny decision: exit 1 is non-blocking for
|
|
35
|
+
PreToolUse, so an envelope anomaly (empty payload, unparseable JSON, missing or
|
|
36
|
+
malformed tool_input) is emitted as a deny rather than raised.
|
|
35
37
|
|
|
36
38
|
.NOTES
|
|
37
39
|
Compatible with PowerShell 7+.
|
|
@@ -40,6 +42,8 @@
|
|
|
40
42
|
[CmdletBinding()]
|
|
41
43
|
param()
|
|
42
44
|
|
|
45
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
46
|
+
|
|
43
47
|
function Test-EvidenceLocationForbidden {
|
|
44
48
|
<#
|
|
45
49
|
.SYNOPSIS
|
|
@@ -104,32 +108,55 @@ function Get-EvidenceLocationBlockDecision {
|
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
function Get-EvidenceLocationAnomalyDecision {
|
|
112
|
+
<#
|
|
113
|
+
.SYNOPSIS
|
|
114
|
+
Build the fail-closed deny decision for an unreadable PreToolUse envelope.
|
|
115
|
+
.PARAMETER Anomaly
|
|
116
|
+
The anomaly code reported by the shared payload reader.
|
|
117
|
+
#>
|
|
118
|
+
[CmdletBinding()]
|
|
119
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
120
|
+
param(
|
|
121
|
+
[AllowNull()]
|
|
122
|
+
[AllowEmptyString()]
|
|
123
|
+
[string] $Anomaly
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return [ordered]@{
|
|
127
|
+
hookSpecificOutput = [ordered]@{
|
|
128
|
+
hookEventName = 'PreToolUse'
|
|
129
|
+
permissionDecision = 'deny'
|
|
130
|
+
permissionDecisionReason = 'EVIDENCE_LOCATION_BLOCKED: payload anomaly - ' +
|
|
131
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $Anomaly) +
|
|
132
|
+
'. The gate fails closed on an envelope it cannot read.'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
function Invoke-EvidenceLocationDecision {
|
|
108
138
|
<#
|
|
109
139
|
.SYNOPSIS
|
|
110
140
|
Parses the Claude Code tool-input JSON and returns an allow-or-block decision.
|
|
111
141
|
.PARAMETER ToolInputRaw
|
|
112
|
-
The raw JSON
|
|
113
|
-
|
|
142
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload. An envelope
|
|
143
|
+
anomaly fails closed as a deny; a well-formed tool_input carrying no file_path
|
|
144
|
+
remains an allow (non-file tool calls have no file_path).
|
|
114
145
|
#>
|
|
115
146
|
[CmdletBinding()]
|
|
116
147
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
117
148
|
param(
|
|
149
|
+
[AllowNull()]
|
|
150
|
+
[AllowEmptyString()]
|
|
118
151
|
[string] $ToolInputRaw
|
|
119
152
|
)
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
155
|
+
if (-not $payload.IsValid) {
|
|
156
|
+
return Get-EvidenceLocationAnomalyDecision -Anomaly $payload.Anomaly
|
|
123
157
|
}
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
127
|
-
} catch {
|
|
128
|
-
# Malformed JSON is a hard failure; caller exits 1 to surface the issue.
|
|
129
|
-
throw "enforce-evidence-locations hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
$filePath = $toolInput.file_path
|
|
159
|
+
$filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
|
|
133
160
|
if (-not $filePath) {
|
|
134
161
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
135
162
|
}
|
|
@@ -147,26 +174,34 @@ function Invoke-EvidenceLocationEntryPoint {
|
|
|
147
174
|
Runs the evidence-location decision and returns the process exit code.
|
|
148
175
|
.DESCRIPTION
|
|
149
176
|
Wraps the dispatch logic that the hook entry point performs so it can be
|
|
150
|
-
exercised by unit tests.
|
|
151
|
-
the
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
exercised by unit tests. It acquires the payload through the shared reader
|
|
178
|
+
unless the caller supplies one, writes the compact JSON decision to the output
|
|
179
|
+
stream, and returns 0. It never returns 1: exit 1 is non-blocking for
|
|
180
|
+
PreToolUse, so every envelope anomaly is already a deny decision by the time
|
|
181
|
+
control reaches here. This function does not call exit; the thin entry-point
|
|
182
|
+
wiring converts the returned code into a process exit.
|
|
154
183
|
.PARAMETER ToolInputRaw
|
|
155
|
-
|
|
184
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
185
|
+
.PARAMETER ReadPayload
|
|
186
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
187
|
+
case without touching a console.
|
|
156
188
|
#>
|
|
157
189
|
[CmdletBinding()]
|
|
158
190
|
[OutputType([int])]
|
|
159
191
|
param(
|
|
160
|
-
[
|
|
192
|
+
[AllowNull()]
|
|
193
|
+
[AllowEmptyString()]
|
|
194
|
+
[string] $ToolInputRaw,
|
|
195
|
+
|
|
196
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
161
197
|
)
|
|
162
198
|
|
|
163
|
-
|
|
164
|
-
$
|
|
165
|
-
} catch {
|
|
166
|
-
Write-Error $_
|
|
167
|
-
return 1
|
|
199
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
200
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
168
201
|
}
|
|
169
202
|
|
|
203
|
+
$decision = Invoke-EvidenceLocationDecision -ToolInputRaw $ToolInputRaw
|
|
204
|
+
|
|
170
205
|
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
171
206
|
|
|
172
207
|
return 0
|
|
@@ -177,4 +212,12 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
177
212
|
return
|
|
178
213
|
}
|
|
179
214
|
|
|
180
|
-
exit
|
|
215
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
216
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
217
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
218
|
+
$entryPointResult = @(Invoke-EvidenceLocationEntryPoint)
|
|
219
|
+
if ($entryPointResult.Count -gt 1) {
|
|
220
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
exit ([int]$entryPointResult[-1])
|