@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,304 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pre-tool-use hook that gates gh pr merge --merge behind epic-mode checkpoint state.
4
+
5
+ .DESCRIPTION
6
+ Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
7
+ command runs. Regex-matches gh pr merge with a --merge flag against
8
+ CLAUDE_TOOL_INPUT.command and, when matched, allows the merge only when one of two
9
+ checkpoint-only conditions holds:
10
+
11
+ 1. Child-feature path: artifacts/orchestration/orchestrator-state.json exists,
12
+ epic_mode == true, and step9_status == "passed" (the per-feature orchestrator has
13
+ already run S9 step 6's CI-green gate before attempting its own merge-on-green).
14
+ 2. Epic-integration path: artifacts/orchestration/epic-orchestrator-state.json exists,
15
+ epic_merge_pr.ci_gate.conclusion == "success", and, when the command names an
16
+ explicit PR number, that number matches epic_merge_pr.pr_number.
17
+
18
+ Otherwise the command is denied with reason EPIC_MERGE_GATE_BLOCKED. A missing or
19
+ unreadable checkpoint in either branch fails closed (denies); standalone (non-epic)
20
+ orchestration never sets epic_mode or populates epic_merge_pr, so it is structurally
21
+ prevented from invoking gh pr merge --merge at all.
22
+
23
+ Design decision: this gate trusts the on-disk checkpoint rather than shelling out live
24
+ to gh pr view for a real-time head-SHA check, matching the same non-adversarial,
25
+ policy-level-not-cryptographic posture already accepted for
26
+ enforce-pr-author-skill.ps1's own receipt mechanism. It is not a cryptographic control.
27
+
28
+ .NOTES
29
+ Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
30
+ through injectable wrapper functions so tests can mock the boundary without writing
31
+ temporary files.
32
+ #>
33
+ [CmdletBinding()]
34
+ param()
35
+
36
+ $script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
37
+ $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
38
+
39
+ function Get-ChildOrchestratorCheckpointContent {
40
+ <#
41
+ .SYNOPSIS
42
+ Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock
43
+ this function (read seam).
44
+ .OUTPUTS
45
+ System.String or $null
46
+ #>
47
+ [CmdletBinding()]
48
+ [OutputType([string])]
49
+ param()
50
+
51
+ if (-not (Test-Path -LiteralPath $script:ChildCheckpointPath -PathType Leaf)) {
52
+ return $null
53
+ }
54
+ return (Get-Content -LiteralPath $script:ChildCheckpointPath -Raw)
55
+ }
56
+
57
+ function Get-EpicOrchestratorCheckpointContent {
58
+ <#
59
+ .SYNOPSIS
60
+ Read the raw JSON text of the epic checkpoint. Tests mock this function
61
+ (read seam).
62
+ .OUTPUTS
63
+ System.String or $null
64
+ #>
65
+ [CmdletBinding()]
66
+ [OutputType([string])]
67
+ param()
68
+
69
+ if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
70
+ return $null
71
+ }
72
+ return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
73
+ }
74
+
75
+ function ConvertFrom-EpicMergeGateJson {
76
+ <#
77
+ .SYNOPSIS
78
+ Parse checkpoint JSON text, returning $null on unreadable/invalid content.
79
+ .PARAMETER Raw
80
+ Raw checkpoint text, or $null when the file does not exist.
81
+ .OUTPUTS
82
+ System.Object or $null
83
+ #>
84
+ [CmdletBinding()]
85
+ param(
86
+ [AllowNull()]
87
+ [string] $Raw
88
+ )
89
+
90
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
91
+ return $null
92
+ }
93
+ try {
94
+ return ($Raw | ConvertFrom-Json -ErrorAction Stop)
95
+ } catch {
96
+ return $null
97
+ }
98
+ }
99
+
100
+ function Get-EpicMergeGateCommandPrNumber {
101
+ <#
102
+ .SYNOPSIS
103
+ Extract an explicit PR number argument from a gh pr merge command, or $null.
104
+ .PARAMETER CommandText
105
+ The Bash command text under evaluation.
106
+ .OUTPUTS
107
+ System.Nullable[int]
108
+ #>
109
+ [CmdletBinding()]
110
+ [OutputType([int])]
111
+ param(
112
+ [Parameter(Mandatory)]
113
+ [string] $CommandText
114
+ )
115
+
116
+ if ($CommandText -match '(?i)\bgh\s+pr\s+merge\s+(\d+)\b') {
117
+ return [int]$Matches[1]
118
+ }
119
+ return $null
120
+ }
121
+
122
+ function Test-ChildCheckpointAllowsEpicMerge {
123
+ <#
124
+ .SYNOPSIS
125
+ Decision logic for the child-feature checkpoint path (branch 1).
126
+ .PARAMETER Checkpoint
127
+ Parsed child orchestrator checkpoint, or $null when absent/unreadable.
128
+ .OUTPUTS
129
+ System.Boolean
130
+ #>
131
+ [CmdletBinding()]
132
+ [OutputType([bool])]
133
+ param(
134
+ [AllowNull()]
135
+ $Checkpoint
136
+ )
137
+
138
+ if ($null -eq $Checkpoint) {
139
+ return $false
140
+ }
141
+ $props = @($Checkpoint.PSObject.Properties.Name)
142
+ if ($props -notcontains 'epic_mode' -or -not [bool]$Checkpoint.epic_mode) {
143
+ return $false
144
+ }
145
+ if ($props -notcontains 'step9_status') {
146
+ return $false
147
+ }
148
+ return ([string]$Checkpoint.step9_status) -eq 'passed'
149
+ }
150
+
151
+ function Test-EpicCheckpointAllowsMerge {
152
+ <#
153
+ .SYNOPSIS
154
+ Decision logic for the epic-integration checkpoint path (branch 2).
155
+ .PARAMETER Checkpoint
156
+ Parsed epic checkpoint, or $null when absent/unreadable.
157
+ .PARAMETER CommandPrNumber
158
+ The explicit PR number parsed from the command, or $null when the command
159
+ does not name one (implying the current branch's PR).
160
+ .OUTPUTS
161
+ System.Boolean
162
+ #>
163
+ [CmdletBinding()]
164
+ [OutputType([bool])]
165
+ param(
166
+ [AllowNull()]
167
+ $Checkpoint,
168
+
169
+ [AllowNull()]
170
+ [Nullable[int]] $CommandPrNumber
171
+ )
172
+
173
+ if ($null -eq $Checkpoint) {
174
+ return $false
175
+ }
176
+ $props = @($Checkpoint.PSObject.Properties.Name)
177
+ if ($props -notcontains 'epic_merge_pr' -or $null -eq $Checkpoint.epic_merge_pr) {
178
+ return $false
179
+ }
180
+ $mergePr = $Checkpoint.epic_merge_pr
181
+ $mergePrProps = @($mergePr.PSObject.Properties.Name)
182
+ if ($mergePrProps -notcontains 'ci_gate' -or $null -eq $mergePr.ci_gate) {
183
+ return $false
184
+ }
185
+ $ciGateProps = @($mergePr.ci_gate.PSObject.Properties.Name)
186
+ if ($ciGateProps -notcontains 'conclusion' -or ([string]$mergePr.ci_gate.conclusion) -ne 'success') {
187
+ return $false
188
+ }
189
+
190
+ # When the command names an explicit PR number, it must match the checkpoint's
191
+ # recorded epic_merge_pr.pr_number; a bare "gh pr merge --merge" implicitly targets
192
+ # the current branch's PR and is trusted per the checkpoint-only design decision.
193
+ if ($null -ne $CommandPrNumber) {
194
+ if ($mergePrProps -notcontains 'pr_number') {
195
+ return $false
196
+ }
197
+ $checkpointPrNumber = 0
198
+ if (-not [int]::TryParse([string]$mergePr.pr_number, [ref] $checkpointPrNumber)) {
199
+ return $false
200
+ }
201
+ if ($checkpointPrNumber -ne $CommandPrNumber) {
202
+ return $false
203
+ }
204
+ }
205
+
206
+ return $true
207
+ }
208
+
209
+ function Get-EpicMergeGateAllowDecision {
210
+ [CmdletBinding()]
211
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
212
+ param()
213
+
214
+ return [ordered]@{
215
+ hookSpecificOutput = [ordered]@{
216
+ hookEventName = 'PreToolUse'
217
+ permissionDecision = 'allow'
218
+ }
219
+ }
220
+ }
221
+
222
+ function Get-EpicMergeGateBlockDecision {
223
+ [CmdletBinding()]
224
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
225
+ param(
226
+ [Parameter(Mandatory)]
227
+ [string] $Reason
228
+ )
229
+
230
+ return [ordered]@{
231
+ hookSpecificOutput = [ordered]@{
232
+ hookEventName = 'PreToolUse'
233
+ permissionDecision = 'deny'
234
+ permissionDecisionReason = $Reason
235
+ }
236
+ }
237
+ }
238
+
239
+ function Invoke-EpicMergeGateDecision {
240
+ <#
241
+ .SYNOPSIS
242
+ Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
243
+ .PARAMETER ToolInputRaw
244
+ The raw JSON tool payload supplied by Claude Code.
245
+ .OUTPUTS
246
+ System.Collections.Specialized.OrderedDictionary
247
+ #>
248
+ [CmdletBinding()]
249
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
250
+ param(
251
+ [string] $ToolInputRaw
252
+ )
253
+
254
+ if (-not $ToolInputRaw) {
255
+ return Get-EpicMergeGateAllowDecision
256
+ }
257
+
258
+ try {
259
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
260
+ } catch {
261
+ throw "enforce-epic-merge-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
262
+ }
263
+
264
+ $commandText = $toolInput.command
265
+ if (-not $commandText) {
266
+ return Get-EpicMergeGateAllowDecision
267
+ }
268
+
269
+ # Only a gh pr merge invocation carrying --merge is in scope for this gate; every
270
+ # other Bash command is unaffected.
271
+ if ($commandText -notmatch '(?i)\bgh\s+pr\s+merge\b' -or $commandText -notmatch '--merge\b') {
272
+ return Get-EpicMergeGateAllowDecision
273
+ }
274
+
275
+ $commandPrNumber = Get-EpicMergeGateCommandPrNumber -CommandText $commandText
276
+
277
+ $childCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ChildOrchestratorCheckpointContent)
278
+ if (Test-ChildCheckpointAllowsEpicMerge -Checkpoint $childCheckpoint) {
279
+ return Get-EpicMergeGateAllowDecision
280
+ }
281
+
282
+ $epicCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-EpicOrchestratorCheckpointContent)
283
+ if (Test-EpicCheckpointAllowsMerge -Checkpoint $epicCheckpoint -CommandPrNumber $commandPrNumber) {
284
+ return Get-EpicMergeGateAllowDecision
285
+ }
286
+
287
+ return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: gh pr merge --merge requires either a per-feature checkpoint with epic_mode == true and step9_status == "passed", or an epic checkpoint with epic_merge_pr.ci_gate.conclusion == "success" and a matching pr_number. Neither checkpoint satisfied this gate.'
288
+ }
289
+
290
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
291
+ if ($MyInvocation.InvocationName -eq '.') {
292
+ return
293
+ }
294
+
295
+ try {
296
+ $decision = Invoke-EpicMergeGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
297
+ } catch {
298
+ Write-Error $_
299
+ exit 1
300
+ }
301
+
302
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
303
+
304
+ exit 0
@@ -0,0 +1,304 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pre-tool-use hook that is the Layer 1 per-call deterrent for the epic wave barrier.
4
+
5
+ .DESCRIPTION
6
+ Invoked by the Claude Code PreToolUse hook on the "Agent" matcher before any Agent
7
+ (Task) call runs. Activates only when CLAUDE_TOOL_INPUT.subagent_type == "orchestrator"
8
+ and the serialized prompt contains the epic-mode kickoff marker "Epic mode: true".
9
+
10
+ Resolution and decision procedure:
11
+ 1. Resolve the target child feature_folder from the prompt text by scanning for a
12
+ docs/features/active/<token> path, mirroring
13
+ enforce-prd-feature-before-planner.ps1's Find-PrdFeatureFolderFromPrompt
14
+ technique (longest match wins; a .md-suffixed match uses its parent directory).
15
+ 2. Read artifacts/orchestration/epic-orchestrator-state.json and locate the
16
+ features[] record whose feature_folder equals the resolved basename.
17
+ 3. Look up that feature's depends_on list, and for every dependency, locate its own
18
+ features[] record.
19
+ 4. Deny with reason EPIC_WAVE_BARRIER_BLOCKED unless every dependency's merge_status
20
+ is merged or worktree_removed. A missing/unreadable checkpoint, an unresolved
21
+ target feature_folder, or a missing dependency record also denies (fail-closed).
22
+
23
+ This is the per-call deterrent (Layer 1) of the two-layer wave-barrier design; the
24
+ retrospective backstop (Layer 2) is the wave-barrier ordering invariant inside
25
+ validate_epic_orchestrator_state_text, enforced separately at epic-orchestrator
26
+ SubagentStop time.
27
+
28
+ .NOTES
29
+ Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
30
+ through an injectable wrapper function so tests can mock the boundary without writing
31
+ temporary files.
32
+ #>
33
+ [CmdletBinding()]
34
+ param()
35
+
36
+ $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
37
+ $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
38
+ $script:EpicModeMarker = 'Epic mode: true'
39
+
40
+ function Get-EpicWaveBarrierCheckpointContent {
41
+ <#
42
+ .SYNOPSIS
43
+ Read the raw JSON text of the epic checkpoint. Tests mock this function
44
+ (read seam).
45
+ .OUTPUTS
46
+ System.String or $null
47
+ #>
48
+ [CmdletBinding()]
49
+ [OutputType([string])]
50
+ param()
51
+
52
+ if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
53
+ return $null
54
+ }
55
+ return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
56
+ }
57
+
58
+ function Find-EpicWaveBarrierFeatureFolderFromPrompt {
59
+ <#
60
+ .SYNOPSIS
61
+ Scans a prompt string for docs/features/active/<...> path tokens and returns the
62
+ longest unique match's basename. Returns $null when no match is found.
63
+ .DESCRIPTION
64
+ Mirrors enforce-prd-feature-before-planner.ps1's
65
+ Find-PrdFeatureFolderFromPrompt technique: forward- or backslash-separated path
66
+ tokens are accepted, the longest match wins, and a .md-suffixed match resolves to
67
+ its parent directory before the basename is extracted.
68
+ .PARAMETER Prompt
69
+ The delegation prompt text under evaluation.
70
+ .OUTPUTS
71
+ System.String or $null
72
+ #>
73
+ [CmdletBinding()]
74
+ [OutputType([string])]
75
+ param(
76
+ [Parameter(Mandatory)]
77
+ [AllowEmptyString()]
78
+ [string] $Prompt
79
+ )
80
+
81
+ if (-not $Prompt) {
82
+ return $null
83
+ }
84
+
85
+ $pattern = 'docs[\\/]+features[\\/]+active[\\/]+[^\s"''`]+'
86
+ $matchList = [regex]::Matches($Prompt, $pattern)
87
+ if ($matchList.Count -eq 0) {
88
+ return $null
89
+ }
90
+
91
+ $unique = @{}
92
+ foreach ($m in $matchList) {
93
+ $normalized = ($m.Value -replace '\\', '/').TrimEnd('/')
94
+ $unique[$normalized] = $true
95
+ }
96
+
97
+ $candidates = @(@($unique.Keys) | Sort-Object -Property Length -Descending)
98
+ $best = $candidates[0]
99
+
100
+ if ($best -match '\.md$') {
101
+ $best = $best -replace '/[^/]+\.md$', ''
102
+ }
103
+
104
+ return ($best -split '/')[-1]
105
+ }
106
+
107
+ function Find-EpicWaveBarrierFeatureRecord {
108
+ <#
109
+ .SYNOPSIS
110
+ Locate the features[] record whose feature_folder equals the target basename.
111
+ .PARAMETER Checkpoint
112
+ Parsed epic checkpoint, or $null when absent/unreadable.
113
+ .PARAMETER FeatureFolder
114
+ The target feature_folder basename.
115
+ .OUTPUTS
116
+ System.Object or $null
117
+ #>
118
+ [CmdletBinding()]
119
+ param(
120
+ [AllowNull()]
121
+ $Checkpoint,
122
+
123
+ [AllowNull()]
124
+ [string] $FeatureFolder
125
+ )
126
+
127
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($FeatureFolder)) {
128
+ return $null
129
+ }
130
+ $checkpointProps = @($Checkpoint.PSObject.Properties.Name)
131
+ if ($checkpointProps -notcontains 'features') {
132
+ return $null
133
+ }
134
+
135
+ # Scan every recorded feature for a feature_folder that equals the target basename.
136
+ foreach ($feature in @($Checkpoint.features)) {
137
+ $featureProps = @($feature.PSObject.Properties.Name)
138
+ if ($featureProps -notcontains 'feature_folder') {
139
+ continue
140
+ }
141
+ if (([string]$feature.feature_folder) -eq $FeatureFolder) {
142
+ return $feature
143
+ }
144
+ }
145
+ return $null
146
+ }
147
+
148
+ function Test-EpicWaveBarrierDependenciesMerged {
149
+ <#
150
+ .SYNOPSIS
151
+ Decision logic: true only when every dependency's merge_status is merged or
152
+ worktree_removed.
153
+ .PARAMETER Checkpoint
154
+ Parsed epic checkpoint, or $null when absent/unreadable.
155
+ .PARAMETER FeatureRecord
156
+ The target feature's own features[] record, or $null when not found.
157
+ .OUTPUTS
158
+ System.Boolean
159
+ #>
160
+ [CmdletBinding()]
161
+ [OutputType([bool])]
162
+ param(
163
+ [AllowNull()]
164
+ $Checkpoint,
165
+
166
+ [AllowNull()]
167
+ $FeatureRecord
168
+ )
169
+
170
+ if ($null -eq $Checkpoint -or $null -eq $FeatureRecord) {
171
+ return $false
172
+ }
173
+ $featureProps = @($FeatureRecord.PSObject.Properties.Name)
174
+ if ($featureProps -notcontains 'depends_on') {
175
+ # No dependencies recorded: an empty/absent depends_on list has nothing to block on.
176
+ return $true
177
+ }
178
+
179
+ $dependsOn = @($FeatureRecord.depends_on)
180
+ if ($dependsOn.Count -eq 0) {
181
+ return $true
182
+ }
183
+
184
+ # Every dependency edge must be durably confirmed merged or worktree_removed before
185
+ # this wave's feature is allowed to start.
186
+ foreach ($dependencyFolder in $dependsOn) {
187
+ $dependencyRecord = Find-EpicWaveBarrierFeatureRecord -Checkpoint $Checkpoint -FeatureFolder ([string]$dependencyFolder)
188
+ if ($null -eq $dependencyRecord) {
189
+ return $false
190
+ }
191
+ $dependencyProps = @($dependencyRecord.PSObject.Properties.Name)
192
+ if ($dependencyProps -notcontains 'merge_status') {
193
+ return $false
194
+ }
195
+ if ($script:AllowedMergeStatuses -notcontains ([string]$dependencyRecord.merge_status)) {
196
+ return $false
197
+ }
198
+ }
199
+ return $true
200
+ }
201
+
202
+ function Get-EpicWaveBarrierAllowDecision {
203
+ [CmdletBinding()]
204
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
205
+ param()
206
+
207
+ return [ordered]@{
208
+ hookSpecificOutput = [ordered]@{
209
+ hookEventName = 'PreToolUse'
210
+ permissionDecision = 'allow'
211
+ }
212
+ }
213
+ }
214
+
215
+ function Get-EpicWaveBarrierBlockDecision {
216
+ [CmdletBinding()]
217
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
218
+ param(
219
+ [Parameter(Mandatory)]
220
+ [string] $Reason
221
+ )
222
+
223
+ return [ordered]@{
224
+ hookSpecificOutput = [ordered]@{
225
+ hookEventName = 'PreToolUse'
226
+ permissionDecision = 'deny'
227
+ permissionDecisionReason = $Reason
228
+ }
229
+ }
230
+ }
231
+
232
+ function Invoke-EpicWaveBarrierDecision {
233
+ <#
234
+ .SYNOPSIS
235
+ Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
236
+ .PARAMETER ToolInputRaw
237
+ The raw JSON tool payload supplied by Claude Code.
238
+ .OUTPUTS
239
+ System.Collections.Specialized.OrderedDictionary
240
+ #>
241
+ [CmdletBinding()]
242
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
243
+ param(
244
+ [string] $ToolInputRaw
245
+ )
246
+
247
+ if (-not $ToolInputRaw) {
248
+ return Get-EpicWaveBarrierAllowDecision
249
+ }
250
+
251
+ try {
252
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
253
+ } catch {
254
+ throw "enforce-epic-wave-barrier hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
255
+ }
256
+
257
+ $subagent = $toolInput.subagent_type
258
+ if (-not $subagent -or $subagent -ne 'orchestrator') {
259
+ return Get-EpicWaveBarrierAllowDecision
260
+ }
261
+
262
+ $prompt = [string]$toolInput.prompt
263
+ if (-not $prompt -or $prompt -notlike "*$script:EpicModeMarker*") {
264
+ return Get-EpicWaveBarrierAllowDecision
265
+ }
266
+
267
+ $featureFolder = Find-EpicWaveBarrierFeatureFolderFromPrompt -Prompt $prompt
268
+ if (-not $featureFolder) {
269
+ return Get-EpicWaveBarrierBlockDecision -Reason 'EPIC_WAVE_BARRIER_BLOCKED: an epic-mode orchestrator delegation must reference the target feature folder in the prompt so its dependency edges can be verified.'
270
+ }
271
+
272
+ $checkpointRaw = Get-EpicWaveBarrierCheckpointContent
273
+ $checkpoint = $null
274
+ if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
275
+ try {
276
+ $checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
277
+ } catch {
278
+ $checkpoint = $null
279
+ }
280
+ }
281
+
282
+ $featureRecord = Find-EpicWaveBarrierFeatureRecord -Checkpoint $checkpoint -FeatureFolder $featureFolder
283
+ if (Test-EpicWaveBarrierDependenciesMerged -Checkpoint $checkpoint -FeatureRecord $featureRecord) {
284
+ return Get-EpicWaveBarrierAllowDecision
285
+ }
286
+
287
+ return Get-EpicWaveBarrierBlockDecision -Reason "EPIC_WAVE_BARRIER_BLOCKED: '$featureFolder' cannot start until every dependency in its depends_on list is durably confirmed merged or worktree_removed in the epic checkpoint. The checkpoint was unreadable, the feature record was not found, or a dependency is not yet safe."
288
+ }
289
+
290
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
291
+ if ($MyInvocation.InvocationName -eq '.') {
292
+ return
293
+ }
294
+
295
+ try {
296
+ $decision = Invoke-EpicWaveBarrierDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
297
+ } catch {
298
+ Write-Error $_
299
+ exit 1
300
+ }
301
+
302
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
303
+
304
+ exit 0