@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.17

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 (58) hide show
  1. package/out/mcp-server.js +639 -27
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +12 -6
  4. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  5. package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
  6. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  11. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  17. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
  18. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  19. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  20. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  23. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  24. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  25. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  26. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  29. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  30. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
  31. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  32. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
  33. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  34. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
  35. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
  36. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  37. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  38. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  39. package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
  40. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
  41. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  42. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  43. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  44. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  45. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  46. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  47. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  48. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  49. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  50. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  51. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  52. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
  53. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  54. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  55. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  56. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  57. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  58. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -18,7 +18,9 @@ tools:
18
18
  - "Bash(bash .claude/lib/bash/compute-cohorts.sh*)"
19
19
  - "Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"
20
20
  - "Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"
21
+ - "Bash(bash .claude/lib/bash/abandon-parallel-item.sh*)"
21
22
  - "Bash(pwsh -NoProfile -File .claude/lib/project-file-merge/Resolve-MergeableConflict.ps1*)"
23
+ - "Bash(pwsh -NoProfile -NonInteractive -File .claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1*)"
22
24
  - "Bash(dotnet tool restore*)"
23
25
  - "Bash(dotnet csharpier check *)"
24
26
  - "Bash(dotnet build *)"
@@ -94,12 +96,16 @@ batching use `compute-cohorts.sh` and `compute-concurrency-batches.sh` under the
94
96
  entry.
95
97
 
96
98
  The `poetry run` grants remain for the repository-local paths that still need an interpreter. The
97
- skill's `## Parallel-Level Checkpoint` section now validates through
98
- `mcp__drm-copilot__validate_orchestration_artifacts`, so exactly one named consumer is left: the
99
- drift-detection CLI, invoked as `poetry run python -m`. Each grant stays scoped to its own
100
- `poetry run python` invocation form — not to `poetry run` as a whole — so `pytest`, `black`,
101
- `ruff`, and every other `poetry run` subcommand remain outside the allowlist. The sibling persona
102
- `.claude/agents/parallel-planner.md` records the same destination-runtime posture.
99
+ skill's `## Parallel-Level Checkpoint` section validates through
100
+ `mcp__drm-copilot__validate_orchestration_artifacts`, and since issue #763 radius drift detection runs
101
+ through the destination-runtime PowerShell entry point
102
+ `.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1` and the abandon disposition through the
103
+ bundled entry point `.claude/lib/bash/abandon-parallel-item.sh`, so no skill step names a
104
+ `poetry run` consumer. The two `poetry run python` grants are left unchanged pending a separate
105
+ removal decision. Each grant stays scoped to its own `poetry run python` invocation form — not to
106
+ `poetry run` as a whole — so `pytest`, `black`, `ruff`, and every other `poetry run` subcommand
107
+ remain outside the allowlist. The sibling persona `.claude/agents/parallel-planner.md` records the
108
+ same destination-runtime posture.
103
109
 
104
110
  ## Startup Protocol
105
111
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: python-typed-engineer
3
3
  model: sonnet
4
- description: Project-scoped worker that implements and verifies Python changes within typed repository boundaries. Applies the Black -> Ruff -> Pyright -> Pytest toolchain, the 3-production + 3-test per-batch budget, and zero-regression quality gates.
4
+ description: Project-scoped worker that implements and verifies Python changes within typed repository boundaries. Applies the Black -> Ruff -> Pyright -> Pytest toolchain, the 1-3 production-file direct-mode budget with routing to the orchestrated large path above it, and zero-regression quality gates.
5
5
  tools:
6
6
  - Read
7
7
  - Write
@@ -37,7 +37,7 @@ Language standards and toolchain are defined in `.claude/rules/python.md` and `.
37
37
  Follow the phased workflow defined by the preloaded skills:
38
38
 
39
39
  1. **Policy compliance** — apply `policy-compliance-order` to load mandatory repo policies before any change.
40
- 2. **Routing and scope** — apply `python-change-budget-router` to estimate scope, select small vs large path, and enforce the 3 production + 3 test per-batch cap.
40
+ 2. **Routing and scope** — apply `python-change-budget-router` to estimate scope and select direct mode (1-3 production files) or, for more than 3 production files, large-path escalation through `/orchestrate`. The large path has no production-file cap, and test files are not counted toward the routing threshold.
41
41
  3. **Plan and baseline** — apply `atomic-plan-contract` for Phase 0 baseline capture and atomic plan structure. Delegate plan authoring to `atomic_planner` when no plan is supplied.
42
42
  4. **Implement in batches** — apply the approved plan. After each batch, run targeted Ruff and Pyright on touched files plus targeted Pytest, and confirm per-file coverage.
43
43
  5. **Final QA gate** — apply `python-qa-gate` to run the full toolchain, enforce zero-regression deltas against the baseline, and produce the required reporting block before declaring completion.
@@ -58,7 +58,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
58
58
 
59
59
  Stop implementation and return to the user when:
60
60
 
61
- - the scope estimate or an in-flight batch would exceed the 3-production-file cap,
61
+ - the scope estimate exceeds 3 production files in direct mode,
62
62
  - a file is near or would exceed the 500-line limit,
63
63
  - any QA gate delta is non-zero after self-correction,
64
64
  - the toolchain cannot be executed in the current environment (mark the change **unverified**),
@@ -1,19 +1,21 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Pure route helpers for the PowerShell batch-budget hook.
3
+ Pure route helpers shared by the batch-budget hooks.
4
4
 
5
5
  .DESCRIPTION
6
6
  Defines the functions that read the selected route from orchestrator checkpoint
7
7
  text and decide whether that checkpoint selects the uncapped orchestrated large
8
- path. The file is dot-sourced by enforce-powershell-batch-budget.ps1 and has no
9
- entry point. None of the functions reads a file or an environment variable; the
10
- checkpoint text is supplied by the caller.
8
+ path. The file is dot-sourced by the PowerShell and Python batch-budget hooks and
9
+ has no entry point. The Claude and Codex runtime copies of this file are
10
+ byte-identical, and a parity test holds them identical. None of the functions
11
+ reads a file or an environment variable; the checkpoint text is supplied by the
12
+ caller.
11
13
 
12
14
  .NOTES
13
15
  Compatible with PowerShell 7+.
14
16
  #>
15
17
 
16
- function ConvertFrom-PowerShellBatchBudgetCheckpoint {
18
+ function ConvertFrom-BatchBudgetCheckpoint {
17
19
  <#
18
20
  .SYNOPSIS
19
21
  Parses orchestrator checkpoint text into a checkpoint object.
@@ -50,7 +52,7 @@ function ConvertFrom-PowerShellBatchBudgetCheckpoint {
50
52
  return $null
51
53
  }
52
54
 
53
- function Get-PowerShellBatchBudgetSelectedRoute {
55
+ function Get-BatchBudgetSelectedRoute {
54
56
  <#
55
57
  .SYNOPSIS
56
58
  Returns the route selected by an orchestrator checkpoint.
@@ -69,7 +71,7 @@ function Get-PowerShellBatchBudgetSelectedRoute {
69
71
  [string] $CheckpointText
70
72
  )
71
73
 
72
- $checkpoint = ConvertFrom-PowerShellBatchBudgetCheckpoint -CheckpointText $CheckpointText
74
+ $checkpoint = ConvertFrom-BatchBudgetCheckpoint -CheckpointText $CheckpointText
73
75
  if ($null -eq $checkpoint) {
74
76
  return ''
75
77
  }
@@ -89,7 +91,7 @@ function Get-PowerShellBatchBudgetSelectedRoute {
89
91
  return ''
90
92
  }
91
93
 
92
- function Test-PowerShellBatchBudgetLargePathRoute {
94
+ function Test-BatchBudgetLargePathRoute {
93
95
  <#
94
96
  .SYNOPSIS
95
97
  Reports whether an orchestrator checkpoint selects the uncapped large path.
@@ -110,12 +112,12 @@ function Test-PowerShellBatchBudgetLargePathRoute {
110
112
  )
111
113
 
112
114
  try {
113
- $checkpoint = ConvertFrom-PowerShellBatchBudgetCheckpoint -CheckpointText $CheckpointText
115
+ $checkpoint = ConvertFrom-BatchBudgetCheckpoint -CheckpointText $CheckpointText
114
116
  if ($null -eq $checkpoint) {
115
117
  return $false
116
118
  }
117
119
 
118
- $route = Get-PowerShellBatchBudgetSelectedRoute -CheckpointText $CheckpointText
120
+ $route = Get-BatchBudgetSelectedRoute -CheckpointText $CheckpointText
119
121
  if (-not (@('large', 'remediation', 'preparation') -ccontains $route)) {
120
122
  return $false
121
123
  }
@@ -0,0 +1,221 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Checkpoint read seams and run-target resolution for the epic merge gate (issue #690).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-epic-merge-gate.ps1 immediately after the authorization sibling.
7
+ Holds:
8
+
9
+ - The import guard for WorktreeRunResolution.psm1. A failed import is recorded in
10
+ $script:EpicMergeGateResolutionImportFailure, and
11
+ Get-EpicMergeGateImportFailureDecision turns it into a deny, so a missing module
12
+ cannot make the gate exit non-zero and fail open.
13
+ - The three checkpoint read seams, relocated from the gate file with their names kept.
14
+ Each takes a mandatory absolute path composed beneath a resolved worktree root.
15
+ - The session worktree root, used for the child branch and for a bare command.
16
+ - The resolution seam Resolve-EpicMergeGateRunTarget, which locates the epic or
17
+ parallel checkpoint that records the command's pull request number.
18
+ - Test-ChildCheckpointPrGateBinding, which binds the command's pull request number to
19
+ the per-feature checkpoint's pr_gate.pr_number when that field is recorded.
20
+ - Get-EpicMergeGateUnresolvedReason, the reason prefix used when neither run branch
21
+ resolves.
22
+
23
+ .NOTES
24
+ PowerShell 7+. Depends on Get-EpicMergeGateBlockDecision from the authorization
25
+ sibling, dot-sourced by the gate before this file. Mirrored byte-identically under
26
+ extensions/drm-copilot/resources/claude-customizations/.
27
+ #>
28
+
29
+ # Import guard (issue #690): a failed import denies instead of failing open.
30
+ $script:EpicMergeGateResolutionImportFailure = $null
31
+ try {
32
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
33
+ }
34
+ catch {
35
+ $script:EpicMergeGateResolutionImportFailure = 'WorktreeRunResolution.psm1'
36
+ }
37
+
38
+ function Get-EpicMergeGateImportFailureDecision {
39
+ <#
40
+ .SYNOPSIS
41
+ Returns the deny for a failed worktree-resolution import, or $null.
42
+ .OUTPUTS
43
+ System.Collections.Specialized.OrderedDictionary, or $null when the import succeeded.
44
+ #>
45
+ [CmdletBinding()]
46
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
47
+ param()
48
+
49
+ if (-not $script:EpicMergeGateResolutionImportFailure) {
50
+ return $null
51
+ }
52
+ return Get-EpicMergeGateBlockDecision -Reason (
53
+ "EPIC_MERGE_GATE_BLOCKED: the worktree-resolution module '$($script:EpicMergeGateResolutionImportFailure)' " +
54
+ 'failed to import, so the run checkpoint that governs this merge cannot be located; the gate fails closed.')
55
+ }
56
+
57
+ function Get-ChildOrchestratorCheckpointContent {
58
+ <#
59
+ .SYNOPSIS
60
+ Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock
61
+ this function (read seam).
62
+ .PARAMETER Path
63
+ The absolute checkpoint path composed beneath the session worktree root.
64
+ .OUTPUTS
65
+ System.String or $null
66
+ #>
67
+ [CmdletBinding()]
68
+ [OutputType([string])]
69
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
70
+
71
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
72
+ return $null
73
+ }
74
+ return (Get-Content -LiteralPath $Path -Raw)
75
+ }
76
+
77
+ function Get-EpicOrchestratorCheckpointContent {
78
+ <#
79
+ .SYNOPSIS
80
+ Read the raw JSON text of the epic checkpoint. Tests mock this function
81
+ (read seam).
82
+ .PARAMETER Path
83
+ The absolute checkpoint path composed beneath the resolved worktree root.
84
+ .OUTPUTS
85
+ System.String or $null
86
+ #>
87
+ [CmdletBinding()]
88
+ [OutputType([string])]
89
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
90
+
91
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
92
+ return $null
93
+ }
94
+ return (Get-Content -LiteralPath $Path -Raw)
95
+ }
96
+
97
+ function Get-ParallelOrchestratorCheckpointContent {
98
+ <#
99
+ .SYNOPSIS
100
+ Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
101
+ this function (read seam).
102
+ .PARAMETER Path
103
+ The absolute checkpoint path composed beneath the resolved worktree root.
104
+ .OUTPUTS
105
+ System.String or $null
106
+ #>
107
+ [CmdletBinding()]
108
+ [OutputType([string])]
109
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
110
+
111
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
112
+ return $null
113
+ }
114
+ return (Get-Content -LiteralPath $Path -Raw)
115
+ }
116
+
117
+ function Get-EpicMergeGateSessionWorktreeRoot {
118
+ <#
119
+ .SYNOPSIS
120
+ Return the worktree root the session runs in (the child branch and bare-command root).
121
+ .OUTPUTS
122
+ System.String
123
+ #>
124
+ [CmdletBinding()]
125
+ [OutputType([string])]
126
+ param()
127
+
128
+ return (Resolve-WorktreeOperandTarget -Path '' -SessionRoot (Get-Location).Path).WorktreeRoot
129
+ }
130
+
131
+ function Resolve-EpicMergeGateRunTarget {
132
+ <#
133
+ .SYNOPSIS
134
+ Resolve the worktree whose run checkpoint records a pull request number (issue #690 seam).
135
+ .PARAMETER Kind
136
+ epic or parallel.
137
+ .PARAMETER PrNumber
138
+ The explicit pull request number the command names.
139
+ .OUTPUTS
140
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
141
+ #>
142
+ [CmdletBinding()]
143
+ [OutputType([pscustomobject])]
144
+ param(
145
+ [Parameter(Mandatory)][ValidateSet('epic', 'parallel')][string] $Kind,
146
+ [Parameter(Mandatory)][int] $PrNumber
147
+ )
148
+
149
+ return Resolve-WorktreeRunTargetByRecord -Kind $Kind -RecordField pr_number -Value ([string]$PrNumber) -SessionRoot (Get-Location).Path
150
+ }
151
+
152
+ function Test-ChildCheckpointPrGateBinding {
153
+ <#
154
+ .SYNOPSIS
155
+ True unless the per-feature checkpoint records a pr_gate.pr_number that differs from the command's.
156
+ .DESCRIPTION
157
+ A bare command, an absent checkpoint, and a checkpoint without pr_gate.pr_number
158
+ keep today's unbound child-branch decision. When both numbers are present they must
159
+ parse as integers and be equal.
160
+ .PARAMETER Checkpoint
161
+ Parsed per-feature checkpoint, or $null.
162
+ .PARAMETER CommandPrNumber
163
+ The explicit pull request number the command names, or $null.
164
+ .OUTPUTS
165
+ System.Boolean
166
+ #>
167
+ [CmdletBinding()]
168
+ [OutputType([bool])]
169
+ param(
170
+ [AllowNull()] $Checkpoint,
171
+ [AllowNull()][Nullable[int]] $CommandPrNumber
172
+ )
173
+
174
+ if ($null -eq $CommandPrNumber -or $null -eq $Checkpoint) {
175
+ return $true
176
+ }
177
+ if (@($Checkpoint.PSObject.Properties.Name) -notcontains 'pr_gate' -or $null -eq $Checkpoint.pr_gate) {
178
+ return $true
179
+ }
180
+ $prGate = $Checkpoint.pr_gate
181
+ if (@($prGate.PSObject.Properties.Name) -notcontains 'pr_number' -or $null -eq $prGate.pr_number) {
182
+ return $true
183
+ }
184
+ $recorded = 0
185
+ if (-not [int]::TryParse([string]$prGate.pr_number, [ref] $recorded)) {
186
+ return $false
187
+ }
188
+ return ($recorded -eq $CommandPrNumber)
189
+ }
190
+
191
+ function Get-EpicMergeGateUnresolvedReason {
192
+ <#
193
+ .SYNOPSIS
194
+ Returns '<code>: <epic detail>; <parallel detail>' when neither run branch resolved, or $null.
195
+ .DESCRIPTION
196
+ The ambiguity code wins when either target is Ambiguous. Returns $null when either
197
+ target is absent (a bare command) or either target resolved.
198
+ .PARAMETER EpicTarget
199
+ The epic run target, or $null.
200
+ .PARAMETER ParallelTarget
201
+ The parallel run target, or $null.
202
+ .OUTPUTS
203
+ System.String or $null
204
+ #>
205
+ [CmdletBinding()]
206
+ [OutputType([string])]
207
+ param(
208
+ [AllowNull()] $EpicTarget,
209
+ [AllowNull()] $ParallelTarget
210
+ )
211
+
212
+ if ($null -eq $EpicTarget -or $null -eq $ParallelTarget) {
213
+ return $null
214
+ }
215
+ $unresolved = @('NoTarget', 'Ambiguous')
216
+ if ($unresolved -notcontains $EpicTarget.Status -or $unresolved -notcontains $ParallelTarget.Status) {
217
+ return $null
218
+ }
219
+ $code = if ($EpicTarget.Status -eq 'Ambiguous') { $EpicTarget.ReasonCode } elseif ($ParallelTarget.Status -eq 'Ambiguous') { $ParallelTarget.ReasonCode } else { $EpicTarget.ReasonCode }
220
+ return ('{0}: {1}; {2}' -f $code, $EpicTarget.Detail, $ParallelTarget.Detail)
221
+ }
@@ -38,10 +38,16 @@
38
38
  policy-level-not-cryptographic posture already accepted for
39
39
  enforce-pr-author-skill.ps1's own receipt mechanism. It is not a cryptographic control.
40
40
 
41
+ Issue #690: with an explicit PR number, the epic and parallel checkpoints are read
42
+ beneath the live worktree whose run checkpoint records that number, and the child
43
+ branch binds the number to pr_gate.pr_number when the per-feature checkpoint records
44
+ it. A bare command reads every checkpoint beneath the session worktree.
45
+
41
46
  .NOTES
42
- Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
43
- through injectable wrapper functions so tests can mock the boundary without writing
44
- temporary files.
47
+ Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 through the
48
+ dot-sourced enforce-epic-merge-gate-resolution.ps1, whose import guard denies on a
49
+ failed import. Filesystem reads go through injectable wrapper functions so tests can
50
+ mock the boundary without writing temporary files.
45
51
 
46
52
  Honest disclosure: the standalone-merge authorization record is a policy-level,
47
53
  auditable declaration and is not a cryptographic or security control. authorized_by is
@@ -59,64 +65,8 @@ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -
59
65
  . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
60
66
  # Standalone-merge authorization predicates and the two decision-envelope factories (issue #670).
61
67
  . (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-authorization.ps1')
62
-
63
- $script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
64
- $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
65
- $script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
66
-
67
- function Get-ChildOrchestratorCheckpointContent {
68
- <#
69
- .SYNOPSIS
70
- Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock
71
- this function (read seam).
72
- .OUTPUTS
73
- System.String or $null
74
- #>
75
- [CmdletBinding()]
76
- [OutputType([string])]
77
- param()
78
-
79
- if (-not (Test-Path -LiteralPath $script:ChildCheckpointPath -PathType Leaf)) {
80
- return $null
81
- }
82
- return (Get-Content -LiteralPath $script:ChildCheckpointPath -Raw)
83
- }
84
-
85
- function Get-EpicOrchestratorCheckpointContent {
86
- <#
87
- .SYNOPSIS
88
- Read the raw JSON text of the epic checkpoint. Tests mock this function
89
- (read seam).
90
- .OUTPUTS
91
- System.String or $null
92
- #>
93
- [CmdletBinding()]
94
- [OutputType([string])]
95
- param()
96
-
97
- if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
98
- return $null
99
- }
100
- return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
101
- }
102
-
103
- function Get-ParallelOrchestratorCheckpointContent {
104
- <#
105
- .SYNOPSIS
106
- Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
107
- this function (read seam).
108
- .OUTPUTS
109
- System.String or $null
110
- #>
111
- [CmdletBinding()]
112
- [OutputType([string])]
113
- param()
114
-
115
- if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
116
- return $null
117
- }
118
- return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
119
- }
68
+ # Checkpoint read seams, the import guard, and run-target resolution (issue #690).
69
+ . (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-resolution.ps1')
120
70
 
121
71
  function ConvertFrom-EpicMergeGateJson {
122
72
  <#
@@ -361,6 +311,12 @@ function Invoke-EpicMergeGateDecision {
361
311
  [string] $ToolInputRaw
362
312
  )
363
313
 
314
+ # A failed worktree-resolution import denies before any other logic (issue #690).
315
+ $importFailure = Get-EpicMergeGateImportFailureDecision
316
+ if ($null -ne $importFailure) {
317
+ return $importFailure
318
+ }
319
+
364
320
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
365
321
  if (-not $payload.IsValid) {
366
322
  return Get-EpicMergeGateBlockDecision -Reason (
@@ -399,18 +355,35 @@ function Invoke-EpicMergeGateDecision {
399
355
  }
400
356
 
401
357
  $commandPrNumber = Get-EpicMergeGateCommandPrNumber -CommandText $commandText
358
+ $sessionRoot = Get-EpicMergeGateSessionWorktreeRoot
402
359
 
403
- $childCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ChildOrchestratorCheckpointContent)
404
- if (Test-ChildCheckpointAllowsEpicMerge -Checkpoint $childCheckpoint) {
360
+ # The child merges from its own worktree, so its checkpoint is the session worktree's;
361
+ # a recorded pr_gate.pr_number must match an explicit command PR number (issue #690).
362
+ $childCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ChildOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind item -WorktreeRoot $sessionRoot))
363
+ if ((Test-ChildCheckpointAllowsEpicMerge -Checkpoint $childCheckpoint) -and
364
+ (Test-ChildCheckpointPrGateBinding -Checkpoint $childCheckpoint -CommandPrNumber $commandPrNumber)) {
405
365
  return Get-EpicMergeGateAllowDecision
406
366
  }
407
367
 
408
- $epicCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-EpicOrchestratorCheckpointContent)
368
+ # A bare command targets the current branch's PR, so it reads the session worktree. An
369
+ # explicit PR number locates each run checkpoint that records it (issue #690).
370
+ $epicTarget = $null
371
+ $parallelTarget = $null
372
+ $epicRoot = $sessionRoot
373
+ $parallelRoot = $sessionRoot
374
+ if ($null -ne $commandPrNumber) {
375
+ $epicTarget = Resolve-EpicMergeGateRunTarget -Kind epic -PrNumber $commandPrNumber
376
+ $parallelTarget = Resolve-EpicMergeGateRunTarget -Kind parallel -PrNumber $commandPrNumber
377
+ $epicRoot = if (@('SessionRoot', 'OtherWorktree') -contains $epicTarget.Status) { $epicTarget.WorktreeRoot } else { $null }
378
+ $parallelRoot = if (@('SessionRoot', 'OtherWorktree') -contains $parallelTarget.Status) { $parallelTarget.WorktreeRoot } else { $null }
379
+ }
380
+
381
+ $epicCheckpoint = if ($epicRoot) { ConvertFrom-EpicMergeGateJson -Raw (Get-EpicOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind epic -WorktreeRoot $epicRoot)) } else { $null }
409
382
  if (Test-EpicCheckpointAllowsMerge -Checkpoint $epicCheckpoint -CommandPrNumber $commandPrNumber) {
410
383
  return Get-EpicMergeGateAllowDecision
411
384
  }
412
385
 
413
- $parallelCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ParallelOrchestratorCheckpointContent)
386
+ $parallelCheckpoint = if ($parallelRoot) { ConvertFrom-EpicMergeGateJson -Raw (Get-ParallelOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind parallel -WorktreeRoot $parallelRoot)) } else { $null }
414
387
  if (Test-ParallelCheckpointAllowsMerge -Checkpoint $parallelCheckpoint -CommandPrNumber $commandPrNumber) {
415
388
  return Get-EpicMergeGateAllowDecision
416
389
  }
@@ -423,7 +396,10 @@ function Invoke-EpicMergeGateDecision {
423
396
  if ($standalone.Allowed) {
424
397
  return Get-EpicMergeGateAllowDecision
425
398
  }
426
- return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $standalone.ReasonCode + ': ' + $standalone.Message)
399
+ # When neither run branch resolved, its reason code and detail precede the standalone reason.
400
+ $unresolved = Get-EpicMergeGateUnresolvedReason -EpicTarget $epicTarget -ParallelTarget $parallelTarget
401
+ $prefix = if ($unresolved) { $unresolved + '; ' } else { '' }
402
+ return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $prefix + $standalone.ReasonCode + ': ' + $standalone.Message)
427
403
  }
428
404
 
429
405
  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", an epic checkpoint with epic_merge_pr.ci_gate.conclusion == "success" and a matching pr_number, or a parallel-orchestrator checkpoint with route_id == "parallel" whose target item (matched by pr_number) has merge_status == "ci_green". No checkpoint satisfied this gate.'
@@ -12,8 +12,11 @@
12
12
  docs/features/active/<token> path, mirroring
13
13
  enforce-prd-feature-before-planner.ps1's Find-PrdFeatureFolderFromPrompt
14
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.
15
+ 2. Resolve the worktree whose epic checkpoint records the prompt's
16
+ integration_branch: value (issue #690), deny an unresolved or ambiguous target,
17
+ then read artifacts/orchestration/epic-orchestrator-state.json beneath that
18
+ worktree and locate the features[] record whose feature_folder equals the
19
+ resolved basename.
17
20
  3. Look up that feature's depends_on list, and for every dependency, locate its own
18
21
  features[] record.
19
22
  4. Deny with reason EPIC_WAVE_BARRIER_BLOCKED unless every dependency's merge_status
@@ -26,16 +29,26 @@
26
29
  SubagentStop time.
27
30
 
28
31
  .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
+ Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 (issue #690),
33
+ imported inside a guard: a failed import is recorded and the decision denies naming
34
+ the module. Filesystem reads go through an injectable wrapper function so tests can
35
+ mock the boundary without writing temporary files.
32
36
  #>
33
37
  [CmdletBinding()]
34
38
  param()
35
39
 
36
40
 
37
41
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
38
- $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
42
+
43
+ # Import guard (issue #690): a failed import denies instead of failing open.
44
+ $script:EpicWaveBarrierResolutionImportFailure = $null
45
+ try {
46
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
47
+ }
48
+ catch {
49
+ $script:EpicWaveBarrierResolutionImportFailure = 'WorktreeRunResolution.psm1'
50
+ }
51
+
39
52
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
40
53
  $script:EpicModeMarker = 'Epic mode: true'
41
54
 
@@ -44,17 +57,39 @@ function Get-EpicWaveBarrierCheckpointContent {
44
57
  .SYNOPSIS
45
58
  Read the raw JSON text of the epic checkpoint. Tests mock this function
46
59
  (read seam).
60
+ .PARAMETER Path
61
+ The absolute checkpoint path composed beneath the resolved worktree root.
47
62
  .OUTPUTS
48
63
  System.String or $null
49
64
  #>
50
65
  [CmdletBinding()]
51
66
  [OutputType([string])]
52
- param()
67
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
53
68
 
54
- if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
69
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
55
70
  return $null
56
71
  }
57
- return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
72
+ return (Get-Content -LiteralPath $Path -Raw)
73
+ }
74
+
75
+ function Resolve-EpicWaveBarrierTarget {
76
+ <#
77
+ .SYNOPSIS
78
+ Resolve the worktree whose epic checkpoint governs a kickoff (issue #690 seam).
79
+ .DESCRIPTION
80
+ Keyed on the prompt's integration_branch: value, with epic_feature_folder: as a
81
+ cross-check; never on a feature-folder path or the payload cwd.
82
+ .PARAMETER Prompt
83
+ The delegation prompt text.
84
+ .OUTPUTS
85
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
86
+ #>
87
+ [CmdletBinding()]
88
+ [OutputType([pscustomobject])]
89
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Prompt)
90
+
91
+ $signal = Find-WorktreeRunIdentitySignal -Text $Prompt
92
+ return Resolve-WorktreeEpicTarget -IntegrationBranch $signal.IntegrationBranch -EpicSlug $signal.EpicSlug -SessionRoot (Get-Location).Path
58
93
  }
59
94
 
60
95
  function Find-EpicWaveBarrierFeatureFolderFromPrompt {
@@ -246,6 +281,13 @@ function Invoke-EpicWaveBarrierDecision {
246
281
  [string] $ToolInputRaw
247
282
  )
248
283
 
284
+ # A failed worktree-resolution import denies before any other logic (issue #690).
285
+ if ($script:EpicWaveBarrierResolutionImportFailure) {
286
+ return Get-EpicWaveBarrierBlockDecision -Reason (
287
+ "EPIC_WAVE_BARRIER_BLOCKED: the worktree-resolution module '$($script:EpicWaveBarrierResolutionImportFailure)' " +
288
+ 'failed to import, so the epic checkpoint that governs this delegation cannot be located; the gate fails closed.')
289
+ }
290
+
249
291
  $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
250
292
  if (-not $envelope.IsValid) {
251
293
  return Get-EpicWaveBarrierBlockDecision -Reason (
@@ -264,12 +306,18 @@ function Invoke-EpicWaveBarrierDecision {
264
306
  return Get-EpicWaveBarrierAllowDecision
265
307
  }
266
308
 
309
+ # The epic checkpoint is located by the kickoff's integration branch (issue #690).
310
+ $target = Resolve-EpicWaveBarrierTarget -Prompt $prompt
311
+ if ($target.Status -eq 'NoTarget' -or $target.Status -eq 'Ambiguous') {
312
+ return Get-EpicWaveBarrierBlockDecision -Reason "EPIC_WAVE_BARRIER_BLOCKED: $($target.ReasonCode): $($target.Detail)"
313
+ }
314
+
267
315
  $featureFolder = Find-EpicWaveBarrierFeatureFolderFromPrompt -Prompt $prompt
268
316
  if (-not $featureFolder) {
269
317
  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
318
  }
271
319
 
272
- $checkpointRaw = Get-EpicWaveBarrierCheckpointContent
320
+ $checkpointRaw = Get-EpicWaveBarrierCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind epic -WorktreeRoot $target.WorktreeRoot)
273
321
  $checkpoint = $null
274
322
  if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
275
323
  try {