@danmoisan/drm-copilot-mcp 1.1.25 → 1.1.27
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 +170 -52
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +3 -3
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +2 -2
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +7 -7
- package/resources/claude-customizations/.claude/agents/feature-review.md +6 -3
- package/resources/claude-customizations/.claude/agents/orchestrator.md +11 -7
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +7 -7
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +6 -6
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +16 -1
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
- package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +50 -16
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -1
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +51 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +116 -4
- package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
- package/resources/claude-customizations/.claude/rules/csharp.md +8 -8
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -2
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
- package/resources/claude-customizations/.claude/rules/powershell.md +2 -2
- package/resources/claude-customizations/.claude/settings.json +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +9 -13
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
- package/resources/claude-customizations/.claude/skills/csharp-qa-gate/SKILL.md +14 -15
- package/resources/claude-customizations/.claude/skills/epic-progress-report/SKILL.md +338 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Logic.ps1 +248 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Metrics.ps1 +232 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Render.ps1 +196 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.State.ps1 +286 -0
- package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.ps1 +475 -0
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +31 -16
- package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +21 -9
- package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +5 -5
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +24 -2
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
- package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +16 -17
- package/resources/claude-customizations/.claude/skills/python-qa-gate/SKILL.md +15 -16
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +6 -6
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
- package/resources/claude-customizations/pack-manifests/core.json +15 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +8 -12
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +31 -16
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +5 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +18 -5
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-csharp-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +16 -1
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +3 -3
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents/skills/powershell-qa-gate/SKILL.md +15 -16
- package/resources/codex-and-agents-customizations/.agents/skills/python-qa-gate/SKILL.md +14 -15
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +2 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-executor.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-executor.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-di-unit-test-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +7 -7
- package/resources/codex-and-agents-customizations/.codex/agents/pytest-unit-test-coding.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-executor.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-execution-only-typed.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +29 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +51 -4
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +6 -6
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
- package/resources/customizations/.github/agents/5.1-Beast-adjusted.agent.md +1 -1
- package/resources/customizations/.github/agents/Powershell DI Unit Test Engineer.agent.md +20 -26
- package/resources/customizations/.github/agents/atomic_executor.agent.md +4 -4
- package/resources/customizations/.github/agents/atomic_planning.agent.md +4 -4
- package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
- package/resources/customizations/.github/agents/commit-steward.agent.md +1 -1
- package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +14 -14
- package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +4 -4
- package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +19 -22
- package/resources/customizations/.github/agents/epic-review.agent.md +1 -1
- package/resources/customizations/.github/agents/gpt-5-beast-mode.agent.md +3 -3
- package/resources/customizations/.github/agents/orchestrator.agent.md +8 -8
- package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +12 -12
- package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +4 -4
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +6 -6
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +21 -24
- package/resources/customizations/.github/agents/pytest-unit-test-coding.agent.md +13 -17
- package/resources/customizations/.github/agents/python-atomic-executor.agent.md +11 -11
- package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +14 -17
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +7 -7
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +17 -20
- package/resources/customizations/.github/agents/staged-review.agent.md +2 -2
- package/resources/customizations/.github/agents/tdd-green.agent.md +1 -1
- package/resources/customizations/.github/agents/tdd-red.agent.md +3 -3
- package/resources/customizations/.github/agents/tdd-refactor.agent.md +2 -2
- package/resources/customizations/.github/agents/typescript-engineer.agent.md +3 -3
- package/resources/customizations/.github/codex/execute-hard-lock.prompt.md +3 -3
- package/resources/customizations/.github/instructions/general-code-change.instructions.md +1 -1
- package/resources/customizations/.github/instructions/powershell-unit-test.instructions.md +1 -1
- package/resources/customizations/.github/instructions/python-code-change.instructions.md +5 -5
- package/resources/customizations/.github/instructions/python-suppressions.instructions.md +3 -3
- package/resources/customizations/.github/instructions/python-unit-test.instructions.md +1 -1
- package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +2 -2
- package/resources/customizations/.github/prompts/generate-commit-message-repo.prompt.md +2 -2
- package/resources/customizations/.github/prompts/remediate-comments.prompt.md +2 -2
- package/resources/customizations/.github/prompts/review-feature.prompt.md +1 -1
- package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +8 -12
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +31 -16
- package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +15 -2
- package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +4 -4
- package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +3 -3
- package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +2 -2
- package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +14 -12
- package/resources/powershell/PoshQC/PoshQC.psm1 +3 -3
- package/resources/powershell/PoshQC/README.md +8 -8
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -2
- package/resources/templates/policy_audit/AGENTS.md +9 -10
- package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +19 -20
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Review guard of the merge gate under review_mode concurrent (issue #1097).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Dot-sourced by enforce-epic-merge-gate.ps1 in both runtimes; this file is byte-identical in the
|
|
7
|
+
Claude and Codex hook folders. Each gate calls Get-EpicMergeGateReviewShapeDecision and then
|
|
8
|
+
Get-EpicMergeGateReviewDecision with the per-feature checkpoint it read, before any allow
|
|
9
|
+
branch. Under the key, a command the gates read reaches no allow branch without a passing review
|
|
10
|
+
at a current head, and text that could be a merge and cannot be bound is denied.
|
|
11
|
+
|
|
12
|
+
The key is the top-level checkpoint key review_mode. Only the exact string concurrent activates
|
|
13
|
+
the guard, compared ordinally; an absent key, null, sequential, a case variant, a number, and
|
|
14
|
+
every other non-string value mean sequential, and the guard then starts no gh call and denies
|
|
15
|
+
nothing; only the Claude shape read below can still resolve worktrees, for at most 4 numbers.
|
|
16
|
+
Test-EpicMergeGateReviewModeConcurrent holds that predicate.
|
|
17
|
+
|
|
18
|
+
Shape step, before the out-of-scope allow and the order below: text that could be a merge
|
|
19
|
+
(Test-EpicMergeGateReviewMergeText) and is not one plain gh pr merge of one number
|
|
20
|
+
(Get-EpicMergeGateReviewBoundPrNumber) is denied with REVIEW_GATE_HEAD_UNRESOLVED, before any
|
|
21
|
+
process, when a checkpoint the gate can read carries the key. Without the key the decision is
|
|
22
|
+
unchanged. On Claude an unbound command also reads the session and the item of each of the first
|
|
23
|
+
4 numbers in its text (EpicMergeGateReviewCandidateCap), which can resolve worktrees; that read
|
|
24
|
+
is best-effort, so a failure sees no key and never denies.
|
|
25
|
+
|
|
26
|
+
Evaluation order; the first match ends it:
|
|
27
|
+
1. The checkpoint is absent or the key is not exactly concurrent: inert, no process.
|
|
28
|
+
2. The command names no pull request number: REVIEW_GATE_HEAD_UNRESOLVED, before any process.
|
|
29
|
+
Only a direct call of Get-EpicMergeGateReviewDecision reaches this step, because under
|
|
30
|
+
both gates the shape step denies a number-less merge text first.
|
|
31
|
+
3. The last remediation_loop.review_outcomes entry is missing, is not an object, or its
|
|
32
|
+
verdict is not exactly PASS: REVIEW_GATE_NOT_PASSED, before any process.
|
|
33
|
+
4. Its head_sha is absent or malformed: REVIEW_GATE_NOT_PASSED, before any process.
|
|
34
|
+
5. The one bounded gh pr view <PR> --json headRefOid fails or is malformed:
|
|
35
|
+
REVIEW_GATE_HEAD_UNRESOLVED.
|
|
36
|
+
6. head_sha equals the live head: pass.
|
|
37
|
+
7. git merge-base --is-ancestor <reviewed> <live> exits 1: REVIEW_GATE_HEAD_STALE.
|
|
38
|
+
8. That command exits anything other than 0 or 1: REVIEW_GATE_HEAD_UNRESOLVED.
|
|
39
|
+
9. feature-folder is not a clean path below docs/features/: REVIEW_GATE_HEAD_UNRESOLVED.
|
|
40
|
+
10. git diff --name-only -z of the reviewed and live heads fails, or its non-empty output does
|
|
41
|
+
not end with NUL: REVIEW_GATE_HEAD_UNRESOLVED.
|
|
42
|
+
11. A listed path does not start with <feature-folder>/ (ordinal): REVIEW_GATE_HEAD_STALE,
|
|
43
|
+
naming the path.
|
|
44
|
+
12. Otherwise, including an empty listing: pass.
|
|
45
|
+
|
|
46
|
+
A code change after the review is therefore stale, including a merge of origin/main, because
|
|
47
|
+
that diff lists code paths. The hook never fetches: a head that is not present locally fails
|
|
48
|
+
step 7 or 8 and denies. The guard applies to whichever checkpoint its gate hands it, with no
|
|
49
|
+
binding exemption, so an explicit PR number that the checkpoint does not bind fails at the head
|
|
50
|
+
comparison; an exemption for an unbound number would open a bypass on Codex, whose child branch
|
|
51
|
+
requires no PR binding.
|
|
52
|
+
|
|
53
|
+
Limits (issue #1097 decision D10): this guard is a policy-level control, not a security control.
|
|
54
|
+
It gates only Bash merge text it can read against a checkpoint it can read, so a same-session
|
|
55
|
+
checkpoint edit, a merge through another tool or surface, an item target that does not resolve,
|
|
56
|
+
and text it cannot read are not gated. The extra read covers at most the first 4 numbers of the
|
|
57
|
+
text, so a key held only by an item named after them is not found. Under the key the rule is
|
|
58
|
+
fail-closed and its trigger is wider than pr merge: prose such as pr (#12) or pr $HOME is denied,
|
|
59
|
+
and so is a plain merge followed by a line feed or ;. D10 in .claude/rules/orchestrator-state.md
|
|
60
|
+
(## Concurrent-Review Scope and Backward Compatibility) lists every limit.
|
|
61
|
+
|
|
62
|
+
.NOTES
|
|
63
|
+
Compatible with PowerShell 7+. This file dot-sources nothing and imports no module. It calls, at
|
|
64
|
+
run time only, Invoke-DocsOnlyGhExe, Invoke-DocsOnlyGitExe, and ConvertFrom-DocsOnlyNameList
|
|
65
|
+
(hook-docs-only-diff.ps1), which the gate loads before it calls this file's functions and of
|
|
66
|
+
which the two process wrappers bound each call and never throw. It also calls
|
|
67
|
+
Read-CommandLineInvocationSegment, ConvertTo-CommandLineNormalizedText, Get-CommandLineGlobalOption,
|
|
68
|
+
Get-CommandLineStructuralWalk, and Test-CommandLineTokenLiteral (hook-command-invocation.ps1 and
|
|
69
|
+
its siblings), and Test-EpicMergeGateApiMutationText and Get-EpicMergeGateSelectorPrNumber
|
|
70
|
+
(enforce-epic-merge-gate-command.ps1). Read-EpicMergeGateReviewItemCheckpoint and
|
|
71
|
+
Get-EpicMergeGateReviewUnscopedDecision are Claude-only: the Codex gate passes no reader.
|
|
72
|
+
The Claude gate runs gh in the session worktree and git in the item worktree; the Codex gate
|
|
73
|
+
runs both in the repository root.
|
|
74
|
+
#>
|
|
75
|
+
|
|
76
|
+
# Dash options and their values that may sit between two subcommand words of one invocation.
|
|
77
|
+
$script:EpicMergeGateReviewOptionGap = '(?:\s+-\S+(?:\s+[^\s-]\S*)?)*'
|
|
78
|
+
|
|
79
|
+
# Text that could be a merge, matched ordinally over the scan text: pr then merge, and pr then a
|
|
80
|
+
# subcommand the text cannot show, because a variable, substitution, or subshell supplies it.
|
|
81
|
+
$script:EpicMergeGateReviewMergePattern = '(?<![\w-])pr' + $script:EpicMergeGateReviewOptionGap + '\s+merge(?![\w-])'
|
|
82
|
+
$script:EpicMergeGateReviewDynamicPattern = '(?<![\w-])pr' + $script:EpicMergeGateReviewOptionGap + '\s+[$`(]'
|
|
83
|
+
|
|
84
|
+
# The REST merge path with any segment in the number position, and a GraphQL call.
|
|
85
|
+
$script:EpicMergeGateReviewRestPattern = '(?<![\w-])pulls/[^\s/]+/merge(?![\w-])'
|
|
86
|
+
$script:EpicMergeGateReviewGraphqlPattern = '(?<![\w-])api' + $script:EpicMergeGateReviewOptionGap + '\s+graphql(?![\w-])'
|
|
87
|
+
$script:EpicMergeGateReviewMutationPattern = '(?<![\w-])mutation(?![\w-])'
|
|
88
|
+
|
|
89
|
+
# The gh api flags that carry a GraphQL field, and the shell words that may lead a segment of a
|
|
90
|
+
# poll loop around a read-only query.
|
|
91
|
+
$script:EpicMergeGateReviewFieldFlags = @('-f', '-F', '--raw-field', '--field')
|
|
92
|
+
$script:EpicMergeGateReviewShellKeywords = @('do', 'then', 'else', 'elif', 'if', 'while', 'until', '!')
|
|
93
|
+
|
|
94
|
+
# The gh pr merge flags a bound command may carry. This is an allowlist, so --auto, --admin, -R,
|
|
95
|
+
# --repo, a redirection, and every unknown token fail by absence.
|
|
96
|
+
$script:EpicMergeGateReviewBoundFlags = @('--merge', '-m', '--squash', '-s', '--rebase', '-r', '--delete-branch', '-d', '--disable-auto')
|
|
97
|
+
$script:EpicMergeGateReviewBoundValueFlags = @('--match-head-commit', '--subject', '-t', '--body', '-b', '--body-file', '-F', '--author-email', '-A')
|
|
98
|
+
|
|
99
|
+
# The most candidate numbers the Claude shape read resolves, in text order, so an unbound command
|
|
100
|
+
# costs a bounded number of worktree resolutions (issue #1097 decision D10).
|
|
101
|
+
$script:EpicMergeGateReviewCandidateCap = 4
|
|
102
|
+
|
|
103
|
+
function Test-EpicMergeGateReviewModeConcurrent {
|
|
104
|
+
<#
|
|
105
|
+
.SYNOPSIS
|
|
106
|
+
True when a parsed checkpoint carries review_mode exactly equal to concurrent (pure).
|
|
107
|
+
.DESCRIPTION
|
|
108
|
+
The value is tested with -is [string] first, because -ceq on an array filters rather than
|
|
109
|
+
compares. The guard and the readiness relaxation share this meaning.
|
|
110
|
+
.PARAMETER Checkpoint
|
|
111
|
+
The parsed per-feature checkpoint, or $null when absent or unreadable.
|
|
112
|
+
.OUTPUTS
|
|
113
|
+
System.Boolean
|
|
114
|
+
#>
|
|
115
|
+
[CmdletBinding()]
|
|
116
|
+
[OutputType([bool])]
|
|
117
|
+
param([AllowNull()] $Checkpoint)
|
|
118
|
+
|
|
119
|
+
if ($Checkpoint -isnot [System.Management.Automation.PSCustomObject]) { return $false }
|
|
120
|
+
if (@($Checkpoint.PSObject.Properties.Name) -cnotcontains 'review_mode') { return $false }
|
|
121
|
+
$mode = $Checkpoint.review_mode
|
|
122
|
+
return ($mode -is [string]) -and ($mode -ceq 'concurrent')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Test-EpicMergeGateReviewObjectId {
|
|
126
|
+
<#
|
|
127
|
+
.SYNOPSIS
|
|
128
|
+
True when a value is a full lowercase hexadecimal object id of 40 or 64 characters.
|
|
129
|
+
.PARAMETER Value
|
|
130
|
+
The candidate value; any non-string value returns $false.
|
|
131
|
+
.OUTPUTS
|
|
132
|
+
System.Boolean
|
|
133
|
+
#>
|
|
134
|
+
[CmdletBinding()]
|
|
135
|
+
[OutputType([bool])]
|
|
136
|
+
param([AllowNull()] $Value)
|
|
137
|
+
|
|
138
|
+
return ($Value -is [string]) -and ($Value -cmatch '\A[0-9a-f]{40}([0-9a-f]{24})?\z')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function Get-EpicMergeGateLastReviewOutcome {
|
|
142
|
+
<#
|
|
143
|
+
.SYNOPSIS
|
|
144
|
+
Return the last remediation_loop.review_outcomes entry when it is an object, else $null.
|
|
145
|
+
.DESCRIPTION
|
|
146
|
+
Last-entry semantics: only the final list element counts, so an earlier PASS never
|
|
147
|
+
satisfies the guard. A missing loop, a value that is not a list, an empty list, and a final
|
|
148
|
+
element that is not an object all return $null.
|
|
149
|
+
.PARAMETER Checkpoint
|
|
150
|
+
The parsed per-feature checkpoint.
|
|
151
|
+
.OUTPUTS
|
|
152
|
+
System.Management.Automation.PSCustomObject or $null
|
|
153
|
+
#>
|
|
154
|
+
[CmdletBinding()]
|
|
155
|
+
[OutputType([pscustomobject])]
|
|
156
|
+
param([AllowNull()] $Checkpoint)
|
|
157
|
+
|
|
158
|
+
$loop = $Checkpoint.remediation_loop
|
|
159
|
+
if ($loop -isnot [System.Management.Automation.PSCustomObject]) { return $null }
|
|
160
|
+
$outcomes = $loop.review_outcomes
|
|
161
|
+
if ($outcomes -isnot [System.Array] -or $outcomes.Count -eq 0) { return $null }
|
|
162
|
+
$last = $outcomes[$outcomes.Count - 1]
|
|
163
|
+
if ($last -isnot [System.Management.Automation.PSCustomObject]) { return $null }
|
|
164
|
+
return $last
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Test-EpicMergeGateReviewFeatureFolder {
|
|
168
|
+
<#
|
|
169
|
+
.SYNOPSIS
|
|
170
|
+
True when a value is a clean relative path below docs/features/ (pure).
|
|
171
|
+
.DESCRIPTION
|
|
172
|
+
A blank value, a value outside docs/features/ (which includes every absolute path), a
|
|
173
|
+
backslash, an empty segment (which includes a trailing slash), and a dot segment all return
|
|
174
|
+
$false, so a value such as . cannot widen the feature-folder rule.
|
|
175
|
+
.PARAMETER Value
|
|
176
|
+
The candidate feature-folder value; any non-string value returns $false.
|
|
177
|
+
.OUTPUTS
|
|
178
|
+
System.Boolean
|
|
179
|
+
#>
|
|
180
|
+
[CmdletBinding()]
|
|
181
|
+
[OutputType([bool])]
|
|
182
|
+
param([AllowNull()] $Value)
|
|
183
|
+
|
|
184
|
+
if ($Value -isnot [string] -or [string]::IsNullOrWhiteSpace($Value)) { return $false }
|
|
185
|
+
if (-not $Value.StartsWith('docs/features/', [System.StringComparison]::Ordinal)) { return $false }
|
|
186
|
+
if ($Value.IndexOf([char]92) -ge 0) { return $false }
|
|
187
|
+
foreach ($segment in $Value.Split('/')) {
|
|
188
|
+
if ($segment.Length -eq 0 -or $segment -ceq '.' -or $segment -ceq '..') { return $false }
|
|
189
|
+
}
|
|
190
|
+
return $true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function Test-EpicMergeGateReviewPathInFolder {
|
|
194
|
+
<#
|
|
195
|
+
.SYNOPSIS
|
|
196
|
+
True when a repository-relative path lies inside the feature folder (pure, ordinal).
|
|
197
|
+
.DESCRIPTION
|
|
198
|
+
The prefix carries the separator, so a sibling folder whose name extends the feature folder
|
|
199
|
+
does not match.
|
|
200
|
+
.PARAMETER Path
|
|
201
|
+
The repository-relative path a diff listed.
|
|
202
|
+
.PARAMETER FeatureFolder
|
|
203
|
+
The validated feature folder.
|
|
204
|
+
.OUTPUTS
|
|
205
|
+
System.Boolean
|
|
206
|
+
#>
|
|
207
|
+
[CmdletBinding()]
|
|
208
|
+
[OutputType([bool])]
|
|
209
|
+
param(
|
|
210
|
+
[Parameter(Mandatory)][string] $Path,
|
|
211
|
+
[Parameter(Mandatory)][string] $FeatureFolder
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return $Path.StartsWith($FeatureFolder + '/', [System.StringComparison]::Ordinal)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Get-EpicMergeGateReviewLiveHeadSha {
|
|
218
|
+
<#
|
|
219
|
+
.SYNOPSIS
|
|
220
|
+
Return the live head commit of a pull request, or $null on any failure (the live-head seam).
|
|
221
|
+
.DESCRIPTION
|
|
222
|
+
Runs gh pr view <PR> --json headRefOid through Invoke-DocsOnlyGhExe, which bounds the call
|
|
223
|
+
and never throws. This is the guard's own seam, because the green-CI sibling that holds a
|
|
224
|
+
similar function exists only on the Claude runtime. A non-zero exit, output that does not
|
|
225
|
+
parse to a JSON object, and a malformed head all return $null. Tests mock this by name.
|
|
226
|
+
.PARAMETER PrNumber
|
|
227
|
+
The pull request number.
|
|
228
|
+
.PARAMETER WorktreeRoot
|
|
229
|
+
The directory gh runs in.
|
|
230
|
+
.OUTPUTS
|
|
231
|
+
System.String or $null
|
|
232
|
+
#>
|
|
233
|
+
[CmdletBinding()]
|
|
234
|
+
[OutputType([string])]
|
|
235
|
+
param(
|
|
236
|
+
[Parameter(Mandatory)][int] $PrNumber,
|
|
237
|
+
[AllowNull()][AllowEmptyString()][string] $WorktreeRoot
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
$lookup = Invoke-DocsOnlyGhExe -WorkingRoot $WorktreeRoot -GhArgs @('pr', 'view', [string]$PrNumber, '--json', 'headRefOid')
|
|
241
|
+
if ($null -eq $lookup -or $lookup.ExitCode -ne 0) { return $null }
|
|
242
|
+
try {
|
|
243
|
+
$pr = ConvertFrom-Json -InputObject ([string]$lookup.Output) -ErrorAction Stop
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
# Unreadable gh output is a lookup failure; the guard fails closed on $null.
|
|
247
|
+
return $null
|
|
248
|
+
}
|
|
249
|
+
if ($pr -isnot [System.Management.Automation.PSCustomObject]) { return $null }
|
|
250
|
+
if (-not (Test-EpicMergeGateReviewObjectId -Value $pr.headRefOid)) { return $null }
|
|
251
|
+
return [string]$pr.headRefOid
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function Get-EpicMergeGateReviewHeadState {
|
|
255
|
+
<#
|
|
256
|
+
.SYNOPSIS
|
|
257
|
+
Classify a reviewed head against the live head as Current, Stale, or Unresolved.
|
|
258
|
+
.DESCRIPTION
|
|
259
|
+
Evaluates steps 6 to 12 of the file header in order. A later head is Current only when the
|
|
260
|
+
reviewed head is its ancestor and every path changed since then lies inside the feature
|
|
261
|
+
folder, so review artifacts and evidence do not force a new review.
|
|
262
|
+
.PARAMETER ReviewedHead
|
|
263
|
+
The head_sha of the last review outcome.
|
|
264
|
+
.PARAMETER LiveHead
|
|
265
|
+
The live pull-request head.
|
|
266
|
+
.PARAMETER FeatureFolder
|
|
267
|
+
The checkpoint's feature-folder value, not yet validated.
|
|
268
|
+
.PARAMETER GitRoot
|
|
269
|
+
The directory git runs in.
|
|
270
|
+
.OUTPUTS
|
|
271
|
+
System.Collections.Specialized.OrderedDictionary with State and Detail.
|
|
272
|
+
#>
|
|
273
|
+
[CmdletBinding()]
|
|
274
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
275
|
+
param(
|
|
276
|
+
[Parameter(Mandatory)][string] $ReviewedHead,
|
|
277
|
+
[Parameter(Mandatory)][string] $LiveHead,
|
|
278
|
+
[AllowNull()] $FeatureFolder,
|
|
279
|
+
[AllowNull()][AllowEmptyString()][string] $GitRoot
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if ([string]::Equals($ReviewedHead, $LiveHead, [System.StringComparison]::Ordinal)) {
|
|
283
|
+
return [ordered]@{ State = 'Current'; Detail = 'the reviewed head is the live head.' }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
$ancestry = Invoke-DocsOnlyGitExe -WorktreeRoot $GitRoot -GitArgs @('merge-base', '--is-ancestor', $ReviewedHead, $LiveHead)
|
|
287
|
+
$ancestryExit = if ($null -eq $ancestry) { -1 } else { [int]$ancestry.ExitCode }
|
|
288
|
+
if ($ancestryExit -eq 1) {
|
|
289
|
+
return [ordered]@{ State = 'Stale'; Detail = "the reviewed head $ReviewedHead is not an ancestor of the live head $LiveHead." }
|
|
290
|
+
}
|
|
291
|
+
if ($ancestryExit -ne 0) {
|
|
292
|
+
return [ordered]@{ State = 'Unresolved'; Detail = "git merge-base could not compare the reviewed head $ReviewedHead with the live head $LiveHead; fetch the pull request head, then retry. This hook does not fetch." }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (-not (Test-EpicMergeGateReviewFeatureFolder -Value $FeatureFolder)) {
|
|
296
|
+
return [ordered]@{ State = 'Unresolved'; Detail = 'the checkpoint feature-folder value is not a clean path below docs/features/.' }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
$diff = Invoke-DocsOnlyGitExe -WorktreeRoot $GitRoot -GitArgs @('diff', '--name-only', '-z', '--no-renames', '--no-color', '--no-ext-diff', '--no-relative', $ReviewedHead, $LiveHead)
|
|
300
|
+
if ($null -eq $diff -or $diff.ExitCode -ne 0) {
|
|
301
|
+
return [ordered]@{ State = 'Unresolved'; Detail = "git diff of the reviewed head $ReviewedHead and the live head $LiveHead could not be computed." }
|
|
302
|
+
}
|
|
303
|
+
$listing = [string]$diff.Output
|
|
304
|
+
if ($listing.Length -gt 0 -and -not $listing.EndsWith([char]0)) {
|
|
305
|
+
return [ordered]@{ State = 'Unresolved'; Detail = 'git diff did not return NUL-terminated output.' }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
foreach ($path in @(ConvertFrom-DocsOnlyNameList -Text $listing)) {
|
|
309
|
+
if (-not (Test-EpicMergeGateReviewPathInFolder -Path $path -FeatureFolder $FeatureFolder)) {
|
|
310
|
+
return [ordered]@{ State = 'Stale'; Detail = "the path $path changed after the reviewed head $ReviewedHead and lies outside $FeatureFolder." }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return [ordered]@{ State = 'Current'; Detail = 'only feature-folder paths changed after the reviewed head.' }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function Get-EpicMergeGateReviewDecision {
|
|
317
|
+
<#
|
|
318
|
+
.SYNOPSIS
|
|
319
|
+
Evaluate the review guard for one merge command and return Denied and Reason.
|
|
320
|
+
.DESCRIPTION
|
|
321
|
+
Applies steps 1 to 5 of the file header and delegates steps 6 to 12 to
|
|
322
|
+
Get-EpicMergeGateReviewHeadState. Every deny reason reads
|
|
323
|
+
EPIC_MERGE_GATE_BLOCKED: <code>: <message>. Without the key it returns immediately and
|
|
324
|
+
starts no process.
|
|
325
|
+
.PARAMETER Checkpoint
|
|
326
|
+
The parsed per-feature checkpoint, or $null.
|
|
327
|
+
.PARAMETER CommandPrNumber
|
|
328
|
+
The explicit PR number the command names, or $null for a bare command.
|
|
329
|
+
.PARAMETER GhRoot
|
|
330
|
+
The directory gh runs in.
|
|
331
|
+
.PARAMETER GitRoot
|
|
332
|
+
The directory git runs in.
|
|
333
|
+
.OUTPUTS
|
|
334
|
+
System.Collections.Specialized.OrderedDictionary with Denied and Reason.
|
|
335
|
+
#>
|
|
336
|
+
[CmdletBinding()]
|
|
337
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
338
|
+
param(
|
|
339
|
+
[AllowNull()] $Checkpoint,
|
|
340
|
+
[AllowNull()][Nullable[int]] $CommandPrNumber,
|
|
341
|
+
[AllowNull()][AllowEmptyString()][string] $GhRoot,
|
|
342
|
+
[AllowNull()][AllowEmptyString()][string] $GitRoot
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
$pass = [ordered]@{ Denied = $false; Reason = $null }
|
|
346
|
+
if (-not (Test-EpicMergeGateReviewModeConcurrent -Checkpoint $Checkpoint)) { return $pass }
|
|
347
|
+
|
|
348
|
+
if ($null -eq $CommandPrNumber) {
|
|
349
|
+
return [ordered]@{ Denied = $true; Reason = 'EPIC_MERGE_GATE_BLOCKED: REVIEW_GATE_HEAD_UNRESOLVED: review_mode concurrent requires a merge command that names the pull request number.' }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
$outcome = Get-EpicMergeGateLastReviewOutcome -Checkpoint $Checkpoint
|
|
353
|
+
if ($null -eq $outcome -or $outcome.verdict -isnot [string] -or $outcome.verdict -cne 'PASS') {
|
|
354
|
+
return [ordered]@{ Denied = $true; Reason = 'EPIC_MERGE_GATE_BLOCKED: REVIEW_GATE_NOT_PASSED: review_mode concurrent requires a last review_outcomes entry with verdict PASS.' }
|
|
355
|
+
}
|
|
356
|
+
if (-not (Test-EpicMergeGateReviewObjectId -Value $outcome.head_sha)) {
|
|
357
|
+
return [ordered]@{ Denied = $true; Reason = 'EPIC_MERGE_GATE_BLOCKED: REVIEW_GATE_NOT_PASSED: review_mode concurrent requires a full lowercase head_sha on the last review_outcomes entry.' }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
$liveHead = Get-EpicMergeGateReviewLiveHeadSha -PrNumber $CommandPrNumber -WorktreeRoot $GhRoot
|
|
361
|
+
if ($null -eq $liveHead) {
|
|
362
|
+
return [ordered]@{ Denied = $true; Reason = "EPIC_MERGE_GATE_BLOCKED: REVIEW_GATE_HEAD_UNRESOLVED: the live head of pull request $CommandPrNumber could not be read." }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
$headState = Get-EpicMergeGateReviewHeadState -ReviewedHead ([string]$outcome.head_sha) -LiveHead $liveHead -FeatureFolder $Checkpoint.'feature-folder' -GitRoot $GitRoot
|
|
366
|
+
if ($headState.State -ceq 'Current') { return $pass }
|
|
367
|
+
$code = if ($headState.State -ceq 'Stale') { 'REVIEW_GATE_HEAD_STALE' } else { 'REVIEW_GATE_HEAD_UNRESOLVED' }
|
|
368
|
+
return [ordered]@{ Denied = $true; Reason = "EPIC_MERGE_GATE_BLOCKED: ${code}: $($headState.Detail)" }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function ConvertTo-EpicMergeGateReviewScanText {
|
|
372
|
+
<#
|
|
373
|
+
.SYNOPSIS
|
|
374
|
+
Return command text as the shell would join it, for a text scan (pure).
|
|
375
|
+
.DESCRIPTION
|
|
376
|
+
Removes each backslash line continuation, then every backslash and quote character, so
|
|
377
|
+
an escaped or quoted word reads as the word the shell passes on. RemoveBacktick also
|
|
378
|
+
removes backticks, which are escape characters in PowerShell.
|
|
379
|
+
.PARAMETER CommandText
|
|
380
|
+
The raw command text.
|
|
381
|
+
.PARAMETER RemoveBacktick
|
|
382
|
+
Produce the second scan form.
|
|
383
|
+
.OUTPUTS
|
|
384
|
+
System.String
|
|
385
|
+
#>
|
|
386
|
+
[CmdletBinding()]
|
|
387
|
+
[OutputType([string])]
|
|
388
|
+
param(
|
|
389
|
+
[Parameter(Mandatory)][AllowEmptyString()][string] $CommandText,
|
|
390
|
+
[switch] $RemoveBacktick
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
$text = [regex]::Replace($CommandText, '\\\r?\n', '')
|
|
394
|
+
$text = [regex]::Replace($text, '[\\"'']', '')
|
|
395
|
+
if ($RemoveBacktick) { $text = $text.Replace('`', '') }
|
|
396
|
+
return $text
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function Test-EpicMergeGateReviewQueryField {
|
|
400
|
+
<#
|
|
401
|
+
.SYNOPSIS
|
|
402
|
+
True when gh api graphql tokens carry a query field and every one is read-only (pure).
|
|
403
|
+
.PARAMETER Token
|
|
404
|
+
The tokens of the invocation.
|
|
405
|
+
.PARAMETER StartIndex
|
|
406
|
+
The position just past the subcommand path.
|
|
407
|
+
.OUTPUTS
|
|
408
|
+
System.Boolean
|
|
409
|
+
#>
|
|
410
|
+
[CmdletBinding()]
|
|
411
|
+
[OutputType([bool])]
|
|
412
|
+
param(
|
|
413
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token,
|
|
414
|
+
[Parameter(Mandatory)][int] $StartIndex
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
$found = $false
|
|
418
|
+
for ($index = $StartIndex; $index -lt $Token.Count; $index++) {
|
|
419
|
+
if ($Token[$index].StartsWith('--input', [System.StringComparison]::Ordinal)) { return $false }
|
|
420
|
+
if ($script:EpicMergeGateReviewFieldFlags -cnotcontains $Token[$index] -or $index + 1 -ge $Token.Count) { continue }
|
|
421
|
+
$field = $Token[$index + 1]
|
|
422
|
+
if (-not $field.StartsWith('query=', [System.StringComparison]::Ordinal)) { continue }
|
|
423
|
+
if ($field -cnotmatch '\Aquery=\s*(?:query|\{)') { return $false }
|
|
424
|
+
$found = $true
|
|
425
|
+
}
|
|
426
|
+
return $found
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function Test-EpicMergeGateReviewGraphqlReadOnly {
|
|
430
|
+
<#
|
|
431
|
+
.SYNOPSIS
|
|
432
|
+
True when every gh api graphql occurrence of a command is proven read-only (pure).
|
|
433
|
+
.DESCRIPTION
|
|
434
|
+
Each textual occurrence must be a structural invocation, after leading shell keywords, whose
|
|
435
|
+
query field is a literal query, with no --input token and no word mutation in the text. A
|
|
436
|
+
command with no occurrence returns $true. The proof does not require a sole segment, so a
|
|
437
|
+
poll loop around the query stays allowed. An argument-injected record proves nothing.
|
|
438
|
+
.OUTPUTS
|
|
439
|
+
System.Boolean
|
|
440
|
+
#>
|
|
441
|
+
[CmdletBinding()]
|
|
442
|
+
[OutputType([bool])]
|
|
443
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
444
|
+
|
|
445
|
+
$scan = ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText
|
|
446
|
+
$occurrences = [regex]::Matches($scan, $script:EpicMergeGateReviewGraphqlPattern).Count
|
|
447
|
+
if ($occurrences -eq 0) { return $true }
|
|
448
|
+
if ([regex]::IsMatch($scan, $script:EpicMergeGateReviewMutationPattern)) { return $false }
|
|
449
|
+
|
|
450
|
+
$option = Get-CommandLineGlobalOption -CommandWord 'gh'
|
|
451
|
+
$proven = 0
|
|
452
|
+
foreach ($record in @(Read-CommandLineInvocationSegment -CommandText $CommandText)) {
|
|
453
|
+
if ($record.ArgumentInjected) { continue }
|
|
454
|
+
$tokens = [string[]]@($record.Tokens)
|
|
455
|
+
$start = 0
|
|
456
|
+
while ($start -lt $tokens.Count -and $script:EpicMergeGateReviewShellKeywords -ccontains $tokens[$start]) { $start++ }
|
|
457
|
+
$command = [string[]]@($tokens | Select-Object -Skip $start)
|
|
458
|
+
$walk = Get-CommandLineStructuralWalk -Token $command -CommandWord 'gh' -SubcommandPath @('api', 'graphql') -Option $option
|
|
459
|
+
if ($walk.Outcome -ceq 'Match' -and (Test-EpicMergeGateReviewQueryField -Token $command -StartIndex $walk.OperandIndex)) { $proven++ }
|
|
460
|
+
}
|
|
461
|
+
return $proven -eq $occurrences
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function Test-EpicMergeGateReviewApiTrigger {
|
|
465
|
+
<#
|
|
466
|
+
.SYNOPSIS
|
|
467
|
+
True when command text could reach a merge through an API (pure).
|
|
468
|
+
.DESCRIPTION
|
|
469
|
+
Names a merge or enqueue mutation, holds a pulls/<segment>/merge path, or holds a gh api
|
|
470
|
+
graphql occurrence that is not proven read-only.
|
|
471
|
+
.OUTPUTS
|
|
472
|
+
System.Boolean
|
|
473
|
+
#>
|
|
474
|
+
[CmdletBinding()]
|
|
475
|
+
[OutputType([bool])]
|
|
476
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
477
|
+
|
|
478
|
+
foreach ($scan in @((ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText), (ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText -RemoveBacktick))) {
|
|
479
|
+
if ((Test-EpicMergeGateApiMutationText -Text $scan) -or [regex]::IsMatch($scan, $script:EpicMergeGateReviewRestPattern)) { return $true }
|
|
480
|
+
}
|
|
481
|
+
return -not (Test-EpicMergeGateReviewGraphqlReadOnly -CommandText $CommandText)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function Test-EpicMergeGateReviewMergeText {
|
|
485
|
+
<#
|
|
486
|
+
.SYNOPSIS
|
|
487
|
+
True when command text could be a merge (pure).
|
|
488
|
+
.DESCRIPTION
|
|
489
|
+
The trigger needs no gh word, so it also reads a variable or path in the command position
|
|
490
|
+
and an alias definition. It is deliberately wider than the scope predicate, and it decides
|
|
491
|
+
only whether the shape rule looks further: without the key the decision does not change.
|
|
492
|
+
.OUTPUTS
|
|
493
|
+
System.Boolean
|
|
494
|
+
#>
|
|
495
|
+
[CmdletBinding()]
|
|
496
|
+
[OutputType([bool])]
|
|
497
|
+
param([Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText)
|
|
498
|
+
|
|
499
|
+
if ([string]::IsNullOrEmpty($CommandText)) { return $false }
|
|
500
|
+
foreach ($scan in @((ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText), (ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText -RemoveBacktick))) {
|
|
501
|
+
if ([regex]::IsMatch($scan, $script:EpicMergeGateReviewMergePattern) -or [regex]::IsMatch($scan, $script:EpicMergeGateReviewDynamicPattern)) { return $true }
|
|
502
|
+
}
|
|
503
|
+
return Test-EpicMergeGateReviewApiTrigger -CommandText $CommandText
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function Get-EpicMergeGateReviewBoundTokenNumber {
|
|
507
|
+
<#
|
|
508
|
+
.SYNOPSIS
|
|
509
|
+
Return the one all-digit selector of gh pr merge tokens, or $null (pure).
|
|
510
|
+
.DESCRIPTION
|
|
511
|
+
The first three tokens must equal gh, pr, and merge ordinally. The rest must hold exactly
|
|
512
|
+
one selector and otherwise only allowlisted flags, each value flag with a literal value.
|
|
513
|
+
.PARAMETER Token
|
|
514
|
+
The tokens of the one segment.
|
|
515
|
+
.OUTPUTS
|
|
516
|
+
System.Nullable[int]
|
|
517
|
+
#>
|
|
518
|
+
[CmdletBinding()]
|
|
519
|
+
[OutputType([int])]
|
|
520
|
+
param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token)
|
|
521
|
+
|
|
522
|
+
if ($Token.Count -lt 4 -or $Token[0] -cne 'gh' -or $Token[1] -cne 'pr' -or $Token[2] -cne 'merge') { return $null }
|
|
523
|
+
$selector = $null
|
|
524
|
+
for ($index = 3; $index -lt $Token.Count; $index++) {
|
|
525
|
+
$current = $Token[$index]
|
|
526
|
+
if ($script:EpicMergeGateReviewBoundFlags -ccontains $current) { continue }
|
|
527
|
+
if ($script:EpicMergeGateReviewBoundValueFlags -ccontains $current) {
|
|
528
|
+
$index++
|
|
529
|
+
if ($index -ge $Token.Count -or -not (Test-CommandLineTokenLiteral -Token $Token[$index])) { return $null }
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
if ($null -ne $selector -or $current -cnotmatch '\A[0-9]+\z') { return $null }
|
|
533
|
+
$selector = $current
|
|
534
|
+
}
|
|
535
|
+
$number = 0
|
|
536
|
+
if ($null -ne $selector -and [int]::TryParse($selector, [ref] $number)) { return $number }
|
|
537
|
+
return $null
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function Get-EpicMergeGateReviewBoundPrNumber {
|
|
541
|
+
<#
|
|
542
|
+
.SYNOPSIS
|
|
543
|
+
Return the pull request number of a command that is one plain merge, or $null (pure).
|
|
544
|
+
.DESCRIPTION
|
|
545
|
+
The command must be exactly one top-level segment, balanced and not opaque, that ends the
|
|
546
|
+
text and spans all of it, so nothing runs before or after the merge. The text must hold one
|
|
547
|
+
pr merge occurrence and no API trigger. Any other text is not bound, which makes a chain
|
|
548
|
+
with a readable number unsafe even though the head lookup would pass.
|
|
549
|
+
.OUTPUTS
|
|
550
|
+
System.Nullable[int]
|
|
551
|
+
#>
|
|
552
|
+
[CmdletBinding()]
|
|
553
|
+
[OutputType([int])]
|
|
554
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
555
|
+
|
|
556
|
+
$records = @(Read-CommandLineInvocationSegment -CommandText $CommandText)
|
|
557
|
+
if ($records.Count -ne 1) { return $null }
|
|
558
|
+
$record = $records[0]
|
|
559
|
+
if ($record.Origin -cne 'TopLevel' -or $record.Unbalanced -or $record.Opaque -or $record.Delimiter.Length -ne 0) { return $null }
|
|
560
|
+
if ($record.RawText.Trim() -cne (ConvertTo-CommandLineNormalizedText -CommandText $CommandText).Trim()) { return $null }
|
|
561
|
+
foreach ($scan in @((ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText), (ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText -RemoveBacktick))) {
|
|
562
|
+
if ([regex]::Matches($scan, $script:EpicMergeGateReviewMergePattern).Count -ne 1) { return $null }
|
|
563
|
+
}
|
|
564
|
+
if (Test-EpicMergeGateReviewApiTrigger -CommandText $CommandText) { return $null }
|
|
565
|
+
return Get-EpicMergeGateReviewBoundTokenNumber -Token ([string[]]@($record.Tokens))
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function Get-EpicMergeGateReviewCandidatePrNumber {
|
|
569
|
+
<#
|
|
570
|
+
.SYNOPSIS
|
|
571
|
+
Return the distinct digit runs of command text that are not part of a longer word (pure).
|
|
572
|
+
.DESCRIPTION
|
|
573
|
+
A number the shape rule cannot bind may still name an item, because a parallel parent
|
|
574
|
+
issues a child's merge from its own root, so each run is a candidate for a checkpoint read.
|
|
575
|
+
.OUTPUTS
|
|
576
|
+
System.Int32[]
|
|
577
|
+
#>
|
|
578
|
+
[CmdletBinding()]
|
|
579
|
+
[OutputType([int[]])]
|
|
580
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
581
|
+
|
|
582
|
+
$scan = ConvertTo-EpicMergeGateReviewScanText -CommandText $CommandText
|
|
583
|
+
$candidates = [System.Collections.Generic.List[int]]::new()
|
|
584
|
+
foreach ($match in [regex]::Matches($scan, '(?<![\w-])[0-9]+(?![\w-])')) {
|
|
585
|
+
$number = 0
|
|
586
|
+
if ([int]::TryParse($match.Value, [ref] $number) -and -not $candidates.Contains($number)) { $candidates.Add($number) }
|
|
587
|
+
}
|
|
588
|
+
return [int[]]$candidates.ToArray()
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function Get-EpicMergeGateReviewShapeDecision {
|
|
592
|
+
<#
|
|
593
|
+
.SYNOPSIS
|
|
594
|
+
Deny text that could be a merge and cannot be bound to one pull request (shape rule).
|
|
595
|
+
.DESCRIPTION
|
|
596
|
+
A bound command passes with no read, so the guard then decides it. Otherwise the decision
|
|
597
|
+
denies when a known checkpoint, or any checkpoint the reader returns, carries the key, and
|
|
598
|
+
passes when none does. Without the key the decision is therefore unchanged, and a failed
|
|
599
|
+
read is best-effort: it sees no key and never denies. The reader is called for the session
|
|
600
|
+
($null) only when KnownPrNumber is set, because a bare command already read it, and for each of
|
|
601
|
+
the first 4 candidate numbers other than KnownPrNumber, in text order. The Codex gate passes
|
|
602
|
+
no reader.
|
|
603
|
+
.PARAMETER CommandText
|
|
604
|
+
The raw command text.
|
|
605
|
+
.PARAMETER KnownCheckpoint
|
|
606
|
+
The parsed checkpoint the gate already read, or $null.
|
|
607
|
+
.PARAMETER KnownPrNumber
|
|
608
|
+
The number that checkpoint was read for, or $null for the session.
|
|
609
|
+
.PARAMETER ReadCheckpoint
|
|
610
|
+
Returns the parsed checkpoint of a number, or of the session for $null.
|
|
611
|
+
.OUTPUTS
|
|
612
|
+
System.Collections.Specialized.OrderedDictionary with Denied and Reason.
|
|
613
|
+
#>
|
|
614
|
+
[CmdletBinding()]
|
|
615
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
616
|
+
param(
|
|
617
|
+
[Parameter(Mandatory)][AllowEmptyString()][string] $CommandText,
|
|
618
|
+
[AllowNull()] $KnownCheckpoint,
|
|
619
|
+
[AllowNull()][Nullable[int]] $KnownPrNumber,
|
|
620
|
+
[scriptblock] $ReadCheckpoint
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
$pass = [ordered]@{ Denied = $false; Reason = $null }
|
|
624
|
+
if ($null -ne (Get-EpicMergeGateReviewBoundPrNumber -CommandText $CommandText)) { return $pass }
|
|
625
|
+
|
|
626
|
+
$deny = [ordered]@{ Denied = $true; Reason = 'EPIC_MERGE_GATE_BLOCKED: REVIEW_GATE_HEAD_UNRESOLVED: review_mode concurrent requires a merge that can be bound to one pull request. Issue gh pr merge <PR> --merge as its own Bash call, and remove the phrase from any other command.' }
|
|
627
|
+
if (Test-EpicMergeGateReviewModeConcurrent -Checkpoint $KnownCheckpoint) { return $deny }
|
|
628
|
+
if ($null -eq $ReadCheckpoint) { return $pass }
|
|
629
|
+
|
|
630
|
+
$targets = [System.Collections.Generic.List[object]]::new()
|
|
631
|
+
if ($null -ne $KnownPrNumber) { $targets.Add($null) }
|
|
632
|
+
$candidates = @(Get-EpicMergeGateReviewCandidatePrNumber -CommandText $CommandText | Where-Object { $_ -ne $KnownPrNumber })
|
|
633
|
+
foreach ($candidate in @($candidates | Select-Object -First $script:EpicMergeGateReviewCandidateCap)) { $targets.Add($candidate) }
|
|
634
|
+
foreach ($target in $targets) {
|
|
635
|
+
$checkpoint = $null
|
|
636
|
+
try {
|
|
637
|
+
$checkpoint = & $ReadCheckpoint $target
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
# Best-effort read: a failure sees no key, so a run without the key keeps its decision.
|
|
641
|
+
$checkpoint = $null
|
|
642
|
+
}
|
|
643
|
+
if (Test-EpicMergeGateReviewModeConcurrent -Checkpoint $checkpoint) { return $deny }
|
|
644
|
+
}
|
|
645
|
+
return $pass
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function Read-EpicMergeGateReviewItemCheckpoint {
|
|
649
|
+
<#
|
|
650
|
+
.SYNOPSIS
|
|
651
|
+
Return the parsed checkpoint of a number, or of the session for $null (Claude only).
|
|
652
|
+
.DESCRIPTION
|
|
653
|
+
Runs at run time in the Claude gate, which holds the functions it calls. The read is
|
|
654
|
+
best-effort: an unresolved target and any failure of the resolver or the read return $null,
|
|
655
|
+
so a run without the key keeps its decision and a failure never denies.
|
|
656
|
+
.PARAMETER PrNumber
|
|
657
|
+
The pull request number, or $null for the session.
|
|
658
|
+
.OUTPUTS
|
|
659
|
+
System.Object or $null
|
|
660
|
+
#>
|
|
661
|
+
[CmdletBinding()]
|
|
662
|
+
param([AllowNull()][Nullable[int]] $PrNumber)
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
$root = $null
|
|
666
|
+
if ($null -eq $PrNumber) {
|
|
667
|
+
$root = Get-EpicMergeGateSessionWorktreeRoot
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
$target = Resolve-EpicMergeGateItemTarget -PrNumber $PrNumber
|
|
671
|
+
if (@('SessionRoot', 'OtherWorktree') -contains $target.Status) { $root = $target.WorktreeRoot }
|
|
672
|
+
}
|
|
673
|
+
if (-not $root) { return $null }
|
|
674
|
+
return ConvertFrom-EpicMergeGateJson -Raw (Get-ChildOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind item -WorktreeRoot $root))
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
# Best-effort read: the caller treats $null as a checkpoint without the key.
|
|
678
|
+
return $null
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function Get-EpicMergeGateReviewUnscopedDecision {
|
|
683
|
+
<#
|
|
684
|
+
.SYNOPSIS
|
|
685
|
+
Apply the shape rule to a command the scope predicate does not read (Claude only).
|
|
686
|
+
.DESCRIPTION
|
|
687
|
+
Reads through Read-EpicMergeGateReviewItemCheckpoint, so a command that today reaches the
|
|
688
|
+
out-of-scope allow can never fail on the gate's own target read.
|
|
689
|
+
.OUTPUTS
|
|
690
|
+
System.Collections.Specialized.OrderedDictionary with Denied and Reason.
|
|
691
|
+
#>
|
|
692
|
+
[CmdletBinding()]
|
|
693
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
694
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
695
|
+
|
|
696
|
+
$number = Get-EpicMergeGateSelectorPrNumber -CommandText $CommandText
|
|
697
|
+
$known = Read-EpicMergeGateReviewItemCheckpoint -PrNumber $number
|
|
698
|
+
return Get-EpicMergeGateReviewShapeDecision -CommandText $CommandText -KnownCheckpoint $known -KnownPrNumber $number -ReadCheckpoint ${function:Read-EpicMergeGateReviewItemCheckpoint}
|
|
699
|
+
}
|