@danmoisan/drm-copilot-mcp 1.1.15 → 1.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +639 -27
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +12 -6
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
- package/resources/claude-customizations/.claude/hooks/enforce-batch-budget-route.ps1 +139 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
- package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
- package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
- package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
- package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +7 -4
- package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
- package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
- package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -2
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
- package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +165 -80
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
- package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
- package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
- package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -0
package/package.json
CHANGED
|
@@ -18,7 +18,9 @@ tools:
|
|
|
18
18
|
- "Bash(bash .claude/lib/bash/compute-cohorts.sh*)"
|
|
19
19
|
- "Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"
|
|
20
20
|
- "Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"
|
|
21
|
+
- "Bash(bash .claude/lib/bash/abandon-parallel-item.sh*)"
|
|
21
22
|
- "Bash(pwsh -NoProfile -File .claude/lib/project-file-merge/Resolve-MergeableConflict.ps1*)"
|
|
23
|
+
- "Bash(pwsh -NoProfile -NonInteractive -File .claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1*)"
|
|
22
24
|
- "Bash(dotnet tool restore*)"
|
|
23
25
|
- "Bash(dotnet csharpier check *)"
|
|
24
26
|
- "Bash(dotnet build *)"
|
|
@@ -94,12 +96,16 @@ batching use `compute-cohorts.sh` and `compute-concurrency-batches.sh` under the
|
|
|
94
96
|
entry.
|
|
95
97
|
|
|
96
98
|
The `poetry run` grants remain for the repository-local paths that still need an interpreter. The
|
|
97
|
-
skill's `## Parallel-Level Checkpoint` section
|
|
98
|
-
`mcp__drm-copilot__validate_orchestration_artifacts`,
|
|
99
|
-
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
skill's `## Parallel-Level Checkpoint` section validates through
|
|
100
|
+
`mcp__drm-copilot__validate_orchestration_artifacts`, and since issue #763 radius drift detection runs
|
|
101
|
+
through the destination-runtime PowerShell entry point
|
|
102
|
+
`.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1` and the abandon disposition through the
|
|
103
|
+
bundled entry point `.claude/lib/bash/abandon-parallel-item.sh`, so no skill step names a
|
|
104
|
+
`poetry run` consumer. The two `poetry run python` grants are left unchanged pending a separate
|
|
105
|
+
removal decision. Each grant stays scoped to its own `poetry run python` invocation form — not to
|
|
106
|
+
`poetry run` as a whole — so `pytest`, `black`, `ruff`, and every other `poetry run` subcommand
|
|
107
|
+
remain outside the allowlist. The sibling persona `.claude/agents/parallel-planner.md` records the
|
|
108
|
+
same destination-runtime posture.
|
|
103
109
|
|
|
104
110
|
## Startup Protocol
|
|
105
111
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: powershell-typed-engineer
|
|
3
3
|
model: sonnet
|
|
4
|
-
description: Project-scoped worker that implements and verifies PowerShell changes within typed repository boundaries. Applies PoshQC format -> PSScriptAnalyzer -> Pester toolchain, the 1-
|
|
4
|
+
description: Project-scoped worker that implements and verifies PowerShell changes within typed repository boundaries. Applies PoshQC format -> PSScriptAnalyzer -> Pester toolchain, the 1-3 production-file direct-mode budget with routing to the orchestrated large path above it, and zero-regression quality gates.
|
|
5
5
|
tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Grep
|
|
@@ -36,7 +36,7 @@ Language standards and toolchain are defined in `.claude/rules/powershell.md` an
|
|
|
36
36
|
Follow the phased workflow defined by the preloaded skills:
|
|
37
37
|
|
|
38
38
|
1. **Policy compliance** — apply `policy-compliance-order` to load mandatory repo policies before any change.
|
|
39
|
-
2. **Routing and scope** — apply `powershell-change-budget-router` to estimate scope and select direct mode (1-
|
|
39
|
+
2. **Routing and scope** — apply `powershell-change-budget-router` to estimate scope and select direct mode (1-3 production files) vs large-path escalation through `/orchestrate`. The large path has no production-file cap.
|
|
40
40
|
3. **Plan and baseline** — apply `atomic-plan-contract` for Phase 0 baseline capture and atomic plan structure. Delegate plan authoring to `atomic_planner` when no plan is supplied. Plans must include the proposed script or module structure, minimal DI seams (wrapper > delegate > adapter), Pester scenario-level test strategy, and the external executable wrapper mock strategy.
|
|
41
41
|
4. **Implement in batches** — apply the approved plan. After each batch, run targeted PSScriptAnalyzer on touched files plus targeted Pester, and confirm per-file coverage.
|
|
42
42
|
5. **Final QA gate** — apply `powershell-qa-gate` to run the full toolchain, enforce zero-regression deltas against the baseline, and produce the required reporting block before declaring completion.
|
|
@@ -46,7 +46,7 @@ For long-running orchestrated runs, apply `powershell-orchestration-state-machin
|
|
|
46
46
|
|
|
47
47
|
## Invocation Modes
|
|
48
48
|
|
|
49
|
-
- **Direct mode** (default, no directive present): strict 1-
|
|
49
|
+
- **Direct mode** (default, no directive present): strict 1-3 production PowerShell files cap. If the estimated scope exceeds 3 production files, stop and instruct the caller to invoke `/orchestrate` per `powershell-change-budget-router`.
|
|
50
50
|
- **Orchestrator handoff mode** (request contains the exact line `DIRECTIVE: ORCHESTRATOR HANDOFF MODE`): overall production-file cap is lifted, but execution requires a complete context package (`objective`, `promotion-type`, `issue-num`, `feature-folder`, `issue.md`, `spec.md`, `user-story.md` or `NONE`, research artifact paths, constraints). In this mode the agent is routing/planning-only until `atomic_planner` returns `PREFLIGHT: ALL CLEAR` from the `atomic-executor` validation loop; all implementation and QA execution must occur via delegated `atomic-executor` handoffs.
|
|
51
51
|
|
|
52
52
|
## Mode Marker Resolution
|
|
@@ -64,8 +64,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
|
|
|
64
64
|
|
|
65
65
|
Stop implementation and return to the user when:
|
|
66
66
|
|
|
67
|
-
- the scope estimate exceeds the
|
|
68
|
-
- an in-flight batch would exceed the 3-production-file or 3-test-file per-batch cap,
|
|
67
|
+
- the scope estimate exceeds the 3-production-file cap in direct mode,
|
|
69
68
|
- a file is near or would exceed the 500-line limit,
|
|
70
69
|
- any QA gate delta is non-zero after self-correction,
|
|
71
70
|
- the toolchain cannot be executed in the current environment (mark the change **unverified**),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: python-typed-engineer
|
|
3
3
|
model: sonnet
|
|
4
|
-
description: Project-scoped worker that implements and verifies Python changes within typed repository boundaries. Applies the Black -> Ruff -> Pyright -> Pytest toolchain, the 3
|
|
4
|
+
description: Project-scoped worker that implements and verifies Python changes within typed repository boundaries. Applies the Black -> Ruff -> Pyright -> Pytest toolchain, the 1-3 production-file direct-mode budget with routing to the orchestrated large path above it, and zero-regression quality gates.
|
|
5
5
|
tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Write
|
|
@@ -37,7 +37,7 @@ Language standards and toolchain are defined in `.claude/rules/python.md` and `.
|
|
|
37
37
|
Follow the phased workflow defined by the preloaded skills:
|
|
38
38
|
|
|
39
39
|
1. **Policy compliance** — apply `policy-compliance-order` to load mandatory repo policies before any change.
|
|
40
|
-
2. **Routing and scope** — apply `python-change-budget-router` to estimate scope
|
|
40
|
+
2. **Routing and scope** — apply `python-change-budget-router` to estimate scope and select direct mode (1-3 production files) or, for more than 3 production files, large-path escalation through `/orchestrate`. The large path has no production-file cap, and test files are not counted toward the routing threshold.
|
|
41
41
|
3. **Plan and baseline** — apply `atomic-plan-contract` for Phase 0 baseline capture and atomic plan structure. Delegate plan authoring to `atomic_planner` when no plan is supplied.
|
|
42
42
|
4. **Implement in batches** — apply the approved plan. After each batch, run targeted Ruff and Pyright on touched files plus targeted Pytest, and confirm per-file coverage.
|
|
43
43
|
5. **Final QA gate** — apply `python-qa-gate` to run the full toolchain, enforce zero-regression deltas against the baseline, and produce the required reporting block before declaring completion.
|
|
@@ -58,7 +58,7 @@ If the marker is missing or malformed, fail closed to `full-feature`.
|
|
|
58
58
|
|
|
59
59
|
Stop implementation and return to the user when:
|
|
60
60
|
|
|
61
|
-
- the scope estimate
|
|
61
|
+
- the scope estimate exceeds 3 production files in direct mode,
|
|
62
62
|
- a file is near or would exceed the 500-line limit,
|
|
63
63
|
- any QA gate delta is non-zero after self-correction,
|
|
64
64
|
- the toolchain cannot be executed in the current environment (mark the change **unverified**),
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pure route helpers shared by the batch-budget hooks.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Defines the functions that read the selected route from orchestrator checkpoint
|
|
7
|
+
text and decide whether that checkpoint selects the uncapped orchestrated large
|
|
8
|
+
path. The file is dot-sourced by the PowerShell and Python batch-budget hooks and
|
|
9
|
+
has no entry point. The Claude and Codex runtime copies of this file are
|
|
10
|
+
byte-identical, and a parity test holds them identical. None of the functions
|
|
11
|
+
reads a file or an environment variable; the checkpoint text is supplied by the
|
|
12
|
+
caller.
|
|
13
|
+
|
|
14
|
+
.NOTES
|
|
15
|
+
Compatible with PowerShell 7+.
|
|
16
|
+
#>
|
|
17
|
+
|
|
18
|
+
function ConvertFrom-BatchBudgetCheckpoint {
|
|
19
|
+
<#
|
|
20
|
+
.SYNOPSIS
|
|
21
|
+
Parses orchestrator checkpoint text into a checkpoint object.
|
|
22
|
+
.DESCRIPTION
|
|
23
|
+
Returns the parsed object only when the text is a JSON object. Null or
|
|
24
|
+
whitespace text, text that is not parseable JSON, and JSON whose root is not
|
|
25
|
+
an object all return $null. Performs no file or environment access.
|
|
26
|
+
.PARAMETER CheckpointText
|
|
27
|
+
Raw text of the orchestrator checkpoint file.
|
|
28
|
+
#>
|
|
29
|
+
[CmdletBinding()]
|
|
30
|
+
[OutputType([psobject])]
|
|
31
|
+
param(
|
|
32
|
+
[AllowNull()]
|
|
33
|
+
[AllowEmptyString()]
|
|
34
|
+
[string] $CheckpointText
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if ([string]::IsNullOrWhiteSpace($CheckpointText)) {
|
|
38
|
+
return $null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
$parsed = ConvertFrom-Json -InputObject $CheckpointText -ErrorAction Stop
|
|
43
|
+
} catch {
|
|
44
|
+
Write-Verbose "Ignoring unparseable orchestrator checkpoint text: $($_.Exception.Message)"
|
|
45
|
+
return $null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ($parsed -is [System.Management.Automation.PSCustomObject]) {
|
|
49
|
+
return $parsed
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return $null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Get-BatchBudgetSelectedRoute {
|
|
56
|
+
<#
|
|
57
|
+
.SYNOPSIS
|
|
58
|
+
Returns the route selected by an orchestrator checkpoint.
|
|
59
|
+
.DESCRIPTION
|
|
60
|
+
Uses route_id when the checkpoint has that key, otherwise path_selected. The
|
|
61
|
+
value is returned only when it is a non-blank string; every other case,
|
|
62
|
+
including unparseable text, returns an empty string.
|
|
63
|
+
.PARAMETER CheckpointText
|
|
64
|
+
Raw text of the orchestrator checkpoint file.
|
|
65
|
+
#>
|
|
66
|
+
[CmdletBinding()]
|
|
67
|
+
[OutputType([string])]
|
|
68
|
+
param(
|
|
69
|
+
[AllowNull()]
|
|
70
|
+
[AllowEmptyString()]
|
|
71
|
+
[string] $CheckpointText
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
$checkpoint = ConvertFrom-BatchBudgetCheckpoint -CheckpointText $CheckpointText
|
|
75
|
+
if ($null -eq $checkpoint) {
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$propertyNames = @($checkpoint.PSObject.Properties.Name)
|
|
80
|
+
$rawRoute = $null
|
|
81
|
+
if ($propertyNames -contains 'route_id') {
|
|
82
|
+
$rawRoute = $checkpoint.route_id
|
|
83
|
+
} elseif ($propertyNames -contains 'path_selected') {
|
|
84
|
+
$rawRoute = $checkpoint.path_selected
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if ($rawRoute -is [string] -and -not [string]::IsNullOrWhiteSpace($rawRoute)) {
|
|
88
|
+
return $rawRoute
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Test-BatchBudgetLargePathRoute {
|
|
95
|
+
<#
|
|
96
|
+
.SYNOPSIS
|
|
97
|
+
Reports whether an orchestrator checkpoint selects the uncapped large path.
|
|
98
|
+
.DESCRIPTION
|
|
99
|
+
Returns $true only when the checkpoint parses to an object, its selected route
|
|
100
|
+
is large, remediation, or preparation (case-sensitive), next_step is not
|
|
101
|
+
complete, and completed_steps does not contain S12_complete. No input causes
|
|
102
|
+
an exception; any failure returns $false.
|
|
103
|
+
.PARAMETER CheckpointText
|
|
104
|
+
Raw text of the orchestrator checkpoint file.
|
|
105
|
+
#>
|
|
106
|
+
[CmdletBinding()]
|
|
107
|
+
[OutputType([bool])]
|
|
108
|
+
param(
|
|
109
|
+
[AllowNull()]
|
|
110
|
+
[AllowEmptyString()]
|
|
111
|
+
[string] $CheckpointText
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
$checkpoint = ConvertFrom-BatchBudgetCheckpoint -CheckpointText $CheckpointText
|
|
116
|
+
if ($null -eq $checkpoint) {
|
|
117
|
+
return $false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$route = Get-BatchBudgetSelectedRoute -CheckpointText $CheckpointText
|
|
121
|
+
if (-not (@('large', 'remediation', 'preparation') -ccontains $route)) {
|
|
122
|
+
return $false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$propertyNames = @($checkpoint.PSObject.Properties.Name)
|
|
126
|
+
if ($propertyNames -contains 'next_step' -and $checkpoint.next_step -is [string] -and $checkpoint.next_step -ceq 'complete') {
|
|
127
|
+
return $false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if ($propertyNames -contains 'completed_steps' -and (@($checkpoint.completed_steps) -ccontains 'S12_complete')) {
|
|
131
|
+
return $false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return $true
|
|
135
|
+
} catch {
|
|
136
|
+
Write-Verbose "Treating the orchestrator checkpoint as direct mode: $($_.Exception.Message)"
|
|
137
|
+
return $false
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Checkpoint read seams and run-target resolution for the epic merge gate (issue #690).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Dot-sourced by enforce-epic-merge-gate.ps1 immediately after the authorization sibling.
|
|
7
|
+
Holds:
|
|
8
|
+
|
|
9
|
+
- The import guard for WorktreeRunResolution.psm1. A failed import is recorded in
|
|
10
|
+
$script:EpicMergeGateResolutionImportFailure, and
|
|
11
|
+
Get-EpicMergeGateImportFailureDecision turns it into a deny, so a missing module
|
|
12
|
+
cannot make the gate exit non-zero and fail open.
|
|
13
|
+
- The three checkpoint read seams, relocated from the gate file with their names kept.
|
|
14
|
+
Each takes a mandatory absolute path composed beneath a resolved worktree root.
|
|
15
|
+
- The session worktree root, used for the child branch and for a bare command.
|
|
16
|
+
- The resolution seam Resolve-EpicMergeGateRunTarget, which locates the epic or
|
|
17
|
+
parallel checkpoint that records the command's pull request number.
|
|
18
|
+
- Test-ChildCheckpointPrGateBinding, which binds the command's pull request number to
|
|
19
|
+
the per-feature checkpoint's pr_gate.pr_number when that field is recorded.
|
|
20
|
+
- Get-EpicMergeGateUnresolvedReason, the reason prefix used when neither run branch
|
|
21
|
+
resolves.
|
|
22
|
+
|
|
23
|
+
.NOTES
|
|
24
|
+
PowerShell 7+. Depends on Get-EpicMergeGateBlockDecision from the authorization
|
|
25
|
+
sibling, dot-sourced by the gate before this file. Mirrored byte-identically under
|
|
26
|
+
extensions/drm-copilot/resources/claude-customizations/.
|
|
27
|
+
#>
|
|
28
|
+
|
|
29
|
+
# Import guard (issue #690): a failed import denies instead of failing open.
|
|
30
|
+
$script:EpicMergeGateResolutionImportFailure = $null
|
|
31
|
+
try {
|
|
32
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
$script:EpicMergeGateResolutionImportFailure = 'WorktreeRunResolution.psm1'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Get-EpicMergeGateImportFailureDecision {
|
|
39
|
+
<#
|
|
40
|
+
.SYNOPSIS
|
|
41
|
+
Returns the deny for a failed worktree-resolution import, or $null.
|
|
42
|
+
.OUTPUTS
|
|
43
|
+
System.Collections.Specialized.OrderedDictionary, or $null when the import succeeded.
|
|
44
|
+
#>
|
|
45
|
+
[CmdletBinding()]
|
|
46
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
47
|
+
param()
|
|
48
|
+
|
|
49
|
+
if (-not $script:EpicMergeGateResolutionImportFailure) {
|
|
50
|
+
return $null
|
|
51
|
+
}
|
|
52
|
+
return Get-EpicMergeGateBlockDecision -Reason (
|
|
53
|
+
"EPIC_MERGE_GATE_BLOCKED: the worktree-resolution module '$($script:EpicMergeGateResolutionImportFailure)' " +
|
|
54
|
+
'failed to import, so the run checkpoint that governs this merge cannot be located; the gate fails closed.')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Get-ChildOrchestratorCheckpointContent {
|
|
58
|
+
<#
|
|
59
|
+
.SYNOPSIS
|
|
60
|
+
Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock
|
|
61
|
+
this function (read seam).
|
|
62
|
+
.PARAMETER Path
|
|
63
|
+
The absolute checkpoint path composed beneath the session worktree root.
|
|
64
|
+
.OUTPUTS
|
|
65
|
+
System.String or $null
|
|
66
|
+
#>
|
|
67
|
+
[CmdletBinding()]
|
|
68
|
+
[OutputType([string])]
|
|
69
|
+
param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
|
|
70
|
+
|
|
71
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
72
|
+
return $null
|
|
73
|
+
}
|
|
74
|
+
return (Get-Content -LiteralPath $Path -Raw)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Get-EpicOrchestratorCheckpointContent {
|
|
78
|
+
<#
|
|
79
|
+
.SYNOPSIS
|
|
80
|
+
Read the raw JSON text of the epic checkpoint. Tests mock this function
|
|
81
|
+
(read seam).
|
|
82
|
+
.PARAMETER Path
|
|
83
|
+
The absolute checkpoint path composed beneath the resolved worktree root.
|
|
84
|
+
.OUTPUTS
|
|
85
|
+
System.String or $null
|
|
86
|
+
#>
|
|
87
|
+
[CmdletBinding()]
|
|
88
|
+
[OutputType([string])]
|
|
89
|
+
param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
|
|
90
|
+
|
|
91
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
92
|
+
return $null
|
|
93
|
+
}
|
|
94
|
+
return (Get-Content -LiteralPath $Path -Raw)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Get-ParallelOrchestratorCheckpointContent {
|
|
98
|
+
<#
|
|
99
|
+
.SYNOPSIS
|
|
100
|
+
Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
|
|
101
|
+
this function (read seam).
|
|
102
|
+
.PARAMETER Path
|
|
103
|
+
The absolute checkpoint path composed beneath the resolved worktree root.
|
|
104
|
+
.OUTPUTS
|
|
105
|
+
System.String or $null
|
|
106
|
+
#>
|
|
107
|
+
[CmdletBinding()]
|
|
108
|
+
[OutputType([string])]
|
|
109
|
+
param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
|
|
110
|
+
|
|
111
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
112
|
+
return $null
|
|
113
|
+
}
|
|
114
|
+
return (Get-Content -LiteralPath $Path -Raw)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function Get-EpicMergeGateSessionWorktreeRoot {
|
|
118
|
+
<#
|
|
119
|
+
.SYNOPSIS
|
|
120
|
+
Return the worktree root the session runs in (the child branch and bare-command root).
|
|
121
|
+
.OUTPUTS
|
|
122
|
+
System.String
|
|
123
|
+
#>
|
|
124
|
+
[CmdletBinding()]
|
|
125
|
+
[OutputType([string])]
|
|
126
|
+
param()
|
|
127
|
+
|
|
128
|
+
return (Resolve-WorktreeOperandTarget -Path '' -SessionRoot (Get-Location).Path).WorktreeRoot
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Resolve-EpicMergeGateRunTarget {
|
|
132
|
+
<#
|
|
133
|
+
.SYNOPSIS
|
|
134
|
+
Resolve the worktree whose run checkpoint records a pull request number (issue #690 seam).
|
|
135
|
+
.PARAMETER Kind
|
|
136
|
+
epic or parallel.
|
|
137
|
+
.PARAMETER PrNumber
|
|
138
|
+
The explicit pull request number the command names.
|
|
139
|
+
.OUTPUTS
|
|
140
|
+
System.Management.Automation.PSCustomObject (the worktree-resolution target result).
|
|
141
|
+
#>
|
|
142
|
+
[CmdletBinding()]
|
|
143
|
+
[OutputType([pscustomobject])]
|
|
144
|
+
param(
|
|
145
|
+
[Parameter(Mandatory)][ValidateSet('epic', 'parallel')][string] $Kind,
|
|
146
|
+
[Parameter(Mandatory)][int] $PrNumber
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return Resolve-WorktreeRunTargetByRecord -Kind $Kind -RecordField pr_number -Value ([string]$PrNumber) -SessionRoot (Get-Location).Path
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function Test-ChildCheckpointPrGateBinding {
|
|
153
|
+
<#
|
|
154
|
+
.SYNOPSIS
|
|
155
|
+
True unless the per-feature checkpoint records a pr_gate.pr_number that differs from the command's.
|
|
156
|
+
.DESCRIPTION
|
|
157
|
+
A bare command, an absent checkpoint, and a checkpoint without pr_gate.pr_number
|
|
158
|
+
keep today's unbound child-branch decision. When both numbers are present they must
|
|
159
|
+
parse as integers and be equal.
|
|
160
|
+
.PARAMETER Checkpoint
|
|
161
|
+
Parsed per-feature checkpoint, or $null.
|
|
162
|
+
.PARAMETER CommandPrNumber
|
|
163
|
+
The explicit pull request number the command names, or $null.
|
|
164
|
+
.OUTPUTS
|
|
165
|
+
System.Boolean
|
|
166
|
+
#>
|
|
167
|
+
[CmdletBinding()]
|
|
168
|
+
[OutputType([bool])]
|
|
169
|
+
param(
|
|
170
|
+
[AllowNull()] $Checkpoint,
|
|
171
|
+
[AllowNull()][Nullable[int]] $CommandPrNumber
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if ($null -eq $CommandPrNumber -or $null -eq $Checkpoint) {
|
|
175
|
+
return $true
|
|
176
|
+
}
|
|
177
|
+
if (@($Checkpoint.PSObject.Properties.Name) -notcontains 'pr_gate' -or $null -eq $Checkpoint.pr_gate) {
|
|
178
|
+
return $true
|
|
179
|
+
}
|
|
180
|
+
$prGate = $Checkpoint.pr_gate
|
|
181
|
+
if (@($prGate.PSObject.Properties.Name) -notcontains 'pr_number' -or $null -eq $prGate.pr_number) {
|
|
182
|
+
return $true
|
|
183
|
+
}
|
|
184
|
+
$recorded = 0
|
|
185
|
+
if (-not [int]::TryParse([string]$prGate.pr_number, [ref] $recorded)) {
|
|
186
|
+
return $false
|
|
187
|
+
}
|
|
188
|
+
return ($recorded -eq $CommandPrNumber)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function Get-EpicMergeGateUnresolvedReason {
|
|
192
|
+
<#
|
|
193
|
+
.SYNOPSIS
|
|
194
|
+
Returns '<code>: <epic detail>; <parallel detail>' when neither run branch resolved, or $null.
|
|
195
|
+
.DESCRIPTION
|
|
196
|
+
The ambiguity code wins when either target is Ambiguous. Returns $null when either
|
|
197
|
+
target is absent (a bare command) or either target resolved.
|
|
198
|
+
.PARAMETER EpicTarget
|
|
199
|
+
The epic run target, or $null.
|
|
200
|
+
.PARAMETER ParallelTarget
|
|
201
|
+
The parallel run target, or $null.
|
|
202
|
+
.OUTPUTS
|
|
203
|
+
System.String or $null
|
|
204
|
+
#>
|
|
205
|
+
[CmdletBinding()]
|
|
206
|
+
[OutputType([string])]
|
|
207
|
+
param(
|
|
208
|
+
[AllowNull()] $EpicTarget,
|
|
209
|
+
[AllowNull()] $ParallelTarget
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if ($null -eq $EpicTarget -or $null -eq $ParallelTarget) {
|
|
213
|
+
return $null
|
|
214
|
+
}
|
|
215
|
+
$unresolved = @('NoTarget', 'Ambiguous')
|
|
216
|
+
if ($unresolved -notcontains $EpicTarget.Status -or $unresolved -notcontains $ParallelTarget.Status) {
|
|
217
|
+
return $null
|
|
218
|
+
}
|
|
219
|
+
$code = if ($EpicTarget.Status -eq 'Ambiguous') { $EpicTarget.ReasonCode } elseif ($ParallelTarget.Status -eq 'Ambiguous') { $ParallelTarget.ReasonCode } else { $EpicTarget.ReasonCode }
|
|
220
|
+
return ('{0}: {1}; {2}' -f $code, $EpicTarget.Detail, $ParallelTarget.Detail)
|
|
221
|
+
}
|
|
@@ -38,10 +38,16 @@
|
|
|
38
38
|
policy-level-not-cryptographic posture already accepted for
|
|
39
39
|
enforce-pr-author-skill.ps1's own receipt mechanism. It is not a cryptographic control.
|
|
40
40
|
|
|
41
|
+
Issue #690: with an explicit PR number, the epic and parallel checkpoints are read
|
|
42
|
+
beneath the live worktree whose run checkpoint records that number, and the child
|
|
43
|
+
branch binds the number to pr_gate.pr_number when the per-feature checkpoint records
|
|
44
|
+
it. A bare command reads every checkpoint beneath the session worktree.
|
|
45
|
+
|
|
41
46
|
.NOTES
|
|
42
|
-
Compatible with PowerShell 7+.
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 through the
|
|
48
|
+
dot-sourced enforce-epic-merge-gate-resolution.ps1, whose import guard denies on a
|
|
49
|
+
failed import. Filesystem reads go through injectable wrapper functions so tests can
|
|
50
|
+
mock the boundary without writing temporary files.
|
|
45
51
|
|
|
46
52
|
Honest disclosure: the standalone-merge authorization record is a policy-level,
|
|
47
53
|
auditable declaration and is not a cryptographic or security control. authorized_by is
|
|
@@ -59,64 +65,8 @@ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -
|
|
|
59
65
|
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
60
66
|
# Standalone-merge authorization predicates and the two decision-envelope factories (issue #670).
|
|
61
67
|
. (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-authorization.ps1')
|
|
62
|
-
|
|
63
|
-
$
|
|
64
|
-
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
65
|
-
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
66
|
-
|
|
67
|
-
function Get-ChildOrchestratorCheckpointContent {
|
|
68
|
-
<#
|
|
69
|
-
.SYNOPSIS
|
|
70
|
-
Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock
|
|
71
|
-
this function (read seam).
|
|
72
|
-
.OUTPUTS
|
|
73
|
-
System.String or $null
|
|
74
|
-
#>
|
|
75
|
-
[CmdletBinding()]
|
|
76
|
-
[OutputType([string])]
|
|
77
|
-
param()
|
|
78
|
-
|
|
79
|
-
if (-not (Test-Path -LiteralPath $script:ChildCheckpointPath -PathType Leaf)) {
|
|
80
|
-
return $null
|
|
81
|
-
}
|
|
82
|
-
return (Get-Content -LiteralPath $script:ChildCheckpointPath -Raw)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function Get-EpicOrchestratorCheckpointContent {
|
|
86
|
-
<#
|
|
87
|
-
.SYNOPSIS
|
|
88
|
-
Read the raw JSON text of the epic checkpoint. Tests mock this function
|
|
89
|
-
(read seam).
|
|
90
|
-
.OUTPUTS
|
|
91
|
-
System.String or $null
|
|
92
|
-
#>
|
|
93
|
-
[CmdletBinding()]
|
|
94
|
-
[OutputType([string])]
|
|
95
|
-
param()
|
|
96
|
-
|
|
97
|
-
if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
|
|
98
|
-
return $null
|
|
99
|
-
}
|
|
100
|
-
return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function Get-ParallelOrchestratorCheckpointContent {
|
|
104
|
-
<#
|
|
105
|
-
.SYNOPSIS
|
|
106
|
-
Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
|
|
107
|
-
this function (read seam).
|
|
108
|
-
.OUTPUTS
|
|
109
|
-
System.String or $null
|
|
110
|
-
#>
|
|
111
|
-
[CmdletBinding()]
|
|
112
|
-
[OutputType([string])]
|
|
113
|
-
param()
|
|
114
|
-
|
|
115
|
-
if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
|
|
116
|
-
return $null
|
|
117
|
-
}
|
|
118
|
-
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
119
|
-
}
|
|
68
|
+
# Checkpoint read seams, the import guard, and run-target resolution (issue #690).
|
|
69
|
+
. (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-resolution.ps1')
|
|
120
70
|
|
|
121
71
|
function ConvertFrom-EpicMergeGateJson {
|
|
122
72
|
<#
|
|
@@ -361,6 +311,12 @@ function Invoke-EpicMergeGateDecision {
|
|
|
361
311
|
[string] $ToolInputRaw
|
|
362
312
|
)
|
|
363
313
|
|
|
314
|
+
# A failed worktree-resolution import denies before any other logic (issue #690).
|
|
315
|
+
$importFailure = Get-EpicMergeGateImportFailureDecision
|
|
316
|
+
if ($null -ne $importFailure) {
|
|
317
|
+
return $importFailure
|
|
318
|
+
}
|
|
319
|
+
|
|
364
320
|
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
365
321
|
if (-not $payload.IsValid) {
|
|
366
322
|
return Get-EpicMergeGateBlockDecision -Reason (
|
|
@@ -399,18 +355,35 @@ function Invoke-EpicMergeGateDecision {
|
|
|
399
355
|
}
|
|
400
356
|
|
|
401
357
|
$commandPrNumber = Get-EpicMergeGateCommandPrNumber -CommandText $commandText
|
|
358
|
+
$sessionRoot = Get-EpicMergeGateSessionWorktreeRoot
|
|
402
359
|
|
|
403
|
-
|
|
404
|
-
|
|
360
|
+
# The child merges from its own worktree, so its checkpoint is the session worktree's;
|
|
361
|
+
# a recorded pr_gate.pr_number must match an explicit command PR number (issue #690).
|
|
362
|
+
$childCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ChildOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind item -WorktreeRoot $sessionRoot))
|
|
363
|
+
if ((Test-ChildCheckpointAllowsEpicMerge -Checkpoint $childCheckpoint) -and
|
|
364
|
+
(Test-ChildCheckpointPrGateBinding -Checkpoint $childCheckpoint -CommandPrNumber $commandPrNumber)) {
|
|
405
365
|
return Get-EpicMergeGateAllowDecision
|
|
406
366
|
}
|
|
407
367
|
|
|
408
|
-
|
|
368
|
+
# A bare command targets the current branch's PR, so it reads the session worktree. An
|
|
369
|
+
# explicit PR number locates each run checkpoint that records it (issue #690).
|
|
370
|
+
$epicTarget = $null
|
|
371
|
+
$parallelTarget = $null
|
|
372
|
+
$epicRoot = $sessionRoot
|
|
373
|
+
$parallelRoot = $sessionRoot
|
|
374
|
+
if ($null -ne $commandPrNumber) {
|
|
375
|
+
$epicTarget = Resolve-EpicMergeGateRunTarget -Kind epic -PrNumber $commandPrNumber
|
|
376
|
+
$parallelTarget = Resolve-EpicMergeGateRunTarget -Kind parallel -PrNumber $commandPrNumber
|
|
377
|
+
$epicRoot = if (@('SessionRoot', 'OtherWorktree') -contains $epicTarget.Status) { $epicTarget.WorktreeRoot } else { $null }
|
|
378
|
+
$parallelRoot = if (@('SessionRoot', 'OtherWorktree') -contains $parallelTarget.Status) { $parallelTarget.WorktreeRoot } else { $null }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
$epicCheckpoint = if ($epicRoot) { ConvertFrom-EpicMergeGateJson -Raw (Get-EpicOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind epic -WorktreeRoot $epicRoot)) } else { $null }
|
|
409
382
|
if (Test-EpicCheckpointAllowsMerge -Checkpoint $epicCheckpoint -CommandPrNumber $commandPrNumber) {
|
|
410
383
|
return Get-EpicMergeGateAllowDecision
|
|
411
384
|
}
|
|
412
385
|
|
|
413
|
-
$parallelCheckpoint = ConvertFrom-EpicMergeGateJson -Raw (Get-ParallelOrchestratorCheckpointContent)
|
|
386
|
+
$parallelCheckpoint = if ($parallelRoot) { ConvertFrom-EpicMergeGateJson -Raw (Get-ParallelOrchestratorCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind parallel -WorktreeRoot $parallelRoot)) } else { $null }
|
|
414
387
|
if (Test-ParallelCheckpointAllowsMerge -Checkpoint $parallelCheckpoint -CommandPrNumber $commandPrNumber) {
|
|
415
388
|
return Get-EpicMergeGateAllowDecision
|
|
416
389
|
}
|
|
@@ -423,7 +396,10 @@ function Invoke-EpicMergeGateDecision {
|
|
|
423
396
|
if ($standalone.Allowed) {
|
|
424
397
|
return Get-EpicMergeGateAllowDecision
|
|
425
398
|
}
|
|
426
|
-
|
|
399
|
+
# When neither run branch resolved, its reason code and detail precede the standalone reason.
|
|
400
|
+
$unresolved = Get-EpicMergeGateUnresolvedReason -EpicTarget $epicTarget -ParallelTarget $parallelTarget
|
|
401
|
+
$prefix = if ($unresolved) { $unresolved + '; ' } else { '' }
|
|
402
|
+
return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $prefix + $standalone.ReasonCode + ': ' + $standalone.Message)
|
|
427
403
|
}
|
|
428
404
|
|
|
429
405
|
return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: gh pr merge --merge requires either a per-feature checkpoint with epic_mode == true and step9_status == "passed", an epic checkpoint with epic_merge_pr.ci_gate.conclusion == "success" and a matching pr_number, or a parallel-orchestrator checkpoint with route_id == "parallel" whose target item (matched by pr_number) has merge_status == "ci_green". No checkpoint satisfied this gate.'
|