@danmoisan/drm-copilot-mcp 1.1.11 → 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 +366 -159
- 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/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 +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 +163 -15
- 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/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/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 +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- 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/pack-manifests/core.json +9 -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-helpers.ps1 +163 -15
- 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 +18 -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 +26 -0
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
7
|
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
-
tool input JSON from the
|
|
9
|
-
|
|
8
|
+
tool input JSON from the envelope's nested tool_input, resolves which item the
|
|
9
|
+
delegation is about, and reads that item's orchestrator checkpoint.
|
|
10
|
+
|
|
11
|
+
The item is identified by portable identity only (issue #673): the canonical
|
|
12
|
+
issue-number line in the delegation prompt, and a branch label. A feature-folder
|
|
13
|
+
path never selects the worktree, because a merged folder exists in every checkout
|
|
14
|
+
branched from main. A delegation the gate cannot identify is denied with a named
|
|
15
|
+
reason code rather than checked against whichever checkpoint occupies the calling
|
|
16
|
+
process's directory, which is how a sibling item's state used to produce an allow.
|
|
10
17
|
|
|
11
18
|
The hook enforces presence only: it cannot read the delegate's chosen
|
|
12
19
|
`model` (no `model` field is exposed in the tool input), so it verifies that
|
|
@@ -24,7 +31,14 @@
|
|
|
24
31
|
empty or absent tool input, and malformed tool-input JSON.
|
|
25
32
|
|
|
26
33
|
The checkpoint read goes through Get-ModelRoutingCheckpoint so tests can
|
|
27
|
-
inject a synthetic checkpoint without touching disk.
|
|
34
|
+
inject a synthetic checkpoint without touching disk. Its path is mandatory and
|
|
35
|
+
carries no default: the caller supplies the absolute path of the resolved
|
|
36
|
+
worktree's checkpoint, so the gate cannot fall back to a relative location.
|
|
37
|
+
|
|
38
|
+
The delegation-identity contract this gate depends on is stated in
|
|
39
|
+
.claude/skills/orchestrate/SKILL.md under `## Issue Number Consistency`: every
|
|
40
|
+
delegation prompt to a receipt-gated subagent type carries the canonical issue
|
|
41
|
+
number line and a `branch:` label naming the item's branch.
|
|
28
42
|
|
|
29
43
|
.NOTES
|
|
30
44
|
Compatible with PowerShell 7+. Read-only presence-gating deterrent.
|
|
@@ -34,6 +48,11 @@ param()
|
|
|
34
48
|
|
|
35
49
|
|
|
36
50
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
51
|
+
# Portable-identity resolution (issue #673). Unguarded and fail-closed on purpose: a gate
|
|
52
|
+
# that cannot load its resolver must not degrade into the cwd-relative read it replaces.
|
|
53
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop
|
|
54
|
+
# Epic scope (issue #663): a delegation for the epic integration branch is gated against the epic checkpoint.
|
|
55
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
|
|
37
56
|
function Get-ModelRoutingCheckpoint {
|
|
38
57
|
<#
|
|
39
58
|
.SYNOPSIS
|
|
@@ -43,7 +62,8 @@ function Get-ModelRoutingCheckpoint {
|
|
|
43
62
|
[CmdletBinding()]
|
|
44
63
|
[OutputType([object])]
|
|
45
64
|
param(
|
|
46
|
-
[
|
|
65
|
+
[Parameter(Mandatory)]
|
|
66
|
+
[string] $CheckpointPath
|
|
47
67
|
)
|
|
48
68
|
|
|
49
69
|
if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
|
|
@@ -117,6 +137,66 @@ function Test-ModelRoutingReceiptPresent {
|
|
|
117
137
|
return $false
|
|
118
138
|
}
|
|
119
139
|
|
|
140
|
+
function Resolve-ModelRoutingWorktreeTarget {
|
|
141
|
+
<#
|
|
142
|
+
.SYNOPSIS
|
|
143
|
+
Resolve the delegation's target worktree. Tests mock this function (resolution seam).
|
|
144
|
+
.DESCRIPTION
|
|
145
|
+
A seam rather than a direct call because the resolver reads real git state: the
|
|
146
|
+
worktree list, each registration's branch, and the filesystem. A test that drove it
|
|
147
|
+
directly would depend on whichever worktrees exist on the machine running it.
|
|
148
|
+
.PARAMETER PromptText
|
|
149
|
+
The delegation prompt, which is the only field scanned for identity.
|
|
150
|
+
.OUTPUTS
|
|
151
|
+
The target result object from Resolve-WorktreeItemTarget.
|
|
152
|
+
#>
|
|
153
|
+
[CmdletBinding()]
|
|
154
|
+
[OutputType([pscustomobject])]
|
|
155
|
+
param(
|
|
156
|
+
[Parameter(Mandatory)]
|
|
157
|
+
[AllowEmptyString()]
|
|
158
|
+
[string] $PromptText
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return (Resolve-WorktreeItemTarget -Text $PromptText -SessionRoot (Get-Location).Path)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Get-ModelRoutingTargetCheckpointResolution {
|
|
165
|
+
<#
|
|
166
|
+
.SYNOPSIS
|
|
167
|
+
Resolve the checkpoint path this delegation must be checked against.
|
|
168
|
+
.DESCRIPTION
|
|
169
|
+
Maps the four resolution states onto a checkpoint path or a deny reason. The two
|
|
170
|
+
resolved states share one branch because the path is composed the same way for
|
|
171
|
+
both: identity selects the worktree, and the checkpoint is read beneath it. The two
|
|
172
|
+
unresolved states deny, carrying the code the worktree-resolution accessors supply;
|
|
173
|
+
neither literal appears in this file.
|
|
174
|
+
.PARAMETER PromptText
|
|
175
|
+
The delegation prompt to resolve identity from.
|
|
176
|
+
.OUTPUTS
|
|
177
|
+
System.Collections.Specialized.OrderedDictionary with CheckpointPath and Reason.
|
|
178
|
+
#>
|
|
179
|
+
[CmdletBinding()]
|
|
180
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
181
|
+
param(
|
|
182
|
+
[Parameter(Mandatory)]
|
|
183
|
+
[AllowEmptyString()]
|
|
184
|
+
[string] $PromptText
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
$target = Resolve-ModelRoutingWorktreeTarget -PromptText $PromptText
|
|
188
|
+
|
|
189
|
+
switch ($target.Status) {
|
|
190
|
+
{ $_ -in @('SessionRoot', 'OtherWorktree') } {
|
|
191
|
+
return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null }
|
|
192
|
+
}
|
|
193
|
+
default {
|
|
194
|
+
$reason = "$($target.ReasonCode): $($target.Detail) The model-routing gate will not check this delegation against a checkpoint that may belong to a different item. Put the line 'Canonical issue number for this feature is <N>.' and a 'branch: <item branch>' label in the delegation prompt so the gate can identify the item."
|
|
195
|
+
return [ordered]@{ CheckpointPath = $null; Reason = $reason }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
120
200
|
function Invoke-ModelRoutingReceiptDecision {
|
|
121
201
|
<#
|
|
122
202
|
.SYNOPSIS
|
|
@@ -154,7 +234,42 @@ function Invoke-ModelRoutingReceiptDecision {
|
|
|
154
234
|
return $allow
|
|
155
235
|
}
|
|
156
236
|
|
|
157
|
-
|
|
237
|
+
# Identity resolution sits after the scope filter and before the checkpoint read, so a
|
|
238
|
+
# delegation outside the gated set is still allowed however unresolvable its target is,
|
|
239
|
+
# and a gated delegation the gate cannot identify is refused rather than answered from
|
|
240
|
+
# unrelated state. An absent prompt is passed as an empty string, which resolves to no
|
|
241
|
+
# identity and therefore to the same deny.
|
|
242
|
+
$prompt = [string](Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'prompt')
|
|
243
|
+
|
|
244
|
+
# Decision: a prompt whose branch label equals the epic checkpoint's integration_branch is
|
|
245
|
+
# epic scope (issue #663), and the receipt is looked up in the epic checkpoint without
|
|
246
|
+
# running per-feature identity resolution. Any other prompt falls through unchanged.
|
|
247
|
+
$epicScope = Resolve-EpicScopeCheckpoint -Text $prompt -SessionRoot (Get-Location).Path
|
|
248
|
+
if ($epicScope.IsEpicScope) {
|
|
249
|
+
if (Test-ModelRoutingReceiptPresent -Checkpoint $epicScope.Checkpoint -Subagent $subagent) {
|
|
250
|
+
return $allow
|
|
251
|
+
}
|
|
252
|
+
return [ordered]@{
|
|
253
|
+
hookSpecificOutput = [ordered]@{
|
|
254
|
+
hookEventName = 'PreToolUse'
|
|
255
|
+
permissionDecision = 'deny'
|
|
256
|
+
permissionDecisionReason = "MODEL_ROUTING_RECEIPT_BLOCKED: cannot delegate to '$subagent' in epic scope before a model_routing_receipts entry for it is recorded in $($epicScope.CheckpointPath); the failed readiness predicate is 'model_routing_receipts'."
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
$resolution = Get-ModelRoutingTargetCheckpointResolution -PromptText $prompt
|
|
262
|
+
if ($resolution.Reason) {
|
|
263
|
+
return [ordered]@{
|
|
264
|
+
hookSpecificOutput = [ordered]@{
|
|
265
|
+
hookEventName = 'PreToolUse'
|
|
266
|
+
permissionDecision = 'deny'
|
|
267
|
+
permissionDecisionReason = $resolution.Reason
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
$checkpoint = Get-ModelRoutingCheckpoint -CheckpointPath $resolution.CheckpointPath
|
|
158
273
|
if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
|
|
159
274
|
return $allow
|
|
160
275
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Epic-scope command and path legs of the preimplementation gate (issue #663).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. Holds two groups of
|
|
7
|
+
functions:
|
|
8
|
+
|
|
9
|
+
- The two per-mode read seams of issue #554 (Get-EpicCheckpointContent and
|
|
10
|
+
Get-ParallelCheckpointContent), relocated verbatim from the gate file by issue #663
|
|
11
|
+
so the gate stays inside the 500-line cap. Their names and behaviour are unchanged,
|
|
12
|
+
so tests that mock or shadow them by name are unaffected.
|
|
13
|
+
- The epic-scope decision for the command and path legs. A staging command or a
|
|
14
|
+
Write/Edit call is epic scope when the effective worktree's HEAD (the -C selector
|
|
15
|
+
worktree when present, otherwise the session root) equals the integration_branch of
|
|
16
|
+
artifacts/orchestration/epic-orchestrator-state.json. In epic scope the call is
|
|
17
|
+
decided by the epic command-leg readiness predicate, and under decision D2 an
|
|
18
|
+
implementation-classified operand is allowed only while a merge is in progress in
|
|
19
|
+
that worktree. Outside epic scope the decision function returns $null and the gate's
|
|
20
|
+
single-feature path runs unchanged.
|
|
21
|
+
|
|
22
|
+
.NOTES
|
|
23
|
+
PowerShell 7+. Depends on functions the gate defines or dot-sources before any call:
|
|
24
|
+
Split-OrchestrationCommandLine and ConvertTo-OrchestrationCommandToken (helpers file),
|
|
25
|
+
Get-OrchestrationDelegationCheckpointPath (modes file), and the gate's allow and block
|
|
26
|
+
decision constructors. Mirrored byte-identically under
|
|
27
|
+
extensions/drm-copilot/resources/claude-customizations/.
|
|
28
|
+
#>
|
|
29
|
+
|
|
30
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
|
|
31
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
|
|
32
|
+
|
|
33
|
+
# The two per-mode read seams (issue #554). Each takes its path from the fixed mode
|
|
34
|
+
# table and never from a delegation's own text; an absent file returns an empty
|
|
35
|
+
# string, which the readiness predicate then treats as a deny.
|
|
36
|
+
function Get-EpicCheckpointContent {
|
|
37
|
+
[CmdletBinding()]
|
|
38
|
+
[OutputType([string])]
|
|
39
|
+
param()
|
|
40
|
+
|
|
41
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
|
|
42
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
return Get-Content -Raw -LiteralPath $path
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Get-ParallelCheckpointContent {
|
|
49
|
+
[CmdletBinding()]
|
|
50
|
+
[OutputType([string])]
|
|
51
|
+
param()
|
|
52
|
+
|
|
53
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
|
|
54
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
return Get-Content -Raw -LiteralPath $path
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-OrchestrationEpicScopeSelector {
|
|
61
|
+
<#
|
|
62
|
+
.SYNOPSIS
|
|
63
|
+
Returns the value of a leading `git -C <value>` selector, or $null.
|
|
64
|
+
.DESCRIPTION
|
|
65
|
+
Pure. Reads only the first segment of the command line, split and tokenized with
|
|
66
|
+
the helpers-file parser so quoting is handled exactly as the staging exemption
|
|
67
|
+
handles it. An unbalanced command line has no decidable selector and returns $null.
|
|
68
|
+
.PARAMETER Command
|
|
69
|
+
The full command line.
|
|
70
|
+
.OUTPUTS
|
|
71
|
+
System.String
|
|
72
|
+
#>
|
|
73
|
+
[CmdletBinding()]
|
|
74
|
+
[OutputType([string])]
|
|
75
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $Command)
|
|
76
|
+
|
|
77
|
+
$split = Split-OrchestrationCommandLine -CommandText $Command
|
|
78
|
+
if (-not $split.Balanced -or @($split.Segments).Count -eq 0) {
|
|
79
|
+
return $null
|
|
80
|
+
}
|
|
81
|
+
$token = @(ConvertTo-OrchestrationCommandToken -Segment ([string]@($split.Segments)[0]).Trim())
|
|
82
|
+
if ($token.Count -ge 3 -and $token[0] -ceq 'git' -and $token[1] -ceq '-C' -and $token[2]) {
|
|
83
|
+
return $token[2]
|
|
84
|
+
}
|
|
85
|
+
return $null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Get-OrchestrationEpicScopeDecision {
|
|
89
|
+
<#
|
|
90
|
+
.SYNOPSIS
|
|
91
|
+
Returns the epic-scope decision for an implementation-classified command or path, or $null.
|
|
92
|
+
.DESCRIPTION
|
|
93
|
+
Resolves epic scope with worktree-HEAD matching and the command's -C selector. When
|
|
94
|
+
the call is not epic scope, returns $null so the caller's single-feature path runs
|
|
95
|
+
unchanged. In epic scope, returns an allow decision when the epic command-leg
|
|
96
|
+
readiness predicate passes, and otherwise a deny naming the epic checkpoint and the
|
|
97
|
+
failed conjunct.
|
|
98
|
+
.PARAMETER Command
|
|
99
|
+
The Bash command line; empty for a path leg.
|
|
100
|
+
.PARAMETER FilePath
|
|
101
|
+
The Write/Edit file path; empty for a command leg.
|
|
102
|
+
.OUTPUTS
|
|
103
|
+
System.Collections.Specialized.OrderedDictionary, or $null outside epic scope.
|
|
104
|
+
#>
|
|
105
|
+
[CmdletBinding()]
|
|
106
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
107
|
+
param(
|
|
108
|
+
[AllowNull()][AllowEmptyString()][string] $Command = '',
|
|
109
|
+
[AllowNull()][AllowEmptyString()][string] $FilePath = ''
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# A call carrying neither a command nor a path has no leg to decide.
|
|
113
|
+
if (-not $Command -and -not $FilePath) {
|
|
114
|
+
return $null
|
|
115
|
+
}
|
|
116
|
+
$selector = if ($Command) { Get-OrchestrationEpicScopeSelector -Command $Command } else { $null }
|
|
117
|
+
$scope = Resolve-EpicScopeCheckpoint -Text ([string]$Command) -SessionRoot (Get-Location).Path -WorktreeSelector $selector -MatchWorktreeHead
|
|
118
|
+
if (-not $scope.IsEpicScope) {
|
|
119
|
+
return $null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$failure = Get-EpicCommandLegReadinessFailure -Checkpoint $scope.Checkpoint -MergeInProgress $scope.MergeInProgress
|
|
123
|
+
if (-not $failure) {
|
|
124
|
+
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
125
|
+
}
|
|
126
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason ("PREIMPLEMENTATION_GATE_BLOCKED: this epic-scope operation was evaluated against $($scope.CheckpointPath), and the failed readiness predicate is '$failure'. Implementation operations in epic scope require that checkpoint to satisfy every readiness predicate, and a production path may be staged or edited only while a merge is in progress.")
|
|
127
|
+
}
|
|
@@ -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,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' }
|