@danmoisan/drm-copilot-mcp 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/out/mcp-server.js +30 -78
  2. package/package.json +2 -2
  3. package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
  4. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  6. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
  7. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
  8. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
  9. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
  10. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
  11. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
  12. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +11 -0
  13. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
  14. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
  15. package/resources/claude-customizations/config/blast-radius.json +14 -1
  16. package/resources/claude-customizations/pack-manifests/core.json +2 -0
  17. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  18. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
  19. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
  20. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  21. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  22. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  23. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
@@ -0,0 +1,446 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Write-intent extraction for the blast-radius library (issue #722).
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell mirror of
7
+ scripts/dev_tools/_blast_radius_write_intent.py. The rules narrow the text
8
+ harvest of derivation and validation so that only write-intent tokens become
9
+ radius entries, and they are active only when the truth table carries
10
+ write_intent_extraction set to true:
11
+
12
+ - W1 glob mention: a token containing * or ? is dropped.
13
+ - W2 command span: every token of an inline span that splits into more
14
+ than one word is dropped.
15
+ - W3 read task: the tokens of a task attribution window (a task line plus
16
+ the following non-task lines up to the next ATX heading) are dropped
17
+ when the title starts with a read verb, after an optional bold label,
18
+ and carries no write verb anywhere.
19
+ - W4 root anchoring: a concrete token whose first segment, after a
20
+ leading ./ is stripped, is not in path_roots and which is not a
21
+ configured root surface is dropped; an empty path_roots disables W4.
22
+ - W5 spec contracts only: the spec contributes contracts and no paths,
23
+ and contract harvesting applies W1 and W2.
24
+ - W6 placeholder stem: a concrete token whose final-component stem is in
25
+ the placeholder-stem set is dropped.
26
+
27
+ Each exported function names its Python counterpart:
28
+
29
+ - Test-WriteIntentExtractionEnabled config_write_intent_extraction
30
+ - Get-ConfigPathRoot config_path_roots
31
+ - Get-WriteIntentPlanPath extract_write_intent_plan_paths
32
+ - Get-WriteIntentSpecContract extract_write_intent_contracts
33
+ - Select-WriteIntentPathEntry select_write_intent_path_entries
34
+ - Get-PlanPathForConfig select_plan_paths
35
+
36
+ Parity notes for maintainers:
37
+ - The three vocabularies below are pinned equal, member for member and in
38
+ order, to the Python constants READ_VERBS, WRITE_VERBS, and
39
+ PLACEHOLDER_STEMS by a Pester parity test that reads the Python source.
40
+ - With the flag absent or false, Get-PlanPathForConfig returns exactly
41
+ Get-PlanPaths, so every caller behaves as before (fail closed).
42
+ - The rules only drop tokens the current classifier accepts; they never
43
+ add one. Every function is pure: no filesystem, subprocess, network, or
44
+ clock access, and no input is mutated.
45
+ - This module imports the extraction, glob, and config leaves only, never
46
+ the validation module or the facade, so no import cycle can form.
47
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
48
+ #>
49
+
50
+ Set-StrictMode -Version Latest
51
+ $ErrorActionPreference = 'Stop'
52
+
53
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusExtraction.psm1') -Force -ErrorAction Stop
54
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force -ErrorAction Stop
55
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1') -Force -ErrorAction Stop
56
+
57
+ # Truth-table keys read by this module.
58
+ $script:WriteIntentKey = 'write_intent_extraction'
59
+ $script:PathRootKey = 'path_roots'
60
+
61
+ # The three rule vocabularies, in the same order as the Python constants.
62
+ $script:WriteIntentReadVerb = @('Read', 'Verify', 'Confirm', 'Inspect', 'Review', 'Baseline')
63
+ $script:WriteIntentWriteVerb = @(
64
+ 'Fix', 'Write', 'Update', 'Edit', 'Add', 'Create', 'Delete', 'Remove',
65
+ 'Rename', 'Author', 'Append', 'Replace'
66
+ )
67
+ $script:WriteIntentPlaceholderStem = @(
68
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
69
+ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
70
+ 'foo', 'bar', 'baz', 'example', 'sample', 'placeholder'
71
+ )
72
+
73
+ # Plan and spec structure patterns, mirroring the extraction module's regexes.
74
+ $script:TaskPattern = [regex]::new('^- \[(?<state>[ xX])\] \[P(?<phase>\d+)-T(?<task>\d+)\] (?<title>.+)$')
75
+ $script:HeadingPattern = [regex]::new('^(?<hashes>#{1,6}) (?<title>.+)$')
76
+ $script:InlineCodeSpanPattern = [regex]::new('`([^`]+)`')
77
+ $script:LineSuffixPattern = [regex]::new(':\d+$')
78
+ $script:ContractLetterPattern = [regex]::new('[A-Za-z]')
79
+ $script:ContractHeadingKeyword = @('API', 'Interface', 'Contract', 'Surface')
80
+
81
+ # W3 title parsing: an optional leading bold label is skipped before the first
82
+ # word is read; write verbs match as whole words anywhere, case-insensitively.
83
+ $script:BoldLabelPattern = [regex]::new('^\*\*[^*]+\*\*\s*')
84
+ $script:FirstWordPattern = [regex]::new('^[A-Za-z]+')
85
+ $script:WriteVerbPattern = [regex]::new(
86
+ '\b(?:' + ($script:WriteIntentWriteVerb -join '|') + ')\b',
87
+ [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
88
+ $script:ReadVerbSet = [System.Collections.Generic.HashSet[string]]::new(
89
+ [string[]]@($script:WriteIntentReadVerb | ForEach-Object { $_.ToLowerInvariant() }),
90
+ [StringComparer]::Ordinal)
91
+ $script:PlaceholderStemSet = [System.Collections.Generic.HashSet[string]]::new(
92
+ [string[]]$script:WriteIntentPlaceholderStem, [StringComparer]::Ordinal)
93
+
94
+
95
+ function Test-WriteIntentExtractionEnabled {
96
+ <#
97
+ .SYNOPSIS
98
+ Read the write_intent_extraction flag strictly.
99
+
100
+ .DESCRIPTION
101
+ Port of config_write_intent_extraction. An absent or null key reads as
102
+ false, which keeps current extraction.
103
+
104
+ .PARAMETER Config
105
+ Parsed config/blast-radius.json.
106
+
107
+ .OUTPUTS
108
+ System.Boolean. The flag value. Throws, naming the key, when the value is
109
+ present and is not a boolean.
110
+ #>
111
+ [CmdletBinding()]
112
+ [OutputType([bool])]
113
+ param(
114
+ [Parameter(Mandatory = $true)]
115
+ [AllowNull()]
116
+ [object] $Config
117
+ )
118
+
119
+ $mapping = Get-RequiredMapping -Value $Config -FieldName 'config'
120
+ if (-not $mapping.ContainsKey($script:WriteIntentKey) -or $null -eq $mapping[$script:WriteIntentKey]) {
121
+ return $false
122
+ }
123
+ $value = $mapping[$script:WriteIntentKey]
124
+ if ($value -isnot [bool]) {
125
+ throw "config[""$($script:WriteIntentKey)""] must be a boolean, got $($value.GetType().Name)."
126
+ }
127
+ return $value
128
+ }
129
+
130
+ function Get-ConfigPathRoot {
131
+ <#
132
+ .SYNOPSIS
133
+ Read the path_roots list strictly.
134
+
135
+ .DESCRIPTION
136
+ Port of config_path_roots. An absent or null key yields an empty list,
137
+ which disables rule W4.
138
+
139
+ .PARAMETER Config
140
+ Parsed config/blast-radius.json.
141
+
142
+ .OUTPUTS
143
+ System.Object[]. First path segments, deduplicated and ordinally sorted.
144
+ Throws, naming the key, when the value is not a list of non-blank
145
+ strings.
146
+ #>
147
+ [CmdletBinding()]
148
+ [OutputType([System.Object[]])]
149
+ param(
150
+ [Parameter(Mandatory = $true)]
151
+ [AllowNull()]
152
+ [object] $Config
153
+ )
154
+
155
+ $mapping = Get-RequiredMapping -Value $Config -FieldName 'config'
156
+ if (-not $mapping.ContainsKey($script:PathRootKey) -or $null -eq $mapping[$script:PathRootKey]) {
157
+ return @()
158
+ }
159
+ return @(Get-RequiredStringList -Value $mapping[$script:PathRootKey] -FieldName "config[""$($script:PathRootKey)""]")
160
+ }
161
+
162
+ # Report whether a task title makes its attribution window a read task (W3).
163
+ function Test-ReadTaskTitle {
164
+ [CmdletBinding()]
165
+ [OutputType([bool])]
166
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Title)
167
+
168
+ $unlabeled = $script:BoldLabelPattern.Replace($Title.Trim(), '', 1)
169
+ $firstWord = $script:FirstWordPattern.Match($unlabeled)
170
+ if (-not $firstWord.Success -or -not $script:ReadVerbSet.Contains($firstWord.Value.ToLowerInvariant())) {
171
+ return $false
172
+ }
173
+ return -not $script:WriteVerbPattern.IsMatch($Title)
174
+ }
175
+
176
+ # Report whether an entry is a feature-folder glob, which is never dropped.
177
+ function Test-FeatureFolderGlob {
178
+ [CmdletBinding()]
179
+ [OutputType([bool])]
180
+ param([Parameter(Mandatory = $true)] [string] $Entry)
181
+
182
+ if (-not ($Entry.StartsWith('docs/features/', [System.StringComparison]::Ordinal) -and
183
+ $Entry.EndsWith('/**', [System.StringComparison]::Ordinal))) {
184
+ return $false
185
+ }
186
+ $body = $Entry.Substring(0, $Entry.Length - 3)
187
+ return ($body.IndexOf('*') -lt 0 -and $body.IndexOf('?') -lt 0)
188
+ }
189
+
190
+ # Apply the token-level rules W1, W4, and W6 to one token; true means it survives.
191
+ function Test-WriteIntentToken {
192
+ [CmdletBinding()]
193
+ [OutputType([bool])]
194
+ param(
195
+ [Parameter(Mandatory = $true)] [string] $Token,
196
+ [AllowEmptyCollection()] [string[]] $RootSurface = @(),
197
+ [AllowEmptyCollection()] [string[]] $PathRoot = @()
198
+ )
199
+
200
+ # W1: a wildcard marks a glob mention rather than a write claim.
201
+ if ($Token.IndexOf('*') -ge 0 -or $Token.IndexOf('?') -ge 0) {
202
+ return $false
203
+ }
204
+
205
+ # W4: enabled only by a non-empty root list; a configured root surface is a
206
+ # repository-root file with no first directory segment to test.
207
+ if ($PathRoot.Count -gt 0 -and $RootSurface -cnotcontains $Token) {
208
+ $stripped = if ($Token.StartsWith('./', [System.StringComparison]::Ordinal)) { $Token.Substring(2) } else { $Token }
209
+ if ($PathRoot -cnotcontains $stripped.Split('/', 2)[0]) {
210
+ return $false
211
+ }
212
+ }
213
+
214
+ # W6: the final component's stem, compared case-insensitively.
215
+ $finalComponent = $script:LineSuffixPattern.Replace($Token.Substring($Token.LastIndexOf('/') + 1), '')
216
+ $stem = if ($finalComponent.Contains('.')) { $finalComponent.Substring(0, $finalComponent.LastIndexOf('.')) } else { $finalComponent }
217
+ return -not $script:PlaceholderStemSet.Contains($stem.ToLowerInvariant())
218
+ }
219
+
220
+ # Yield the token of every inline span that holds exactly one word (W2).
221
+ function Get-SingleWordToken {
222
+ [CmdletBinding()]
223
+ [OutputType([System.Object[]])]
224
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Line)
225
+
226
+ $token = [System.Collections.Generic.List[string]]::new()
227
+ foreach ($match in $script:InlineCodeSpanPattern.Matches($Line)) {
228
+ $word = @($match.Groups[1].Value -split '\s+' | Where-Object { $_.Length -gt 0 })
229
+ if ($word.Count -eq 1) {
230
+ $token.Add($word[0])
231
+ }
232
+ }
233
+ return @($token.ToArray())
234
+ }
235
+
236
+ function Get-WriteIntentPlanPath {
237
+ <#
238
+ .SYNOPSIS
239
+ Extract plan paths under rules W1 through W4 and W6.
240
+
241
+ .DESCRIPTION
242
+ Port of extract_write_intent_plan_paths. A task line opens a window
243
+ whose read status comes from its title; an ATX heading closes any
244
+ window; every other line belongs to the open window. Tokens of a read
245
+ window are dropped (W3); every other line is harvested under W1, W2, W4,
246
+ and W6 after the current classifier accepts the token.
247
+
248
+ .PARAMETER PlanText
249
+ Full atomic-plan document text; may be empty.
250
+
251
+ .PARAMETER RootSurface
252
+ Configured separator-free root surfaces, forwarded to the classifier and
253
+ exempt from W4.
254
+
255
+ .PARAMETER PathRoot
256
+ Configured first segments for W4; empty disables W4.
257
+
258
+ .OUTPUTS
259
+ System.Object[]. Surviving tokens, deduplicated and ordinally sorted.
260
+ #>
261
+ [CmdletBinding()]
262
+ [OutputType([System.Object[]])]
263
+ param(
264
+ [Parameter(Mandatory = $true)]
265
+ [AllowEmptyString()]
266
+ [string] $PlanText,
267
+ [AllowEmptyCollection()] [string[]] $RootSurface = @(),
268
+ [AllowEmptyCollection()] [string[]] $PathRoot = @()
269
+ )
270
+
271
+ $accepted = [System.Collections.Generic.List[string]]::new()
272
+ $inReadWindow = $false
273
+ foreach ($line in @(ConvertTo-NormalizedLine -Text $PlanText)) {
274
+ $taskMatch = $script:TaskPattern.Match($line)
275
+ if ($taskMatch.Success) {
276
+ $inReadWindow = Test-ReadTaskTitle -Title $taskMatch.Groups['title'].Value
277
+ } elseif ($script:HeadingPattern.IsMatch($line)) {
278
+ $inReadWindow = $false
279
+ }
280
+ if ($inReadWindow) {
281
+ continue
282
+ }
283
+
284
+ foreach ($token in @(Get-SingleWordToken -Line $line)) {
285
+ if ($null -ne (Get-PathTokenKind -Token $token -RootSurface $RootSurface) -and
286
+ (Test-WriteIntentToken -Token $token -RootSurface $RootSurface -PathRoot $PathRoot)) {
287
+ $accepted.Add($token)
288
+ }
289
+ }
290
+ }
291
+ return @(Get-OrdinalSortedEntry -Entry $accepted.ToArray())
292
+ }
293
+
294
+ function Get-WriteIntentSpecContract {
295
+ <#
296
+ .SYNOPSIS
297
+ Extract spec contract identifiers with W1 and W2 applied (W5).
298
+
299
+ .DESCRIPTION
300
+ Port of extract_write_intent_contracts. Identifiers are harvested only
301
+ inside a section whose heading, or an ancestor heading, names an API,
302
+ interface, contract, or surface; a multi-word span and a wildcard token
303
+ contribute nothing.
304
+
305
+ .PARAMETER SpecText
306
+ Full feature spec.md document text; may be empty.
307
+
308
+ .OUTPUTS
309
+ System.Object[]. Identifiers carrying an ASCII letter and no separator,
310
+ deduplicated and ordinally sorted.
311
+ #>
312
+ [CmdletBinding()]
313
+ [OutputType([System.Object[]])]
314
+ param(
315
+ [Parameter(Mandatory = $true)]
316
+ [AllowEmptyString()]
317
+ [string] $SpecText
318
+ )
319
+
320
+ $identifier = [System.Collections.Generic.List[string]]::new()
321
+ $qualifyingDepth = 0
322
+ foreach ($line in @(ConvertTo-NormalizedLine -Text $SpecText)) {
323
+ $headingMatch = $script:HeadingPattern.Match($line)
324
+
325
+ # A deeper heading stays inside the qualifying section; a heading at or
326
+ # above that level ends it and is judged on its own title.
327
+ if ($headingMatch.Success) {
328
+ $level = $headingMatch.Groups['hashes'].Value.Length
329
+ if ($qualifyingDepth -ne 0 -and $level -gt $qualifyingDepth) {
330
+ continue
331
+ }
332
+ $title = $headingMatch.Groups['title'].Value
333
+ $qualifyingDepth = 0
334
+ foreach ($keyword in $script:ContractHeadingKeyword) {
335
+ if ($title.IndexOf($keyword, [System.StringComparison]::Ordinal) -ge 0) {
336
+ $qualifyingDepth = $level
337
+ break
338
+ }
339
+ }
340
+ continue
341
+ }
342
+ if ($qualifyingDepth -eq 0) {
343
+ continue
344
+ }
345
+
346
+ foreach ($token in @(Get-SingleWordToken -Line $line)) {
347
+ if ($token.IndexOf('/') -lt 0 -and $token.IndexOf('*') -lt 0 -and
348
+ $token.IndexOf('?') -lt 0 -and $script:ContractLetterPattern.IsMatch($token)) {
349
+ $identifier.Add($token)
350
+ }
351
+ }
352
+ }
353
+ return @(Get-OrdinalSortedEntry -Entry $identifier.ToArray())
354
+ }
355
+
356
+ function Select-WriteIntentPathEntry {
357
+ <#
358
+ .SYNOPSIS
359
+ Apply the token-level rules W1, W4, and W6 to recorded radius entries.
360
+
361
+ .DESCRIPTION
362
+ Port of select_write_intent_path_entries. Normalization calls this
363
+ before the mandate-read filter when the flag is true. The feature-folder
364
+ glob is always kept.
365
+
366
+ .PARAMETER Entry
367
+ Recorded entries already accepted by the current classifier.
368
+
369
+ .PARAMETER RootSurface
370
+ Configured separator-free root surfaces.
371
+
372
+ .PARAMETER PathRoot
373
+ Configured first segments for W4.
374
+
375
+ .OUTPUTS
376
+ System.Object[]. Surviving entries, deduplicated and ordinally sorted.
377
+ #>
378
+ [CmdletBinding()]
379
+ [OutputType([System.Object[]])]
380
+ param(
381
+ [Parameter(Mandatory = $true)]
382
+ [AllowEmptyCollection()]
383
+ [string[]] $Entry,
384
+ [AllowEmptyCollection()] [string[]] $RootSurface = @(),
385
+ [AllowEmptyCollection()] [string[]] $PathRoot = @()
386
+ )
387
+
388
+ $kept = [System.Collections.Generic.List[string]]::new()
389
+ foreach ($item in $Entry) {
390
+ if ((Test-FeatureFolderGlob -Entry $item) -or
391
+ (Test-WriteIntentToken -Token $item -RootSurface $RootSurface -PathRoot $PathRoot)) {
392
+ $kept.Add($item)
393
+ }
394
+ }
395
+ return @(Get-OrdinalSortedEntry -Entry $kept.ToArray())
396
+ }
397
+
398
+ function Get-PlanPathForConfig {
399
+ <#
400
+ .SYNOPSIS
401
+ Select the plan-side extractor from the truth table's flag.
402
+
403
+ .DESCRIPTION
404
+ Port of select_plan_paths, the single selector shared by derivation and
405
+ by validation rules V1 and V2, which keeps a derived radius passing V1
406
+ and V2 against its own plan in both modes.
407
+
408
+ .PARAMETER PlanText
409
+ Full atomic-plan document text; may be empty.
410
+
411
+ .PARAMETER Config
412
+ Parsed config/blast-radius.json.
413
+
414
+ .PARAMETER RootSurface
415
+ Configured separator-free root surfaces.
416
+
417
+ .OUTPUTS
418
+ System.Object[]. The write-intent plan paths when the flag is true, and
419
+ exactly Get-PlanPaths otherwise.
420
+ #>
421
+ [CmdletBinding()]
422
+ [OutputType([System.Object[]])]
423
+ param(
424
+ [Parameter(Mandatory = $true)]
425
+ [AllowEmptyString()]
426
+ [string] $PlanText,
427
+ [Parameter(Mandatory = $true)]
428
+ [AllowNull()]
429
+ [object] $Config,
430
+ [AllowEmptyCollection()] [string[]] $RootSurface = @()
431
+ )
432
+
433
+ if (Test-WriteIntentExtractionEnabled -Config $Config) {
434
+ return @(Get-WriteIntentPlanPath -PlanText $PlanText -RootSurface $RootSurface `
435
+ -PathRoot ([string[]]@(Get-ConfigPathRoot -Config $Config)))
436
+ }
437
+ return @(Get-PlanPaths -PlanText $PlanText -RootSurface $RootSurface)
438
+ }
439
+
440
+ Export-ModuleMember -Function `
441
+ Test-WriteIntentExtractionEnabled, `
442
+ Get-ConfigPathRoot, `
443
+ Get-WriteIntentPlanPath, `
444
+ Get-WriteIntentSpecContract, `
445
+ Select-WriteIntentPathEntry, `
446
+ Get-PlanPathForConfig
@@ -209,6 +209,8 @@ All nine enums of the parallel surface are owned by the schema-and-validator fea
209
209
 
210
210
  The wave-4 features — F6 (mutation protocol), F7 (enforcement hooks), and F8 (drift detection) — CONSUME these member sets and NEVER extend them. A wave-4 feature that needs a new member must amend this rule file and the validators at spec review, not add the member at implementation time. This constraint exists because the wave-4 features are prepared concurrently and would otherwise add fields to the same files at the same time.
211
211
 
212
+ Integration-cost scheduling (issue #722) adds no `conflict_edges[].reason` member; the four members above are unchanged. The scheduling data travels in tolerated-not-validated fields: an edge may carry the extra fields `hard`, `cost`, and `benefit`, and the planner and orchestrator checkpoints may carry a `tolerated_overlaps` list. No validator reads these fields, and the Python validators and the TypeScript validator port accept them with zero errors.
213
+
212
214
  ## F7 Seam
213
215
 
214
216
  The retrospective cohort-ordering invariant `PARALLEL_COHORT_BARRIER_VIOLATION` (design section 9, Layer 2) is F7's explicitly assigned addition to the orchestrator validator. It is NOT implemented here. The entry point of `scripts/dev_tools/validate_parallel_orchestrator_state.py` contains a clearly delimited, appendable helper-invocation block, marked with explicit begin and end comments that name F7 and the invariant token, so that F7's edit is one appended helper call with no reflow of existing code. The TypeScript core `extensions/drm-copilot/src/lib/validate/parallel-orchestrator-state-core.ts` carries the matching comment-delimited seam. Existing helper calls sit outside the block.
@@ -241,6 +243,11 @@ plan-side extraction so V1 and V2 stay self-consistent against a radius derived
241
243
  The key is optional and fail-closed: a truth table that omits it excludes nothing and reproduces
242
244
  pre-change behaviour exactly.
243
245
 
246
+ The Copilot instructions file `.github/copilot-instructions.md` is a mandate read as well (issue
247
+ #722): the policy reading order begins with it, so a plan that cites copilot-instructions.md is
248
+ reporting that it read the tone and communication policy. Both committed copies of
249
+ `config/blast-radius.json` list it in `mandate_reads`.
250
+
244
251
  Three constraints bound the mandate-read exclusion:
245
252
 
246
253
  1. **The planner remains obliged to enumerate a genuine write explicitly.** An exclusion describes
@@ -391,6 +398,144 @@ weakens the relation below the path level: two items editing the same file still
391
398
  A candidate module belongs in the map when it names a subsystem an item could plausibly not touch.
392
399
  A candidate that matches the majority of work items belongs nowhere.
393
400
 
401
+ ### Integration-cost scheduling (issue #722)
402
+
403
+ Detection and scheduling are separate steps. The detection relation (`conflicts` in Python,
404
+ `Test-BlastRadiusConflict` in PowerShell) is unchanged and still answers one question: do two radii
405
+ contend? Scheduling decides whether a detected contention must serialize the two items. The entry
406
+ points are `schedule_conflict_edges` in `scripts/dev_tools/_blast_radius_scheduling.py` and
407
+ Get-BlastRadiusConflictEdge in `.claude/lib/blast-radius/BlastRadiusScheduling.psm1`; both return
408
+ the edges and the tolerated overlaps of a set of items. Planners call the entry point instead of
409
+ applying the detection relation to every pair by hand.
410
+
411
+ This is an operator-directed configured policy change (issue #722, 2026-09-27), not ad hoc
412
+ narrowing. The only sanctioned mechanisms for relaxing contention are the configured
413
+ `conflict_tolerance` key and configured write-intent extraction. Planners still never hand-narrow a
414
+ radius: editing a declared radius to suppress an edge remains prohibited.
415
+
416
+ **Configuration.** `config/blast-radius.json` carries an optional `conflict_tolerance` object with
417
+ tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only, and
418
+ module, each an integer of at least 1), `band_durations` (C1 through C4, each an integer of at least
419
+ 1), `default_band` (one of C1 through C4), and append_only_paths (a list of exact entries and `**`
420
+ globs). The reader fails fast with an error naming the key for any other shape; booleans are
421
+ rejected wherever an integer is required. An absent key means strict scheduling, identical to
422
+ tolerance_percent 0.
423
+
424
+ **Edge rule.** For each unordered pair of items:
425
+
426
+ 1. Run the unchanged detection relation. No conflict means no edge and no tolerated overlap.
427
+ 2. **Hard classes.** A reason list containing `shared_surface_overlap` or `contract_dependency`
428
+ makes the pair an edge at every tolerance.
429
+ 3. **Cost** (integer, computed only for a detected conflict). Re-enumerate the overlapping entry
430
+ pairs with the same mergeable-path exclusion and the same entry-overlap primitive the relation
431
+ uses, then sum the append_only weight for a concrete overlapping path that matches an
432
+ append_only_paths entry, the same_file weight for two equal concrete entries, the
433
+ possible_overlap weight for a glob or directory-prefix overlap, and the module weight times the
434
+ number of shared modules. The append_only_paths check runs before the same_file check (append-only
435
+ precedence), so an append-only file stays low cost even when both items name it. Mergeable paths
436
+ contribute 0 because they are excluded before enumeration.
437
+ 4. **Benefit** (integer, pairwise). The smaller of the two items' band durations; a missing band uses
438
+ `default_band`. Running the pair concurrently saves at most one cohort step of the shorter item.
439
+ 5. **Rule.** The pair is an edge if and only if it conflicts and either it is hard or
440
+ `cost * 100 > benefit * tolerance_percent`.
441
+ 6. **Recorded reason.** The first member of the relation's reason list in canonical kind order.
442
+
443
+ **Strict-identity proof.** Every weight and every band duration is validated as an integer of at
444
+ least 1. A detected conflict that is not hard therefore has a cost of at least 1, so at
445
+ tolerance_percent 0 the inequality `cost * 100 > 0` always holds and the edge set equals the
446
+ detected-conflict set exactly; cohort coloring of that set is identical to strict scheduling. At
447
+ every tolerance an edge requires a conflict, so every edge implies a conflict and the edge set is
448
+ always a subset of the detected-conflict set. Raising tolerance_percent never adds an edge, and the
449
+ decision for (a, b) equals the decision for (b, a).
450
+
451
+ **Soft-overlap handling.** A detected, non-hard pair within tolerance is a tolerated overlap, not an
452
+ edge. The planner records it in a tolerated_overlaps list on the planner checkpoint (and the
453
+ orchestrator carries it), each entry holding `a`, `b`, `reasons`, `cost`, and `benefit`. Tolerated
454
+ pairs run in the same or adjacent cohorts without a barrier. The later-merging item of a tolerated
455
+ pair merges `origin/main` under the existing per-item merge-conflict handling and re-passes CI before
456
+ it merges.
457
+
458
+ **Drift behaviour.** When an item's observed radius replaces its declared radius, drift detection
459
+ evaluates each in-flight peer pair through the same edge rule, using the items' complexity bands from
460
+ the checkpoint (`default_band` when absent), through the helper module
461
+ `scripts/dev_tools/_parallel_drift_scheduling.py`. A pair is newly conflicting only when the rule
462
+ yields an edge and the pair is not already a recorded edge. A tolerated pair whose observed overlap
463
+ stays within tolerance halts neither item; one whose observed overlap exceeds tolerance, or becomes
464
+ hard, is reported. An unevaluable peer radius still counts as an edge (fail closed). At
465
+ tolerance_percent 0 the drift output equals strict drift output.
466
+
467
+ ### Write-intent extraction (issue #722)
468
+
469
+ The extractor harvests every inline-code token of a plan and spec, so a path an item only reads,
470
+ mentions as a glob, or quotes inside a command becomes a radius path and can create a false edge.
471
+ Write-intent extraction keeps only tokens that state a write. It is the second sanctioned mechanism,
472
+ beside `conflict_tolerance`, for relaxing contention; like it, it is an operator-directed configured
473
+ policy change, and planners still never hand-narrow a radius.
474
+
475
+ **Configuration.** `config/blast-radius.json` carries two optional keys. `write_intent_extraction`
476
+ is a boolean; when it is true, rules W1 through W6 below apply. `path_roots` is a list of non-empty
477
+ first path segments that enables rule W4. Both readers fail fast with an error naming the key for any
478
+ other shape. An absent or false `write_intent_extraction` reproduces pre-change extraction exactly
479
+ (fail closed), and an empty or absent `path_roots` disables W4. The self-hosted copy sets
480
+ `path_roots` to this repository's tracked top-level directories. The implementations are
481
+ `scripts/dev_tools/_blast_radius_write_intent.py` and
482
+ `.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1`.
483
+
484
+ **Rules.** Each rule is a pure, statically decidable function of the text and the truth table.
485
+
486
+ - **W1 glob mention.** Drop any harvested token containing a wildcard character. The feature-folder
487
+ glob is added after filtering and is never dropped.
488
+ - **W2 command span.** Drop every token of an inline span whose whitespace split yields more than
489
+ one word.
490
+ - **W3 read task.** Drop the tokens of a task attribution window (a task line plus the following
491
+ non-task lines up to the next ATX heading, as defined in `.claude/rules/plan-acceptance-gates.md`)
492
+ whose title's first word, after an optional bold label, is in the read-verb set (Read, Verify,
493
+ Confirm, Inspect, Review, Baseline) and whose title contains no member of the write-verb set (Fix,
494
+ Write, Update, Edit, Add, Create, Delete, Remove, Rename, Author, Append, Replace). A write verb
495
+ anywhere in the title overrides the read verb.
496
+ - **W4 root anchoring.** Drop a concrete token whose first segment, after stripping a leading `./`,
497
+ is not in `path_roots` and is not a configured root surface.
498
+ - **W5 spec paths.** The spec contributes contracts only, not paths. Contract harvesting from the
499
+ spec applies W1 and W2.
500
+ - **W6 placeholder stem.** Drop a concrete token whose final-component stem is in the placeholder
501
+ set (the single ASCII letters, and foo, bar, baz, example, sample, placeholder), compared
502
+ case-insensitively.
503
+
504
+ The read-verb, write-verb, and placeholder-stem sets are code constants pinned equal across Python
505
+ and PowerShell by a parity test. Shared surfaces are still resolved from the surviving concrete
506
+ paths, so a shared surface cited only in a read task, a command span, or the spec produces no hard
507
+ edge, while a shared surface an item writes in a write task survives and stays hard.
508
+
509
+ **One extractor for derivation and validation.** W1, W4, and W6 are token-level and also apply when
510
+ `normalize_declared_radius` and Get-NormalizedDeclaredRadius re-filter a recorded radius; an entry
511
+ that starts with `docs/features/`, ends with `/**`, and carries no other wildcard (the feature-folder
512
+ glob) is always kept. W2, W3, and W5 need line context and apply only to derivation and to the
513
+ plan-side extraction of validation. `derive_blast_radius` and `validate_blast_radius` (and their
514
+ PowerShell ports) select the same extractor from the same flag, so a derived radius still passes V1
515
+ and V2 against its own plan in both modes.
516
+
517
+ #### Known false negatives
518
+
519
+ Write-intent extraction can drop a genuine write in these cases:
520
+
521
+ 1. a genuine write stated only as a glob (W1);
522
+ 2. a genuine write stated only inside a command span (W2);
523
+ 3. a write named only in spec prose (W5);
524
+ 4. a read-verb task that also writes without a write verb in its title (W3);
525
+ 5. a new top-level directory not yet in `path_roots` (W4);
526
+ 6. a genuine file whose stem is in the placeholder set (W6).
527
+
528
+ Three mitigations bound these cases:
529
+
530
+ 1. **The planner remains obliged to enumerate a genuine write explicitly.** When an item's plan will
531
+ write a path that one of the rules drops, the planner appends that exact path to the declared
532
+ radius after normalization, as for the mandate-read exclusion.
533
+ 2. **Execution-time escaped-path detection.** `detect_escaped_paths` compares the declared radius
534
+ against the paths a diff actually touched, so a dropped write is caught against observed evidence
535
+ rather than against prose.
536
+ 3. **The fail-closed absent key.** A truth table without `write_intent_extraction`, or with it set
537
+ to false, reproduces pre-change extraction exactly, so an operator can disable the rules at once.
538
+
394
539
  ### The published truth table is not a copy of this one (issue #500)
395
540
 
396
541
  The push-down publishes a second truth table into a destination workspace at
@@ -430,8 +575,11 @@ non-vacuous input.
430
575
  subset, not a copy of the self-hosted sets.** They were authored narrow when the bundled copy was
431
576
  created and were never a copy that fell behind, so the correct gate is portable-set equality against
432
577
  a declared constant plus a subset relation against the self-hosted list — never byte-equality with
433
- the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, and `mergeable_paths`
434
- are byte-equal across the two copies.
578
+ the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, `mergeable_paths`,
579
+ `conflict_tolerance`, and `write_intent_extraction` are byte-equal across the two copies.
580
+ `path_roots` is a Class 2 key: its self-hosted value is this repository's tracked top-level
581
+ directory list, and its bundled value is an empty list, because the bundle cannot know a
582
+ destination's top-level directories and an empty list disables root anchoring there.
435
583
 
436
584
  The reason the two key groups take different relations is an asymmetry between surfaces and modules.
437
585
  An over-matching MODULE glob costs concurrency on every pair of items it touches, because a module
@@ -85,7 +85,7 @@ The window size is fixed at four adjacent non-blank lines. Blank lines are remov
85
85
 
86
86
  ### The shared measurement behind G7, G8, G8b, and G9
87
87
 
88
- The four severities below were fixed by one corpus measurement recorded in `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`. The pre-declared decision rule was written and committed **before** the driver existed and before any count was taken, so the ordering is verifiable from git history rather than only asserted in prose. Its form matches the G5 precedent: the shipped severity is Blocking if and only if the total finding count for that rule is greater than zero **and** the recorded false-positive count for that rule is zero; otherwise Warning.
88
+ The four severities below were fixed by one corpus measurement recorded in `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`. The pre-declared decision rule was written and committed **before** the driver existed and before any count was taken, so the ordering is verifiable from git history rather than only asserted in prose. Its form matches the G5 precedent: the shipped severity is Blocking if and only if the total finding count for that rule is greater than zero **and** the recorded false-positive count for that rule is zero; otherwise Warning.
89
89
 
90
90
  The false-positive definition the rule consumes is likewise pre-declared: a finding is a false positive when the acceptance condition it reports is in fact falsifiable — when the plan states an observation sufficient to distinguish a passing run from a failing one, by a mechanism the rule's predicate does not recognise. A finding is a true positive when the acceptance condition it reports genuinely cannot fail, or can only be satisfied vacuously.
91
91
 
@@ -108,7 +108,7 @@ G7's finding count of 466 satisfies the first conjunct, so the rule was not deci
108
108
 
109
109
  Both classes are addressable by a later feature — Class 1 by adding a check-flag exclusion to the `prettier-write` entry, Class 2 by admitting a tree-observation span as an alternative to a marker — but neither was changed here, because narrowing a predicate after reading its measurement would invalidate the measurement that decided its severity. A later feature that narrows either class must re-take the measurement the same way.
110
110
 
111
- Counts, and every one of the 22 false positives named by plan path, task identifier, and offending span: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
111
+ Counts, and every one of the 22 false positives named by plan path, task identifier, and offending span: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
112
112
 
113
113
  ### G8 — ships as a Warning because the measurement recorded 7 false positives
114
114
 
@@ -118,7 +118,7 @@ G8's finding count of 82 satisfies the first conjunct. It fails the second: 7 fi
118
118
 
119
119
  **Class 2, one finding — the unmerged-path filter.** `--diff-filter=U` selects conflicted paths during a merge or rebase. In that state the worktree-against-index comparison is the correct one and does not become vacuous on commit, because the conflict is precisely what is not yet committed.
120
120
 
121
- Counts, and every one of the 7 false positives named by plan path, task identifier, and offending span: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
121
+ Counts, and every one of the 7 false positives named by plan path, task identifier, and offending span: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
122
122
 
123
123
  ### G8b — ships as a Warning unconditionally, by the pre-declared rule
124
124
 
@@ -126,7 +126,7 @@ G8b is exempt from the two-conjunct rule and **cannot reach the Blocking channel
126
126
 
127
127
  The reason is that G8b carries the highest false-positive surface of the set, and a false-positive count taken over one corpus does not bound the false-positive surface of a predicate. Promoting a rule on the strength of a clean run over one corpus would convert an accident of that corpus into a gate. The 19 findings were nonetheless examined individually and all 19 were retained as true positives, across three sub-classes: a placeholder ref operand, which resolves to a real ref at run time and still cannot make the diff see an untracked path; an artifact-field label inside the span, which is cosmetic and leaves the underlying acceptance condition as the finding describes it; and a pathspec written without the `--` separator, where the predicate reads the pathspec as a ref operand. That last sub-class is a true positive for G8b and is simultaneously recorded in the measurement as a **G8 false negative**: the same span should also have been reported by G8, and was not.
128
128
 
129
- Counts, the zero false-positive record, and the three sub-classes with their plan paths and offending spans: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
129
+ Counts, the zero false-positive record, and the three sub-classes with their plan paths and offending spans: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
130
130
 
131
131
  ### G9 — ships as a Warning because the measurement recorded 4 false positives
132
132
 
@@ -134,7 +134,7 @@ G9's finding count of 8 satisfies the first conjunct. It fails the second: 4 of
134
134
 
135
135
  The remaining 4 findings are true positives, and each is the exact defect the rule was written for: the acceptance condition demands a numeric coverage percentage, the command supplies no terminal reporter, the project `addopts` supplies only an LCOV reporter, and the number the acceptance demands is therefore never printed.
136
136
 
137
- Counts, and every one of the 4 false positives named by plan path, task identifier, offending span, and the reason its acceptance condition is falsifiable: `docs/features/active/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
137
+ Counts, and every one of the 4 false positives named by plan path, task identifier, offending span, and the reason its acceptance condition is falsifiable: `docs/features/completed/2026-08-23-plan-acceptance-gates-miss-unobservable-and-ambient-state-gates-519/evidence/qa-gates/corpus-measurement.2026-08-24T00-00.md`.
138
138
 
139
139
  ## The Write-Mode Register
140
140