@danmoisan/drm-copilot-mcp 1.1.16 → 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.
- package/out/mcp-server.js +639 -27
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +12 -6
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
- package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
- package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
- package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
- package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
- package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
- package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
- package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
- package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -1
|
@@ -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
|
+
}
|
package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1
CHANGED
|
@@ -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+.
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
71
|
-
$
|
|
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
|
|
74
|
-
#
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
6
|
+
Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. Holds four groups of
|
|
7
7
|
functions:
|
|
8
8
|
|
|
9
|
-
- The
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- The
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 $
|
|
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
|
-
|
|
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 $
|
|
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 {
|
|
@@ -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),
|
|
23
|
-
# seams
|
|
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
|
-
$
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
$
|
|
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
|
|
29
|
-
|
|
30
|
-
tests/scripts/dev_tools/test_parallel_abandon_token_seam.py parses
|
|
31
|
-
time so a rename on one side without the
|
|
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()
|