@danmoisan/drm-copilot-mcp 1.1.15 → 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 (71) 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/powershell-typed-engineer.md +4 -5
  5. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  6. package/resources/claude-customizations/.claude/hooks/enforce-batch-budget-route.ps1 +139 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
  19. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  20. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  24. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  25. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  26. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  30. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  31. package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
  32. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
  33. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  34. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +7 -4
  35. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  36. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
  37. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
  38. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  39. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
  40. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  41. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  42. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -2
  43. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  44. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
  45. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  46. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
  47. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
  48. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
  49. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
  50. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
  51. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
  52. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  53. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  54. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  55. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  56. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  57. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  58. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  59. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +165 -80
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  63. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  64. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
  65. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
  66. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  67. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  68. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
  69. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  70. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
  71. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.15",
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: powershell-typed-engineer
3
3
  model: sonnet
4
- description: Project-scoped worker that implements and verifies PowerShell changes within typed repository boundaries. Applies PoshQC format -> PSScriptAnalyzer -> Pester toolchain, the 1-2 production-file direct-mode budget, the 3-production + 3-test per-batch cap, and zero-regression quality gates.
4
+ description: Project-scoped worker that implements and verifies PowerShell changes within typed repository boundaries. Applies PoshQC format -> PSScriptAnalyzer -> Pester 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
  - Grep
@@ -36,7 +36,7 @@ Language standards and toolchain are defined in `.claude/rules/powershell.md` an
36
36
  Follow the phased workflow defined by the preloaded skills:
37
37
 
38
38
  1. **Policy compliance** — apply `policy-compliance-order` to load mandatory repo policies before any change.
39
- 2. **Routing and scope** — apply `powershell-change-budget-router` to estimate scope and select direct mode (1-2 production files) vs `powershell-orchestrator` escalation. Enforce the 3 production + 3 test per-batch cap in all modes.
39
+ 2. **Routing and scope** — apply `powershell-change-budget-router` to estimate scope and select direct mode (1-3 production files) vs large-path escalation through `/orchestrate`. The large path has no production-file cap.
40
40
  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. Plans must include the proposed script or module structure, minimal DI seams (wrapper > delegate > adapter), Pester scenario-level test strategy, and the external executable wrapper mock strategy.
41
41
  4. **Implement in batches** — apply the approved plan. After each batch, run targeted PSScriptAnalyzer on touched files plus targeted Pester, and confirm per-file coverage.
42
42
  5. **Final QA gate** — apply `powershell-qa-gate` to run the full toolchain, enforce zero-regression deltas against the baseline, and produce the required reporting block before declaring completion.
@@ -46,7 +46,7 @@ For long-running orchestrated runs, apply `powershell-orchestration-state-machin
46
46
 
47
47
  ## Invocation Modes
48
48
 
49
- - **Direct mode** (default, no directive present): strict 1-2 production PowerShell files cap. If the estimated scope exceeds 2 production files, stop and instruct the caller to invoke `powershell-orchestrator` per `powershell-change-budget-router`.
49
+ - **Direct mode** (default, no directive present): strict 1-3 production PowerShell files cap. If the estimated scope exceeds 3 production files, stop and instruct the caller to invoke `/orchestrate` per `powershell-change-budget-router`.
50
50
  - **Orchestrator handoff mode** (request contains the exact line `DIRECTIVE: ORCHESTRATOR HANDOFF MODE`): overall production-file cap is lifted, but execution requires a complete context package (`objective`, `promotion-type`, `issue-num`, `feature-folder`, `issue.md`, `spec.md`, `user-story.md` or `NONE`, research artifact paths, constraints). In this mode the agent is routing/planning-only until `atomic_planner` returns `PREFLIGHT: ALL CLEAR` from the `atomic-executor` validation loop; all implementation and QA execution must occur via delegated `atomic-executor` handoffs.
51
51
 
52
52
  ## Mode Marker Resolution
@@ -64,8 +64,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
64
64
 
65
65
  Stop implementation and return to the user when:
66
66
 
67
- - the scope estimate exceeds the 2-production-file cap in direct mode,
68
- - an in-flight batch would exceed the 3-production-file or 3-test-file per-batch cap,
67
+ - the scope estimate exceeds the 3-production-file cap in direct mode,
69
68
  - a file is near or would exceed the 500-line limit,
70
69
  - any QA gate delta is non-zero after self-correction,
71
70
  - the toolchain cannot be executed in the current environment (mark the change **unverified**),
@@ -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**),
@@ -0,0 +1,139 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure route helpers shared by the batch-budget hooks.
4
+
5
+ .DESCRIPTION
6
+ Defines the functions that read the selected route from orchestrator checkpoint
7
+ text and decide whether that checkpoint selects the uncapped orchestrated large
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.
13
+
14
+ .NOTES
15
+ Compatible with PowerShell 7+.
16
+ #>
17
+
18
+ function ConvertFrom-BatchBudgetCheckpoint {
19
+ <#
20
+ .SYNOPSIS
21
+ Parses orchestrator checkpoint text into a checkpoint object.
22
+ .DESCRIPTION
23
+ Returns the parsed object only when the text is a JSON object. Null or
24
+ whitespace text, text that is not parseable JSON, and JSON whose root is not
25
+ an object all return $null. Performs no file or environment access.
26
+ .PARAMETER CheckpointText
27
+ Raw text of the orchestrator checkpoint file.
28
+ #>
29
+ [CmdletBinding()]
30
+ [OutputType([psobject])]
31
+ param(
32
+ [AllowNull()]
33
+ [AllowEmptyString()]
34
+ [string] $CheckpointText
35
+ )
36
+
37
+ if ([string]::IsNullOrWhiteSpace($CheckpointText)) {
38
+ return $null
39
+ }
40
+
41
+ try {
42
+ $parsed = ConvertFrom-Json -InputObject $CheckpointText -ErrorAction Stop
43
+ } catch {
44
+ Write-Verbose "Ignoring unparseable orchestrator checkpoint text: $($_.Exception.Message)"
45
+ return $null
46
+ }
47
+
48
+ if ($parsed -is [System.Management.Automation.PSCustomObject]) {
49
+ return $parsed
50
+ }
51
+
52
+ return $null
53
+ }
54
+
55
+ function Get-BatchBudgetSelectedRoute {
56
+ <#
57
+ .SYNOPSIS
58
+ Returns the route selected by an orchestrator checkpoint.
59
+ .DESCRIPTION
60
+ Uses route_id when the checkpoint has that key, otherwise path_selected. The
61
+ value is returned only when it is a non-blank string; every other case,
62
+ including unparseable text, returns an empty string.
63
+ .PARAMETER CheckpointText
64
+ Raw text of the orchestrator checkpoint file.
65
+ #>
66
+ [CmdletBinding()]
67
+ [OutputType([string])]
68
+ param(
69
+ [AllowNull()]
70
+ [AllowEmptyString()]
71
+ [string] $CheckpointText
72
+ )
73
+
74
+ $checkpoint = ConvertFrom-BatchBudgetCheckpoint -CheckpointText $CheckpointText
75
+ if ($null -eq $checkpoint) {
76
+ return ''
77
+ }
78
+
79
+ $propertyNames = @($checkpoint.PSObject.Properties.Name)
80
+ $rawRoute = $null
81
+ if ($propertyNames -contains 'route_id') {
82
+ $rawRoute = $checkpoint.route_id
83
+ } elseif ($propertyNames -contains 'path_selected') {
84
+ $rawRoute = $checkpoint.path_selected
85
+ }
86
+
87
+ if ($rawRoute -is [string] -and -not [string]::IsNullOrWhiteSpace($rawRoute)) {
88
+ return $rawRoute
89
+ }
90
+
91
+ return ''
92
+ }
93
+
94
+ function Test-BatchBudgetLargePathRoute {
95
+ <#
96
+ .SYNOPSIS
97
+ Reports whether an orchestrator checkpoint selects the uncapped large path.
98
+ .DESCRIPTION
99
+ Returns $true only when the checkpoint parses to an object, its selected route
100
+ is large, remediation, or preparation (case-sensitive), next_step is not
101
+ complete, and completed_steps does not contain S12_complete. No input causes
102
+ an exception; any failure returns $false.
103
+ .PARAMETER CheckpointText
104
+ Raw text of the orchestrator checkpoint file.
105
+ #>
106
+ [CmdletBinding()]
107
+ [OutputType([bool])]
108
+ param(
109
+ [AllowNull()]
110
+ [AllowEmptyString()]
111
+ [string] $CheckpointText
112
+ )
113
+
114
+ try {
115
+ $checkpoint = ConvertFrom-BatchBudgetCheckpoint -CheckpointText $CheckpointText
116
+ if ($null -eq $checkpoint) {
117
+ return $false
118
+ }
119
+
120
+ $route = Get-BatchBudgetSelectedRoute -CheckpointText $CheckpointText
121
+ if (-not (@('large', 'remediation', 'preparation') -ccontains $route)) {
122
+ return $false
123
+ }
124
+
125
+ $propertyNames = @($checkpoint.PSObject.Properties.Name)
126
+ if ($propertyNames -contains 'next_step' -and $checkpoint.next_step -is [string] -and $checkpoint.next_step -ceq 'complete') {
127
+ return $false
128
+ }
129
+
130
+ if ($propertyNames -contains 'completed_steps' -and (@($checkpoint.completed_steps) -ccontains 'S12_complete')) {
131
+ return $false
132
+ }
133
+
134
+ return $true
135
+ } catch {
136
+ Write-Verbose "Treating the orchestrator checkpoint as direct mode: $($_.Exception.Message)"
137
+ return $false
138
+ }
139
+ }
@@ -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.'