@danmoisan/drm-copilot-mcp 1.1.10 → 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 (82) hide show
  1. package/out/mcp-server.js +3517 -1104
  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/parallel-orchestrator.md +15 -2
  6. package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
  7. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  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 +82 -51
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
  11. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
  17. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
  18. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  21. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  22. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
  23. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
  24. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
  25. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
  28. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
  29. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  30. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
  31. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
  32. package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
  33. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  37. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  38. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  39. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
  41. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  42. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
  43. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
  44. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
  45. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
  46. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
  47. package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
  48. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  49. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  50. package/resources/claude-customizations/config/blast-radius.json +7 -0
  51. package/resources/claude-customizations/pack-manifests/core.json +16 -0
  52. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  53. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
  54. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
  55. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
  56. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
  58. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
  59. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
  63. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
  64. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
  65. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
  66. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  67. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  68. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  69. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  70. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  71. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  72. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  73. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  74. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  75. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  76. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  77. package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
  78. package/resources/config/orchestration-handoff-registry.json +138 -0
  79. package/resources/config/orchestration-handoff.schema.json +472 -0
  80. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  81. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  82. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -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.
@@ -260,6 +260,61 @@ wildcard occupies or truncates the feature-folder segment, a contract token carr
260
260
  letter, and a token containing a placeholder or interpolation marker. `artifacts/` is not a known
261
261
  top-level segment, so a bare `artifacts/**` subtree claim no longer satisfies the shape rules.
262
262
 
263
+ ### Mechanically-mergeable path class (issue #643)
264
+
265
+ Some contended paths are not a design collision. A `.csproj` item list, a `packages.config` package
266
+ list, and an `app.config` binding-redirect list are additive registries: two items that each append a
267
+ distinct entry produce a textual conflict that a deterministic union resolves without judgement.
268
+ Serializing those items buys nothing.
269
+
270
+ `config/blast-radius.json` carries an optional `mergeable_paths` list of exact entries and `**`
271
+ globs. That list is the mechanically-mergeable path class. Its published default is the five-entry
272
+ list `**/*.csproj`, `**/packages.config`, `**/app.config`, `**/*.vbproj`, `**/*.props`.
273
+
274
+ A matching path **stays in the declared `paths`**. The exclusion is applied at exactly one place per
275
+ runtime — inside `conflicts` (`scripts/dev_tools/_blast_radius_conflicts.py`) and inside
276
+ `Test-BlastRadiusConflict` (`.claude/lib/blast-radius/BlastRadius.psm1`), on both radii's `paths`
277
+ immediately before the smallest-path-overlap computation. Derivation, normalization,
278
+ observed-radius construction, V1, V2, V3, and `detect_escaped_paths` are therefore unchanged: the
279
+ class narrows one comparison and nothing else. The key is optional and fail-closed: a truth table
280
+ that omits it excludes nothing and reproduces pre-change behaviour exactly.
281
+
282
+ The matcher is local to this comparison and does not extend the glob engine. A declared path matches
283
+ the class when, against some configured entry, any of the following holds: the two strings are
284
+ ordinally equal; or the declared path is concrete (carries no wildcard) and the configured entry is
285
+ a glob that contains it; or the declared path is concrete and, for a configured entry beginning with
286
+ `**/`, the same two-step test succeeds against the entry with that prefix removed, which is what
287
+ makes a root-level `Directory.Build.props` match `**/*.props`. A declared path that is itself a glob
288
+ is mergeable only when it equals a configured entry verbatim, because a glob can expand over
289
+ non-mergeable files. `_glob_to_regex_text` (`scripts/dev_tools/_blast_radius_glob.py`) and its
290
+ PowerShell mirror `BlastRadiusGlob.psm1` are unchanged by this class; if either is ever changed, both
291
+ it and this matcher must be re-examined together, since the containment step depends on the glob
292
+ engine's semantics.
293
+
294
+ Three constraints bound the mergeable-path class:
295
+
296
+ 1. **The planner remains obliged to enumerate a genuine write explicitly.** The class describes how a
297
+ contention is scored, not what an item may touch. An item that will write a `.csproj` still
298
+ declares that exact path in its radius.
299
+ 2. **The path stays in the declared radius, so the audits still see it.** Nothing is removed from any
300
+ stored record. Over-breadth, shared-surface, and manifest audits read the same `paths` they read
301
+ before; only the pairwise overlap set is narrowed.
302
+ 3. **`detect_escaped_paths` remains the backstop at execution time.** A mergeable classification is a
303
+ scheduling decision taken from declared text; `detect_escaped_paths` still compares the declared
304
+ radius against the paths a diff actually touched, so an item that wrote outside its radius is
305
+ caught against observed evidence regardless of this class.
306
+
307
+ An item record of the parallel-orchestrator checkpoint MAY carry an optional
308
+ `mergeable_conflicts_resolved` list recording the mergeable conflicts an item resolved mechanically.
309
+ The field is additive and tolerated-not-validated: no validator in Python, TypeScript, or bash reads
310
+ it, and it follows the `expected_conflict_components` diagnostic precedent — a record written for a
311
+ human reader and for post-hoc audit, never a scheduling input. Each entry carries `path`,
312
+ `resolved_at`, `merged_against`, `merge_commit_sha`, `entries_added_from_ours`,
313
+ `entries_added_from_theirs`, and `version_resolutions`. Entry keys inside those three lists use
314
+ `<ItemType>:<Include>` for MSBuild items, `package:<id>` for `packages.config` packages, and
315
+ `bindingRedirect:<name>` for `app.config` binding redirects. Invariant 9 (the six `blast_radius`
316
+ keys) and invariant 15 (the four edge reasons) are unchanged by this field.
317
+
263
318
  ### Placeholder-shape rejection (issue #502)
264
319
 
265
320
  The fourth shape is a token containing any member of the marker set
@@ -353,6 +408,15 @@ reading the file is not told something false, and because
353
408
  `tests/scripts/dev_tools/test_blast_radius_config.py` calls `load_module_globs` on it and that
354
409
  helper raises on an absent key. Nothing schedules on it.
355
410
 
411
+ **The .NET manifest family is a structure signal, not a module source (issue #643).** `.csproj`,
412
+ `.fsproj`, `.vbproj`, `.sln`, and `.slnx` are classified in
413
+ `extensions/drm-copilot/src/lib/push-down/claude-blast-radius-derive-manifests.ts` as
414
+ structure-observing manifests: each one marks a project root and suppresses the
415
+ top-level-directory fallback, and none of them contributes a module. A .NET solution layout
416
+ therefore derives exactly `{ "config": ["config/**"] }` from `PAYLOAD_MODULES` alone. The reason is
417
+ the granularity criterion: a per-assembly module would fire for the majority of work items in that
418
+ solution, so it discriminates nothing and belongs in neither the module map nor the surface list.
419
+
356
420
  **`PAYLOAD_MODULES` carries `config` only.** `claude-runtime` was removed from it by the same
357
421
  granularity criterion that removed it from this repository's own map. The criterion transfers
358
422
  without modification: every agent in the runtime is instructed to read the policy rules and process
@@ -366,8 +430,8 @@ non-vacuous input.
366
430
  subset, not a copy of the self-hosted sets.** They were authored narrow when the bundled copy was
367
431
  created and were never a copy that fell behind, so the correct gate is portable-set equality against
368
432
  a declared constant plus a subset relation against the self-hosted list — never byte-equality with
369
- the self-hosted file. Only `version`, `over_breadth_fraction`, and `mandate_reads` are byte-equal
370
- across the two copies.
433
+ the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, and `mergeable_paths`
434
+ are byte-equal across the two copies.
371
435
 
372
436
  The reason the two key groups take different relations is an asymmetry between surfaces and modules.
373
437
  An over-matching MODULE glob costs concurrency on every pair of items it touches, because a module