@danmoisan/drm-copilot-mcp 1.0.1 → 1.0.8

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 (69) hide show
  1. package/out/mcp-server.js +896 -202
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +1 -0
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +1 -0
  5. package/resources/claude-customizations/.claude/agents/commit-message.md +42 -0
  6. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -0
  7. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +128 -0
  8. package/resources/claude-customizations/.claude/agents/epic-review.md +1 -0
  9. package/resources/claude-customizations/.claude/agents/feature-review.md +1 -0
  10. package/resources/claude-customizations/.claude/agents/human-exception-runbook.md +48 -0
  11. package/resources/claude-customizations/.claude/agents/orchestrator.md +80 -2
  12. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -0
  13. package/resources/claude-customizations/.claude/agents/pr-author.md +37 -21
  14. package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -0
  15. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -0
  16. package/resources/claude-customizations/.claude/agents/staged-review.md +1 -0
  17. package/resources/claude-customizations/.claude/agents/status-updater.md +1 -0
  18. package/resources/claude-customizations/.claude/agents/typescript-engineer.md +1 -0
  19. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +103 -65
  20. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +12 -9
  21. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +24 -16
  22. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +19 -13
  23. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +304 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +304 -0
  25. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +237 -0
  26. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +16 -12
  27. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +14 -10
  28. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +182 -0
  29. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +35 -8
  30. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +17 -14
  31. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +104 -0
  32. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +268 -93
  33. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +19 -11
  34. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +28 -6
  35. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +17 -14
  36. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +146 -36
  37. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +27 -7
  38. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +53 -0
  39. package/resources/claude-customizations/.claude/settings.json +38 -1
  40. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +252 -0
  41. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +59 -13
  42. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +28 -0
  43. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +22 -18
  44. package/resources/claude-customizations/pack-manifests/core.json +9 -0
  45. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +8 -2
  46. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -3
  47. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +13 -0
  48. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
  49. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +65 -0
  50. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +79 -0
  51. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +66 -0
  52. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +15 -14
  53. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -25
  54. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +144 -28
  55. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +163 -0
  56. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +257 -93
  57. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +98 -0
  58. package/resources/codex-and-agents-customizations/pack-manifests/core.json +24 -0
  59. package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +11 -0
  60. package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +10 -0
  61. package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +15 -0
  62. package/resources/codex-and-agents-customizations/pack-manifests/python.json +15 -0
  63. package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +9 -0
  64. package/resources/config/orchestration-routing.json +92 -0
  65. package/resources/customizations/.github/agents/pr-author.agent.md +24 -18
  66. package/resources/icon.png +0 -0
  67. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
  68. package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +0 -68
  69. package/resources/codex-and-agents-customizations/.github/workflows/validate-orchestrator-state.yml +0 -15
@@ -20,9 +20,9 @@
20
20
  - subprocess execution (Start-Process with raw executables)
21
21
  - time-based flakiness (Start-Sleep)
22
22
 
23
- If the content contains any forbidden pattern, the script writes a JSON response
24
- to stdout with 'decision': 'block' and exits with code 0 to let Claude Code surface
25
- the reason.
23
+ If the content contains any forbidden pattern, the script writes a PreToolUse JSON
24
+ response to stdout with hookSpecificOutput.permissionDecision = 'deny' and exits with
25
+ code 0 to let Claude Code surface the reason.
26
26
 
27
27
  .NOTES
28
28
  Compatible with PowerShell 7+.
@@ -32,80 +32,118 @@
32
32
  [CmdletBinding()]
33
33
  param()
34
34
 
35
- $toolInputRaw = $env:CLAUDE_TOOL_INPUT
36
- if (-not $toolInputRaw) {
37
- exit 0
35
+ function Get-PowerShellTestPurityBlockDecision {
36
+ [CmdletBinding()]
37
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
38
+ param(
39
+ [Parameter(Mandatory)]
40
+ [string] $Reason
41
+ )
42
+
43
+ [ordered]@{
44
+ hookSpecificOutput = [ordered]@{
45
+ hookEventName = 'PreToolUse'
46
+ permissionDecision = 'deny'
47
+ permissionDecisionReason = $Reason
48
+ }
49
+ }
38
50
  }
39
51
 
40
- try {
41
- $toolInput = $toolInputRaw | ConvertFrom-Json -ErrorAction Stop
42
- }
43
- catch {
44
- exit 0
45
- }
52
+ function Test-PowerShellTestFilePath {
53
+ [CmdletBinding()]
54
+ [OutputType([bool])]
55
+ param(
56
+ [Parameter(Mandatory)]
57
+ [string] $FilePath
58
+ )
46
59
 
47
- $filePath = $toolInput.file_path
48
- if (-not $filePath) {
49
- exit 0
60
+ $normalized = $FilePath -replace '\\', '/'
61
+ return (($normalized -match '(^|/)tests/.*\.ps1$') -or ($normalized -match '\.Tests\.ps1$'))
50
62
  }
51
63
 
52
- $normalized = $filePath -replace '\\', '/'
53
- $isPowerShellTestFile = ($normalized -match '(^|/)tests/.*\.ps1$') -or ($normalized -match '\.Tests\.ps1$')
54
- if (-not $isPowerShellTestFile) {
55
- exit 0
56
- }
64
+ function Invoke-PowerShellTestPurityDecision {
65
+ [CmdletBinding()]
66
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
67
+ param(
68
+ [string] $ToolInputRaw
69
+ )
57
70
 
58
- $content = $null
59
- if ($null -ne $toolInput.content) {
60
- $content = [string]$toolInput.content
61
- }
62
- elseif ($null -ne $toolInput.new_string) {
63
- $content = [string]$toolInput.new_string
64
- }
71
+ if (-not $ToolInputRaw) {
72
+ return $null
73
+ }
65
74
 
66
- if (-not $content) {
67
- exit 0
68
- }
75
+ try {
76
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
77
+ }
78
+ catch {
79
+ return $null
80
+ }
69
81
 
70
- $forbiddenPatterns = @(
71
- @{ Pattern = '(?m)^\s*Mock\s+git\b'; Reason = 'direct Mock git forbidden in Pester tests; mock the Invoke-GitExe wrapper instead' },
72
- @{ Pattern = '(?m)^\s*Mock\s+gh\b'; Reason = 'direct Mock gh forbidden in Pester tests; mock the Invoke-GhExe wrapper instead' },
73
- @{ Pattern = '(?m)^\s*Mock\s+actionlint\b'; Reason = 'direct Mock actionlint forbidden in Pester tests; mock the Invoke-ActionlintExe wrapper instead' },
74
- @{ Pattern = "(?m)^\s*Mock\s+['""]git['""]"; Reason = 'direct Mock ''git'' forbidden in Pester tests; mock the Invoke-GitExe wrapper instead' },
75
- @{ Pattern = "(?m)^\s*Mock\s+['""]gh['""]"; Reason = 'direct Mock ''gh'' forbidden in Pester tests; mock the Invoke-GhExe wrapper instead' },
76
- @{ Pattern = '\bNew-TemporaryFile\b'; Reason = 'New-TemporaryFile forbidden in Pester unit tests' },
77
- @{ Pattern = '\[System\.IO\.Path\]::GetTempFileName'; Reason = 'temporary files forbidden in Pester unit tests' },
78
- @{ Pattern = '\[System\.IO\.Path\]::GetTempPath'; Reason = 'temp path usage forbidden in Pester unit tests' },
79
- @{ Pattern = '\$env:TEMP\b'; Reason = '$env:TEMP usage forbidden in Pester unit tests' },
80
- @{ Pattern = '\$env:TMP\b'; Reason = '$env:TMP usage forbidden in Pester unit tests' },
81
- @{ Pattern = '\bInvoke-WebRequest\b'; Reason = 'network access (Invoke-WebRequest) forbidden in Pester unit tests' },
82
- @{ Pattern = '\bInvoke-RestMethod\b'; Reason = 'network access (Invoke-RestMethod) forbidden in Pester unit tests' },
83
- @{ Pattern = '\[System\.Net\.Http\.'; Reason = 'System.Net.Http usage forbidden in Pester unit tests' },
84
- @{ Pattern = '\[System\.Net\.WebRequest\]'; Reason = 'System.Net.WebRequest usage forbidden in Pester unit tests' },
85
- @{ Pattern = '\[System\.Net\.Sockets\.'; Reason = 'raw socket access forbidden in Pester unit tests' },
86
- @{ Pattern = '\bStart-Process\b'; Reason = 'Start-Process forbidden in Pester unit tests; mock the wrapper seam instead' },
87
- @{ Pattern = '\bStart-Sleep\b'; Reason = 'Start-Sleep forbidden in Pester unit tests; avoid timing hacks' }
88
- )
89
-
90
- $violations = @()
91
- foreach ($entry in $forbiddenPatterns) {
92
- if ($content -match $entry.Pattern) {
93
- $violations += $entry.Reason
82
+ $filePath = $toolInput.file_path
83
+ if (-not $filePath) {
84
+ return $null
85
+ }
86
+
87
+ if (-not (Test-PowerShellTestFilePath -FilePath $filePath)) {
88
+ return $null
94
89
  }
95
- }
96
90
 
97
- if ($violations.Count -eq 0) {
98
- exit 0
91
+ $content = $null
92
+ if ($null -ne $toolInput.content) {
93
+ $content = [string]$toolInput.content
94
+ }
95
+ elseif ($null -ne $toolInput.new_string) {
96
+ $content = [string]$toolInput.new_string
97
+ }
98
+
99
+ if (-not $content) {
100
+ return $null
101
+ }
102
+
103
+ $forbiddenPatterns = @(
104
+ @{ Pattern = '(?m)^\s*Mock\s+git\b'; Reason = 'direct Mock git forbidden in Pester tests; mock the Invoke-GitExe wrapper instead' },
105
+ @{ Pattern = '(?m)^\s*Mock\s+gh\b'; Reason = 'direct Mock gh forbidden in Pester tests; mock the Invoke-GhExe wrapper instead' },
106
+ @{ Pattern = '(?m)^\s*Mock\s+actionlint\b'; Reason = 'direct Mock actionlint forbidden in Pester tests; mock the Invoke-ActionlintExe wrapper instead' },
107
+ @{ Pattern = "(?m)^\s*Mock\s+['""]git['""]"; Reason = 'direct Mock ''git'' forbidden in Pester tests; mock the Invoke-GitExe wrapper instead' },
108
+ @{ Pattern = "(?m)^\s*Mock\s+['""]gh['""]"; Reason = 'direct Mock ''gh'' forbidden in Pester tests; mock the Invoke-GhExe wrapper instead' },
109
+ @{ Pattern = '\bNew-TemporaryFile\b'; Reason = 'New-TemporaryFile forbidden in Pester unit tests' },
110
+ @{ Pattern = '\[System\.IO\.Path\]::GetTempFileName'; Reason = 'temporary files forbidden in Pester unit tests' },
111
+ @{ Pattern = '\[System\.IO\.Path\]::GetTempPath'; Reason = 'temp path usage forbidden in Pester unit tests' },
112
+ @{ Pattern = '\$env:TEMP\b'; Reason = '$env:TEMP usage forbidden in Pester unit tests' },
113
+ @{ Pattern = '\$env:TMP\b'; Reason = '$env:TMP usage forbidden in Pester unit tests' },
114
+ @{ Pattern = '\bInvoke-WebRequest\b'; Reason = 'network access (Invoke-WebRequest) forbidden in Pester unit tests' },
115
+ @{ Pattern = '\bInvoke-RestMethod\b'; Reason = 'network access (Invoke-RestMethod) forbidden in Pester unit tests' },
116
+ @{ Pattern = '\[System\.Net\.Http\.'; Reason = 'System.Net.Http usage forbidden in Pester unit tests' },
117
+ @{ Pattern = '\[System\.Net\.WebRequest\]'; Reason = 'System.Net.WebRequest usage forbidden in Pester unit tests' },
118
+ @{ Pattern = '\[System\.Net\.Sockets\.'; Reason = 'raw socket access forbidden in Pester unit tests' },
119
+ @{ Pattern = '\bStart-Process\b'; Reason = 'Start-Process forbidden in Pester unit tests; mock the wrapper seam instead' },
120
+ @{ Pattern = '\bStart-Sleep\b'; Reason = 'Start-Sleep forbidden in Pester unit tests; avoid timing hacks' }
121
+ )
122
+
123
+ $violations = @()
124
+ foreach ($entry in $forbiddenPatterns) {
125
+ if ($content -match $entry.Pattern) {
126
+ $violations += $entry.Reason
127
+ }
128
+ }
129
+
130
+ if ($violations.Count -eq 0) {
131
+ return $null
132
+ }
133
+
134
+ $uniqueViolations = $violations | Select-Object -Unique
135
+ $reason = "PowerShell unit test purity violations in '$filePath': " + ($uniqueViolations -join '; ') + ". Replace with wrapper-seam mocks, in-memory fakes, or pure code paths per .claude/rules/powershell.md."
136
+
137
+ return Get-PowerShellTestPurityBlockDecision -Reason $reason
99
138
  }
100
139
 
101
- $uniqueViolations = $violations | Select-Object -Unique
102
- $reason = "PowerShell unit test purity violations in '$filePath': " + ($uniqueViolations -join '; ') + ". Replace with wrapper-seam mocks, in-memory fakes, or pure code paths per .claude/rules/powershell.md."
140
+ if ($MyInvocation.InvocationName -eq '.') {
141
+ return
142
+ }
103
143
 
104
- $response = @{
105
- decision = 'block'
106
- reason = $reason
107
- } | ConvertTo-Json -Compress
144
+ $decision = Invoke-PowerShellTestPurityDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
145
+ if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
146
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
147
+ }
108
148
 
109
- Write-Output $response
110
149
  exit 0
111
-
@@ -38,8 +38,11 @@ function Get-PythonTestPurityBlockDecision {
38
38
  )
39
39
 
40
40
  [ordered]@{
41
- decision = 'block'
42
- reason = $Reason
41
+ hookSpecificOutput = [ordered]@{
42
+ hookEventName = 'PreToolUse'
43
+ permissionDecision = 'deny'
44
+ permissionDecisionReason = $Reason
45
+ }
43
46
  }
44
47
  }
45
48
 
@@ -63,7 +66,7 @@ function Invoke-PythonTestPurityDecision {
63
66
  )
64
67
 
65
68
  if (-not $ToolInputRaw) {
66
- return [ordered]@{ decision = 'allow' }
69
+ return $null
67
70
  }
68
71
 
69
72
  try {
@@ -74,11 +77,11 @@ function Invoke-PythonTestPurityDecision {
74
77
 
75
78
  $filePath = $toolInput.file_path
76
79
  if (-not $filePath) {
77
- return [ordered]@{ decision = 'allow' }
80
+ return $null
78
81
  }
79
82
 
80
83
  if (-not (Test-PythonTestFilePath -FilePath $filePath)) {
81
- return [ordered]@{ decision = 'allow' }
84
+ return $null
82
85
  }
83
86
 
84
87
  $content = $null
@@ -89,7 +92,7 @@ function Invoke-PythonTestPurityDecision {
89
92
  }
90
93
 
91
94
  if (-not $content) {
92
- return [ordered]@{ decision = 'allow' }
95
+ return $null
93
96
  }
94
97
 
95
98
  $forbiddenPatterns = @(
@@ -125,7 +128,7 @@ function Invoke-PythonTestPurityDecision {
125
128
  }
126
129
 
127
130
  if ($violations.Count -eq 0) {
128
- return [ordered]@{ decision = 'allow' }
131
+ return $null
129
132
  }
130
133
 
131
134
  $uniqueViolations = $violations | Select-Object -Unique
@@ -139,8 +142,8 @@ if ($MyInvocation.InvocationName -eq '.') {
139
142
  }
140
143
 
141
144
  $decision = Invoke-PythonTestPurityDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
142
- if ($decision.decision -eq 'block') {
143
- $decision | ConvertTo-Json -Compress | Write-Output
145
+ if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
146
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
144
147
  }
145
148
 
146
149
  exit 0
@@ -27,9 +27,11 @@
27
27
 
28
28
  Entries that do not match any canonical prefix are treated as informational
29
29
  and ignored. If any pair (i, j) with i < j has a higher canonical index at
30
- position i than at position j, the script blocks with a reason listing the
31
- offending pair. A non-empty rollback_history array suppresses this check
32
- because rollbacks legitimately reorder steps.
30
+ position i than at position j, the script denies via a PreToolUse JSON
31
+ response with hookSpecificOutput.permissionDecision='deny' and a reason
32
+ listing the offending pair. A non-empty rollback_history array suppresses
33
+ this check because rollbacks legitimately reorder steps. All allow paths
34
+ emit hookSpecificOutput.permissionDecision='allow'.
33
35
 
34
36
  Edit tool calls supply only old_string/new_string (a partial patch) and
35
37
  cannot be reliably validated without the full target file content, so they
@@ -204,7 +206,7 @@ function Invoke-CheckpointMonotonicDecision {
204
206
  )
205
207
 
206
208
  if (-not $ToolInputRaw) {
207
- return [ordered]@{ decision = 'allow' }
209
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
208
210
  }
209
211
 
210
212
  try {
@@ -216,19 +218,19 @@ function Invoke-CheckpointMonotonicDecision {
216
218
 
217
219
  $filePath = $toolInput.file_path
218
220
  if (-not $filePath) {
219
- return [ordered]@{ decision = 'allow' }
221
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
220
222
  }
221
223
 
222
224
  $normalized = $filePath -replace '\\', '/'
223
225
  if (-not (Test-IsCheckpointPath -NormalizedPath $normalized)) {
224
- return [ordered]@{ decision = 'allow' }
226
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
225
227
  }
226
228
 
227
229
  # Write tool: validate the content payload. Edit tool: partial new_string is
228
230
  # not reliable without the full target file content, so allow.
229
231
  $content = $toolInput.content
230
232
  if (-not $content) {
231
- return [ordered]@{ decision = 'allow' }
233
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
232
234
  }
233
235
 
234
236
  try {
@@ -237,11 +239,11 @@ function Invoke-CheckpointMonotonicDecision {
237
239
  catch {
238
240
  # The content itself is not valid JSON. Let downstream tools surface the
239
241
  # error rather than blocking with a misleading reason here.
240
- return [ordered]@{ decision = 'allow' }
242
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
241
243
  }
242
244
 
243
245
  if (-not $payload.PSObject.Properties.Name -contains 'completed_steps') {
244
- return [ordered]@{ decision = 'allow' }
246
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
245
247
  }
246
248
 
247
249
  $steps = @()
@@ -256,14 +258,17 @@ function Invoke-CheckpointMonotonicDecision {
256
258
  $rollbackHistory = $payload.rollback_history
257
259
  }
258
260
  if ($rollbackHistory -and @($rollbackHistory).Count -gt 0) {
259
- return [ordered]@{ decision = 'allow' }
261
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
260
262
  }
261
263
 
262
264
  $pair = if ($steps.Count -ge 2) { Get-OutOfOrderPair -CompletedSteps $steps } else { $null }
263
265
  if ($null -ne $pair) {
264
266
  return [ordered]@{
265
- decision = 'block'
266
- reason = "CHECKPOINT_ORDER_BLOCKED: completed_steps lists '$($pair.EarlierEntry)' at position $($pair.EarlierPos) before '$($pair.LaterEntry)' at position $($pair.LaterPos), but the canonical orchestrator workflow requires the later step to follow the earlier one. Reorder completed_steps or, if a rollback occurred, record it in rollback_history."
267
+ hookSpecificOutput = [ordered]@{
268
+ hookEventName = 'PreToolUse'
269
+ permissionDecision = 'deny'
270
+ permissionDecisionReason = "CHECKPOINT_ORDER_BLOCKED: completed_steps lists '$($pair.EarlierEntry)' at position $($pair.EarlierPos) before '$($pair.LaterEntry)' at position $($pair.LaterPos), but the canonical orchestrator workflow requires the later step to follow the earlier one. Reorder completed_steps or, if a rollback occurred, record it in rollback_history."
271
+ }
267
272
  }
268
273
  }
269
274
 
@@ -277,12 +282,15 @@ function Invoke-CheckpointMonotonicDecision {
277
282
  $missing += 'S4_atomic_planning'
278
283
  }
279
284
  return [ordered]@{
280
- decision = 'block'
281
- reason = "CHECKPOINT_ORDER_BLOCKED: completed_steps lists '$($missingPrerequisite.Step)' before required prerequisite step(s): $($missing -join ', '). Record promotion and planning completion before implementation, review, PR, CI, or DONE steps."
285
+ hookSpecificOutput = [ordered]@{
286
+ hookEventName = 'PreToolUse'
287
+ permissionDecision = 'deny'
288
+ permissionDecisionReason = "CHECKPOINT_ORDER_BLOCKED: completed_steps lists '$($missingPrerequisite.Step)' before required prerequisite step(s): $($missing -join ', '). Record promotion and planning completion before implementation, review, PR, CI, or DONE steps."
289
+ }
282
290
  }
283
291
  }
284
292
 
285
- return [ordered]@{ decision = 'allow' }
293
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
286
294
  }
287
295
 
288
296
  # Guard allows dot-sourcing in tests without executing the entrypoint.
@@ -298,6 +306,6 @@ catch {
298
306
  exit 1
299
307
  }
300
308
 
301
- $decision | ConvertTo-Json -Compress | Write-Output
309
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
302
310
 
303
311
  exit 0
@@ -23,9 +23,11 @@
23
23
  variables.feature-folder);
24
24
  - a ci_gate object with conclusion == "success" and a non-empty head_sha.
25
25
 
26
- The block reason names the specific missing evidence so the caller can
27
- remediate. When completion is not asserted, the write is allowed
28
- (backward compatibility).
26
+ When completion evidence is missing the hook emits a PreToolUse JSON
27
+ response with hookSpecificOutput.permissionDecision='deny' and a reason that
28
+ names the specific missing evidence so the caller can remediate. When
29
+ completion is not asserted, the write is allowed via
30
+ hookSpecificOutput.permissionDecision='allow' (backward compatibility).
29
31
 
30
32
  Edit tool calls supply only old_string/new_string (a partial patch) and
31
33
  cannot be reliably validated without the full target file content, so they
@@ -331,7 +333,7 @@ function Invoke-CompletionConsistencyDecision {
331
333
  )
332
334
 
333
335
  if (-not $ToolInputRaw) {
334
- return [ordered]@{ decision = 'allow' }
336
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
335
337
  }
336
338
 
337
339
  try {
@@ -343,12 +345,12 @@ function Invoke-CompletionConsistencyDecision {
343
345
 
344
346
  $filePath = $toolInput.file_path
345
347
  if (-not $filePath) {
346
- return [ordered]@{ decision = 'allow' }
348
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
347
349
  }
348
350
 
349
351
  $normalized = $filePath -replace '\\', '/'
350
352
  if (-not (Test-IsCheckpointPath -NormalizedPath $normalized)) {
351
- return [ordered]@{ decision = 'allow' }
353
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
352
354
  }
353
355
 
354
356
  # Write tool: validate the content payload directly. Edit tool: no content is
@@ -360,7 +362,7 @@ function Invoke-CompletionConsistencyDecision {
360
362
  if (-not $content) {
361
363
  # No content, and the Edit could not be resolved against on-disk
362
364
  # state (missing file or non-matching patch): defer and allow.
363
- return [ordered]@{ decision = 'allow' }
365
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
364
366
  }
365
367
  }
366
368
 
@@ -370,11 +372,11 @@ function Invoke-CompletionConsistencyDecision {
370
372
  catch {
371
373
  # The content itself is not valid JSON. Let downstream tools surface the
372
374
  # error rather than blocking with a misleading reason here.
373
- return [ordered]@{ decision = 'allow' }
375
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
374
376
  }
375
377
 
376
378
  if (-not (Test-CompletionAsserted -Payload $payload)) {
377
- return [ordered]@{ decision = 'allow' }
379
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
378
380
  }
379
381
 
380
382
  $missingArgs = @{ Payload = $payload; FolderExistsCheck = $FolderExistsCheck }
@@ -383,12 +385,16 @@ function Invoke-CompletionConsistencyDecision {
383
385
  }
384
386
  $missing = Get-MissingCompletionEvidence @missingArgs
385
387
  if ($missing.Count -eq 0) {
386
- return [ordered]@{ decision = 'allow' }
388
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
387
389
  }
388
390
 
391
+ $reason = "COMPLETION_CONSISTENCY_BLOCKED: the checkpoint asserts completion but is missing required completion evidence: $($missing -join ', '). A completion-asserting checkpoint must include a non-empty issue-num, a non-empty feature-folder, and a ci_gate object with conclusion == 'success' and a non-empty head_sha; routes whose requires_pr_gate is true must also include pr_gate evidence with a matching head_sha. Supply the missing evidence or remove the completion assertion."
389
392
  return [ordered]@{
390
- decision = 'block'
391
- reason = "COMPLETION_CONSISTENCY_BLOCKED: the checkpoint asserts completion but is missing required completion evidence: $($missing -join ', '). A completion-asserting checkpoint must include a non-empty issue-num, a non-empty feature-folder, and a ci_gate object with conclusion == 'success' and a non-empty head_sha; routes whose requires_pr_gate is true must also include pr_gate evidence with a matching head_sha. Supply the missing evidence or remove the completion assertion."
393
+ hookSpecificOutput = [ordered]@{
394
+ hookEventName = 'PreToolUse'
395
+ permissionDecision = 'deny'
396
+ permissionDecisionReason = $reason
397
+ }
392
398
  }
393
399
  }
394
400
 
@@ -405,6 +411,6 @@ catch {
405
411
  exit 1
406
412
  }
407
413
 
408
- $decision | ConvertTo-Json -Compress | Write-Output
414
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
409
415
 
410
416
  exit 0