@danmoisan/drm-copilot-mcp 1.1.15 → 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 (71) 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/powershell-typed-engineer.md +4 -5
  5. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  6. package/resources/claude-customizations/.claude/hooks/enforce-batch-budget-route.ps1 +139 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
  19. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  20. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  24. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  25. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  26. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  30. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  31. package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
  32. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
  33. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  34. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +7 -4
  35. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  36. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
  37. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
  38. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  39. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
  40. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  41. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  42. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -2
  43. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  44. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
  45. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  46. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
  47. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
  48. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
  49. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
  50. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
  51. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
  52. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  53. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  54. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  55. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  56. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  57. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  58. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  59. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +165 -80
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  63. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  64. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
  65. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
  66. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  67. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  68. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
  69. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  70. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
  71. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -0
@@ -19,17 +19,15 @@ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -
19
19
  # proof the issue #539 exemption is behaviourally unchanged.
20
20
  . (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-modes.ps1')
21
21
 
22
- # Epic-scope command and path legs (issue #663, decision D2), plus the two per-mode read
23
- # seams relocated from this file to keep it inside the 500-line cap.
22
+ # Epic-scope command and path legs (issue #663, decision D2), the three checkpoint read
23
+ # seams, the mode deny reason, and the target resolution with its import guard (issue
24
+ # #690), all relocated to this sibling to keep the gate inside the 500-line cap.
24
25
  . (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-epic-scope.ps1')
25
26
 
26
27
  # Shared command-line parser (issue #545): per-segment scan text and structural matching.
27
28
  . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
28
29
  . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
29
30
 
30
- # The readiness checkpoint this gate reads and names in its block message.
31
- $script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
32
-
33
31
  # Every orchestration checkpoint a planner or orchestrator surface writes. Writing one
34
32
  # of these is orchestration bookkeeping, not implementation, so the gate must not
35
33
  # require a ready checkpoint before the checkpoint itself can be created. The set is a
@@ -254,17 +252,6 @@ function Test-OrchestrationReady {
254
252
  )
255
253
  }
256
254
 
257
- function Get-CheckpointContent {
258
- [CmdletBinding()]
259
- [OutputType([string])]
260
- param()
261
-
262
- if (-not (Test-Path -LiteralPath $script:CheckpointPath)) {
263
- return ''
264
- }
265
- return Get-Content -Raw -LiteralPath $script:CheckpointPath
266
- }
267
-
268
255
  function Get-OrchestrationPreimplementationGateAllowDecision {
269
256
  [CmdletBinding()]
270
257
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -295,23 +282,6 @@ function Get-OrchestrationPreimplementationGateBlockDecision {
295
282
  }
296
283
  }
297
284
 
298
- # Builds a mode-specific deny reason naming the checkpoint actually consulted and the
299
- # predicate that failed, behind the unchanged PREIMPLEMENTATION_GATE_BLOCKED prefix
300
- # that downstream reason-matching reads.
301
- function Get-OrchestrationModeDenyReason {
302
- [CmdletBinding()]
303
- [OutputType([string])]
304
- param(
305
- [Parameter(Mandatory)][string] $Mode,
306
- [Parameter(Mandatory)][string] $Failure
307
- )
308
-
309
- $path = Get-OrchestrationDelegationCheckpointPath -Mode $Mode
310
- return ("PREIMPLEMENTATION_GATE_BLOCKED: this $Mode-mode delegation was evaluated against " +
311
- "$path, and the failed readiness predicate is '$Failure'. Implementation operations " +
312
- 'require that checkpoint to satisfy every readiness predicate before implementation begins.')
313
- }
314
-
315
285
  function Invoke-OrchestrationPreimplementationGateDecision {
316
286
  [CmdletBinding()]
317
287
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -337,6 +307,9 @@ function Invoke-OrchestrationPreimplementationGateDecision {
337
307
  [string] $ParallelCheckpointRaw
338
308
  )
339
309
 
310
+ # A failed worktree-resolution import denies before any other logic (issue #690).
311
+ $importFailure = Get-OrchestrationGateImportFailureDecision; if ($null -ne $importFailure) { return $importFailure }
312
+
340
313
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
341
314
  if (-not $payload.IsValid) {
342
315
  return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
@@ -396,9 +369,16 @@ function Invoke-OrchestrationPreimplementationGateDecision {
396
369
  if ($mode -eq 'epic' -or $mode -eq 'parallel') {
397
370
  $isEpic = ($mode -eq 'epic')
398
371
  $injected = if ($isEpic) { 'EpicCheckpointRaw' } else { 'ParallelCheckpointRaw' }
399
- $modeRaw = if ($PSBoundParameters.ContainsKey($injected)) {
400
- [string]$PSBoundParameters[$injected]
401
- } elseif ($isEpic) { Get-EpicCheckpointContent } else { Get-ParallelCheckpointContent }
372
+ $modePath = ''
373
+ if ($PSBoundParameters.ContainsKey($injected)) {
374
+ $modeRaw = [string]$PSBoundParameters[$injected]
375
+ } else {
376
+ # The run checkpoint is read beneath the worktree the kickoff identifies (#690).
377
+ $read = Read-OrchestrationGateCheckpoint -Mode $mode -Prompt $prompt
378
+ if (-not $read.Resolved) { return Get-OrchestrationPreimplementationGateBlockDecision -Reason $read.DenyReason }
379
+ $modeRaw = $read.Raw
380
+ $modePath = $read.Path
381
+ }
402
382
  try {
403
383
  $modeCheckpoint = ConvertFrom-CheckpointJson -Json ([string]$modeRaw)
404
384
  } catch { $modeCheckpoint = $null }
@@ -411,11 +391,14 @@ function Invoke-OrchestrationPreimplementationGateDecision {
411
391
  }
412
392
  if (-not $failure) { return Get-OrchestrationPreimplementationGateAllowDecision }
413
393
  return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
414
- Get-OrchestrationModeDenyReason -Mode $mode -Failure $failure)
394
+ Get-OrchestrationModeDenyReason -Mode $mode -Failure $failure -CheckpointPath $modePath)
415
395
  }
416
396
 
397
+ # Truthiness fall-through is kept: an empty -CheckpointRaw resolves like an unbound one.
417
398
  if (-not $CheckpointRaw) {
418
- $CheckpointRaw = Get-CheckpointContent
399
+ $read = Read-OrchestrationGateCheckpoint -Mode 'single-feature' -Prompt $prompt -FilePath $filePath -Command $command
400
+ if (-not $read.Resolved) { return Get-OrchestrationPreimplementationGateBlockDecision -Reason $read.DenyReason }
401
+ $CheckpointRaw = $read.Raw
419
402
  }
420
403
  try {
421
404
  $checkpoint = ConvertFrom-CheckpointJson -Json $CheckpointRaw
@@ -25,10 +25,10 @@
25
25
  .claude/hooks/enforce-epic-worktree-removal-gate.ps1.
26
26
 
27
27
  The two token values are declared ONCE EACH below and every match in this file reads
28
- those variables rather than repeating a literal. The producer side of the same pair is
29
- scripts/dev_tools/parallel_mutation_abandon_cli.py, and
30
- tests/scripts/dev_tools/test_parallel_abandon_token_seam.py parses both sides at run
31
- time so a rename on one side without the other fails.
28
+ those variables rather than repeating a literal. The pushed-down producer of the same pair
29
+ is .claude/lib/bash/abandon-parallel-item.sh (the Python CLI it ports remains the parity
30
+ reference), and tests/scripts/dev_tools/test_parallel_abandon_token_seam.py parses every
31
+ side at run time so a rename on one side without the others fails.
32
32
  #>
33
33
  [CmdletBinding()]
34
34
  param()
@@ -16,8 +16,11 @@
16
16
  1. Resolve the target item's feature folder from the prompt text by scanning for a
17
17
  docs/features/active/<token> path, mirroring the epic hook's technique
18
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.
19
+ 2. Resolve the worktree whose parallel checkpoint records the prompt's
20
+ parallel_slug: value (issue #690), deny an unresolved or ambiguous target, then
21
+ read artifacts/orchestration/parallel-orchestrator-state.json beneath that
22
+ worktree and locate the items[] record whose feature_folder resolves to the same
23
+ basename.
21
24
  3. Project the cohort coloring to the cohorts[] rows whose generation equals the
22
25
  top-level recolor_generation, and read the target item's cohort index from that
23
26
  projection.
@@ -40,16 +43,26 @@
40
43
  SubagentStop time.
41
44
 
42
45
  .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
+ Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 (issue #690),
47
+ imported inside a guard: a failed import is recorded and the decision denies naming
48
+ the module. Filesystem reads go through an injectable wrapper function so tests can
49
+ mock the boundary without writing temporary files.
46
50
  #>
47
51
  [CmdletBinding()]
48
52
  param()
49
53
 
50
54
 
51
55
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
52
- $script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
56
+
57
+ # Import guard (issue #690): a failed import denies instead of failing open.
58
+ $script:ParallelCohortBarrierResolutionImportFailure = $null
59
+ try {
60
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
61
+ }
62
+ catch {
63
+ $script:ParallelCohortBarrierResolutionImportFailure = 'WorktreeRunResolution.psm1'
64
+ }
65
+
53
66
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
54
67
  $script:ParallelModeMarker = 'Parallel mode: true'
55
68
 
@@ -62,17 +75,39 @@ function Get-ParallelCohortBarrierCheckpointContent {
62
75
  .SYNOPSIS
63
76
  Read the raw JSON text of the parallel checkpoint. Tests mock this function
64
77
  (read seam).
78
+ .PARAMETER Path
79
+ The absolute checkpoint path composed beneath the resolved worktree root.
65
80
  .OUTPUTS
66
81
  System.String or $null
67
82
  #>
68
83
  [CmdletBinding()]
69
84
  [OutputType([string])]
70
- param()
85
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
71
86
 
72
- if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
87
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
73
88
  return $null
74
89
  }
75
- return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
90
+ return (Get-Content -LiteralPath $Path -Raw)
91
+ }
92
+
93
+ function Resolve-ParallelCohortBarrierTarget {
94
+ <#
95
+ .SYNOPSIS
96
+ Resolve the worktree whose parallel checkpoint governs a kickoff (issue #690 seam).
97
+ .DESCRIPTION
98
+ Keyed on the prompt's parallel_slug: value; never on a feature-folder path or the
99
+ payload cwd.
100
+ .PARAMETER Prompt
101
+ The delegation prompt text.
102
+ .OUTPUTS
103
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
104
+ #>
105
+ [CmdletBinding()]
106
+ [OutputType([pscustomobject])]
107
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Prompt)
108
+
109
+ $signal = Find-WorktreeRunIdentitySignal -Text $Prompt
110
+ return Resolve-WorktreeParallelTarget -ParallelSlug $signal.ParallelSlug -SessionRoot (Get-Location).Path
76
111
  }
77
112
 
78
113
  function Get-ParallelCohortBarrierFolderBasename {
@@ -196,6 +231,13 @@ function Invoke-ParallelCohortBarrierDecision {
196
231
  [string] $ToolInputRaw
197
232
  )
198
233
 
234
+ # A failed worktree-resolution import denies before any other logic (issue #690).
235
+ if ($script:ParallelCohortBarrierResolutionImportFailure) {
236
+ return Get-ParallelCohortBarrierBlockDecision -Reason (
237
+ "PARALLEL_COHORT_BARRIER_BLOCKED: the worktree-resolution module '$($script:ParallelCohortBarrierResolutionImportFailure)' " +
238
+ 'failed to import, so the parallel checkpoint that governs this delegation cannot be located; the gate fails closed.')
239
+ }
240
+
199
241
  $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
200
242
  if (-not $envelope.IsValid) {
201
243
  return Get-ParallelCohortBarrierBlockDecision -Reason (
@@ -214,12 +256,18 @@ function Invoke-ParallelCohortBarrierDecision {
214
256
  return Get-ParallelCohortBarrierAllowDecision
215
257
  }
216
258
 
259
+ # The parallel checkpoint is located by the kickoff's parallel_slug (issue #690).
260
+ $target = Resolve-ParallelCohortBarrierTarget -Prompt $prompt
261
+ if ($target.Status -eq 'NoTarget' -or $target.Status -eq 'Ambiguous') {
262
+ return Get-ParallelCohortBarrierBlockDecision -Reason "PARALLEL_COHORT_BARRIER_BLOCKED: $($target.ReasonCode): $($target.Detail)"
263
+ }
264
+
217
265
  $featureFolder = Find-ParallelCohortBarrierFeatureFolderFromPrompt -Prompt $prompt
218
266
  if (-not $featureFolder) {
219
267
  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.'
220
268
  }
221
269
 
222
- $checkpointRaw = Get-ParallelCohortBarrierCheckpointContent
270
+ $checkpointRaw = Get-ParallelCohortBarrierCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind parallel -WorktreeRoot $target.WorktreeRoot)
223
271
  $checkpoint = $null
224
272
  if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
225
273
  try {
@@ -45,10 +45,15 @@
45
45
  retrospective backstop, is the PARALLEL_DRIFT_GATE_VIOLATION invariant in
46
46
  scripts/dev_tools/_parallel_orchestrator_state_drift.py.
47
47
 
48
+ Issue #690: the parallel checkpoint is read beneath the worktree that the prompt's
49
+ parallel_slug: value resolves to, never beneath the payload cwd. An unresolved or
50
+ ambiguous target denies after the subagent and marker filter and before the folder check.
51
+
48
52
  .NOTES
49
- PowerShell 7+, no module dependencies. Both read boundaries -- the checkpoint read and the
50
- finding-file existence check -- are injectable wrapper functions, so tests mock them
51
- without writing temporary files.
53
+ PowerShell 7+. Depends on WorktreeRunResolution.psm1 (issue #690), imported inside a
54
+ guard: a failed import is recorded and the decision denies naming the module. Both read
55
+ boundaries -- the checkpoint read and the finding-file existence check -- are injectable
56
+ wrapper functions, so tests mock them without writing temporary files.
52
57
 
53
58
  The eight shape-and-derivation helpers this hook calls live in the dot-sourced sibling
54
59
  module .claude/hooks/enforce-parallel-drift-gate-helpers.ps1, together with the
@@ -63,12 +68,21 @@ param()
63
68
 
64
69
 
65
70
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
71
+
72
+ # Import guard (issue #690): a failed import denies instead of failing open.
73
+ $script:ParallelDriftGateResolutionImportFailure = $null
74
+ try {
75
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
76
+ }
77
+ catch {
78
+ $script:ParallelDriftGateResolutionImportFailure = 'WorktreeRunResolution.psm1'
79
+ }
80
+
66
81
  # Dot-source the shape-and-derivation helpers. Guarded so a missing file produces a clear error
67
82
  # and so dot-sourcing this hook in tests loads the helpers too.
68
83
  $script:ParallelDriftGateHelpersPath = Join-Path $PSScriptRoot 'enforce-parallel-drift-gate-helpers.ps1'
69
84
  . $script:ParallelDriftGateHelpersPath
70
85
 
71
- $script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
72
86
  $script:ParallelModeMarker = 'Parallel mode: true'
73
87
  $script:ReviewSubagentType = 'feature-review'
74
88
  $script:ActiveFeatureRoot = 'docs/features/active'
@@ -85,15 +99,38 @@ function Get-ParallelDriftGateCheckpointContent {
85
99
  .SYNOPSIS
86
100
  Read the raw JSON text of the parallel checkpoint, or $null when the file is absent.
87
101
  Tests mock this function (checkpoint-read seam).
102
+ .PARAMETER Path
103
+ The absolute checkpoint path composed beneath the resolved worktree root.
88
104
  #>
89
105
  [CmdletBinding()]
90
106
  [OutputType([string])]
91
- param()
107
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
92
108
 
93
- if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
109
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
94
110
  return $null
95
111
  }
96
- return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
112
+ return (Get-Content -LiteralPath $Path -Raw)
113
+ }
114
+
115
+ function Resolve-ParallelDriftGateTarget {
116
+ <#
117
+ .SYNOPSIS
118
+ Resolve the worktree whose parallel checkpoint governs a review delegation (issue #690
119
+ seam).
120
+ .DESCRIPTION
121
+ Keyed on the prompt's parallel_slug: value; never on a feature-folder path or the
122
+ payload cwd.
123
+ .PARAMETER Prompt
124
+ The delegation prompt text.
125
+ .OUTPUTS
126
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
127
+ #>
128
+ [CmdletBinding()]
129
+ [OutputType([pscustomobject])]
130
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Prompt)
131
+
132
+ $signal = Find-WorktreeRunIdentitySignal -Text $Prompt
133
+ return Resolve-WorktreeParallelTarget -ParallelSlug $signal.ParallelSlug -SessionRoot (Get-Location).Path
97
134
  }
98
135
 
99
136
  function Test-ParallelDriftFindingPresent {
@@ -282,6 +319,13 @@ function Invoke-ParallelDriftGateDecision {
282
319
  [string] $ToolInputRaw
283
320
  )
284
321
 
322
+ # A failed worktree-resolution import denies before any other logic (issue #690).
323
+ if ($script:ParallelDriftGateResolutionImportFailure) {
324
+ return Get-ParallelDriftGateBlockDecision -Reason (
325
+ "PARALLEL_DRIFT_GATE_BLOCKED: the worktree-resolution module '$($script:ParallelDriftGateResolutionImportFailure)' " +
326
+ 'failed to import, so the parallel checkpoint that governs this review cannot be located; the gate fails closed.')
327
+ }
328
+
285
329
  $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
286
330
  if (-not $envelope.IsValid) {
287
331
  return Get-ParallelDriftGateBlockDecision -Reason (
@@ -302,12 +346,18 @@ function Invoke-ParallelDriftGateDecision {
302
346
  return Get-ParallelDriftGateAllowDecision
303
347
  }
304
348
 
349
+ # The parallel checkpoint is located by the delegation's parallel_slug (issue #690).
350
+ $target = Resolve-ParallelDriftGateTarget -Prompt $prompt
351
+ if ($target.Status -eq 'NoTarget' -or $target.Status -eq 'Ambiguous') {
352
+ return Get-ParallelDriftGateBlockDecision -Reason "PARALLEL_DRIFT_GATE_BLOCKED: $($target.ReasonCode): $($target.Detail)"
353
+ }
354
+
305
355
  $featureFolder = Find-ParallelDriftGateFeatureFolderFromPrompt -Prompt $prompt
306
356
  if (-not $featureFolder) {
307
357
  return Get-ParallelDriftGateBlockDecision -Reason 'PARALLEL_DRIFT_GATE_BLOCKED: a parallel-mode feature-review delegation must reference the target item feature folder in the prompt so its drift state can be verified.'
308
358
  }
309
359
 
310
- $checkpointRaw = Get-ParallelDriftGateCheckpointContent
360
+ $checkpointRaw = Get-ParallelDriftGateCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind parallel -WorktreeRoot $target.WorktreeRoot)
311
361
  $checkpoint = $null
312
362
  if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
313
363
  try {
@@ -317,7 +367,7 @@ function Invoke-ParallelDriftGateDecision {
317
367
  }
318
368
  }
319
369
  if ($null -eq $checkpoint) {
320
- return Get-ParallelDriftGateBlockDecision -Reason "PARALLEL_DRIFT_GATE_BLOCKED: the parallel checkpoint '$script:ParallelCheckpointPath' is missing or unreadable, so the drift state of '$featureFolder' cannot be verified."
370
+ return Get-ParallelDriftGateBlockDecision -Reason "PARALLEL_DRIFT_GATE_BLOCKED: the parallel checkpoint 'artifacts/orchestration/parallel-orchestrator-state.json' in worktree '$($target.WorktreeRoot)' is missing or unreadable, so the drift state of '$featureFolder' cannot be verified."
321
371
  }
322
372
 
323
373
  $item = Find-ParallelDriftGateItemRecord -Checkpoint $checkpoint -FeatureFolder $featureFolder
@@ -20,10 +20,15 @@
20
20
  opens its own pull request against main, so a removed worktree is unrecoverable work
21
21
  unless that item's own merge has been durably confirmed.
22
22
 
23
+ Issue #690: each checkpoint is read beneath the live worktree whose run checkpoint
24
+ records the removal target under worktree_path. An ambiguous target denies before any
25
+ allow; when neither kind resolves, the deny names TARGET_WORKTREE_NOT_DERIVABLE.
26
+
23
27
  .NOTES
24
- Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
25
- through an injectable wrapper function so tests can mock the boundary without writing
26
- temporary files.
28
+ Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 (issue #690),
29
+ imported inside a guard: a failed import is recorded and the decision denies naming
30
+ the module. Filesystem reads go through injectable wrapper functions so tests can
31
+ mock the boundary without writing temporary files.
27
32
  #>
28
33
  [CmdletBinding()]
29
34
  param()
@@ -39,17 +44,19 @@ Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeM
39
44
  # concern now has one implementation.
40
45
  . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
41
46
  . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
42
- $script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
47
+
48
+ # Import guard (issue #690): a failed import denies instead of failing open.
49
+ $script:ParallelWorktreeGateResolutionImportFailure = $null
50
+ try {
51
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
52
+ }
53
+ catch {
54
+ $script:ParallelWorktreeGateResolutionImportFailure = 'WorktreeRunResolution.psm1'
55
+ }
56
+
43
57
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
44
- # Epic checkpoint location (issue #688). An epic run records per-child state in
45
- # features[] and never writes the parallel checkpoint, so without this read every
46
- # merged epic child worktree reached the deny below even though
47
- # enforce-epic-worktree-removal-gate.ps1 authorized the same call. Both gates run on
48
- # the same Bash call, so that deny won and epic worktrees could never be removed.
49
- $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
50
- # Sanctioned-removal manifest location. Recorded here beside the checkpoint path so
51
- # every hook-read document this gate consults is named in one place; the module owns
52
- # the read itself.
58
+ # Sanctioned-removal manifest location. Recorded here so every hook-read document this
59
+ # gate consults is named; the module owns the read itself.
53
60
  $script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
54
61
 
55
62
  function Get-ParallelWorktreeRemovalGateCheckpointContent {
@@ -57,17 +64,19 @@ function Get-ParallelWorktreeRemovalGateCheckpointContent {
57
64
  .SYNOPSIS
58
65
  Read the raw JSON text of the parallel checkpoint. Tests mock this function
59
66
  (read seam).
67
+ .PARAMETER Path
68
+ The absolute checkpoint path composed beneath the resolved worktree root.
60
69
  .OUTPUTS
61
70
  System.String or $null
62
71
  #>
63
72
  [CmdletBinding()]
64
73
  [OutputType([string])]
65
- param()
74
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
66
75
 
67
- if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
76
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
68
77
  return $null
69
78
  }
70
- return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
79
+ return (Get-Content -LiteralPath $Path -Raw)
71
80
  }
72
81
 
73
82
  function Get-ParallelWorktreeRemovalGateEpicCheckpointContent {
@@ -78,18 +87,81 @@ function Get-ParallelWorktreeRemovalGateEpicCheckpointContent {
78
87
  .DESCRIPTION
79
88
  A separate seam from the parallel checkpoint reader so a test can drive the
80
89
  two documents independently: the epic branch must be exercised with the
81
- parallel checkpoint absent, present-but-not-covering, and covering.
90
+ parallel checkpoint absent, present-but-not-covering, and covering. An epic run
91
+ records per-child state in features[] and never writes the parallel checkpoint,
92
+ so without this read every merged epic child worktree reached the deny below.
93
+ .PARAMETER Path
94
+ The absolute checkpoint path composed beneath the resolved worktree root.
82
95
  .OUTPUTS
83
96
  System.String or $null
84
97
  #>
85
98
  [CmdletBinding()]
86
99
  [OutputType([string])]
87
- param()
100
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
88
101
 
89
- if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
102
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
90
103
  return $null
91
104
  }
92
- return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
105
+ return (Get-Content -LiteralPath $Path -Raw)
106
+ }
107
+
108
+ function Resolve-ParallelWorktreeGateRunTarget {
109
+ <#
110
+ .SYNOPSIS
111
+ Resolve the worktree whose run checkpoint records a worktree path (issue #690 seam).
112
+ .PARAMETER Kind
113
+ epic or parallel.
114
+ .PARAMETER WorktreePath
115
+ The removal target the command names.
116
+ .OUTPUTS
117
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
118
+ #>
119
+ [CmdletBinding()]
120
+ [OutputType([pscustomobject])]
121
+ param(
122
+ [Parameter(Mandatory)][ValidateSet('epic', 'parallel')][string] $Kind,
123
+ [AllowNull()][AllowEmptyString()][string] $WorktreePath
124
+ )
125
+
126
+ return Resolve-WorktreeRunTargetByRecord -Kind $Kind -RecordField worktree_path -Value $WorktreePath -SessionRoot (Get-Location).Path
127
+ }
128
+
129
+ function Read-ParallelWorktreeGateRunCheckpoint {
130
+ <#
131
+ .SYNOPSIS
132
+ Resolve one run kind and read its checkpoint beneath the resolved root.
133
+ .PARAMETER Kind
134
+ epic or parallel.
135
+ .PARAMETER WorktreePath
136
+ The removal target the command names.
137
+ .OUTPUTS
138
+ System.Management.Automation.PSCustomObject with Target and Checkpoint ($null when
139
+ unresolved, absent, or unparseable).
140
+ #>
141
+ [CmdletBinding()]
142
+ [OutputType([pscustomobject])]
143
+ param(
144
+ [Parameter(Mandatory)][ValidateSet('epic', 'parallel')][string] $Kind,
145
+ [AllowNull()][AllowEmptyString()][string] $WorktreePath
146
+ )
147
+
148
+ $target = Resolve-ParallelWorktreeGateRunTarget -Kind $Kind -WorktreePath $WorktreePath
149
+ $checkpoint = $null
150
+ if ($target.Status -eq 'SessionRoot' -or $target.Status -eq 'OtherWorktree') {
151
+ $path = Get-WorktreeRunCheckpointPath -Kind $Kind -WorktreeRoot $target.WorktreeRoot
152
+ $raw = if ($Kind -eq 'parallel') { Get-ParallelWorktreeRemovalGateCheckpointContent -Path $path } else { Get-ParallelWorktreeRemovalGateEpicCheckpointContent -Path $path }
153
+ if (-not [string]::IsNullOrWhiteSpace($raw)) {
154
+ try {
155
+ $checkpoint = $raw | ConvertFrom-Json -ErrorAction Stop
156
+ } catch {
157
+ $checkpoint = $null
158
+ }
159
+ }
160
+ }
161
+ return [pscustomobject]@{
162
+ Target = $target
163
+ Checkpoint = $checkpoint
164
+ }
93
165
  }
94
166
 
95
167
  function Get-ParallelWorktreeRemovalCommandPath {
@@ -263,6 +335,13 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
263
335
  [string] $ToolInputRaw
264
336
  )
265
337
 
338
+ # A failed worktree-resolution import denies before any other logic (issue #690).
339
+ if ($script:ParallelWorktreeGateResolutionImportFailure) {
340
+ return Get-ParallelWorktreeGateBlockDecision -Reason (
341
+ "PARALLEL_WORKTREE_REMOVAL_BLOCKED: the worktree-resolution module '$($script:ParallelWorktreeGateResolutionImportFailure)' " +
342
+ 'failed to import, so the run checkpoint that governs this removal cannot be located; the gate fails closed.')
343
+ }
344
+
266
345
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
267
346
  if (-not $payload.IsValid) {
268
347
  return Get-ParallelWorktreeGateBlockDecision -Reason (
@@ -285,15 +364,16 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
285
364
 
286
365
  $worktreePath = Get-ParallelWorktreeRemovalCommandPath -CommandText $commandText
287
366
 
288
- $checkpointRaw = Get-ParallelWorktreeRemovalGateCheckpointContent
289
- $checkpoint = $null
290
- if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
291
- try {
292
- $checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
293
- } catch {
294
- $checkpoint = $null
367
+ # Each run checkpoint is read beneath the worktree that records this path (issue #690),
368
+ # parallel kind first; an ambiguous target denies before any allow or manifest check.
369
+ $parallelRead = Read-ParallelWorktreeGateRunCheckpoint -Kind parallel -WorktreePath $worktreePath
370
+ $epicRead = Read-ParallelWorktreeGateRunCheckpoint -Kind epic -WorktreePath $worktreePath
371
+ foreach ($read in @($parallelRead, $epicRead)) {
372
+ if ($read.Target.Status -eq 'Ambiguous') {
373
+ return Get-ParallelWorktreeGateBlockDecision -Reason "PARALLEL_WORKTREE_REMOVAL_BLOCKED: $($read.Target.ReasonCode): $($read.Target.Detail)"
295
374
  }
296
375
  }
376
+ $checkpoint = $parallelRead.Checkpoint
297
377
 
298
378
  $itemRecord = Find-ParallelWorktreeItemRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
299
379
  if (Test-ParallelWorktreeRemovalAllowed -ItemRecord $itemRecord) {
@@ -310,15 +390,7 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
310
390
  #
311
391
  # Fail-closed is preserved: an absent, unreadable, or malformed epic checkpoint parses to
312
392
  # $null, which yields no record, which is not allowed.
313
- $epicCheckpointRaw = Get-ParallelWorktreeRemovalGateEpicCheckpointContent
314
- $epicCheckpoint = $null
315
- if (-not [string]::IsNullOrWhiteSpace($epicCheckpointRaw)) {
316
- try {
317
- $epicCheckpoint = $epicCheckpointRaw | ConvertFrom-Json -ErrorAction Stop
318
- } catch {
319
- $epicCheckpoint = $null
320
- }
321
- }
393
+ $epicCheckpoint = $epicRead.Checkpoint
322
394
 
323
395
  $featureRecord = Find-ParallelWorktreeItemRecord -Checkpoint $epicCheckpoint -WorktreePath $worktreePath -RecordArrayName 'features'
324
396
  if (Test-ParallelWorktreeRemovalAllowed -ItemRecord $featureRecord) {
@@ -340,7 +412,12 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
340
412
  return Get-ParallelWorktreeGateAllowDecision
341
413
  }
342
414
 
343
- return Get-ParallelWorktreeGateBlockDecision -Reason "PARALLEL_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching parallel checkpoint items[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal."
415
+ # When neither kind resolved, the deny names the resolution reason first (issue #690).
416
+ $prefix = ''
417
+ if ($parallelRead.Target.Status -eq 'NoTarget' -and $epicRead.Target.Status -eq 'NoTarget') {
418
+ $prefix = "PARALLEL_WORKTREE_REMOVAL_BLOCKED: $($parallelRead.Target.ReasonCode): $($parallelRead.Target.Detail). "
419
+ }
420
+ return Get-ParallelWorktreeGateBlockDecision -Reason ($prefix + "PARALLEL_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching parallel checkpoint items[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal.")
344
421
  }
345
422
 
346
423
  function Invoke-ParallelWorktreeRemovalGateEntryPoint {