@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.14

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 (66) hide show
  1. package/out/mcp-server.js +368 -209
  2. package/package.json +2 -2
  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/parallel-planner.md +18 -1
  6. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
  10. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  11. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
  15. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
  16. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
  17. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  18. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  20. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
  24. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  25. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  26. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  30. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  31. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
  32. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
  33. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
  34. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  35. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +15 -0
  36. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
  37. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
  38. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
  39. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
  40. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  41. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  42. package/resources/claude-customizations/config/blast-radius.json +14 -1
  43. package/resources/claude-customizations/pack-manifests/core.json +11 -0
  44. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  45. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  46. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
  47. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
  48. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
  49. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
  50. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
  51. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  52. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  53. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  54. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  55. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  56. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  57. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  58. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  59. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  60. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  61. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  62. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  63. package/resources/codex-and-agents-customizations/pack-manifests/core.json +20 -1
  64. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  65. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  66. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +37 -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.
@@ -209,6 +209,8 @@ All nine enums of the parallel surface are owned by the schema-and-validator fea
209
209
 
210
210
  The wave-4 features — F6 (mutation protocol), F7 (enforcement hooks), and F8 (drift detection) — CONSUME these member sets and NEVER extend them. A wave-4 feature that needs a new member must amend this rule file and the validators at spec review, not add the member at implementation time. This constraint exists because the wave-4 features are prepared concurrently and would otherwise add fields to the same files at the same time.
211
211
 
212
+ Integration-cost scheduling (issue #722) adds no `conflict_edges[].reason` member; the four members above are unchanged. The scheduling data travels in tolerated-not-validated fields: an edge may carry the extra fields `hard`, `cost`, and `benefit`, and the planner and orchestrator checkpoints may carry a `tolerated_overlaps` list. No validator reads these fields, and the Python validators and the TypeScript validator port accept them with zero errors.
213
+
212
214
  ## F7 Seam
213
215
 
214
216
  The retrospective cohort-ordering invariant `PARALLEL_COHORT_BARRIER_VIOLATION` (design section 9, Layer 2) is F7's explicitly assigned addition to the orchestrator validator. It is NOT implemented here. The entry point of `scripts/dev_tools/validate_parallel_orchestrator_state.py` contains a clearly delimited, appendable helper-invocation block, marked with explicit begin and end comments that name F7 and the invariant token, so that F7's edit is one appended helper call with no reflow of existing code. The TypeScript core `extensions/drm-copilot/src/lib/validate/parallel-orchestrator-state-core.ts` carries the matching comment-delimited seam. Existing helper calls sit outside the block.
@@ -241,6 +243,11 @@ plan-side extraction so V1 and V2 stay self-consistent against a radius derived
241
243
  The key is optional and fail-closed: a truth table that omits it excludes nothing and reproduces
242
244
  pre-change behaviour exactly.
243
245
 
246
+ The Copilot instructions file `.github/copilot-instructions.md` is a mandate read as well (issue
247
+ #722): the policy reading order begins with it, so a plan that cites copilot-instructions.md is
248
+ reporting that it read the tone and communication policy. Both committed copies of
249
+ `config/blast-radius.json` list it in `mandate_reads`.
250
+
244
251
  Three constraints bound the mandate-read exclusion:
245
252
 
246
253
  1. **The planner remains obliged to enumerate a genuine write explicitly.** An exclusion describes
@@ -391,6 +398,144 @@ weakens the relation below the path level: two items editing the same file still
391
398
  A candidate module belongs in the map when it names a subsystem an item could plausibly not touch.
392
399
  A candidate that matches the majority of work items belongs nowhere.
393
400
 
401
+ ### Integration-cost scheduling (issue #722)
402
+
403
+ Detection and scheduling are separate steps. The detection relation (`conflicts` in Python,
404
+ `Test-BlastRadiusConflict` in PowerShell) is unchanged and still answers one question: do two radii
405
+ contend? Scheduling decides whether a detected contention must serialize the two items. The entry
406
+ points are `schedule_conflict_edges` in `scripts/dev_tools/_blast_radius_scheduling.py` and
407
+ Get-BlastRadiusConflictEdge in `.claude/lib/blast-radius/BlastRadiusScheduling.psm1`; both return
408
+ the edges and the tolerated overlaps of a set of items. Planners call the entry point instead of
409
+ applying the detection relation to every pair by hand.
410
+
411
+ This is an operator-directed configured policy change (issue #722, 2026-09-27), not ad hoc
412
+ narrowing. The only sanctioned mechanisms for relaxing contention are the configured
413
+ `conflict_tolerance` key and configured write-intent extraction. Planners still never hand-narrow a
414
+ radius: editing a declared radius to suppress an edge remains prohibited.
415
+
416
+ **Configuration.** `config/blast-radius.json` carries an optional `conflict_tolerance` object with
417
+ tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only, and
418
+ module, each an integer of at least 1), `band_durations` (C1 through C4, each an integer of at least
419
+ 1), `default_band` (one of C1 through C4), and append_only_paths (a list of exact entries and `**`
420
+ globs). The reader fails fast with an error naming the key for any other shape; booleans are
421
+ rejected wherever an integer is required. An absent key means strict scheduling, identical to
422
+ tolerance_percent 0.
423
+
424
+ **Edge rule.** For each unordered pair of items:
425
+
426
+ 1. Run the unchanged detection relation. No conflict means no edge and no tolerated overlap.
427
+ 2. **Hard classes.** A reason list containing `shared_surface_overlap` or `contract_dependency`
428
+ makes the pair an edge at every tolerance.
429
+ 3. **Cost** (integer, computed only for a detected conflict). Re-enumerate the overlapping entry
430
+ pairs with the same mergeable-path exclusion and the same entry-overlap primitive the relation
431
+ uses, then sum the append_only weight for a concrete overlapping path that matches an
432
+ append_only_paths entry, the same_file weight for two equal concrete entries, the
433
+ possible_overlap weight for a glob or directory-prefix overlap, and the module weight times the
434
+ number of shared modules. The append_only_paths check runs before the same_file check (append-only
435
+ precedence), so an append-only file stays low cost even when both items name it. Mergeable paths
436
+ contribute 0 because they are excluded before enumeration.
437
+ 4. **Benefit** (integer, pairwise). The smaller of the two items' band durations; a missing band uses
438
+ `default_band`. Running the pair concurrently saves at most one cohort step of the shorter item.
439
+ 5. **Rule.** The pair is an edge if and only if it conflicts and either it is hard or
440
+ `cost * 100 > benefit * tolerance_percent`.
441
+ 6. **Recorded reason.** The first member of the relation's reason list in canonical kind order.
442
+
443
+ **Strict-identity proof.** Every weight and every band duration is validated as an integer of at
444
+ least 1. A detected conflict that is not hard therefore has a cost of at least 1, so at
445
+ tolerance_percent 0 the inequality `cost * 100 > 0` always holds and the edge set equals the
446
+ detected-conflict set exactly; cohort coloring of that set is identical to strict scheduling. At
447
+ every tolerance an edge requires a conflict, so every edge implies a conflict and the edge set is
448
+ always a subset of the detected-conflict set. Raising tolerance_percent never adds an edge, and the
449
+ decision for (a, b) equals the decision for (b, a).
450
+
451
+ **Soft-overlap handling.** A detected, non-hard pair within tolerance is a tolerated overlap, not an
452
+ edge. The planner records it in a tolerated_overlaps list on the planner checkpoint (and the
453
+ orchestrator carries it), each entry holding `a`, `b`, `reasons`, `cost`, and `benefit`. Tolerated
454
+ pairs run in the same or adjacent cohorts without a barrier. The later-merging item of a tolerated
455
+ pair merges `origin/main` under the existing per-item merge-conflict handling and re-passes CI before
456
+ it merges.
457
+
458
+ **Drift behaviour.** When an item's observed radius replaces its declared radius, drift detection
459
+ evaluates each in-flight peer pair through the same edge rule, using the items' complexity bands from
460
+ the checkpoint (`default_band` when absent), through the helper module
461
+ `scripts/dev_tools/_parallel_drift_scheduling.py`. A pair is newly conflicting only when the rule
462
+ yields an edge and the pair is not already a recorded edge. A tolerated pair whose observed overlap
463
+ stays within tolerance halts neither item; one whose observed overlap exceeds tolerance, or becomes
464
+ hard, is reported. An unevaluable peer radius still counts as an edge (fail closed). At
465
+ tolerance_percent 0 the drift output equals strict drift output.
466
+
467
+ ### Write-intent extraction (issue #722)
468
+
469
+ The extractor harvests every inline-code token of a plan and spec, so a path an item only reads,
470
+ mentions as a glob, or quotes inside a command becomes a radius path and can create a false edge.
471
+ Write-intent extraction keeps only tokens that state a write. It is the second sanctioned mechanism,
472
+ beside `conflict_tolerance`, for relaxing contention; like it, it is an operator-directed configured
473
+ policy change, and planners still never hand-narrow a radius.
474
+
475
+ **Configuration.** `config/blast-radius.json` carries two optional keys. `write_intent_extraction`
476
+ is a boolean; when it is true, rules W1 through W6 below apply. `path_roots` is a list of non-empty
477
+ first path segments that enables rule W4. Both readers fail fast with an error naming the key for any
478
+ other shape. An absent or false `write_intent_extraction` reproduces pre-change extraction exactly
479
+ (fail closed), and an empty or absent `path_roots` disables W4. The self-hosted copy sets
480
+ `path_roots` to this repository's tracked top-level directories. The implementations are
481
+ `scripts/dev_tools/_blast_radius_write_intent.py` and
482
+ `.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1`.
483
+
484
+ **Rules.** Each rule is a pure, statically decidable function of the text and the truth table.
485
+
486
+ - **W1 glob mention.** Drop any harvested token containing a wildcard character. The feature-folder
487
+ glob is added after filtering and is never dropped.
488
+ - **W2 command span.** Drop every token of an inline span whose whitespace split yields more than
489
+ one word.
490
+ - **W3 read task.** Drop the tokens of a task attribution window (a task line plus the following
491
+ non-task lines up to the next ATX heading, as defined in `.claude/rules/plan-acceptance-gates.md`)
492
+ whose title's first word, after an optional bold label, is in the read-verb set (Read, Verify,
493
+ Confirm, Inspect, Review, Baseline) and whose title contains no member of the write-verb set (Fix,
494
+ Write, Update, Edit, Add, Create, Delete, Remove, Rename, Author, Append, Replace). A write verb
495
+ anywhere in the title overrides the read verb.
496
+ - **W4 root anchoring.** Drop a concrete token whose first segment, after stripping a leading `./`,
497
+ is not in `path_roots` and is not a configured root surface.
498
+ - **W5 spec paths.** The spec contributes contracts only, not paths. Contract harvesting from the
499
+ spec applies W1 and W2.
500
+ - **W6 placeholder stem.** Drop a concrete token whose final-component stem is in the placeholder
501
+ set (the single ASCII letters, and foo, bar, baz, example, sample, placeholder), compared
502
+ case-insensitively.
503
+
504
+ The read-verb, write-verb, and placeholder-stem sets are code constants pinned equal across Python
505
+ and PowerShell by a parity test. Shared surfaces are still resolved from the surviving concrete
506
+ paths, so a shared surface cited only in a read task, a command span, or the spec produces no hard
507
+ edge, while a shared surface an item writes in a write task survives and stays hard.
508
+
509
+ **One extractor for derivation and validation.** W1, W4, and W6 are token-level and also apply when
510
+ `normalize_declared_radius` and Get-NormalizedDeclaredRadius re-filter a recorded radius; an entry
511
+ that starts with `docs/features/`, ends with `/**`, and carries no other wildcard (the feature-folder
512
+ glob) is always kept. W2, W3, and W5 need line context and apply only to derivation and to the
513
+ plan-side extraction of validation. `derive_blast_radius` and `validate_blast_radius` (and their
514
+ PowerShell ports) select the same extractor from the same flag, so a derived radius still passes V1
515
+ and V2 against its own plan in both modes.
516
+
517
+ #### Known false negatives
518
+
519
+ Write-intent extraction can drop a genuine write in these cases:
520
+
521
+ 1. a genuine write stated only as a glob (W1);
522
+ 2. a genuine write stated only inside a command span (W2);
523
+ 3. a write named only in spec prose (W5);
524
+ 4. a read-verb task that also writes without a write verb in its title (W3);
525
+ 5. a new top-level directory not yet in `path_roots` (W4);
526
+ 6. a genuine file whose stem is in the placeholder set (W6).
527
+
528
+ Three mitigations bound these cases:
529
+
530
+ 1. **The planner remains obliged to enumerate a genuine write explicitly.** When an item's plan will
531
+ write a path that one of the rules drops, the planner appends that exact path to the declared
532
+ radius after normalization, as for the mandate-read exclusion.
533
+ 2. **Execution-time escaped-path detection.** `detect_escaped_paths` compares the declared radius
534
+ against the paths a diff actually touched, so a dropped write is caught against observed evidence
535
+ rather than against prose.
536
+ 3. **The fail-closed absent key.** A truth table without `write_intent_extraction`, or with it set
537
+ to false, reproduces pre-change extraction exactly, so an operator can disable the rules at once.
538
+
394
539
  ### The published truth table is not a copy of this one (issue #500)
395
540
 
396
541
  The push-down publishes a second truth table into a destination workspace at
@@ -430,8 +575,11 @@ non-vacuous input.
430
575
  subset, not a copy of the self-hosted sets.** They were authored narrow when the bundled copy was
431
576
  created and were never a copy that fell behind, so the correct gate is portable-set equality against
432
577
  a declared constant plus a subset relation against the self-hosted list — never byte-equality with
433
- the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, and `mergeable_paths`
434
- are byte-equal across the two copies.
578
+ the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, `mergeable_paths`,
579
+ `conflict_tolerance`, and `write_intent_extraction` are byte-equal across the two copies.
580
+ `path_roots` is a Class 2 key: its self-hosted value is this repository's tracked top-level
581
+ directory list, and its bundled value is an empty list, because the bundle cannot know a
582
+ destination's top-level directories and an empty list disables root anchoring there.
435
583
 
436
584
  The reason the two key groups take different relations is an asymmetry between surfaces and modules.
437
585
  An over-matching MODULE glob costs concurrency on every pair of items it touches, because a module
@@ -85,7 +85,7 @@ The window size is fixed at four adjacent non-blank lines. Blank lines are remov
85
85
 
86
86
  ### The shared measurement behind G7, G8, G8b, and G9
87
87
 
88
- The four severities below were fixed by one corpus measurement recorded in `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`. The pre-declared decision rule was written and committed **before** the driver existed and before any count was taken, so the ordering is verifiable from git history rather than only asserted in prose. Its form matches the G5 precedent: the shipped severity is Blocking if and only if the total finding count for that rule is greater than zero **and** the recorded false-positive count for that rule is zero; otherwise Warning.
88
+ The four severities below were fixed by one corpus measurement recorded in `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`. The pre-declared decision rule was written and committed **before** the driver existed and before any count was taken, so the ordering is verifiable from git history rather than only asserted in prose. Its form matches the G5 precedent: the shipped severity is Blocking if and only if the total finding count for that rule is greater than zero **and** the recorded false-positive count for that rule is zero; otherwise Warning.
89
89
 
90
90
  The false-positive definition the rule consumes is likewise pre-declared: a finding is a false positive when the acceptance condition it reports is in fact falsifiable — when the plan states an observation sufficient to distinguish a passing run from a failing one, by a mechanism the rule's predicate does not recognise. A finding is a true positive when the acceptance condition it reports genuinely cannot fail, or can only be satisfied vacuously.
91
91
 
@@ -108,7 +108,7 @@ G7's finding count of 466 satisfies the first conjunct, so the rule was not deci
108
108
 
109
109
  Both classes are addressable by a later feature — Class 1 by adding a check-flag exclusion to the `prettier-write` entry, Class 2 by admitting a tree-observation span as an alternative to a marker — but neither was changed here, because narrowing a predicate after reading its measurement would invalidate the measurement that decided its severity. A later feature that narrows either class must re-take the measurement the same way.
110
110
 
111
- Counts, and every one of the 22 false positives named by plan path, task identifier, and offending span: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
111
+ Counts, and every one of the 22 false positives named by plan path, task identifier, and offending span: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
112
112
 
113
113
  ### G8 — ships as a Warning because the measurement recorded 7 false positives
114
114
 
@@ -118,7 +118,7 @@ G8's finding count of 82 satisfies the first conjunct. It fails the second: 7 fi
118
118
 
119
119
  **Class 2, one finding — the unmerged-path filter.** `--diff-filter=U` selects conflicted paths during a merge or rebase. In that state the worktree-against-index comparison is the correct one and does not become vacuous on commit, because the conflict is precisely what is not yet committed.
120
120
 
121
- Counts, and every one of the 7 false positives named by plan path, task identifier, and offending span: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
121
+ Counts, and every one of the 7 false positives named by plan path, task identifier, and offending span: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
122
122
 
123
123
  ### G8b — ships as a Warning unconditionally, by the pre-declared rule
124
124
 
@@ -126,7 +126,7 @@ G8b is exempt from the two-conjunct rule and **cannot reach the Blocking channel
126
126
 
127
127
  The reason is that G8b carries the highest false-positive surface of the set, and a false-positive count taken over one corpus does not bound the false-positive surface of a predicate. Promoting a rule on the strength of a clean run over one corpus would convert an accident of that corpus into a gate. The 19 findings were nonetheless examined individually and all 19 were retained as true positives, across three sub-classes: a placeholder ref operand, which resolves to a real ref at run time and still cannot make the diff see an untracked path; an artifact-field label inside the span, which is cosmetic and leaves the underlying acceptance condition as the finding describes it; and a pathspec written without the `--` separator, where the predicate reads the pathspec as a ref operand. That last sub-class is a true positive for G8b and is simultaneously recorded in the measurement as a **G8 false negative**: the same span should also have been reported by G8, and was not.
128
128
 
129
- Counts, the zero false-positive record, and the three sub-classes with their plan paths and offending spans: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
129
+ Counts, the zero false-positive record, and the three sub-classes with their plan paths and offending spans: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
130
130
 
131
131
  ### G9 — ships as a Warning because the measurement recorded 4 false positives
132
132
 
@@ -134,7 +134,7 @@ G9's finding count of 8 satisfies the first conjunct. It fails the second: 4 of
134
134
 
135
135
  The remaining 4 findings are true positives, and each is the exact defect the rule was written for: the acceptance condition demands a numeric coverage percentage, the command supplies no terminal reporter, the project `addopts` supplies only an LCOV reporter, and the number the acceptance demands is therefore never printed.
136
136
 
137
- Counts, and every one of the 4 false positives named by plan path, task identifier, offending span, and the reason its acceptance condition is falsifiable: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
137
+ Counts, and every one of the 4 false positives named by plan path, task identifier, offending span, and the reason its acceptance condition is falsifiable: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
138
138
 
139
139
  ## The Write-Mode Register
140
140
 
@@ -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