@danmoisan/drm-copilot-mcp 1.1.10 → 1.1.12
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 +3517 -1104
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
- package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/config/blast-radius.json +7 -0
- package/resources/claude-customizations/pack-manifests/core.json +16 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
- package/resources/config/orchestration-handoff-registry.json +138 -0
- package/resources/config/orchestration-handoff.schema.json +472 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -0
|
@@ -27,15 +27,34 @@ $script:OrchestrationBookkeepingTrees = @(
|
|
|
27
27
|
'artifacts/orchestration/'
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
# Characters that make a command line statically unresolvable
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
30
|
+
# Characters that make a command line statically unresolvable (D4 row 12, as narrowed by
|
|
31
|
+
# issue #663). Interpolation characters are unresolvable in every quote state, because a
|
|
32
|
+
# double-quoted span still expands them. Redirection characters are unresolvable only
|
|
33
|
+
# outside a quoted span: inside single or double quotes the shell treats `<` and `>` as
|
|
34
|
+
# literal text, so a quoted Co-Authored-By trailer is not a redirection.
|
|
35
|
+
$script:InterpolationCommandCharacters = [char[]]@('$', '`')
|
|
36
|
+
$script:RedirectionCommandCharacters = [char[]]@('>', '<')
|
|
34
37
|
|
|
35
38
|
# Wildcards that make an operand a glob (D4 row 15). Only the literal prefix before the
|
|
36
39
|
# first of these is prefix-tested.
|
|
37
40
|
$script:PathspecWildcardCharacters = [char[]]@('*', '?', '[')
|
|
38
41
|
|
|
42
|
+
# The single repository selector modelled between the command name and the subcommand: D4
|
|
43
|
+
# row 14 as narrowed by issue #671, the Lexical Absolute-Canonical Selector (LACS) rule in
|
|
44
|
+
# docs/features/active/2026-09-13-preimplementation-gate-worktree-selector-671/spec.md.
|
|
45
|
+
# Compared case-sensitively; no other relocating spelling is modelled.
|
|
46
|
+
$script:OrchestrationSelectorOptionName = '-C'
|
|
47
|
+
|
|
48
|
+
# Accepted widening (issue #671): a selector naming a nested subdirectory of a worktree
|
|
49
|
+
# passes L1 through L8 lexically yet relocates what a relative operand denotes, so
|
|
50
|
+
# `docs/features/active/X` under `-C <root>/tests/fixtures/resolve_execute_plan_prompt`
|
|
51
|
+
# stages `tests/fixtures/resolve_execute_plan_prompt/docs/features/active/X`. Measured
|
|
52
|
+
# exposure in this repository is seven Markdown test fixtures under the
|
|
53
|
+
# resolve_execute_plan_prompt fixture tree, all of which the gate's file_path leg already
|
|
54
|
+
# classifies as non-implementation. This module stays pure string logic; the epic's F1
|
|
55
|
+
# resolution module composes upstream to close the escape later without a schema change,
|
|
56
|
+
# the same posture D4 row 16 takes toward issue #516.
|
|
57
|
+
|
|
39
58
|
function Split-OrchestrationCommandLine {
|
|
40
59
|
<#
|
|
41
60
|
.SYNOPSIS
|
|
@@ -88,6 +107,59 @@ function Split-OrchestrationCommandLine {
|
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
109
|
|
|
110
|
+
function Test-OrchestrationCommandTextUnresolvable {
|
|
111
|
+
<#
|
|
112
|
+
.SYNOPSIS
|
|
113
|
+
Reports whether a command line carries a statically unresolvable character.
|
|
114
|
+
.DESCRIPTION
|
|
115
|
+
Realizes D4 row 12 as narrowed by issue #663. Quote state is tracked with the same
|
|
116
|
+
state machine as Split-OrchestrationCommandLine. An interpolation character (`$` or
|
|
117
|
+
backtick) answers true in any quote state; a redirection character (`<` or `>`)
|
|
118
|
+
answers true only outside a quoted span. Backslash escapes are not modelled, so a
|
|
119
|
+
backslash before any quote character, or anywhere inside a double-quoted span,
|
|
120
|
+
answers true: the shell may end the span where this scan does not (fail closed).
|
|
121
|
+
.PARAMETER CommandText
|
|
122
|
+
The full command line as the shell would receive it.
|
|
123
|
+
.OUTPUTS
|
|
124
|
+
System.Boolean
|
|
125
|
+
#>
|
|
126
|
+
[CmdletBinding()]
|
|
127
|
+
[OutputType([bool])]
|
|
128
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
129
|
+
|
|
130
|
+
# An escaped quote moves a span boundary the scan cannot model (issue #663 remediation CR-1).
|
|
131
|
+
if ($CommandText.Contains('\"') -or $CommandText.Contains("\'")) {
|
|
132
|
+
return $true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$openQuote = [char]0
|
|
136
|
+
|
|
137
|
+
# Scan every character once, tracking whether it sits inside a quoted span so that
|
|
138
|
+
# redirection characters are judged only where the shell would honour them.
|
|
139
|
+
foreach ($character in $CommandText.ToCharArray()) {
|
|
140
|
+
if ($script:InterpolationCommandCharacters -contains $character) {
|
|
141
|
+
return $true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Decide by quote state: inside a span only the closing quote matters, and any
|
|
145
|
+
# backslash inside a double-quoted span is an unmodelled escape; outside a span a
|
|
146
|
+
# quote opens one and a redirection character is unresolvable.
|
|
147
|
+
if ($openQuote -ne [char]0) {
|
|
148
|
+
if ($openQuote -eq '"' -and $character -eq '\') {
|
|
149
|
+
return $true
|
|
150
|
+
}
|
|
151
|
+
if ($character -eq $openQuote) {
|
|
152
|
+
$openQuote = [char]0
|
|
153
|
+
}
|
|
154
|
+
} elseif ($character -eq '"' -or $character -eq "'") {
|
|
155
|
+
$openQuote = $character
|
|
156
|
+
} elseif ($script:RedirectionCommandCharacters -contains $character) {
|
|
157
|
+
return $true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return $false
|
|
161
|
+
}
|
|
162
|
+
|
|
91
163
|
function ConvertTo-OrchestrationCommandToken {
|
|
92
164
|
<#
|
|
93
165
|
.SYNOPSIS
|
|
@@ -203,6 +275,67 @@ function Test-ExemptOrchestrationOperand {
|
|
|
203
275
|
return $false
|
|
204
276
|
}
|
|
205
277
|
|
|
278
|
+
function Test-ExemptOrchestrationSelector {
|
|
279
|
+
<#
|
|
280
|
+
.SYNOPSIS
|
|
281
|
+
Tests the repository selector of one tokenized segment against LACS L1 through L8.
|
|
282
|
+
.DESCRIPTION
|
|
283
|
+
Realizes D4 row 14 as narrowed by issue #671 (the Lexical Absolute-Canonical Selector
|
|
284
|
+
rule in docs/features/active/2026-09-13-preimplementation-gate-worktree-selector-671/spec.md).
|
|
285
|
+
Returns true only when the segment reads `git -C <value> <subcommand> ...` and the
|
|
286
|
+
value is decidable from the command text alone: exactly one case-sensitive `-C`, at
|
|
287
|
+
index 1 (L1, L2); a value at index 2 and the subcommand at index 3 (L3); a
|
|
288
|
+
drive-lettered or rooted, non-UNC value after separator normalization (L4); no `.` or
|
|
289
|
+
`..` segment (L5); no wildcard (L6); no colon other than the drive colon (L7); and a
|
|
290
|
+
non-empty value (L8). Each rejection writes a diagnostic token through Write-Debug.
|
|
291
|
+
The tokens are not contractual and change no decision.
|
|
292
|
+
L3 checks only that a non-option token follows the value; the caller
|
|
293
|
+
Test-ExemptOrchestrationSegmentToken rejects any subcommand other than add or commit.
|
|
294
|
+
.OUTPUTS
|
|
295
|
+
System.Boolean
|
|
296
|
+
#>
|
|
297
|
+
[CmdletBinding()]
|
|
298
|
+
[OutputType([bool])]
|
|
299
|
+
param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token)
|
|
300
|
+
|
|
301
|
+
$selector = $script:OrchestrationSelectorOptionName
|
|
302
|
+
if ($Token.Count -lt 2 -or $Token[1] -cne $selector) {
|
|
303
|
+
Write-Debug 'PREIMPL_SELECTOR_UNMODELLED_OPTION: the token after the command name is not the -C selector.'
|
|
304
|
+
return $false
|
|
305
|
+
}
|
|
306
|
+
if (@($Token | Where-Object { $_ -ceq $selector }).Count -gt 1) {
|
|
307
|
+
Write-Debug 'PREIMPL_SELECTOR_REPEATED: the segment carries more than one -C selector.'
|
|
308
|
+
return $false
|
|
309
|
+
}
|
|
310
|
+
if ($Token.Count -lt 4 -or $Token[3].StartsWith('-')) {
|
|
311
|
+
Write-Debug 'PREIMPL_SELECTOR_MALFORMED: no subcommand immediately follows the selector value.'
|
|
312
|
+
return $false
|
|
313
|
+
}
|
|
314
|
+
$value = $Token[2]
|
|
315
|
+
if (-not $value) {
|
|
316
|
+
Write-Debug 'PREIMPL_SELECTOR_MALFORMED: the selector value is empty.'
|
|
317
|
+
return $false
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
$normalized = $value -replace '\\', '/'
|
|
321
|
+
$isDriveRooted = $normalized -match '^[A-Za-z]:/'
|
|
322
|
+
if ($normalized.StartsWith('//') -or -not ($isDriveRooted -or $normalized.StartsWith('/'))) {
|
|
323
|
+
Write-Debug 'PREIMPL_SELECTOR_NOT_ROOTED: the selector value is relative or UNC.'
|
|
324
|
+
return $false
|
|
325
|
+
}
|
|
326
|
+
$segments = $normalized -split '/'
|
|
327
|
+
if ($segments -contains '..' -or $segments -contains '.') {
|
|
328
|
+
Write-Debug 'PREIMPL_SELECTOR_TRAVERSAL: the selector value carries a dot segment.'
|
|
329
|
+
return $false
|
|
330
|
+
}
|
|
331
|
+
$afterDrive = if ($isDriveRooted) { $normalized.Substring(2) } else { $normalized }
|
|
332
|
+
if ($normalized.IndexOfAny($script:PathspecWildcardCharacters) -ge 0 -or $afterDrive.Contains(':')) {
|
|
333
|
+
Write-Debug 'PREIMPL_SELECTOR_NOT_LITERAL: the selector value carries a wildcard or a stray colon.'
|
|
334
|
+
return $false
|
|
335
|
+
}
|
|
336
|
+
return $true
|
|
337
|
+
}
|
|
338
|
+
|
|
206
339
|
function Test-ExemptOrchestrationSegmentToken {
|
|
207
340
|
<#
|
|
208
341
|
.SYNOPSIS
|
|
@@ -218,18 +351,27 @@ function Test-ExemptOrchestrationSegmentToken {
|
|
|
218
351
|
#>
|
|
219
352
|
[CmdletBinding()]
|
|
220
353
|
[OutputType([bool])]
|
|
221
|
-
param([Parameter(Mandatory)][AllowEmptyCollection()][string[]] $Token)
|
|
354
|
+
param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token)
|
|
222
355
|
|
|
223
356
|
if ($Token.Count -lt 2) {
|
|
224
357
|
return $false
|
|
225
358
|
}
|
|
226
359
|
|
|
227
|
-
# Row 14: the command name leads and the subcommand follows
|
|
228
|
-
#
|
|
229
|
-
#
|
|
360
|
+
# Row 14, narrowed by issue #671: the command name leads and the subcommand follows it,
|
|
361
|
+
# either immediately or after one lexically absolute `-C <value>` selector (LACS L1-L8).
|
|
362
|
+
# Anything else in between - an unmodelled or unresolvable relocating option, or an
|
|
363
|
+
# env-style prefix - moves the pathspec base undecidably and is rejected here.
|
|
230
364
|
if ($Token[0] -cne 'git') {
|
|
231
365
|
return $false
|
|
232
366
|
}
|
|
367
|
+
if ($Token[1] -cne 'add' -and $Token[1] -cne 'commit') {
|
|
368
|
+
if (-not (Test-ExemptOrchestrationSelector -Token $Token)) {
|
|
369
|
+
return $false
|
|
370
|
+
}
|
|
371
|
+
# Drop the accepted selector and its value so the subcommand is again at index 1
|
|
372
|
+
# and operand collection below starts at index 2 exactly as before.
|
|
373
|
+
$Token = @($Token[0]) + @($Token[3..($Token.Count - 1)])
|
|
374
|
+
}
|
|
233
375
|
$subcommand = $Token[1]
|
|
234
376
|
if ($subcommand -cne 'add' -and $subcommand -cne 'commit') {
|
|
235
377
|
return $false
|
|
@@ -322,9 +464,9 @@ function Test-ExemptOrchestrationStagingCommand {
|
|
|
322
464
|
return $false
|
|
323
465
|
}
|
|
324
466
|
|
|
325
|
-
# Row 12: interpolation
|
|
326
|
-
#
|
|
327
|
-
if (
|
|
467
|
+
# Row 12: interpolation anywhere, redirection outside quotes, and unmodelled backslash
|
|
468
|
+
# escapes are not statically resolvable, so the operand list cannot be trusted.
|
|
469
|
+
if (Test-OrchestrationCommandTextUnresolvable -CommandText $CommandText) {
|
|
328
470
|
return $false
|
|
329
471
|
}
|
|
330
472
|
|
|
@@ -339,11 +481,17 @@ function Test-ExemptOrchestrationStagingCommand {
|
|
|
339
481
|
return $false
|
|
340
482
|
}
|
|
341
483
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
484
|
+
# Fail closed (issue #671): an error raised while classifying any segment is a parse
|
|
485
|
+
# ambiguity, so it answers false instead of letting the caller continue past it.
|
|
486
|
+
try {
|
|
487
|
+
foreach ($segment in $segments) {
|
|
488
|
+
$tokens = @(ConvertTo-OrchestrationCommandToken -Segment $segment)
|
|
489
|
+
if (-not (Test-ExemptOrchestrationSegmentToken -Token $tokens)) {
|
|
490
|
+
return $false
|
|
491
|
+
}
|
|
346
492
|
}
|
|
493
|
+
} catch {
|
|
494
|
+
return $false
|
|
347
495
|
}
|
|
348
496
|
return $true
|
|
349
497
|
}
|
|
@@ -19,6 +19,14 @@ 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.
|
|
24
|
+
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-epic-scope.ps1')
|
|
25
|
+
|
|
26
|
+
# Shared command-line parser (issue #545): per-segment scan text and structural matching.
|
|
27
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
28
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
29
|
+
|
|
22
30
|
# The readiness checkpoint this gate reads and names in its block message.
|
|
23
31
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
24
32
|
|
|
@@ -132,9 +140,11 @@ function Test-ImplementationCommand {
|
|
|
132
140
|
'(^|\s)npx\s+(prettier|eslint|tsc|jest)\b',
|
|
133
141
|
'(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
|
|
134
142
|
)
|
|
143
|
+
$scanText = @(Read-CommandLineSegment -CommandText $normalizedCommand).ScanText
|
|
144
|
+
$isStaging = @('add', 'commit').Where({ Test-CommandLineInvocation -CommandText $normalizedCommand -CommandWord 'git' -SubcommandPath @($_) }).Count -gt 0
|
|
135
145
|
|
|
136
146
|
for ($index = 0; $index -lt $implementationCommandPatterns.Count; $index++) {
|
|
137
|
-
if ($
|
|
147
|
+
if (-not (($scanText | Where-Object { $_ -match $implementationCommandPatterns[$index] }) -or ($index -eq 0 -and $isStaging))) {
|
|
138
148
|
continue
|
|
139
149
|
}
|
|
140
150
|
# Allow-side only (issue #539). Index 0 is the git staging trigger, whose pattern
|
|
@@ -255,33 +265,6 @@ function Get-CheckpointContent {
|
|
|
255
265
|
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
256
266
|
}
|
|
257
267
|
|
|
258
|
-
# The two per-mode read seams (issue #554). Each takes its path from the fixed mode
|
|
259
|
-
# table and never from a delegation's own text; an absent file returns an empty
|
|
260
|
-
# string, which the readiness predicate then treats as a deny.
|
|
261
|
-
function Get-EpicCheckpointContent {
|
|
262
|
-
[CmdletBinding()]
|
|
263
|
-
[OutputType([string])]
|
|
264
|
-
param()
|
|
265
|
-
|
|
266
|
-
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
|
|
267
|
-
if (-not (Test-Path -LiteralPath $path)) {
|
|
268
|
-
return ''
|
|
269
|
-
}
|
|
270
|
-
return Get-Content -Raw -LiteralPath $path
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function Get-ParallelCheckpointContent {
|
|
274
|
-
[CmdletBinding()]
|
|
275
|
-
[OutputType([string])]
|
|
276
|
-
param()
|
|
277
|
-
|
|
278
|
-
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
|
|
279
|
-
if (-not (Test-Path -LiteralPath $path)) {
|
|
280
|
-
return ''
|
|
281
|
-
}
|
|
282
|
-
return Get-Content -Raw -LiteralPath $path
|
|
283
|
-
}
|
|
284
|
-
|
|
285
268
|
function Get-OrchestrationPreimplementationGateAllowDecision {
|
|
286
269
|
[CmdletBinding()]
|
|
287
270
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -371,6 +354,7 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
371
354
|
# default wording they have today.
|
|
372
355
|
$mode = $script:OrchestrationDelegationDefaultMode
|
|
373
356
|
$prompt = ''
|
|
357
|
+
$command = ''
|
|
374
358
|
$filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
|
|
375
359
|
if ($filePath) {
|
|
376
360
|
$normalized = ([string]$filePath) -replace '\\', '/'
|
|
@@ -400,6 +384,15 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
400
384
|
Get-OrchestrationModeDenyReason -Mode $mode -Failure 'declared-checkpoint-path')
|
|
401
385
|
}
|
|
402
386
|
|
|
387
|
+
# The command and path legs consult the epic checkpoint first (issue #663). A call that
|
|
388
|
+
# is not epic scope yields $null and continues to the unchanged single-feature path.
|
|
389
|
+
if ($filePath -or $command) {
|
|
390
|
+
$epicDecision = Get-OrchestrationEpicScopeDecision -Command $command -FilePath $filePath
|
|
391
|
+
if ($null -ne $epicDecision) {
|
|
392
|
+
return $epicDecision
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
403
396
|
if ($mode -eq 'epic' -or $mode -eq 'parallel') {
|
|
404
397
|
$isEpic = ($mode -eq 'epic')
|
|
405
398
|
$injected = if ($isEpic) { 'EpicCheckpointRaw' } else { 'ParallelCheckpointRaw' }
|
|
@@ -44,6 +44,13 @@ $script:AbandonConfirmToken = '--confirm-abandon'
|
|
|
44
44
|
# Literal prefix on the deny reason, so the reason code is greppable in transcripts.
|
|
45
45
|
$script:AbandonBlockedReasonCode = 'PARALLEL_ABANDON_BLOCKED'
|
|
46
46
|
|
|
47
|
+
# Shared command-line parser (issue #545). Dot-sourced BELOW the two token assignments
|
|
48
|
+
# rather than above them, deliberately: the assignments keep their existing line numbers,
|
|
49
|
+
# so the diff shows no change at either line. Both are still declared before any function
|
|
50
|
+
# body runs, because the file executes top to bottom before any of its functions is called.
|
|
51
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
52
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
53
|
+
|
|
47
54
|
function Get-ParallelAbandonGateToolInput {
|
|
48
55
|
<#
|
|
49
56
|
.SYNOPSIS
|
|
@@ -85,10 +92,77 @@ function Get-ParallelAbandonNormalizedCommand {
|
|
|
85
92
|
return ($CommandText -replace '\s+', ' ').Trim()
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
function Test-ParallelAbandonSegmentDisposition {
|
|
96
|
+
<#
|
|
97
|
+
.SYNOPSIS
|
|
98
|
+
Report whether one segment's tokens request the abandon disposition.
|
|
99
|
+
.DESCRIPTION
|
|
100
|
+
The option name and its value are derived from $script:AbandonDispositionToken by
|
|
101
|
+
splitting it on whitespace, so this function restates neither literal and the seam
|
|
102
|
+
test's single-source-of-truth property is preserved. Both spellings the producer's
|
|
103
|
+
argparse registration accepts are recognized: the separated pair
|
|
104
|
+
'--disposition' 'abandon' as two adjacent tokens, and the equals-joined single token
|
|
105
|
+
'--disposition=abandon'.
|
|
106
|
+
|
|
107
|
+
Comparison is by whole-token equality, which is case-insensitive in PowerShell and
|
|
108
|
+
therefore preserves the OrdinalIgnoreCase behaviour of the containment test it
|
|
109
|
+
replaces. Because a quoted span is one token, a grep whose quoted search term is the
|
|
110
|
+
disposition token no longer requests an abandon.
|
|
111
|
+
|
|
112
|
+
A raw-scan leg runs after the token loop, on the segments the scanner itself scans
|
|
113
|
+
raw, so a disposition carried inside a wrapper's quoted argument is still recognized.
|
|
114
|
+
.PARAMETER Segment
|
|
115
|
+
One segment record produced by Read-CommandLineSegment.
|
|
116
|
+
.OUTPUTS
|
|
117
|
+
System.Boolean
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([bool])]
|
|
121
|
+
param([Parameter(Mandatory)][AllowNull()] $Segment)
|
|
122
|
+
|
|
123
|
+
$parts = @($script:AbandonDispositionToken -split '\s+' | Where-Object { $_ })
|
|
124
|
+
if ($parts.Count -lt 2) {
|
|
125
|
+
return $false
|
|
126
|
+
}
|
|
127
|
+
$optionName = $parts[0]
|
|
128
|
+
$optionValue = $parts[1]
|
|
129
|
+
$joined = $optionName + '=' + $optionValue
|
|
130
|
+
|
|
131
|
+
$token = @($Segment.Tokens)
|
|
132
|
+
|
|
133
|
+
for ($index = 0; $index -lt $token.Count; $index++) {
|
|
134
|
+
if ($token[$index] -eq $joined) {
|
|
135
|
+
return $true
|
|
136
|
+
}
|
|
137
|
+
if ($token[$index] -eq $optionName -and
|
|
138
|
+
$index + 1 -lt $token.Count -and
|
|
139
|
+
$token[$index + 1] -eq $optionValue) {
|
|
140
|
+
return $true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (-not (Test-CommandLineSegmentRawScan -Segment $Segment)) {
|
|
145
|
+
return $false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# A wrapper's quoted argument is a nested command line and collapses into ONE token, so
|
|
149
|
+
# neither the adjacent pair nor the equals-joined token can form inside it. Both accepted
|
|
150
|
+
# spellings are reconstructed from the split constant rather than restated, so the
|
|
151
|
+
# single-source-of-truth seam test still finds each literal exactly once in this file.
|
|
152
|
+
$scanText = [string]$Segment.ScanText
|
|
153
|
+
$comparison = [System.StringComparison]::OrdinalIgnoreCase
|
|
154
|
+
return ($scanText.IndexOf($joined, $comparison) -ge 0 -or
|
|
155
|
+
$scanText.IndexOf(($optionName + ' ' + $optionValue), $comparison) -ge 0)
|
|
156
|
+
}
|
|
157
|
+
|
|
88
158
|
function Test-ParallelAbandonCommandInScope {
|
|
89
159
|
<#
|
|
90
160
|
.SYNOPSIS
|
|
91
161
|
Report whether a command carries the abandon disposition token at all.
|
|
162
|
+
.DESCRIPTION
|
|
163
|
+
Evaluated per segment through the shared parser rather than as a containment test
|
|
164
|
+
over the whole command string. In scope when ANY segment requests the abandon
|
|
165
|
+
disposition in either accepted spelling.
|
|
92
166
|
.PARAMETER NormalizedCommand
|
|
93
167
|
The whitespace-normalized command text.
|
|
94
168
|
.OUTPUTS
|
|
@@ -105,15 +179,25 @@ function Test-ParallelAbandonCommandInScope {
|
|
|
105
179
|
if ([string]::IsNullOrWhiteSpace($NormalizedCommand)) {
|
|
106
180
|
return $false
|
|
107
181
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
182
|
+
|
|
183
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $NormalizedCommand)) {
|
|
184
|
+
if (Test-ParallelAbandonSegmentDisposition -Segment $segment) {
|
|
185
|
+
return $true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return $false
|
|
111
190
|
}
|
|
112
191
|
|
|
113
192
|
function Test-ParallelAbandonCommandConfirmed {
|
|
114
193
|
<#
|
|
115
194
|
.SYNOPSIS
|
|
116
195
|
Report whether a command carries the confirmation marker.
|
|
196
|
+
.DESCRIPTION
|
|
197
|
+
Confirmed only when a SINGLE segment carries both the abandon disposition and the
|
|
198
|
+
confirmation marker. The confirmation is the caller's deliberate acknowledgement of
|
|
199
|
+
this abandon, so a marker echoed in an unrelated segment is not an acknowledgement
|
|
200
|
+
of the command that follows it.
|
|
117
201
|
.PARAMETER NormalizedCommand
|
|
118
202
|
The whitespace-normalized command text.
|
|
119
203
|
.OUTPUTS
|
|
@@ -130,9 +214,22 @@ function Test-ParallelAbandonCommandConfirmed {
|
|
|
130
214
|
if ([string]::IsNullOrWhiteSpace($NormalizedCommand)) {
|
|
131
215
|
return $false
|
|
132
216
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
217
|
+
|
|
218
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $NormalizedCommand)) {
|
|
219
|
+
$tokens = @($segment.Tokens)
|
|
220
|
+
if (-not (Test-ParallelAbandonSegmentDisposition -Segment $segment)) {
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if ($tokens -contains $script:AbandonConfirmToken) {
|
|
224
|
+
return $true
|
|
225
|
+
}
|
|
226
|
+
if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
|
|
227
|
+
([string]$segment.ScanText).IndexOf($script:AbandonConfirmToken, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
228
|
+
return $true
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return $false
|
|
136
233
|
}
|
|
137
234
|
|
|
138
235
|
function Get-ParallelAbandonGateAllowDecision {
|
package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1
CHANGED
|
@@ -30,8 +30,27 @@ param()
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
33
|
+
# Sanctioned-removal manifest reader (issue #635), consumed by the manifest branch in
|
|
34
|
+
# Invoke-ParallelWorktreeRemovalGateDecision. Same module the epic gate imports.
|
|
35
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force
|
|
36
|
+
# Shared command-line parser (issue #545), consumed by the scope filter in
|
|
37
|
+
# Invoke-ParallelWorktreeRemovalGateDecision and by Get-ParallelWorktreeRemovalCommandPath.
|
|
38
|
+
# Both call sites are byte-for-byte the calls the epic gate makes, so the duplicated
|
|
39
|
+
# concern now has one implementation.
|
|
40
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
41
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
33
42
|
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
34
43
|
$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.
|
|
53
|
+
$script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
|
|
35
54
|
|
|
36
55
|
function Get-ParallelWorktreeRemovalGateCheckpointContent {
|
|
37
56
|
<#
|
|
@@ -51,10 +70,50 @@ function Get-ParallelWorktreeRemovalGateCheckpointContent {
|
|
|
51
70
|
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
function Get-ParallelWorktreeRemovalGateEpicCheckpointContent {
|
|
74
|
+
<#
|
|
75
|
+
.SYNOPSIS
|
|
76
|
+
Read the raw JSON text of the epic checkpoint. Tests mock this function
|
|
77
|
+
(read seam, issue #688).
|
|
78
|
+
.DESCRIPTION
|
|
79
|
+
A separate seam from the parallel checkpoint reader so a test can drive the
|
|
80
|
+
two documents independently: the epic branch must be exercised with the
|
|
81
|
+
parallel checkpoint absent, present-but-not-covering, and covering.
|
|
82
|
+
.OUTPUTS
|
|
83
|
+
System.String or $null
|
|
84
|
+
#>
|
|
85
|
+
[CmdletBinding()]
|
|
86
|
+
[OutputType([string])]
|
|
87
|
+
param()
|
|
88
|
+
|
|
89
|
+
if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
|
|
90
|
+
return $null
|
|
91
|
+
}
|
|
92
|
+
return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
function Get-ParallelWorktreeRemovalCommandPath {
|
|
55
96
|
<#
|
|
56
97
|
.SYNOPSIS
|
|
57
98
|
Extract the target worktree path argument from a git worktree remove command.
|
|
99
|
+
.DESCRIPTION
|
|
100
|
+
The operand comes from the segment that structurally invokes git worktree remove,
|
|
101
|
+
so a 'cd <path> &&' segment chained before the removal contributes nothing and a
|
|
102
|
+
quoted mention of the phrase resolves to no operand at all. Quotes around the path
|
|
103
|
+
are already stripped by the tokenizer.
|
|
104
|
+
|
|
105
|
+
'--force' is a zero-argument flag, so it never contributes an operand and may be
|
|
106
|
+
written on either side of the target path. Its presence is read structurally through
|
|
107
|
+
Test-CommandLineFlag rather than by searching the raw text, so a '--force' spelling
|
|
108
|
+
that appears inside an unrelated quoted argument cannot change how the operand list
|
|
109
|
+
is read. When no operand resolves, a present '--force' is reported in its place, so
|
|
110
|
+
the checkpoint lookup fails closed on a value that matches no recorded worktree_path
|
|
111
|
+
- the same value the previous raw-text pattern returned for that input.
|
|
112
|
+
|
|
113
|
+
This body is identical to Get-EpicWorktreeRemovalCommandPath in
|
|
114
|
+
enforce-epic-worktree-removal-gate.ps1. The two gates fire on the same command and
|
|
115
|
+
now delegate the shared concern to one parser rather than to two patterns that had
|
|
116
|
+
already diverged from the Codex copy.
|
|
58
117
|
.PARAMETER CommandText
|
|
59
118
|
The Bash command text under evaluation.
|
|
60
119
|
.OUTPUTS
|
|
@@ -67,8 +126,14 @@ function Get-ParallelWorktreeRemovalCommandPath {
|
|
|
67
126
|
[string] $CommandText
|
|
68
127
|
)
|
|
69
128
|
|
|
70
|
-
|
|
71
|
-
|
|
129
|
+
$hasForce = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
|
|
130
|
+
$operands = @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
|
|
131
|
+
|
|
132
|
+
if ($operands.Count -gt 0) {
|
|
133
|
+
return $operands[0]
|
|
134
|
+
}
|
|
135
|
+
if ($hasForce) {
|
|
136
|
+
return '--force'
|
|
72
137
|
}
|
|
73
138
|
return $null
|
|
74
139
|
}
|
|
@@ -76,11 +141,16 @@ function Get-ParallelWorktreeRemovalCommandPath {
|
|
|
76
141
|
function Find-ParallelWorktreeItemRecord {
|
|
77
142
|
<#
|
|
78
143
|
.SYNOPSIS
|
|
79
|
-
Locate the
|
|
144
|
+
Locate the record whose worktree_path matches the target path.
|
|
80
145
|
.PARAMETER Checkpoint
|
|
81
|
-
Parsed
|
|
146
|
+
Parsed checkpoint, or $null when absent/unreadable.
|
|
82
147
|
.PARAMETER WorktreePath
|
|
83
148
|
The target worktree path extracted from the command text.
|
|
149
|
+
.PARAMETER RecordArrayName
|
|
150
|
+
Name of the record array to scan. Defaults to 'items', the parallel
|
|
151
|
+
checkpoint's array, so every existing caller is unchanged; the epic branch
|
|
152
|
+
added for issue #688 passes 'features'. Parameterized rather than duplicated
|
|
153
|
+
so both topologies share one path-normalization implementation.
|
|
84
154
|
.OUTPUTS
|
|
85
155
|
System.Object or $null
|
|
86
156
|
#>
|
|
@@ -90,14 +160,17 @@ function Find-ParallelWorktreeItemRecord {
|
|
|
90
160
|
$Checkpoint,
|
|
91
161
|
|
|
92
162
|
[AllowNull()]
|
|
93
|
-
[string] $WorktreePath
|
|
163
|
+
[string] $WorktreePath,
|
|
164
|
+
|
|
165
|
+
[ValidateNotNullOrEmpty()]
|
|
166
|
+
[string] $RecordArrayName = 'items'
|
|
94
167
|
)
|
|
95
168
|
|
|
96
169
|
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
|
|
97
170
|
return $null
|
|
98
171
|
}
|
|
99
172
|
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
100
|
-
if ($checkpointProps -notcontains
|
|
173
|
+
if ($checkpointProps -notcontains $RecordArrayName) {
|
|
101
174
|
return $null
|
|
102
175
|
}
|
|
103
176
|
|
|
@@ -105,7 +178,7 @@ function Find-ParallelWorktreeItemRecord {
|
|
|
105
178
|
|
|
106
179
|
# Scan every recorded item for a worktree_path that matches the removal target;
|
|
107
180
|
# path separators are normalized so Windows- and POSIX-style paths compare equal.
|
|
108
|
-
foreach ($item in @($Checkpoint
|
|
181
|
+
foreach ($item in @($Checkpoint.$RecordArrayName)) {
|
|
109
182
|
$itemProps = @($item.PSObject.Properties.Name)
|
|
110
183
|
if ($itemProps -notcontains 'worktree_path') {
|
|
111
184
|
continue
|
|
@@ -203,7 +276,10 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
|
|
|
203
276
|
return Get-ParallelWorktreeGateAllowDecision
|
|
204
277
|
}
|
|
205
278
|
|
|
206
|
-
|
|
279
|
+
# Scope filter. The test is structural, so a relocating spelling such as
|
|
280
|
+
# 'git -C <dir> worktree remove <path>' is now in scope and quoted prose that merely
|
|
281
|
+
# mentions the phrase is not (issue #545). This is the same call the epic gate makes.
|
|
282
|
+
if (-not (Test-CommandLineInvocation -CommandText $commandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))) {
|
|
207
283
|
return Get-ParallelWorktreeGateAllowDecision
|
|
208
284
|
}
|
|
209
285
|
|
|
@@ -224,6 +300,46 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
|
|
|
224
300
|
return Get-ParallelWorktreeGateAllowDecision
|
|
225
301
|
}
|
|
226
302
|
|
|
303
|
+
# Epic-authorization branch (issue #688). It runs after the parallel allow above and
|
|
304
|
+
# before the manifest branch below, so neither of those decisions changes. An epic run
|
|
305
|
+
# records per-child state in the epic checkpoint's features[] array and never writes the
|
|
306
|
+
# parallel checkpoint, so before this branch every merged epic child worktree fell
|
|
307
|
+
# through to the deny even though enforce-epic-worktree-removal-gate.ps1 authorized the
|
|
308
|
+
# same call from that same record. The merge_status predicate is the one already used
|
|
309
|
+
# for parallel items, so the two topologies cannot drift apart on what counts as safe.
|
|
310
|
+
#
|
|
311
|
+
# Fail-closed is preserved: an absent, unreadable, or malformed epic checkpoint parses to
|
|
312
|
+
# $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
|
+
}
|
|
322
|
+
|
|
323
|
+
$featureRecord = Find-ParallelWorktreeItemRecord -Checkpoint $epicCheckpoint -WorktreePath $worktreePath -RecordArrayName 'features'
|
|
324
|
+
if (Test-ParallelWorktreeRemovalAllowed -ItemRecord $featureRecord) {
|
|
325
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# Sanctioned-removal manifest branch (issue #635). It runs last, so a
|
|
329
|
+
# checkpoint-authorized removal still allows at the same decision point it does
|
|
330
|
+
# today and no transcript attribution changes.
|
|
331
|
+
#
|
|
332
|
+
# The coverage test is a PRESENCE test, deliberately not an authorization test. A
|
|
333
|
+
# target this checkpoint records at all is excluded from this branch regardless of
|
|
334
|
+
# that record's merge_status, so a removal the checkpoint does not authorize still
|
|
335
|
+
# reaches the unchanged deny below. This gate defines only the parallel checkpoint
|
|
336
|
+
# seam and has no epic-checkpoint seam, so its exclusion covers the items array
|
|
337
|
+
# only; the epic gate covers the features array with its own exclusion.
|
|
338
|
+
if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
|
|
339
|
+
(Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
|
|
340
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
341
|
+
}
|
|
342
|
+
|
|
227
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."
|
|
228
344
|
}
|
|
229
345
|
|