@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.14
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 +368 -209
- package/package.json +2 -2
- 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-planner.md +18 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
- 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 +33 -37
- 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 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
- 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/lib/blast-radius/BlastRadius.psm1 +43 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -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 +150 -2
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
- 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 +14 -1
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- 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 +20 -1
- 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 +37 -0
|
@@ -27,24 +27,42 @@ $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
|
+
# issues #663 and #713): interpolation characters outside single quotes; outside-quote
|
|
32
|
+
# characters (`<`, `>`, and the `#` comment introducer) outside any quote; and typographic
|
|
33
|
+
# quotes (U+2018 to U+201E) anywhere, because a PowerShell host reads them as quotes.
|
|
34
|
+
$script:InterpolationCommandCharacters = [char[]]@('$', '`')
|
|
35
|
+
$script:OutsideQuoteCommandCharacters = [char[]]@('>', '<', '#')
|
|
36
|
+
$script:TypographicQuoteCharacters = [char[]]@(0x2018, 0x2019, 0x201A, 0x201B, 0x201C, 0x201D, 0x201E)
|
|
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
|
|
42
61
|
Splits a command line into segments on chain operators outside quotes.
|
|
43
62
|
.DESCRIPTION
|
|
44
|
-
Realizes D4 row 13. Quote state
|
|
45
|
-
|
|
46
|
-
the
|
|
47
|
-
caller denies (D4 rows 11 and 13). Empty and whitespace-only segments are dropped.
|
|
63
|
+
Realizes D4 row 13. Quote state and POSIX backslash escapes (issue #710) are tracked, so a quoted or
|
|
64
|
+
escaped chain operator does not split. `Balanced` reports whether the scan ended outside every quote;
|
|
65
|
+
the caller denies unbalanced text (D4 rows 11 and 13). Empty and whitespace-only segments are dropped.
|
|
48
66
|
.OUTPUTS
|
|
49
67
|
System.Collections.Hashtable with keys `Balanced` (bool) and `Segments` (string[]).
|
|
50
68
|
#>
|
|
@@ -55,8 +73,9 @@ function Split-OrchestrationCommandLine {
|
|
|
55
73
|
$segments = [System.Collections.Generic.List[string]]::new()
|
|
56
74
|
$current = [System.Text.StringBuilder]::new()
|
|
57
75
|
$openQuote = [char]0
|
|
58
|
-
|
|
76
|
+
$escaped = $false
|
|
59
77
|
foreach ($character in $CommandText.ToCharArray()) {
|
|
78
|
+
if ($escaped -or ($character -eq '\' -and $openQuote -ne "'")) { $escaped = -not $escaped; [void]$current.Append($character); continue }
|
|
60
79
|
if ($openQuote -ne [char]0) {
|
|
61
80
|
if ($character -eq $openQuote) {
|
|
62
81
|
$openQuote = [char]0
|
|
@@ -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 issues #663 and #713, with quote state tracked as in
|
|
116
|
+
Split-OrchestrationCommandLine. `$` or backtick answers true outside quotes or inside
|
|
117
|
+
double quotes (single quotes keep it literal); `<`, `>`, or a `#` comment answers true
|
|
118
|
+
only outside a quoted span. A typographic quote (U+2018 to U+201E), a backslash before
|
|
119
|
+
any quote character, or a backslash inside a double-quoted span answers true anywhere:
|
|
120
|
+
a 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 or typographic quote moves a span boundary the scan cannot model (#663, #713).
|
|
131
|
+
if ($CommandText.Contains('\"') -or $CommandText.Contains("\'") -or $CommandText.IndexOfAny($script:TypographicQuoteCharacters) -ge 0) {
|
|
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
|
+
# outside-quote characters are judged only where the shell would honour them.
|
|
139
|
+
foreach ($character in $CommandText.ToCharArray()) {
|
|
140
|
+
if ($openQuote -ne "'" -and $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:OutsideQuoteCommandCharacters -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
|
|
@@ -251,21 +393,21 @@ function Test-ExemptOrchestrationSegmentToken {
|
|
|
251
393
|
}
|
|
252
394
|
|
|
253
395
|
if ($candidate.StartsWith('-')) {
|
|
254
|
-
#
|
|
396
|
+
# Only the message and trailer options of commit are modelled (issue #713). Rows
|
|
255
397
|
# 2, 5, 6, 8, and 10 all land here and deny, including a dash-leading
|
|
256
398
|
# operand supplied without a preceding separator.
|
|
257
399
|
if ($subcommand -cne 'commit') {
|
|
258
400
|
return $false
|
|
259
401
|
}
|
|
260
|
-
if ($candidate -ceq '-m' -or $candidate -ceq '--message') {
|
|
261
|
-
# The message value is the following token and is not a pathspec.
|
|
402
|
+
if ($candidate -ceq '-m' -or $candidate -ceq '--message' -or $candidate -ceq '--trailer') {
|
|
403
|
+
# The message or trailer value is the following token and is not a pathspec.
|
|
262
404
|
$index += 2
|
|
263
405
|
if ($index -gt $Token.Count) {
|
|
264
406
|
return $false
|
|
265
407
|
}
|
|
266
408
|
continue
|
|
267
409
|
}
|
|
268
|
-
if ($candidate.StartsWith('--message=') -or
|
|
410
|
+
if ($candidate.StartsWith('--message=') -or $candidate.StartsWith('--trailer=') -or
|
|
269
411
|
($candidate.Length -gt 2 -and $candidate.StartsWith('-m'))) {
|
|
270
412
|
$index++
|
|
271
413
|
continue
|
|
@@ -322,9 +464,9 @@ function Test-ExemptOrchestrationStagingCommand {
|
|
|
322
464
|
return $false
|
|
323
465
|
}
|
|
324
466
|
|
|
325
|
-
# Row 12:
|
|
326
|
-
#
|
|
327
|
-
if (
|
|
467
|
+
# Row 12: `$` or backtick outside single quotes, `<`, `>`, or `#` outside quotes, any
|
|
468
|
+
# typographic quote, and unmodelled backslash escapes make the operand list untrustworthy.
|
|
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,10 @@ 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
|
+
|
|
22
26
|
# Shared command-line parser (issue #545): per-segment scan text and structural matching.
|
|
23
27
|
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
24
28
|
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
@@ -261,33 +265,6 @@ function Get-CheckpointContent {
|
|
|
261
265
|
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
262
266
|
}
|
|
263
267
|
|
|
264
|
-
# The two per-mode read seams (issue #554). Each takes its path from the fixed mode
|
|
265
|
-
# table and never from a delegation's own text; an absent file returns an empty
|
|
266
|
-
# string, which the readiness predicate then treats as a deny.
|
|
267
|
-
function Get-EpicCheckpointContent {
|
|
268
|
-
[CmdletBinding()]
|
|
269
|
-
[OutputType([string])]
|
|
270
|
-
param()
|
|
271
|
-
|
|
272
|
-
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
|
|
273
|
-
if (-not (Test-Path -LiteralPath $path)) {
|
|
274
|
-
return ''
|
|
275
|
-
}
|
|
276
|
-
return Get-Content -Raw -LiteralPath $path
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function Get-ParallelCheckpointContent {
|
|
280
|
-
[CmdletBinding()]
|
|
281
|
-
[OutputType([string])]
|
|
282
|
-
param()
|
|
283
|
-
|
|
284
|
-
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
|
|
285
|
-
if (-not (Test-Path -LiteralPath $path)) {
|
|
286
|
-
return ''
|
|
287
|
-
}
|
|
288
|
-
return Get-Content -Raw -LiteralPath $path
|
|
289
|
-
}
|
|
290
|
-
|
|
291
268
|
function Get-OrchestrationPreimplementationGateAllowDecision {
|
|
292
269
|
[CmdletBinding()]
|
|
293
270
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -377,6 +354,7 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
377
354
|
# default wording they have today.
|
|
378
355
|
$mode = $script:OrchestrationDelegationDefaultMode
|
|
379
356
|
$prompt = ''
|
|
357
|
+
$command = ''
|
|
380
358
|
$filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
|
|
381
359
|
if ($filePath) {
|
|
382
360
|
$normalized = ([string]$filePath) -replace '\\', '/'
|
|
@@ -406,6 +384,15 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
406
384
|
Get-OrchestrationModeDenyReason -Mode $mode -Failure 'declared-checkpoint-path')
|
|
407
385
|
}
|
|
408
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
|
+
|
|
409
396
|
if ($mode -eq 'epic' -or $mode -eq 'parallel') {
|
|
410
397
|
$isEpic = ($mode -eq 'epic')
|
|
411
398
|
$injected = if ($isEpic) { 'EpicCheckpointRaw' } else { 'ParallelCheckpointRaw' }
|
package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1
CHANGED
|
@@ -41,6 +41,12 @@ Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeM
|
|
|
41
41
|
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
42
42
|
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
43
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'
|
|
44
50
|
# Sanctioned-removal manifest location. Recorded here beside the checkpoint path so
|
|
45
51
|
# every hook-read document this gate consults is named in one place; the module owns
|
|
46
52
|
# the read itself.
|
|
@@ -64,6 +70,28 @@ function Get-ParallelWorktreeRemovalGateCheckpointContent {
|
|
|
64
70
|
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
65
71
|
}
|
|
66
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
|
+
|
|
67
95
|
function Get-ParallelWorktreeRemovalCommandPath {
|
|
68
96
|
<#
|
|
69
97
|
.SYNOPSIS
|
|
@@ -113,11 +141,16 @@ function Get-ParallelWorktreeRemovalCommandPath {
|
|
|
113
141
|
function Find-ParallelWorktreeItemRecord {
|
|
114
142
|
<#
|
|
115
143
|
.SYNOPSIS
|
|
116
|
-
Locate the
|
|
144
|
+
Locate the record whose worktree_path matches the target path.
|
|
117
145
|
.PARAMETER Checkpoint
|
|
118
|
-
Parsed
|
|
146
|
+
Parsed checkpoint, or $null when absent/unreadable.
|
|
119
147
|
.PARAMETER WorktreePath
|
|
120
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.
|
|
121
154
|
.OUTPUTS
|
|
122
155
|
System.Object or $null
|
|
123
156
|
#>
|
|
@@ -127,14 +160,17 @@ function Find-ParallelWorktreeItemRecord {
|
|
|
127
160
|
$Checkpoint,
|
|
128
161
|
|
|
129
162
|
[AllowNull()]
|
|
130
|
-
[string] $WorktreePath
|
|
163
|
+
[string] $WorktreePath,
|
|
164
|
+
|
|
165
|
+
[ValidateNotNullOrEmpty()]
|
|
166
|
+
[string] $RecordArrayName = 'items'
|
|
131
167
|
)
|
|
132
168
|
|
|
133
169
|
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
|
|
134
170
|
return $null
|
|
135
171
|
}
|
|
136
172
|
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
137
|
-
if ($checkpointProps -notcontains
|
|
173
|
+
if ($checkpointProps -notcontains $RecordArrayName) {
|
|
138
174
|
return $null
|
|
139
175
|
}
|
|
140
176
|
|
|
@@ -142,7 +178,7 @@ function Find-ParallelWorktreeItemRecord {
|
|
|
142
178
|
|
|
143
179
|
# Scan every recorded item for a worktree_path that matches the removal target;
|
|
144
180
|
# path separators are normalized so Windows- and POSIX-style paths compare equal.
|
|
145
|
-
foreach ($item in @($Checkpoint
|
|
181
|
+
foreach ($item in @($Checkpoint.$RecordArrayName)) {
|
|
146
182
|
$itemProps = @($item.PSObject.Properties.Name)
|
|
147
183
|
if ($itemProps -notcontains 'worktree_path') {
|
|
148
184
|
continue
|
|
@@ -264,6 +300,31 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
|
|
|
264
300
|
return Get-ParallelWorktreeGateAllowDecision
|
|
265
301
|
}
|
|
266
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
|
+
|
|
267
328
|
# Sanctioned-removal manifest branch (issue #635). It runs last, so a
|
|
268
329
|
# checkpoint-authorized removal still allows at the same decision point it does
|
|
269
330
|
# today and no transcript attribution changes.
|
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
payload-reader migration (issue #501) added the module import, the envelope-anomaly
|
|
16
16
|
mapping, and the entry-point seam. The split is a pure move with no behaviour change.
|
|
17
17
|
|
|
18
|
-
Both helpers read the script-scoped configuration
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Both helpers read the script-scoped configuration the parent hook declares and call the
|
|
19
|
+
parent hook's injectable read seams, so this file is only ever dot-sourced from that hook
|
|
20
|
+
and never invoked on its own. $script:PrContextArtifactPath is a process-directory-relative
|
|
21
|
+
artifact path and stays one. $script:OrchestratorStateCheckpointPath is different after
|
|
22
|
+
issue #673: the parent declares it null, and Get-PrAuthorBypassReason assigns it the
|
|
23
|
+
absolute checkpoint path of the worktree identity resolution selected, so every later
|
|
24
|
+
reader receives one resolved path rather than deriving its own.
|
|
22
25
|
|
|
23
26
|
.NOTES
|
|
24
27
|
Compatible with PowerShell 7+. No external module dependencies.
|
|
@@ -30,6 +33,92 @@ param()
|
|
|
30
33
|
# so this file keeps working when a test dot-sources it directly.
|
|
31
34
|
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
32
35
|
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
36
|
+
# Worktree target resolution (issue #687). The checkpoint this gate validates must belong to
|
|
37
|
+
# the item the call is about. Reading a session-root-relative path made that the item whose
|
|
38
|
+
# worktree happened to occupy the session root, so in a parallel or epic topology the gate
|
|
39
|
+
# could return allow on the strength of a sibling item's state -- a false green, which is
|
|
40
|
+
# worse than a false denial because it reports success for a lifecycle it never checked.
|
|
41
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeResolution.psm1') -Force -ErrorAction Stop
|
|
42
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeTargetResolution.psm1') -Force -ErrorAction Stop
|
|
43
|
+
# Portable-identity resolution (issue #673). A path signal cannot select a worktree: a merged
|
|
44
|
+
# feature folder exists in every checkout branched from main, so it places a call in many at
|
|
45
|
+
# once. The item is identified by its canonical issue number and its branch instead.
|
|
46
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop
|
|
47
|
+
# Epic scope (issue #663): the epic integration pull request is gated against the epic checkpoint.
|
|
48
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
|
|
49
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
|
|
50
|
+
|
|
51
|
+
function Resolve-PrAuthorWorktreeTarget {
|
|
52
|
+
<#
|
|
53
|
+
.SYNOPSIS
|
|
54
|
+
Resolve the call's target worktree. Tests mock this function (resolution seam).
|
|
55
|
+
.DESCRIPTION
|
|
56
|
+
A seam rather than a direct call because the resolver reads real git state: the
|
|
57
|
+
worktree list, each registration's branch, and the filesystem. A test that drove it
|
|
58
|
+
directly would depend on the machine's live worktrees and would pass or fail
|
|
59
|
+
according to what other sessions happen to have checked out, which the repository's
|
|
60
|
+
determinism policy forbids. Mocking here lets a test state the resolution outcome it
|
|
61
|
+
is exercising and nothing else.
|
|
62
|
+
.OUTPUTS
|
|
63
|
+
The target result object from Resolve-WorktreeItemTarget.
|
|
64
|
+
#>
|
|
65
|
+
[CmdletBinding()]
|
|
66
|
+
param(
|
|
67
|
+
[Parameter(Mandatory)]
|
|
68
|
+
[string] $CommandText
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (Resolve-WorktreeItemTarget -Text $CommandText -SessionRoot (Get-Location).Path)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Get-PrAuthorTargetCheckpointResolution {
|
|
75
|
+
<#
|
|
76
|
+
.SYNOPSIS
|
|
77
|
+
Resolve the checkpoint path this call must be validated against.
|
|
78
|
+
.DESCRIPTION
|
|
79
|
+
Resolves the target worktree from the command text by portable identity and maps the
|
|
80
|
+
four resolution states onto a checkpoint path or a deny reason:
|
|
81
|
+
|
|
82
|
+
SessionRoot -> that worktree's own checkpoint, composed absolutely.
|
|
83
|
+
OtherWorktree -> that worktree's own checkpoint, composed the same way.
|
|
84
|
+
NoTarget -> deny, carrying the code Get-WorktreeResolutionNoTargetReasonCode
|
|
85
|
+
returns. pr-author passes --head on every gh pr create, so a call
|
|
86
|
+
reaching here omitted the explicit target; the remedy is to supply
|
|
87
|
+
it, never to fall back to whichever checkpoint occupies the
|
|
88
|
+
session root.
|
|
89
|
+
Ambiguous -> deny, carrying the code
|
|
90
|
+
Get-WorktreeResolutionAmbiguityReasonCode returns.
|
|
91
|
+
|
|
92
|
+
The two resolved states share one branch because the path is now composed the same
|
|
93
|
+
way for both: identity selects the worktree, and the checkpoint is read beneath it.
|
|
94
|
+
A session-root result is not a special case, it is the case where identity happened
|
|
95
|
+
to select the worktree the process is already running in.
|
|
96
|
+
|
|
97
|
+
Returning a deny for the two unresolved states is what keeps the gate honest: it
|
|
98
|
+
refuses to answer rather than answering from unrelated state. Neither reason-code
|
|
99
|
+
literal appears in this file; both come from the accessors named above.
|
|
100
|
+
.OUTPUTS
|
|
101
|
+
System.Collections.Specialized.OrderedDictionary with CheckpointPath and Reason.
|
|
102
|
+
#>
|
|
103
|
+
[CmdletBinding()]
|
|
104
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
105
|
+
param(
|
|
106
|
+
[Parameter(Mandatory)]
|
|
107
|
+
[string] $CommandText
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
$target = Resolve-PrAuthorWorktreeTarget -CommandText $CommandText
|
|
111
|
+
|
|
112
|
+
switch ($target.Status) {
|
|
113
|
+
{ $_ -in @('SessionRoot', 'OtherWorktree') } {
|
|
114
|
+
return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null }
|
|
115
|
+
}
|
|
116
|
+
default {
|
|
117
|
+
$reason = "$($target.ReasonCode): $($target.Detail) The pr-author gate will not validate this call against the session root's checkpoint, because that checkpoint may belong to a different item. Pass --head <branch> on the gh pr create command so the call names its own target."
|
|
118
|
+
return [ordered]@{ CheckpointPath = $null; Reason = $reason }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
33
122
|
|
|
34
123
|
function Test-PrAuthorReceiptVerification {
|
|
35
124
|
<#
|
|
@@ -55,6 +144,12 @@ function Test-PrAuthorReceiptVerification {
|
|
|
55
144
|
with Write access to artifacts/ can replace the body file and the receipt together.
|
|
56
145
|
.PARAMETER CommandText
|
|
57
146
|
The Bash command text containing the --body-file argument.
|
|
147
|
+
.PARAMETER CheckpointPath
|
|
148
|
+
The absolute checkpoint path identity resolution selected, passed through to check 6
|
|
149
|
+
so that check reads the resolved worktree's checkpoint rather than deriving its own.
|
|
150
|
+
.PARAMETER EpicScope
|
|
151
|
+
Optional. The epic-scope result Get-PrAuthorBypassReason resolved (issue #663), passed
|
|
152
|
+
through to check 6 so the call is resolved once; $null outside epic scope.
|
|
58
153
|
.OUTPUTS
|
|
59
154
|
System.String or $null
|
|
60
155
|
#>
|
|
@@ -62,7 +157,13 @@ function Test-PrAuthorReceiptVerification {
|
|
|
62
157
|
[OutputType([string])]
|
|
63
158
|
param(
|
|
64
159
|
[Parameter(Mandatory)]
|
|
65
|
-
[string] $CommandText
|
|
160
|
+
[string] $CommandText,
|
|
161
|
+
|
|
162
|
+
[Parameter(Mandatory)]
|
|
163
|
+
[string] $CheckpointPath,
|
|
164
|
+
|
|
165
|
+
[AllowNull()]
|
|
166
|
+
[object] $EpicScope
|
|
66
167
|
)
|
|
67
168
|
|
|
68
169
|
# Check 1: the --body-file argument must match the canonical artifacts/pr_body_<N>.md pattern.
|
|
@@ -133,7 +234,7 @@ function Test-PrAuthorReceiptVerification {
|
|
|
133
234
|
}
|
|
134
235
|
|
|
135
236
|
# Check 6: under epic_mode, gh pr create must carry a matching --base override.
|
|
136
|
-
$epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText
|
|
237
|
+
$epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText -CheckpointPath $CheckpointPath -EpicScope $EpicScope
|
|
137
238
|
if ($epicBaseBranchReason) {
|
|
138
239
|
return $epicBaseBranchReason
|
|
139
240
|
}
|
|
@@ -234,13 +335,36 @@ function Get-PrAuthorBypassReason {
|
|
|
234
335
|
return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
|
|
235
336
|
}
|
|
236
337
|
|
|
338
|
+
# Epic scope (issue #663): when the call's --head equals the epic checkpoint's
|
|
339
|
+
# integration_branch, the integration pull request is gated by the epic PR-creation
|
|
340
|
+
# readiness predicate on artifacts/orchestration/epic-orchestrator-state.json, and no
|
|
341
|
+
# per-feature checkpoint is resolved or read. Resolved once; check 6 reuses the result.
|
|
342
|
+
$epicScope = $null
|
|
343
|
+
if ($hasBodyFile -and $ContextExists) {
|
|
344
|
+
$epicScope = Resolve-EpicScopeCheckpoint -Text $CommandText -SessionRoot (Get-Location).Path
|
|
345
|
+
}
|
|
346
|
+
|
|
237
347
|
# Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
|
|
238
348
|
# bypassed by invoking gh pr create/edit directly) before receipt verification.
|
|
239
|
-
if ($
|
|
240
|
-
$
|
|
349
|
+
if ($null -ne $epicScope -and $epicScope.IsEpicScope) {
|
|
350
|
+
$failure = Get-EpicPrCreationReadinessFailure -Checkpoint $epicScope.Checkpoint -HeadBranch $epicScope.Branch
|
|
351
|
+
if ($failure) {
|
|
352
|
+
return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: this epic-scope pull request was evaluated against $($epicScope.CheckpointPath), and the failed readiness predicate is '$failure'."
|
|
353
|
+
}
|
|
354
|
+
$script:OrchestratorStateCheckpointPath = $epicScope.CheckpointPath
|
|
355
|
+
} elseif ($hasBodyFile -and $ContextExists) {
|
|
356
|
+
$resolution = Get-PrAuthorTargetCheckpointResolution -CommandText $CommandText
|
|
357
|
+
if ($resolution.Reason) {
|
|
358
|
+
return $resolution.Reason
|
|
359
|
+
}
|
|
360
|
+
# Assigned once here and read by every later consumer, so the preflight, the receipt
|
|
361
|
+
# verifier, and the epic base-branch check cannot disagree about which item is gated.
|
|
362
|
+
$script:OrchestratorStateCheckpointPath = $resolution.CheckpointPath
|
|
363
|
+
$checkpointPath = $script:OrchestratorStateCheckpointPath
|
|
364
|
+
$preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $checkpointPath
|
|
241
365
|
if ($preflightResult.HasErrors) {
|
|
242
366
|
$preflightSummary = if ([string]::IsNullOrWhiteSpace($preflightResult.ErrorText)) {
|
|
243
|
-
"checkpoint missing at $
|
|
367
|
+
"checkpoint missing at $checkpointPath"
|
|
244
368
|
} else {
|
|
245
369
|
$preflightResult.ErrorText
|
|
246
370
|
}
|
|
@@ -250,7 +374,7 @@ function Get-PrAuthorBypassReason {
|
|
|
250
374
|
|
|
251
375
|
# Receipt verification: extends, and does not replace, the previously-allowed path.
|
|
252
376
|
if ($hasBodyFile -and $ContextExists) {
|
|
253
|
-
$receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText
|
|
377
|
+
$receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText -CheckpointPath $script:OrchestratorStateCheckpointPath -EpicScope $epicScope
|
|
254
378
|
if ($receiptReason) {
|
|
255
379
|
return $receiptReason
|
|
256
380
|
}
|