@danmoisan/drm-copilot-mcp 1.0.27 → 1.1.1

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 (42) hide show
  1. package/out/mcp-server.js +0 -1
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
  4. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
  5. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
  6. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
  7. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +64 -20
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
  12. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
  13. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
  14. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +36 -24
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +349 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +177 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  22. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  23. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  25. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  26. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  27. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  28. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  29. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +34 -60
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +187 -0
  32. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  33. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +125 -4
  34. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +27 -0
  35. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +28 -0
  36. package/resources/claude-customizations/config/blast-radius.json +9 -3
  37. package/resources/claude-customizations/pack-manifests/core.json +5 -0
  38. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  39. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +349 -0
  40. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +123 -6
  41. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  42. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +32 -1
@@ -5,8 +5,40 @@
5
5
  [CmdletBinding()]
6
6
  param()
7
7
 
8
+
9
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
10
+
11
+ # Pure pathspec classifier for the issue #539 orchestration-bookkeeping staging exemption.
12
+ # Extracted to a dot-sourced sibling so this file stays inside the 500-line cap, following
13
+ # the enforce-pr-author-skill.ps1 headroom-split precedent.
14
+ . (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-helpers.ps1')
15
+
16
+ # The readiness checkpoint this gate reads and names in its block message.
8
17
  $script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
9
18
 
19
+ # Every orchestration checkpoint a planner or orchestrator surface writes. Writing one
20
+ # of these is orchestration bookkeeping, not implementation, so the gate must not
21
+ # require a ready checkpoint before the checkpoint itself can be created. The set is a
22
+ # list of repo-relative literals behind a single membership check: no directory prefix,
23
+ # no glob, and no absolute-path entry.
24
+ $script:CheckpointPaths = @(
25
+ 'artifacts/orchestration/orchestrator-state.json'
26
+ 'artifacts/orchestration/parallel-planner-state.json'
27
+ 'artifacts/orchestration/parallel-orchestrator-state.json'
28
+ 'artifacts/orchestration/epic-planner-state.json'
29
+ 'artifacts/orchestration/epic-orchestrator-state.json'
30
+ 'artifacts/orchestration/powershell-orchestrator-state.json'
31
+ 'artifacts/orchestration/csharp-orchestrator-state.json'
32
+ )
33
+
34
+ # Both markers must appear in the field-scoped prompt for a delegation to qualify as a
35
+ # preparation-mode kickoff. The literals are reused verbatim from
36
+ # .claude/skills/parallel-plan/SKILL.md and .claude/skills/epic-plan/SKILL.md.
37
+ $script:PreparationModeMarkers = @(
38
+ 'Preparation mode: true.'
39
+ 'route_id: preparation.'
40
+ )
41
+
10
42
  function ConvertFrom-CheckpointJson {
11
43
  [CmdletBinding()]
12
44
  param([Parameter(Mandatory)][string] $Json)
@@ -33,7 +65,13 @@ function Test-FeatureDocumentationOrEvidencePath {
33
65
  [OutputType([bool])]
34
66
  param([Parameter(Mandatory)][string] $NormalizedPath)
35
67
 
36
- return $NormalizedPath.StartsWith('docs/features/active/')
68
+ # The segment anchor (^|/) admits both the repo-relative spelling and an
69
+ # absolute spelling of the same feature document, which the Write tool
70
+ # supplies by contract. -cmatch is deliberate and must not be normalized into
71
+ # -match: String.StartsWith is case-sensitive, so the case-sensitive operator
72
+ # is what preserves the previous semantics exactly. PowerShell -match is
73
+ # case-insensitive and would widen this predicate.
74
+ return $NormalizedPath -cmatch '(^|/)docs/features/active/'
37
75
  }
38
76
 
39
77
  function Test-ImplementationPath {
@@ -44,8 +82,29 @@ function Test-ImplementationPath {
44
82
  if (Test-FeatureDocumentationOrEvidencePath -NormalizedPath $NormalizedPath) {
45
83
  return $false
46
84
  }
47
- if ($NormalizedPath -eq $script:CheckpointPath) {
48
- return $false
85
+ # Segment-anchored and end-anchored, so an absolute spelling of a checkpoint is
86
+ # exempt exactly as its repo-relative spelling already was. -match is
87
+ # deliberate here and must not be narrowed into -cmatch: -contains was
88
+ # case-insensitive, so the case-insensitive operator is what preserves the
89
+ # previous semantics exactly.
90
+ #
91
+ # Accepted widening: this also exempts a path OUTSIDE the workspace whose tail
92
+ # is an artifacts/orchestration/ segment followed by one of the seven names.
93
+ # Measured exposure in this repository is one matching file, the real
94
+ # checkpoint; there is no nested or vendored second copy. The same widening is
95
+ # already accepted for the identical literal in four other hooks. Resolving a
96
+ # workspace root instead would reintroduce every root-resolution failure mode
97
+ # (8.3 short names, drive-letter case, symlinks, linked worktrees), and a strip
98
+ # that failed to match would leave the path absolute and deny, reinstating the
99
+ # reported defect in a subtler form.
100
+ #
101
+ # Known deliberate miss: a path reaching a checkpoint name only through a '..'
102
+ # hop stays denied. The Write tool does not emit '..' segments, and a
103
+ # canonicalizer would reintroduce filesystem dependence for no measured gain.
104
+ foreach ($checkpoint in $script:CheckpointPaths) {
105
+ if ($NormalizedPath -match ('(^|/)' + [regex]::Escape($checkpoint) + '$')) {
106
+ return $false
107
+ }
49
108
  }
50
109
  return $NormalizedPath -match '\.(py|ps1|psm1|ts|tsx|js|jsx|cs|json|yml|yaml)$'
51
110
  }
@@ -68,14 +127,58 @@ function Test-ImplementationCommand {
68
127
  '(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
69
128
  )
70
129
 
71
- foreach ($pattern in $implementationCommandPatterns) {
72
- if ($normalizedCommand -match $pattern) {
73
- return $true
130
+ for ($index = 0; $index -lt $implementationCommandPatterns.Count; $index++) {
131
+ if ($normalizedCommand -notmatch $implementationCommandPatterns[$index]) {
132
+ continue
74
133
  }
134
+ # Allow-side only (issue #539). Index 0 is the git staging trigger, whose pattern
135
+ # text is unchanged. It is the sole leg the orchestration-bookkeeping exemption may
136
+ # clear, and only when no other implementation pattern matches the same line: the
137
+ # loop continues rather than returning, so a chained line carrying any non-git
138
+ # implementation segment still classifies as implementation.
139
+ if ($index -eq 0 -and (Test-ExemptOrchestrationStagingCommand -CommandText $normalizedCommand)) {
140
+ continue
141
+ }
142
+ return $true
75
143
  }
76
144
  return $false
77
145
  }
78
146
 
147
+ function Test-PreparationModeDelegation {
148
+ <#
149
+ .SYNOPSIS
150
+ Identifies an orchestrator delegation that is a preparation-mode kickoff.
151
+ .DESCRIPTION
152
+ Returns true only when all three conjuncts hold: the payload is present, the
153
+ delegated agent is exactly 'orchestrator', and the field-scoped prompt carries
154
+ both preparation markers. The prompt is read as a named field rather than from
155
+ the serialized payload so that marker text planted in an unrelated field cannot
156
+ exempt an implementation delegation.
157
+ .OUTPUTS
158
+ System.Boolean
159
+ #>
160
+ [CmdletBinding()]
161
+ [OutputType([bool])]
162
+ param([Parameter(Mandatory)][AllowNull()] $ToolInput)
163
+
164
+ if ($null -eq $ToolInput) {
165
+ return $false
166
+ }
167
+
168
+ $subagentType = Get-ClaudeHookToolInputString -ToolInput $ToolInput -Name 'subagent_type'
169
+ if ($subagentType -ne 'orchestrator') {
170
+ return $false
171
+ }
172
+
173
+ $prompt = Get-ClaudeHookToolInputString -ToolInput $ToolInput -Name 'prompt'
174
+ foreach ($marker in $script:PreparationModeMarkers) {
175
+ if (-not $prompt.Contains($marker)) {
176
+ return $false
177
+ }
178
+ }
179
+ return $true
180
+ }
181
+
79
182
  function Test-ImplementationDelegation {
80
183
  [CmdletBinding()]
81
184
  [OutputType([bool])]
@@ -85,6 +188,17 @@ function Test-ImplementationDelegation {
85
188
  return $false
86
189
  }
87
190
 
191
+ try {
192
+ if (Test-PreparationModeDelegation -ToolInput $ToolInput) {
193
+ return $false
194
+ }
195
+ } catch {
196
+ # An envelope the field reader cannot probe falls through to the unchanged
197
+ # whole-payload regex below. An extraction failure must never become an
198
+ # exemption, so the gate stays closed on the stricter classifier.
199
+ Write-Debug "Preparation-mode probe failed: $($_.Exception.Message)"
200
+ }
201
+
88
202
  $payloadText = ($ToolInput | ConvertTo-Json -Depth 20 -Compress)
89
203
  return $payloadText -match '(python-typed-engineer|powershell-typed-engineer|typescript-engineer|csharp-typed-engineer|atomic-executor|implementation|execute)'
90
204
  }
@@ -164,19 +278,23 @@ function Invoke-OrchestrationPreimplementationGateDecision {
164
278
  [CmdletBinding()]
165
279
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
166
280
  param(
281
+ [AllowNull()]
282
+ [AllowEmptyString()]
167
283
  [string] $ToolInputRaw,
284
+
168
285
  [string] $CheckpointRaw
169
286
  )
170
287
 
171
- if (-not $ToolInputRaw) {
172
- return Get-OrchestrationPreimplementationGateAllowDecision
173
- }
174
- try {
175
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
176
- } catch {
177
- throw "enforce-orchestration-preimplementation-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
288
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
289
+ if (-not $payload.IsValid) {
290
+ return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
291
+ 'PREIMPLEMENTATION_GATE_BLOCKED: payload anomaly - ' +
292
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
293
+ '. The gate fails closed on an envelope it cannot read.')
178
294
  }
179
295
 
296
+ $toolInput = $payload.Value
297
+
180
298
  $requiresReadyCheckpoint = $false
181
299
  $filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
182
300
  if ($filePath) {
@@ -210,16 +328,55 @@ function Invoke-OrchestrationPreimplementationGateDecision {
210
328
  return Get-OrchestrationPreimplementationGateBlockDecision -Reason 'PREIMPLEMENTATION_GATE_BLOCKED: Implementation operations require artifacts/orchestration/orchestrator-state.json to contain issue number, feature folder, route metadata, lifecycle readiness, and checkpoint state before implementation begins.'
211
329
  }
212
330
 
331
+ function Invoke-OrchestrationPreimplementationGateEntryPoint {
332
+ <#
333
+ .SYNOPSIS
334
+ Runs the hook decision and returns the process exit code.
335
+ .DESCRIPTION
336
+ Acquires the payload through the shared reader unless the caller supplies
337
+ one, emits the compact decision JSON, and returns 0. It never returns 1:
338
+ exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
339
+ decision by the time control reaches here. The function does not call exit;
340
+ the thin tail converts the returned code into a process exit.
341
+ .PARAMETER ToolInputRaw
342
+ Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
343
+ .PARAMETER ReadPayload
344
+ Seam for payload acquisition, so tests can drive the empty-on-all-transports
345
+ case without touching a console.
346
+ .OUTPUTS
347
+ System.Int32
348
+ #>
349
+ [CmdletBinding()]
350
+ [OutputType([int])]
351
+ param(
352
+ [AllowNull()]
353
+ [AllowEmptyString()]
354
+ [string] $ToolInputRaw,
355
+
356
+ [scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
357
+ )
358
+
359
+ if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
360
+ $ToolInputRaw = [string](& $ReadPayload)
361
+ }
362
+
363
+ $decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $ToolInputRaw
364
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
365
+
366
+ return 0
367
+ }
368
+
369
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
213
370
  if ($MyInvocation.InvocationName -eq '.') {
214
371
  return
215
372
  }
216
373
 
217
- try {
218
- $decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
219
- } catch {
220
- Write-Error $_
221
- exit 1
374
+ # The entry point returns its [int] exit code as the last pipeline element and the
375
+ # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
376
+ # expression and emit nothing, so the decision is written explicitly here first.
377
+ $entryPointResult = @(Invoke-OrchestrationPreimplementationGateEntryPoint)
378
+ if ($entryPointResult.Count -gt 1) {
379
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
222
380
  }
223
381
 
224
- $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
225
- exit 0
382
+ exit ([int]$entryPointResult[-1])
@@ -5,7 +5,8 @@
5
5
 
6
6
  .DESCRIPTION
7
7
  Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
8
- command runs. Reads CLAUDE_TOOL_INPUT, extracts the command text, and matches it
8
+ command runs. Reads the hook payload through the shared reader, extracts the
9
+ command text from the envelope's nested tool_input, and matches it
9
10
  against the two tokens declared at the top of this file: the abandon disposition
10
11
  token and the confirmation marker. A command carrying the abandon disposition token
11
12
  without the confirmation marker in the SAME command is denied with reason prefix
@@ -32,6 +33,8 @@
32
33
  [CmdletBinding()]
33
34
  param()
34
35
 
36
+
37
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
35
38
  # The two tokens this gate matches on. These are the ONLY places either token literal
36
39
  # appears in this file; the seam test extracts the consumer-side values from exactly
37
40
  # these two named assignments.
@@ -44,8 +47,8 @@ $script:AbandonBlockedReasonCode = 'PARALLEL_ABANDON_BLOCKED'
44
47
  function Get-ParallelAbandonGateToolInput {
45
48
  <#
46
49
  .SYNOPSIS
47
- Read the raw JSON tool payload supplied by Claude Code. Tests mock this
48
- function (read seam).
50
+ Read the raw JSON hook payload through the shared reader (stdin first, then
51
+ the two environment fallbacks). Tests mock this function (read seam).
49
52
  .OUTPUTS
50
53
  System.String or $null
51
54
  #>
@@ -53,7 +56,7 @@ function Get-ParallelAbandonGateToolInput {
53
56
  [OutputType([string])]
54
57
  param()
55
58
 
56
- return $env:CLAUDE_TOOL_INPUT
59
+ return Read-ClaudeHookRawPayload
57
60
  }
58
61
 
59
62
  function Get-ParallelAbandonNormalizedCommand {
@@ -201,7 +204,7 @@ function Get-ParallelAbandonGateBlockReason {
201
204
  function Invoke-ParallelAbandonGateDecision {
202
205
  <#
203
206
  .SYNOPSIS
204
- Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
207
+ Parses the PreToolUse envelope and returns an allow-or-block decision.
205
208
  .PARAMETER ToolInputRaw
206
209
  The raw JSON tool payload supplied by Claude Code.
207
210
  .OUTPUTS
@@ -218,17 +221,16 @@ function Invoke-ParallelAbandonGateDecision {
218
221
  # An absent payload carries no command to gate, so there is nothing in scope. This is
219
222
  # allow rather than deny because the gate constrains one specific destructive command
220
223
  # shape and must not become a blanket Bash block.
221
- if (-not $ToolInputRaw) {
222
- return Get-ParallelAbandonGateAllowDecision
224
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
225
+ if (-not $payload.IsValid) {
226
+ return Get-ParallelAbandonGateBlockDecision -Reason (
227
+ $script:AbandonBlockedReasonCode + ': payload anomaly - ' +
228
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
229
+ '. The gate fails closed on an envelope it cannot read.')
223
230
  }
224
231
 
225
- try {
226
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
227
- } catch {
228
- throw "enforce-parallel-abandon-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
229
- }
230
-
231
- $normalizedCommand = Get-ParallelAbandonNormalizedCommand -CommandText ([string]$toolInput.command)
232
+ $commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
233
+ $normalizedCommand = Get-ParallelAbandonNormalizedCommand -CommandText $commandText
232
234
  if (-not (Test-ParallelAbandonCommandInScope -NormalizedCommand $normalizedCommand)) {
233
235
  return Get-ParallelAbandonGateAllowDecision
234
236
  }
@@ -247,12 +249,7 @@ if ($MyInvocation.InvocationName -eq '.') {
247
249
  return
248
250
  }
249
251
 
250
- try {
251
- $decision = Invoke-ParallelAbandonGateDecision -ToolInputRaw (Get-ParallelAbandonGateToolInput)
252
- } catch {
253
- Write-Error $_
254
- exit 1
255
- }
252
+ $decision = Invoke-ParallelAbandonGateDecision -ToolInputRaw (Get-ParallelAbandonGateToolInput)
256
253
 
257
254
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
258
255
 
@@ -0,0 +1,278 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Dot-sourced record-resolution and barrier helpers for enforce-parallel-cohort-barrier.ps1.
4
+
5
+ .DESCRIPTION
6
+ Holds the five resolution helpers the Layer 1 parallel cohort barrier uses once a
7
+ delegation has been identified as a parallel-mode orchestrator call:
8
+
9
+ - Find-ParallelCohortBarrierItemRecord: resolves the items[] record for a folder.
10
+ - Find-ParallelCohortBarrierItemByKey: resolves an items[] record by issue_num.
11
+ - Find-ParallelCohortBarrierCohortIndex: projects the current-generation coloring.
12
+ - Get-ParallelCohortBarrierConflictNeighborList: collects conflict-edge neighbours.
13
+ - Test-ParallelCohortBarrierClear: applies the per-edge barrier rule.
14
+
15
+ They are split out of the parent hook, .claude/hooks/enforce-parallel-cohort-barrier.ps1,
16
+ because that file stood at 499 lines -- one below the 500-line limit -- with no room
17
+ for the shared payload-reader migration (issue #501). The split is a pure move with no
18
+ behaviour change; the precedent is .claude/hooks/enforce-parallel-drift-gate-helpers.ps1.
19
+
20
+ The helpers read the script-scoped configuration the parent hook assigns
21
+ ($script:AllowedMergeStatuses), so this file is only ever dot-sourced from that hook
22
+ and never invoked on its own.
23
+
24
+ .NOTES
25
+ Compatible with PowerShell 7+. No external module dependencies. Every function is
26
+ pure: no filesystem, subprocess, network, or wall-clock access.
27
+ #>
28
+ [CmdletBinding()]
29
+ param()
30
+
31
+ function Find-ParallelCohortBarrierItemRecord {
32
+ <#
33
+ .SYNOPSIS
34
+ Locate the items[] record whose feature_folder resolves to the target basename.
35
+ .PARAMETER Checkpoint
36
+ Parsed parallel checkpoint, or $null when absent/unreadable.
37
+ .PARAMETER FeatureFolder
38
+ The target feature_folder basename resolved from the prompt.
39
+ .OUTPUTS
40
+ System.Object or $null
41
+ #>
42
+ [CmdletBinding()]
43
+ param(
44
+ [AllowNull()]
45
+ $Checkpoint,
46
+
47
+ [AllowNull()]
48
+ [string] $FeatureFolder
49
+ )
50
+
51
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($FeatureFolder)) {
52
+ return $null
53
+ }
54
+ $checkpointProps = @($Checkpoint.PSObject.Properties.Name)
55
+ if ($checkpointProps -notcontains 'items') {
56
+ return $null
57
+ }
58
+
59
+ # Scan every recorded item for a feature_folder whose basename equals the target.
60
+ foreach ($item in @($Checkpoint.items)) {
61
+ $itemProps = @($item.PSObject.Properties.Name)
62
+ if ($itemProps -notcontains 'feature_folder') {
63
+ continue
64
+ }
65
+ $itemBasename = Get-ParallelCohortBarrierFolderBasename -FolderPath ([string]$item.feature_folder)
66
+ if ($itemBasename -eq $FeatureFolder) {
67
+ return $item
68
+ }
69
+ }
70
+ return $null
71
+ }
72
+
73
+ function Find-ParallelCohortBarrierItemByKey {
74
+ <#
75
+ .SYNOPSIS
76
+ Locate the items[] record whose issue_num equals the supplied key.
77
+ .PARAMETER Checkpoint
78
+ Parsed parallel checkpoint, or $null when absent/unreadable.
79
+ .PARAMETER IssueKey
80
+ The item primary key (issue_num) rendered as a string.
81
+ .OUTPUTS
82
+ System.Object or $null
83
+ #>
84
+ [CmdletBinding()]
85
+ param(
86
+ [AllowNull()]
87
+ $Checkpoint,
88
+
89
+ [AllowNull()]
90
+ [string] $IssueKey
91
+ )
92
+
93
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
94
+ return $null
95
+ }
96
+ $checkpointProps = @($Checkpoint.PSObject.Properties.Name)
97
+ if ($checkpointProps -notcontains 'items') {
98
+ return $null
99
+ }
100
+
101
+ foreach ($item in @($Checkpoint.items)) {
102
+ $itemProps = @($item.PSObject.Properties.Name)
103
+ if ($itemProps -notcontains 'issue_num') {
104
+ continue
105
+ }
106
+ if (([string]$item.issue_num) -eq $IssueKey) {
107
+ return $item
108
+ }
109
+ }
110
+ return $null
111
+ }
112
+
113
+ function Find-ParallelCohortBarrierCohortIndex {
114
+ <#
115
+ .SYNOPSIS
116
+ Return the current-generation cohort index that holds the supplied item key.
117
+ .DESCRIPTION
118
+ The cohort projection is restricted to cohorts[] rows whose generation equals the
119
+ top-level recolor_generation, which is the current coloring. Rows from a superseded
120
+ generation are ignored. Returns $null when the checkpoint records no
121
+ recolor_generation, no cohorts, or no current-generation cohort containing the key.
122
+ .PARAMETER Checkpoint
123
+ Parsed parallel checkpoint, or $null when absent/unreadable.
124
+ .PARAMETER IssueKey
125
+ The item primary key (issue_num) rendered as a string.
126
+ .OUTPUTS
127
+ System.Int32 or $null
128
+ #>
129
+ [CmdletBinding()]
130
+ [OutputType([int])]
131
+ param(
132
+ [AllowNull()]
133
+ $Checkpoint,
134
+
135
+ [AllowNull()]
136
+ [string] $IssueKey
137
+ )
138
+
139
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
140
+ return $null
141
+ }
142
+ $checkpointProps = @($Checkpoint.PSObject.Properties.Name)
143
+ if ($checkpointProps -notcontains 'cohorts' -or $checkpointProps -notcontains 'recolor_generation') {
144
+ return $null
145
+ }
146
+
147
+ $currentGeneration = [string]$Checkpoint.recolor_generation
148
+
149
+ foreach ($cohort in @($Checkpoint.cohorts)) {
150
+ $cohortProps = @($cohort.PSObject.Properties.Name)
151
+ if ($cohortProps -notcontains 'index' -or $cohortProps -notcontains 'generation' -or $cohortProps -notcontains 'item_keys') {
152
+ continue
153
+ }
154
+ if (([string]$cohort.generation) -ne $currentGeneration) {
155
+ continue
156
+ }
157
+ $parsedIndex = 0
158
+ if (-not [int]::TryParse(([string]$cohort.index), [ref] $parsedIndex)) {
159
+ continue
160
+ }
161
+ foreach ($key in @($cohort.item_keys)) {
162
+ if (([string]$key) -eq $IssueKey) {
163
+ return $parsedIndex
164
+ }
165
+ }
166
+ }
167
+ return $null
168
+ }
169
+
170
+ function Get-ParallelCohortBarrierConflictNeighborList {
171
+ <#
172
+ .SYNOPSIS
173
+ Return the item keys that share a conflict edge with the supplied item key.
174
+ .PARAMETER Checkpoint
175
+ Parsed parallel checkpoint, or $null when absent/unreadable.
176
+ .PARAMETER IssueKey
177
+ The item primary key (issue_num) rendered as a string.
178
+ .OUTPUTS
179
+ System.Object[] holding each neighbor item key rendered as a string.
180
+ #>
181
+ [CmdletBinding()]
182
+ [OutputType([object[]])]
183
+ param(
184
+ [AllowNull()]
185
+ $Checkpoint,
186
+
187
+ [AllowNull()]
188
+ [string] $IssueKey
189
+ )
190
+
191
+ $neighborList = @()
192
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
193
+ return $neighborList
194
+ }
195
+ $checkpointProps = @($Checkpoint.PSObject.Properties.Name)
196
+ if ($checkpointProps -notcontains 'conflict_edges') {
197
+ return $neighborList
198
+ }
199
+
200
+ # Edges are undirected and canonically normalized to a < b, so both endpoints are
201
+ # inspected and the opposite endpoint is returned as the neighbor.
202
+ foreach ($edge in @($Checkpoint.conflict_edges)) {
203
+ $edgeProps = @($edge.PSObject.Properties.Name)
204
+ if ($edgeProps -notcontains 'a' -or $edgeProps -notcontains 'b') {
205
+ continue
206
+ }
207
+ $endpointA = [string]$edge.a
208
+ $endpointB = [string]$edge.b
209
+ if ($endpointA -eq $IssueKey) {
210
+ $neighborList += $endpointB
211
+ } elseif ($endpointB -eq $IssueKey) {
212
+ $neighborList += $endpointA
213
+ }
214
+ }
215
+ return $neighborList
216
+ }
217
+
218
+ function Test-ParallelCohortBarrierClear {
219
+ <#
220
+ .SYNOPSIS
221
+ Decision logic: true only when every conflicting neighbor in a strictly prior
222
+ current-generation cohort has merge_status merged or worktree_removed.
223
+ .PARAMETER Checkpoint
224
+ Parsed parallel checkpoint, or $null when absent/unreadable.
225
+ .PARAMETER ItemRecord
226
+ The target item's own items[] record, or $null when not found.
227
+ .OUTPUTS
228
+ System.Boolean
229
+ #>
230
+ [CmdletBinding()]
231
+ [OutputType([bool])]
232
+ param(
233
+ [AllowNull()]
234
+ $Checkpoint,
235
+
236
+ [AllowNull()]
237
+ $ItemRecord
238
+ )
239
+
240
+ if ($null -eq $Checkpoint -or $null -eq $ItemRecord) {
241
+ return $false
242
+ }
243
+ $itemProps = @($ItemRecord.PSObject.Properties.Name)
244
+ if ($itemProps -notcontains 'issue_num') {
245
+ return $false
246
+ }
247
+
248
+ $targetKey = [string]$ItemRecord.issue_num
249
+ $targetIndex = Find-ParallelCohortBarrierCohortIndex -Checkpoint $Checkpoint -IssueKey $targetKey
250
+ if ($null -eq $targetIndex) {
251
+ # No current-generation cohort assignment: position is unknown, so fail closed.
252
+ return $false
253
+ }
254
+
255
+ foreach ($neighborKey in @(Get-ParallelCohortBarrierConflictNeighborList -Checkpoint $Checkpoint -IssueKey $targetKey)) {
256
+ $neighborIndex = Find-ParallelCohortBarrierCohortIndex -Checkpoint $Checkpoint -IssueKey $neighborKey
257
+ if ($null -eq $neighborIndex) {
258
+ # Not part of the current coloring, so not a strictly prior cohort neighbor.
259
+ continue
260
+ }
261
+ if ($neighborIndex -ge $targetIndex) {
262
+ # Same-cohort and later-cohort contention is not a Layer 1 concern.
263
+ continue
264
+ }
265
+ $neighborRecord = Find-ParallelCohortBarrierItemByKey -Checkpoint $Checkpoint -IssueKey $neighborKey
266
+ if ($null -eq $neighborRecord) {
267
+ return $false
268
+ }
269
+ $neighborProps = @($neighborRecord.PSObject.Properties.Name)
270
+ if ($neighborProps -notcontains 'merge_status') {
271
+ return $false
272
+ }
273
+ if ($script:AllowedMergeStatuses -notcontains ([string]$neighborRecord.merge_status)) {
274
+ return $false
275
+ }
276
+ }
277
+ return $true
278
+ }