@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +542 -42
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +16 -2
- package/resources/claude-customizations/pack-manifests/core.json +22 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +31 -1
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
This script is invoked by the Claude Code PreToolUse hook before any Bash
|
|
7
|
-
command runs. It
|
|
8
|
-
|
|
7
|
+
command runs. It acquires the hook payload through the shared reader and reads
|
|
8
|
+
the command from the envelope's nested tool_input, then blocks direct promotion
|
|
9
9
|
script execution that would bypass the repository's MCP-only promotion path.
|
|
10
10
|
|
|
11
11
|
Forbidden command tokens (legacy promotion-script bypass):
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
[CmdletBinding()]
|
|
30
30
|
param()
|
|
31
31
|
|
|
32
|
+
|
|
33
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
32
34
|
$script:PromotionMcpOnlyBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct Bash promotion-script execution is not allowed in agent sessions. Use the drm-copilot MCP promotion tools instead.'
|
|
33
35
|
|
|
34
36
|
$script:PromotionMcpOnlyGhIssueBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct GitHub issue creation via `gh` bypasses the approved drm-copilot MCP promotion path (`mcp__drm-copilot__new_potential_entry` -> `mcp__drm-copilot__potential_to_issue` -> `mcp__drm-copilot__new_active_feature_folder`). Use those MCP tools instead.'
|
|
@@ -70,7 +72,7 @@ function Get-PromotionBypassReason {
|
|
|
70
72
|
Returns the gh-CLI issue creation reason when a forbidden gh pattern is matched.
|
|
71
73
|
Returns $null when the command is allowed.
|
|
72
74
|
.PARAMETER CommandText
|
|
73
|
-
The Bash command text extracted from
|
|
75
|
+
The Bash command text extracted from the envelope's tool_input.
|
|
74
76
|
.OUTPUTS
|
|
75
77
|
System.String or $null.
|
|
76
78
|
#>
|
|
@@ -118,7 +120,7 @@ function Test-PromotionBypassToken {
|
|
|
118
120
|
.SYNOPSIS
|
|
119
121
|
Return $true when a Bash command contains a forbidden promotion bypass pattern.
|
|
120
122
|
.PARAMETER CommandText
|
|
121
|
-
The Bash command text extracted from
|
|
123
|
+
The Bash command text extracted from the envelope's tool_input.
|
|
122
124
|
.OUTPUTS
|
|
123
125
|
System.Boolean
|
|
124
126
|
#>
|
|
@@ -183,7 +185,7 @@ function Get-PromotionMcpOnlyAllowDecision {
|
|
|
183
185
|
function Invoke-PromotionMcpOnlyDecision {
|
|
184
186
|
<#
|
|
185
187
|
.SYNOPSIS
|
|
186
|
-
Parse
|
|
188
|
+
Parse the PreToolUse envelope and return an allow-or-block decision.
|
|
187
189
|
.PARAMETER ToolInputRaw
|
|
188
190
|
The raw JSON tool payload supplied by Claude Code.
|
|
189
191
|
.OUTPUTS
|
|
@@ -198,17 +200,15 @@ function Invoke-PromotionMcpOnlyDecision {
|
|
|
198
200
|
[string] $ToolInputRaw
|
|
199
201
|
)
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} catch {
|
|
208
|
-
throw "enforce-promotion-mcp-only hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
203
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
204
|
+
if (-not $payload.IsValid) {
|
|
205
|
+
return Get-PromotionMcpOnlyBlockDecision -Reason (
|
|
206
|
+
'PROMOTION_MCP_ONLY_BLOCKED: payload anomaly - ' +
|
|
207
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
208
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
$commandText = $
|
|
211
|
+
$commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
|
|
212
212
|
if (-not $commandText) {
|
|
213
213
|
return Get-PromotionMcpOnlyAllowDecision
|
|
214
214
|
}
|
|
@@ -221,18 +221,55 @@ function Invoke-PromotionMcpOnlyDecision {
|
|
|
221
221
|
return Get-PromotionMcpOnlyAllowDecision
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
function Invoke-PromotionMcpOnlyEntryPoint {
|
|
225
|
+
<#
|
|
226
|
+
.SYNOPSIS
|
|
227
|
+
Runs the hook decision and returns the process exit code.
|
|
228
|
+
.DESCRIPTION
|
|
229
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
230
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
231
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
232
|
+
decision by the time control reaches here. The function does not call exit;
|
|
233
|
+
the thin tail converts the returned code into a process exit.
|
|
234
|
+
.PARAMETER ToolInputRaw
|
|
235
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
236
|
+
.PARAMETER ReadPayload
|
|
237
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
238
|
+
case without touching a console.
|
|
239
|
+
.OUTPUTS
|
|
240
|
+
System.Int32
|
|
241
|
+
#>
|
|
242
|
+
[CmdletBinding()]
|
|
243
|
+
[OutputType([int])]
|
|
244
|
+
param(
|
|
245
|
+
[AllowNull()]
|
|
246
|
+
[AllowEmptyString()]
|
|
247
|
+
[string] $ToolInputRaw,
|
|
248
|
+
|
|
249
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
253
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
$decision = Invoke-PromotionMcpOnlyDecision -ToolInputRaw $ToolInputRaw
|
|
257
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
258
|
+
|
|
259
|
+
return 0
|
|
260
|
+
}
|
|
261
|
+
|
|
224
262
|
# Allow dot-sourcing in tests without executing the entrypoint.
|
|
225
263
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
226
264
|
return
|
|
227
265
|
}
|
|
228
266
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
267
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
268
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
269
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
270
|
+
$entryPointResult = @(Invoke-PromotionMcpOnlyEntryPoint)
|
|
271
|
+
if ($entryPointResult.Count -gt 1) {
|
|
272
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
234
273
|
}
|
|
235
274
|
|
|
236
|
-
$
|
|
237
|
-
|
|
238
|
-
exit 0
|
|
275
|
+
exit ([int]$entryPointResult[-1])
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
[CmdletBinding()]
|
|
35
35
|
param()
|
|
36
36
|
|
|
37
|
+
|
|
38
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
37
39
|
function Get-PythonBatchBudgetState {
|
|
38
40
|
[CmdletBinding()]
|
|
39
41
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -162,17 +164,15 @@ function Invoke-PythonBatchBudgetHook {
|
|
|
162
164
|
}
|
|
163
165
|
)
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} catch {
|
|
172
|
-
return Get-PythonBatchBudgetBlockDecision -Reason 'Python batch-budget hook received malformed JSON in CLAUDE_TOOL_INPUT.'
|
|
167
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
168
|
+
if (-not $payload.IsValid) {
|
|
169
|
+
return Get-PythonBatchBudgetBlockDecision -Reason (
|
|
170
|
+
'Python batch-budget hook received an unreadable PreToolUse envelope: ' +
|
|
171
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
172
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
$filePath = $
|
|
175
|
+
$filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
|
|
176
176
|
if (-not $filePath) {
|
|
177
177
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
178
178
|
}
|
|
@@ -211,28 +211,71 @@ function Invoke-PythonBatchBudgetHook {
|
|
|
211
211
|
return $decision
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
function Invoke-PythonBatchBudgetEntryPoint {
|
|
215
|
+
<#
|
|
216
|
+
.SYNOPSIS
|
|
217
|
+
Runs the Python batch-budget decision and returns the process exit code.
|
|
218
|
+
.DESCRIPTION
|
|
219
|
+
Wraps the dispatch logic that the hook entry point performs so it can be
|
|
220
|
+
exercised by unit tests. It acquires the payload through the shared reader
|
|
221
|
+
unless the caller supplies one, writes the compact JSON decision to the output
|
|
222
|
+
stream only when the decision is a deny (this hook is deny-only: an allow
|
|
223
|
+
decision emits nothing), and returns 0. This function does not call exit; the
|
|
224
|
+
thin entry-point wiring converts the returned code into a process exit.
|
|
225
|
+
.PARAMETER ToolInputRaw
|
|
226
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
227
|
+
.PARAMETER ReadPayload
|
|
228
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
229
|
+
case without touching a console.
|
|
230
|
+
#>
|
|
231
|
+
[CmdletBinding()]
|
|
232
|
+
[OutputType([int])]
|
|
233
|
+
param(
|
|
234
|
+
[AllowNull()]
|
|
235
|
+
[AllowEmptyString()]
|
|
236
|
+
[string] $ToolInputRaw,
|
|
217
237
|
|
|
218
|
-
$
|
|
219
|
-
|
|
220
|
-
$sessionId = 'default'
|
|
221
|
-
}
|
|
238
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
239
|
+
)
|
|
222
240
|
|
|
223
|
-
$
|
|
224
|
-
$
|
|
225
|
-
|
|
226
|
-
|
|
241
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
242
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
$sessionId = $env:CLAUDE_SESSION_ID
|
|
246
|
+
if (-not $sessionId) {
|
|
247
|
+
$sessionId = 'default'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
$prodCap = 3
|
|
251
|
+
$testCap = 3
|
|
252
|
+
if ($env:CLAUDE_PYTHON_BUDGET_PROD -match '^\d+$') {
|
|
253
|
+
$prodCap = [int]$env:CLAUDE_PYTHON_BUDGET_PROD
|
|
254
|
+
}
|
|
255
|
+
if ($env:CLAUDE_PYTHON_BUDGET_TEST -match '^\d+$') {
|
|
256
|
+
$testCap = [int]$env:CLAUDE_PYTHON_BUDGET_TEST
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
$decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $ToolInputRaw -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
|
|
260
|
+
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
261
|
+
$decision.Remove('state')
|
|
262
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return 0
|
|
227
266
|
}
|
|
228
|
-
|
|
229
|
-
|
|
267
|
+
|
|
268
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
269
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
270
|
+
return
|
|
230
271
|
}
|
|
231
272
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
273
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
274
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
275
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
276
|
+
$entryPointResult = @(Invoke-PythonBatchBudgetEntryPoint)
|
|
277
|
+
if ($entryPointResult.Count -gt 1) {
|
|
278
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
236
279
|
}
|
|
237
280
|
|
|
238
|
-
exit
|
|
281
|
+
exit ([int]$entryPointResult[-1])
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
This script is invoked by the Claude Code PreToolUse hook before any Bash
|
|
7
|
-
command is executed. It
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
command is executed. It acquires the hook payload through the shared reader
|
|
8
|
+
(.claude/lib/hook-payload/HookPayload.psm1: stdin first, then the two
|
|
9
|
+
environment-variable fallbacks) and reads the proposed command string from the
|
|
10
|
+
envelope's nested tool_input.command, or falls back to the first positional
|
|
11
|
+
argument. If the command matches any blocked pattern (destructive operations
|
|
12
|
+
such as forced deletions, forced pushes, or hard resets), the script writes a
|
|
13
|
+
PreToolUse deny decision to stdout and exits with code 0. The deny decision
|
|
14
|
+
uses the Claude Code PreToolUse schema:
|
|
14
15
|
|
|
15
16
|
{"hookSpecificOutput":{"hookEventName":"PreToolUse",
|
|
16
17
|
"permissionDecision":"deny","permissionDecisionReason":"<reason>"}}
|
|
@@ -20,6 +21,13 @@
|
|
|
20
21
|
deny-path 'exit 1' are intentionally NOT used: PreToolUse fail-opens on both,
|
|
21
22
|
so they would silently fail to block.
|
|
22
23
|
|
|
24
|
+
Deliberate exception to the fail-closed envelope policy (issue #501, AC-5): this
|
|
25
|
+
hook is a dangerous-command denylist, not a receipt gate, so an empty payload
|
|
26
|
+
remains an allow and unparseable raw text is still treated as the command text
|
|
27
|
+
for denylist matching. Both behaviours preserve the documented manual/CLI usage
|
|
28
|
+
'pwsh -NoProfile -File validate-bash.ps1 "<command>"'. Every other PreToolUse
|
|
29
|
+
hook denies on those two conditions.
|
|
30
|
+
|
|
23
31
|
.NOTES
|
|
24
32
|
Compatible with PowerShell 7+.
|
|
25
33
|
This script must not modify any state; it is a read-only validation gate.
|
|
@@ -30,6 +38,8 @@ param(
|
|
|
30
38
|
[string]$CommandInput
|
|
31
39
|
)
|
|
32
40
|
|
|
41
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
42
|
+
|
|
33
43
|
function Get-BlockedBashPattern {
|
|
34
44
|
[CmdletBinding()]
|
|
35
45
|
[OutputType([string[]])]
|
|
@@ -119,14 +129,21 @@ function Get-BashCommandToCheck {
|
|
|
119
129
|
)
|
|
120
130
|
|
|
121
131
|
if ($ToolInputRaw) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
$parsed = ConvertFrom-ClaudeHookEnvelope -Raw $ToolInputRaw
|
|
133
|
+
if (-not $parsed.IsValid) {
|
|
134
|
+
# AC-5 exception: unparseable raw text is still treated as the command
|
|
135
|
+
# text, which is what the documented manual/CLI usage supplies.
|
|
136
|
+
if ($parsed.Anomaly -eq 'UnparseableJson') {
|
|
137
|
+
return [string]$ToolInputRaw
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
$extracted = Get-ClaudeHookToolInput -Envelope $parsed.Value
|
|
141
|
+
if ($extracted.IsValid) {
|
|
142
|
+
$command = Get-ClaudeHookToolInputString -ToolInput $extracted.Value -Name 'command'
|
|
143
|
+
if ($command) {
|
|
144
|
+
return $command
|
|
145
|
+
}
|
|
126
146
|
}
|
|
127
|
-
} catch {
|
|
128
|
-
# If JSON parsing fails, treat the raw input as the command.
|
|
129
|
-
return $ToolInputRaw
|
|
130
147
|
}
|
|
131
148
|
}
|
|
132
149
|
|
|
@@ -166,10 +183,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
166
183
|
return
|
|
167
184
|
}
|
|
168
185
|
|
|
169
|
-
$toolInputRaw =
|
|
170
|
-
if (-not $toolInputRaw) {
|
|
171
|
-
$toolInputRaw = $env:CLAUDE_HOOK_INPUT
|
|
172
|
-
}
|
|
186
|
+
$toolInputRaw = Read-ClaudeHookRawPayload
|
|
173
187
|
|
|
174
188
|
$decision = Invoke-ValidateBashDecision -ToolInputRaw $toolInputRaw -PositionalInput $CommandInput
|
|
175
189
|
if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
@@ -54,6 +54,7 @@ Set-StrictMode -Version Latest
|
|
|
54
54
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force
|
|
55
55
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force
|
|
56
56
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force
|
|
57
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusNormalization.psm1') -Force
|
|
57
58
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.psm1') -Force
|
|
58
59
|
|
|
59
60
|
# Feature-folder handling. Every radius contains its own feature folder, and a
|
|
@@ -66,6 +67,11 @@ $script:FeatureFolderPrefix = 'docs/features/'
|
|
|
66
67
|
$script:SourceDerived = 'derived'
|
|
67
68
|
$script:SourceObserved = 'observed'
|
|
68
69
|
|
|
70
|
+
# A contract identifier names something callable or referenceable, so it must
|
|
71
|
+
# carry at least one ASCII letter (issue #489). Get-NormalizedDeclaredRadius
|
|
72
|
+
# re-applies that rule to identifiers a pre-#489 extractor already recorded.
|
|
73
|
+
$script:ContractLetterPattern = [regex]::new('[A-Za-z]')
|
|
74
|
+
|
|
69
75
|
# Contention reason kinds, in the fixed order every result reports them. These
|
|
70
76
|
# strings are contract literals consumed by the downstream parallel schema.
|
|
71
77
|
$script:ConflictPathOverlap = 'path_overlap'
|
|
@@ -167,10 +173,20 @@ function Get-BlastRadius {
|
|
|
167
173
|
$entry = [System.Collections.Generic.List[string]]::new()
|
|
168
174
|
$entry.AddRange([string[]]@(Get-PlanPaths -PlanText $PlanText -RootSurface $rootSurface))
|
|
169
175
|
$entry.AddRange([string[]]@(Get-PathFromLine -Line $specLine -RootSurface $rootSurface))
|
|
170
|
-
|
|
176
|
+
|
|
177
|
+
# Read-by-mandate citations are dropped before the feature folder is added: a
|
|
178
|
+
# plan cites the policy rules because its author was told to read them, not
|
|
179
|
+
# because the change will write them (issue #489). Test-BlastRadius applies
|
|
180
|
+
# the same filter, which is what keeps the derived radius passing V1 and V2
|
|
181
|
+
# against its own plan. The feature-folder glob is added afterwards so it can
|
|
182
|
+
# never be excluded.
|
|
183
|
+
$surviving = [System.Collections.Generic.List[string]]::new()
|
|
184
|
+
$surviving.AddRange([string[]]@(Get-NonMandateReadEntry -Entry $entry.ToArray() `
|
|
185
|
+
-MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config))))
|
|
186
|
+
$surviving.Add((Get-FeatureFolderGlob -FeatureFolder (
|
|
171
187
|
Get-RequiredText -Value $FeatureFolder -FieldName 'feature_folder')))
|
|
172
188
|
|
|
173
|
-
$paths = [string[]]@(Get-OrdinalSortedEntry -Entry $
|
|
189
|
+
$paths = [string[]]@(Get-OrdinalSortedEntry -Entry $surviving.ToArray())
|
|
174
190
|
$concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
|
|
175
191
|
|
|
176
192
|
return ConvertTo-NormalizedBlastRadius -Radius @{
|
|
@@ -183,6 +199,92 @@ function Get-BlastRadius {
|
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
|
|
202
|
+
function Get-NormalizedDeclaredRadius {
|
|
203
|
+
<#
|
|
204
|
+
.SYNOPSIS
|
|
205
|
+
Re-apply the current extraction rules to an already-recorded radius.
|
|
206
|
+
|
|
207
|
+
.DESCRIPTION
|
|
208
|
+
Port of normalize_declared_radius. A radius recorded by an older
|
|
209
|
+
extractor can carry entries the current rules reject: directory-shaped
|
|
210
|
+
tokens, cross-corpus documentation globs, read-by-mandate citations, and
|
|
211
|
+
letterless contract tokens. Re-deriving from the plan text is not always
|
|
212
|
+
possible, so this function re-filters the recorded radius in place of a
|
|
213
|
+
fresh derivation and re-resolves the levels that depend on the surviving
|
|
214
|
+
paths (issue #489). source and computed_at are preserved and the input is
|
|
215
|
+
never mutated.
|
|
216
|
+
|
|
217
|
+
.PARAMETER Radius
|
|
218
|
+
A derived or declared radius hashtable to re-filter.
|
|
219
|
+
|
|
220
|
+
.PARAMETER Config
|
|
221
|
+
Parsed config/blast-radius.json. The root-surface set, the mandate-read
|
|
222
|
+
list, the module map, and the shared-surface list are all read from this
|
|
223
|
+
one value.
|
|
224
|
+
|
|
225
|
+
.OUTPUTS
|
|
226
|
+
System.Collections.Hashtable. A new radius carrying the surviving paths,
|
|
227
|
+
contracts re-filtered by the ASCII-letter rule, and modules and shared
|
|
228
|
+
surfaces re-resolved from the surviving paths. Throws when the radius
|
|
229
|
+
source is observed: an observed radius records a diff listing rather than
|
|
230
|
+
a plan-text harvest, so the plan-text acceptance rules do not apply to it
|
|
231
|
+
and re-filtering one would silently discard genuine evidence.
|
|
232
|
+
#>
|
|
233
|
+
[CmdletBinding()]
|
|
234
|
+
[OutputType([hashtable])]
|
|
235
|
+
param(
|
|
236
|
+
[Parameter(Mandatory = $true)]
|
|
237
|
+
[AllowNull()]
|
|
238
|
+
[object] $Radius,
|
|
239
|
+
[Parameter(Mandatory = $true)]
|
|
240
|
+
[AllowNull()]
|
|
241
|
+
[object] $Config
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
$normalized = ConvertTo-NormalizedBlastRadius -Radius $Radius
|
|
245
|
+
|
|
246
|
+
# Fail fast, before any other work, so the prohibition is unambiguous and no
|
|
247
|
+
# partially-filtered value can escape.
|
|
248
|
+
if ($normalized['source'] -eq $script:SourceObserved) {
|
|
249
|
+
throw ("Get-NormalizedDeclaredRadius rejects a radius whose source is " +
|
|
250
|
+
"'$($script:SourceObserved)': an observed radius records a diff listing, " +
|
|
251
|
+
'not a plan-text harvest, so the plan-text acceptance rules must not ' +
|
|
252
|
+
'be applied to it.')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
$rootSurface = [string[]]@(Get-ConfigRootSurface -Config $Config)
|
|
256
|
+
|
|
257
|
+
# Re-run the classifier over each recorded entry. An entry the current rules
|
|
258
|
+
# reject is dropped; the mandate-read filter then removes the citations that
|
|
259
|
+
# are evidence of a read rather than of a write.
|
|
260
|
+
$accepted = [System.Collections.Generic.List[string]]::new()
|
|
261
|
+
foreach ($entry in @($normalized['paths'])) {
|
|
262
|
+
if ($null -ne (Get-PathTokenKind -Token $entry -RootSurface $rootSurface)) {
|
|
263
|
+
$accepted.Add([string]$entry)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
$paths = [string[]]@(Get-NonMandateReadEntry -Entry $accepted.ToArray() `
|
|
268
|
+
-MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config)))
|
|
269
|
+
$concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
|
|
270
|
+
|
|
271
|
+
$contract = [System.Collections.Generic.List[string]]::new()
|
|
272
|
+
foreach ($identifier in @($normalized['contracts'])) {
|
|
273
|
+
if ($script:ContractLetterPattern.IsMatch([string]$identifier)) {
|
|
274
|
+
$contract.Add([string]$identifier)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return ConvertTo-NormalizedBlastRadius -Radius @{
|
|
279
|
+
paths = $paths
|
|
280
|
+
modules = @(Resolve-BlastRadiusModule -PathEntry $paths -Config $Config)
|
|
281
|
+
shared_surfaces = @(Resolve-BlastRadiusSharedSurface -ConcretePath $concrete -Config $Config)
|
|
282
|
+
contracts = @($contract.ToArray())
|
|
283
|
+
source = $normalized['source']
|
|
284
|
+
computed_at = $normalized['computed_at']
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
186
288
|
function Get-BlastRadiusFromObservedPaths {
|
|
187
289
|
<#
|
|
188
290
|
.SYNOPSIS
|
|
@@ -374,6 +476,7 @@ function Test-BlastRadiusConflict {
|
|
|
374
476
|
Export-ModuleMember -Function `
|
|
375
477
|
Get-PlanPaths, `
|
|
376
478
|
Get-BlastRadius, `
|
|
479
|
+
Get-NormalizedDeclaredRadius, `
|
|
377
480
|
Get-BlastRadiusFromObservedPaths, `
|
|
378
481
|
Test-BlastRadius, `
|
|
379
482
|
Test-BlastRadiusConflict
|
|
@@ -39,6 +39,7 @@ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1')
|
|
|
39
39
|
$script:ConfigSharedSurfaceKey = 'shared_surfaces'
|
|
40
40
|
$script:ConfigSharedSurfaceGlobKey = 'shared_surface_globs'
|
|
41
41
|
$script:ConfigModuleKey = 'modules'
|
|
42
|
+
$script:ConfigMandateReadKey = 'mandate_reads'
|
|
42
43
|
$script:ConfigOverBreadthKey = 'over_breadth_fraction'
|
|
43
44
|
|
|
44
45
|
# Numeric types a JSON or literal truth table may carry for the V3 threshold.
|
|
@@ -279,6 +280,36 @@ function Get-ConfigRootSurface {
|
|
|
279
280
|
return @($rootSurface.ToArray())
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
function Get-ConfigMandateRead {
|
|
284
|
+
<#
|
|
285
|
+
.SYNOPSIS
|
|
286
|
+
Read the read-by-mandate exclusion list from the truth table.
|
|
287
|
+
|
|
288
|
+
.DESCRIPTION
|
|
289
|
+
Port of config_mandate_reads. Mandate reads are the paths every agent is
|
|
290
|
+
instructed to read before doing any work, so a citation of one of them is
|
|
291
|
+
evidence that the author obeyed the reading order rather than evidence
|
|
292
|
+
that the change will write the file (issue #489).
|
|
293
|
+
|
|
294
|
+
.PARAMETER Config
|
|
295
|
+
Parsed config/blast-radius.json. Only the mandate_reads key is read.
|
|
296
|
+
|
|
297
|
+
.OUTPUTS
|
|
298
|
+
System.Object[]. Entries sorted and deduplicated by the underlying
|
|
299
|
+
reader. A config with no mandate_reads key yields an empty array, which
|
|
300
|
+
excludes nothing and reproduces pre-change behavior.
|
|
301
|
+
#>
|
|
302
|
+
[CmdletBinding()]
|
|
303
|
+
[OutputType([System.Object[]])]
|
|
304
|
+
param(
|
|
305
|
+
[Parameter(Mandatory = $true)]
|
|
306
|
+
[AllowNull()]
|
|
307
|
+
[object] $Config
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return @(Get-ConfigStringList -Config $Config -Key $script:ConfigMandateReadKey)
|
|
311
|
+
}
|
|
312
|
+
|
|
282
313
|
function Get-ConfigModuleEntry {
|
|
283
314
|
<#
|
|
284
315
|
.SYNOPSIS
|
|
@@ -374,57 +405,6 @@ function Get-ConfigOverBreadthFraction {
|
|
|
374
405
|
return $fraction
|
|
375
406
|
}
|
|
376
407
|
|
|
377
|
-
function Resolve-BlastRadiusModule {
|
|
378
|
-
<#
|
|
379
|
-
.SYNOPSIS
|
|
380
|
-
Resolve path entries to the module names of the truth-table map.
|
|
381
|
-
|
|
382
|
-
.DESCRIPTION
|
|
383
|
-
Port of resolve_modules. A module joins the radius as soon as one of its
|
|
384
|
-
globs covers one entry, so the search stops at the first hit per module.
|
|
385
|
-
A path matching no glob resolves to no module.
|
|
386
|
-
|
|
387
|
-
.PARAMETER PathEntry
|
|
388
|
-
Concrete paths and globs of a radius. An empty collection is accepted.
|
|
389
|
-
|
|
390
|
-
.PARAMETER Config
|
|
391
|
-
Parsed config/blast-radius.json.
|
|
392
|
-
|
|
393
|
-
.OUTPUTS
|
|
394
|
-
System.Object[]. Matched module names, deduplicated and ordinally sorted.
|
|
395
|
-
#>
|
|
396
|
-
[CmdletBinding()]
|
|
397
|
-
[OutputType([System.Object[]])]
|
|
398
|
-
param(
|
|
399
|
-
[Parameter(Mandatory = $true)]
|
|
400
|
-
[AllowEmptyCollection()]
|
|
401
|
-
[AllowEmptyString()]
|
|
402
|
-
[string[]] $PathEntry,
|
|
403
|
-
[Parameter(Mandatory = $true)]
|
|
404
|
-
[AllowNull()]
|
|
405
|
-
[object] $Config
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
$matched = [System.Collections.Generic.List[string]]::new()
|
|
409
|
-
foreach ($pair in @(Get-ConfigModuleEntry -Config $Config)) {
|
|
410
|
-
foreach ($pattern in $pair['globs']) {
|
|
411
|
-
$hit = $false
|
|
412
|
-
foreach ($entry in $PathEntry) {
|
|
413
|
-
if (Test-GlobMatch -Pattern $pattern -Candidate $entry) {
|
|
414
|
-
$matched.Add([string]$pair['name'])
|
|
415
|
-
$hit = $true
|
|
416
|
-
break
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if ($hit) {
|
|
420
|
-
break
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return @(Get-OrdinalSortedEntry -Entry $matched.ToArray())
|
|
426
|
-
}
|
|
427
|
-
|
|
428
408
|
function Resolve-BlastRadiusSharedSurface {
|
|
429
409
|
<#
|
|
430
410
|
.SYNOPSIS
|
|
@@ -485,7 +465,7 @@ Export-ModuleMember -Function `
|
|
|
485
465
|
Get-RequiredMapping, `
|
|
486
466
|
Get-ConfigStringList, `
|
|
487
467
|
Get-ConfigRootSurface, `
|
|
468
|
+
Get-ConfigMandateRead, `
|
|
488
469
|
Get-ConfigModuleEntry, `
|
|
489
470
|
Get-ConfigOverBreadthFraction, `
|
|
490
|
-
Resolve-BlastRadiusModule, `
|
|
491
471
|
Resolve-BlastRadiusSharedSurface
|