@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.17

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 (58) hide show
  1. package/out/mcp-server.js +639 -27
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +12 -6
  4. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  5. package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
  6. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  11. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  17. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
  18. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  19. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  20. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  23. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  24. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  25. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  26. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  29. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  30. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
  31. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  32. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
  33. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  34. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
  35. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
  36. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  37. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  38. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  39. package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
  40. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
  41. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  42. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  43. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  44. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  45. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  46. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  47. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  48. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  49. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  50. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  51. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  52. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
  53. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  54. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  55. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  56. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  57. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  58. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -1
@@ -0,0 +1,388 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Halt selection and input shape guards for parallel radius-drift detection.
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell port (issue #763) of the halt-selection half of
7
+ parallel drift detection. It ports select_halted_item and _start_rank from
8
+ scripts/dev_tools/parallel_drift_halt.py, the halted_item_keys selection from
9
+ scripts/dev_tools/parallel_drift_detection_cli.py, and the guards
10
+ require_item_key, require_text, require_paths, require_enum_member,
11
+ as_item_key, and canonical_pair from scripts/dev_tools/_parallel_drift_shape.py.
12
+ The Python modules remain the repository authority and the parity reference.
13
+
14
+ Every function is pure: no filesystem, subprocess, network, or clock access,
15
+ and no input is mutated. A guard failure throws with a message naming the
16
+ offending field. Booleans are never accepted where an integer is required,
17
+ matching the Python reference, which rejects bool although it subclasses int.
18
+ A function that returns an array uses the unary comma so a one-element or
19
+ empty array is not unrolled; it declares both the array type and
20
+ System.Object[] as output types.
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
+ # Integer runtime types a deserialized checkpoint value may carry. Boolean is
28
+ # deliberately absent, so a boolean in an integer slot is malformed.
29
+ $script:IntegerTypeName = @('System.Int16', 'System.Int32', 'System.Int64', 'System.Byte',
30
+ 'System.SByte', 'System.UInt16', 'System.UInt32', 'System.UInt64')
31
+
32
+ function Test-ParallelDriftIntegerValue {
33
+ <#
34
+ .SYNOPSIS
35
+ Report whether a value is a non-boolean integer.
36
+ .PARAMETER Value
37
+ Any deserialized value.
38
+ .OUTPUTS
39
+ System.Boolean.
40
+ #>
41
+ [CmdletBinding()]
42
+ [OutputType([bool])]
43
+ param([AllowNull()][object] $Value)
44
+
45
+ return ($null -ne $Value) -and ($script:IntegerTypeName -contains $Value.GetType().FullName)
46
+ }
47
+
48
+ function Format-ParallelDriftValue {
49
+ <#
50
+ .SYNOPSIS
51
+ Render a value for an error message; null renders as None, as in Python.
52
+ .PARAMETER Value
53
+ Any value.
54
+ .OUTPUTS
55
+ System.String.
56
+ #>
57
+ [CmdletBinding()]
58
+ [OutputType([string])]
59
+ param([AllowNull()][object] $Value)
60
+
61
+ if ($null -eq $Value) { return 'None' }
62
+ return [string]$Value
63
+ }
64
+
65
+ function ConvertTo-ParallelDriftSortedDistinct {
66
+ <#
67
+ .SYNOPSIS
68
+ Deduplicate and ordinally sort a string collection.
69
+ .DESCRIPTION
70
+ Port of the tuple(sorted(set(...))) idiom. Ordinal comparison matches
71
+ Python's code-point ordering; culture-sensitive sorting would not.
72
+ .PARAMETER Entry
73
+ The validated entries.
74
+ .OUTPUTS
75
+ System.String[]. Always an array, including when empty.
76
+ #>
77
+ [CmdletBinding()]
78
+ [OutputType([string[]], [System.Object[]])]
79
+ param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]] $Entry)
80
+
81
+ $unique = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
82
+ # Collapse duplicates before sorting so each path appears once.
83
+ foreach ($item in $Entry) { [void]$unique.Add($item) }
84
+ $sorted = [System.Collections.Generic.List[string]]::new($unique)
85
+ $sorted.Sort([System.StringComparer]::Ordinal)
86
+ return , [string[]]$sorted.ToArray()
87
+ }
88
+
89
+ function Assert-ParallelDriftItemKey {
90
+ <#
91
+ .SYNOPSIS
92
+ Require an issue_num primary key: a positive, non-boolean integer.
93
+ .DESCRIPTION
94
+ Port of require_item_key.
95
+ .PARAMETER Value
96
+ The candidate key.
97
+ .PARAMETER FieldName
98
+ Field label rendered into the error message.
99
+ .OUTPUTS
100
+ System.Int64. The validated key.
101
+ #>
102
+ [CmdletBinding()]
103
+ [OutputType([long])]
104
+ param(
105
+ [Parameter(Mandatory = $true)][AllowNull()][object] $Value,
106
+ [Parameter(Mandatory = $true)][string] $FieldName
107
+ )
108
+
109
+ if (-not (Test-ParallelDriftIntegerValue -Value $Value) -or [long]$Value -le 0) {
110
+ throw "$FieldName must be a positive integer issue_num; found: $(Format-ParallelDriftValue -Value $Value)."
111
+ }
112
+ return [long]$Value
113
+ }
114
+
115
+ function Assert-ParallelDriftText {
116
+ <#
117
+ .SYNOPSIS
118
+ Require a string that carries at least one non-space character.
119
+ .DESCRIPTION
120
+ Port of require_text.
121
+ .PARAMETER Value
122
+ The candidate string.
123
+ .PARAMETER FieldName
124
+ Field label rendered into the error message.
125
+ .OUTPUTS
126
+ System.String. The validated value, unchanged.
127
+ #>
128
+ [CmdletBinding()]
129
+ [OutputType([string])]
130
+ param(
131
+ [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][object] $Value,
132
+ [Parameter(Mandatory = $true)][string] $FieldName
133
+ )
134
+
135
+ if ($Value -isnot [string] -or [string]::IsNullOrWhiteSpace($Value)) {
136
+ throw "$FieldName must be a non-empty string; found: $(Format-ParallelDriftValue -Value $Value)."
137
+ }
138
+ return $Value
139
+ }
140
+
141
+ function Assert-ParallelDriftPathList {
142
+ <#
143
+ .SYNOPSIS
144
+ Require a collection of non-blank repository paths.
145
+ .DESCRIPTION
146
+ Port of require_paths. A bare string is rejected rather than iterated,
147
+ because iterating one would treat each character as a path. A blank or
148
+ non-string entry is rejected rather than dropped, because dropping it
149
+ would narrow the compared set and under-report drift.
150
+ .PARAMETER Value
151
+ The candidate collection.
152
+ .PARAMETER FieldName
153
+ Field label rendered into the error messages.
154
+ .PARAMETER AllowEmpty
155
+ Accept an empty collection. It is not accepted for escaped paths, where
156
+ emptiness means there was no drift.
157
+ .OUTPUTS
158
+ System.String[]. The entries deduplicated and ordinally sorted.
159
+ #>
160
+ [CmdletBinding()]
161
+ [OutputType([string[]], [System.Object[]])]
162
+ param(
163
+ [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyCollection()][object] $Value,
164
+ [Parameter(Mandatory = $true)][string] $FieldName,
165
+ [switch] $AllowEmpty
166
+ )
167
+
168
+ if ($Value -is [string]) {
169
+ throw "$FieldName must be a collection of paths, not a single string; found: '$Value'."
170
+ }
171
+ if ($Value -isnot [System.Collections.IEnumerable] -or $Value -is [System.Collections.IDictionary]) {
172
+ throw "$FieldName must be a collection of paths; found: $(Format-ParallelDriftValue -Value $Value)."
173
+ }
174
+
175
+ $entry = [System.Collections.Generic.List[string]]::new()
176
+ # Validate every entry before normalizing so an error names the offending value.
177
+ foreach ($item in $Value) {
178
+ if ($item -isnot [string] -or [string]::IsNullOrWhiteSpace($item)) {
179
+ throw "$FieldName entries must be non-empty strings; found: $(Format-ParallelDriftValue -Value $item)."
180
+ }
181
+ $entry.Add($item)
182
+ }
183
+ if ($entry.Count -eq 0 -and -not $AllowEmpty) {
184
+ throw "$FieldName must not be empty."
185
+ }
186
+ return , (ConvertTo-ParallelDriftSortedDistinct -Entry $entry.ToArray())
187
+ }
188
+
189
+ function Assert-ParallelDriftEnumMember {
190
+ <#
191
+ .SYNOPSIS
192
+ Require a member of a fixed vocabulary.
193
+ .DESCRIPTION
194
+ Port of require_enum_member. Matching is case-sensitive.
195
+ .PARAMETER Value
196
+ The candidate member.
197
+ .PARAMETER Vocabulary
198
+ The permitted members.
199
+ .PARAMETER FieldName
200
+ Field label rendered into the error message.
201
+ .OUTPUTS
202
+ System.String. The validated member, unchanged.
203
+ #>
204
+ [CmdletBinding()]
205
+ [OutputType([string])]
206
+ param(
207
+ [Parameter(Mandatory = $true)][AllowNull()][object] $Value,
208
+ [Parameter(Mandatory = $true)][string[]] $Vocabulary,
209
+ [Parameter(Mandatory = $true)][string] $FieldName
210
+ )
211
+
212
+ if ($Value -isnot [string] -or $Vocabulary -cnotcontains $Value) {
213
+ throw "$FieldName must be one of $($Vocabulary -join ', '); found: $(Format-ParallelDriftValue -Value $Value)."
214
+ }
215
+ return $Value
216
+ }
217
+
218
+ function ConvertTo-ParallelDriftItemKey {
219
+ <#
220
+ .SYNOPSIS
221
+ Read a value as an item key without throwing.
222
+ .DESCRIPTION
223
+ Port of as_item_key.
224
+ .PARAMETER Value
225
+ Any deserialized checkpoint value.
226
+ .OUTPUTS
227
+ System.Int64, or $null when the value is not a positive, non-boolean integer.
228
+ #>
229
+ [CmdletBinding()]
230
+ [OutputType([long])]
231
+ param([AllowNull()][object] $Value)
232
+
233
+ if ((Test-ParallelDriftIntegerValue -Value $Value) -and [long]$Value -gt 0) {
234
+ return [long]$Value
235
+ }
236
+ return $null
237
+ }
238
+
239
+ function Get-ParallelDriftCanonicalPair {
240
+ <#
241
+ .SYNOPSIS
242
+ Normalize two item keys into the canonical a < b edge identity.
243
+ .DESCRIPTION
244
+ Port of canonical_pair, so an edge recorded in either order has one identity.
245
+ .PARAMETER First
246
+ One item key.
247
+ .PARAMETER Second
248
+ The other item key.
249
+ .OUTPUTS
250
+ System.Int64[]. The two keys in ascending order.
251
+ #>
252
+ [CmdletBinding()]
253
+ [OutputType([long[]], [System.Object[]])]
254
+ param([Parameter(Mandatory = $true)][long] $First, [Parameter(Mandatory = $true)][long] $Second)
255
+
256
+ if ($First -lt $Second) { return , [long[]]@($First, $Second) }
257
+ return , [long[]]@($Second, $First)
258
+ }
259
+
260
+ function Get-ParallelDriftStartRank {
261
+ <#
262
+ .SYNOPSIS
263
+ Render a start marker as the comparable rank halt selection maximizes.
264
+ .DESCRIPTION
265
+ Port of _start_rank plus the ItemStart construction checks. The rank is
266
+ (Unknown, Timestamp, ItemKey); Unknown is 1 when no usable timestamp
267
+ exists, which ranks an item of unknown start above every timestamped
268
+ item and so makes the timestamped item earlier-started.
269
+ .PARAMETER Start
270
+ A hashtable carrying ItemKey and WorktreeCreatedAt (null, blank, or a
271
+ timestamp string).
272
+ .OUTPUTS
273
+ System.Management.Automation.PSCustomObject with Unknown, Timestamp, ItemKey.
274
+ #>
275
+ [CmdletBinding()]
276
+ [OutputType([pscustomobject])]
277
+ param([Parameter(Mandatory = $true)][System.Collections.IDictionary] $Start)
278
+
279
+ $itemKey = Assert-ParallelDriftItemKey -Value $Start['ItemKey'] -FieldName 'ItemStart.item_key'
280
+ $timestamp = $Start['WorktreeCreatedAt']
281
+ if ($null -ne $timestamp -and $timestamp -isnot [string]) {
282
+ throw "ItemStart.worktree_created_at must be a string or None; found: $timestamp."
283
+ }
284
+ if ([string]::IsNullOrWhiteSpace($timestamp)) {
285
+ return [pscustomobject]@{ Unknown = 1; Timestamp = ''; ItemKey = $itemKey }
286
+ }
287
+ return [pscustomobject]@{ Unknown = 0; Timestamp = $timestamp; ItemKey = $itemKey }
288
+ }
289
+
290
+ function Select-ParallelDriftHaltedItem {
291
+ <#
292
+ .SYNOPSIS
293
+ Return the issue_num of the later-started item of a pair.
294
+ .DESCRIPTION
295
+ Port of select_halted_item: argmax over (start unknown, ordinal start
296
+ timestamp, item key). Equal timestamps halt the larger key; a start
297
+ present on exactly one item makes that item earlier-started; both
298
+ unknown falls through to the item-key tie-break.
299
+ .PARAMETER First
300
+ One start marker.
301
+ .PARAMETER Second
302
+ The other start marker.
303
+ .OUTPUTS
304
+ System.Int64. The halted item key.
305
+ #>
306
+ [CmdletBinding()]
307
+ [OutputType([long])]
308
+ param(
309
+ [Parameter(Mandatory = $true)][System.Collections.IDictionary] $First,
310
+ [Parameter(Mandatory = $true)][System.Collections.IDictionary] $Second
311
+ )
312
+
313
+ $left = Get-ParallelDriftStartRank -Start $First
314
+ $right = Get-ParallelDriftStartRank -Start $Second
315
+ if ($left.ItemKey -eq $right.ItemKey) {
316
+ throw "select_halted_item requires two distinct items; the pair names one item twice (item_key $($left.ItemKey))."
317
+ }
318
+
319
+ # Compare the ranks field by field in priority order; the larger rank is halted.
320
+ $order = $left.Unknown.CompareTo($right.Unknown)
321
+ if ($order -eq 0) { $order = [string]::CompareOrdinal($left.Timestamp, $right.Timestamp) }
322
+ if ($order -eq 0) { $order = $left.ItemKey.CompareTo($right.ItemKey) }
323
+ if ($order -gt 0) { return $left.ItemKey }
324
+ return $right.ItemKey
325
+ }
326
+
327
+ function Get-ParallelDriftHaltedItemKey {
328
+ <#
329
+ .SYNOPSIS
330
+ Select the halted item of every newly conflicting pair, never the drifter.
331
+ .DESCRIPTION
332
+ Port of halted_item_keys. The drifting key is dropped from each pair's
333
+ candidates before the comparator runs, because halting the drifting item
334
+ would deadlock the remediation that resolves the drift. A canonical pair
335
+ holds two distinct keys, so one or two candidates remain: one is halted
336
+ directly, two go through Select-ParallelDriftHaltedItem.
337
+ .PARAMETER Item
338
+ The checkpoint's item records, read for issue_num and worktree_created_at.
339
+ Every record's start marker is validated, as the Python reference does.
340
+ .PARAMETER Pair
341
+ The canonical newly conflicting pairs, each a two-element key array.
342
+ .PARAMETER DriftingItemKey
343
+ The issue_num of the item whose diff escaped; never returned.
344
+ .OUTPUTS
345
+ System.Int64[]. The halted keys, deduplicated and ascending; always an array.
346
+ #>
347
+ [CmdletBinding()]
348
+ [OutputType([long[]], [System.Object[]])]
349
+ param(
350
+ [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Item,
351
+ [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]] $Pair,
352
+ [Parameter(Mandatory = $true)][long] $DriftingItemKey
353
+ )
354
+
355
+ if ($Pair.Count -eq 0) { return , [long[]]@() }
356
+
357
+ $start = @{}
358
+ # Build and validate one start marker per item so a pair resolves by key lookup.
359
+ foreach ($record in $Item) {
360
+ $marker = @{ ItemKey = $record['issue_num']; WorktreeCreatedAt = $record['worktree_created_at'] }
361
+ $rank = Get-ParallelDriftStartRank -Start $marker
362
+ $start[$rank.ItemKey] = $marker
363
+ }
364
+
365
+ $halted = [System.Collections.Generic.SortedSet[long]]::new()
366
+ # Apply the rule to each pair independently; the set collapses an item that is
367
+ # the halted member of several pairs.
368
+ foreach ($entry in $Pair) {
369
+ $candidate = @($entry | Where-Object { $_ -ne $DriftingItemKey })
370
+ if ($candidate.Count -eq 1) {
371
+ [void]$halted.Add([long]$candidate[0])
372
+ } else {
373
+ [void]$halted.Add((Select-ParallelDriftHaltedItem -First $start[[long]$candidate[0]] -Second $start[[long]$candidate[1]]))
374
+ }
375
+ }
376
+ return , [long[]]@($halted)
377
+ }
378
+
379
+ Export-ModuleMember -Function `
380
+ Assert-ParallelDriftItemKey, `
381
+ Assert-ParallelDriftText, `
382
+ Assert-ParallelDriftPathList, `
383
+ Assert-ParallelDriftEnumMember, `
384
+ ConvertTo-ParallelDriftItemKey, `
385
+ Get-ParallelDriftCanonicalPair, `
386
+ Get-ParallelDriftStartRank, `
387
+ Select-ParallelDriftHaltedItem, `
388
+ Get-ParallelDriftHaltedItemKey
@@ -8,22 +8,26 @@
8
8
  for one gated call, whether the call is an operation of that epic and, if so, returns
9
9
  the absolute path and parsed content of the governing epic checkpoint.
10
10
 
11
- A call is epic scope only when the session root's epic checkpoint parses, carries
12
- route_id "epic" and a non-empty integration_branch, and that branch equals either the
13
- call's branch signal (--head, --branch, or a branch: label) or, for a command or path
14
- leg, the HEAD branch of the effective worktree (the -C selector worktree when present,
15
- otherwise the session root). A command or path leg ignores any text branch signal, so
16
- its merge probe inspects the worktree it operates on. Anything else is not epic scope,
17
- so the caller's existing per-feature resolution runs unchanged.
18
-
19
- The checkpoint path is always composed from the resolved session root and is never
20
- read from command or prompt text (the issue #554 posture and the issue #673
21
- invariant). Exactly one checkpoint read happens per resolution.
11
+ A call is epic scope only when the epic checkpoint governing the matched branch parses,
12
+ carries route_id "epic" and a non-empty integration_branch, and that branch equals the
13
+ matched branch. The matched branch is the call's branch signal (--head, --branch, or a
14
+ branch: label) or, for a command or path leg, the HEAD branch of the effective worktree
15
+ (the -C selector worktree when present, otherwise the session root). A command or path
16
+ leg ignores any text branch signal, so its merge probe inspects the worktree it operates
17
+ on. Anything else is not epic scope, so the caller's existing per-feature resolution
18
+ runs unchanged.
19
+
20
+ The checkpoint is located by Resolve-WorktreeEpicTarget (WorktreeRunResolution.psm1,
21
+ issue #690) keyed on the matched branch: the live worktree whose epic checkpoint records
22
+ that integration branch. Its path is never composed from the session root and never read
23
+ from command or prompt text (the issue #554 posture and the issue #673 invariant). An
24
+ unresolved target is not epic scope; an ambiguous one is not epic scope with reason
25
+ target-worktree-ambiguous. Exactly one checkpoint read happens per resolution.
22
26
 
23
27
  .NOTES
24
28
  PowerShell 7+. Every filesystem contact passes through the exported seams of the
25
- sibling WorktreeResolution.psm1, so tests model any topology by mocking those seams
26
- inside this module and create no file. No subprocess, no network, no clock read, and
29
+ sibling WorktreeResolution.psm1 and WorktreeRunResolution.psm1, so tests model any
30
+ topology by mocking those seams inside this module and create no file. No subprocess, no network, no clock read, and
27
31
  no environment read. Mirrored byte-identically under
28
32
  extensions/drm-copilot/resources/claude-customizations/.
29
33
  AUTHORITY: PowerShell-authoritative. The epic readiness predicates have no Python reference implementation, and no enforcement hook invokes Python (issue #663, D5).
@@ -35,6 +39,7 @@ $ErrorActionPreference = 'Stop'
35
39
 
36
40
  Import-Module (Join-Path $PSScriptRoot 'WorktreeResolution.psm1') -ErrorAction Stop
37
41
  Import-Module (Join-Path $PSScriptRoot 'WorktreeTargetResolution.psm1') -ErrorAction Stop
42
+ Import-Module (Join-Path $PSScriptRoot 'WorktreeRunResolution.psm1') -ErrorAction Stop
38
43
 
39
44
  # The one definition of the epic checkpoint location relative to a worktree root.
40
45
  $script:EpicCheckpointRelativePath = 'artifacts/orchestration/epic-orchestrator-state.json'
@@ -278,10 +283,13 @@ function Resolve-EpicScopeCheckpoint {
278
283
  Returns an object with IsEpicScope, CheckpointPath, Checkpoint, WorktreeRoot,
279
284
  Branch, MergeInProgress, and Reason. The first matching step decides: no branch
280
285
  signal without head matching (no-branch-signal, and no checkpoint read); an
281
- unresolvable session root (session-root-unresolved); an absent or unparseable epic
282
- checkpoint (epic-checkpoint-absent-or-unparseable); a route_id other than epic
283
- (route_id); an empty integration_branch (integration_branch); an unresolvable
284
- selector (selector-unresolved); a branch that does not match (branch-mismatch).
286
+ unresolvable session root (session-root-unresolved); an unresolvable selector
287
+ (selector-unresolved); no candidate branch (branch-mismatch); no worktree whose epic
288
+ checkpoint records the candidate branch (epic-checkpoint-absent-or-unparseable); more
289
+ than one such worktree (target-worktree-ambiguous); an absent or unparseable epic
290
+ checkpoint at the resolved root (epic-checkpoint-absent-or-unparseable); a route_id
291
+ other than epic (route_id); an empty integration_branch (integration_branch); a
292
+ branch that does not match (branch-mismatch).
285
293
  .PARAMETER Text
286
294
  The command or prompt text; only its branch signal is read.
287
295
  .PARAMETER SessionRoot
@@ -319,20 +327,8 @@ function Resolve-EpicScopeCheckpoint {
319
327
  if ($null -eq $root) {
320
328
  return (New-EpicScopeResult -IsEpicScope $false -Reason 'session-root-unresolved')
321
329
  }
322
- $path = Join-WorktreeResolutionPath -WorktreeRoot $root -RepoRelativePath (Get-EpicScopeCheckpointRelativePath)
323
- $checkpoint = ConvertFrom-EpicScopeCheckpointText -Text (Get-EpicScopeCheckpointText -Path $path)
324
- if ($null -eq $checkpoint) {
325
- return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'epic-checkpoint-absent-or-unparseable')
326
- }
327
- if ([string](Get-EpicScopePropertyValue -InputObject $checkpoint -Name 'route_id') -cne $script:EpicRouteId) {
328
- return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'route_id')
329
- }
330
- $integrationBranch = [string](Get-EpicScopePropertyValue -InputObject $checkpoint -Name 'integration_branch')
331
- if ([string]::IsNullOrWhiteSpace($integrationBranch)) {
332
- return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'integration_branch')
333
- }
334
330
 
335
- # Decide the matched branch. A command or path leg (-MatchWorktreeHead) matches the
331
+ # Decide the candidate branch. A command or path leg (-MatchWorktreeHead) matches the
336
332
  # effective worktree's HEAD, the selector worktree taking precedence, and ignores any
337
333
  # text branch signal so the merge probe inspects the worktree the call operates on
338
334
  # (issue #663 remediation CR-2); otherwise the explicit branch signal decides.
@@ -344,11 +340,36 @@ function Resolve-EpicScopeCheckpoint {
344
340
  $effectiveRoot = Find-WorktreeResolutionRoot -Path $WorktreeSelector
345
341
  }
346
342
  if ($null -eq $effectiveRoot) {
347
- return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'selector-unresolved')
343
+ return (New-EpicScopeResult -IsEpicScope $false -Reason 'selector-unresolved')
348
344
  }
349
345
  $candidate = Get-EpicScopeWorktreeHeadBranch -WorktreeRoot $effectiveRoot
350
346
  }
351
- if ($null -eq $candidate -or $candidate -cne $integrationBranch) {
347
+ if ($null -eq $candidate) {
348
+ return (New-EpicScopeResult -IsEpicScope $false -Reason 'branch-mismatch')
349
+ }
350
+
351
+ # Locate the governing checkpoint by the candidate branch (issue #690), never by
352
+ # composing it from the session root.
353
+ $target = Resolve-WorktreeEpicTarget -IntegrationBranch $candidate -SessionRoot $root
354
+ if ($target.Status -eq 'NoTarget') {
355
+ return (New-EpicScopeResult -IsEpicScope $false -Reason 'epic-checkpoint-absent-or-unparseable')
356
+ }
357
+ if ($target.Status -eq 'Ambiguous') {
358
+ return (New-EpicScopeResult -IsEpicScope $false -Reason 'target-worktree-ambiguous')
359
+ }
360
+ $path = Join-WorktreeResolutionPath -WorktreeRoot $target.WorktreeRoot -RepoRelativePath (Get-EpicScopeCheckpointRelativePath)
361
+ $checkpoint = ConvertFrom-EpicScopeCheckpointText -Text (Get-EpicScopeCheckpointText -Path $path)
362
+ if ($null -eq $checkpoint) {
363
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'epic-checkpoint-absent-or-unparseable')
364
+ }
365
+ if ([string](Get-EpicScopePropertyValue -InputObject $checkpoint -Name 'route_id') -cne $script:EpicRouteId) {
366
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'route_id')
367
+ }
368
+ $integrationBranch = [string](Get-EpicScopePropertyValue -InputObject $checkpoint -Name 'integration_branch')
369
+ if ([string]::IsNullOrWhiteSpace($integrationBranch)) {
370
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'integration_branch')
371
+ }
372
+ if ($candidate -cne $integrationBranch) {
352
373
  return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'branch-mismatch')
353
374
  }
354
375
 
@@ -213,11 +213,25 @@ function Get-WorktreeItemLiveRoot {
213
213
  return , [string[]] $live.ToArray()
214
214
  }
215
215
 
216
- # Private: convert a resolved root and session path into a result, labelling it
217
- # SessionRoot or OtherWorktree so that test is written once, as the sibling normalisation
218
- # constructor does. The ConvertTo verb is deliberate: a New- verb is state-changing to
219
- # PSScriptAnalyzer and this function changes nothing.
220
216
  function ConvertTo-WorktreeItemResolvedResult {
217
+ <#
218
+ .SYNOPSIS
219
+ Convert a resolved root and session path into a SessionRoot or OtherWorktree result.
220
+ .DESCRIPTION
221
+ Labels the result SessionRoot when the root is the session's own worktree and
222
+ OtherWorktree otherwise, so that test is written once. Exported for issue #690 so
223
+ the run resolver shares this one labelling definition rather than a second copy.
224
+ The ConvertTo verb is deliberate: a New- verb is state-changing to PSScriptAnalyzer
225
+ and this function changes nothing.
226
+ .PARAMETER WorktreeRoot
227
+ The resolved worktree root.
228
+ .PARAMETER SessionRoot
229
+ The calling process's path.
230
+ .PARAMETER Detail
231
+ A prose clause safe to append to a gate's deny reason.
232
+ .PARAMETER Branch
233
+ Optional. The branch signal that produced the target.
234
+ #>
221
235
  [CmdletBinding()]
222
236
  [OutputType([pscustomobject])]
223
237
  param(
@@ -389,4 +403,5 @@ Export-ModuleMember -Function `
389
403
  Get-WorktreeItemCheckpointText, `
390
404
  Get-WorktreeItemCheckpointIssue, `
391
405
  Get-WorktreeItemLiveRoot, `
406
+ ConvertTo-WorktreeItemResolvedResult, `
392
407
  Resolve-WorktreeItemTarget