@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.12
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 +366 -159
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/pack-manifests/core.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +18 -1
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +26 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Call-target derivation for target-worktree resolution (issue #669).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Reads target signals from a tool-call payload (a feature-folder path, a file
|
|
7
|
+
path, and a branch name) and derives the worktree the call pertains to as one
|
|
8
|
+
of four states: SessionRoot, OtherWorktree, NoTarget, or Ambiguous.
|
|
9
|
+
|
|
10
|
+
The presence of a signal, not the success of resolving it, separates NoTarget
|
|
11
|
+
from Ambiguous. Signals that resolve to different worktree roots are
|
|
12
|
+
Ambiguous. The precedence order FeatureFolderPath, FilePath, Branch only
|
|
13
|
+
selects which agreeing signal is reported; no run state is ever read to break
|
|
14
|
+
a disagreement.
|
|
15
|
+
|
|
16
|
+
.NOTES
|
|
17
|
+
Compatible with PowerShell 7+. Pure given its sibling WorktreeResolution.psm1,
|
|
18
|
+
which owns every filesystem read. No subprocess, no network, no clock read, and
|
|
19
|
+
no environment read. Mirrored byte-identically under
|
|
20
|
+
extensions/drm-copilot/resources/claude-customizations/.
|
|
21
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
22
|
+
#>
|
|
23
|
+
|
|
24
|
+
Set-StrictMode -Version Latest
|
|
25
|
+
$ErrorActionPreference = 'Stop'
|
|
26
|
+
|
|
27
|
+
Import-Module (Join-Path $PSScriptRoot 'WorktreeResolution.psm1') -ErrorAction Stop
|
|
28
|
+
|
|
29
|
+
# The four states of a derived target. The set is closed so a caller writes exactly
|
|
30
|
+
# one branch per state; a fifth value would silently fall through every caller.
|
|
31
|
+
$script:StatusSessionRoot = 'SessionRoot'
|
|
32
|
+
$script:StatusOtherWorktree = 'OtherWorktree'
|
|
33
|
+
$script:StatusNoTarget = 'NoTarget'
|
|
34
|
+
$script:StatusAmbiguous = 'Ambiguous'
|
|
35
|
+
|
|
36
|
+
# The signal kinds a result may report. SessionRoot names no payload signal; it is
|
|
37
|
+
# reserved for a caller that resolves against the session root deliberately.
|
|
38
|
+
$script:SignalFeatureFolderPath = 'FeatureFolderPath'
|
|
39
|
+
$script:SignalFilePath = 'FilePath'
|
|
40
|
+
$script:SignalBranch = 'Branch'
|
|
41
|
+
$script:SignalSessionRoot = 'SessionRoot'
|
|
42
|
+
|
|
43
|
+
# The prose label used for each payload signal in a Detail clause.
|
|
44
|
+
$script:SignalLabel = @{
|
|
45
|
+
FeatureFolderPath = 'feature folder token'
|
|
46
|
+
FilePath = 'file path'
|
|
47
|
+
Branch = 'branch'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# A feature-folder token: an optional prefix ending in a separator, then
|
|
51
|
+
# docs/features/active/<folder>. The prefix is kept rather than discarded, because
|
|
52
|
+
# an absolute prefix is the only unambiguous placement of the token.
|
|
53
|
+
$script:FeatureFolderPattern = '(?<![^\s"''`(])(?:[^\s"''`]*?[\\/])?docs[\\/]+features[\\/]+active[\\/]+[A-Za-z0-9][A-Za-z0-9._-]*'
|
|
54
|
+
|
|
55
|
+
# An absolute file path with an extension. Only the absolute form is read from free
|
|
56
|
+
# text, because a relative file path names a file present in every worktree.
|
|
57
|
+
$script:FilePathPattern = '(?<![^\s"''`(])(?:[A-Za-z]:[\\/]|/)[^\s"''`]*[\\/][^\s"''`\\/]+\.[A-Za-z0-9]+'
|
|
58
|
+
|
|
59
|
+
# A branch named by a --head or --branch option, or by a "branch:" label. Narrow on
|
|
60
|
+
# purpose: a bare word is never read as a branch.
|
|
61
|
+
$script:BranchPattern = '(?i)(?:(?<![\w-])--(?:head|branch)(?:=|\s+)|\bbranch:\s*)(?<name>[A-Za-z0-9][A-Za-z0-9._/-]*)'
|
|
62
|
+
|
|
63
|
+
# A drive-rooted or slash-rooted normalised path.
|
|
64
|
+
$script:AbsolutePathPattern = '^([A-Za-z]:(/|$)|/)'
|
|
65
|
+
|
|
66
|
+
function New-WorktreeResolutionTargetResult {
|
|
67
|
+
<#
|
|
68
|
+
.SYNOPSIS
|
|
69
|
+
Build a target result object. The single constructor of that shape.
|
|
70
|
+
.DESCRIPTION
|
|
71
|
+
Enforces the field invariants: SessionRoot and Detail are always non-empty,
|
|
72
|
+
Candidates is always an array, WorktreeRoot is populated only for the two
|
|
73
|
+
resolved states, Signal and SignalValue are null for NoTarget, and
|
|
74
|
+
ReasonCode is the ambiguity code when Status is Ambiguous and the no-target
|
|
75
|
+
code when Status is NoTarget (issue #687); it is null for the two resolved
|
|
76
|
+
states.
|
|
77
|
+
.PARAMETER Status
|
|
78
|
+
One of SessionRoot, OtherWorktree, NoTarget, or Ambiguous.
|
|
79
|
+
.PARAMETER SessionRoot
|
|
80
|
+
The worktree root the invoking process runs in.
|
|
81
|
+
.PARAMETER WorktreeRoot
|
|
82
|
+
The resolved target root; required for the two resolved states.
|
|
83
|
+
.PARAMETER Signal
|
|
84
|
+
The signal kind that produced or failed to produce the target.
|
|
85
|
+
.PARAMETER SignalValue
|
|
86
|
+
The raw token the signal was read from.
|
|
87
|
+
.PARAMETER Candidate
|
|
88
|
+
The distinct candidate worktree roots considered.
|
|
89
|
+
.PARAMETER Detail
|
|
90
|
+
A prose clause safe to append to a gate's deny reason.
|
|
91
|
+
#>
|
|
92
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure in-memory factory that changes no system state; the exported name is fixed by the issue #669 contract.')]
|
|
93
|
+
[CmdletBinding()]
|
|
94
|
+
[OutputType([pscustomobject])]
|
|
95
|
+
param(
|
|
96
|
+
[Parameter(Mandatory = $true)]
|
|
97
|
+
[ValidateSet('SessionRoot', 'OtherWorktree', 'NoTarget', 'Ambiguous')]
|
|
98
|
+
[string] $Status,
|
|
99
|
+
[Parameter(Mandatory = $true)]
|
|
100
|
+
[ValidatePattern('\S')]
|
|
101
|
+
[string] $SessionRoot,
|
|
102
|
+
[string] $WorktreeRoot,
|
|
103
|
+
[ValidateSet('', 'FeatureFolderPath', 'FilePath', 'Branch', 'SessionRoot')]
|
|
104
|
+
[string] $Signal,
|
|
105
|
+
[string] $SignalValue,
|
|
106
|
+
[AllowEmptyCollection()]
|
|
107
|
+
[string[]] $Candidate = @(),
|
|
108
|
+
[Parameter(Mandatory = $true)]
|
|
109
|
+
[ValidatePattern('\S')]
|
|
110
|
+
[string] $Detail
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
$isResolved = $Status -in @($script:StatusSessionRoot, $script:StatusOtherWorktree)
|
|
114
|
+
$root = ConvertTo-WorktreeResolutionNormalizedPath -Path $WorktreeRoot
|
|
115
|
+
if ($isResolved -and $null -eq $root) {
|
|
116
|
+
throw "A target result with Status '$Status' requires a WorktreeRoot."
|
|
117
|
+
}
|
|
118
|
+
$hasSignal = ($Status -ne $script:StatusNoTarget) -and -not [string]::IsNullOrEmpty($Signal)
|
|
119
|
+
$kept = if ($Status -eq $script:StatusNoTarget) { @() } else { $Candidate }
|
|
120
|
+
$candidates = [string[]] @($kept | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
121
|
+
return [pscustomobject]@{
|
|
122
|
+
Status = $Status
|
|
123
|
+
WorktreeRoot = if ($isResolved) { $root } else { $null }
|
|
124
|
+
SessionRoot = ConvertTo-WorktreeResolutionNormalizedPath -Path $SessionRoot
|
|
125
|
+
Signal = if ($hasSignal) { $Signal } else { $null }
|
|
126
|
+
SignalValue = if ($hasSignal) { $SignalValue } else { $null }
|
|
127
|
+
Candidates = $candidates
|
|
128
|
+
ReasonCode = switch ($Status) {
|
|
129
|
+
$script:StatusAmbiguous { Get-WorktreeResolutionAmbiguityReasonCode; break }
|
|
130
|
+
$script:StatusNoTarget { Get-WorktreeResolutionNoTargetReasonCode; break }
|
|
131
|
+
default { $null }
|
|
132
|
+
}
|
|
133
|
+
Detail = $Detail
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Find-WorktreeResolutionFeatureFolderSignal {
|
|
138
|
+
<#
|
|
139
|
+
.SYNOPSIS
|
|
140
|
+
Return the first feature-folder token in a text, with any absolute prefix preserved, or $null.
|
|
141
|
+
.DESCRIPTION
|
|
142
|
+
The match does not start at the literal docs segment: a drive or worktree
|
|
143
|
+
prefix written before it stays part of the token, so the token can be
|
|
144
|
+
placed in exactly one worktree. The token ends at the feature-folder name.
|
|
145
|
+
.PARAMETER Text
|
|
146
|
+
The prompt or payload text to scan.
|
|
147
|
+
#>
|
|
148
|
+
[CmdletBinding()]
|
|
149
|
+
[OutputType([string])]
|
|
150
|
+
param(
|
|
151
|
+
[Parameter(Mandatory = $true)]
|
|
152
|
+
[AllowNull()]
|
|
153
|
+
[AllowEmptyString()]
|
|
154
|
+
[string] $Text
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
$match = [regex]::Match([string] $Text, $script:FeatureFolderPattern)
|
|
158
|
+
if (-not $match.Success) {
|
|
159
|
+
return $null
|
|
160
|
+
}
|
|
161
|
+
return $match.Value.TrimEnd('.')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Find-WorktreeResolutionBranchSignal {
|
|
165
|
+
<#
|
|
166
|
+
.SYNOPSIS
|
|
167
|
+
Return the first branch named by --head, --branch, or a "branch:" label, or $null.
|
|
168
|
+
.PARAMETER Text
|
|
169
|
+
The prompt or payload text to scan.
|
|
170
|
+
#>
|
|
171
|
+
[CmdletBinding()]
|
|
172
|
+
[OutputType([string])]
|
|
173
|
+
param(
|
|
174
|
+
[Parameter(Mandatory = $true)]
|
|
175
|
+
[AllowNull()]
|
|
176
|
+
[AllowEmptyString()]
|
|
177
|
+
[string] $Text
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
$match = [regex]::Match([string] $Text, $script:BranchPattern)
|
|
181
|
+
if (-not $match.Success) {
|
|
182
|
+
return $null
|
|
183
|
+
}
|
|
184
|
+
return $match.Groups['name'].Value.TrimEnd('.', '/')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Find-WorktreeResolutionFilePathSignal {
|
|
188
|
+
<#
|
|
189
|
+
.SYNOPSIS
|
|
190
|
+
Return the first absolute file path (with an extension) in a text, or $null.
|
|
191
|
+
.PARAMETER Text
|
|
192
|
+
The prompt or payload text to scan.
|
|
193
|
+
#>
|
|
194
|
+
[CmdletBinding()]
|
|
195
|
+
[OutputType([string])]
|
|
196
|
+
param(
|
|
197
|
+
[Parameter(Mandatory = $true)]
|
|
198
|
+
[AllowNull()]
|
|
199
|
+
[AllowEmptyString()]
|
|
200
|
+
[string] $Text
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
$match = [regex]::Match([string] $Text, $script:FilePathPattern)
|
|
204
|
+
if (-not $match.Success) {
|
|
205
|
+
return $null
|
|
206
|
+
}
|
|
207
|
+
return $match.Value
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function Get-WorktreeResolutionSignalCandidate {
|
|
211
|
+
# The distinct worktree roots one present signal resolves to. An absolute path
|
|
212
|
+
# is placed by the upward walk; a relative path is looked up in every worktree
|
|
213
|
+
# reachable from the session root; a branch is matched against each HEAD.
|
|
214
|
+
[OutputType([string[]])]
|
|
215
|
+
param([hashtable] $Signal, [string] $SessionRoot)
|
|
216
|
+
|
|
217
|
+
if ($Signal.Kind -eq $script:SignalBranch) {
|
|
218
|
+
$byBranch = Get-WorktreeResolutionWorktreeRoot -SessionRoot $SessionRoot -Branch $Signal.Value
|
|
219
|
+
return , [string[]] @($byBranch)
|
|
220
|
+
}
|
|
221
|
+
$normalized = ConvertTo-WorktreeResolutionNormalizedPath -Path $Signal.Value
|
|
222
|
+
if ($normalized -match $script:AbsolutePathPattern) {
|
|
223
|
+
$placed = ConvertTo-WorktreeResolutionRepoRelativePath -Path $normalized
|
|
224
|
+
if (-not $placed.IsNormalized) {
|
|
225
|
+
return , [string[]] @()
|
|
226
|
+
}
|
|
227
|
+
return , [string[]] @($placed.WorktreeRoot)
|
|
228
|
+
}
|
|
229
|
+
$byPath = Get-WorktreeResolutionWorktreeRoot -SessionRoot $SessionRoot -RepoRelativePath $normalized
|
|
230
|
+
return , [string[]] @($byPath)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function Resolve-WorktreeCallTarget {
|
|
234
|
+
<#
|
|
235
|
+
.SYNOPSIS
|
|
236
|
+
Derive the worktree a tool call pertains to, as one of four states.
|
|
237
|
+
.DESCRIPTION
|
|
238
|
+
Always returns a target result object. A call naming no signal is NoTarget.
|
|
239
|
+
A present signal that places in no worktree or in several, or two present
|
|
240
|
+
signals that place in different worktrees, is Ambiguous. Otherwise the one
|
|
241
|
+
agreed root is SessionRoot when it is the session root and OtherWorktree
|
|
242
|
+
when it is not. Signals are reported in the precedence FeatureFolderPath,
|
|
243
|
+
FilePath, Branch, which never suppresses a disagreement.
|
|
244
|
+
.PARAMETER Text
|
|
245
|
+
Optional prompt or payload text scanned for all three signal kinds.
|
|
246
|
+
.PARAMETER Branch
|
|
247
|
+
Optional explicit branch; takes the place of a branch read from Text.
|
|
248
|
+
.PARAMETER FilePath
|
|
249
|
+
Optional explicit file path, relative or absolute; takes the place of a
|
|
250
|
+
file path read from Text.
|
|
251
|
+
.PARAMETER SessionRoot
|
|
252
|
+
Optional. Defaults to the worktree containing the current location.
|
|
253
|
+
#>
|
|
254
|
+
[CmdletBinding()]
|
|
255
|
+
[OutputType([pscustomobject])]
|
|
256
|
+
param(
|
|
257
|
+
[AllowNull()]
|
|
258
|
+
[AllowEmptyString()]
|
|
259
|
+
[string] $Text,
|
|
260
|
+
[string] $Branch,
|
|
261
|
+
[string] $FilePath,
|
|
262
|
+
[string] $SessionRoot
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
$session = ConvertTo-WorktreeResolutionNormalizedPath -Path $SessionRoot
|
|
266
|
+
if ($null -eq $session) {
|
|
267
|
+
$current = (Get-Location).ProviderPath
|
|
268
|
+
$session = Find-WorktreeResolutionRoot -Path $current
|
|
269
|
+
if ($null -eq $session) {
|
|
270
|
+
$session = ConvertTo-WorktreeResolutionNormalizedPath -Path $current
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
$filePathValue = if ([string]::IsNullOrWhiteSpace($FilePath)) { Find-WorktreeResolutionFilePathSignal -Text $Text } else { $FilePath.Trim() }
|
|
275
|
+
$branchValue = if ([string]::IsNullOrWhiteSpace($Branch)) { Find-WorktreeResolutionBranchSignal -Text $Text } else { $Branch.Trim() }
|
|
276
|
+
$present = @(
|
|
277
|
+
@{ Kind = $script:SignalFeatureFolderPath; Value = (Find-WorktreeResolutionFeatureFolderSignal -Text $Text) }
|
|
278
|
+
@{ Kind = $script:SignalFilePath; Value = $filePathValue }
|
|
279
|
+
@{ Kind = $script:SignalBranch; Value = $branchValue }
|
|
280
|
+
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Value) }
|
|
281
|
+
$present = @($present)
|
|
282
|
+
if ($present.Count -eq 0) {
|
|
283
|
+
return (New-WorktreeResolutionTargetResult -Status $script:StatusNoTarget -SessionRoot $session -Detail "the call names no feature folder, file path, or branch, so it has no target and the session root '$session' applies")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
$agreed = [System.Collections.Generic.List[string]]::new()
|
|
287
|
+
foreach ($signal in $present) {
|
|
288
|
+
$label = $script:SignalLabel[$signal.Kind]
|
|
289
|
+
$candidates = Get-WorktreeResolutionSignalCandidate -Signal $signal -SessionRoot $session
|
|
290
|
+
if ($candidates.Count -ne 1) {
|
|
291
|
+
$detail = "$label '$($signal.Value)' matched $($candidates.Count) candidate worktrees; supply an absolute path inside exactly one worktree to disambiguate"
|
|
292
|
+
return (New-WorktreeResolutionTargetResult -Status $script:StatusAmbiguous -SessionRoot $session -Signal $signal.Kind -SignalValue $signal.Value -Candidate $candidates -Detail $detail)
|
|
293
|
+
}
|
|
294
|
+
if (-not ($agreed -contains $candidates[0])) {
|
|
295
|
+
$agreed.Add($candidates[0])
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
$reported = $present[0]
|
|
300
|
+
$reportedLabel = $script:SignalLabel[$reported.Kind]
|
|
301
|
+
if ($agreed.Count -gt 1) {
|
|
302
|
+
$detail = "the call's signals resolve to $($agreed.Count) different worktrees ($($agreed -join ', ')); precedence does not choose between disagreeing signals"
|
|
303
|
+
return (New-WorktreeResolutionTargetResult -Status $script:StatusAmbiguous -SessionRoot $session -Signal $reported.Kind -SignalValue $reported.Value -Candidate $agreed.ToArray() -Detail $detail)
|
|
304
|
+
}
|
|
305
|
+
$root = $agreed[0]
|
|
306
|
+
$status = if ($root -eq $session) { $script:StatusSessionRoot } else { $script:StatusOtherWorktree }
|
|
307
|
+
$detail = "$reportedLabel '$($reported.Value)' resolves to worktree '$root'"
|
|
308
|
+
return (New-WorktreeResolutionTargetResult -Status $status -SessionRoot $session -WorktreeRoot $root -Signal $reported.Kind -SignalValue $reported.Value -Candidate @($root) -Detail $detail)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function Join-WorktreeResolutionPath {
|
|
312
|
+
<#
|
|
313
|
+
.SYNOPSIS
|
|
314
|
+
Compose an absolute forward-slash path from a worktree root and a repo-relative path.
|
|
315
|
+
.PARAMETER WorktreeRoot
|
|
316
|
+
The absolute worktree root, in either separator style.
|
|
317
|
+
.PARAMETER RepoRelativePath
|
|
318
|
+
The path below the root, in either separator style.
|
|
319
|
+
#>
|
|
320
|
+
[CmdletBinding()]
|
|
321
|
+
[OutputType([string])]
|
|
322
|
+
param(
|
|
323
|
+
[Parameter(Mandatory = $true)]
|
|
324
|
+
[string] $WorktreeRoot,
|
|
325
|
+
[Parameter(Mandatory = $true)]
|
|
326
|
+
[string] $RepoRelativePath
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
$root = ConvertTo-WorktreeResolutionNormalizedPath -Path $WorktreeRoot
|
|
330
|
+
if ($root -notmatch $script:AbsolutePathPattern) {
|
|
331
|
+
throw "WorktreeRoot must be an absolute path: '$WorktreeRoot'."
|
|
332
|
+
}
|
|
333
|
+
$relative = ConvertTo-WorktreeResolutionNormalizedPath -Path $RepoRelativePath
|
|
334
|
+
if ($null -eq $relative) {
|
|
335
|
+
return $root
|
|
336
|
+
}
|
|
337
|
+
$separator = if ($root.EndsWith('/')) { '' } else { '/' }
|
|
338
|
+
return $root + $separator + $relative.TrimStart('/')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
Export-ModuleMember -Function `
|
|
342
|
+
New-WorktreeResolutionTargetResult, `
|
|
343
|
+
Find-WorktreeResolutionFeatureFolderSignal, `
|
|
344
|
+
Find-WorktreeResolutionBranchSignal, `
|
|
345
|
+
Find-WorktreeResolutionFilePathSignal, `
|
|
346
|
+
Resolve-WorktreeCallTarget, `
|
|
347
|
+
Join-WorktreeResolutionPath
|
|
@@ -122,6 +122,30 @@ The rest of the completion gate is unchanged. An epic whose feature is not merge
|
|
|
122
122
|
|
|
123
123
|
Enforcement is validator logic plus this prose, never an imported JSON Schema. The activation scope lives in `scripts/dev_tools/_epic_orchestrator_state_launch_binding.py`, and the TypeScript parity port at `extensions/drm-copilot/src/lib/validate/epic-orchestrator-state-launch-binding.ts` reproduces it with byte-identical error strings. No schema file is authored, imported, or read for it.
|
|
124
124
|
|
|
125
|
+
## Standalone-Merge-Authorization Scope and Backward Compatibility
|
|
126
|
+
|
|
127
|
+
These invariants apply only when a checkpoint contains a top-level `standalone_merge_authorizations` array. The key may appear in any of the three checkpoints the epic merge gate reads: `artifacts/orchestration/orchestrator-state.json`, `artifacts/orchestration/epic-orchestrator-state.json`, and `artifacts/orchestration/parallel-orchestrator-state.json`. A checkpoint with no such key (the existing checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive and key-gated. No JSON Schema file is authored, imported, or read for the block (see the Foreign Schema Warning above); the enforcing surface is the `PreToolUse` merge gate (issue #670).
|
|
128
|
+
|
|
129
|
+
Each entry records one authorized standalone merge with the shape `{ pr_number, pr_url, issue_num, branch_name, authorized_by, authorized_at, session_id, basis, run_slug? }`. The gate consults the array only for a `gh pr merge --merge` command that names an explicit pull request number, and only after its per-feature, epic, and parallel allow conditions have all declined.
|
|
130
|
+
|
|
131
|
+
Honest disclosure: the standalone-merge authorization record is a policy-level, auditable declaration and is
|
|
132
|
+
not a cryptographic or security control.
|
|
133
|
+
`authorized_by` is a declaration that the gate does not verify, because the runtime exposes no attested agent identity at Bash `PreToolUse` time. The record is not tamper-proof: any actor able to write `artifacts/orchestration/*.json` inside the authorizing session can write one. The `session_id` cross-check binds a record to the session that wrote it, so a stale record from a previous run or a record copied between runs authorizes nothing; it does not stop same-session forgery. This is a documented accepted trade, not an unexamined gap.
|
|
134
|
+
|
|
135
|
+
## Invariants (standalone_merge_authorizations array)
|
|
136
|
+
|
|
137
|
+
1. **PR-specific block.** The value of `standalone_merge_authorizations` must be a non-empty array whose every entry is an object with a `pr_number` that is a JSON integer greater than zero. Any other shape (a boolean or other blanket flag, a string, an object, an empty array, a non-object entry, or a `pr_number` that is absent, `null`, zero, negative, fractional, a string, or an array) is rejected with `STANDALONE_MERGE_AUTHORIZATION_NOT_PR_SPECIFIC`.
|
|
138
|
+
2. **Binding by `pr_number`.** The gate selects the entry whose `pr_number` equals the pull request number extracted from the command. When records exist but none matches, the gate denies with `STANDALONE_MERGE_AUTHORIZATION_PR_MISMATCH`; when no checkpoint carries the key, it denies with `STANDALONE_MERGE_AUTHORIZATION_ABSENT`.
|
|
139
|
+
3. **`pr_url`.** Non-empty, and it must end with `/pull/` followed by the entry's own `pr_number` and nothing else.
|
|
140
|
+
4. **`issue_num`.** A JSON integer greater than zero.
|
|
141
|
+
5. **`branch_name`.** A non-empty string. It is never compared against the live branch or the working directory.
|
|
142
|
+
6. **`authorized_by`.** A non-empty string; presence only.
|
|
143
|
+
7. **`authorized_at`.** A date-time string that parses with the invariant culture, assuming and adjusting to UTC. No clock is read.
|
|
144
|
+
8. **`basis` and `run_slug`.** `basis` is at least 20 characters after trimming. `run_slug` is optional; when present it is a non-empty string.
|
|
145
|
+
9. **`session_id`.** A non-empty string equal, by ordinal comparison, to the `session_id` of the live hook envelope. An envelope without a `session_id` fails closed.
|
|
146
|
+
|
|
147
|
+
Field checks 3 through 9 run in the fixed order `pr_url`, `issue_num`, `branch_name`, `authorized_by`, `authorized_at`, `basis`, `run_slug`, `session_id`; the first failure wins and the gate denies with `STANDALONE_MERGE_AUTHORIZATION_MALFORMED` naming that field.
|
|
148
|
+
|
|
125
149
|
## Enforcement
|
|
126
150
|
|
|
127
151
|
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated invariant when a `remediation_loop` is present, using the existing validator message style (literal, checkpoint-context prefixed). The validator returns a list of error strings and does not mutate its input.
|
|
@@ -130,3 +154,6 @@ Enforcement is validator logic plus this prose, never an imported JSON Schema. T
|
|
|
130
154
|
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `model_routing_receipts` invariant when a `model_routing_receipts` key is present, delegating to `scripts/dev_tools/_orchestrator_state_model_routing.py`, which recomputes the resolved model via `resolve_delegation_model`. The check does not import or read any schema file.
|
|
131
155
|
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `require_model_routing` invariant only when the caller passes `require_model_routing=True`, delegating to `scripts/dev_tools/_orchestrator_state_model_routing_gate.py`, which reuses the complexity and model-routing per-entry validators. When the flag is not passed the gate does not run, so existing calls are byte-identical.
|
|
132
156
|
- The validator is consumed by the MCP tool `validate_orchestration_artifacts`; backward compatibility for existing step-based checkpoints is preserved.
|
|
157
|
+
- The standalone-merge authorization invariants are enforced by the `PreToolUse` merge gate (`.claude/hooks/enforce-epic-merge-gate.ps1` with its dot-sourced helpers file, and `.codex/hooks/enforce-epic-merge-gate.ps1`) at merge time. The Python checkpoint validator
|
|
158
|
+
does not currently validate standalone_merge_authorizations
|
|
159
|
+
entries; adding that check is recorded as follow-up FU-3.
|
|
@@ -178,6 +178,12 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
|
|
|
178
178
|
defer to that skill for it rather than restating any value from it here. For `<N>`,
|
|
179
179
|
use the GitHub issue number the run executes under when the run has one. When the run
|
|
180
180
|
has none, `<N>` is an arbitrary run-scoped identifier chosen by the pr-author agent.
|
|
181
|
+
The delegation prompt MUST carry a `branch: <name>` label naming the consolidation branch,
|
|
182
|
+
and the canonical issue number line when the run has an issue number.
|
|
183
|
+
`enforce-model-routing-receipt.ps1` identifies the item from those two lines and denies a
|
|
184
|
+
gated delegation it cannot identify. A cleanup run with no issue number is therefore
|
|
185
|
+
identified by the branch label alone, which is why the label is required and the issue line
|
|
186
|
+
is not.
|
|
181
187
|
It is not a pull-request number, and the only requirement on it is that the body-file
|
|
182
188
|
path, the receipt's `number` field, and the body bytes all agree. This skill never
|
|
183
189
|
authors or creates the PR itself.
|
|
@@ -519,6 +525,12 @@ and is prohibited by this skill, which is the term the residual rests on.
|
|
|
519
525
|
only a distinct, individually confirmed manual action outside that automated path for
|
|
520
526
|
`NOT_MERGED`/`HAS_UNIQUE_RESIDUALS` — never a change to the classification ladder
|
|
521
527
|
itself, and never for `PROTECTED_CURRENT`.
|
|
528
|
+
- Never delete the base branch `main` and never remove a worktree checked out on it.
|
|
529
|
+
`compute_protected` protects `main` by name in every checkout topology, so it
|
|
530
|
+
classifies `PROTECTED_CURRENT` even when no protected worktree has it checked out, and
|
|
531
|
+
`delete_candidate` refuses a deletion request for it with
|
|
532
|
+
`ACTION|delete|main|BLOCKED-PROTECTED-BASE` before any re-verification, worktree
|
|
533
|
+
removal, or branch deletion runs.
|
|
522
534
|
- Never use commit-message text matching as a classification input, and never
|
|
523
535
|
auto-resolve cherry-pick conflicts.
|
|
524
536
|
- Never delete an origin branch, or run plain filesystem removal on an orphaned
|
|
@@ -170,6 +170,13 @@ frontmatter default — `opus` for these workers — which suppresses a `fable`
|
|
|
170
170
|
MUST NOT hard-code `model=opus` in a way that overrides the resolved routing model, mirroring
|
|
171
171
|
step 5 of `## Model Selection` in `.claude/skills/orchestrate/SKILL.md`.
|
|
172
172
|
|
|
173
|
+
Every such spawn of `Agent(pr-author)` also carries the canonical issue number line and a
|
|
174
|
+
`branch: <name>` label naming the branch checked out in the worktree the pull request is opened
|
|
175
|
+
from, per `## Issue Number Consistency` in `.claude/skills/orchestrate/SKILL.md`.
|
|
176
|
+
`enforce-model-routing-receipt.ps1` identifies the item from those two lines and denies a gated
|
|
177
|
+
delegation it cannot identify, so a spawn that omits both is refused rather than validated against
|
|
178
|
+
whichever checkpoint occupies the calling session's root.
|
|
179
|
+
|
|
173
180
|
`route` is never an input to model selection; `route` remains file-count driven and governs only
|
|
174
181
|
agents, skills, and MCP tools. A skill whose frontmatter `context` field holds the value `fork`
|
|
175
182
|
inherits the parent model and ignores a model override, so model selection applies to agent
|
|
@@ -281,7 +288,9 @@ checkpoint JSON remains the durable, machine-authoritative source.
|
|
|
281
288
|
`epic_feature_folder`, `epic_manifest_path` (which points at
|
|
282
289
|
`docs/features/epics/<epic-slug>/epic.md`), `epic_status_doc_path`, `integration_branch`,
|
|
283
290
|
`completed_steps`, `next_step`, `last_updated`, `current_wave`, `waves[]`, `features[]`,
|
|
284
|
-
`epic_merge_pr`,
|
|
291
|
+
`epic_merge_pr`, `epic_issue_num` (the epic-level issue `epic-planner` promoted),
|
|
292
|
+
`model_routing_receipts[]` (the routing receipt for each delegation `epic-orchestrator` itself
|
|
293
|
+
spawns), and the three receipt arrays (`delegation_receipts[]`, `skill_receipts[]`,
|
|
285
294
|
`mcp_call_receipts[]`). The
|
|
286
295
|
`merge_status` enum is: `not_started`, `worktree_created`, `pr_open`, `ci_green`,
|
|
287
296
|
`merge_conflict`, `blocked_conflict_loop_limit`, `merged`, `worktree_removed`. The optional
|
|
@@ -298,6 +307,10 @@ Validate the checkpoint through the
|
|
|
298
307
|
that same call at the completion gate. The validation is implemented in
|
|
299
308
|
`scripts/dev_tools/validate_epic_orchestrator_state.py`.
|
|
300
309
|
|
|
310
|
+
**Checkpoint hygiene (issue #673).** The coordinating session never holds a per-feature checkpoint at its own root. Before the first child delegation of a run it moves any `artifacts/orchestration/orchestrator-state.json` at its root to `artifacts/orchestration/handoff/orchestrator-state.issue-<issue-num>.<yyyy-MM-ddTHH-mm>.json`, and writes none there for the rest of the run, because each item's checkpoint lives in that item's worktree. A gated call the coordinator issues on an item's behalf is resolved by the item's issue number and branch, never by the coordinator root.
|
|
311
|
+
|
|
312
|
+
**Integration-PR checkpoint shape (issue #663).** The integration-to-`main` pull request and its `Agent(pr-author)` delegation are gated against `artifacts/orchestration/epic-orchestrator-state.json`, and no per-feature checkpoint is written for them. Before `epic-orchestrator` opens that pull request, the epic checkpoint carries `route_id: "epic"`, `integration_branch`, `epic_issue_num`, and a `features[]` array in which every entry's `merge_status` is `merged` or `worktree_removed`. Before the `Agent(pr-author)` delegation, `epic-orchestrator` records the `pr-author` routing receipt in that file's `model_routing_receipts[]`. The pr-author gate, the epic base-branch check, the model-routing gate, and the preimplementation gate treat a call as epic scope when its branch signal (or, for a staging command or file edit, the worktree HEAD) equals `integration_branch`; the pull request base must then be `main`.
|
|
313
|
+
|
|
301
314
|
## Completion Requirements
|
|
302
315
|
|
|
303
316
|
`epic-orchestrator` must not report completion until:
|
|
@@ -66,6 +66,10 @@ At manifest-authoring time child issues do not exist yet, so `issue_num` values
|
|
|
66
66
|
placeholders and back-filled from each child's promotion receipt as preparation completes. The
|
|
67
67
|
manifest is committed in final, resolved form before the kickoff artifact is written.
|
|
68
68
|
|
|
69
|
+
## Epic-Level Issue Promotion
|
|
70
|
+
|
|
71
|
+
Issue #663. Before the first preparation-mode child delegation, `epic-planner` promotes one epic-level GitHub issue for the epic itself. It authors a potential entry under `docs/features/potential/` describing the epic and promotes it through `mcp__drm-copilot__potential_to_issue` with `promotion_type: epic`; `gh issue create` is not used. It records the resulting number as `epic_issue_num` in the epic planning checkpoint (`artifacts/orchestration/epic-planner-state.json`) and in the kickoff prompt artifact, and `epic-orchestrator` copies it into the epic checkpoint `artifacts/orchestration/epic-orchestrator-state.json` at its first checkpoint write. The integration-to-`main` pull request uses `epic_issue_num` as its canonical issue number.
|
|
72
|
+
|
|
69
73
|
## Complexity Assessment
|
|
70
74
|
|
|
71
75
|
Assess each child feature's complexity band (`C1`-`C4`) using the `model_policy` scale and
|
|
@@ -33,6 +33,7 @@ On every invocation, the main session must:
|
|
|
33
33
|
1. Read `artifacts/orchestration/orchestrator-state.json` to check for existing state.
|
|
34
34
|
2. If a valid checkpoint exists with a matching objective, resume from the recorded `next_step`.
|
|
35
35
|
3. If no checkpoint exists or the objective is new, begin the orchestration lifecycle from the start.
|
|
36
|
+
4. **Checkpoint hygiene (issue #673).** When the checkpoint at `artifacts/orchestration/orchestrator-state.json` records an `issue-num` other than the item this invocation is about, move it to `artifacts/orchestration/handoff/orchestrator-state.issue-<issue-num>.<yyyy-MM-ddTHH-mm>.json` before writing a new checkpoint, rather than overwriting it or leaving it in place. Apply the same move when this session hands an item whose checkpoint it holds to another session or worktree. A checkpoint left at a session root is read by gates that resolve against that root, so a foreign one there is state a gate can answer from.
|
|
36
37
|
|
|
37
38
|
## Prepared-State Portable Handoff Intake
|
|
38
39
|
|
|
@@ -255,10 +256,12 @@ A bounded loop consisting of five steps. The loop variable `remediation_pass` st
|
|
|
255
256
|
|
|
256
257
|
The canonical issue number is derived once from the active feature folder name: extract the trailing integer from the folder base name (e.g., `2026-04-26-push-down-claude-customizations-162` yields `162`). Record as `issue_num` in the checkpoint.
|
|
257
258
|
|
|
258
|
-
Every delegation prompt to `atomic-planner`, `atomic-executor`,
|
|
259
|
+
Every delegation prompt to a receipt-gated subagent type — `atomic-planner`, `atomic-executor`, `feature-review`, `task-researcher`, `prd-feature`, and `pr-author` — must include the line:
|
|
259
260
|
|
|
260
261
|
> `Canonical issue number for this feature is <issue_num>. All artifact content, file paths, and cross-references must use this number.`
|
|
261
262
|
|
|
263
|
+
Each such prompt also carries a `branch: <name>` label naming the branch checked out in the item's worktree, written as the first `branch:` occurrence in the prompt. Before promotion assigns an issue number, the branch label alone is required. Two gates, `enforce-model-routing-receipt.ps1` and `enforce-prd-feature-before-planner.ps1`, identify the item from these two lines: they deny a gated delegation they cannot identify rather than checking it against whichever checkpoint occupies the calling session's root, and they never use a feature-folder path to choose the worktree, because a merged feature folder exists in every checkout branched from main.
|
|
264
|
+
|
|
262
265
|
If a subagent artifact references a different issue number, the orchestrator rejects it, requests correction, and records the discrepancy under `artifact_errors` in the checkpoint.
|
|
263
266
|
|
|
264
267
|
## Step S9 — CI Green Gate
|
|
@@ -234,6 +234,8 @@ There is no final integration pull request and no fan-in path: the epic surface'
|
|
|
234
234
|
integration-branch lifecycle has no counterpart on this surface, and its absence is structural
|
|
235
235
|
rather than an omission.
|
|
236
236
|
|
|
237
|
+
**Checkpoint hygiene (issue #673).** The coordinating session never holds a per-feature checkpoint at its own root. Before the first item delegation of a run it moves any `artifacts/orchestration/orchestrator-state.json` at its root to `artifacts/orchestration/handoff/orchestrator-state.issue-<issue-num>.<yyyy-MM-ddTHH-mm>.json`, and writes none there for the rest of the run, because each item's checkpoint lives in that item's worktree. A gated call the coordinator issues on an item's behalf is resolved by the item's issue number and branch, never by the coordinator root.
|
|
238
|
+
|
|
237
239
|
## Parallel-Mode Kickoff Parameter
|
|
238
240
|
|
|
239
241
|
When `parallel-orchestrator` delegates an item to `Agent(orchestrator)`, the delegation prompt
|
|
@@ -326,12 +328,43 @@ Procedure, per item:
|
|
|
326
328
|
|
|
327
329
|
**Merge-gate authorization.** `.claude/hooks/enforce-epic-merge-gate.ps1` is a project-wide
|
|
328
330
|
`PreToolUse` Bash-matcher hook that denies any `gh pr merge --merge` unless a checkpoint satisfies
|
|
329
|
-
one of its allow conditions; its block reason is `EPIC_MERGE_GATE_BLOCKED`. The gate
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
one of its allow conditions; its block reason is `EPIC_MERGE_GATE_BLOCKED`. The gate has
|
|
332
|
+
four allow conditions
|
|
333
|
+
which it evaluates in this order: a per-feature checkpoint with `epic_mode == true` and
|
|
334
|
+
`step9_status == "passed"`; an epic checkpoint whose `epic_merge_pr.ci_gate.conclusion` is
|
|
335
|
+
`success` with a matching `pr_number`; the parallel condition described next; and a standalone
|
|
336
|
+
merge authorization record, described after it. The parallel condition authorizes a per-item merge
|
|
337
|
+
when the parallel-orchestrator checkpoint has `route_id == "parallel"`, the target item's
|
|
338
|
+
`merge_status == "ci_green"`, and, when a PR number is named, it matches the item's `pr_number`.
|
|
339
|
+
Step 3 above is therefore permitted for a legitimate parallel merge; a missing, unreadable, or
|
|
340
|
+
invalid parallel checkpoint, a target item whose `merge_status` is not `ci_green`, or a PR number
|
|
341
|
+
that matches no item still fails closed with `EPIC_MERGE_GATE_BLOCKED`.
|
|
342
|
+
|
|
343
|
+
The standalone condition (issue #670) is the only route for merging a pull request that is not a
|
|
344
|
+
parallel item, such as a standalone fix that unblocks the run. It is evaluated last and never
|
|
345
|
+
overrides the other three. Writer procedure for the coordinating session:
|
|
346
|
+
|
|
347
|
+
1. Write the record into a top-level `standalone_merge_authorizations` array in the
|
|
348
|
+
parallel-orchestrator checkpoint (`artifacts/orchestration/parallel-orchestrator-state.json`);
|
|
349
|
+
the gate also reads the key from the per-feature and epic checkpoints.
|
|
350
|
+
2. Write one entry per authorized pull request, with these fields: `pr_number` (JSON integer),
|
|
351
|
+
`pr_url` (ending in `/pull/<pr_number>`), `issue_num` (JSON integer), `branch_name`,
|
|
352
|
+
`authorized_by`, `authorized_at` (ISO 8601 date-time), `session_id`, `basis` (at least 20
|
|
353
|
+
characters), and optionally `run_slug`.
|
|
354
|
+
3. Set `session_id` to the session id of the coordinating session that will run the merge
|
|
355
|
+
command. The gate compares it with the live hook envelope and denies on any difference, so a
|
|
356
|
+
record written by another session or a previous run authorizes nothing.
|
|
357
|
+
4. Name the pull request explicitly in the merge command (`gh pr merge <PR> --merge`); a bare
|
|
358
|
+
command never satisfies the standalone condition.
|
|
359
|
+
|
|
360
|
+
A blanket flag is actively rejected: a `standalone_merge_authorizations` value that names no
|
|
361
|
+
specific pull request (for example `true`) denies with
|
|
362
|
+
`STANDALONE_MERGE_AUTHORIZATION_NOT_PR_SPECIFIC`. Other denials carry
|
|
363
|
+
`STANDALONE_MERGE_AUTHORIZATION_ABSENT`, `STANDALONE_MERGE_AUTHORIZATION_PR_MISMATCH`, or
|
|
364
|
+
`STANDALONE_MERGE_AUTHORIZATION_MALFORMED` (naming the failing field) after the
|
|
365
|
+
`EPIC_MERGE_GATE_BLOCKED` token. The record is a policy-level, auditable declaration, not a
|
|
366
|
+
security control; see the Standalone-Merge-Authorization section of
|
|
367
|
+
`.claude/rules/orchestrator-state.md` for the full field rules and the disclosure.
|
|
335
368
|
|
|
336
369
|
Branch protection on `main` affects only the pacing of step 3, not its ownership: if `main`
|
|
337
370
|
requires branches to be up to date, an automated `gh pr update-branch` plus re-green cycle is
|
|
@@ -76,3 +76,18 @@ writes and the `gh pr create` / `gh pr edit --body-file` handoff.
|
|
|
76
76
|
- Do not treat PR numbers as issues.
|
|
77
77
|
- Auto-close bullets must use exactly `- Closes #NNN` format, sourced only from "Issues to autoclose" or "Author-asserted autoclose issues."
|
|
78
78
|
- If GitHub validation is unavailable/unverified, do not emit `Closes`; use the fallback `None` bullet.
|
|
79
|
+
|
|
80
|
+
## Required `--head` on `gh pr create` (issue #687)
|
|
81
|
+
|
|
82
|
+
Every `gh pr create` command this agent issues MUST carry an explicit
|
|
83
|
+
`--head <branch>` naming the branch the pull request is opened from.
|
|
84
|
+
|
|
85
|
+
This is what makes the call's target derivable. `enforce-pr-author-skill.ps1` resolves the
|
|
86
|
+
worktree the call pertains to from the command text, and validates the orchestrator checkpoint
|
|
87
|
+
belonging to that worktree. Without `--head`, `gh pr create` infers its head branch from the
|
|
88
|
+
current checkout and the command carries no signal at all, so the gate denies with
|
|
89
|
+
`TARGET_WORKTREE_NOT_DERIVABLE` rather than validating against whichever checkpoint occupies the
|
|
90
|
+
session root, which in a parallel or epic topology belongs to a different item.
|
|
91
|
+
|
|
92
|
+
Pass `--head` even when the branch is the current one: the gate resolves it to the session root
|
|
93
|
+
and behaves exactly as before, and the command stays correct if it is ever replayed elsewhere.
|