@danmoisan/drm-copilot-mcp 1.0.0 → 1.0.7

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 (52) hide show
  1. package/out/mcp-server.js +814 -194
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/commit-message.md +42 -0
  4. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +127 -0
  5. package/resources/claude-customizations/.claude/agents/human-exception-runbook.md +48 -0
  6. package/resources/claude-customizations/.claude/agents/orchestrator.md +67 -2
  7. package/resources/claude-customizations/.claude/agents/pr-author.md +37 -21
  8. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +103 -65
  9. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +12 -9
  10. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +24 -16
  11. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +19 -13
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +304 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +304 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +237 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +16 -12
  16. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +14 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +35 -8
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +17 -14
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +104 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +268 -93
  21. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +19 -11
  22. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +28 -6
  23. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +17 -14
  24. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +146 -36
  25. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +19 -7
  26. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +36 -0
  27. package/resources/claude-customizations/.claude/settings.json +34 -1
  28. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +252 -0
  29. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -13
  30. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +28 -0
  31. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +22 -18
  32. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +6 -3
  34. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
  35. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +65 -0
  36. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +79 -0
  37. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +66 -0
  38. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -25
  39. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +257 -93
  40. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +98 -0
  41. package/resources/codex-and-agents-customizations/pack-manifests/core.json +24 -0
  42. package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +11 -0
  43. package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +10 -0
  44. package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +15 -0
  45. package/resources/codex-and-agents-customizations/pack-manifests/python.json +15 -0
  46. package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +9 -0
  47. package/resources/config/orchestration-routing.json +92 -0
  48. package/resources/customizations/.github/agents/pr-author.agent.md +24 -18
  49. package/resources/icon.png +0 -0
  50. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
  51. package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +0 -68
  52. package/resources/codex-and-agents-customizations/.github/workflows/validate-orchestrator-state.yml +0 -15
@@ -23,10 +23,10 @@
23
23
  CLAUDE_PYTHON_BUDGET_PROD or CLAUDE_PYTHON_BUDGET_TEST to a positive integer before
24
24
  the session starts, or by writing {"prodCap": N, "testCap": M} into the state file.
25
25
 
26
- When the cap would be exceeded by a new file, the script emits a JSON response with
27
- 'decision': 'block' and exits 0. The session must explicitly reset the counter by
28
- deleting the state file before starting a new batch. Files already counted are
29
- always allowed through.
26
+ When the cap would be exceeded by a new file, the script emits a PreToolUse JSON
27
+ response with hookSpecificOutput.permissionDecision = 'deny' and exits 0. The session
28
+ must explicitly reset the counter by deleting the state file before starting a new
29
+ batch. Files already counted are always allowed through.
30
30
 
31
31
  .NOTES
32
32
  Compatible with PowerShell 7+.
@@ -87,8 +87,11 @@ function Get-PythonBatchBudgetBlockDecision {
87
87
  )
88
88
 
89
89
  $decision = [ordered]@{
90
- decision = 'block'
91
- reason = $Reason
90
+ hookSpecificOutput = [ordered]@{
91
+ hookEventName = 'PreToolUse'
92
+ permissionDecision = 'deny'
93
+ permissionDecisionReason = $Reason
94
+ }
92
95
  }
93
96
  if ($State) {
94
97
  $decision.state = $State
@@ -113,7 +116,7 @@ function Invoke-PythonBatchBudgetDecision {
113
116
 
114
117
  $normalized = $FilePath -replace '\\', '/'
115
118
  if ($normalized -notmatch '\.py$') {
116
- return [ordered]@{ decision = 'allow'; state = $State; shouldWriteState = $false }
119
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
117
120
  }
118
121
 
119
122
  $isTestFile = ($normalized -match '(^|/)tests/.*\.py$') -or ($normalized -match '(^|/)test_[^/]+\.py$')
@@ -122,7 +125,7 @@ function Invoke-PythonBatchBudgetDecision {
122
125
  $kind = if ($isTestFile) { 'test' } else { 'production' }
123
126
 
124
127
  if ($targetList -contains $normalized) {
125
- return [ordered]@{ decision = 'allow'; state = $State; shouldWriteState = $false }
128
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
126
129
  }
127
130
 
128
131
  if ($targetList.Count -ge $cap) {
@@ -138,7 +141,7 @@ function Invoke-PythonBatchBudgetDecision {
138
141
  $State.prodFiles = @($State.prodFiles) + @($normalized)
139
142
  }
140
143
 
141
- return [ordered]@{ decision = 'allow'; state = $State; shouldWriteState = $true }
144
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $true }
142
145
  }
143
146
 
144
147
  function Invoke-PythonBatchBudgetHook {
@@ -160,7 +163,7 @@ function Invoke-PythonBatchBudgetHook {
160
163
  )
161
164
 
162
165
  if (-not $ToolInputRaw) {
163
- return [ordered]@{ decision = 'allow' }
166
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
164
167
  }
165
168
 
166
169
  try {
@@ -171,12 +174,12 @@ function Invoke-PythonBatchBudgetHook {
171
174
 
172
175
  $filePath = $toolInput.file_path
173
176
  if (-not $filePath) {
174
- return [ordered]@{ decision = 'allow' }
177
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
175
178
  }
176
179
 
177
180
  $normalized = $filePath -replace '\\', '/'
178
181
  if ($normalized -notmatch '\.py$') {
179
- return [ordered]@{ decision = 'allow' }
182
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
180
183
  }
181
184
 
182
185
  $stateDir = Join-Path -Path $Root -ChildPath '.claude/state'
@@ -227,9 +230,9 @@ if ($env:CLAUDE_PYTHON_BUDGET_TEST -match '^\d+$') {
227
230
  }
228
231
 
229
232
  $decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $env:CLAUDE_TOOL_INPUT -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
230
- if ($decision.decision -eq 'block') {
233
+ if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
231
234
  $decision.Remove('state')
232
- $decision | ConvertTo-Json -Compress | Write-Output
235
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
233
236
  }
234
237
 
235
238
  exit 0
@@ -5,11 +5,20 @@
5
5
  .DESCRIPTION
6
6
  This script is invoked by the Claude Code PreToolUse hook before any Bash
7
7
  command is executed. It reads the proposed command string from the
8
- CLAUDE_TOOL_INPUT environment variable (JSON with a 'command' field) or
9
- falls back to the first positional argument. If the command matches any
10
- blocked pattern (destructive operations such as forced deletions, forced
11
- pushes, or hard resets), the script exits with code 1 to prevent execution.
12
- Safe commands exit with code 0.
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:
14
+
15
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse",
16
+ "permissionDecision":"deny","permissionDecisionReason":"<reason>"}}
17
+
18
+ Safe commands emit no decision and exit with code 0 (an absent decision is a
19
+ valid allow at PreToolUse). The legacy top-level decision/block form and the
20
+ deny-path 'exit 1' are intentionally NOT used: PreToolUse fail-opens on both,
21
+ so they would silently fail to block.
13
22
 
14
23
  .NOTES
15
24
  Compatible with PowerShell 7+.
@@ -21,49 +30,150 @@ param(
21
30
  [string]$CommandInput
22
31
  )
23
32
 
24
- # Blocked patterns that represent destructive or irreversible operations.
25
- $blockedPatterns = @(
26
- 'rm -rf',
27
- 'git push --force',
28
- 'git push origin --force',
29
- 'Remove-Item -Recurse -Force',
30
- 'git reset --hard',
31
- 'git push -f'
32
- )
33
+ function Get-BlockedBashPattern {
34
+ [CmdletBinding()]
35
+ [OutputType([string[]])]
36
+ param()
37
+
38
+ return [string[]]@(
39
+ 'rm -rf',
40
+ 'git push --force',
41
+ 'git push origin --force',
42
+ 'Remove-Item -Recurse -Force',
43
+ 'git reset --hard',
44
+ 'git push -f'
45
+ )
46
+ }
47
+
48
+ function Get-BlockedPatternMatch {
49
+ [CmdletBinding()]
50
+ [OutputType([string])]
51
+ param(
52
+ [Parameter(Mandatory = $false)]
53
+ [AllowEmptyString()]
54
+ [AllowNull()]
55
+ [string] $Command
56
+ )
33
57
 
34
- # Resolve the command string from CLAUDE_TOOL_INPUT JSON or positional argument.
35
- $commandToCheck = ''
58
+ if (-not $Command) {
59
+ return $null
60
+ }
36
61
 
37
- if ($env:CLAUDE_TOOL_INPUT) {
38
- try {
39
- $parsed = $env:CLAUDE_TOOL_INPUT | ConvertFrom-Json
40
- if ($parsed.command) {
41
- $commandToCheck = $parsed.command
62
+ foreach ($pattern in (Get-BlockedBashPattern)) {
63
+ if ($Command.Contains($pattern)) {
64
+ return $pattern
42
65
  }
43
66
  }
44
- catch {
45
- # If JSON parsing fails, treat the raw environment variable as the command.
46
- $commandToCheck = $env:CLAUDE_TOOL_INPUT
67
+
68
+ return $null
69
+ }
70
+
71
+ function Get-BashBlockReason {
72
+ [CmdletBinding()]
73
+ [OutputType([string])]
74
+ param(
75
+ [Parameter(Mandatory = $false)]
76
+ [AllowEmptyString()]
77
+ [AllowNull()]
78
+ [string] $Command
79
+ )
80
+
81
+ $pattern = Get-BlockedPatternMatch -Command $Command
82
+ if (-not $pattern) {
83
+ return $null
47
84
  }
85
+
86
+ return "Blocked dangerous command pattern detected: '$pattern'"
48
87
  }
49
88
 
50
- # Fall back to positional argument when the environment variable is absent or empty.
51
- if (-not $commandToCheck -and $CommandInput) {
52
- $commandToCheck = $CommandInput
89
+ function Get-BashDenyDecision {
90
+ [CmdletBinding()]
91
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
92
+ param(
93
+ [Parameter(Mandatory)]
94
+ [string] $Reason
95
+ )
96
+
97
+ [ordered]@{
98
+ hookSpecificOutput = [ordered]@{
99
+ hookEventName = 'PreToolUse'
100
+ permissionDecision = 'deny'
101
+ permissionDecisionReason = $Reason
102
+ }
103
+ }
53
104
  }
54
105
 
55
- # When no command is provided, allow execution (nothing to validate).
56
- if (-not $commandToCheck) {
57
- exit 0
106
+ function Get-BashCommandToCheck {
107
+ [CmdletBinding()]
108
+ [OutputType([string])]
109
+ param(
110
+ [Parameter(Mandatory = $false)]
111
+ [AllowEmptyString()]
112
+ [AllowNull()]
113
+ [string] $ToolInputRaw,
114
+
115
+ [Parameter(Mandatory = $false)]
116
+ [AllowEmptyString()]
117
+ [AllowNull()]
118
+ [string] $PositionalInput
119
+ )
120
+
121
+ if ($ToolInputRaw) {
122
+ try {
123
+ $parsed = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
124
+ if ($parsed.command) {
125
+ return [string]$parsed.command
126
+ }
127
+ } catch {
128
+ # If JSON parsing fails, treat the raw input as the command.
129
+ return $ToolInputRaw
130
+ }
131
+ }
132
+
133
+ if ($PositionalInput) {
134
+ return $PositionalInput
135
+ }
136
+
137
+ return ''
58
138
  }
59
139
 
60
- # Check the command against each blocked pattern.
61
- foreach ($pattern in $blockedPatterns) {
62
- if ($commandToCheck.Contains($pattern)) {
63
- Write-Error "Blocked dangerous command pattern detected: '$pattern'"
64
- exit 1
140
+ function Invoke-ValidateBashDecision {
141
+ [CmdletBinding()]
142
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
143
+ param(
144
+ [Parameter(Mandatory = $false)]
145
+ [AllowEmptyString()]
146
+ [AllowNull()]
147
+ [string] $ToolInputRaw,
148
+
149
+ [Parameter(Mandatory = $false)]
150
+ [AllowEmptyString()]
151
+ [AllowNull()]
152
+ [string] $PositionalInput
153
+ )
154
+
155
+ $commandToCheck = Get-BashCommandToCheck -ToolInputRaw $ToolInputRaw -PositionalInput $PositionalInput
156
+
157
+ $reason = Get-BashBlockReason -Command $commandToCheck
158
+ if ($reason) {
159
+ return Get-BashDenyDecision -Reason $reason
65
160
  }
161
+
162
+ return $null
163
+ }
164
+
165
+ if ($MyInvocation.InvocationName -eq '.') {
166
+ return
167
+ }
168
+
169
+ $toolInputRaw = $env:CLAUDE_TOOL_INPUT
170
+ if (-not $toolInputRaw) {
171
+ $toolInputRaw = $env:CLAUDE_HOOK_INPUT
172
+ }
173
+
174
+ $decision = Invoke-ValidateBashDecision -ToolInputRaw $toolInputRaw -PositionalInput $CommandInput
175
+ if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
176
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
66
177
  }
67
178
 
68
- # Command passed all checks.
69
179
  exit 0
@@ -27,7 +27,13 @@
27
27
  #>
28
28
 
29
29
  [CmdletBinding()]
30
- param()
30
+ param(
31
+ [Parameter(Mandatory = $false)]
32
+ [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json',
33
+
34
+ [Parameter(Mandatory = $false)]
35
+ [string] $ArtifactType = 'orchestrator-state'
36
+ )
31
37
 
32
38
  Set-StrictMode -Version Latest
33
39
  $ErrorActionPreference = 'Stop'
@@ -150,9 +156,11 @@ function Invoke-RoutingContractValidation {
150
156
  Invokes the validator through an injectable subprocess scriptblock seam.
151
157
  The default Invoker runs the authoritative Python CLI:
152
158
  python -m scripts.dev_tools.validate_orchestration_artifacts \
153
- orchestrator-state <CheckpointPath> --require-complete
159
+ <ArtifactType> <CheckpointPath> --require-complete
154
160
  Tests inject a mock scriptblock so no Python process runs. The function
155
161
  does not reimplement routing logic; it delegates to the Python validator.
162
+ ArtifactType defaults to 'orchestrator-state' so the default invocation
163
+ string is unchanged for every existing caller of this hook.
156
164
 
157
165
  Returns a hashtable with keys:
158
166
  - HasErrors: $true when the validator reported a non-zero exit or
@@ -165,11 +173,14 @@ function Invoke-RoutingContractValidation {
165
173
  [Parameter(Mandatory = $true)]
166
174
  [string] $CheckpointPath,
167
175
 
176
+ [Parameter(Mandatory = $false)]
177
+ [string] $ArtifactType = 'orchestrator-state',
178
+
168
179
  [Parameter(Mandatory = $false)]
169
180
  [scriptblock] $Invoker = {
170
- param($Path)
181
+ param($Path, $Type)
171
182
  $output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
172
- orchestrator-state $Path --require-complete 2>&1
183
+ $Type $Path --require-complete 2>&1
173
184
  [pscustomobject]@{
174
185
  ExitCode = $LASTEXITCODE
175
186
  Output = ($output | Out-String)
@@ -177,7 +188,7 @@ function Invoke-RoutingContractValidation {
177
188
  }
178
189
  )
179
190
 
180
- $result = & $Invoker $CheckpointPath
191
+ $result = & $Invoker $CheckpointPath $ArtifactType
181
192
  $exitCode = 0
182
193
  if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'ExitCode')) {
183
194
  $exitCode = [int]$result.ExitCode
@@ -207,6 +218,7 @@ function Invoke-OrchestratorOutputValidation {
207
218
  param(
208
219
  [string] $RawPayload,
209
220
  [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json',
221
+ [string] $ArtifactType = 'orchestrator-state',
210
222
 
211
223
  [Parameter(Mandatory = $false)]
212
224
  [scriptblock] $RoutingInvoker
@@ -275,7 +287,7 @@ function Invoke-OrchestratorOutputValidation {
275
287
  # Delegate to the authoritative Python routing-contract validator. The
276
288
  # optional RoutingInvoker seam lets tests inject a mock; the default seam
277
289
  # produces the real subprocess call.
278
- $routingArgs = @{ CheckpointPath = $CheckpointPath }
290
+ $routingArgs = @{ CheckpointPath = $CheckpointPath; ArtifactType = $ArtifactType }
279
291
  if ($PSBoundParameters.ContainsKey('RoutingInvoker') -and $null -ne $RoutingInvoker) {
280
292
  $routingArgs['Invoker'] = $RoutingInvoker
281
293
  }
@@ -292,7 +304,7 @@ if ($MyInvocation.InvocationName -eq '.') {
292
304
  return
293
305
  }
294
306
 
295
- $result = Invoke-OrchestratorOutputValidation -RawPayload $env:CLAUDE_HOOK_INPUT
307
+ $result = Invoke-OrchestratorOutputValidation -RawPayload $env:CLAUDE_HOOK_INPUT -CheckpointPath $CheckpointPath -ArtifactType $ArtifactType
296
308
  if (-not $result.Ok) {
297
309
  Write-Error $result.Message
298
310
  exit 1
@@ -32,8 +32,44 @@ These invariants apply only when the checkpoint contains a top-level `human_inte
32
32
 
33
33
  3. **Exception requires `runbook_path`.** A requirement whose `response == "exception"` must carry a non-empty `runbook_path` string. A missing, non-string, or empty/whitespace-only `runbook_path` on an `exception` requirement is a malformed requirement.
34
34
 
35
+ ## Complexity-Assessment Scope and Backward Compatibility
36
+
37
+ These invariants apply only when the checkpoint contains a top-level `complexity_assessments` array. A checkpoint with no `complexity_assessments` key (the existing checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive and support the two-axis model-selection mechanism documented in `.claude/skills/orchestrate/SKILL.md` (`## Model Selection`). Enforcement is the Python validator, not an imported schema.
38
+
39
+ ## Invariants (complexity_assessments array)
40
+
41
+ Each entry records one assessed phase with the shape `{ phase, band, floor, signals_present[], rationale, assessed_at }`.
42
+
43
+ 1. **Band enum membership.** Each entry's `band` must be one of `C1`, `C2`, `C3`, `C4`. A missing or out-of-enum `band` is a malformed entry.
44
+
45
+ 2. **Band at or above floor.** Each entry must satisfy `band >= floor` using the band ordering `C1 < C2 < C3 < C4`. The floor constrains the lower bound only; a `band` below its `floor` is a malformed entry.
46
+
47
+ 3. **Floor equals the computed floor.** Each entry's `floor` must equal `compute_complexity_floor(signals_present)` (the reference implementation in `scripts/dev_tools/compute_complexity_floor.py`). A `floor` that does not equal the recomputed value is a malformed entry. C4 is never floor-forced; the floor never exceeds `C3`.
48
+
49
+ 4. **Non-empty `rationale`.** Each entry's `rationale` must be a non-empty string. A missing, non-string, or empty/whitespace-only `rationale` is a malformed entry.
50
+
51
+ The validator never judges the merit of the assessed band; it checks shape, floor equality, and lower-bound ordering only.
52
+
53
+ ## Model-Routing-Receipt Scope and Backward Compatibility
54
+
55
+ These invariants apply only when the checkpoint contains a top-level `model_routing_receipts` array. A checkpoint with no `model_routing_receipts` key (the existing checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive. Enforcement is the Python validator, not an imported schema.
56
+
57
+ ## Invariants (model_routing_receipts array)
58
+
59
+ Each entry records one delegation with the shape `{ agent, phase, complexity_band, fable_policy, table_model, clamped_from | null, model }`.
60
+
61
+ 1. **Model equals the resolved model.** Each entry's `model` must equal `resolve_delegation_model(agent, complexity_band, fable_policy)["model"]` (the reference implementation in `scripts/dev_tools/resolve_delegation_model.py`). A `complexity_band` outside `C1`..`C4` is a malformed entry. A `model` that does not equal the resolved model is a malformed entry.
62
+
63
+ 2. **Disabled-mode clamp.** Under `fable_policy == "disabled"` no entry's `model` may equal `fable`; any entry whose `table_model == "fable"` must record `clamped_from == "fable"` and `model == "opus"`. A disabled-mode entry that resolves to `fable`, or a disabled-mode `fable` table cell that does not record the clamp, is a malformed entry.
64
+
65
+ ## Model-Budget Contract
66
+
67
+ The session `model_budget.fable_policy` switch is a three-way enum `disabled | available | preferred` defined in `config/orchestration-routing.json`, defaulting to `disabled`. It governs only the delegation model tier and is not a route input. `disabled` removes `fable` from the consideration set and clamps `fable` cells to `opus`; `available` applies the base `complexity_to_model` table as-is; `preferred` applies the `preferred_overlay` (which redirects only the C3 cell to `fable` for the overlay agents `atomic-planner`, `prd-feature`, `feature-review`, `task-researcher`) and leaves `atomic-executor` and `pr-author` C3 cells at `opus`. `route` is never an input to model selection.
68
+
35
69
  ## Enforcement
36
70
 
37
71
  - `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated invariant when a `remediation_loop` is present, using the existing validator message style (literal, checkpoint-context prefixed). The validator returns a list of error strings and does not mutate its input.
38
72
  - `scripts/dev_tools/validate_orchestrator_state.py` likewise appends one error per violated `human_interaction` invariant when a `human_interaction` key is present, using the same literal, checkpoint-context-prefixed message style. The check does not import or read any schema file.
73
+ - `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `complexity_assessments` invariant when a `complexity_assessments` key is present, delegating to `scripts/dev_tools/_orchestrator_state_complexity.py`, which recomputes the floor via `compute_complexity_floor`. The check does not import or read any schema file.
74
+ - `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `model_routing_receipts` invariant when a `model_routing_receipts` key is present, delegating to `scripts/dev_tools/_orchestrator_state_model_routing.py`, which recomputes the resolved model via `resolve_delegation_model`. The check does not import or read any schema file.
39
75
  - The validator is consumed by the MCP tool `validate_orchestration_artifacts`; backward compatibility for existing step-based checkpoints is preserved.
@@ -31,10 +31,13 @@
31
31
  "Agent(epic-review)",
32
32
  "Agent(status-updater)",
33
33
  "Agent(pr-author)",
34
+ "Agent(commit-message)",
35
+ "Agent(human-exception-runbook)",
34
36
  "Agent(python-typed-engineer)",
35
37
  "Agent(powershell-typed-engineer)",
36
38
  "Agent(csharp-typed-engineer)",
37
39
  "Agent(typescript-engineer)",
40
+ "Agent(epic-orchestrator)",
38
41
  "Skill(orchestrate *)",
39
42
  "Skill(commit-message *)",
40
43
  "Skill(pr-author *)",
@@ -87,6 +90,14 @@
87
90
  {
88
91
  "type": "command",
89
92
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-orchestration-preimplementation-gate.ps1"
93
+ },
94
+ {
95
+ "type": "command",
96
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-merge-gate.ps1"
97
+ },
98
+ {
99
+ "type": "command",
100
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-worktree-removal-gate.ps1"
90
101
  }
91
102
  ]
92
103
  },
@@ -141,13 +152,17 @@
141
152
  {
142
153
  "type": "command",
143
154
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-orchestration-preimplementation-gate.ps1"
155
+ },
156
+ {
157
+ "type": "command",
158
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-wave-barrier.ps1"
144
159
  }
145
160
  ]
146
161
  }
147
162
  ],
148
163
  "SubagentStop": [
149
164
  {
150
- "matcher": "atomic-planner|atomic-executor|feature-review|task-researcher|prd-feature|staged-review|epic-review|status-updater|python-typed-engineer|powershell-typed-engineer|csharp-typed-engineer|typescript-engineer",
165
+ "matcher": "atomic-planner|atomic-executor|feature-review|task-researcher|prd-feature|staged-review|epic-review|status-updater|python-typed-engineer|powershell-typed-engineer|csharp-typed-engineer|typescript-engineer|orchestrator|epic-orchestrator",
151
166
  "hooks": [
152
167
  {
153
168
  "type": "command",
@@ -181,6 +196,24 @@
181
196
  "command": "pwsh -NoProfile -File .claude/hooks/validate-pr-author-output.ps1"
182
197
  }
183
198
  ]
199
+ },
200
+ {
201
+ "matcher": "orchestrator",
202
+ "hooks": [
203
+ {
204
+ "type": "command",
205
+ "command": "pwsh -NoProfile -File .claude/hooks/validate-orchestrator-output.ps1"
206
+ }
207
+ ]
208
+ },
209
+ {
210
+ "matcher": "epic-orchestrator",
211
+ "hooks": [
212
+ {
213
+ "type": "command",
214
+ "command": "pwsh -NoProfile -File .claude/hooks/validate-orchestrator-output.ps1 -CheckpointPath artifacts/orchestration/epic-orchestrator-state.json -ArtifactType epic-orchestrator-state"
215
+ }
216
+ ]
184
217
  }
185
218
  ]
186
219
  },