@danmoisan/drm-copilot-mcp 1.1.22 → 1.1.23

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 (85) hide show
  1. package/out/mcp-server.js +2568 -1261
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +1 -1
  4. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +4 -4
  5. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -1
  6. package/resources/claude-customizations/.claude/agents/feature-review.md +8 -0
  7. package/resources/claude-customizations/.claude/agents/orchestrator.md +1 -1
  8. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +46 -6
  9. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
  10. package/resources/claude-customizations/.claude/agents/pr-author.md +32 -7
  11. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -1
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -6
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
  15. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
  16. package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +48 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +8 -2
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +15 -10
  21. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
  22. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
  23. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +25 -3
  24. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
  25. package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
  30. package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
  31. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -5
  32. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +2 -2
  33. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  34. package/resources/claude-customizations/.claude/rules/shell.md +1 -1
  35. package/resources/claude-customizations/.claude/settings.json +4 -0
  36. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
  37. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +1 -1
  38. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
  39. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +11 -4
  40. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +4 -1
  41. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +90 -14
  42. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +3 -1
  43. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +39 -9
  44. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
  45. package/resources/claude-customizations/pack-manifests/core.json +7 -0
  46. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +1 -1
  47. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  48. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
  49. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
  50. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
  51. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
  52. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
  53. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
  54. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
  55. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
  56. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
  58. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
  59. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
  61. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
  62. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
  63. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  64. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
  65. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
  66. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
  67. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
  68. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
  69. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
  70. package/resources/codex-and-agents-customizations/.codex/config.toml +7 -1
  71. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
  72. package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
  73. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
  74. package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
  75. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  76. package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
  77. package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
  78. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
  79. package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
  80. package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
  81. package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
  82. package/resources/customizations/.github/skills/execution-environment/SKILL.md +1 -1
  83. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
  84. package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
  85. package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -7,7 +7,7 @@ metadata:
7
7
  ---
8
8
 
9
9
  - [Policy requirements are not optional gaps](feedback_policy_compliance_not_optional.md) — never frame skipped policy requirements as "known gaps" or defer them; comply before reporting completion.
10
- - [test-files-count-against-500-cap](feedback_test_files_count_against_500_cap.md) — the 500-line file cap applies to test files too; QA must scan changed/created production AND test files.
10
+ - [test-files-count-against-500-cap](feedback_test_files_count_against_500_cap.md) — the tiered file-size policy applies to test files too; QA must scan changed/created production AND test files.
11
11
  - [every-change-through-lifecycle](feedback_every_change_through_lifecycle.md) — every change, including small tooling changes, goes through issue promotion, an active feature folder, and feature-review before commit.
12
12
  - [remediation-plan-em-dash-required](feedback_remediation_plan_em_dash_required.md) — the plan validator rejects any token between "Phase N" and the em-dash; only `### Phase N — <Title>` passes.
13
13
  - [branch-base-check-unmerged-pr-deps](feedback_branch_base_check_unmerged_pr_deps.md) — verify required symbols/files exist on the chosen branch base; if they only exist in an open PR, stack or merge first.
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: test-files-count-against-500-cap
3
- description: The 500-line file cap applies to test files as well as production files; QA phases must scan all changed/created production AND test files.
3
+ description: The tiered file-size policy (hard limit above 800 lines) applies to test files as well as production files; QA phases must scan all changed/created production AND test files.
4
4
  metadata:
5
5
  type: feedback
6
6
  scope: general
7
7
  ---
8
8
 
9
- The `.claude/rules/general-code-change.md` 500-line file cap applies to test files as well as production files. Only throwaway scripts, raw text fixtures, and Markdown are exempt. When a plan adds substantial tests, Phase 0 baseline and final QA tasks must scan ALL changed/created test files (not only production files) and assert each is <= 500 lines.
9
+ The tiered file-size policy in `.claude/rules/general-code-change.md` (`## File Size Limit`) applies to test files as well as production files. Only throwaway scripts, raw text fixtures, and Markdown are exempt. When a plan adds substantial tests, Phase 0 baseline and final QA tasks must scan ALL changed/created test files (not only production files), assert each is at or below the hard limit, and report any file at or above the advisory tier.
10
10
 
11
- **Why:** A plan that adds tests but only size-checks production files can create a remediation cycle when test files cross the cap — the only finding raised during feature-review.
11
+ **Why:** A plan that adds tests but only size-checks production files can create a remediation cycle when test files cross the hard limit — the only finding raised during feature-review.
12
12
 
13
- **How to apply:** When briefing the planner for any change that adds tests, require the QA phase to include a line-count assertion across all changed/created production AND test files. If a test file would exceed the cap, split it into a sibling test module before reporting completion.
13
+ **How to apply:** When briefing the planner for any change that adds tests, require the QA phase to include a line-count assertion across all changed/created production AND test files. If a test file would exceed the hard limit, split it into a sibling test module before reporting completion.
@@ -54,7 +54,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
54
54
  Stop implementation and return to the user when:
55
55
 
56
56
  - the scope estimate or an in-flight batch would exceed the 3-production-file cap in direct mode,
57
- - a file is near or would exceed the 500-line limit,
57
+ - a file is above or would exceed the 800-line hard limit of the tiered file-size policy (split it before proceeding; at 500 lines look for a logical split, and at 700 lines split more aggressively),
58
58
  - any QA gate delta is non-zero after self-correction,
59
59
  - the toolchain cannot be executed in the current environment (mark the change **unverified**),
60
60
  - orchestrator handoff mode is requested but the required context package is incomplete,
@@ -143,6 +143,14 @@ For each language that has changed files in the feature branch:
143
143
 
144
144
  The agent does NOT rerun coverage generation. Evidence verification from existing artifacts is the required model.
145
145
 
146
+ ## File Size Advisory
147
+
148
+ The tiered file-size policy (`## File Size Limit` in `.claude/rules/general-code-change.md`) sets a 500-line guidance tier, a 700-line advisory tier, and an 800-line hard limit. Measure the logical line count of every added or modified covered file in the branch diff. Covered files have one of the extensions `.py`, `.ps1`, `.psm1`, `.ts`, `.tsx`, `.js`, `.cjs`, `.mjs`, `.cs`, `.sh`, or `.bats` and are not under a build, cache, dependency, virtual-environment, or `docs` directory. A logical line count splits the content on line terminators, and a trailing terminator does not add a line; it equals `@(Get-Content -LiteralPath <path>).Count`. Do not use `wc -l` or `Measure-Object -Line`.
149
+
150
+ - Report each covered file at or above 700 lines as a non-blocking `Warning` row in the Findings Table of `code-review.<timestamp>.md`, naming the file and its line count. Do not write the Warning to `remediation-inputs.<timestamp>.md`, and do not give it a remediation severity label.
151
+ - Report a covered file above 800 lines that has no entry in `config/file-size-baseline.json`, and any added or increased entry in `config/file-size-baseline.json`, as a blocking policy finding.
152
+ - Produce no finding for the 500-line guidance tier.
153
+
146
154
  ## Evidence Location Invariant
147
155
 
148
156
  All evidence artifacts this agent produces (baselines, QA gates, regression results, coverage) MUST be written to `<FEATURE>/evidence/<kind>/` as defined in `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`.
@@ -91,7 +91,7 @@ For required delegated steps, delegation is mandatory. If a handoff cannot be st
91
91
 
92
92
  ## PR Creation Delegation
93
93
 
94
- PR creation and PR body edits must be delegated to `Agent(pr-author)`. The orchestrator must not call `gh pr create` or `gh pr edit --body*` directly from the main thread; those commands are blocked by the `enforce-pr-author-skill.ps1` PreToolUse hook unless the `--body-file` argument resolves to a canonical `artifacts/pr_body_<N>.md` path with a matching, verified `artifacts/pr_body_<N>.receipt.json`. The orchestrator first refreshes the PR-context artifact via `mcp__drm-copilot__collect_pr_context`, then runs the orchestrator-state validator against `artifacts/orchestration/orchestrator-state.json --require-pr-creation-ready` and records the result under `pr_author_preflight` (`{status, checked_at, checkpoint_path, validator_command, output_summary}`) before delegating to `Agent(pr-author)`. This is a local enforcement mechanism, not a CI check: `enforce-pr-author-skill.ps1` independently re-validates the same checkpoint inside the PreToolUse hook itself (via an injectable `$Invoker` subprocess seam) and blocks with `ORCHESTRATOR_STATE_PREFLIGHT_FAILED` when it is missing or invalid, closing the bypass path that a CI-only check could never close. `Agent(pr-author)` authors the PR body via the `pr-author` skill, writes `artifacts/pr_body_<N>.md` and the sibling receipt `artifacts/pr_body_<N>.receipt.json` (carrying the lowercase-hex SHA-256 of the body bytes), issues `gh pr create --body-file ...`, and reports the resulting PR URL or PR number. The authoritative handoff contract is `.claude/skills/orchestrate/SKILL.md` `## PR Authoring (pr-author Handoff)`; this section defers to it. The orchestrator records `pr_author_receipt` in the checkpoint.
94
+ PR creation and PR body edits must be delegated to `Agent(pr-author)`. The orchestrator must not call `gh pr create` or `gh pr edit --body*` directly from the main thread; those commands are blocked by the `enforce-pr-author-skill.ps1` PreToolUse hook unless the `--body-file` argument resolves to a canonical `artifacts/pr_body_<N>.md` path with a matching, verified `artifacts/pr_body_<N>.receipt.json`. The orchestrator first refreshes the PR-context artifact via `mcp__drm-copilot__collect_pr_context`, then runs the orchestrator-state validator against `artifacts/orchestration/orchestrator-state.json --require-pr-creation-ready` and records the result under `pr_author_preflight` (`{status, checked_at, checkpoint_path, validator_command, output_summary}`) before delegating to `Agent(pr-author)`. This is a local enforcement mechanism, not a CI check: `enforce-pr-author-skill.ps1` independently re-validates the same checkpoint inside the PreToolUse hook itself (via an injectable `$Invoker` subprocess seam) and blocks with `ORCHESTRATOR_STATE_PREFLIGHT_FAILED` when it is missing or invalid, closing the bypass path that a CI-only check could never close. `Agent(pr-author)` authors the PR body via the `pr-author` skill, writes `artifacts/pr_body_<N>.md` and the sibling receipt `artifacts/pr_body_<N>.receipt.json` (carrying the lowercase-hex SHA-256 of the body bytes), issues `gh pr create --body-file ...`, and reports the resulting PR URL or PR number. The delegation prompt carries the line `Author-asserted autoclose issues: #<issue-num>` from the checkpoint `issue-num`, omitted only when the checkpoint has no `issue-num` or `epic_mode` is true; the skill section named in the next sentence states the rule and its hook enforcement. The authoritative handoff contract is `.claude/skills/orchestrate/SKILL.md` `## PR Authoring (pr-author Handoff)`; this section defers to it. The orchestrator records `pr_author_receipt` in the checkpoint.
95
95
 
96
96
  ### Remediation Loop Checkpoint Shape
97
97
 
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: parallel-orchestrator
3
3
  model: opus
4
- description: Execution half of the parallel orchestration surface. Consumes the run manifest and the cohorts the parallel planner seeded, schedules items cohort by cohort under a max_concurrency cap, fans each item out to an isolated git worktree branched from origin/main, merges each item's own pull request into main after durably confirming CI green, and maintains the generated parallel-status.md projection. There is no integration branch and no final integration pull request. Authorized to delegate Agent(orchestrator).
4
+ description: Execution half of the parallel orchestration surface. Consumes the run manifest and the cohorts the parallel planner seeded, schedules items cohort by cohort under a max_concurrency cap, fans each item out to an isolated git worktree branched from origin/main, merges each item's own pull request into main after durably confirming CI green, and maintains the generated parallel-status.md projection. There is no integration branch and no final integration pull request. Authorized to delegate Agent(orchestrator), and Agent(pr-author) for the Run Closing records pull request only.
5
5
  tools:
6
6
  - "Agent(orchestrator)"
7
+ - "Agent(pr-author)"
7
8
  - Read
8
9
  - Grep
9
10
  - Glob
@@ -19,6 +20,7 @@ tools:
19
20
  - "Bash(bash .claude/lib/bash/abandon-parallel-item.sh*)"
20
21
  - "Bash(bash .claude/lib/bash/remove-parallel-item.sh*)"
21
22
  - "Bash(bash .claude/lib/bash/mutate-parallel-run.sh*)"
23
+ - "Bash(bash .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh*)"
22
24
  - "Bash(pwsh -NoProfile -File .claude/lib/project-file-merge/Resolve-MergeableConflict.ps1*)"
23
25
  - "Bash(pwsh -NoProfile -NonInteractive -File .claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1*)"
24
26
  - "Bash(pwsh -NoProfile -File .claude/lib/ci-gate/Invoke-CiGateParser.ps1*)"
@@ -173,7 +175,7 @@ Given a prepared run:
173
175
 
174
176
  ## Delegation Model
175
177
 
176
- You delegate through exactly one channel: `Agent(orchestrator)`, one delegation per item in the
178
+ You delegate items through exactly one channel: `Agent(orchestrator)`, one delegation per item in the
177
179
  manifest, carrying the parallel-mode kickoff prompt defined in the `parallel-orchestrate` skill's
178
180
  `## Parallel-Mode Kickoff Parameter` section. Each delegation is spawned with all four of these
179
181
  parameters:
@@ -202,9 +204,10 @@ Each child `orchestrator` runs its own route inside that worktree, including its
202
204
  `atomic-planner`, `atomic-executor`, `feature-review`, and `pr-author`. You do not delegate to
203
205
  those agents directly.
204
206
 
205
- There is no `Agent(pr-author)` channel on this surface: each item's pull request is authored inside
206
- that item's own child run, and you merge the already-authored, already-green pull request with a
207
- `gh` command. There are also no upstream-context citation lines to emit: this surface carries no
207
+ The `Agent(pr-author)` channel is used only for the Run Closing records pull request, which lands
208
+ the run's generated documents after every item has merged. Each item's pull request is still
209
+ authored inside that item's own child run, and you merge the already-authored, already-green item
210
+ pull request with a `gh` command. There are also no upstream-context citation lines to emit: this surface carries no
208
211
  `depends_on` field, so no item cites another item's output. Ordering exists only as blast-radius
209
212
  overlap expressed by the cohort table.
210
213
 
@@ -303,10 +306,47 @@ In `closed` mode, do not report completion until:
303
306
  confirm that the `headRefOid` value equals that reported `ci_gate.head_sha` and that the item
304
307
  reported no pending CI-dependent criteria. You never commit acceptance-criteria check-offs from
305
308
  the coordinator root.
309
+ 5. `### Run Closing` below has run, and its records pull request is merged or reported as a
310
+ deferral.
306
311
 
307
312
  In `open` mode there is no automatic completion. The run is a standing queue and terminates only
308
313
  via `/parallel-close`, which is owned by F6 and is not specified here or shipped by this feature. Do
309
314
  not synthesize a completion condition for an `open`-mode run.
310
315
 
311
316
  No completion condition involves a run-level pull request. Each item merges its own pull request
312
- into `main`, so completion keys on the per-item terminal states above and on nothing else.
317
+ into `main`, so completion keys on the per-item terminal states above and on nothing else. The Run
318
+ Closing records pull request is a bookkeeping pull request for the run's generated documents; it
319
+ is not a run-level integration pull request and merges no item work.
320
+
321
+ ### Run Closing
322
+
323
+ Run Closing runs once per run, after every non-withdrawn item has `merge_status` of `merged` or
324
+ `worktree_removed` and before you report completion (issue #995). It re-derives state from
325
+ `git worktree list --porcelain`, `git`, and `gh` rather than from the checkpoint, so it is safe to
326
+ repeat: a substep whose outcome already holds is skipped. The full procedure is in the
327
+ `parallel-orchestrate` skill's `### Run Closing` section under `## Worktree Cleanup`; its five
328
+ substeps, in order:
329
+
330
+ 1. **Item worktrees.** For each `merged` item, take the absolute path from
331
+ `git worktree list --porcelain`, never from the relative `items[].worktree_path`. Run
332
+ `git worktree unlock <path>` only when the lock reason names the item's own agent, then
333
+ `git worktree remove <path>` with no force flag. On success set `merge_status: worktree_removed`
334
+ and `worktree_removed_at`; on refusal leave `merged`, append a `completed_steps` deferral naming
335
+ the path and refusal reason, and continue.
336
+ 2. **Final status.** Regenerate `docs/features/parallel/<slug>/parallel-status.md`.
337
+ 3. **Run documents.** Stage the run folder and the promoted lifecycle files created for the run by
338
+ literal path, with every `git add` and `git commit` pathspec beginning `docs/features/`. The
339
+ `artifacts/` tree is gitignored, so Run Closing never stages it. Never use `git add -A` or
340
+ `git add .`, and never stage a
341
+ `docs/features/active/<item>/` folder. Require that `git diff --name-only origin/main...HEAD`
342
+ lists only `docs/` paths, otherwise stop and report without forcing or splitting the branch.
343
+ Push, delegate the pull request to `Agent(pr-author)` with a `branch:` label, merge it with
344
+ `gh pr merge <PR> --merge`, and confirm the merge by the skill's merge-confirmation procedure.
345
+ 4. **Scratch worktrees.** After the records merge is confirmed, run the `cleanup-merged-worktrees`
346
+ report with `bash .claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh`, and run
347
+ it with `--apply` only when every delete-eligible row is a worktree this run created; otherwise
348
+ apply nothing and report each scratch worktree as a deferral. Add no hook allow.
349
+ 5. **Report.** Report every deferral, with LV1 (removal refused after unlock), LV2 (records pull
350
+ request still open in the merge queue or `origin/main` absent at merge-gate time), and LV3
351
+ (records pull request needs manual authoring because `Agent(pr-author)` failed its routing
352
+ receipt check) as the named deferral cases.
@@ -65,7 +65,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
65
65
  Stop implementation and return to the user when:
66
66
 
67
67
  - the scope estimate exceeds the 3-production-file cap in direct mode,
68
- - a file is near or would exceed the 500-line limit,
68
+ - a file is above or would exceed the 800-line hard limit of the tiered file-size policy (split it before proceeding; at 500 lines look for a logical split, and at 700 lines split more aggressively),
69
69
  - any QA gate delta is non-zero after self-correction,
70
70
  - the toolchain cannot be executed in the current environment (mark the change **unverified**),
71
71
  - orchestrator handoff mode is requested but the required context package is incomplete,
@@ -11,6 +11,7 @@ tools:
11
11
  - "Bash(git rev-parse *)"
12
12
  - "Bash(gh pr create *)"
13
13
  - "Bash(gh pr edit *)"
14
+ - "Bash(gh pr view *)"
14
15
  - "Bash(sha256sum *)"
15
16
  - "Bash(date *)"
16
17
  - "Write(/artifacts/**)"
@@ -78,9 +79,22 @@ the SHA-256 receipt.
78
79
 
79
80
  The `enforce-pr-author-command-allowlist.ps1` PreToolUse hook registered in this agent's frontmatter
80
81
  evaluates every Bash segment and denies anything outside the allowed forms (`git log`,
81
- `git rev-parse`, `gh pr create`, `gh pr edit`, and, each as a single command,
82
+ `git rev-parse`, `gh pr create`, `gh pr edit`, exactly
83
+ `gh pr view <N> --json closingIssuesReferences,body`, and, each as a single command,
82
84
  `sha256sum artifacts/pr_body_<N>.md` and `date -u +%Y-%m-%dT%H:%M:%SZ`) with
83
- `PR_AUTHOR_COMMAND_NOT_ALLOWED:`.
85
+ `PR_AUTHOR_COMMAND_NOT_ALLOWED:`. The `Bash(gh pr view *)` tool entry grants the command class;
86
+ the hook narrows it to that one read-only form.
87
+
88
+ ## Closing-Reference Verification
89
+
90
+ Run `gh pr view <N> --json closingIssuesReferences,body` as a single command (no chaining, no
91
+ extra flags) after every `gh pr create` or `gh pr edit` you issue, where `<N>` is the pull request
92
+ number. Report the closing issue numbers that `closingIssuesReferences` lists, and report any
93
+ mismatch with the author-asserted list: an author-asserted issue that GitHub does not list as
94
+ closing, or a closing issue that no source named. An issue named on the delegation-prompt line
95
+ `Author-asserted autoclose issues: #NNN` is a valid `- Closes #NNN` source even when the PR
96
+ context lists no verified autoclose issues; the `pr-author` skill section
97
+ `## Issue/PR Reference Rules` lists every source.
84
98
 
85
99
  ## Final Output Requirement
86
100
 
@@ -109,14 +123,25 @@ outside that enumeration. Do not invent issue or PR numbers.
109
123
  ## Required `--head` on `gh pr create` (issue #687)
110
124
 
111
125
  Every `gh pr create` command this agent issues MUST carry an explicit
112
- `--head <branch>` naming the branch the pull request is opened from.
126
+ `--head <branch>` naming the branch the pull request is opened from, for example
127
+ `gh pr create --head <branch> --body-file artifacts/pr_body_<N>.md`.
113
128
 
114
129
  This is what makes the call's target derivable. `enforce-pr-author-skill.ps1` resolves the
115
130
  worktree the call pertains to from the command text, and validates the orchestrator checkpoint
116
131
  belonging to that worktree. Without `--head`, `gh pr create` infers its head branch from the
117
- current checkout and the command carries no signal at all, so the gate denies with
118
- `TARGET_WORKTREE_NOT_DERIVABLE` rather than validating against whichever checkpoint occupies the
119
- session root, which in a parallel or epic topology belongs to a different item.
132
+ current checkout and the command carries no signal at all, so the gate falls back to the branch
133
+ checked out in its own working directory (issue #989) and denies with
134
+ `TARGET_WORKTREE_NOT_DERIVABLE` when that checkout is detached or unreadable, rather than
135
+ validating against whichever checkpoint occupies the session root, which in a parallel or epic
136
+ topology belongs to a different item.
120
137
 
121
138
  Pass `--head` even when the branch is the current one: the gate resolves it to the session root
122
- and behaves exactly as before, and the command stays correct if it is ever replayed elsewhere.
139
+ and behaves exactly as before, and the command stays correct if it is ever replayed elsewhere.
140
+
141
+ `gh pr edit` takes no head-branch flag, and --head is not valid for gh pr edit. Name the target of
142
+ an edit in one of two ways:
143
+
144
+ - Run `gh pr edit <N> --body-file artifacts/pr_body_<N>.md` from the item worktree. The gate
145
+ derives the branch from that worktree's checkout.
146
+ - When running outside the item worktree, append the shell comment `# branch: <branch>` to the
147
+ command, and pass the absolute canonical body path beneath the item worktree.
@@ -59,7 +59,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
59
59
  Stop implementation and return to the user when:
60
60
 
61
61
  - the scope estimate exceeds 3 production files in direct mode,
62
- - a file is near or would exceed the 500-line limit,
62
+ - a file is above or would exceed the 800-line hard limit of the tiered file-size policy (split it before proceeding; at 500 lines look for a logical split, and at 700 lines split more aggressively),
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**),
65
65
  - policy instructions conflict.
@@ -0,0 +1,325 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Green-CI standalone merge branch of the merge gate (issue #997).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-epic-merge-gate.ps1. Invoke-EpicMergeGateDecision calls
7
+ Get-EpicMergeGateGreenCiDecision after the parallel allow-branch declines and before the
8
+ standalone authorization branch, so branches 1 to 3 keep their order and behavior. The
9
+ branch allows a standalone run (neither an epic child nor a parallel item) to merge its own
10
+ pull request once its per-feature checkpoint records a green S9 gate for that exact pull
11
+ request and the live pull-request head still equals the head that CI passed on.
12
+
13
+ Stages run cheapest first, and only stage D starts a process:
14
+ A. The item target resolved to one worktree and its per-feature checkpoint records a green
15
+ standalone S9 gate bound to the explicit PR number (Test-ChildCheckpointGreenCiRecorded).
16
+ B. No epic or parallel run checkpoint records the PR number (both run targets NoTarget),
17
+ and no parallel run checkpoint records the item worktree as a parallel item.
18
+ C. The command is one structural gh pr merge with no chain, wrapper, inline environment
19
+ prefix, -R or --repo, /pull/ URL selector, or --auto (Get-DocsOnlyMergeShapeViolation),
20
+ and carries no --admin flag (declined silently).
21
+ D. gh pr view <PR> --json headRefOid returns a well-formed head (one bounded call).
22
+ E. The live head equals ci_gate.head_sha, and --match-head-commit, when present, equals it.
23
+ Stages A to C decline silently, so every existing deny text is unchanged for a non-green
24
+ case. A stage D or E decline returns a suffix that the gate appends to its deny reason:
25
+ GREEN_CI_LIVE_HEAD_UNRESOLVED, GREEN_CI_HEAD_STALE, or GREEN_CI_MATCH_HEAD_MISMATCH.
26
+
27
+ Residual parallel-exclusion window: the Parallel mode: true marker lives in the Agent
28
+ delegation prompt and is not in a Bash PreToolUse payload, so this hook cannot see it. The
29
+ exclusion is layered and best-effort: a run target recording the PR number (layer 1a), a
30
+ parallel item recording the worktree path (layer 1b), and child-recorded parallel_mode or
31
+ parallel_slug keys (layer 2), with skill prose as layer 3. A parallel child whose parent has
32
+ recorded neither the PR number nor the worktree path, and which omits the layer 2 keys, is
33
+ not excluded mechanically. Like the standalone authorization record, this is a policy-level
34
+ control, not a cryptographic one. A push after the stage D lookup can also change the head
35
+ unless the command passes --match-head-commit; the hook does not require the flag.
36
+
37
+ .NOTES
38
+ Compatible with PowerShell 7+. This file dot-sources nothing and imports no module. It
39
+ calls, at run time only, Invoke-DocsOnlyGhExe and Get-DocsOnlyMergeShapeViolation
40
+ (hook-docs-only-diff.ps1), Test-CommandLineFlag and Get-CommandLineFlagValue (the shared
41
+ command parser), and Resolve-WorktreeRunTargetByRecord (WorktreeRunResolution.psm1), all of
42
+ which the gate loads before it calls this file's functions.
43
+ #>
44
+
45
+ function Test-EpicMergeGateGreenCiObjectId {
46
+ <#
47
+ .SYNOPSIS
48
+ True when a value is a full lowercase hexadecimal object id of 40 or 64 characters.
49
+ .PARAMETER Value
50
+ The candidate value; any non-string value returns $false.
51
+ .OUTPUTS
52
+ System.Boolean
53
+ #>
54
+ [CmdletBinding()]
55
+ [OutputType([bool])]
56
+ param([AllowNull()] $Value)
57
+
58
+ return ($Value -is [string]) -and ($Value -cmatch '^[0-9a-f]{40}([0-9a-f]{24})?$')
59
+ }
60
+
61
+ function Test-EpicMergeGateGreenCiNotParallel {
62
+ <#
63
+ .SYNOPSIS
64
+ True when a parsed per-feature checkpoint carries no epic or parallel marker (pure).
65
+ .DESCRIPTION
66
+ epic_mode must be absent, JSON null, or JSON false; epic_mode true belongs to branch 1,
67
+ and any other value (for example the string "false") fails closed. parallel_mode must
68
+ be absent, JSON null, or JSON false, and parallel_slug must be absent or blank.
69
+ .PARAMETER Checkpoint
70
+ The parsed checkpoint object.
71
+ .OUTPUTS
72
+ System.Boolean
73
+ #>
74
+ [CmdletBinding()]
75
+ [OutputType([bool])]
76
+ param([Parameter(Mandatory)] $Checkpoint)
77
+
78
+ $props = @($Checkpoint.PSObject.Properties.Name)
79
+ # Each marker key passes only when it is absent, JSON null, or the JSON boolean false.
80
+ foreach ($marker in @('epic_mode', 'parallel_mode')) {
81
+ if ($props -ccontains $marker -and $null -ne $Checkpoint.$marker -and
82
+ -not (($Checkpoint.$marker -is [bool]) -and -not $Checkpoint.$marker)) {
83
+ return $false
84
+ }
85
+ }
86
+ return -not ($props -ccontains 'parallel_slug' -and -not [string]::IsNullOrWhiteSpace([string]$Checkpoint.parallel_slug))
87
+ }
88
+
89
+ function Test-ChildCheckpointGreenCiRecorded {
90
+ <#
91
+ .SYNOPSIS
92
+ True when a per-feature checkpoint records a green standalone S9 gate for the PR (pure).
93
+ .DESCRIPTION
94
+ Every condition must hold: the command names a PR number above zero; the checkpoint
95
+ carries no epic or parallel marker (Test-EpicMergeGateGreenCiNotParallel);
96
+ step9_status is exactly "passed"; ci_gate is an object whose conclusion is exactly
97
+ "success"; pr_gate.pr_number is a JSON integer equal to the command number, with no
98
+ fallback to a standalone_merge_authorizations record; ci_gate.head_sha is a lowercase
99
+ 40 or 64 character object id; and pr_gate.head_sha, when present and not null, equals
100
+ ci_gate.head_sha. Comparisons are ordinal, so a case variant fails closed.
101
+ .PARAMETER Checkpoint
102
+ Parsed per-feature checkpoint, or $null when absent or unreadable.
103
+ .PARAMETER CommandPrNumber
104
+ The explicit PR number the command names, or $null for a bare command.
105
+ .OUTPUTS
106
+ System.Boolean
107
+ #>
108
+ [CmdletBinding()]
109
+ [OutputType([bool])]
110
+ param(
111
+ [AllowNull()] $Checkpoint,
112
+ [AllowNull()][Nullable[int]] $CommandPrNumber
113
+ )
114
+
115
+ if ($null -eq $CommandPrNumber -or $CommandPrNumber -le 0) { return $false }
116
+ if ($Checkpoint -isnot [System.Management.Automation.PSCustomObject]) { return $false }
117
+ if (-not (Test-EpicMergeGateGreenCiNotParallel -Checkpoint $Checkpoint)) { return $false }
118
+ if (-not (($Checkpoint.step9_status -is [string]) -and ($Checkpoint.step9_status -ceq 'passed'))) { return $false }
119
+
120
+ $ciGate = $Checkpoint.ci_gate
121
+ if ($ciGate -isnot [System.Management.Automation.PSCustomObject]) { return $false }
122
+ if (-not (($ciGate.conclusion -is [string]) -and ($ciGate.conclusion -ceq 'success'))) { return $false }
123
+ if (-not (Test-EpicMergeGateGreenCiObjectId -Value $ciGate.head_sha)) { return $false }
124
+
125
+ # The PR binding comes from pr_gate alone; an authorization record must not stand in for a CI result.
126
+ $prGate = $Checkpoint.pr_gate
127
+ if ($prGate -isnot [System.Management.Automation.PSCustomObject]) { return $false }
128
+ $recordedNumber = $prGate.pr_number
129
+ if (-not (($recordedNumber -is [int]) -or ($recordedNumber -is [long])) -or [long]$recordedNumber -ne $CommandPrNumber) { return $false }
130
+
131
+ # A recorded PR head must agree with the head CI passed on; null or absent means not recorded.
132
+ if (@($prGate.PSObject.Properties.Name) -ccontains 'head_sha' -and $null -ne $prGate.head_sha) {
133
+ return [string]::Equals([string]$prGate.head_sha, [string]$ciGate.head_sha, [System.StringComparison]::Ordinal)
134
+ }
135
+ return $true
136
+ }
137
+
138
+ function Get-EpicMergeGateLivePrHeadSha {
139
+ <#
140
+ .SYNOPSIS
141
+ Return the live head commit of a pull request, or $null on any failure (the live-head seam).
142
+ .DESCRIPTION
143
+ Runs gh pr view <PR> --json headRefOid through Invoke-DocsOnlyGhExe, which bounds the
144
+ call at 7 seconds and never throws. Returns the head only for exit code 0, output that
145
+ parses to a JSON object, and a well-formed lowercase object id; a non-zero exit
146
+ (including 124 for a timeout and -1 for a missing root), unparseable output, and a
147
+ malformed head all return $null. Tests mock this function by name.
148
+ .PARAMETER PrNumber
149
+ The pull request number.
150
+ .PARAMETER WorktreeRoot
151
+ The directory gh runs in.
152
+ .OUTPUTS
153
+ System.String or $null
154
+ #>
155
+ [CmdletBinding()]
156
+ [OutputType([string])]
157
+ param(
158
+ [Parameter(Mandatory)][int] $PrNumber,
159
+ [AllowNull()][AllowEmptyString()][string] $WorktreeRoot
160
+ )
161
+
162
+ $lookup = Invoke-DocsOnlyGhExe -WorkingRoot $WorktreeRoot -GhArgs @('pr', 'view', [string]$PrNumber, '--json', 'headRefOid')
163
+ if ($null -eq $lookup -or $lookup.ExitCode -ne 0) { return $null }
164
+ try {
165
+ $pr = ConvertFrom-Json -InputObject ([string]$lookup.Output) -ErrorAction Stop
166
+ }
167
+ catch {
168
+ # Unreadable gh output is a lookup failure; the gate fails closed on $null.
169
+ return $null
170
+ }
171
+ if ($pr -isnot [System.Management.Automation.PSCustomObject]) { return $null }
172
+ if (-not (Test-EpicMergeGateGreenCiObjectId -Value $pr.headRefOid)) { return $null }
173
+ return [string]$pr.headRefOid
174
+ }
175
+
176
+ function Test-EpicMergeGateGreenCiHeadAgreement {
177
+ <#
178
+ .SYNOPSIS
179
+ True when the live head equals ci_gate.head_sha and any pinned head equals the live head (pure).
180
+ .PARAMETER Checkpoint
181
+ Parsed per-feature checkpoint carrying ci_gate.head_sha.
182
+ .PARAMETER LiveHeadSha
183
+ The live pull-request head, or $null when unresolved.
184
+ .PARAMETER PinnedHeadSha
185
+ The --match-head-commit value, or $null when the flag is absent. An empty string means
186
+ the flag is present without a usable value and fails closed.
187
+ .OUTPUTS
188
+ System.Boolean
189
+ #>
190
+ [CmdletBinding()]
191
+ [OutputType([bool])]
192
+ param(
193
+ [AllowNull()] $Checkpoint,
194
+ [AllowNull()] $LiveHeadSha,
195
+ [AllowNull()] $PinnedHeadSha
196
+ )
197
+
198
+ if ($null -eq $Checkpoint -or $LiveHeadSha -isnot [string] -or $null -eq $Checkpoint.ci_gate) { return $false }
199
+ if (-not [string]::Equals($LiveHeadSha, [string]$Checkpoint.ci_gate.head_sha, [System.StringComparison]::Ordinal)) { return $false }
200
+ if ($null -ne $PinnedHeadSha -and -not [string]::Equals([string]$PinnedHeadSha, $LiveHeadSha, [System.StringComparison]::Ordinal)) { return $false }
201
+ return $true
202
+ }
203
+
204
+ function Resolve-EpicMergeGateParallelItemByWorktree {
205
+ <#
206
+ .SYNOPSIS
207
+ Resolve the parallel run whose items[] record a worktree path (layer 1b seam).
208
+ .DESCRIPTION
209
+ A one-call seam over Resolve-WorktreeRunTargetByRecord so tests can mock it. Status
210
+ NoTarget means no parallel checkpoint records the worktree as a parallel item.
211
+ .PARAMETER WorktreeRoot
212
+ The item worktree whose per-feature checkpoint was read.
213
+ .OUTPUTS
214
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
215
+ #>
216
+ [CmdletBinding()]
217
+ [OutputType([pscustomobject])]
218
+ param([AllowNull()][AllowEmptyString()][string] $WorktreeRoot)
219
+
220
+ return Resolve-WorktreeRunTargetByRecord -Kind parallel -RecordField worktree_path -Value $WorktreeRoot -SessionRoot (Get-Location).Path
221
+ }
222
+
223
+ function Test-EpicMergeGateGreenCiNoRunRecord {
224
+ <#
225
+ .SYNOPSIS
226
+ True when no epic or parallel run checkpoint records the PR or the item worktree (stage B).
227
+ .DESCRIPTION
228
+ Both run targets must have status NoTarget; a resolved or Ambiguous target declines.
229
+ The worktree lookup declines on any status other than NoTarget, on $null, and on a
230
+ thrown error, because an unreadable parallel record cannot prove the item is standalone.
231
+ .PARAMETER EpicTarget
232
+ The epic run target resolved for the PR number.
233
+ .PARAMETER ParallelTarget
234
+ The parallel run target resolved for the PR number.
235
+ .PARAMETER WorktreeRoot
236
+ The item worktree root.
237
+ .OUTPUTS
238
+ System.Boolean
239
+ #>
240
+ [CmdletBinding()]
241
+ [OutputType([bool])]
242
+ param(
243
+ [AllowNull()] $EpicTarget,
244
+ [AllowNull()] $ParallelTarget,
245
+ [AllowNull()][AllowEmptyString()][string] $WorktreeRoot
246
+ )
247
+
248
+ foreach ($target in @($EpicTarget, $ParallelTarget)) {
249
+ if ($null -eq $target -or [string]$target.Status -cne 'NoTarget') { return $false }
250
+ }
251
+ try {
252
+ $byWorktree = Resolve-EpicMergeGateParallelItemByWorktree -WorktreeRoot $WorktreeRoot
253
+ }
254
+ catch {
255
+ # A failed lookup cannot show the worktree is not a parallel item, so the stage declines.
256
+ return $false
257
+ }
258
+ return ($null -ne $byWorktree -and [string]$byWorktree.Status -ceq 'NoTarget')
259
+ }
260
+
261
+ function Get-EpicMergeGateGreenCiDecision {
262
+ <#
263
+ .SYNOPSIS
264
+ Evaluate stages A to E of the green-CI branch and return Allowed and a deny suffix.
265
+ Stage C declines silently a command that carries --admin.
266
+ .PARAMETER CommandText
267
+ The Bash command text under evaluation.
268
+ .PARAMETER CommandPrNumber
269
+ The explicit PR number, or $null for a bare command.
270
+ .PARAMETER ChildCheckpoint
271
+ The parsed per-feature checkpoint the gate read, or $null.
272
+ .PARAMETER ItemTarget
273
+ The item target, or $null for a bare command.
274
+ .PARAMETER EpicTarget
275
+ The epic run target, or $null for a bare command.
276
+ .PARAMETER ParallelTarget
277
+ The parallel run target, or $null for a bare command.
278
+ .PARAMETER SessionRoot
279
+ The session worktree root, where gh runs.
280
+ .OUTPUTS
281
+ System.Collections.Specialized.OrderedDictionary with Allowed and Suffix.
282
+ #>
283
+ [CmdletBinding()]
284
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
285
+ param(
286
+ [AllowNull()][AllowEmptyString()][string] $CommandText,
287
+ [AllowNull()][Nullable[int]] $CommandPrNumber,
288
+ [AllowNull()] $ChildCheckpoint,
289
+ [AllowNull()] $ItemTarget,
290
+ [AllowNull()] $EpicTarget,
291
+ [AllowNull()] $ParallelTarget,
292
+ [AllowNull()][AllowEmptyString()][string] $SessionRoot
293
+ )
294
+
295
+ $silent = [ordered]@{ Allowed = $false; Suffix = '' }
296
+ # Stage A: one resolved item worktree whose checkpoint records a green S9 gate for this PR.
297
+ if ($null -eq $ItemTarget -or @('SessionRoot', 'OtherWorktree') -cnotcontains [string]$ItemTarget.Status) { return $silent }
298
+ if (-not (Test-ChildCheckpointGreenCiRecorded -Checkpoint $ChildCheckpoint -CommandPrNumber $CommandPrNumber)) { return $silent }
299
+ # Stage B: no epic or parallel run records the PR or the item worktree.
300
+ if (-not (Test-EpicMergeGateGreenCiNoRunRecord -EpicTarget $EpicTarget -ParallelTarget $ParallelTarget -WorktreeRoot ([string]$ItemTarget.WorktreeRoot))) { return $silent }
301
+ # Stage C: a single structural gh pr merge naming the PR, with no repository or auto override.
302
+ if ($null -ne (Get-DocsOnlyMergeShapeViolation -CommandText $CommandText -PrNumber $CommandPrNumber)) { return $silent }
303
+ # --admin bypasses branch protection, so a green checkpoint must not authorize it.
304
+ if (Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--admin') { return $silent }
305
+
306
+ # Stage D: the only process call, made after every pure stage passed.
307
+ $liveHead = Get-EpicMergeGateLivePrHeadSha -PrNumber $CommandPrNumber -WorktreeRoot $SessionRoot
308
+ if ($null -eq $liveHead) {
309
+ return [ordered]@{ Allowed = $false; Suffix = ' (green-CI merge not applied: GREEN_CI_LIVE_HEAD_UNRESOLVED)' }
310
+ }
311
+
312
+ # Stage E: a stale checkpoint head is reported before a mismatched --match-head-commit pin.
313
+ $subcommandPath = @('pr', 'merge')
314
+ $pinned = $null
315
+ if (Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--match-head-commit') {
316
+ $pinned = [string](Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--match-head-commit')
317
+ }
318
+ if (Test-EpicMergeGateGreenCiHeadAgreement -Checkpoint $ChildCheckpoint -LiveHeadSha $liveHead -PinnedHeadSha $pinned) {
319
+ return [ordered]@{ Allowed = $true; Suffix = '' }
320
+ }
321
+ if (-not (Test-EpicMergeGateGreenCiHeadAgreement -Checkpoint $ChildCheckpoint -LiveHeadSha $liveHead -PinnedHeadSha $null)) {
322
+ return [ordered]@{ Allowed = $false; Suffix = ' (green-CI merge not applied: GREEN_CI_HEAD_STALE)' }
323
+ }
324
+ return [ordered]@{ Allowed = $false; Suffix = ' (green-CI merge not applied: GREEN_CI_MATCH_HEAD_MISMATCH)' }
325
+ }