@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.
- package/out/mcp-server.js +368 -209
- package/package.json +2 -2
- 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-planner.md +18 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
- 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 +14 -1
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- 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 +20 -1
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +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`,
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|