@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.
Files changed (52) hide show
  1. package/out/mcp-server.js +366 -159
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
  4. package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
  5. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  6. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
  8. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  9. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  11. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
  13. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
  14. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
  15. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  16. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  18. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  19. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  20. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  21. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  22. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  23. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  24. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  25. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
  26. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  27. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
  28. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
  29. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
  30. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  31. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  32. package/resources/claude-customizations/pack-manifests/core.json +9 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  34. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
  37. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  38. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  39. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  40. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  41. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  42. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  43. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  44. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  45. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  46. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  47. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  48. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  49. package/resources/codex-and-agents-customizations/pack-manifests/core.json +18 -1
  50. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  51. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  52. 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`, and the three receipt arrays (`delegation_receipts[]`, `skill_receipts[]`,
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`, and `feature-review` must include the line:
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 now authorizes
330
- a parallel per-item merge when the parallel-orchestrator checkpoint has `route_id == "parallel"`,
331
- the target item's `merge_status == "ci_green"`, and, when a PR number is named, it matches the
332
- item's `pr_number`. Step 3 above is therefore permitted for a legitimate parallel merge; a missing,
333
- unreadable, or invalid parallel checkpoint, a target item whose `merge_status` is not `ci_green`, or
334
- a PR number that matches no item still fails closed with `EPIC_MERGE_GATE_BLOCKED`.
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.