@danmoisan/drm-copilot-mcp 1.0.27 → 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 +0 -1
- 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 +64 -20
- 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 +36 -24
- 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/hook-payload/HookPayload.psm1 +494 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +63 -0
- package/resources/claude-customizations/config/blast-radius.json +9 -3
- package/resources/claude-customizations/pack-manifests/core.json +3 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +15 -0
|
@@ -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,
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
[CmdletBinding()]
|
|
40
40
|
param()
|
|
41
41
|
|
|
42
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
43
|
+
|
|
42
44
|
$script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
43
45
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
44
46
|
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
@@ -338,29 +340,34 @@ function Get-EpicMergeGateBlockDecision {
|
|
|
338
340
|
function Invoke-EpicMergeGateDecision {
|
|
339
341
|
<#
|
|
340
342
|
.SYNOPSIS
|
|
341
|
-
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.
|
|
342
349
|
.PARAMETER ToolInputRaw
|
|
343
|
-
The raw JSON
|
|
350
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
344
351
|
.OUTPUTS
|
|
345
352
|
System.Collections.Specialized.OrderedDictionary
|
|
346
353
|
#>
|
|
347
354
|
[CmdletBinding()]
|
|
348
355
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
349
356
|
param(
|
|
357
|
+
[AllowNull()]
|
|
358
|
+
[AllowEmptyString()]
|
|
350
359
|
[string] $ToolInputRaw
|
|
351
360
|
)
|
|
352
361
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
} catch {
|
|
360
|
-
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.')
|
|
361
368
|
}
|
|
362
369
|
|
|
363
|
-
$commandText = $
|
|
370
|
+
$commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
|
|
364
371
|
if (-not $commandText) {
|
|
365
372
|
return Get-EpicMergeGateAllowDecision
|
|
366
373
|
}
|
|
@@ -391,18 +398,55 @@ function Invoke-EpicMergeGateDecision {
|
|
|
391
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.'
|
|
392
399
|
}
|
|
393
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
|
|
437
|
+
}
|
|
438
|
+
|
|
394
439
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
395
440
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
396
441
|
return
|
|
397
442
|
}
|
|
398
443
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
404
450
|
}
|
|
405
451
|
|
|
406
|
-
$
|
|
407
|
-
|
|
408
|
-
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])
|
|
@@ -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])
|