@danmoisan/drm-copilot-mcp 1.1.21 → 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.
- package/out/mcp-server.js +2568 -1261
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +1 -1
- package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +4 -4
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/feature-review.md +8 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +46 -6
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +32 -7
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +4 -3
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +2 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -6
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
- package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +48 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +2 -1
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +8 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +15 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +2 -1
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +25 -3
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
- package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
- package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -5
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +2 -2
- package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
- package/resources/claude-customizations/.claude/rules/shell.md +1 -1
- package/resources/claude-customizations/.claude/settings.json +4 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +2 -1
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +11 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +90 -14
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +39 -9
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
- package/resources/claude-customizations/pack-manifests/core.json +14 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +2 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +2 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +2 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
- package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
- package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
- package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
- package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
- package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +2 -1
- package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
- package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
- package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +4 -4
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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`,
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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.
|
|
@@ -50,9 +50,9 @@ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
|
|
53
|
-
# Dot-source the shared validation helpers.
|
|
54
|
-
#
|
|
55
|
-
try { . (Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1') } catch {
|
|
53
|
+
# Dot-source the shared validation helpers (handler H9, issue #984). A failed load is recorded in
|
|
54
|
+
# $script:CompletionHelpersImportFailure and denies only checkpoint writes; other paths are allowed.
|
|
55
|
+
$script:CompletionHelpersImportFailure = $null; try { . (Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1') } catch { $script:CompletionHelpersImportFailure = 'enforce-completion-helpers.ps1' }
|
|
56
56
|
|
|
57
57
|
function ConvertFrom-CheckpointJson {
|
|
58
58
|
<#
|
|
@@ -402,6 +402,7 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
402
402
|
if (-not (Test-IsCheckpointPath -NormalizedPath $normalized)) {
|
|
403
403
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
404
404
|
}
|
|
405
|
+
if ($script:CompletionHelpersImportFailure) { return ConvertTo-CompletionDenyDecision -Reason "COMPLETION_CONSISTENCY_BLOCKED: the shared helpers '$($script:CompletionHelpersImportFailure)' failed to load, so the canonical checkpoint cannot be validated; the gate fails closed." }
|
|
405
406
|
|
|
406
407
|
# Write tool: a present content property is validated directly, and empty
|
|
407
408
|
# content is denied. Edit tool: no content property is supplied, so read the
|
|
@@ -36,7 +36,7 @@ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
|
|
39
|
-
try { Import-Module (Join-Path $PSScriptRoot '../lib/discovery-validation/DiscoveryValidation.psm1') -Force -Global -ErrorAction Stop } catch {
|
|
39
|
+
$script:DiscoveryValidationImportFailure = $null; try { Import-Module (Join-Path $PSScriptRoot '../lib/discovery-validation/DiscoveryValidation.psm1') -Force -Global -ErrorAction Stop } catch { $script:DiscoveryValidationImportFailure = 'DiscoveryValidation.psm1' }
|
|
40
40
|
function Invoke-DiscoveryValidatorExe {
|
|
41
41
|
<#
|
|
42
42
|
.SYNOPSIS
|
|
@@ -201,6 +201,7 @@ function Invoke-DiscoveryArtifactGateDecision {
|
|
|
201
201
|
if (-not $artifactType) {
|
|
202
202
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
203
203
|
}
|
|
204
|
+
if ($script:DiscoveryValidationImportFailure) { return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'deny'; permissionDecisionReason = "DISCOVERY_ARTIFACT_GATE_BLOCKED: the validation module '$($script:DiscoveryValidationImportFailure)' failed to load, so the $artifactType artifact cannot be validated; the gate fails closed." } } }
|
|
204
205
|
|
|
205
206
|
$requiredDeclaration = & $RequiredArtifactReader
|
|
206
207
|
if (-not $requiredDeclaration.Present) {
|
|
@@ -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
|
+
}
|