@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
@@ -0,0 +1,237 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pre-tool-use hook that gates git worktree remove behind epic checkpoint merge state.
4
+
5
+ .DESCRIPTION
6
+ Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
7
+ command runs. Regex-matches git worktree remove against CLAUDE_TOOL_INPUT.command,
8
+ extracts the target worktree path argument, reads
9
+ artifacts/orchestration/epic-orchestrator-state.json, and finds the features[] record
10
+ whose worktree_path matches. Allows removal only when that record's merge_status is
11
+ merged or worktree_removed. Denies with reason EPIC_WORKTREE_REMOVAL_BLOCKED when the
12
+ checkpoint is unreadable, no matching record exists, or merge_status is anything else -
13
+ fail-closed, following the enforce-orchestration-preimplementation-gate.ps1 precedent of
14
+ treating an unreadable/no-match checkpoint as deny.
15
+
16
+ .NOTES
17
+ Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
18
+ through an injectable wrapper function so tests can mock the boundary without writing
19
+ temporary files.
20
+ #>
21
+ [CmdletBinding()]
22
+ param()
23
+
24
+ $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
25
+ $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
26
+
27
+ function Get-EpicWorktreeGateCheckpointContent {
28
+ <#
29
+ .SYNOPSIS
30
+ Read the raw JSON text of the epic checkpoint. Tests mock this function
31
+ (read seam).
32
+ .OUTPUTS
33
+ System.String or $null
34
+ #>
35
+ [CmdletBinding()]
36
+ [OutputType([string])]
37
+ param()
38
+
39
+ if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
40
+ return $null
41
+ }
42
+ return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
43
+ }
44
+
45
+ function Get-EpicWorktreeRemovalCommandPath {
46
+ <#
47
+ .SYNOPSIS
48
+ Extract the target worktree path argument from a git worktree remove command.
49
+ .PARAMETER CommandText
50
+ The Bash command text under evaluation.
51
+ .OUTPUTS
52
+ System.String or $null
53
+ #>
54
+ [CmdletBinding()]
55
+ [OutputType([string])]
56
+ param(
57
+ [Parameter(Mandatory)]
58
+ [string] $CommandText
59
+ )
60
+
61
+ if ($CommandText -match '(?i)\bgit\s+worktree\s+remove\s+(?<path>\S+)') {
62
+ return $Matches['path'].Trim('"''')
63
+ }
64
+ return $null
65
+ }
66
+
67
+ function Find-EpicWorktreeFeatureRecord {
68
+ <#
69
+ .SYNOPSIS
70
+ Locate the features[] record whose worktree_path matches the target path.
71
+ .PARAMETER Checkpoint
72
+ Parsed epic checkpoint, or $null when absent/unreadable.
73
+ .PARAMETER WorktreePath
74
+ The target worktree path extracted from the command text.
75
+ .OUTPUTS
76
+ System.Object or $null
77
+ #>
78
+ [CmdletBinding()]
79
+ param(
80
+ [AllowNull()]
81
+ $Checkpoint,
82
+
83
+ [AllowNull()]
84
+ [string] $WorktreePath
85
+ )
86
+
87
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
88
+ return $null
89
+ }
90
+ $checkpointProps = @($Checkpoint.PSObject.Properties.Name)
91
+ if ($checkpointProps -notcontains 'features') {
92
+ return $null
93
+ }
94
+
95
+ $normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
96
+
97
+ # Scan every recorded feature for a worktree_path that matches the removal target;
98
+ # path separators are normalized so Windows- and POSIX-style paths compare equal.
99
+ foreach ($feature in @($Checkpoint.features)) {
100
+ $featureProps = @($feature.PSObject.Properties.Name)
101
+ if ($featureProps -notcontains 'worktree_path') {
102
+ continue
103
+ }
104
+ $normalizedFeaturePath = (([string]$feature.worktree_path) -replace '\\', '/').TrimEnd('/')
105
+ if ($normalizedFeaturePath -eq $normalizedTarget) {
106
+ return $feature
107
+ }
108
+ }
109
+ return $null
110
+ }
111
+
112
+ function Test-EpicWorktreeRemovalAllowed {
113
+ <#
114
+ .SYNOPSIS
115
+ Decision logic: allow only when the matching feature record's merge_status is
116
+ merged or worktree_removed.
117
+ .PARAMETER FeatureRecord
118
+ The matched features[] record, or $null when no match was found.
119
+ .OUTPUTS
120
+ System.Boolean
121
+ #>
122
+ [CmdletBinding()]
123
+ [OutputType([bool])]
124
+ param(
125
+ [AllowNull()]
126
+ $FeatureRecord
127
+ )
128
+
129
+ if ($null -eq $FeatureRecord) {
130
+ return $false
131
+ }
132
+ $props = @($FeatureRecord.PSObject.Properties.Name)
133
+ if ($props -notcontains 'merge_status') {
134
+ return $false
135
+ }
136
+ return $script:AllowedMergeStatuses -contains ([string]$FeatureRecord.merge_status)
137
+ }
138
+
139
+ function Get-EpicWorktreeGateAllowDecision {
140
+ [CmdletBinding()]
141
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
142
+ param()
143
+
144
+ return [ordered]@{
145
+ hookSpecificOutput = [ordered]@{
146
+ hookEventName = 'PreToolUse'
147
+ permissionDecision = 'allow'
148
+ }
149
+ }
150
+ }
151
+
152
+ function Get-EpicWorktreeGateBlockDecision {
153
+ [CmdletBinding()]
154
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
155
+ param(
156
+ [Parameter(Mandatory)]
157
+ [string] $Reason
158
+ )
159
+
160
+ return [ordered]@{
161
+ hookSpecificOutput = [ordered]@{
162
+ hookEventName = 'PreToolUse'
163
+ permissionDecision = 'deny'
164
+ permissionDecisionReason = $Reason
165
+ }
166
+ }
167
+ }
168
+
169
+ function Invoke-EpicWorktreeRemovalGateDecision {
170
+ <#
171
+ .SYNOPSIS
172
+ Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
173
+ .PARAMETER ToolInputRaw
174
+ The raw JSON tool payload supplied by Claude Code.
175
+ .OUTPUTS
176
+ System.Collections.Specialized.OrderedDictionary
177
+ #>
178
+ [CmdletBinding()]
179
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
180
+ param(
181
+ [string] $ToolInputRaw
182
+ )
183
+
184
+ if (-not $ToolInputRaw) {
185
+ return Get-EpicWorktreeGateAllowDecision
186
+ }
187
+
188
+ try {
189
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
190
+ } catch {
191
+ throw "enforce-epic-worktree-removal-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
192
+ }
193
+
194
+ $commandText = $toolInput.command
195
+ if (-not $commandText) {
196
+ return Get-EpicWorktreeGateAllowDecision
197
+ }
198
+
199
+ if ($commandText -notmatch '(?i)\bgit\s+worktree\s+remove\b') {
200
+ return Get-EpicWorktreeGateAllowDecision
201
+ }
202
+
203
+ $worktreePath = Get-EpicWorktreeRemovalCommandPath -CommandText $commandText
204
+
205
+ $checkpointRaw = Get-EpicWorktreeGateCheckpointContent
206
+ $checkpoint = $null
207
+ if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
208
+ try {
209
+ $checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
210
+ } catch {
211
+ $checkpoint = $null
212
+ }
213
+ }
214
+
215
+ $featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
216
+ if (Test-EpicWorktreeRemovalAllowed -FeatureRecord $featureRecord) {
217
+ return Get-EpicWorktreeGateAllowDecision
218
+ }
219
+
220
+ return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching epic checkpoint features[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal."
221
+ }
222
+
223
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
224
+ if ($MyInvocation.InvocationName -eq '.') {
225
+ return
226
+ }
227
+
228
+ try {
229
+ $decision = Invoke-EpicWorktreeRemovalGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
230
+ } catch {
231
+ Write-Error $_
232
+ exit 1
233
+ }
234
+
235
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
236
+
237
+ exit 0
@@ -27,10 +27,11 @@
27
27
  is written to the tracked roots docs/features/<feature>/research/
28
28
  (feature-associated) or docs/research/ (one-off).
29
29
 
30
- If the file_path resolves to a forbidden prefix, the script writes a JSON response
31
- to stdout with 'decision': 'block' and exits with code 0 so Claude Code surfaces
32
- the reason. For allowed paths, 'decision': 'allow' is written to stdout and the
33
- script exits 0. On hard failure (malformed JSON input), the script exits 1.
30
+ If the file_path resolves to a forbidden prefix, the script writes a PreToolUse JSON
31
+ response to stdout with hookSpecificOutput.permissionDecision = 'deny' and exits with
32
+ code 0 so Claude Code surfaces the reason. For allowed paths, a PreToolUse response
33
+ with permissionDecision = 'allow' is written to stdout and the script exits 0. On hard
34
+ failure (malformed JSON input), the script exits 1.
34
35
 
35
36
  .NOTES
36
37
  Compatible with PowerShell 7+.
@@ -83,9 +84,9 @@ function Test-EvidenceLocationForbidden {
83
84
  function Get-EvidenceLocationBlockDecision {
84
85
  <#
85
86
  .SYNOPSIS
86
- Constructs a block-decision ordered dictionary for the supplied forbidden path.
87
+ Constructs a deny-decision ordered dictionary for the supplied forbidden path.
87
88
  .PARAMETER FilePath
88
- The file path that triggered the block.
89
+ The file path that triggered the deny decision.
89
90
  #>
90
91
  [CmdletBinding()]
91
92
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -95,8 +96,11 @@ function Get-EvidenceLocationBlockDecision {
95
96
  )
96
97
 
97
98
  [ordered]@{
98
- decision = 'block'
99
- reason = "EVIDENCE_LOCATION_BLOCKED: '$FilePath' is not a canonical evidence location. Use <FEATURE>/evidence/<kind>/ instead. See .claude/skills/evidence-and-timestamp-conventions/SKILL.md for the canonical scheme."
99
+ hookSpecificOutput = [ordered]@{
100
+ hookEventName = 'PreToolUse'
101
+ permissionDecision = 'deny'
102
+ permissionDecisionReason = "EVIDENCE_LOCATION_BLOCKED: '$FilePath' is not a canonical evidence location. Use <FEATURE>/evidence/<kind>/ instead. See .claude/skills/evidence-and-timestamp-conventions/SKILL.md for the canonical scheme."
103
+ }
100
104
  }
101
105
  }
102
106
 
@@ -115,7 +119,7 @@ function Invoke-EvidenceLocationDecision {
115
119
  )
116
120
 
117
121
  if (-not $ToolInputRaw) {
118
- return [ordered]@{ decision = 'allow' }
122
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
119
123
  }
120
124
 
121
125
  try {
@@ -127,14 +131,14 @@ function Invoke-EvidenceLocationDecision {
127
131
 
128
132
  $filePath = $toolInput.file_path
129
133
  if (-not $filePath) {
130
- return [ordered]@{ decision = 'allow' }
134
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
131
135
  }
132
136
 
133
137
  if (Test-EvidenceLocationForbidden -FilePath $filePath) {
134
138
  return Get-EvidenceLocationBlockDecision -FilePath $filePath
135
139
  }
136
140
 
137
- return [ordered]@{ decision = 'allow' }
141
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
138
142
  }
139
143
 
140
144
  function Invoke-EvidenceLocationEntryPoint {
@@ -163,7 +167,7 @@ function Invoke-EvidenceLocationEntryPoint {
163
167
  return 1
164
168
  }
165
169
 
166
- $decision | ConvertTo-Json -Compress | Write-Output
170
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
167
171
 
168
172
  return 0
169
173
  }
@@ -10,9 +10,10 @@
10
10
  docs/features/(active|archive)/<folder>/plan.md, the script verifies that
11
11
  each of issue.md, spec.md, and user-story.md exists in the same folder.
12
12
 
13
- If any of the three sibling files is missing, the script emits a JSON
14
- response with decision='block' and exits 0 so Claude Code surfaces the
15
- reason. All other paths pass through with decision='allow'.
13
+ If any of the three sibling files is missing, the script emits a PreToolUse
14
+ JSON response with hookSpecificOutput.permissionDecision='deny' and exits 0 so
15
+ Claude Code surfaces the reason. All other paths pass through with
16
+ permissionDecision='allow'.
16
17
 
17
18
  Filesystem reads go through Get-FeatureFolderFileExistence so tests can
18
19
  inject a fake without touching disk.
@@ -97,7 +98,7 @@ function Invoke-FeatureFolderOrderDecision {
97
98
  )
98
99
 
99
100
  if (-not $ToolInputRaw) {
100
- return [ordered]@{ decision = 'allow' }
101
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
101
102
  }
102
103
 
103
104
  try {
@@ -109,24 +110,27 @@ function Invoke-FeatureFolderOrderDecision {
109
110
 
110
111
  $filePath = $toolInput.file_path
111
112
  if (-not $filePath) {
112
- return [ordered]@{ decision = 'allow' }
113
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
113
114
  }
114
115
 
115
116
  $normalized = $filePath -replace '\\', '/'
116
117
 
117
118
  if (-not (Test-IsFeaturePlanPath -NormalizedPath $normalized)) {
118
- return [ordered]@{ decision = 'allow' }
119
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
119
120
  }
120
121
 
121
122
  $missing = Get-FeatureFolderMissingFile -PlanFilePath $normalized
122
123
  if ($missing.Count -eq 0) {
123
- return [ordered]@{ decision = 'allow' }
124
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
124
125
  }
125
126
 
126
127
  $list = ($missing -join ', ')
127
128
  return [ordered]@{
128
- decision = 'block'
129
- reason = "FEATURE_FOLDER_ORDER_BLOCKED: cannot write plan.md before producing prerequisite documents. Missing in feature folder: $list. Invoke the prd-feature subagent to generate the missing file(s) before authoring plan.md."
129
+ hookSpecificOutput = [ordered]@{
130
+ hookEventName = 'PreToolUse'
131
+ permissionDecision = 'deny'
132
+ permissionDecisionReason = "FEATURE_FOLDER_ORDER_BLOCKED: cannot write plan.md before producing prerequisite documents. Missing in feature folder: $list. Invoke the prd-feature subagent to generate the missing file(s) before authoring plan.md."
133
+ }
130
134
  }
131
135
  }
132
136
 
@@ -143,6 +147,6 @@ catch {
143
147
  exit 1
144
148
  }
145
149
 
146
- $decision | ConvertTo-Json -Compress | Write-Output
150
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
147
151
 
148
152
  exit 0
@@ -0,0 +1,182 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pre-tool-use hook that blocks a delegation to a gated subagent when the
4
+ orchestrator checkpoint records no model-routing receipt for that agent.
5
+
6
+ .DESCRIPTION
7
+ Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
8
+ tool input JSON from the CLAUDE_TOOL_INPUT environment variable and the
9
+ orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
10
+
11
+ The hook enforces presence only: it cannot read the delegate's chosen
12
+ `model` (no `model` field is exposed in the tool input), so it verifies that
13
+ a `model_routing_receipts[]` entry already exists for the target
14
+ `subagent_type`. Correctness of the recorded model stays with the
15
+ authoritative Python validator.
16
+
17
+ Gated subagent types are the Agent-tool delegates that participate in model
18
+ selection: atomic-planner, atomic-executor, feature-review, task-researcher,
19
+ prd-feature, pr-author. The `orchestrator` type is deliberately excluded: it
20
+ is the calling agent, not a subagent delegated via the Agent tool, so it is
21
+ never a receipt-gated `subagent_type`.
22
+
23
+ Allow-through (graceful allow) applies to a non-delegating `subagent_type`,
24
+ empty or absent tool input, and malformed tool-input JSON.
25
+
26
+ The checkpoint read goes through Get-ModelRoutingCheckpoint so tests can
27
+ inject a synthetic checkpoint without touching disk.
28
+
29
+ .NOTES
30
+ Compatible with PowerShell 7+. Read-only presence-gating deterrent.
31
+ #>
32
+ [CmdletBinding()]
33
+ param()
34
+
35
+ function Get-ModelRoutingCheckpoint {
36
+ <#
37
+ .SYNOPSIS
38
+ Returns the parsed orchestrator checkpoint object, or $null when the
39
+ file is missing or not valid JSON. Tests mock this seam.
40
+ #>
41
+ [CmdletBinding()]
42
+ [OutputType([object])]
43
+ param(
44
+ [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
45
+ )
46
+
47
+ if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
48
+ return $null
49
+ }
50
+
51
+ try {
52
+ $raw = Get-Content -LiteralPath $CheckpointPath -Raw -ErrorAction Stop
53
+ return $raw | ConvertFrom-Json -ErrorAction Stop
54
+ }
55
+ catch {
56
+ return $null
57
+ }
58
+ }
59
+
60
+ function Get-ModelRoutingGatedAgent {
61
+ <#
62
+ .SYNOPSIS
63
+ Returns the set of subagent types that are receipt-gated: the Agent-tool
64
+ delegates that participate in model selection. `orchestrator` is
65
+ excluded because it is the caller, not a delegated subagent.
66
+ #>
67
+ [CmdletBinding()]
68
+ [OutputType([string[]])]
69
+ param()
70
+
71
+ return [string[]] @(
72
+ 'atomic-planner',
73
+ 'atomic-executor',
74
+ 'feature-review',
75
+ 'task-researcher',
76
+ 'prd-feature',
77
+ 'pr-author'
78
+ )
79
+ }
80
+
81
+ function Test-ModelRoutingReceiptPresent {
82
+ <#
83
+ .SYNOPSIS
84
+ Returns $true when the checkpoint carries a model_routing_receipts entry
85
+ whose agent equals the target subagent type.
86
+ #>
87
+ [CmdletBinding()]
88
+ [OutputType([bool])]
89
+ param(
90
+ [Parameter(Mandatory)]
91
+ [AllowNull()]
92
+ $Checkpoint,
93
+
94
+ [Parameter(Mandatory)]
95
+ [string] $Subagent
96
+ )
97
+
98
+ if ($null -eq $Checkpoint) {
99
+ return $false
100
+ }
101
+ if ($Checkpoint.PSObject.Properties.Name -notcontains 'model_routing_receipts') {
102
+ return $false
103
+ }
104
+
105
+ # Scan every receipt for one whose agent matches the delegated subagent type.
106
+ foreach ($receipt in @($Checkpoint.model_routing_receipts)) {
107
+ if ($null -eq $receipt) {
108
+ continue
109
+ }
110
+ if ($receipt.PSObject.Properties.Name -contains 'agent' -and
111
+ [string]$receipt.agent -eq $Subagent) {
112
+ return $true
113
+ }
114
+ }
115
+ return $false
116
+ }
117
+
118
+ function Invoke-ModelRoutingReceiptDecision {
119
+ <#
120
+ .SYNOPSIS
121
+ Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision object.
122
+ #>
123
+ [CmdletBinding()]
124
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
125
+ param(
126
+ [string] $ToolInputRaw
127
+ )
128
+
129
+ $allow = [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
130
+
131
+ # Empty or absent tool input is not a delegation this hook can gate.
132
+ if (-not $ToolInputRaw) {
133
+ return $allow
134
+ }
135
+
136
+ # Malformed tool-input JSON is allowed through gracefully; this hook is a
137
+ # deterrent, not the authoritative validator.
138
+ try {
139
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
140
+ }
141
+ catch {
142
+ return $allow
143
+ }
144
+
145
+ $subagent = [string]$toolInput.subagent_type
146
+
147
+ # Only the gated Agent-tool delegates are receipt-checked; any other
148
+ # subagent_type (including orchestrator) passes through.
149
+ if (-not $subagent -or ((Get-ModelRoutingGatedAgent) -notcontains $subagent)) {
150
+ return $allow
151
+ }
152
+
153
+ $checkpoint = Get-ModelRoutingCheckpoint
154
+ if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
155
+ return $allow
156
+ }
157
+
158
+ return [ordered]@{
159
+ hookSpecificOutput = [ordered]@{
160
+ hookEventName = 'PreToolUse'
161
+ permissionDecision = 'deny'
162
+ permissionDecisionReason = "MODEL_ROUTING_RECEIPT_BLOCKED: cannot delegate to '$subagent' before a model_routing_receipts entry for it is recorded in the orchestrator checkpoint. Perform Model Selection (record the complexity assessment and routing receipt) before delegating."
163
+ }
164
+ }
165
+ }
166
+
167
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
168
+ if ($MyInvocation.InvocationName -eq '.') {
169
+ return
170
+ }
171
+
172
+ try {
173
+ $decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
174
+ }
175
+ catch {
176
+ Write-Error $_
177
+ exit 1
178
+ }
179
+
180
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
181
+
182
+ exit 0
@@ -130,6 +130,36 @@ function Get-CheckpointContent {
130
130
  return Get-Content -Raw -LiteralPath $script:CheckpointPath
131
131
  }
132
132
 
133
+ function Get-OrchestrationPreimplementationGateAllowDecision {
134
+ [CmdletBinding()]
135
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
136
+ param()
137
+
138
+ return [ordered]@{
139
+ hookSpecificOutput = [ordered]@{
140
+ hookEventName = 'PreToolUse'
141
+ permissionDecision = 'allow'
142
+ }
143
+ }
144
+ }
145
+
146
+ function Get-OrchestrationPreimplementationGateBlockDecision {
147
+ [CmdletBinding()]
148
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
149
+ param(
150
+ [Parameter(Mandatory)]
151
+ [string] $Reason
152
+ )
153
+
154
+ return [ordered]@{
155
+ hookSpecificOutput = [ordered]@{
156
+ hookEventName = 'PreToolUse'
157
+ permissionDecision = 'deny'
158
+ permissionDecisionReason = $Reason
159
+ }
160
+ }
161
+ }
162
+
133
163
  function Invoke-OrchestrationPreimplementationGateDecision {
134
164
  [CmdletBinding()]
135
165
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -139,7 +169,7 @@ function Invoke-OrchestrationPreimplementationGateDecision {
139
169
  )
140
170
 
141
171
  if (-not $ToolInputRaw) {
142
- return [ordered]@{ decision = 'allow' }
172
+ return Get-OrchestrationPreimplementationGateAllowDecision
143
173
  }
144
174
  try {
145
175
  $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
@@ -162,7 +192,7 @@ function Invoke-OrchestrationPreimplementationGateDecision {
162
192
  }
163
193
 
164
194
  if (-not $requiresReadyCheckpoint) {
165
- return [ordered]@{ decision = 'allow' }
195
+ return Get-OrchestrationPreimplementationGateAllowDecision
166
196
  }
167
197
 
168
198
  if (-not $CheckpointRaw) {
@@ -175,12 +205,9 @@ function Invoke-OrchestrationPreimplementationGateDecision {
175
205
  }
176
206
 
177
207
  if (Test-OrchestrationReady -Payload $checkpoint) {
178
- return [ordered]@{ decision = 'allow' }
179
- }
180
- return [ordered]@{
181
- decision = 'block'
182
- 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.'
208
+ return Get-OrchestrationPreimplementationGateAllowDecision
183
209
  }
210
+ 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.'
184
211
  }
185
212
 
186
213
  if ($MyInvocation.InvocationName -eq '.') {
@@ -194,5 +221,5 @@ try {
194
221
  exit 1
195
222
  }
196
223
 
197
- $decision | ConvertTo-Json -Compress | Write-Output
224
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
198
225
  exit 0