@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.22
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/out/mcp-server.js +1624 -190
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +238 -0
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +149 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
- package/resources/claude-customizations/.claude/settings.json +25 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +148 -0
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +960 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +420 -0
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
- package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
- package/resources/claude-customizations/pack-manifests/core.json +19 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/config/orchestration-routing.json +22 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pre-tool-use hook that is the Layer 1 per-call deterrent for the parallel cohort barrier.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Invoked by the Claude Code PreToolUse hook on the "Agent" matcher before any Agent
|
|
7
|
+
(Task) call runs. Activates only when CLAUDE_TOOL_INPUT.subagent_type == "orchestrator"
|
|
8
|
+
and the serialized prompt contains the parallel kickoff marker "Parallel mode: true".
|
|
9
|
+
|
|
10
|
+
Adapted from enforce-epic-wave-barrier.ps1. The parallel surface has no depends_on
|
|
11
|
+
field, so the epic hook's depends_on lookup is replaced by conflict-edge plus
|
|
12
|
+
cohort-index logic: ordering is derived from blast-radius contention recorded as
|
|
13
|
+
conflict_edges[], and position is read from the current cohort coloring.
|
|
14
|
+
|
|
15
|
+
Resolution and decision procedure:
|
|
16
|
+
1. Resolve the target item's feature folder from the prompt text by scanning for a
|
|
17
|
+
docs/features/active/<token> path, mirroring the epic hook's technique
|
|
18
|
+
(longest match wins; a .md-suffixed match uses its parent directory).
|
|
19
|
+
2. Read artifacts/orchestration/parallel-orchestrator-state.json and locate the
|
|
20
|
+
items[] record whose feature_folder resolves to the same basename.
|
|
21
|
+
3. Project the cohort coloring to the cohorts[] rows whose generation equals the
|
|
22
|
+
top-level recolor_generation, and read the target item's cohort index from that
|
|
23
|
+
projection.
|
|
24
|
+
4. Collect every conflict_edges[] neighbor of the target item.
|
|
25
|
+
5. Deny with reason PARALLEL_COHORT_BARRIER_BLOCKED unless every neighbor that sits
|
|
26
|
+
in a strictly prior current-generation cohort has merge_status in
|
|
27
|
+
{merged, worktree_removed}. ci_green does NOT satisfy the barrier: an item whose
|
|
28
|
+
CI is green has not merged, so its worktree is still live and its contention is
|
|
29
|
+
unresolved. Same-cohort and later-cohort neighbors do not block Layer 1.
|
|
30
|
+
6. A missing or unparseable checkpoint, an unresolved feature-folder token, a
|
|
31
|
+
missing items[] record, a target with no current-generation cohort assignment, a
|
|
32
|
+
missing neighbor record, and a missing merge_status all deny (fail-closed).
|
|
33
|
+
|
|
34
|
+
This is the per-call deterrent (Layer 1) of the two-layer cohort-barrier design.
|
|
35
|
+
Neither layer alone closes the gap: a PreToolUse hook fires once per tool call with no
|
|
36
|
+
cross-call or conversation-state visibility, so it cannot validate a batch of
|
|
37
|
+
concurrent Agent calls. The retrospective backstop (Layer 2) is the
|
|
38
|
+
PARALLEL_COHORT_BARRIER_VIOLATION ordering invariant inside
|
|
39
|
+
validate_parallel_orchestrator_state_text, enforced at parallel-orchestrator
|
|
40
|
+
SubagentStop time.
|
|
41
|
+
|
|
42
|
+
.NOTES
|
|
43
|
+
Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
|
|
44
|
+
through an injectable wrapper function so tests can mock the boundary without writing
|
|
45
|
+
temporary files.
|
|
46
|
+
#>
|
|
47
|
+
[CmdletBinding()]
|
|
48
|
+
param()
|
|
49
|
+
|
|
50
|
+
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
51
|
+
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
52
|
+
$script:ParallelModeMarker = 'Parallel mode: true'
|
|
53
|
+
|
|
54
|
+
function Get-ParallelCohortBarrierCheckpointContent {
|
|
55
|
+
<#
|
|
56
|
+
.SYNOPSIS
|
|
57
|
+
Read the raw JSON text of the parallel checkpoint. Tests mock this function
|
|
58
|
+
(read seam).
|
|
59
|
+
.OUTPUTS
|
|
60
|
+
System.String or $null
|
|
61
|
+
#>
|
|
62
|
+
[CmdletBinding()]
|
|
63
|
+
[OutputType([string])]
|
|
64
|
+
param()
|
|
65
|
+
|
|
66
|
+
if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
|
|
67
|
+
return $null
|
|
68
|
+
}
|
|
69
|
+
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Get-ParallelCohortBarrierFolderBasename {
|
|
73
|
+
<#
|
|
74
|
+
.SYNOPSIS
|
|
75
|
+
Normalize a feature-folder path or token to its basename.
|
|
76
|
+
.DESCRIPTION
|
|
77
|
+
Separators are normalized to forward slashes, a trailing slash is trimmed, and a
|
|
78
|
+
.md-suffixed value resolves to its parent directory before the basename is taken.
|
|
79
|
+
Applied to both sides of the target comparison so a checkpoint that records a full
|
|
80
|
+
docs/features/active/<folder> path and one that records a bare basename both match
|
|
81
|
+
the prompt token.
|
|
82
|
+
.PARAMETER FolderPath
|
|
83
|
+
The path or token to reduce.
|
|
84
|
+
.OUTPUTS
|
|
85
|
+
System.String or $null
|
|
86
|
+
#>
|
|
87
|
+
[CmdletBinding()]
|
|
88
|
+
[OutputType([string])]
|
|
89
|
+
param(
|
|
90
|
+
[AllowNull()]
|
|
91
|
+
[AllowEmptyString()]
|
|
92
|
+
[string] $FolderPath
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if ([string]::IsNullOrWhiteSpace($FolderPath)) {
|
|
96
|
+
return $null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$normalized = ($FolderPath -replace '\\', '/').TrimEnd('/')
|
|
100
|
+
if ($normalized -match '\.md$') {
|
|
101
|
+
$normalized = $normalized -replace '/[^/]+\.md$', ''
|
|
102
|
+
}
|
|
103
|
+
return ($normalized -split '/')[-1]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Find-ParallelCohortBarrierFeatureFolderFromPrompt {
|
|
107
|
+
<#
|
|
108
|
+
.SYNOPSIS
|
|
109
|
+
Scans a prompt string for docs/features/active/<...> path tokens and returns the
|
|
110
|
+
longest unique match's basename. Returns $null when no match is found.
|
|
111
|
+
.DESCRIPTION
|
|
112
|
+
Mirrors the epic wave-barrier hook's technique: forward- or backslash-separated
|
|
113
|
+
path tokens are accepted, the longest match wins, and a .md-suffixed match resolves
|
|
114
|
+
to its parent directory before the basename is extracted.
|
|
115
|
+
.PARAMETER Prompt
|
|
116
|
+
The delegation prompt text under evaluation.
|
|
117
|
+
.OUTPUTS
|
|
118
|
+
System.String or $null
|
|
119
|
+
#>
|
|
120
|
+
[CmdletBinding()]
|
|
121
|
+
[OutputType([string])]
|
|
122
|
+
param(
|
|
123
|
+
[Parameter(Mandatory)]
|
|
124
|
+
[AllowEmptyString()]
|
|
125
|
+
[string] $Prompt
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if (-not $Prompt) {
|
|
129
|
+
return $null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$pattern = 'docs[\\/]+features[\\/]+active[\\/]+[^\s"''`]+'
|
|
133
|
+
$matchList = [regex]::Matches($Prompt, $pattern)
|
|
134
|
+
if ($matchList.Count -eq 0) {
|
|
135
|
+
return $null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
$unique = @{}
|
|
139
|
+
foreach ($m in $matchList) {
|
|
140
|
+
$normalized = ($m.Value -replace '\\', '/').TrimEnd('/')
|
|
141
|
+
$unique[$normalized] = $true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$candidates = @(@($unique.Keys) | Sort-Object -Property Length -Descending)
|
|
145
|
+
return (Get-ParallelCohortBarrierFolderBasename -FolderPath $candidates[0])
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Find-ParallelCohortBarrierItemRecord {
|
|
149
|
+
<#
|
|
150
|
+
.SYNOPSIS
|
|
151
|
+
Locate the items[] record whose feature_folder resolves to the target basename.
|
|
152
|
+
.PARAMETER Checkpoint
|
|
153
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
154
|
+
.PARAMETER FeatureFolder
|
|
155
|
+
The target feature_folder basename resolved from the prompt.
|
|
156
|
+
.OUTPUTS
|
|
157
|
+
System.Object or $null
|
|
158
|
+
#>
|
|
159
|
+
[CmdletBinding()]
|
|
160
|
+
param(
|
|
161
|
+
[AllowNull()]
|
|
162
|
+
$Checkpoint,
|
|
163
|
+
|
|
164
|
+
[AllowNull()]
|
|
165
|
+
[string] $FeatureFolder
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($FeatureFolder)) {
|
|
169
|
+
return $null
|
|
170
|
+
}
|
|
171
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
172
|
+
if ($checkpointProps -notcontains 'items') {
|
|
173
|
+
return $null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Scan every recorded item for a feature_folder whose basename equals the target.
|
|
177
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
178
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
179
|
+
if ($itemProps -notcontains 'feature_folder') {
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
$itemBasename = Get-ParallelCohortBarrierFolderBasename -FolderPath ([string]$item.feature_folder)
|
|
183
|
+
if ($itemBasename -eq $FeatureFolder) {
|
|
184
|
+
return $item
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return $null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function Find-ParallelCohortBarrierItemByKey {
|
|
191
|
+
<#
|
|
192
|
+
.SYNOPSIS
|
|
193
|
+
Locate the items[] record whose issue_num equals the supplied key.
|
|
194
|
+
.PARAMETER Checkpoint
|
|
195
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
196
|
+
.PARAMETER IssueKey
|
|
197
|
+
The item primary key (issue_num) rendered as a string.
|
|
198
|
+
.OUTPUTS
|
|
199
|
+
System.Object or $null
|
|
200
|
+
#>
|
|
201
|
+
[CmdletBinding()]
|
|
202
|
+
param(
|
|
203
|
+
[AllowNull()]
|
|
204
|
+
$Checkpoint,
|
|
205
|
+
|
|
206
|
+
[AllowNull()]
|
|
207
|
+
[string] $IssueKey
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
|
|
211
|
+
return $null
|
|
212
|
+
}
|
|
213
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
214
|
+
if ($checkpointProps -notcontains 'items') {
|
|
215
|
+
return $null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
219
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
220
|
+
if ($itemProps -notcontains 'issue_num') {
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if (([string]$item.issue_num) -eq $IssueKey) {
|
|
224
|
+
return $item
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return $null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function Find-ParallelCohortBarrierCohortIndex {
|
|
231
|
+
<#
|
|
232
|
+
.SYNOPSIS
|
|
233
|
+
Return the current-generation cohort index that holds the supplied item key.
|
|
234
|
+
.DESCRIPTION
|
|
235
|
+
The cohort projection is restricted to cohorts[] rows whose generation equals the
|
|
236
|
+
top-level recolor_generation, which is the current coloring. Rows from a superseded
|
|
237
|
+
generation are ignored. Returns $null when the checkpoint records no
|
|
238
|
+
recolor_generation, no cohorts, or no current-generation cohort containing the key.
|
|
239
|
+
.PARAMETER Checkpoint
|
|
240
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
241
|
+
.PARAMETER IssueKey
|
|
242
|
+
The item primary key (issue_num) rendered as a string.
|
|
243
|
+
.OUTPUTS
|
|
244
|
+
System.Int32 or $null
|
|
245
|
+
#>
|
|
246
|
+
[CmdletBinding()]
|
|
247
|
+
[OutputType([int])]
|
|
248
|
+
param(
|
|
249
|
+
[AllowNull()]
|
|
250
|
+
$Checkpoint,
|
|
251
|
+
|
|
252
|
+
[AllowNull()]
|
|
253
|
+
[string] $IssueKey
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
|
|
257
|
+
return $null
|
|
258
|
+
}
|
|
259
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
260
|
+
if ($checkpointProps -notcontains 'cohorts' -or $checkpointProps -notcontains 'recolor_generation') {
|
|
261
|
+
return $null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
$currentGeneration = [string]$Checkpoint.recolor_generation
|
|
265
|
+
|
|
266
|
+
foreach ($cohort in @($Checkpoint.cohorts)) {
|
|
267
|
+
$cohortProps = @($cohort.PSObject.Properties.Name)
|
|
268
|
+
if ($cohortProps -notcontains 'index' -or $cohortProps -notcontains 'generation' -or $cohortProps -notcontains 'item_keys') {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
if (([string]$cohort.generation) -ne $currentGeneration) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
$parsedIndex = 0
|
|
275
|
+
if (-not [int]::TryParse(([string]$cohort.index), [ref] $parsedIndex)) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
foreach ($key in @($cohort.item_keys)) {
|
|
279
|
+
if (([string]$key) -eq $IssueKey) {
|
|
280
|
+
return $parsedIndex
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return $null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function Get-ParallelCohortBarrierConflictNeighborList {
|
|
288
|
+
<#
|
|
289
|
+
.SYNOPSIS
|
|
290
|
+
Return the item keys that share a conflict edge with the supplied item key.
|
|
291
|
+
.PARAMETER Checkpoint
|
|
292
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
293
|
+
.PARAMETER IssueKey
|
|
294
|
+
The item primary key (issue_num) rendered as a string.
|
|
295
|
+
.OUTPUTS
|
|
296
|
+
System.Object[] holding each neighbor item key rendered as a string.
|
|
297
|
+
#>
|
|
298
|
+
[CmdletBinding()]
|
|
299
|
+
[OutputType([object[]])]
|
|
300
|
+
param(
|
|
301
|
+
[AllowNull()]
|
|
302
|
+
$Checkpoint,
|
|
303
|
+
|
|
304
|
+
[AllowNull()]
|
|
305
|
+
[string] $IssueKey
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
$neighborList = @()
|
|
309
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
|
|
310
|
+
return $neighborList
|
|
311
|
+
}
|
|
312
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
313
|
+
if ($checkpointProps -notcontains 'conflict_edges') {
|
|
314
|
+
return $neighborList
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Edges are undirected and canonically normalized to a < b, so both endpoints are
|
|
318
|
+
# inspected and the opposite endpoint is returned as the neighbor.
|
|
319
|
+
foreach ($edge in @($Checkpoint.conflict_edges)) {
|
|
320
|
+
$edgeProps = @($edge.PSObject.Properties.Name)
|
|
321
|
+
if ($edgeProps -notcontains 'a' -or $edgeProps -notcontains 'b') {
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
$endpointA = [string]$edge.a
|
|
325
|
+
$endpointB = [string]$edge.b
|
|
326
|
+
if ($endpointA -eq $IssueKey) {
|
|
327
|
+
$neighborList += $endpointB
|
|
328
|
+
} elseif ($endpointB -eq $IssueKey) {
|
|
329
|
+
$neighborList += $endpointA
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return $neighborList
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function Test-ParallelCohortBarrierClear {
|
|
336
|
+
<#
|
|
337
|
+
.SYNOPSIS
|
|
338
|
+
Decision logic: true only when every conflicting neighbor in a strictly prior
|
|
339
|
+
current-generation cohort has merge_status merged or worktree_removed.
|
|
340
|
+
.PARAMETER Checkpoint
|
|
341
|
+
Parsed parallel checkpoint, or $null when absent/unreadable.
|
|
342
|
+
.PARAMETER ItemRecord
|
|
343
|
+
The target item's own items[] record, or $null when not found.
|
|
344
|
+
.OUTPUTS
|
|
345
|
+
System.Boolean
|
|
346
|
+
#>
|
|
347
|
+
[CmdletBinding()]
|
|
348
|
+
[OutputType([bool])]
|
|
349
|
+
param(
|
|
350
|
+
[AllowNull()]
|
|
351
|
+
$Checkpoint,
|
|
352
|
+
|
|
353
|
+
[AllowNull()]
|
|
354
|
+
$ItemRecord
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if ($null -eq $Checkpoint -or $null -eq $ItemRecord) {
|
|
358
|
+
return $false
|
|
359
|
+
}
|
|
360
|
+
$itemProps = @($ItemRecord.PSObject.Properties.Name)
|
|
361
|
+
if ($itemProps -notcontains 'issue_num') {
|
|
362
|
+
return $false
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
$targetKey = [string]$ItemRecord.issue_num
|
|
366
|
+
$targetIndex = Find-ParallelCohortBarrierCohortIndex -Checkpoint $Checkpoint -IssueKey $targetKey
|
|
367
|
+
if ($null -eq $targetIndex) {
|
|
368
|
+
# No current-generation cohort assignment: position is unknown, so fail closed.
|
|
369
|
+
return $false
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
foreach ($neighborKey in @(Get-ParallelCohortBarrierConflictNeighborList -Checkpoint $Checkpoint -IssueKey $targetKey)) {
|
|
373
|
+
$neighborIndex = Find-ParallelCohortBarrierCohortIndex -Checkpoint $Checkpoint -IssueKey $neighborKey
|
|
374
|
+
if ($null -eq $neighborIndex) {
|
|
375
|
+
# Not part of the current coloring, so not a strictly prior cohort neighbor.
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
if ($neighborIndex -ge $targetIndex) {
|
|
379
|
+
# Same-cohort and later-cohort contention is not a Layer 1 concern.
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
$neighborRecord = Find-ParallelCohortBarrierItemByKey -Checkpoint $Checkpoint -IssueKey $neighborKey
|
|
383
|
+
if ($null -eq $neighborRecord) {
|
|
384
|
+
return $false
|
|
385
|
+
}
|
|
386
|
+
$neighborProps = @($neighborRecord.PSObject.Properties.Name)
|
|
387
|
+
if ($neighborProps -notcontains 'merge_status') {
|
|
388
|
+
return $false
|
|
389
|
+
}
|
|
390
|
+
if ($script:AllowedMergeStatuses -notcontains ([string]$neighborRecord.merge_status)) {
|
|
391
|
+
return $false
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return $true
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function Get-ParallelCohortBarrierAllowDecision {
|
|
398
|
+
[CmdletBinding()]
|
|
399
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
400
|
+
param()
|
|
401
|
+
|
|
402
|
+
return [ordered]@{
|
|
403
|
+
hookSpecificOutput = [ordered]@{
|
|
404
|
+
hookEventName = 'PreToolUse'
|
|
405
|
+
permissionDecision = 'allow'
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function Get-ParallelCohortBarrierBlockDecision {
|
|
411
|
+
[CmdletBinding()]
|
|
412
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
413
|
+
param(
|
|
414
|
+
[Parameter(Mandatory)]
|
|
415
|
+
[string] $Reason
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return [ordered]@{
|
|
419
|
+
hookSpecificOutput = [ordered]@{
|
|
420
|
+
hookEventName = 'PreToolUse'
|
|
421
|
+
permissionDecision = 'deny'
|
|
422
|
+
permissionDecisionReason = $Reason
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function Invoke-ParallelCohortBarrierDecision {
|
|
428
|
+
<#
|
|
429
|
+
.SYNOPSIS
|
|
430
|
+
Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
|
|
431
|
+
.PARAMETER ToolInputRaw
|
|
432
|
+
The raw JSON tool payload supplied by Claude Code.
|
|
433
|
+
.OUTPUTS
|
|
434
|
+
System.Collections.Specialized.OrderedDictionary
|
|
435
|
+
#>
|
|
436
|
+
[CmdletBinding()]
|
|
437
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
438
|
+
param(
|
|
439
|
+
[string] $ToolInputRaw
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if (-not $ToolInputRaw) {
|
|
443
|
+
return Get-ParallelCohortBarrierAllowDecision
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
448
|
+
} catch {
|
|
449
|
+
throw "enforce-parallel-cohort-barrier hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
$subagent = $toolInput.subagent_type
|
|
453
|
+
if (-not $subagent -or $subagent -ne 'orchestrator') {
|
|
454
|
+
return Get-ParallelCohortBarrierAllowDecision
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
$prompt = [string]$toolInput.prompt
|
|
458
|
+
if (-not $prompt -or $prompt -notlike "*$script:ParallelModeMarker*") {
|
|
459
|
+
return Get-ParallelCohortBarrierAllowDecision
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
$featureFolder = Find-ParallelCohortBarrierFeatureFolderFromPrompt -Prompt $prompt
|
|
463
|
+
if (-not $featureFolder) {
|
|
464
|
+
return Get-ParallelCohortBarrierBlockDecision -Reason 'PARALLEL_COHORT_BARRIER_BLOCKED: a parallel-mode orchestrator delegation must reference the target item feature folder path (docs/features/active/<folder>) in the prompt so its conflict edges and cohort position can be verified.'
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
$checkpointRaw = Get-ParallelCohortBarrierCheckpointContent
|
|
468
|
+
$checkpoint = $null
|
|
469
|
+
if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
|
|
470
|
+
try {
|
|
471
|
+
$checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
|
|
472
|
+
} catch {
|
|
473
|
+
$checkpoint = $null
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
$itemRecord = Find-ParallelCohortBarrierItemRecord -Checkpoint $checkpoint -FeatureFolder $featureFolder
|
|
478
|
+
if (Test-ParallelCohortBarrierClear -Checkpoint $checkpoint -ItemRecord $itemRecord) {
|
|
479
|
+
return Get-ParallelCohortBarrierAllowDecision
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return Get-ParallelCohortBarrierBlockDecision -Reason "PARALLEL_COHORT_BARRIER_BLOCKED: '$featureFolder' cannot start until every conflicting item in a strictly prior current-generation cohort is durably confirmed merged or worktree_removed in the parallel checkpoint. The checkpoint was unreadable, the items[] record was not found, the item has no current-generation cohort assignment, or a conflicting prior-cohort item is not yet safe (ci_green does not satisfy the barrier)."
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
486
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
$decision = Invoke-ParallelCohortBarrierDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
492
|
+
} catch {
|
|
493
|
+
Write-Error $_
|
|
494
|
+
exit 1
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
498
|
+
|
|
499
|
+
exit 0
|