@danmoisan/drm-copilot-mcp 1.1.24 → 1.1.26
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/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
- package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
- package/resources/claude-customizations/.claude/settings.json +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +15 -5
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +26 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +15 -5
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +15 -5
- package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -10
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pure precedence and scheduling-record functions for the parallel surface (issue #1047).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Holds the operator precedence input of /parallel-plan and /parallel-add
|
|
7
|
+
(`--after`): validation of the precedence[] entries, their normalization into
|
|
8
|
+
separating conflict edges, the precedence lift over a cohort table, the
|
|
9
|
+
active-entry resolution and cohort-table check, and the durable scheduling
|
|
10
|
+
record with its edge-reuse rule.
|
|
11
|
+
|
|
12
|
+
A precedence entry {before, after} states that the `after` item must start
|
|
13
|
+
in a later cohort than the `before` item. It is a separating input, never a
|
|
14
|
+
`depends_on` key. Every function is pure: no filesystem, subprocess, network,
|
|
15
|
+
or clock access, and no input is mutated. Rejections throw a message that
|
|
16
|
+
starts with the rejection code (precedence_unknown_item, precedence_cycle).
|
|
17
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
18
|
+
#>
|
|
19
|
+
|
|
20
|
+
Set-StrictMode -Version Latest
|
|
21
|
+
$ErrorActionPreference = 'Stop'
|
|
22
|
+
|
|
23
|
+
function Get-ParallelPrecedenceMapValue {
|
|
24
|
+
<#
|
|
25
|
+
.SYNOPSIS
|
|
26
|
+
Return the value stored under Key in a dictionary, or null when the key or the map is absent.
|
|
27
|
+
#>
|
|
28
|
+
[CmdletBinding()]
|
|
29
|
+
[OutputType([object])]
|
|
30
|
+
param([AllowNull()][object] $Map, [string] $Key)
|
|
31
|
+
|
|
32
|
+
if ($Map -isnot [System.Collections.IDictionary]) { return $null }
|
|
33
|
+
if (-not $Map.Contains($Key)) { return $null }
|
|
34
|
+
return $Map[$Key]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ConvertTo-ParallelPrecedenceKey {
|
|
38
|
+
<#
|
|
39
|
+
.SYNOPSIS
|
|
40
|
+
Convert an item key (integer or decimal-digit string) to Int64, or return null.
|
|
41
|
+
#>
|
|
42
|
+
[CmdletBinding()]
|
|
43
|
+
[OutputType([object], [long])]
|
|
44
|
+
param([AllowNull()][object] $Value)
|
|
45
|
+
|
|
46
|
+
if ($null -eq $Value -or $Value -is [bool]) { return $null }
|
|
47
|
+
$parsed = [long]0
|
|
48
|
+
if ($Value -is [string]) {
|
|
49
|
+
if ($Value -match '^[0-9]+$' -and [long]::TryParse($Value, [ref] $parsed)) { return $parsed }
|
|
50
|
+
return $null
|
|
51
|
+
}
|
|
52
|
+
if ($Value -is [int] -or $Value -is [long] -or $Value -is [int16] -or $Value -is [byte]) { return [long]$Value }
|
|
53
|
+
return $null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Get-ParallelPrecedencePair {
|
|
57
|
+
<#
|
|
58
|
+
.SYNOPSIS
|
|
59
|
+
Read precedence entries into [before, after] Int64 pairs.
|
|
60
|
+
|
|
61
|
+
.DESCRIPTION
|
|
62
|
+
Each entry is a dictionary carrying `before` and `after`. An entry whose
|
|
63
|
+
key is missing or is not an integer throws precedence_unknown_item.
|
|
64
|
+
#>
|
|
65
|
+
[CmdletBinding()]
|
|
66
|
+
[OutputType([System.Object[]])]
|
|
67
|
+
param([AllowNull()][AllowEmptyCollection()][object[]] $Entry = @())
|
|
68
|
+
|
|
69
|
+
$pairs = [System.Collections.Generic.List[object]]::new()
|
|
70
|
+
foreach ($item in @($Entry)) {
|
|
71
|
+
if ($null -eq $item) { continue }
|
|
72
|
+
$before = ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $item -Key 'before')
|
|
73
|
+
$after = ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $item -Key 'after')
|
|
74
|
+
if ($null -eq $before -or $null -eq $after) {
|
|
75
|
+
throw "precedence_unknown_item: a precedence entry must carry integer before and after item keys."
|
|
76
|
+
}
|
|
77
|
+
$pairs.Add([long[]]@($before, $after))
|
|
78
|
+
}
|
|
79
|
+
return , $pairs.ToArray()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Get-ParallelPrecedenceOrder {
|
|
83
|
+
<#
|
|
84
|
+
.SYNOPSIS
|
|
85
|
+
Return the item keys in topological precedence order, ties by ascending key.
|
|
86
|
+
|
|
87
|
+
.DESCRIPTION
|
|
88
|
+
Kahn's algorithm over the before -> after pairs. Keys left unvisited lie on
|
|
89
|
+
a cycle, which throws precedence_cycle naming them.
|
|
90
|
+
#>
|
|
91
|
+
[CmdletBinding()]
|
|
92
|
+
[OutputType([long[]], [System.Object[]])]
|
|
93
|
+
param([AllowEmptyCollection()][object[]] $Pair = @(), [AllowEmptyCollection()][long[]] $ItemKey = @())
|
|
94
|
+
|
|
95
|
+
$keys = [System.Collections.Generic.SortedSet[long]]::new()
|
|
96
|
+
foreach ($key in @($ItemKey)) { [void]$keys.Add($key) }
|
|
97
|
+
# The loop variable is not named $pair: variable names are case-insensitive, so it would overwrite $Pair.
|
|
98
|
+
foreach ($edge in @($Pair)) { [void]$keys.Add($edge[0]); [void]$keys.Add($edge[1]) }
|
|
99
|
+
$inDegree = @{}
|
|
100
|
+
$successor = @{}
|
|
101
|
+
foreach ($key in $keys) { $inDegree[$key] = 0; $successor[$key] = [System.Collections.Generic.SortedSet[long]]::new() }
|
|
102
|
+
foreach ($edge in @($Pair)) {
|
|
103
|
+
if ($successor[$edge[0]].Add($edge[1])) { $inDegree[$edge[1]]++ }
|
|
104
|
+
}
|
|
105
|
+
$ready = [System.Collections.Generic.SortedSet[long]]::new()
|
|
106
|
+
foreach ($key in $keys) { if ($inDegree[$key] -eq 0) { [void]$ready.Add($key) } }
|
|
107
|
+
$order = [System.Collections.Generic.List[long]]::new()
|
|
108
|
+
while ($ready.Count -gt 0) {
|
|
109
|
+
$next = $ready.Min
|
|
110
|
+
[void]$ready.Remove($next)
|
|
111
|
+
$order.Add($next)
|
|
112
|
+
foreach ($after in $successor[$next]) {
|
|
113
|
+
$inDegree[$after]--
|
|
114
|
+
if ($inDegree[$after] -eq 0) { [void]$ready.Add($after) }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if ($order.Count -lt $keys.Count) {
|
|
118
|
+
$cyclic = @($keys | Where-Object { $order -notcontains $_ })
|
|
119
|
+
throw "precedence_cycle: the precedence entries form a cycle among items $($cyclic -join ', ')."
|
|
120
|
+
}
|
|
121
|
+
return , $order.ToArray()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Test-ParallelPrecedenceInput {
|
|
125
|
+
<#
|
|
126
|
+
.SYNOPSIS
|
|
127
|
+
Validate precedence entries against the run's item keys.
|
|
128
|
+
|
|
129
|
+
.DESCRIPTION
|
|
130
|
+
Throws precedence_unknown_item when an entry names a key that is not an
|
|
131
|
+
item, and precedence_cycle when an entry pairs an item with itself or the
|
|
132
|
+
entries form a cycle. Returns true when the entries are valid.
|
|
133
|
+
#>
|
|
134
|
+
[CmdletBinding()]
|
|
135
|
+
[OutputType([bool])]
|
|
136
|
+
param([AllowNull()][AllowEmptyCollection()][object[]] $Entry = @(), [AllowEmptyCollection()][long[]] $ItemKey = @())
|
|
137
|
+
|
|
138
|
+
$pairs = Get-ParallelPrecedencePair -Entry $Entry
|
|
139
|
+
foreach ($pair in $pairs) {
|
|
140
|
+
foreach ($key in $pair) {
|
|
141
|
+
if (@($ItemKey) -notcontains $key) { throw "precedence_unknown_item: precedence entry $($pair[0]):$($pair[1]) names item $key, which is not an item of the run." }
|
|
142
|
+
}
|
|
143
|
+
if ($pair[0] -eq $pair[1]) { throw "precedence_cycle: precedence entry $($pair[0]):$($pair[1]) pairs an item with itself." }
|
|
144
|
+
}
|
|
145
|
+
[void](Get-ParallelPrecedenceOrder -Pair $pairs -ItemKey $ItemKey)
|
|
146
|
+
return $true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ConvertTo-ParallelPrecedenceEdge {
|
|
150
|
+
<#
|
|
151
|
+
.SYNOPSIS
|
|
152
|
+
Normalize precedence entries into separating `a:b` edges with a < b.
|
|
153
|
+
|
|
154
|
+
.DESCRIPTION
|
|
155
|
+
Each before/after pair becomes the undirected edge `min:max`, so the
|
|
156
|
+
cohort coloring always separates the pair. Duplicates are removed and the
|
|
157
|
+
edges are sorted by (a, b).
|
|
158
|
+
#>
|
|
159
|
+
[CmdletBinding()]
|
|
160
|
+
[OutputType([string[]], [System.Object[]])]
|
|
161
|
+
param([AllowNull()][AllowEmptyCollection()][object[]] $Entry = @())
|
|
162
|
+
|
|
163
|
+
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
164
|
+
$edges = [System.Collections.Generic.List[long[]]]::new()
|
|
165
|
+
foreach ($pair in (Get-ParallelPrecedencePair -Entry $Entry)) {
|
|
166
|
+
$a = [math]::Min($pair[0], $pair[1])
|
|
167
|
+
$b = [math]::Max($pair[0], $pair[1])
|
|
168
|
+
if ($a -eq $b -or -not $seen.Add("${a}:${b}")) { continue }
|
|
169
|
+
$edges.Add([long[]]@($a, $b))
|
|
170
|
+
}
|
|
171
|
+
$sorted = @($edges | Sort-Object -Property @{ Expression = { $_[0] } }, @{ Expression = { $_[1] } })
|
|
172
|
+
return , [string[]]@($sorted | ForEach-Object { "$($_[0]):$($_[1])" })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function Get-ParallelPrecedenceNeighbor {
|
|
176
|
+
<#
|
|
177
|
+
.SYNOPSIS
|
|
178
|
+
Build the undirected neighbor map of conflict edges plus precedence pairs.
|
|
179
|
+
|
|
180
|
+
.DESCRIPTION
|
|
181
|
+
Edge entries are `a:b` strings of integer keys; a malformed entry throws.
|
|
182
|
+
#>
|
|
183
|
+
[CmdletBinding()]
|
|
184
|
+
[OutputType([hashtable])]
|
|
185
|
+
param([AllowEmptyCollection()][string[]] $Edge = @(), [AllowEmptyCollection()][object[]] $Pair = @())
|
|
186
|
+
|
|
187
|
+
$neighbor = @{}
|
|
188
|
+
foreach ($text in @($Edge)) {
|
|
189
|
+
if ([string]::IsNullOrWhiteSpace($text)) { continue }
|
|
190
|
+
if ($text -notmatch '^\s*([0-9]+):([0-9]+)\s*$') { throw "Conflict edge '$text' is not of the form <a>:<b>." }
|
|
191
|
+
Add-ParallelPrecedenceNeighborLink -Neighbor $neighbor -Left ([long]$Matches[1]) -Right ([long]$Matches[2])
|
|
192
|
+
}
|
|
193
|
+
foreach ($edgePair in @($Pair)) { Add-ParallelPrecedenceNeighborLink -Neighbor $neighbor -Left $edgePair[0] -Right $edgePair[1] }
|
|
194
|
+
return $neighbor
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function Add-ParallelPrecedenceNeighborLink {
|
|
198
|
+
<#
|
|
199
|
+
.SYNOPSIS
|
|
200
|
+
Record one undirected link in a neighbor map (key to HashSet of keys), in both directions.
|
|
201
|
+
#>
|
|
202
|
+
[CmdletBinding()]
|
|
203
|
+
[OutputType([void])]
|
|
204
|
+
param([hashtable] $Neighbor, [long] $Left, [long] $Right)
|
|
205
|
+
|
|
206
|
+
foreach ($side in @(@($Left, $Right), @($Right, $Left))) {
|
|
207
|
+
if (-not $Neighbor.ContainsKey($side[0])) { $Neighbor[$side[0]] = [System.Collections.Generic.HashSet[long]]::new() }
|
|
208
|
+
[void]$Neighbor[$side[0]].Add($side[1])
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function Invoke-ParallelPrecedenceLift {
|
|
213
|
+
<#
|
|
214
|
+
.SYNOPSIS
|
|
215
|
+
Lift unstarted `after` items into later cohorts than their `before` items.
|
|
216
|
+
|
|
217
|
+
.DESCRIPTION
|
|
218
|
+
Visits the cohort items in topological precedence order (ties by ascending
|
|
219
|
+
key). An unpinned `after` item whose cohort index is not above every
|
|
220
|
+
`before` index moves to the smallest index at or above both
|
|
221
|
+
max(before index) + 1 and Floor that holds no conflict neighbor (conflict
|
|
222
|
+
edges plus precedence pairs, pinned neighbors included). Pinned items
|
|
223
|
+
never move and unstarted items only move upward, so every cohort stays an
|
|
224
|
+
independent set and a second application changes nothing. A pinned
|
|
225
|
+
`after` item that violates its precedence is left in place; the
|
|
226
|
+
cohort-table check reports it.
|
|
227
|
+
|
|
228
|
+
.PARAMETER Cohort
|
|
229
|
+
The cohort table: one array of item keys per cohort index.
|
|
230
|
+
|
|
231
|
+
.PARAMETER Edge
|
|
232
|
+
Conflict edges as `a:b` strings.
|
|
233
|
+
|
|
234
|
+
.PARAMETER Precedence
|
|
235
|
+
precedence[] entries carrying `before` and `after`.
|
|
236
|
+
|
|
237
|
+
.PARAMETER Pinned
|
|
238
|
+
Item keys that have started and therefore cannot move.
|
|
239
|
+
|
|
240
|
+
.PARAMETER Floor
|
|
241
|
+
The lowest cohort index a moved item may take.
|
|
242
|
+
|
|
243
|
+
.OUTPUTS
|
|
244
|
+
System.Object[] of Int64 arrays, one per cohort; trailing empty cohorts are dropped.
|
|
245
|
+
#>
|
|
246
|
+
[CmdletBinding()]
|
|
247
|
+
[OutputType([System.Object[]])]
|
|
248
|
+
param(
|
|
249
|
+
[AllowEmptyCollection()][object[]] $Cohort = @(),
|
|
250
|
+
[AllowEmptyCollection()][string[]] $Edge = @(),
|
|
251
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $Precedence = @(),
|
|
252
|
+
[AllowEmptyCollection()][long[]] $Pinned = @(),
|
|
253
|
+
[int] $Floor = 0
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
$table = [System.Collections.Generic.List[System.Collections.Generic.SortedSet[long]]]::new()
|
|
257
|
+
$position = @{}
|
|
258
|
+
foreach ($members in @($Cohort)) {
|
|
259
|
+
$set = [System.Collections.Generic.SortedSet[long]]::new()
|
|
260
|
+
foreach ($member in @($members)) {
|
|
261
|
+
$key = ConvertTo-ParallelPrecedenceKey -Value $member
|
|
262
|
+
if ($null -eq $key) { throw "Cohort member '$member' is not an integer item key." }
|
|
263
|
+
if ($position.ContainsKey($key)) { throw "Item $key appears in more than one cohort." }
|
|
264
|
+
[void]$set.Add($key)
|
|
265
|
+
$position[$key] = $table.Count
|
|
266
|
+
}
|
|
267
|
+
$table.Add($set)
|
|
268
|
+
}
|
|
269
|
+
$itemKey = [long[]]@($position.Keys)
|
|
270
|
+
[void](Test-ParallelPrecedenceInput -Entry $Precedence -ItemKey $itemKey)
|
|
271
|
+
$pairs = Get-ParallelPrecedencePair -Entry $Precedence
|
|
272
|
+
$neighbor = Get-ParallelPrecedenceNeighbor -Edge $Edge -Pair $pairs
|
|
273
|
+
|
|
274
|
+
foreach ($key in (Get-ParallelPrecedenceOrder -Pair $pairs -ItemKey $itemKey)) {
|
|
275
|
+
$before = @($pairs | Where-Object { $_[1] -eq $key } | ForEach-Object { $_[0] })
|
|
276
|
+
if ($before.Count -eq 0 -or @($Pinned) -contains $key) { continue }
|
|
277
|
+
$need = (@($before | ForEach-Object { $position[$_] }) | Measure-Object -Maximum).Maximum + 1
|
|
278
|
+
if ($position[$key] -ge $need) { continue }
|
|
279
|
+
$target = [math]::Max([int]$need, $Floor)
|
|
280
|
+
while ($true) {
|
|
281
|
+
while ($table.Count -le $target) { $table.Add([System.Collections.Generic.SortedSet[long]]::new()) }
|
|
282
|
+
$blocked = $neighbor.ContainsKey($key) -and @($table[$target] | Where-Object { $neighbor[$key].Contains($_) }).Count -gt 0
|
|
283
|
+
if (-not $blocked) { break }
|
|
284
|
+
$target++
|
|
285
|
+
}
|
|
286
|
+
[void]$table[$position[$key]].Remove($key)
|
|
287
|
+
[void]$table[$target].Add($key)
|
|
288
|
+
$position[$key] = $target
|
|
289
|
+
}
|
|
290
|
+
while ($table.Count -gt 0 -and $table[$table.Count - 1].Count -eq 0) { $table.RemoveAt($table.Count - 1) }
|
|
291
|
+
$result = [System.Collections.Generic.List[object]]::new()
|
|
292
|
+
foreach ($set in $table) { $result.Add([long[]]@($set)) }
|
|
293
|
+
return , $result.ToArray()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# A predecessor in one of these merge states, or in state withdrawn, no longer
|
|
297
|
+
# constrains its dependent (decision D6). A blocked predecessor still holds.
|
|
298
|
+
$script:ReleasedMergeStatus = @('merged', 'worktree_removed')
|
|
299
|
+
|
|
300
|
+
function Get-ParallelActivePrecedence {
|
|
301
|
+
<#
|
|
302
|
+
.SYNOPSIS
|
|
303
|
+
Resolve the precedence entries that still constrain scheduling.
|
|
304
|
+
|
|
305
|
+
.DESCRIPTION
|
|
306
|
+
Each entry's keys are mapped to the item that carries them: an item
|
|
307
|
+
carries its own issue_num and every key in its combined_issue_nums, so a
|
|
308
|
+
combined item inherits the union of its members' entries, and a pair
|
|
309
|
+
inside one combination is dropped. An entry is inactive when its `before`
|
|
310
|
+
item has merge_status merged or worktree_removed, or state withdrawn; a
|
|
311
|
+
blocked predecessor holds (D6). A key that resolves to no item throws
|
|
312
|
+
precedence_unknown_item. Duplicate entries collapse to one.
|
|
313
|
+
|
|
314
|
+
.OUTPUTS
|
|
315
|
+
System.Object[] of pscustomobject {before, after}, sorted by (before, after).
|
|
316
|
+
#>
|
|
317
|
+
[CmdletBinding()]
|
|
318
|
+
[OutputType([System.Object[]])]
|
|
319
|
+
param([AllowNull()][AllowEmptyCollection()][object[]] $Precedence = @(), [AllowNull()][AllowEmptyCollection()][object[]] $Item = @())
|
|
320
|
+
|
|
321
|
+
$owner = @{}
|
|
322
|
+
$record = @{}
|
|
323
|
+
foreach ($entry in @($Item)) {
|
|
324
|
+
$key = ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $entry -Key 'issue_num')
|
|
325
|
+
if ($null -eq $key) { continue }
|
|
326
|
+
$record[$key] = $entry
|
|
327
|
+
$owner[$key] = $key
|
|
328
|
+
foreach ($member in @(Get-ParallelPrecedenceMapValue -Map $entry -Key 'combined_issue_nums')) {
|
|
329
|
+
$memberKey = ConvertTo-ParallelPrecedenceKey -Value $member
|
|
330
|
+
if ($null -ne $memberKey) { $owner[$memberKey] = $key }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
$active = [System.Collections.Generic.SortedDictionary[string, object]]::new([System.StringComparer]::Ordinal)
|
|
334
|
+
foreach ($pair in (Get-ParallelPrecedencePair -Entry $Precedence)) {
|
|
335
|
+
foreach ($side in $pair) {
|
|
336
|
+
if (-not $owner.ContainsKey($side)) { throw "precedence_unknown_item: precedence entry $($pair[0]):$($pair[1]) names item $side, which is not an item of the run." }
|
|
337
|
+
}
|
|
338
|
+
$before = [long]$owner[$pair[0]]
|
|
339
|
+
$after = [long]$owner[$pair[1]]
|
|
340
|
+
if ($before -eq $after) { continue }
|
|
341
|
+
$predecessor = $record[$before]
|
|
342
|
+
$mergeStatus = Get-ParallelPrecedenceMapValue -Map $predecessor -Key 'merge_status'
|
|
343
|
+
$state = Get-ParallelPrecedenceMapValue -Map $predecessor -Key 'state'
|
|
344
|
+
if ($script:ReleasedMergeStatus -ccontains $mergeStatus -or $state -ceq 'withdrawn') { continue }
|
|
345
|
+
$active[('{0:D20}:{1:D20}' -f $before, $after)] = [pscustomobject]@{ before = $before; after = $after }
|
|
346
|
+
}
|
|
347
|
+
return , [object[]]@($active.Values)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function Test-ParallelPrecedenceCohortTable {
|
|
351
|
+
<#
|
|
352
|
+
.SYNOPSIS
|
|
353
|
+
Check a parallel checkpoint's current-generation cohort table against its active precedence entries.
|
|
354
|
+
|
|
355
|
+
.DESCRIPTION
|
|
356
|
+
For each active entry, the cohort index of the `after` item must be
|
|
357
|
+
strictly greater than the index of the `before` item within the cohorts
|
|
358
|
+
whose generation equals recolor_generation. An entry whose item is in no
|
|
359
|
+
current-generation cohort is not compared. An absent precedence key gives
|
|
360
|
+
a satisfied table.
|
|
361
|
+
|
|
362
|
+
.OUTPUTS
|
|
363
|
+
pscustomobject with Satisfied, Violation (strings), and Active (the active entries).
|
|
364
|
+
#>
|
|
365
|
+
[CmdletBinding()]
|
|
366
|
+
[OutputType([pscustomobject])]
|
|
367
|
+
param([AllowNull()][object] $Checkpoint)
|
|
368
|
+
|
|
369
|
+
if ($Checkpoint -isnot [System.Collections.IDictionary]) { throw 'The parallel checkpoint must be a JSON object.' }
|
|
370
|
+
$active = Get-ParallelActivePrecedence -Precedence @(Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'precedence') -Item @(Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'items')
|
|
371
|
+
$generation = Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'recolor_generation'
|
|
372
|
+
$index = @{}
|
|
373
|
+
foreach ($cohort in @(Get-ParallelPrecedenceMapValue -Map $Checkpoint -Key 'cohorts')) {
|
|
374
|
+
if ((Get-ParallelPrecedenceMapValue -Map $cohort -Key 'generation') -ne $generation) { continue }
|
|
375
|
+
$cohortIndex = Get-ParallelPrecedenceMapValue -Map $cohort -Key 'index'
|
|
376
|
+
foreach ($member in @(Get-ParallelPrecedenceMapValue -Map $cohort -Key 'item_keys')) {
|
|
377
|
+
$memberKey = ConvertTo-ParallelPrecedenceKey -Value $member
|
|
378
|
+
if ($null -ne $memberKey) { $index[$memberKey] = [long]$cohortIndex }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
$violation = [System.Collections.Generic.List[string]]::new()
|
|
382
|
+
foreach ($entry in $active) {
|
|
383
|
+
if (-not $index.ContainsKey($entry.before) -or -not $index.ContainsKey($entry.after)) { continue }
|
|
384
|
+
if ($index[$entry.after] -le $index[$entry.before]) {
|
|
385
|
+
$violation.Add("item $($entry.after) in cohort $($index[$entry.after]) must follow item $($entry.before) in cohort $($index[$entry.before])")
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return [pscustomobject]@{ Satisfied = ($violation.Count -eq 0); Violation = [string[]]$violation.ToArray(); Active = $active }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function ConvertTo-ParallelSchedulingProjection {
|
|
392
|
+
<#
|
|
393
|
+
.SYNOPSIS
|
|
394
|
+
Project each dictionary onto the named keys in ordinal key order; a missing key becomes null.
|
|
395
|
+
#>
|
|
396
|
+
[CmdletBinding()]
|
|
397
|
+
[OutputType([System.Object[]])]
|
|
398
|
+
param([AllowNull()][AllowEmptyCollection()][object[]] $Value = @(), [string[]] $Key)
|
|
399
|
+
|
|
400
|
+
$sortedKey = [string[]]@($Key)
|
|
401
|
+
[Array]::Sort($sortedKey, [System.StringComparer]::Ordinal)
|
|
402
|
+
$projection = [System.Collections.Generic.List[object]]::new()
|
|
403
|
+
foreach ($entry in @($Value)) {
|
|
404
|
+
if ($entry -isnot [System.Collections.IDictionary]) { continue }
|
|
405
|
+
$row = [ordered]@{}
|
|
406
|
+
foreach ($name in $sortedKey) { $row[$name] = Get-ParallelPrecedenceMapValue -Map $entry -Key $name }
|
|
407
|
+
$projection.Add($row)
|
|
408
|
+
}
|
|
409
|
+
return , $projection.ToArray()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
$script:RecordItemKey = @('issue_num', 'branch_name', 'plan_path', 'plan_hash', 'base_commit', 'preparation_status', 'complexity_band')
|
|
413
|
+
|
|
414
|
+
function New-ParallelSchedulingRecord {
|
|
415
|
+
<#
|
|
416
|
+
.SYNOPSIS
|
|
417
|
+
Build the durable scheduling record docs/features/parallel/<slug>/parallel-scheduling.json.
|
|
418
|
+
|
|
419
|
+
.DESCRIPTION
|
|
420
|
+
The record carries exactly schema_version, parallel_slug, edges,
|
|
421
|
+
tolerated_overlaps, precedence, and items. Edges keep a, b, and reason;
|
|
422
|
+
tolerated overlaps keep a, b, reasons, cost, and benefit; precedence
|
|
423
|
+
entries keep before, after, source, and recorded_at; items keep
|
|
424
|
+
issue_num, branch_name, plan_path, plan_hash, base_commit,
|
|
425
|
+
preparation_status, and complexity_band. Keys are in ordinal order at
|
|
426
|
+
every level and lists are sorted by their keys, so the same input always
|
|
427
|
+
yields the same record. The function builds content only; the caller
|
|
428
|
+
writes and commits it.
|
|
429
|
+
|
|
430
|
+
.OUTPUTS
|
|
431
|
+
System.Collections.Specialized.OrderedDictionary.
|
|
432
|
+
#>
|
|
433
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure record constructor; no state is changed.')]
|
|
434
|
+
[CmdletBinding()]
|
|
435
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
436
|
+
param(
|
|
437
|
+
[string] $Slug,
|
|
438
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $Edge = @(),
|
|
439
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $ToleratedOverlap = @(),
|
|
440
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $Precedence = @(),
|
|
441
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $Item = @()
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
if ([string]::IsNullOrWhiteSpace($Slug)) { throw 'New-ParallelSchedulingRecord requires -Slug.' }
|
|
445
|
+
$byPair = @(@{ Expression = { [long]$_['a'] } }, @{ Expression = { [long]$_['b'] } })
|
|
446
|
+
# Each projection is parenthesized so the pipeline enumerates its rows rather than passing the array as one object.
|
|
447
|
+
$edges = @((ConvertTo-ParallelSchedulingProjection -Value $Edge -Key @('a', 'b', 'reason')) | Sort-Object -Property $byPair)
|
|
448
|
+
$tolerated = @((ConvertTo-ParallelSchedulingProjection -Value $ToleratedOverlap -Key @('a', 'b', 'reasons', 'cost', 'benefit')) | Sort-Object -Property $byPair)
|
|
449
|
+
$precedence = @((ConvertTo-ParallelSchedulingProjection -Value $Precedence -Key @('before', 'after', 'source', 'recorded_at')) |
|
|
450
|
+
Sort-Object -Property @{ Expression = { [long]$_['before'] } }, @{ Expression = { [long]$_['after'] } })
|
|
451
|
+
$items = @((ConvertTo-ParallelSchedulingProjection -Value $Item -Key $script:RecordItemKey) | Sort-Object -Property @{ Expression = { [long]$_['issue_num'] } })
|
|
452
|
+
$record = [ordered]@{}
|
|
453
|
+
$record['edges'] = [object[]]$edges
|
|
454
|
+
$record['items'] = [object[]]$items
|
|
455
|
+
$record['parallel_slug'] = $Slug
|
|
456
|
+
$record['precedence'] = [object[]]$precedence
|
|
457
|
+
$record['schema_version'] = [long]1
|
|
458
|
+
$record['tolerated_overlaps'] = [object[]]$tolerated
|
|
459
|
+
return $record
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function Get-ParallelSchedulingRecordReuse {
|
|
463
|
+
<#
|
|
464
|
+
.SYNOPSIS
|
|
465
|
+
Decide whether a re-seed may reuse an item's recorded conflict edges.
|
|
466
|
+
|
|
467
|
+
.DESCRIPTION
|
|
468
|
+
Reuse applies only when the record lists the item and its recorded
|
|
469
|
+
plan_hash equals the current plan blob hash; the item's recorded edges
|
|
470
|
+
(those with the item at either end) are then returned. Otherwise the
|
|
471
|
+
result is a recompute marker with the reason unknown_item or
|
|
472
|
+
plan_hash_changed.
|
|
473
|
+
|
|
474
|
+
.OUTPUTS
|
|
475
|
+
pscustomobject with action (reuse or recompute), reason, and edges.
|
|
476
|
+
#>
|
|
477
|
+
[CmdletBinding()]
|
|
478
|
+
[OutputType([pscustomobject])]
|
|
479
|
+
param([AllowNull()][object] $Record, [long] $IssueNum, [AllowNull()][AllowEmptyString()][string] $CurrentPlanHash)
|
|
480
|
+
|
|
481
|
+
$recorded = @(@(Get-ParallelPrecedenceMapValue -Map $Record -Key 'items') | Where-Object {
|
|
482
|
+
(ConvertTo-ParallelPrecedenceKey -Value (Get-ParallelPrecedenceMapValue -Map $_ -Key 'issue_num')) -eq $IssueNum
|
|
483
|
+
})
|
|
484
|
+
if ($recorded.Count -eq 0) {
|
|
485
|
+
return [pscustomobject]@{ action = 'recompute'; reason = 'unknown_item'; edges = [object[]]@() }
|
|
486
|
+
}
|
|
487
|
+
$hash = Get-ParallelPrecedenceMapValue -Map $recorded[0] -Key 'plan_hash'
|
|
488
|
+
if ([string]::IsNullOrEmpty($CurrentPlanHash) -or [string]$hash -cne $CurrentPlanHash) {
|
|
489
|
+
return [pscustomobject]@{ action = 'recompute'; reason = 'plan_hash_changed'; edges = [object[]]@() }
|
|
490
|
+
}
|
|
491
|
+
$edges = @(@(Get-ParallelPrecedenceMapValue -Map $Record -Key 'edges') | Where-Object {
|
|
492
|
+
$_ -is [System.Collections.IDictionary] -and (
|
|
493
|
+
(ConvertTo-ParallelPrecedenceKey -Value $_['a']) -eq $IssueNum -or (ConvertTo-ParallelPrecedenceKey -Value $_['b']) -eq $IssueNum)
|
|
494
|
+
})
|
|
495
|
+
return [pscustomobject]@{ action = 'reuse'; reason = 'plan_hash_matches'; edges = [object[]]$edges }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
Export-ModuleMember -Function `
|
|
499
|
+
ConvertTo-ParallelPrecedenceKey, `
|
|
500
|
+
Get-ParallelPrecedencePair, `
|
|
501
|
+
Get-ParallelPrecedenceOrder, `
|
|
502
|
+
Test-ParallelPrecedenceInput, `
|
|
503
|
+
ConvertTo-ParallelPrecedenceEdge, `
|
|
504
|
+
Invoke-ParallelPrecedenceLift, `
|
|
505
|
+
Get-ParallelActivePrecedence, `
|
|
506
|
+
Test-ParallelPrecedenceCohortTable, `
|
|
507
|
+
New-ParallelSchedulingRecord, `
|
|
508
|
+
Get-ParallelSchedulingRecordReuse
|
|
@@ -60,7 +60,7 @@ Enforced by `validate_parallel_orchestrator_state_text(text, *, require_complete
|
|
|
60
60
|
|
|
61
61
|
9. **Blast-radius shape.** Each item's `blast_radius` must be an object carrying `paths`, `modules`, `shared_surfaces`, and `contracts` as lists of non-empty strings, a `source` in `{derived, declared, observed}`, and a non-empty `computed_at` string. `modules`, `shared_surfaces`, and `contracts` may be empty lists.
|
|
62
62
|
|
|
63
|
-
10. **Prohibited dependency edges.** No object anywhere in the checkpoint may carry a `depends_on` key, and no top-level `depends_on` key may be present. Ordering is expressed only as blast-radius overlap. Presence is an explicit rejection, not mere absence.
|
|
63
|
+
10. **Prohibited dependency edges.** No object anywhere in the checkpoint may carry a `depends_on` key, and no top-level `depends_on` key may be present. Ordering is expressed only as blast-radius overlap and as the operator precedence input declared in `## Prepared-Item Admission (issue #1047)`, which is a separating input and not a dependency key. Presence is an explicit rejection, not mere absence.
|
|
64
64
|
|
|
65
65
|
11. **Prohibited integration-branch fields.** The checkpoint must not carry `integration_branch` or `epic_merge_pr` at any level. Each parallel item opens its own pull request against `main`, so there is no integration branch and no final integration pull request.
|
|
66
66
|
|
|
@@ -256,6 +256,22 @@ F3 deliberately excludes the kickoff-prompt contract module `scripts/dev_tools/p
|
|
|
256
256
|
|
|
257
257
|
F3's `require_ready_for_execution` gate is STRUCTURAL ONLY. It enforces the kickoff-PATH invariant (P9: `kickoff_prompt_path` must equal `artifacts/orchestration/parallel-kickoff-<parallel_slug>.md`) and does not parse or cross-check kickoff CONTENT. The deeper readiness-integrity machinery of the epic surface — git-integrity checks, launch-evidence binding, and kickoff-contract cross-checks — is left to F4, which may layer repository-aware checks behind an additional keyword without changing the schema. F3 likewise does not recompute the cohort coloring (planner invariant P5).
|
|
258
258
|
|
|
259
|
+
## Prepared-Item Admission (issue #1047)
|
|
260
|
+
|
|
261
|
+
`/parallel-add` and `/parallel-plan` admit an item at the stage its pushed branch already reached. The decisions are made in code by the admission helper `.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1` (subcommands `guard-run`, `assess`, `order-cohorts`, `test-cohorts`, `record`, and `reuse-edges`). Every field below is tolerated and not validated: the Python validators and the TypeScript validator port accept it with zero errors, and no enum of `## Enum Ownership (F6/F7/F8 consume, never extend)` changes.
|
|
262
|
+
|
|
263
|
+
**Tolerated item fields.** A parallel-orchestrator checkpoint item may carry `preparation_status` (`none|research|spec|plan|cleared`, as reported by `assess`), `plan_hash` (the git blob hash of the plan at the item branch head), `base_commit` (the commit the staleness check compares `origin/main` against), and `band_source` (`caller|planner_checkpoint|assessment`). A parallel-planner checkpoint item may carry `plan_hash` and `base_commit`. The planner keeps its ready value `preparation_status: prepared` (invariant P7), which corresponds to the admission stage `cleared`; no second ready value is introduced.
|
|
264
|
+
|
|
265
|
+
**Precedence input.** Both checkpoints may carry a top-level `precedence[]` list of `{before, after, source, recorded_at}` entries, recorded from the operator's `--after` flags with `source` equal to `operator`. An entry states that the `after` item starts in a later cohort than the `before` item and only after the `before` item has merged. It is a separating input: each pair is passed to the cohort computation as an extra edge, and `order-cohorts` lifts an unstarted `after` item above its `before` item. It is not a `depends_on` key, and invariant 10, P3, and M7 are unchanged. Keys must resolve to items, an item is never paired with itself, and the entries must be acyclic. An entry is inactive once its `before` item has `merge_status` `merged` or `worktree_removed` or `state` `withdrawn`; a `blocked` predecessor still holds its dependent. A combined item inherits the union of its members' entries, and a pair inside one combination is dropped.
|
|
266
|
+
|
|
267
|
+
**Preparation receipt.** The preparation child writes `<feature folder>/evidence/other/preparation-receipt.<yyyy-MM-ddTHH-mm>.json` before its commit, carrying `schema_version`, `stage`, `research_path`, `spec_path`, `plan_path`, `plan_hash`, `base_commit`, `preflight_status`, and `recorded_at`. The lexicographically greatest receipt name wins, a newer receipt supersedes an older one, and no receipt is deleted. A receipt whose `plan_hash` differs from the current blob hash is downgraded to `plan`.
|
|
268
|
+
|
|
269
|
+
**Durable scheduling record.** `docs/features/parallel/<slug>/parallel-scheduling.json`, committed on `parallel/<slug>-plan`, carries `schema_version`, `parallel_slug`, `edges[]`, `tolerated_overlaps[]`, `precedence[]`, and `items[]` with `issue_num`, `branch_name`, `plan_path`, `plan_hash`, `base_commit`, `preparation_status`, and `complexity_band`. The planner writes it at seeding and `/parallel-add` rewrites it after each admission; the helper's `record` subcommand prints the content and performs no git write. A re-seed applies the reuse rule through the helper's `reuse-edges` subcommand, which reads the record by ref and reuses an item's recorded edges only when the recorded `plan_hash` equals the current blob hash. The run manifest stays read-only to the orchestrator.
|
|
270
|
+
|
|
271
|
+
**Admission output signal.** A helper rule rejection exits 1 with one stderr line `PARALLEL_ADMISSION_REJECTED: <code>: <detail>`, where the code is one of `run_closed`, `precedence_cycle`, `precedence_unknown_item`, and `precedence_violated`. This signal is separate from the mutation rejection signal of `## Mutation Rejection Signal (issue #1012)`: a `run_closed` rejection is emitted after a checkpoint exists and emits no mutation rejection signal, and that section's closed code set is unchanged.
|
|
272
|
+
|
|
273
|
+
**Known gap (Layer 2).** The Python orchestrator-state validator does not check precedence. Precedence is enforced by the helper's `test-cohorts` rejection and by Layer 1 of the cohort barrier hook (`Test-ParallelCohortBarrierClear` in `.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1`); the retrospective Layer 2 invariant reads `conflict_edges[]` only.
|
|
274
|
+
|
|
259
275
|
## Blast-Radius Contention Doctrine (issue #489)
|
|
260
276
|
|
|
261
277
|
The conflict graph that seeds cohorts is only as good as the evidence that produces its edges. Two
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"Bash(bash .claude/lib/bash/mutate-parallel-run.sh*)",
|
|
12
12
|
"Bash(bash .claude/lib/bash/remove-parallel-item.sh*)",
|
|
13
13
|
"Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)",
|
|
14
|
+
"Bash(pwsh -NoProfile -NonInteractive -File .claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1*)",
|
|
14
15
|
"Read",
|
|
15
16
|
"Edit(docs/**)",
|
|
16
17
|
"Edit(artifacts/**)",
|
|
@@ -248,7 +248,7 @@ Rules:
|
|
|
248
248
|
- **Observe a command's success-case output before asserting over that output (mandatory).** Run the command, or read a recorded run of it, and confirm the value you intend to assert is actually printed on a *successful* run. Do not infer it from the tool's documentation, from the plan, or from what the tool prints on failure. This requirement exists because the class of defect it prevents is not detectable by reading: an assertion on a line the tool prints only when it changed something is unsatisfiable on a clean run; a demand for a zero-diagnostic count from a tool that returns only an ok flag names a value with no source; a demand for separate line and branch percentages from a coverage run that prints one combined column reads two numbers that are never printed. G7 and G9 cover two decidable slices of this class. The remainder is covered by this requirement and by nothing else, so a reviewer cannot rely on the gate to catch it.
|
|
249
249
|
- **Fix the evidence in the plan; never leave the executor to select it.** An executor free to choose the evidence it is judged against cannot fail. Do not write an acceptance condition that asks the executor to identify "the known-genuine pair", to choose a survivor list, or to pick any suitable instance. Name the instances, or state the mechanical derivation that produces them, so a third party re-running it obtains the same set. This is authoring guidance and not a rule: a validator rule that scanned acceptance text for selection vocabulary was proposed and rejected, because that vocabulary is ordinary plan prose used in roles carrying no selection semantics and a keyword scan over prose is not statically decidable for the property it would claim to detect. The judgment is the author's, and it is not automated. `.claude/rules/plan-acceptance-gates.md` records the rejection and the reason for it.
|
|
250
250
|
- **Check that the task-ordering does not make the condition unsatisfiable.** No rule covers this. A gate that runs a test path containing deliberately-failing cases added by an earlier task, before the later task that makes them pass, cannot exit 0. A baseline captured after a write-mode formatter has already repaired pre-existing drift becomes either a blanket waiver or makes a later gate unsatisfiable. Read each acceptance condition against the state the plan will actually be in when its task runs.
|
|
251
|
-
- **Name the runner and job for a Pester acceptance criterion.** In `.github/workflows/_poshqc.yml`, the Pester suites run with coverage on `windows-latest` in the shard jobs `poshqc / PowerShell QC shard 1`, `shard 2`, and `shard 3`; `poshqc / PowerShell QC` is the aggregating
|
|
251
|
+
- **Name the runner and job for a Pester acceptance criterion.** In `.github/workflows/_poshqc.yml`, the Pester suites run with coverage on `windows-latest` in the shard jobs `poshqc / PowerShell QC shard 1`, `shard 2`, and `shard 3`; `poshqc / PowerShell QC` is the aggregating check, whose result `CI Required Gate` requires, and it requires the static and shard jobs to succeed and merges shard coverage; and `poshqc / PowerShell hook suites (Linux)` runs only `tests/scripts/claude-hooks` and `tests/scripts/codex-hooks` on `ubuntu-latest`, with coverage disabled. The execution-environment brief's `## CI Shape and Required Checks` section is the authoritative description of this shape. A Pester acceptance criterion may cite `ubuntu-latest` only for suites under `tests/scripts/claude-hooks` or `tests/scripts/codex-hooks`, never for coverage, and must name the job it relies on (`poshqc / PowerShell QC` or `poshqc / PowerShell hook suites (Linux)`).
|
|
252
252
|
- **Do not assert a fixed-string search literal that contains a backslash.** Git for Windows grep 3.0 reads a doubled backslash in a `grep -F` pattern as one backslash, so the same check returns a different count locally than on a CI runner and cannot pass in both places. Assert a backslash-free substring of the literal, a named test, or a hash or byte-identity check instead.
|
|
253
253
|
|
|
254
254
|
## Plan-Path Continuity Contract (Mandatory)
|