@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0

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 (63) hide show
  1. package/out/mcp-server.js +542 -42
  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 +172 -24
  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 +402 -0
  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.ps1 +59 -14
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  22. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  25. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  26. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  27. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  28. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  29. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
  32. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
  33. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
  34. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  35. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
  36. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
  37. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
  38. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
  39. package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
  40. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
  41. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
  42. package/resources/claude-customizations/.claude/settings.json +5 -0
  43. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
  44. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  45. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
  46. package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
  47. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
  48. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
  49. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
  50. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
  51. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
  52. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
  53. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
  54. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
  55. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
  57. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
  58. package/resources/claude-customizations/config/blast-radius.json +16 -2
  59. package/resources/claude-customizations/pack-manifests/core.json +22 -1
  60. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  61. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  62. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  63. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +31 -1
@@ -5,6 +5,8 @@
5
5
  [CmdletBinding()]
6
6
  param()
7
7
 
8
+
9
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
8
10
  $script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
9
11
 
10
12
  function ConvertFrom-CheckpointJson {
@@ -164,19 +166,23 @@ function Invoke-OrchestrationPreimplementationGateDecision {
164
166
  [CmdletBinding()]
165
167
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
166
168
  param(
169
+ [AllowNull()]
170
+ [AllowEmptyString()]
167
171
  [string] $ToolInputRaw,
172
+
168
173
  [string] $CheckpointRaw
169
174
  )
170
175
 
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: $_"
176
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
177
+ if (-not $payload.IsValid) {
178
+ return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
179
+ 'PREIMPLEMENTATION_GATE_BLOCKED: payload anomaly - ' +
180
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
181
+ '. The gate fails closed on an envelope it cannot read.')
178
182
  }
179
183
 
184
+ $toolInput = $payload.Value
185
+
180
186
  $requiresReadyCheckpoint = $false
181
187
  $filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
182
188
  if ($filePath) {
@@ -210,16 +216,55 @@ function Invoke-OrchestrationPreimplementationGateDecision {
210
216
  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
217
  }
212
218
 
219
+ function Invoke-OrchestrationPreimplementationGateEntryPoint {
220
+ <#
221
+ .SYNOPSIS
222
+ Runs the hook decision and returns the process exit code.
223
+ .DESCRIPTION
224
+ Acquires the payload through the shared reader unless the caller supplies
225
+ one, emits the compact decision JSON, and returns 0. It never returns 1:
226
+ exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
227
+ decision by the time control reaches here. The function does not call exit;
228
+ the thin tail converts the returned code into a process exit.
229
+ .PARAMETER ToolInputRaw
230
+ Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
231
+ .PARAMETER ReadPayload
232
+ Seam for payload acquisition, so tests can drive the empty-on-all-transports
233
+ case without touching a console.
234
+ .OUTPUTS
235
+ System.Int32
236
+ #>
237
+ [CmdletBinding()]
238
+ [OutputType([int])]
239
+ param(
240
+ [AllowNull()]
241
+ [AllowEmptyString()]
242
+ [string] $ToolInputRaw,
243
+
244
+ [scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
245
+ )
246
+
247
+ if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
248
+ $ToolInputRaw = [string](& $ReadPayload)
249
+ }
250
+
251
+ $decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $ToolInputRaw
252
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
253
+
254
+ return 0
255
+ }
256
+
257
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
213
258
  if ($MyInvocation.InvocationName -eq '.') {
214
259
  return
215
260
  }
216
261
 
217
- try {
218
- $decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
219
- } catch {
220
- Write-Error $_
221
- exit 1
262
+ # The entry point returns its [int] exit code as the last pipeline element and the
263
+ # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
264
+ # expression and emit nothing, so the decision is written explicitly here first.
265
+ $entryPointResult = @(Invoke-OrchestrationPreimplementationGateEntryPoint)
266
+ if ($entryPointResult.Count -gt 1) {
267
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
222
268
  }
223
269
 
224
- $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
225
- exit 0
270
+ 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
+ }