@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
@@ -12,8 +12,11 @@
12
12
  docs/features/active/<token> path, mirroring
13
13
  enforce-prd-feature-before-planner.ps1's Find-PrdFeatureFolderFromPrompt
14
14
  technique (longest match wins; a .md-suffixed match uses its parent directory).
15
- 2. Read artifacts/orchestration/epic-orchestrator-state.json and locate the
16
- features[] record whose feature_folder equals the resolved basename.
15
+ 2. Resolve the worktree whose epic checkpoint records the prompt's
16
+ integration_branch: value (issue #690), deny an unresolved or ambiguous target,
17
+ then read artifacts/orchestration/epic-orchestrator-state.json beneath that
18
+ worktree and locate the features[] record whose feature_folder equals the
19
+ resolved basename.
17
20
  3. Look up that feature's depends_on list, and for every dependency, locate its own
18
21
  features[] record.
19
22
  4. Deny with reason EPIC_WAVE_BARRIER_BLOCKED unless every dependency's merge_status
@@ -26,16 +29,26 @@
26
29
  SubagentStop time.
27
30
 
28
31
  .NOTES
29
- Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
30
- through an injectable wrapper function so tests can mock the boundary without writing
31
- temporary files.
32
+ Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 (issue #690),
33
+ imported inside a guard: a failed import is recorded and the decision denies naming
34
+ the module. Filesystem reads go through an injectable wrapper function so tests can
35
+ mock the boundary without writing temporary files.
32
36
  #>
33
37
  [CmdletBinding()]
34
38
  param()
35
39
 
36
40
 
37
41
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
38
- $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
42
+
43
+ # Import guard (issue #690): a failed import denies instead of failing open.
44
+ $script:EpicWaveBarrierResolutionImportFailure = $null
45
+ try {
46
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
47
+ }
48
+ catch {
49
+ $script:EpicWaveBarrierResolutionImportFailure = 'WorktreeRunResolution.psm1'
50
+ }
51
+
39
52
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
40
53
  $script:EpicModeMarker = 'Epic mode: true'
41
54
 
@@ -44,17 +57,39 @@ function Get-EpicWaveBarrierCheckpointContent {
44
57
  .SYNOPSIS
45
58
  Read the raw JSON text of the epic checkpoint. Tests mock this function
46
59
  (read seam).
60
+ .PARAMETER Path
61
+ The absolute checkpoint path composed beneath the resolved worktree root.
47
62
  .OUTPUTS
48
63
  System.String or $null
49
64
  #>
50
65
  [CmdletBinding()]
51
66
  [OutputType([string])]
52
- param()
67
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
53
68
 
54
- if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
69
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
55
70
  return $null
56
71
  }
57
- return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
72
+ return (Get-Content -LiteralPath $Path -Raw)
73
+ }
74
+
75
+ function Resolve-EpicWaveBarrierTarget {
76
+ <#
77
+ .SYNOPSIS
78
+ Resolve the worktree whose epic checkpoint governs a kickoff (issue #690 seam).
79
+ .DESCRIPTION
80
+ Keyed on the prompt's integration_branch: value, with epic_feature_folder: as a
81
+ cross-check; never on a feature-folder path or the payload cwd.
82
+ .PARAMETER Prompt
83
+ The delegation prompt text.
84
+ .OUTPUTS
85
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
86
+ #>
87
+ [CmdletBinding()]
88
+ [OutputType([pscustomobject])]
89
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Prompt)
90
+
91
+ $signal = Find-WorktreeRunIdentitySignal -Text $Prompt
92
+ return Resolve-WorktreeEpicTarget -IntegrationBranch $signal.IntegrationBranch -EpicSlug $signal.EpicSlug -SessionRoot (Get-Location).Path
58
93
  }
59
94
 
60
95
  function Find-EpicWaveBarrierFeatureFolderFromPrompt {
@@ -246,6 +281,13 @@ function Invoke-EpicWaveBarrierDecision {
246
281
  [string] $ToolInputRaw
247
282
  )
248
283
 
284
+ # A failed worktree-resolution import denies before any other logic (issue #690).
285
+ if ($script:EpicWaveBarrierResolutionImportFailure) {
286
+ return Get-EpicWaveBarrierBlockDecision -Reason (
287
+ "EPIC_WAVE_BARRIER_BLOCKED: the worktree-resolution module '$($script:EpicWaveBarrierResolutionImportFailure)' " +
288
+ 'failed to import, so the epic checkpoint that governs this delegation cannot be located; the gate fails closed.')
289
+ }
290
+
249
291
  $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
250
292
  if (-not $envelope.IsValid) {
251
293
  return Get-EpicWaveBarrierBlockDecision -Reason (
@@ -264,12 +306,18 @@ function Invoke-EpicWaveBarrierDecision {
264
306
  return Get-EpicWaveBarrierAllowDecision
265
307
  }
266
308
 
309
+ # The epic checkpoint is located by the kickoff's integration branch (issue #690).
310
+ $target = Resolve-EpicWaveBarrierTarget -Prompt $prompt
311
+ if ($target.Status -eq 'NoTarget' -or $target.Status -eq 'Ambiguous') {
312
+ return Get-EpicWaveBarrierBlockDecision -Reason "EPIC_WAVE_BARRIER_BLOCKED: $($target.ReasonCode): $($target.Detail)"
313
+ }
314
+
267
315
  $featureFolder = Find-EpicWaveBarrierFeatureFolderFromPrompt -Prompt $prompt
268
316
  if (-not $featureFolder) {
269
317
  return Get-EpicWaveBarrierBlockDecision -Reason 'EPIC_WAVE_BARRIER_BLOCKED: an epic-mode orchestrator delegation must reference the target feature folder in the prompt so its dependency edges can be verified.'
270
318
  }
271
319
 
272
- $checkpointRaw = Get-EpicWaveBarrierCheckpointContent
320
+ $checkpointRaw = Get-EpicWaveBarrierCheckpointContent -Path (Get-WorktreeRunCheckpointPath -Kind epic -WorktreeRoot $target.WorktreeRoot)
273
321
  $checkpoint = $null
274
322
  if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
275
323
  try {
@@ -0,0 +1,144 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Checkpoint read seams and run-target resolution for the epic worktree-removal gate (issue #690).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-epic-worktree-removal-gate.ps1 after its command-parser
7
+ dot-sources. Holds:
8
+
9
+ - The import guard for WorktreeRunResolution.psm1. A failed import is recorded in
10
+ $script:EpicWorktreeGateResolutionImportFailure, and
11
+ Get-EpicWorktreeGateImportFailureDecision turns it into a deny, so a missing module
12
+ cannot make the gate exit non-zero and fail open.
13
+ - The two checkpoint read seams, relocated from the gate file with their names kept.
14
+ Each takes a mandatory absolute path composed beneath a resolved worktree root.
15
+ - The resolution seam Resolve-EpicWorktreeGateRunTarget, which locates the epic or
16
+ parallel checkpoint that records the removal target under worktree_path.
17
+ - Read-EpicWorktreeGateRunCheckpoint, which returns the target and the parsed
18
+ checkpoint beneath it ($null when the target is unresolved).
19
+
20
+ .NOTES
21
+ PowerShell 7+. Depends on Get-EpicWorktreeGateBlockDecision and
22
+ ConvertFrom-EpicWorktreeGateJson from the gate file, resolved at call time. Mirrored
23
+ byte-identically under extensions/drm-copilot/resources/claude-customizations/.
24
+ #>
25
+
26
+ # Import guard (issue #690): a failed import denies instead of failing open.
27
+ $script:EpicWorktreeGateResolutionImportFailure = $null
28
+ try {
29
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
30
+ }
31
+ catch {
32
+ $script:EpicWorktreeGateResolutionImportFailure = 'WorktreeRunResolution.psm1'
33
+ }
34
+
35
+ function Get-EpicWorktreeGateImportFailureDecision {
36
+ <#
37
+ .SYNOPSIS
38
+ Returns the deny for a failed worktree-resolution import, or $null.
39
+ .OUTPUTS
40
+ System.Collections.Specialized.OrderedDictionary, or $null when the import succeeded.
41
+ #>
42
+ [CmdletBinding()]
43
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
44
+ param()
45
+
46
+ if (-not $script:EpicWorktreeGateResolutionImportFailure) {
47
+ return $null
48
+ }
49
+ return Get-EpicWorktreeGateBlockDecision -Reason (
50
+ "EPIC_WORKTREE_REMOVAL_BLOCKED: the worktree-resolution module '$($script:EpicWorktreeGateResolutionImportFailure)' " +
51
+ 'failed to import, so the run checkpoint that governs this removal cannot be located; the gate fails closed.')
52
+ }
53
+
54
+ function Get-EpicWorktreeGateCheckpointContent {
55
+ <#
56
+ .SYNOPSIS
57
+ Read the raw JSON text of the epic checkpoint. Tests mock this function
58
+ (read seam).
59
+ .PARAMETER Path
60
+ The absolute checkpoint path composed beneath the resolved worktree root.
61
+ .OUTPUTS
62
+ System.String or $null
63
+ #>
64
+ [CmdletBinding()]
65
+ [OutputType([string])]
66
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
67
+
68
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
69
+ return $null
70
+ }
71
+ return (Get-Content -LiteralPath $Path -Raw)
72
+ }
73
+
74
+ function Get-EpicWorktreeGateParallelCheckpointContent {
75
+ <#
76
+ .SYNOPSIS
77
+ Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
78
+ this function (read seam).
79
+ .PARAMETER Path
80
+ The absolute checkpoint path composed beneath the resolved worktree root.
81
+ .OUTPUTS
82
+ System.String or $null
83
+ #>
84
+ [CmdletBinding()]
85
+ [OutputType([string])]
86
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
87
+
88
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
89
+ return $null
90
+ }
91
+ return (Get-Content -LiteralPath $Path -Raw)
92
+ }
93
+
94
+ function Resolve-EpicWorktreeGateRunTarget {
95
+ <#
96
+ .SYNOPSIS
97
+ Resolve the worktree whose run checkpoint records a worktree path (issue #690 seam).
98
+ .PARAMETER Kind
99
+ epic or parallel.
100
+ .PARAMETER WorktreePath
101
+ The removal target the command names.
102
+ .OUTPUTS
103
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
104
+ #>
105
+ [CmdletBinding()]
106
+ [OutputType([pscustomobject])]
107
+ param(
108
+ [Parameter(Mandatory)][ValidateSet('epic', 'parallel')][string] $Kind,
109
+ [AllowNull()][AllowEmptyString()][string] $WorktreePath
110
+ )
111
+
112
+ return Resolve-WorktreeRunTargetByRecord -Kind $Kind -RecordField worktree_path -Value $WorktreePath -SessionRoot (Get-Location).Path
113
+ }
114
+
115
+ function Read-EpicWorktreeGateRunCheckpoint {
116
+ <#
117
+ .SYNOPSIS
118
+ Resolve one run kind and read its checkpoint beneath the resolved root.
119
+ .PARAMETER Kind
120
+ epic or parallel.
121
+ .PARAMETER WorktreePath
122
+ The removal target the command names.
123
+ .OUTPUTS
124
+ System.Management.Automation.PSCustomObject with Target and Checkpoint ($null when unresolved).
125
+ #>
126
+ [CmdletBinding()]
127
+ [OutputType([pscustomobject])]
128
+ param(
129
+ [Parameter(Mandatory)][ValidateSet('epic', 'parallel')][string] $Kind,
130
+ [AllowNull()][AllowEmptyString()][string] $WorktreePath
131
+ )
132
+
133
+ $target = Resolve-EpicWorktreeGateRunTarget -Kind $Kind -WorktreePath $WorktreePath
134
+ $checkpoint = $null
135
+ if ($target.Status -eq 'SessionRoot' -or $target.Status -eq 'OtherWorktree') {
136
+ $path = Get-WorktreeRunCheckpointPath -Kind $Kind -WorktreeRoot $target.WorktreeRoot
137
+ $raw = if ($Kind -eq 'epic') { Get-EpicWorktreeGateCheckpointContent -Path $path } else { Get-EpicWorktreeGateParallelCheckpointContent -Path $path }
138
+ $checkpoint = ConvertFrom-EpicWorktreeGateJson -Raw $raw
139
+ }
140
+ return [pscustomobject]@{
141
+ Target = $target
142
+ Checkpoint = $checkpoint
143
+ }
144
+ }
@@ -50,10 +50,16 @@
50
50
  allow for a removal to proceed; this gate keeps the EPIC_WORKTREE_REMOVAL_BLOCKED
51
51
  prefix for both of its branches so transcript attribution stays unambiguous.
52
52
 
53
+ Issue #690: each checkpoint is read beneath the live worktree whose run checkpoint
54
+ records the removal target under worktree_path, located by
55
+ enforce-epic-worktree-removal-gate-resolution.ps1. An ambiguous target denies before
56
+ any allow; when neither kind resolves, the deny names TARGET_WORKTREE_NOT_DERIVABLE.
57
+
53
58
  .NOTES
54
- Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
55
- through an injectable wrapper function so tests can mock the boundary without writing
56
- temporary files.
59
+ Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 through the
60
+ dot-sourced resolution sibling, whose import guard denies on a failed import.
61
+ Filesystem reads go through injectable wrapper functions so tests can mock the
62
+ boundary without writing temporary files.
57
63
  #>
58
64
  [CmdletBinding()]
59
65
  param()
@@ -67,50 +73,13 @@ Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeM
67
73
  # Invoke-EpicWorktreeRemovalGateDecision and by Get-EpicWorktreeRemovalCommandPath.
68
74
  . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
69
75
  . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
70
- $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
71
- $script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
76
+ # Checkpoint read seams, the import guard, and run-target resolution (issue #690).
77
+ . (Join-Path $PSScriptRoot 'enforce-epic-worktree-removal-gate-resolution.ps1')
72
78
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
73
- # Sanctioned-removal manifest location. Recorded here beside the two checkpoint paths
74
- # so every hook-read document this gate consults is named in one place; the module
75
- # owns the read itself.
79
+ # Sanctioned-removal manifest location. Recorded here so every hook-read document this
80
+ # gate consults is named; the module owns the read itself.
76
81
  $script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
77
82
 
78
- function Get-EpicWorktreeGateCheckpointContent {
79
- <#
80
- .SYNOPSIS
81
- Read the raw JSON text of the epic checkpoint. Tests mock this function
82
- (read seam).
83
- .OUTPUTS
84
- System.String or $null
85
- #>
86
- [CmdletBinding()]
87
- [OutputType([string])]
88
- param()
89
-
90
- if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
91
- return $null
92
- }
93
- return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
94
- }
95
-
96
- function Get-EpicWorktreeGateParallelCheckpointContent {
97
- <#
98
- .SYNOPSIS
99
- Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
100
- this function (read seam).
101
- .OUTPUTS
102
- System.String or $null
103
- #>
104
- [CmdletBinding()]
105
- [OutputType([string])]
106
- param()
107
-
108
- if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
109
- return $null
110
- }
111
- return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
112
- }
113
-
114
83
  function ConvertFrom-EpicWorktreeGateJson {
115
84
  <#
116
85
  .SYNOPSIS
@@ -361,6 +330,12 @@ function Invoke-EpicWorktreeRemovalGateDecision {
361
330
  [string] $ToolInputRaw
362
331
  )
363
332
 
333
+ # A failed worktree-resolution import denies before any other logic (issue #690).
334
+ $importFailure = Get-EpicWorktreeGateImportFailureDecision
335
+ if ($null -ne $importFailure) {
336
+ return $importFailure
337
+ }
338
+
364
339
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
365
340
  if (-not $payload.IsValid) {
366
341
  return Get-EpicWorktreeGateBlockDecision -Reason (
@@ -383,14 +358,23 @@ function Invoke-EpicWorktreeRemovalGateDecision {
383
358
 
384
359
  $worktreePath = Get-EpicWorktreeRemovalCommandPath -CommandText $commandText
385
360
 
386
- $checkpoint = ConvertFrom-EpicWorktreeGateJson -Raw (Get-EpicWorktreeGateCheckpointContent)
361
+ # Each run checkpoint is read beneath the worktree that records this path (issue #690);
362
+ # an ambiguous target denies before any allow or manifest evaluation.
363
+ $epicRead = Read-EpicWorktreeGateRunCheckpoint -Kind epic -WorktreePath $worktreePath
364
+ $parallelRead = Read-EpicWorktreeGateRunCheckpoint -Kind parallel -WorktreePath $worktreePath
365
+ foreach ($read in @($epicRead, $parallelRead)) {
366
+ if ($read.Target.Status -eq 'Ambiguous') {
367
+ return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: $($read.Target.ReasonCode): $($read.Target.Detail)"
368
+ }
369
+ }
370
+ $checkpoint = $epicRead.Checkpoint
387
371
 
388
372
  $featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
389
373
  if (Test-EpicWorktreeRemovalAllowed -FeatureRecord $featureRecord) {
390
374
  return Get-EpicWorktreeGateAllowDecision
391
375
  }
392
376
 
393
- $parallelCheckpoint = ConvertFrom-EpicWorktreeGateJson -Raw (Get-EpicWorktreeGateParallelCheckpointContent)
377
+ $parallelCheckpoint = $parallelRead.Checkpoint
394
378
  if (Test-ParallelCheckpointAllowsWorktreeRemoval -Checkpoint $parallelCheckpoint -WorktreePath $worktreePath) {
395
379
  return Get-EpicWorktreeGateAllowDecision
396
380
  }
@@ -411,7 +395,12 @@ function Invoke-EpicWorktreeRemovalGateDecision {
411
395
  return Get-EpicWorktreeGateAllowDecision
412
396
  }
413
397
 
414
- return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal."
398
+ # When neither kind resolved, the deny names the resolution reason first (issue #690).
399
+ $prefix = ''
400
+ if ($epicRead.Target.Status -eq 'NoTarget' -and $parallelRead.Target.Status -eq 'NoTarget') {
401
+ $prefix = "EPIC_WORKTREE_REMOVAL_BLOCKED: $($epicRead.Target.ReasonCode): $($epicRead.Target.Detail). "
402
+ }
403
+ return Get-EpicWorktreeGateBlockDecision -Reason ($prefix + "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal.")
415
404
  }
416
405
 
417
406
  function Invoke-EpicWorktreeRemovalGateEntryPoint {
@@ -1,60 +1,212 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Epic-scope command and path legs of the preimplementation gate (issue #663).
3
+ Epic-scope legs, checkpoint read seams, and target resolution of the preimplementation gate.
4
4
 
5
5
  .DESCRIPTION
6
- Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. Holds two groups of
6
+ Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. Holds four groups of
7
7
  functions:
8
8
 
9
- - The two per-mode read seams of issue #554 (Get-EpicCheckpointContent and
10
- Get-ParallelCheckpointContent), relocated verbatim from the gate file by issue #663
11
- so the gate stays inside the 500-line cap. Their names and behaviour are unchanged,
12
- so tests that mock or shadow them by name are unaffected.
13
- - The epic-scope decision for the command and path legs. A staging command or a
14
- Write/Edit call is epic scope when the effective worktree's HEAD (the -C selector
15
- worktree when present, otherwise the session root) equals the integration_branch of
16
- artifacts/orchestration/epic-orchestrator-state.json. In epic scope the call is
17
- decided by the epic command-leg readiness predicate, and under decision D2 an
18
- implementation-classified operand is allowed only while a merge is in progress in
19
- that worktree. Outside epic scope the decision function returns $null and the gate's
20
- single-feature path runs unchanged.
9
+ - The import guard for the worktree-resolution modules (issue #690). A failed import
10
+ is recorded in $script:OrchestrationGateResolutionImportFailure, and
11
+ Get-OrchestrationGateImportFailureDecision turns that record into a deny, so a
12
+ missing module cannot make the gate exit non-zero and fail open.
13
+ - The three checkpoint read seams, Get-CheckpointContent, Get-EpicCheckpointContent,
14
+ and Get-ParallelCheckpointContent. Each takes a mandatory absolute path, so no
15
+ read binds to the calling process's directory. Get-CheckpointContent and
16
+ Get-OrchestrationModeDenyReason were relocated here from the gate file by issue #690
17
+ to keep the gate inside the 500-line cap.
18
+ - The target resolution of issue #690. Resolve-OrchestrationGateTarget selects the
19
+ worktree whose checkpoint governs the call from portable identity only: the
20
+ integration_branch: value for an epic delegation, the parallel_slug: value for a
21
+ parallel delegation, the file_path or git -C operand for the path and command legs,
22
+ and the canonical issue-number line and branch: label for a single-feature
23
+ delegation. Read-OrchestrationGateCheckpoint reads the checkpoint beneath the
24
+ resolved root, or returns the deny for an unresolved target.
25
+ - The epic-scope decision for the command and path legs (issue #663). A staging
26
+ command or a Write/Edit call is epic scope when the effective worktree's HEAD equals
27
+ the integration branch of the epic checkpoint that Resolve-EpicScopeCheckpoint
28
+ locates. Outside epic scope the decision function returns $null and the gate's
29
+ single-feature path runs.
21
30
 
22
31
  .NOTES
23
32
  PowerShell 7+. Depends on functions the gate defines or dot-sources before any call:
24
33
  Split-OrchestrationCommandLine and ConvertTo-OrchestrationCommandToken (helpers file),
25
34
  Get-OrchestrationDelegationCheckpointPath (modes file), and the gate's allow and block
26
- decision constructors. Mirrored byte-identically under
35
+ decision constructors. Imports WorktreeItemResolution.psm1 and WorktreeRunResolution.psm1
36
+ before EpicScopeResolution.psm1, so the nested import inside EpicScopeResolution reuses
37
+ the same module instance. Mirrored byte-identically under
27
38
  extensions/drm-copilot/resources/claude-customizations/.
28
39
  #>
29
40
 
30
- Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
41
+ # Import guard (issue #690). Each worktree-resolution import is attempted in order; the
42
+ # first failure is recorded by file name and the decision function denies on it.
43
+ $script:OrchestrationGateResolutionImportFailure = $null
44
+ foreach ($resolutionModule in @('WorktreeItemResolution.psm1', 'WorktreeRunResolution.psm1', 'EpicScopeResolution.psm1')) {
45
+ try {
46
+ Import-Module (Join-Path $PSScriptRoot "../lib/worktree-resolution/$resolutionModule") -Force -ErrorAction Stop
47
+ }
48
+ catch {
49
+ if (-not $script:OrchestrationGateResolutionImportFailure) { $script:OrchestrationGateResolutionImportFailure = $resolutionModule }
50
+ }
51
+ }
31
52
  Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
32
53
 
33
- # The two per-mode read seams (issue #554). Each takes its path from the fixed mode
34
- # table and never from a delegation's own text; an absent file returns an empty
35
- # string, which the readiness predicate then treats as a deny.
54
+ function Get-OrchestrationGateImportFailureDecision {
55
+ <#
56
+ .SYNOPSIS
57
+ Returns the deny for a failed worktree-resolution import, or $null.
58
+ .OUTPUTS
59
+ System.Collections.Specialized.OrderedDictionary, or $null when every import succeeded.
60
+ #>
61
+ [CmdletBinding()]
62
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
63
+ param()
64
+
65
+ if (-not $script:OrchestrationGateResolutionImportFailure) {
66
+ return $null
67
+ }
68
+ return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
69
+ "PREIMPLEMENTATION_GATE_BLOCKED: the worktree-resolution module '$($script:OrchestrationGateResolutionImportFailure)' " +
70
+ 'failed to import, so the checkpoint that governs this call cannot be located; the gate fails closed.')
71
+ }
72
+
73
+ # The three checkpoint read seams. Each takes the absolute path composed beneath a
74
+ # resolved worktree root; an absent file returns an empty string, which the readiness
75
+ # predicate then treats as a deny.
76
+ function Get-CheckpointContent {
77
+ [CmdletBinding()]
78
+ [OutputType([string])]
79
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
80
+
81
+ if (-not (Test-Path -LiteralPath $Path)) {
82
+ return ''
83
+ }
84
+ return Get-Content -Raw -LiteralPath $Path
85
+ }
86
+
36
87
  function Get-EpicCheckpointContent {
37
88
  [CmdletBinding()]
38
89
  [OutputType([string])]
39
- param()
90
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
40
91
 
41
- $path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
42
- if (-not (Test-Path -LiteralPath $path)) {
92
+ if (-not (Test-Path -LiteralPath $Path)) {
43
93
  return ''
44
94
  }
45
- return Get-Content -Raw -LiteralPath $path
95
+ return Get-Content -Raw -LiteralPath $Path
46
96
  }
47
97
 
48
98
  function Get-ParallelCheckpointContent {
49
99
  [CmdletBinding()]
50
100
  [OutputType([string])]
51
- param()
101
+ param([Parameter(Mandatory)][ValidatePattern('^([A-Za-z]:[\\/]|/)')][string] $Path)
52
102
 
53
- $path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
54
- if (-not (Test-Path -LiteralPath $path)) {
103
+ if (-not (Test-Path -LiteralPath $Path)) {
55
104
  return ''
56
105
  }
57
- return Get-Content -Raw -LiteralPath $path
106
+ return Get-Content -Raw -LiteralPath $Path
107
+ }
108
+
109
+ # Builds a mode-specific deny reason naming the checkpoint actually consulted and the
110
+ # predicate that failed, behind the unchanged PREIMPLEMENTATION_GATE_BLOCKED prefix
111
+ # that downstream reason-matching reads. A resolved absolute path is named when known.
112
+ function Get-OrchestrationModeDenyReason {
113
+ [CmdletBinding()]
114
+ [OutputType([string])]
115
+ param(
116
+ [Parameter(Mandatory)][string] $Mode,
117
+ [Parameter(Mandatory)][string] $Failure,
118
+ [AllowNull()][AllowEmptyString()][string] $CheckpointPath = ''
119
+ )
120
+
121
+ $path = if ($CheckpointPath) { $CheckpointPath } else { Get-OrchestrationDelegationCheckpointPath -Mode $Mode }
122
+ return ("PREIMPLEMENTATION_GATE_BLOCKED: this $Mode-mode delegation was evaluated against " +
123
+ "$path, and the failed readiness predicate is '$Failure'. Implementation operations " +
124
+ 'require that checkpoint to satisfy every readiness predicate before implementation begins.')
125
+ }
126
+
127
+ function Resolve-OrchestrationGateTarget {
128
+ <#
129
+ .SYNOPSIS
130
+ Resolves the worktree whose checkpoint governs a gated call (issue #690 seam).
131
+ .DESCRIPTION
132
+ An epic delegation is keyed on integration_branch: (with epic_feature_folder: as a
133
+ cross-check), a parallel delegation on parallel_slug:, a path leg on its file_path
134
+ operand, a command leg on its git -C selector (none means the session root), and a
135
+ single-feature delegation on its canonical issue-number line and branch: label.
136
+ .OUTPUTS
137
+ System.Management.Automation.PSCustomObject (the worktree-resolution target result).
138
+ #>
139
+ [CmdletBinding()]
140
+ [OutputType([pscustomobject])]
141
+ param(
142
+ [Parameter(Mandatory)][string] $Mode,
143
+ [AllowNull()][AllowEmptyString()][string] $Prompt = '',
144
+ [AllowNull()][AllowEmptyString()][string] $FilePath = '',
145
+ [AllowNull()][AllowEmptyString()][string] $Command = ''
146
+ )
147
+
148
+ $sessionRoot = (Get-Location).Path
149
+ if ($Mode -eq 'epic' -or $Mode -eq 'parallel') {
150
+ $signal = Find-WorktreeRunIdentitySignal -Text $Prompt
151
+ if ($Mode -eq 'epic') {
152
+ return Resolve-WorktreeEpicTarget -IntegrationBranch $signal.IntegrationBranch -EpicSlug $signal.EpicSlug -SessionRoot $sessionRoot
153
+ }
154
+ return Resolve-WorktreeParallelTarget -ParallelSlug $signal.ParallelSlug -SessionRoot $sessionRoot
155
+ }
156
+ if ($FilePath) {
157
+ return Resolve-WorktreeOperandTarget -Path $FilePath -SessionRoot $sessionRoot
158
+ }
159
+ if ($Command) {
160
+ return Resolve-WorktreeOperandTarget -Path (Get-OrchestrationEpicScopeSelector -Command $Command) -SessionRoot $sessionRoot
161
+ }
162
+ return Resolve-WorktreeItemTarget -Text $Prompt -SessionRoot $sessionRoot
163
+ }
164
+
165
+ function Read-OrchestrationGateCheckpoint {
166
+ <#
167
+ .SYNOPSIS
168
+ Reads the checkpoint beneath the resolved target worktree, or returns the deny reason.
169
+ .DESCRIPTION
170
+ Resolved is $false, with DenyReason set, when the target is NoTarget or Ambiguous.
171
+ Otherwise Raw holds the checkpoint text (empty when absent) and Path its absolute
172
+ path. Only Status, WorktreeRoot, ReasonCode, and Detail are read from the target.
173
+ .OUTPUTS
174
+ System.Management.Automation.PSCustomObject
175
+ #>
176
+ [CmdletBinding()]
177
+ [OutputType([pscustomobject])]
178
+ param(
179
+ [Parameter(Mandatory)][string] $Mode,
180
+ [AllowNull()][AllowEmptyString()][string] $Prompt = '',
181
+ [AllowNull()][AllowEmptyString()][string] $FilePath = '',
182
+ [AllowNull()][AllowEmptyString()][string] $Command = ''
183
+ )
184
+
185
+ $target = Resolve-OrchestrationGateTarget -Mode $Mode -Prompt $Prompt -FilePath $FilePath -Command $Command
186
+ if ($target.Status -eq 'NoTarget' -or $target.Status -eq 'Ambiguous') {
187
+ return [pscustomobject]@{
188
+ Resolved = $false
189
+ Raw = ''
190
+ Path = $null
191
+ DenyReason = ("PREIMPLEMENTATION_GATE_BLOCKED: $($target.ReasonCode): the target worktree of this $Mode call " +
192
+ "could not be resolved: $($target.Detail). Implementation operations require an identifiable target " +
193
+ 'worktree whose checkpoint is ready.')
194
+ }
195
+ }
196
+
197
+ $kind = if ($Mode -eq 'epic' -or $Mode -eq 'parallel') { $Mode } else { 'item' }
198
+ $path = Get-WorktreeRunCheckpointPath -Kind $kind -WorktreeRoot $target.WorktreeRoot
199
+ $raw = switch ($kind) {
200
+ 'epic' { Get-EpicCheckpointContent -Path $path }
201
+ 'parallel' { Get-ParallelCheckpointContent -Path $path }
202
+ default { Get-CheckpointContent -Path $path }
203
+ }
204
+ return [pscustomobject]@{
205
+ Resolved = $true
206
+ Raw = [string]$raw
207
+ Path = $path
208
+ DenyReason = $null
209
+ }
58
210
  }
59
211
 
60
212
  function Get-OrchestrationEpicScopeSelector {