@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
|
@@ -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
|
-
Edit)
|
|
7
|
+
operation. It acquires the hook payload through the shared reader and reads the
|
|
8
|
+
envelope's nested tool_input ('file_path' plus 'content' for Write, or
|
|
9
|
+
'old_string'/'new_string' for Edit), then applies two independent gates:
|
|
10
10
|
|
|
11
11
|
1. Syntax gate. On a Write of a '.mmd'/'.mermaid' file, the whole file is one
|
|
12
12
|
diagram. On a Write of a Markdown file, every fenced ```mermaid block is a
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
Blocking a valid diagram is worse than missing an invalid one, so the hook declines
|
|
22
22
|
to judge rather than rejecting whenever it cannot classify content confidently:
|
|
23
23
|
|
|
24
|
-
-
|
|
25
|
-
|
|
24
|
+
- missing 'file_path' inside a well-formed tool_input, or a path outside the
|
|
25
|
+
'.mmd'/'.mermaid'/Markdown scope: allow;
|
|
26
26
|
- the validation module absent from disk: allow;
|
|
27
27
|
- an Edit payload (the syntax check needs the whole file, which an
|
|
28
28
|
old_string/new_string fragment cannot supply): allow;
|
|
@@ -32,13 +32,17 @@
|
|
|
32
32
|
- a fence immediately preceded by '<!-- mermaid-validator: ignore -->': skip that
|
|
33
33
|
block, and only that block.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
ENVELOPE VERSUS CONTENT (revised by issue #501). The hook now distinguishes two
|
|
36
|
+
kinds of unreadability that the original note conflated. An ENVELOPE-level anomaly
|
|
37
|
+
-- empty payload on every transport, unparseable envelope JSON, or a payload with
|
|
38
|
+
no usable tool_input -- means the PreToolUse contract itself drifted, which is the
|
|
39
|
+
exact defect #501 fixed and which must fail loudly rather than silently allow; the
|
|
40
|
+
hook therefore denies. A CONTENT-level judgement remains permissive, and that half
|
|
41
|
+
of the original note stands unchanged and must not be "fixed" into a hard failure:
|
|
42
|
+
an Edit fragment, a Markdown file with no fence, an absent validation module, and a
|
|
43
|
+
diagram the classifier cannot classify all still allow, because blocking a valid
|
|
44
|
+
diagram is worse than missing an invalid one. The prior blanket allow-on-unparseable
|
|
45
|
+
made the gate inert against the payload the harness actually sends.
|
|
42
46
|
|
|
43
47
|
The extension scope check runs before any content scan, so a write outside the
|
|
44
48
|
Mermaid scope pays only the JSON parse.
|
|
@@ -55,6 +59,8 @@ param()
|
|
|
55
59
|
|
|
56
60
|
Set-StrictMode -Version Latest
|
|
57
61
|
|
|
62
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
63
|
+
|
|
58
64
|
$script:MermaidModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../lib/mermaid/MermaidValidation.psm1'
|
|
59
65
|
$script:MermaidSkillPointer = 'See .claude/skills/mermaid-diagram/SKILL.md.'
|
|
60
66
|
$script:MermaidSyncPointer = 'Change it through the Mermaid Chart sync workflow in VS Code (Mermaid Chart extension: Sync Diagram with Mermaid, then Review Mermaid Sync) and pull the synced result instead of hand-editing. See .claude/rules/mermaid.md.'
|
|
@@ -288,7 +294,7 @@ function Invoke-MermaidValidationDecision {
|
|
|
288
294
|
business (out of scope, unparseable, or a Markdown file with no fence), which the
|
|
289
295
|
entry point treats as a silent allow.
|
|
290
296
|
.PARAMETER ToolInputRaw
|
|
291
|
-
The raw JSON
|
|
297
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
292
298
|
#>
|
|
293
299
|
[CmdletBinding()]
|
|
294
300
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -298,16 +304,18 @@ function Invoke-MermaidValidationDecision {
|
|
|
298
304
|
[string] $ToolInputRaw
|
|
299
305
|
)
|
|
300
306
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
# Envelope-level anomaly: fail closed. See the ENVELOPE VERSUS CONTENT note in
|
|
308
|
+
# this file's header before changing this back to a silent allow.
|
|
309
|
+
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
310
|
+
if (-not $envelope.IsValid) {
|
|
311
|
+
return Get-MermaidDenyDecision -Reason (
|
|
312
|
+
'MERMAID_VALIDATION_BLOCKED: payload anomaly - ' +
|
|
313
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
|
|
314
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
309
315
|
}
|
|
310
316
|
|
|
317
|
+
$toolInput = $envelope.Value
|
|
318
|
+
|
|
311
319
|
$filePath = [string](Get-MermaidToolInputField -InputObject $toolInput -Name 'file_path')
|
|
312
320
|
if ([string]::IsNullOrWhiteSpace($filePath)) { return $null }
|
|
313
321
|
|
|
@@ -362,16 +370,20 @@ function Invoke-MermaidValidationEntryPoint {
|
|
|
362
370
|
output stream as the JSON, so the caller would consume the JSON instead of
|
|
363
371
|
printing it and the decision would never reach Claude Code.
|
|
364
372
|
.PARAMETER ToolInputRaw
|
|
365
|
-
The raw JSON
|
|
373
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
366
374
|
#>
|
|
367
375
|
[CmdletBinding()]
|
|
368
376
|
[OutputType([string])]
|
|
369
377
|
param(
|
|
370
378
|
[AllowEmptyString()]
|
|
371
379
|
[AllowNull()]
|
|
372
|
-
[string] $ToolInputRaw
|
|
380
|
+
[string] $ToolInputRaw
|
|
373
381
|
)
|
|
374
382
|
|
|
383
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
384
|
+
$ToolInputRaw = Read-ClaudeHookRawPayload
|
|
385
|
+
}
|
|
386
|
+
|
|
375
387
|
$decision = Invoke-MermaidValidationDecision -ToolInputRaw $ToolInputRaw
|
|
376
388
|
if ($null -eq $decision) { return }
|
|
377
389
|
|
|
@@ -383,7 +395,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
383
395
|
return
|
|
384
396
|
}
|
|
385
397
|
|
|
386
|
-
Invoke-MermaidValidationEntryPoint -ToolInputRaw
|
|
398
|
+
Invoke-MermaidValidationEntryPoint -ToolInputRaw (Read-ClaudeHookRawPayload)
|
|
387
399
|
|
|
388
400
|
# Exit 0 on allow and on deny alike: the decision travels in the JSON, never in the
|
|
389
401
|
# exit code.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
7
|
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
-
tool input JSON from the
|
|
8
|
+
tool input JSON from the the envelope's nested tool_input environment variable and the
|
|
9
9
|
orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
|
|
10
10
|
|
|
11
11
|
The hook enforces presence only: it cannot read the delegate's chosen
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
[CmdletBinding()]
|
|
33
33
|
param()
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
35
37
|
function Get-ModelRoutingCheckpoint {
|
|
36
38
|
<#
|
|
37
39
|
.SYNOPSIS
|
|
@@ -118,7 +120,7 @@ function Test-ModelRoutingReceiptPresent {
|
|
|
118
120
|
function Invoke-ModelRoutingReceiptDecision {
|
|
119
121
|
<#
|
|
120
122
|
.SYNOPSIS
|
|
121
|
-
Parses
|
|
123
|
+
Parses the envelope's nested tool_input and returns an allow-or-block decision object.
|
|
122
124
|
#>
|
|
123
125
|
[CmdletBinding()]
|
|
124
126
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -128,21 +130,23 @@ function Invoke-ModelRoutingReceiptDecision {
|
|
|
128
130
|
|
|
129
131
|
$allow = [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
130
132
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
# Envelope-level anomalies fail closed (issue #501): an unreadable envelope means
|
|
134
|
+
# the PreToolUse contract drifted, which is exactly the condition that made every
|
|
135
|
+
# gate in this runtime inert, so it must deny rather than pass through.
|
|
136
|
+
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
137
|
+
if (-not $envelope.IsValid) {
|
|
138
|
+
return [ordered]@{
|
|
139
|
+
hookSpecificOutput = [ordered]@{
|
|
140
|
+
hookEventName = 'PreToolUse'
|
|
141
|
+
permissionDecision = 'deny'
|
|
142
|
+
permissionDecisionReason = 'MODEL_ROUTING_RECEIPT_BLOCKED: payload anomaly - ' +
|
|
143
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
|
|
144
|
+
'. The gate fails closed on an envelope it cannot read.'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
$subagent =
|
|
149
|
+
$subagent = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'subagent_type'
|
|
146
150
|
|
|
147
151
|
# Only the gated Agent-tool delegates are receipt-checked; any other
|
|
148
152
|
# subagent_type (including orchestrator) passes through.
|
|
@@ -169,13 +173,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
169
173
|
return
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
$decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
Write-Error $_
|
|
177
|
-
exit 1
|
|
178
|
-
}
|
|
176
|
+
$decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
|
|
179
177
|
|
|
180
178
|
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
181
179
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[CmdletBinding()]
|
|
6
6
|
param()
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
8
10
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
9
11
|
|
|
10
12
|
function ConvertFrom-CheckpointJson {
|
|
@@ -164,19 +166,23 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
164
166
|
[CmdletBinding()]
|
|
165
167
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
166
168
|
param(
|
|
169
|
+
[AllowNull()]
|
|
170
|
+
[AllowEmptyString()]
|
|
167
171
|
[string] $ToolInputRaw,
|
|
172
|
+
|
|
168
173
|
[string] $CheckpointRaw
|
|
169
174
|
)
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
throw "enforce-orchestration-preimplementation-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
176
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
177
|
+
if (-not $payload.IsValid) {
|
|
178
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
|
|
179
|
+
'PREIMPLEMENTATION_GATE_BLOCKED: payload anomaly - ' +
|
|
180
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
181
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
178
182
|
}
|
|
179
183
|
|
|
184
|
+
$toolInput = $payload.Value
|
|
185
|
+
|
|
180
186
|
$requiresReadyCheckpoint = $false
|
|
181
187
|
$filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
|
|
182
188
|
if ($filePath) {
|
|
@@ -210,16 +216,55 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
210
216
|
return Get-OrchestrationPreimplementationGateBlockDecision -Reason 'PREIMPLEMENTATION_GATE_BLOCKED: Implementation operations require artifacts/orchestration/orchestrator-state.json to contain issue number, feature folder, route metadata, lifecycle readiness, and checkpoint state before implementation begins.'
|
|
211
217
|
}
|
|
212
218
|
|
|
219
|
+
function Invoke-OrchestrationPreimplementationGateEntryPoint {
|
|
220
|
+
<#
|
|
221
|
+
.SYNOPSIS
|
|
222
|
+
Runs the hook decision and returns the process exit code.
|
|
223
|
+
.DESCRIPTION
|
|
224
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
225
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
226
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
227
|
+
decision by the time control reaches here. The function does not call exit;
|
|
228
|
+
the thin tail converts the returned code into a process exit.
|
|
229
|
+
.PARAMETER ToolInputRaw
|
|
230
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
231
|
+
.PARAMETER ReadPayload
|
|
232
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
233
|
+
case without touching a console.
|
|
234
|
+
.OUTPUTS
|
|
235
|
+
System.Int32
|
|
236
|
+
#>
|
|
237
|
+
[CmdletBinding()]
|
|
238
|
+
[OutputType([int])]
|
|
239
|
+
param(
|
|
240
|
+
[AllowNull()]
|
|
241
|
+
[AllowEmptyString()]
|
|
242
|
+
[string] $ToolInputRaw,
|
|
243
|
+
|
|
244
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
248
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
$decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $ToolInputRaw
|
|
252
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
253
|
+
|
|
254
|
+
return 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
213
258
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
214
259
|
return
|
|
215
260
|
}
|
|
216
261
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
262
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
263
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
264
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
265
|
+
$entryPointResult = @(Invoke-OrchestrationPreimplementationGateEntryPoint)
|
|
266
|
+
if ($entryPointResult.Count -gt 1) {
|
|
267
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
222
268
|
}
|
|
223
269
|
|
|
224
|
-
$
|
|
225
|
-
exit 0
|
|
270
|
+
exit ([int]$entryPointResult[-1])
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
7
|
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
8
|
-
command runs. Reads
|
|
8
|
+
command runs. Reads the hook payload through the shared reader, extracts the
|
|
9
|
+
command text from the envelope's nested tool_input, and matches it
|
|
9
10
|
against the two tokens declared at the top of this file: the abandon disposition
|
|
10
11
|
token and the confirmation marker. A command carrying the abandon disposition token
|
|
11
12
|
without the confirmation marker in the SAME command is denied with reason prefix
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
[CmdletBinding()]
|
|
33
34
|
param()
|
|
34
35
|
|
|
36
|
+
|
|
37
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
35
38
|
# The two tokens this gate matches on. These are the ONLY places either token literal
|
|
36
39
|
# appears in this file; the seam test extracts the consumer-side values from exactly
|
|
37
40
|
# these two named assignments.
|
|
@@ -44,8 +47,8 @@ $script:AbandonBlockedReasonCode = 'PARALLEL_ABANDON_BLOCKED'
|
|
|
44
47
|
function Get-ParallelAbandonGateToolInput {
|
|
45
48
|
<#
|
|
46
49
|
.SYNOPSIS
|
|
47
|
-
Read the raw JSON
|
|
48
|
-
function (read seam).
|
|
50
|
+
Read the raw JSON hook payload through the shared reader (stdin first, then
|
|
51
|
+
the two environment fallbacks). Tests mock this function (read seam).
|
|
49
52
|
.OUTPUTS
|
|
50
53
|
System.String or $null
|
|
51
54
|
#>
|
|
@@ -53,7 +56,7 @@ function Get-ParallelAbandonGateToolInput {
|
|
|
53
56
|
[OutputType([string])]
|
|
54
57
|
param()
|
|
55
58
|
|
|
56
|
-
return
|
|
59
|
+
return Read-ClaudeHookRawPayload
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function Get-ParallelAbandonNormalizedCommand {
|
|
@@ -201,7 +204,7 @@ function Get-ParallelAbandonGateBlockReason {
|
|
|
201
204
|
function Invoke-ParallelAbandonGateDecision {
|
|
202
205
|
<#
|
|
203
206
|
.SYNOPSIS
|
|
204
|
-
Parses
|
|
207
|
+
Parses the PreToolUse envelope and returns an allow-or-block decision.
|
|
205
208
|
.PARAMETER ToolInputRaw
|
|
206
209
|
The raw JSON tool payload supplied by Claude Code.
|
|
207
210
|
.OUTPUTS
|
|
@@ -218,17 +221,16 @@ function Invoke-ParallelAbandonGateDecision {
|
|
|
218
221
|
# An absent payload carries no command to gate, so there is nothing in scope. This is
|
|
219
222
|
# allow rather than deny because the gate constrains one specific destructive command
|
|
220
223
|
# shape and must not become a blanket Bash block.
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
225
|
+
if (-not $payload.IsValid) {
|
|
226
|
+
return Get-ParallelAbandonGateBlockDecision -Reason (
|
|
227
|
+
$script:AbandonBlockedReasonCode + ': payload anomaly - ' +
|
|
228
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
229
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} catch {
|
|
228
|
-
throw "enforce-parallel-abandon-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
$normalizedCommand = Get-ParallelAbandonNormalizedCommand -CommandText ([string]$toolInput.command)
|
|
232
|
+
$commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
|
|
233
|
+
$normalizedCommand = Get-ParallelAbandonNormalizedCommand -CommandText $commandText
|
|
232
234
|
if (-not (Test-ParallelAbandonCommandInScope -NormalizedCommand $normalizedCommand)) {
|
|
233
235
|
return Get-ParallelAbandonGateAllowDecision
|
|
234
236
|
}
|
|
@@ -247,12 +249,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
247
249
|
return
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
|
|
251
|
-
$decision = Invoke-ParallelAbandonGateDecision -ToolInputRaw (Get-ParallelAbandonGateToolInput)
|
|
252
|
-
} catch {
|
|
253
|
-
Write-Error $_
|
|
254
|
-
exit 1
|
|
255
|
-
}
|
|
252
|
+
$decision = Invoke-ParallelAbandonGateDecision -ToolInputRaw (Get-ParallelAbandonGateToolInput)
|
|
256
253
|
|
|
257
254
|
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
258
255
|
|
package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Dot-sourced record-resolution and barrier helpers for enforce-parallel-cohort-barrier.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Holds the five resolution helpers the Layer 1 parallel cohort barrier uses once a
|
|
7
|
+
delegation has been identified as a parallel-mode orchestrator call:
|
|
8
|
+
|
|
9
|
+
- Find-ParallelCohortBarrierItemRecord: resolves the items[] record for a folder.
|
|
10
|
+
- Find-ParallelCohortBarrierItemByKey: resolves an items[] record by issue_num.
|
|
11
|
+
- Find-ParallelCohortBarrierCohortIndex: projects the current-generation coloring.
|
|
12
|
+
- Get-ParallelCohortBarrierConflictNeighborList: collects conflict-edge neighbours.
|
|
13
|
+
- Test-ParallelCohortBarrierClear: applies the per-edge barrier rule.
|
|
14
|
+
|
|
15
|
+
They are split out of the parent hook, .claude/hooks/enforce-parallel-cohort-barrier.ps1,
|
|
16
|
+
because that file stood at 499 lines -- one below the 500-line limit -- with no room
|
|
17
|
+
for the shared payload-reader migration (issue #501). The split is a pure move with no
|
|
18
|
+
behaviour change; the precedent is .claude/hooks/enforce-parallel-drift-gate-helpers.ps1.
|
|
19
|
+
|
|
20
|
+
The helpers read the script-scoped configuration the parent hook assigns
|
|
21
|
+
($script:AllowedMergeStatuses), so this file is only ever dot-sourced from that hook
|
|
22
|
+
and never invoked on its own.
|
|
23
|
+
|
|
24
|
+
.NOTES
|
|
25
|
+
Compatible with PowerShell 7+. No external module dependencies. Every function is
|
|
26
|
+
pure: no filesystem, subprocess, network, or wall-clock access.
|
|
27
|
+
#>
|
|
28
|
+
[CmdletBinding()]
|
|
29
|
+
param()
|
|
30
|
+
|
|
31
|
+
function Find-ParallelCohortBarrierItemRecord {
|
|
32
|
+
<#
|
|
33
|
+
.SYNOPSIS
|
|
34
|
+
Locate the items[] record whose feature_folder resolves to the target basename.
|
|
35
|
+
.PARAMETER Checkpoint
|
|
36
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
37
|
+
.PARAMETER FeatureFolder
|
|
38
|
+
The target feature_folder basename resolved from the prompt.
|
|
39
|
+
.OUTPUTS
|
|
40
|
+
System.Object or $null
|
|
41
|
+
#>
|
|
42
|
+
[CmdletBinding()]
|
|
43
|
+
param(
|
|
44
|
+
[AllowNull()]
|
|
45
|
+
$Checkpoint,
|
|
46
|
+
|
|
47
|
+
[AllowNull()]
|
|
48
|
+
[string] $FeatureFolder
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($FeatureFolder)) {
|
|
52
|
+
return $null
|
|
53
|
+
}
|
|
54
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
55
|
+
if ($checkpointProps -notcontains 'items') {
|
|
56
|
+
return $null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Scan every recorded item for a feature_folder whose basename equals the target.
|
|
60
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
61
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
62
|
+
if ($itemProps -notcontains 'feature_folder') {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
$itemBasename = Get-ParallelCohortBarrierFolderBasename -FolderPath ([string]$item.feature_folder)
|
|
66
|
+
if ($itemBasename -eq $FeatureFolder) {
|
|
67
|
+
return $item
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return $null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function Find-ParallelCohortBarrierItemByKey {
|
|
74
|
+
<#
|
|
75
|
+
.SYNOPSIS
|
|
76
|
+
Locate the items[] record whose issue_num equals the supplied key.
|
|
77
|
+
.PARAMETER Checkpoint
|
|
78
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
79
|
+
.PARAMETER IssueKey
|
|
80
|
+
The item primary key (issue_num) rendered as a string.
|
|
81
|
+
.OUTPUTS
|
|
82
|
+
System.Object or $null
|
|
83
|
+
#>
|
|
84
|
+
[CmdletBinding()]
|
|
85
|
+
param(
|
|
86
|
+
[AllowNull()]
|
|
87
|
+
$Checkpoint,
|
|
88
|
+
|
|
89
|
+
[AllowNull()]
|
|
90
|
+
[string] $IssueKey
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
|
|
94
|
+
return $null
|
|
95
|
+
}
|
|
96
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
97
|
+
if ($checkpointProps -notcontains 'items') {
|
|
98
|
+
return $null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
102
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
103
|
+
if ($itemProps -notcontains 'issue_num') {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (([string]$item.issue_num) -eq $IssueKey) {
|
|
107
|
+
return $item
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return $null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Find-ParallelCohortBarrierCohortIndex {
|
|
114
|
+
<#
|
|
115
|
+
.SYNOPSIS
|
|
116
|
+
Return the current-generation cohort index that holds the supplied item key.
|
|
117
|
+
.DESCRIPTION
|
|
118
|
+
The cohort projection is restricted to cohorts[] rows whose generation equals the
|
|
119
|
+
top-level recolor_generation, which is the current coloring. Rows from a superseded
|
|
120
|
+
generation are ignored. Returns $null when the checkpoint records no
|
|
121
|
+
recolor_generation, no cohorts, or no current-generation cohort containing the key.
|
|
122
|
+
.PARAMETER Checkpoint
|
|
123
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
124
|
+
.PARAMETER IssueKey
|
|
125
|
+
The item primary key (issue_num) rendered as a string.
|
|
126
|
+
.OUTPUTS
|
|
127
|
+
System.Int32 or $null
|
|
128
|
+
#>
|
|
129
|
+
[CmdletBinding()]
|
|
130
|
+
[OutputType([int])]
|
|
131
|
+
param(
|
|
132
|
+
[AllowNull()]
|
|
133
|
+
$Checkpoint,
|
|
134
|
+
|
|
135
|
+
[AllowNull()]
|
|
136
|
+
[string] $IssueKey
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
|
|
140
|
+
return $null
|
|
141
|
+
}
|
|
142
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
143
|
+
if ($checkpointProps -notcontains 'cohorts' -or $checkpointProps -notcontains 'recolor_generation') {
|
|
144
|
+
return $null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$currentGeneration = [string]$Checkpoint.recolor_generation
|
|
148
|
+
|
|
149
|
+
foreach ($cohort in @($Checkpoint.cohorts)) {
|
|
150
|
+
$cohortProps = @($cohort.PSObject.Properties.Name)
|
|
151
|
+
if ($cohortProps -notcontains 'index' -or $cohortProps -notcontains 'generation' -or $cohortProps -notcontains 'item_keys') {
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (([string]$cohort.generation) -ne $currentGeneration) {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
$parsedIndex = 0
|
|
158
|
+
if (-not [int]::TryParse(([string]$cohort.index), [ref] $parsedIndex)) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
foreach ($key in @($cohort.item_keys)) {
|
|
162
|
+
if (([string]$key) -eq $IssueKey) {
|
|
163
|
+
return $parsedIndex
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return $null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function Get-ParallelCohortBarrierConflictNeighborList {
|
|
171
|
+
<#
|
|
172
|
+
.SYNOPSIS
|
|
173
|
+
Return the item keys that share a conflict edge with the supplied item key.
|
|
174
|
+
.PARAMETER Checkpoint
|
|
175
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
176
|
+
.PARAMETER IssueKey
|
|
177
|
+
The item primary key (issue_num) rendered as a string.
|
|
178
|
+
.OUTPUTS
|
|
179
|
+
System.Object[] holding each neighbor item key rendered as a string.
|
|
180
|
+
#>
|
|
181
|
+
[CmdletBinding()]
|
|
182
|
+
[OutputType([object[]])]
|
|
183
|
+
param(
|
|
184
|
+
[AllowNull()]
|
|
185
|
+
$Checkpoint,
|
|
186
|
+
|
|
187
|
+
[AllowNull()]
|
|
188
|
+
[string] $IssueKey
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
$neighborList = @()
|
|
192
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
|
|
193
|
+
return $neighborList
|
|
194
|
+
}
|
|
195
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
196
|
+
if ($checkpointProps -notcontains 'conflict_edges') {
|
|
197
|
+
return $neighborList
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Edges are undirected and canonically normalized to a < b, so both endpoints are
|
|
201
|
+
# inspected and the opposite endpoint is returned as the neighbor.
|
|
202
|
+
foreach ($edge in @($Checkpoint.conflict_edges)) {
|
|
203
|
+
$edgeProps = @($edge.PSObject.Properties.Name)
|
|
204
|
+
if ($edgeProps -notcontains 'a' -or $edgeProps -notcontains 'b') {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
$endpointA = [string]$edge.a
|
|
208
|
+
$endpointB = [string]$edge.b
|
|
209
|
+
if ($endpointA -eq $IssueKey) {
|
|
210
|
+
$neighborList += $endpointB
|
|
211
|
+
} elseif ($endpointB -eq $IssueKey) {
|
|
212
|
+
$neighborList += $endpointA
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return $neighborList
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Test-ParallelCohortBarrierClear {
|
|
219
|
+
<#
|
|
220
|
+
.SYNOPSIS
|
|
221
|
+
Decision logic: true only when every conflicting neighbor in a strictly prior
|
|
222
|
+
current-generation cohort has merge_status merged or worktree_removed.
|
|
223
|
+
.PARAMETER Checkpoint
|
|
224
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
225
|
+
.PARAMETER ItemRecord
|
|
226
|
+
The target item's own items[] record, or $null when not found.
|
|
227
|
+
.OUTPUTS
|
|
228
|
+
System.Boolean
|
|
229
|
+
#>
|
|
230
|
+
[CmdletBinding()]
|
|
231
|
+
[OutputType([bool])]
|
|
232
|
+
param(
|
|
233
|
+
[AllowNull()]
|
|
234
|
+
$Checkpoint,
|
|
235
|
+
|
|
236
|
+
[AllowNull()]
|
|
237
|
+
$ItemRecord
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if ($null -eq $Checkpoint -or $null -eq $ItemRecord) {
|
|
241
|
+
return $false
|
|
242
|
+
}
|
|
243
|
+
$itemProps = @($ItemRecord.PSObject.Properties.Name)
|
|
244
|
+
if ($itemProps -notcontains 'issue_num') {
|
|
245
|
+
return $false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
$targetKey = [string]$ItemRecord.issue_num
|
|
249
|
+
$targetIndex = Find-ParallelCohortBarrierCohortIndex -Checkpoint $Checkpoint -IssueKey $targetKey
|
|
250
|
+
if ($null -eq $targetIndex) {
|
|
251
|
+
# No current-generation cohort assignment: position is unknown, so fail closed.
|
|
252
|
+
return $false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
foreach ($neighborKey in @(Get-ParallelCohortBarrierConflictNeighborList -Checkpoint $Checkpoint -IssueKey $targetKey)) {
|
|
256
|
+
$neighborIndex = Find-ParallelCohortBarrierCohortIndex -Checkpoint $Checkpoint -IssueKey $neighborKey
|
|
257
|
+
if ($null -eq $neighborIndex) {
|
|
258
|
+
# Not part of the current coloring, so not a strictly prior cohort neighbor.
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
if ($neighborIndex -ge $targetIndex) {
|
|
262
|
+
# Same-cohort and later-cohort contention is not a Layer 1 concern.
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
$neighborRecord = Find-ParallelCohortBarrierItemByKey -Checkpoint $Checkpoint -IssueKey $neighborKey
|
|
266
|
+
if ($null -eq $neighborRecord) {
|
|
267
|
+
return $false
|
|
268
|
+
}
|
|
269
|
+
$neighborProps = @($neighborRecord.PSObject.Properties.Name)
|
|
270
|
+
if ($neighborProps -notcontains 'merge_status') {
|
|
271
|
+
return $false
|
|
272
|
+
}
|
|
273
|
+
if ($script:AllowedMergeStatuses -notcontains ([string]$neighborRecord.merge_status)) {
|
|
274
|
+
return $false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return $true
|
|
278
|
+
}
|