@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.
- package/out/mcp-server.js +896 -202
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +1 -0
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +1 -0
- package/resources/claude-customizations/.claude/agents/commit-message.md +42 -0
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +128 -0
- package/resources/claude-customizations/.claude/agents/epic-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/feature-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/human-exception-runbook.md +48 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +80 -2
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/pr-author.md +37 -21
- package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -0
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/staged-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/status-updater.md +1 -0
- package/resources/claude-customizations/.claude/agents/typescript-engineer.md +1 -0
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +103 -65
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +12 -9
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +24 -16
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +19 -13
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +304 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +304 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +237 -0
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +16 -12
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +14 -10
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +182 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +35 -8
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +17 -14
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +104 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +268 -93
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +19 -11
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +28 -6
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +17 -14
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +146 -36
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +27 -7
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +53 -0
- package/resources/claude-customizations/.claude/settings.json +38 -1
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +252 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +59 -13
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +28 -0
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +22 -18
- package/resources/claude-customizations/pack-manifests/core.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +8 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -3
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +65 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +79 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +66 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +15 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -25
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +144 -28
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +163 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +257 -93
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +98 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +24 -0
- package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +10 -0
- package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +15 -0
- package/resources/codex-and-agents-customizations/pack-manifests/python.json +15 -0
- package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +9 -0
- package/resources/config/orchestration-routing.json +92 -0
- package/resources/customizations/.github/agents/pr-author.agent.md +24 -18
- package/resources/icon.png +0 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
- package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +0 -68
- 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
|