@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.18

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 (105) hide show
  1. package/out/mcp-server.js +1139 -133
  2. package/package.json +4 -3
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +10 -1
  4. package/resources/claude-customizations/.claude/agents/orchestrator.md +16 -3
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +18 -6
  6. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  7. package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  19. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  21. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +375 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  31. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  32. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  33. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  37. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +119 -3
  38. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  39. package/resources/claude-customizations/.claude/rules/quality-tiers.md +1 -1
  40. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  41. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -0
  42. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  43. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  44. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +169 -5
  45. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  46. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  47. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +4 -15
  48. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +15 -6
  49. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  50. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  51. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +3 -1
  52. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  53. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
  54. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  55. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +33 -13
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +50 -21
  57. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  58. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  59. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +5 -1
  60. package/resources/claude-customizations/pack-manifests/core.json +10 -0
  61. package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
  62. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  63. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +5 -3
  64. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  65. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  66. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  67. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +3 -1
  68. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
  69. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  70. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +30 -7
  71. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  72. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +18 -3
  73. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  74. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +1 -1
  75. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  76. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  77. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  78. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  79. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  80. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  81. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  82. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +1 -1
  83. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +1 -1
  84. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +1 -1
  85. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +1 -1
  86. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +1 -1
  87. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +1 -1
  88. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  89. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  90. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  91. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  92. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  93. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  94. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  95. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  96. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  97. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
  98. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  99. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  100. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  101. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  102. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  103. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  104. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  105. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -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.18",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -38,11 +38,12 @@
38
38
  "prepack": "node prepack.cjs"
39
39
  },
40
40
  "overrides": {
41
- "fast-uri": "^3.1.4",
41
+ "fast-uri": "^3.1.8",
42
42
  "hono": "^4.12.27",
43
43
  "ip-address": "^10.7.2",
44
44
  "qs": "^6.15.2",
45
- "@hono/node-server": "^2.0.5"
45
+ "@hono/node-server": "^2.0.5",
46
+ "brace-expansion": "^5.0.12"
46
47
  },
47
48
  "devDependencies": {
48
49
  "esbuild": "^0.28.0"
@@ -9,6 +9,7 @@ tools:
9
9
  - "Bash(git diff *)"
10
10
  - "Bash(git log *)"
11
11
  - "Write(/docs/features/active/**)"
12
+ - "mcp__drm-copilot__validate_orchestration_artifacts"
12
13
  skills:
13
14
  - policy-compliance-order
14
15
  - acceptance-criteria-tracking
@@ -32,7 +33,12 @@ When the active review scope is a selected version folder such as `docs/features
32
33
  1. `docs/features/active/<feature-or-selected-version>/policy-audit.<timestamp>.md` — policy compliance audit with PASS/PARTIAL/FAIL verdicts and evidence
33
34
  2. `docs/features/active/<feature-or-selected-version>/code-review.<timestamp>.md` — code quality review covering best practices
34
35
  3. `docs/features/active/<feature-or-selected-version>/feature-audit.<timestamp>.md` — acceptance criteria verification relative to baseline
35
- 4. If remediation is needed: `docs/features/active/<feature-or-selected-version>/remediation-inputs.<timestamp>.md` with explicit remediation-required findings and artifact paths
36
+ 4. For every review with at least one blocking finding, including halt (`HALT_NON_REMEDIABLE`) and wait (`AWAITING_CI`) verdicts: `docs/features/active/<feature-or-selected-version>/remediation-inputs.<timestamp>.md` with explicit remediation-required findings and artifact paths. The file carries these line formats:
37
+ - one line `Review-Verdict: <VERDICT>`, where the verdict is `PASS`, `REMEDIATION_REQUIRED`, `HALT_NON_REMEDIABLE`, or `AWAITING_CI`,
38
+ - inside each blocking finding block, one line `Remediability: <class>`, where the class is `autonomous`, `external_dependency`, `policy_hold`, `awaiting_ci`, or `human_decision_required` (`autonomous` when the line is absent),
39
+ - inside each blocking finding block, one line `Remediability-Evidence: <text>` stating why the class applies.
40
+
41
+ These lines, including the evidence text, never contain the substrings the orchestrator counts when it counts blocking findings, so their presence does not change the blocking count. The verdict follows from the classes: no blocking finding is `PASS`; any `autonomous` finding is `REMEDIATION_REQUIRED`; otherwise any `external_dependency`, `policy_hold`, or `human_decision_required` finding is `HALT_NON_REMEDIABLE`; otherwise `AWAITING_CI`.
36
42
 
37
43
  Timestamp format: `yyyy-MM-ddTHH-mm` (ISO-8601).
38
44
 
@@ -44,6 +50,9 @@ Report the required artifact paths in the final response using these tokens:
44
50
  - `code-review-path: docs/features/active/<feature-or-selected-version>/code-review.<timestamp>.md`
45
51
  - `feature-audit-path: docs/features/active/<feature-or-selected-version>/feature-audit.<timestamp>.md`
46
52
  - When remediation inputs are produced: `remediation-inputs-path: docs/features/active/<feature-or-selected-version>/remediation-inputs.<timestamp>.md`
53
+ - Optional: `review-status: <VERDICT>` with the same verdict as the remediation inputs (`PASS` when none were produced). The orchestrator treats the remediation inputs as authoritative and this token as a cross-check.
54
+
55
+ Validate each review artifact immediately after writing it with `mcp__drm-copilot__validate_orchestration_artifacts`, passing `artifact_path` set to that artifact: use `artifact_type: "policy-audit"` for `policy-audit.<timestamp>.md`, `artifact_type: "code-review"` for `code-review.<timestamp>.md`, and `artifact_type: "feature-audit"` for `feature-audit.<timestamp>.md`. When the validator reports a failure, fix the artifact in the same review and validate it again. Report an artifact path only after its validation passes.
47
56
 
48
57
  ## Context Sources
49
58
 
@@ -96,7 +96,9 @@ PR creation and PR body edits must be delegated to `Agent(pr-author)`. The orche
96
96
 
97
97
  When the orchestrator runs the remediation loop, it records a top-level `remediation_loop` object in `artifacts/orchestration/orchestrator-state.json`:
98
98
 
99
- - `current_cycle` — integer index of the active cycle.
99
+ - `current_cycle` — integer index of the active cycle's record in `cycles[]`; a record position, not an attempt count.
100
+ - `completed_attempts` — non-negative integer equal to the number of cycles whose `candidate_applied` is `true`. This is the shared attempt count for local, CI-failure, merge-conflict, and parallel-drift cycles; the guard halts after three completed attempts.
101
+ - `review_outcomes[]` — one entry per review, including `PASS`, appended in order. Each entry is an object with `verdict` (one of `PASS`, `REMEDIATION_REQUIRED`, `HALT_NON_REMEDIABLE`, `AWAITING_CI`) and `findings[]`, the review's blocking findings only, each an object with `remediability` (one of `autonomous`, `external_dependency`, `policy_hold`, `awaiting_ci`, `human_decision_required`). An optional `inputs_path` names the `remediation-inputs` file.
100
102
  - `cycles[]` — an ordered array of cycle records. Each cycle is an object with:
101
103
  - `entry_timestamp` — ISO-8601 timestamp when the cycle was entered.
102
104
  - `inputs_path` — path to the `remediation-inputs.<entry-ts>.md` that opened the cycle.
@@ -106,14 +108,25 @@ When the orchestrator runs the remediation loop, it records a top-level `remedia
106
108
  - `audit_paths` — the reaudit artifact paths produced at cycle exit (`code-review`, `feature-audit`, `policy-audit`).
107
109
  - `blocking_count` — the total number of blocking findings across the reaudit artifacts.
108
110
  - `exit_condition_met` — boolean; `true` only when the cycle's exit gate is satisfied.
111
+ - `candidate_applied` — boolean; `true` only for a completed attempt (execution `complete` and a non-empty pre-reaudit commit). A cycle without a candidate records `false` and consumes no attempt number.
112
+ - `opened_by_review` — zero-based index of the `review_outcomes[]` entry that opened the cycle.
113
+
114
+ These four fields are recorded by producers. Verdicts follow from the classes of a review's blocking findings: none is `PASS`; any `autonomous` is `REMEDIATION_REQUIRED`; otherwise any `external_dependency`, `policy_hold`, or `human_decision_required` is `HALT_NON_REMEDIABLE`; otherwise `AWAITING_CI`. A `HALT_NON_REMEDIABLE` or `AWAITING_CI` review opens no cycle.
109
115
 
110
116
  Malformed-cycle rules:
111
117
 
112
118
  - `plan_path` must be a non-empty string.
113
119
  - `execution_status` may be in `{in_progress, complete, failed}` only when `preflight.final_status == 'clear'`; any other preflight status with one of those execution statuses is malformed (execution recorded before preflight cleared).
114
120
  - `exit_condition_met == true` requires `blocking_count == 0`; a non-zero `blocking_count` with `exit_condition_met == true` is malformed.
121
+ - R5: `candidate_applied`, when present, must be a boolean.
122
+ - R6: `candidate_applied == true` requires `execution_status == "complete"`.
123
+ - R7: `completed_attempts`, when present, must be a non-negative integer equal to the number of object cycles whose `candidate_applied` is `true`.
124
+ - R8: `review_outcomes`, when present, must be a list, and each entry must be an object.
125
+ - R9: each outcome's `verdict` must be one of the four verdicts, its `findings` must be a list, and each finding must be an object whose `remediability` is one of the five classes (case-sensitive).
126
+ - R10: each well-formed outcome's `verdict` must equal the verdict derived from its findings' classes.
127
+ - R11: `opened_by_review`, when present, must be a non-negative integer that indexes an object entry of `review_outcomes` whose `verdict` is `REMEDIATION_REQUIRED`.
115
128
 
116
- Cycle-aware `next_step` uses the form `remediation.cycle_N.{plan,preflight,execute,reaudit,exit_check}`, where `N` is the cycle index and the sub-step names the current position in the loop.
129
+ Cycle-aware `next_step` uses the form `remediation.cycle_N.{plan,preflight,execute,reaudit,exit_check}`, where `N` is `completed_attempts + 1` (the active cycle number) and the sub-step names the current position in the loop; `current_cycle` remains a record position in `cycles[]`.
117
130
 
118
131
  ### CI Monitoring and Post-PR Remediation
119
132
 
@@ -149,7 +162,7 @@ A new finding discovered during execution triggers a NEW cycle with a follow-up
149
162
 
150
163
  ### Exit Gate
151
164
 
152
- `blocking_count` is the total of FAIL and blocking-PARTIAL findings across the three reaudit artifacts (`code-review`, `feature-audit`, `policy-audit`). Only `blocking_count == 0` sets `exit_condition_met = true`. A non-zero `blocking_count` leaves the gate unmet and opens the next cycle.
165
+ `blocking_count` is the total of FAIL and blocking-PARTIAL findings across the three reaudit artifacts (`code-review`, `feature-audit`, `policy-audit`). Only `blocking_count == 0` sets `exit_condition_met = true`. The exit gate counts only `autonomous` blocking findings when deciding whether to open the next cycle: a non-zero count of `autonomous` blocking findings leaves the gate unmet and opens the next cycle, while remaining blocking findings that are all non-remediable halt (`HALT_NON_REMEDIABLE`) or wait (`AWAITING_CI`) instead of opening a cycle.
153
166
 
154
167
  ### Citations
155
168
 
@@ -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
 
@@ -269,6 +275,12 @@ In `closed` mode, do not report completion until:
269
275
  `artifact_type: "parallel-orchestrator-state"`).
270
276
  4. Acceptance criteria in the AC source files of each item have been checked off by that item's own
271
277
  run, per the `acceptance-criteria-tracking` skill.
278
+ A CI-dependent criterion, whose verification requires the result of CI on the pull-request head,
279
+ is checked off and pushed by the item's own run after its S9 success and before its DONE, and
280
+ that run re-runs S9 so its reported `ci_gate.head_sha` is the final head. Before `gh pr merge`,
281
+ confirm that the `headRefOid` value equals that reported `ci_gate.head_sha` and that the item
282
+ reported no pending CI-dependent criteria. You never commit acceptance-criteria check-offs from
283
+ the coordinator root.
272
284
 
273
285
  In `open` mode there is no automatic completion. The run is a standing queue and terminates only
274
286
  via `/parallel-close`, which is owned by F6 and is not specified here or shipped by this feature. Do
@@ -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.'