@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.
Files changed (34) hide show
  1. package/out/mcp-server.js +0 -1
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
  4. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
  5. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
  6. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
  7. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +64 -20
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
  12. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
  13. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
  14. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +36 -24
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  22. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  25. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  26. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  27. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  28. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  29. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  30. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +63 -0
  31. package/resources/claude-customizations/config/blast-radius.json +9 -3
  32. package/resources/claude-customizations/pack-manifests/core.json +3 -0
  33. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  34. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +15 -0
@@ -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 reads the tool input from the CLAUDE_TOOL_INPUT environment
8
- variable, inspects the attempted command text, and blocks direct promotion
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 CLAUDE_TOOL_INPUT.
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 CLAUDE_TOOL_INPUT.
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 CLAUDE_TOOL_INPUT and return an allow-or-block decision.
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
- if (-not $ToolInputRaw) {
202
- return Get-PromotionMcpOnlyAllowDecision
203
- }
204
-
205
- try {
206
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
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 = $toolInput.command
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
- try {
230
- $decision = Invoke-PromotionMcpOnlyDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
231
- } catch {
232
- Write-Error $_
233
- exit 1
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
- $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
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
- if (-not $ToolInputRaw) {
166
- return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
167
- }
168
-
169
- try {
170
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
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 = $toolInput.file_path
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
- if ($MyInvocation.InvocationName -eq '.') {
215
- return
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
- $sessionId = $env:CLAUDE_SESSION_ID
219
- if (-not $sessionId) {
220
- $sessionId = 'default'
221
- }
238
+ [scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
239
+ )
222
240
 
223
- $prodCap = 3
224
- $testCap = 3
225
- if ($env:CLAUDE_PYTHON_BUDGET_PROD -match '^\d+$') {
226
- $prodCap = [int]$env:CLAUDE_PYTHON_BUDGET_PROD
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
- if ($env:CLAUDE_PYTHON_BUDGET_TEST -match '^\d+$') {
229
- $testCap = [int]$env:CLAUDE_PYTHON_BUDGET_TEST
267
+
268
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
269
+ if ($MyInvocation.InvocationName -eq '.') {
270
+ return
230
271
  }
231
272
 
232
- $decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $env:CLAUDE_TOOL_INPUT -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
233
- if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
234
- $decision.Remove('state')
235
- $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
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 0
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 reads the proposed command string from the
8
- CLAUDE_TOOL_INPUT (or CLAUDE_HOOK_INPUT) environment variable (JSON with a
9
- 'command' field) or falls back to the first positional argument. If the
10
- command matches any blocked pattern (destructive operations such as forced
11
- deletions, forced pushes, or hard resets), the script writes a PreToolUse
12
- deny decision to stdout and exits with code 0. The deny decision uses the
13
- Claude Code PreToolUse schema:
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
- try {
123
- $parsed = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
124
- if ($parsed.command) {
125
- return [string]$parsed.command
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 = $env:CLAUDE_TOOL_INPUT
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') {