@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.
- package/out/mcp-server.js +3517 -1104
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
- package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/config/blast-radius.json +7 -0
- package/resources/claude-customizations/pack-manifests/core.json +16 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
- package/resources/config/orchestration-handoff-registry.json +138 -0
- package/resources/config/orchestration-handoff.schema.json +472 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +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 `
|
|
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
|