@danmoisan/drm-copilot-mcp 1.0.27 → 1.1.1
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 +0 -1
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +64 -20
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +36 -24
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +349 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +177 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +34 -60
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +187 -0
- package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +125 -4
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +27 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +28 -0
- package/resources/claude-customizations/config/blast-radius.json +9 -3
- package/resources/claude-customizations/pack-manifests/core.json +5 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +349 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +123 -6
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +32 -1
|
@@ -522,6 +522,34 @@ Validate the artifact through `mcp__drm-copilot__validate_orchestration_artifact
|
|
|
522
522
|
`artifact_type: "parallel-kickoff"`. That artifact type dispatches to
|
|
523
523
|
`scripts/dev_tools/parallel_kickoff_contract.py`, which this feature delivers.
|
|
524
524
|
|
|
525
|
+
### Integration Commit Form (issue #539)
|
|
526
|
+
|
|
527
|
+
The parallel planner's own version-control operations — committing the run manifest
|
|
528
|
+
`docs/features/parallel/<slug>/parallel.md`, each item's prepared feature folder and approved
|
|
529
|
+
plan-path, and the durable kickoff copy `docs/features/parallel/<slug>/parallel-kickoff.md` — are
|
|
530
|
+
orchestration bookkeeping, not implementation. The preimplementation gate
|
|
531
|
+
(`.claude/hooks/enforce-orchestration-preimplementation-gate.ps1`) exempts them only when the
|
|
532
|
+
invocation is **pathspec-bearing**: every staging or integration invocation must name at least one
|
|
533
|
+
explicit path operand, and every operand must resolve inside one of the five exempt
|
|
534
|
+
orchestration-bookkeeping trees:
|
|
535
|
+
|
|
536
|
+
- `docs/features/epics/`
|
|
537
|
+
- `docs/features/parallel/`
|
|
538
|
+
- `docs/features/active/`
|
|
539
|
+
- `docs/features/potential/`
|
|
540
|
+
- `artifacts/orchestration/`
|
|
541
|
+
|
|
542
|
+
Use one of the two pathspec-bearing spellings:
|
|
543
|
+
|
|
544
|
+
- the message option followed by a double-dash separator and then the exempt path operands, or
|
|
545
|
+
- the exempt path operands named directly before the message option.
|
|
546
|
+
|
|
547
|
+
A pathless integration invocation — a message option with no path operand — is denied (D4 row 4),
|
|
548
|
+
as are whole-tree operands, tree-wide flags, pathspec-from-file options, history-rewriting or
|
|
549
|
+
content-widening options, and any operand that resolves outside the five exempt trees. Mixing one
|
|
550
|
+
exempt operand with one production operand denies the whole invocation. Name each exempt path
|
|
551
|
+
explicitly; the exemption is allow-side only and every parse ambiguity denies.
|
|
552
|
+
|
|
525
553
|
## Completion Report
|
|
526
554
|
|
|
527
555
|
The final report to the operator must include:
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"shared_surfaces": [
|
|
4
4
|
".claude/settings.json",
|
|
5
|
+
"config/blast-radius.json",
|
|
5
6
|
"config/orchestration-routing.json",
|
|
6
|
-
"
|
|
7
|
+
"package-lock.json",
|
|
8
|
+
"poetry.lock",
|
|
9
|
+
"quality-tiers.yml"
|
|
7
10
|
],
|
|
8
11
|
"shared_surface_globs": [],
|
|
9
12
|
"mandate_reads": [
|
|
@@ -12,10 +15,13 @@
|
|
|
12
15
|
".claude/skills/evidence-and-timestamp-conventions/SKILL.md",
|
|
13
16
|
".github/instructions/**",
|
|
14
17
|
"artifacts/**",
|
|
15
|
-
"quality-tiers.yml"
|
|
18
|
+
"quality-tiers.yml",
|
|
19
|
+
".claude/skills/acceptance-criteria-tracking/SKILL.md",
|
|
20
|
+
".claude/skills/policy-compliance-order/SKILL.md",
|
|
21
|
+
".claude/agent-memory/**",
|
|
22
|
+
".agents/skills/**"
|
|
16
23
|
],
|
|
17
24
|
"modules": {
|
|
18
|
-
"claude-runtime": [".claude/**"],
|
|
19
25
|
"config": ["config/**"]
|
|
20
26
|
},
|
|
21
27
|
"over_breadth_fraction": 0.25
|
|
@@ -33,13 +33,16 @@
|
|
|
33
33
|
".claude/hooks/enforce-feature-folder-order.ps1",
|
|
34
34
|
".claude/hooks/enforce-model-routing-receipt.ps1",
|
|
35
35
|
".claude/hooks/enforce-orchestration-preimplementation-gate.ps1",
|
|
36
|
+
".claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1",
|
|
36
37
|
".claude/hooks/enforce-parallel-abandon-gate.ps1",
|
|
37
38
|
".claude/hooks/enforce-parallel-cohort-barrier.ps1",
|
|
39
|
+
".claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1",
|
|
38
40
|
".claude/hooks/enforce-parallel-drift-gate.ps1",
|
|
39
41
|
".claude/hooks/enforce-parallel-drift-gate-helpers.ps1",
|
|
40
42
|
".claude/hooks/enforce-parallel-worktree-removal-gate.ps1",
|
|
41
43
|
".claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1",
|
|
42
44
|
".claude/hooks/enforce-pr-author-skill.ps1",
|
|
45
|
+
".claude/hooks/enforce-pr-author-skill-helpers.ps1",
|
|
43
46
|
".claude/hooks/enforce-prd-feature-before-planner.ps1",
|
|
44
47
|
".claude/hooks/enforce-promotion-mcp-only.ps1",
|
|
45
48
|
".claude/hooks/persist-session-id.ps1",
|
|
@@ -105,6 +108,7 @@
|
|
|
105
108
|
".claude/skills/skill-canonical-location-audit/SKILL.md",
|
|
106
109
|
".claude/skills/translate-copilot-to-claude/SKILL.md",
|
|
107
110
|
".claude/skills/update-status/SKILL.md",
|
|
111
|
+
".claude/lib/hook-payload/HookPayload.psm1",
|
|
108
112
|
".claude/lib/model-routing/ModelRouting.psm1",
|
|
109
113
|
".claude/lib/orchestrator-state/OrchestratorState.psm1",
|
|
110
114
|
".claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1",
|
|
@@ -126,6 +130,7 @@
|
|
|
126
130
|
".claude/lib/blast-radius/BlastRadiusValidation.psm1",
|
|
127
131
|
".claude/lib/blast-radius/BlastRadius.psm1",
|
|
128
132
|
".claude/lib/blast-radius/BlastRadiusNormalization.psm1",
|
|
133
|
+
".claude/lib/blast-radius/BlastRadiusTokenShape.psm1",
|
|
129
134
|
".claude/rules/parallel-orchestration.md",
|
|
130
135
|
".claude/rules/shell.md",
|
|
131
136
|
".claude/lib/bash/compute-cohorts.sh",
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pathspec classifier for the orchestration-bookkeeping staging exemption (issue #539).
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Pure string logic only: no disk, process, network, or environment access. The single
|
|
6
|
+
entry predicate `Test-ExemptOrchestrationStagingCommand` answers one question - does
|
|
7
|
+
this command text parse, in its entirety, as recognized staging or integration
|
|
8
|
+
invocations whose every pathspec operand resolves inside an orchestration-bookkeeping
|
|
9
|
+
tree? Every parse ambiguity answers false, so the caller's pre-change deny is the
|
|
10
|
+
fallback for every unmodeled form.
|
|
11
|
+
|
|
12
|
+
The normative contract is the D4 fail-closed rule table in
|
|
13
|
+
docs/features/active/2026-08-24-preimplementation-gate-blocks-planner-integration-commits-539/spec.md.
|
|
14
|
+
Rule-table row numbers are cited inline against the code that realizes them.
|
|
15
|
+
|
|
16
|
+
This file is dot-sourced by the sibling gate hook, following the headroom-split
|
|
17
|
+
precedent set by enforce-pr-author-skill.ps1.
|
|
18
|
+
#>
|
|
19
|
+
|
|
20
|
+
# The five exempt orchestration-bookkeeping trees (D2). Directory prefixes, repo-relative,
|
|
21
|
+
# forward-slash spelled. No glob, no absolute entry, no literal-file entry.
|
|
22
|
+
$script:OrchestrationBookkeepingTrees = @(
|
|
23
|
+
'docs/features/epics/'
|
|
24
|
+
'docs/features/parallel/'
|
|
25
|
+
'docs/features/active/'
|
|
26
|
+
'docs/features/potential/'
|
|
27
|
+
'artifacts/orchestration/'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Characters that make a command line statically unresolvable: shell interpolation and
|
|
31
|
+
# redirection (D4 row 12). Tested across the whole line rather than per operand, because a
|
|
32
|
+
# redirection anywhere in the line moves content the operand list cannot describe.
|
|
33
|
+
$script:UnresolvableCommandCharacters = [char[]]@('$', '`', '>', '<')
|
|
34
|
+
|
|
35
|
+
# Wildcards that make an operand a glob (D4 row 15). Only the literal prefix before the
|
|
36
|
+
# first of these is prefix-tested.
|
|
37
|
+
$script:PathspecWildcardCharacters = [char[]]@('*', '?', '[')
|
|
38
|
+
|
|
39
|
+
function Split-OrchestrationCommandLine {
|
|
40
|
+
<#
|
|
41
|
+
.SYNOPSIS
|
|
42
|
+
Splits a command line into segments on chain operators outside quotes.
|
|
43
|
+
.DESCRIPTION
|
|
44
|
+
Realizes D4 row 13. Quote state is tracked while scanning so a chain operator
|
|
45
|
+
inside a quoted span does not split. The returned `Balanced` flag reports whether
|
|
46
|
+
the scan ended outside every quote; unbalanced text is not splittable and the
|
|
47
|
+
caller denies (D4 rows 11 and 13). Empty and whitespace-only segments are dropped.
|
|
48
|
+
.OUTPUTS
|
|
49
|
+
System.Collections.Hashtable with keys `Balanced` (bool) and `Segments` (string[]).
|
|
50
|
+
#>
|
|
51
|
+
[CmdletBinding()]
|
|
52
|
+
[OutputType([hashtable])]
|
|
53
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
54
|
+
|
|
55
|
+
$segments = [System.Collections.Generic.List[string]]::new()
|
|
56
|
+
$current = [System.Text.StringBuilder]::new()
|
|
57
|
+
$openQuote = [char]0
|
|
58
|
+
|
|
59
|
+
foreach ($character in $CommandText.ToCharArray()) {
|
|
60
|
+
if ($openQuote -ne [char]0) {
|
|
61
|
+
if ($character -eq $openQuote) {
|
|
62
|
+
$openQuote = [char]0
|
|
63
|
+
}
|
|
64
|
+
[void]$current.Append($character)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ($character -eq '"' -or $character -eq "'") {
|
|
69
|
+
$openQuote = $character
|
|
70
|
+
[void]$current.Append($character)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if ($character -eq ';' -or $character -eq '&' -or $character -eq '|' -or
|
|
75
|
+
$character -eq "`n" -or $character -eq "`r") {
|
|
76
|
+
$segments.Add($current.ToString())
|
|
77
|
+
[void]$current.Clear()
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[void]$current.Append($character)
|
|
82
|
+
}
|
|
83
|
+
$segments.Add($current.ToString())
|
|
84
|
+
|
|
85
|
+
return @{
|
|
86
|
+
Balanced = ($openQuote -eq [char]0)
|
|
87
|
+
Segments = @($segments | Where-Object { $_.Trim() })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ConvertTo-OrchestrationCommandToken {
|
|
92
|
+
<#
|
|
93
|
+
.SYNOPSIS
|
|
94
|
+
Splits one segment into whitespace-delimited tokens with balanced quotes stripped.
|
|
95
|
+
.DESCRIPTION
|
|
96
|
+
Realizes the quote handling of D4 row 11. A quoted span contributes to the token it
|
|
97
|
+
sits in, so `-m "epic scaffold"` yields the two tokens `-m` and `epic scaffold`, and
|
|
98
|
+
a quoted operand arrives at the prefix test unquoted. The caller has already
|
|
99
|
+
rejected unbalanced text, so an unterminated quote simply ends the final token.
|
|
100
|
+
.OUTPUTS
|
|
101
|
+
System.String[]
|
|
102
|
+
#>
|
|
103
|
+
[CmdletBinding()]
|
|
104
|
+
[OutputType([string[]])]
|
|
105
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $Segment)
|
|
106
|
+
|
|
107
|
+
$tokens = [System.Collections.Generic.List[string]]::new()
|
|
108
|
+
$current = [System.Text.StringBuilder]::new()
|
|
109
|
+
$hasToken = $false
|
|
110
|
+
$openQuote = [char]0
|
|
111
|
+
|
|
112
|
+
foreach ($character in $Segment.ToCharArray()) {
|
|
113
|
+
if ($openQuote -ne [char]0) {
|
|
114
|
+
if ($character -eq $openQuote) {
|
|
115
|
+
$openQuote = [char]0
|
|
116
|
+
} else {
|
|
117
|
+
[void]$current.Append($character)
|
|
118
|
+
}
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ($character -eq '"' -or $character -eq "'") {
|
|
123
|
+
$openQuote = $character
|
|
124
|
+
$hasToken = $true
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ([char]::IsWhiteSpace($character)) {
|
|
129
|
+
if ($hasToken) {
|
|
130
|
+
$tokens.Add($current.ToString())
|
|
131
|
+
[void]$current.Clear()
|
|
132
|
+
$hasToken = $false
|
|
133
|
+
}
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$hasToken = $true
|
|
138
|
+
[void]$current.Append($character)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if ($hasToken) {
|
|
142
|
+
$tokens.Add($current.ToString())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return $tokens.ToArray()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Test-ExemptOrchestrationOperand {
|
|
149
|
+
<#
|
|
150
|
+
.SYNOPSIS
|
|
151
|
+
Tests one pathspec operand against the five exempt orchestration trees.
|
|
152
|
+
.DESCRIPTION
|
|
153
|
+
Realizes D4 rows 3, 9, 15, 16, 17, and 18. Backslashes normalize to forward slashes
|
|
154
|
+
before the prefix test (row 18); pathspec magic, absolute spellings, parent-directory
|
|
155
|
+
segments, and globs whose literal prefix escapes the exempt set all deny.
|
|
156
|
+
.OUTPUTS
|
|
157
|
+
System.Boolean
|
|
158
|
+
#>
|
|
159
|
+
[CmdletBinding()]
|
|
160
|
+
[OutputType([bool])]
|
|
161
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $Operand)
|
|
162
|
+
|
|
163
|
+
if (-not $Operand) {
|
|
164
|
+
return $false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Row 3b and row 9: any leading colon is pathspec magic, which can escape or invert the
|
|
168
|
+
# tree scope, so no colon-led operand is ever resolvable by a prefix test.
|
|
169
|
+
if ($Operand.StartsWith(':')) {
|
|
170
|
+
return $false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Row 18: separator normalization precedes every prefix comparison below.
|
|
174
|
+
$normalized = $Operand -replace '\\', '/'
|
|
175
|
+
|
|
176
|
+
# Row 16: rooted, drive-lettered, and UNC spellings all deny; the exempt prefixes stay
|
|
177
|
+
# repo-relative (this is the posture issue #516 later composes with).
|
|
178
|
+
if ($normalized.StartsWith('/')) {
|
|
179
|
+
return $false
|
|
180
|
+
}
|
|
181
|
+
if ($normalized -match '^[A-Za-z]:') {
|
|
182
|
+
return $false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Rows 15c and 17: any parent-directory component escapes the prefix.
|
|
186
|
+
if (($normalized -split '/') -contains '..') {
|
|
187
|
+
return $false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Row 15: only the literal prefix before the first wildcard is prefix-tested, so a glob
|
|
191
|
+
# whose wildcard occupies or truncates an ancestor segment cannot pass.
|
|
192
|
+
$literalPrefix = $normalized
|
|
193
|
+
$wildcardIndex = $normalized.IndexOfAny($script:PathspecWildcardCharacters)
|
|
194
|
+
if ($wildcardIndex -ge 0) {
|
|
195
|
+
$literalPrefix = $normalized.Substring(0, $wildcardIndex)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
foreach ($tree in $script:OrchestrationBookkeepingTrees) {
|
|
199
|
+
if ($literalPrefix.StartsWith($tree)) {
|
|
200
|
+
return $true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return $false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Test-ExemptOrchestrationSegmentToken {
|
|
207
|
+
<#
|
|
208
|
+
.SYNOPSIS
|
|
209
|
+
Tests one already-tokenized segment as a recognized all-exempt invocation.
|
|
210
|
+
.DESCRIPTION
|
|
211
|
+
Realizes D4 rows 1, 2, 4, 5, 6, 7, 8, 10, 14, and 19. The command name must lead the
|
|
212
|
+
segment and the subcommand must follow it immediately (row 14), the option table is
|
|
213
|
+
modelled positively so any unmodelled dash-leading token denies (rows 2, 5, 6, 8, 10),
|
|
214
|
+
tokens after the double-dash separator are pathspecs (row 7), at least one operand is
|
|
215
|
+
required (rows 1, 4, 7), and every operand must pass (row 19).
|
|
216
|
+
.OUTPUTS
|
|
217
|
+
System.Boolean
|
|
218
|
+
#>
|
|
219
|
+
[CmdletBinding()]
|
|
220
|
+
[OutputType([bool])]
|
|
221
|
+
param([Parameter(Mandatory)][AllowEmptyCollection()][string[]] $Token)
|
|
222
|
+
|
|
223
|
+
if ($Token.Count -lt 2) {
|
|
224
|
+
return $false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Row 14: the command name leads and the subcommand follows immediately. Anything in
|
|
228
|
+
# between - a relocating option or an env-style prefix - moves the pathspec base and is
|
|
229
|
+
# rejected here rather than modelled.
|
|
230
|
+
if ($Token[0] -cne 'git') {
|
|
231
|
+
return $false
|
|
232
|
+
}
|
|
233
|
+
$subcommand = $Token[1]
|
|
234
|
+
if ($subcommand -cne 'add' -and $subcommand -cne 'commit') {
|
|
235
|
+
return $false
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
$operands = [System.Collections.Generic.List[string]]::new()
|
|
239
|
+
$afterSeparator = $false
|
|
240
|
+
$index = 2
|
|
241
|
+
|
|
242
|
+
while ($index -lt $Token.Count) {
|
|
243
|
+
$candidate = $Token[$index]
|
|
244
|
+
|
|
245
|
+
if (-not $afterSeparator) {
|
|
246
|
+
if ($candidate -ceq '--') {
|
|
247
|
+
# Row 7: every remaining token is a pathspec, dash-leading or not.
|
|
248
|
+
$afterSeparator = $true
|
|
249
|
+
$index++
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if ($candidate.StartsWith('-')) {
|
|
254
|
+
# The message option is the only modelled option on either subcommand. Rows
|
|
255
|
+
# 2, 5, 6, 8, and 10 all land here and deny, including a dash-leading
|
|
256
|
+
# operand supplied without a preceding separator.
|
|
257
|
+
if ($subcommand -cne 'commit') {
|
|
258
|
+
return $false
|
|
259
|
+
}
|
|
260
|
+
if ($candidate -ceq '-m' -or $candidate -ceq '--message') {
|
|
261
|
+
# The message value is the following token and is not a pathspec.
|
|
262
|
+
$index += 2
|
|
263
|
+
if ($index -gt $Token.Count) {
|
|
264
|
+
return $false
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
if ($candidate.StartsWith('--message=') -or
|
|
269
|
+
($candidate.Length -gt 2 -and $candidate.StartsWith('-m'))) {
|
|
270
|
+
$index++
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
return $false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
$operands.Add($candidate)
|
|
278
|
+
$index++
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Rows 1, 4, and 7: an invocation with no pathspec operand claims no path, so there is
|
|
282
|
+
# nothing for the prefix test to scope and the exemption is not available.
|
|
283
|
+
if ($operands.Count -eq 0) {
|
|
284
|
+
return $false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Row 19: all-operands-exempt is the invariant; one non-exempt operand denies the set.
|
|
288
|
+
foreach ($operand in $operands) {
|
|
289
|
+
if (-not (Test-ExemptOrchestrationOperand -Operand $operand)) {
|
|
290
|
+
return $false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return $true
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function Test-ExemptOrchestrationStagingCommand {
|
|
297
|
+
<#
|
|
298
|
+
.SYNOPSIS
|
|
299
|
+
Reports whether a command line is an orchestration-bookkeeping staging invocation.
|
|
300
|
+
.DESCRIPTION
|
|
301
|
+
The entry predicate of the issue #539 exemption. Returns true only when the whole
|
|
302
|
+
command line splits cleanly into segments and EVERY segment parses as a complete,
|
|
303
|
+
recognized staging or integration invocation carrying at least one pathspec operand,
|
|
304
|
+
with every operand resolving inside one of the five exempt orchestration-bookkeeping
|
|
305
|
+
trees after balanced-quote stripping and separator normalization.
|
|
306
|
+
|
|
307
|
+
The all-segments reading is deliberate and fail-closed: a chained line denies unless
|
|
308
|
+
each of its segments is independently a recognized all-exempt invocation, so a
|
|
309
|
+
relocating spelling or a prose fragment anywhere in the line withholds the exemption
|
|
310
|
+
even when another segment on the same line would have qualified on its own.
|
|
311
|
+
|
|
312
|
+
Consumed allow-side only. A false result restores the caller's unchanged
|
|
313
|
+
classification; it never suppresses the trigger.
|
|
314
|
+
.OUTPUTS
|
|
315
|
+
System.Boolean
|
|
316
|
+
#>
|
|
317
|
+
[CmdletBinding()]
|
|
318
|
+
[OutputType([bool])]
|
|
319
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
|
|
320
|
+
|
|
321
|
+
if (-not $CommandText) {
|
|
322
|
+
return $false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Row 12: interpolation and redirection are not statically resolvable, so the operand
|
|
326
|
+
# list cannot be trusted to describe what the line actually touches.
|
|
327
|
+
if ($CommandText.IndexOfAny($script:UnresolvableCommandCharacters) -ge 0) {
|
|
328
|
+
return $false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
$split = Split-OrchestrationCommandLine -CommandText $CommandText
|
|
332
|
+
if (-not $split.Balanced) {
|
|
333
|
+
# Rows 11 and 13: unbalanced quoting makes the segment boundaries ambiguous.
|
|
334
|
+
return $false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
$segments = @($split.Segments)
|
|
338
|
+
if ($segments.Count -eq 0) {
|
|
339
|
+
return $false
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
foreach ($segment in $segments) {
|
|
343
|
+
$tokens = @(ConvertTo-OrchestrationCommandToken -Segment $segment)
|
|
344
|
+
if (-not (Test-ExemptOrchestrationSegmentToken -Token $tokens)) {
|
|
345
|
+
return $false
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return $true
|
|
349
|
+
}
|
|
@@ -10,8 +10,37 @@ param()
|
|
|
10
10
|
# mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
|
|
11
11
|
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
12
12
|
|
|
13
|
+
# Pure pathspec classifier for the issue #539 orchestration-bookkeeping staging exemption.
|
|
14
|
+
# Extracted to a dot-sourced sibling so this file stays inside the 500-line cap, following
|
|
15
|
+
# the headroom-split precedent already used on this side by enforce-completion-helpers.ps1.
|
|
16
|
+
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-helpers.ps1')
|
|
17
|
+
|
|
18
|
+
# The readiness checkpoint this gate reads and names in its block message.
|
|
13
19
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
14
20
|
|
|
21
|
+
# Every orchestration checkpoint a planner or orchestrator surface writes. Writing one
|
|
22
|
+
# of these is orchestration bookkeeping, not implementation, so the gate must not
|
|
23
|
+
# require a ready checkpoint before the checkpoint itself can be created. The set is a
|
|
24
|
+
# list of repo-relative literals behind a single membership check: no directory prefix,
|
|
25
|
+
# no glob, and no absolute-path entry.
|
|
26
|
+
$script:CheckpointPaths = @(
|
|
27
|
+
'artifacts/orchestration/orchestrator-state.json'
|
|
28
|
+
'artifacts/orchestration/parallel-planner-state.json'
|
|
29
|
+
'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
30
|
+
'artifacts/orchestration/epic-planner-state.json'
|
|
31
|
+
'artifacts/orchestration/epic-orchestrator-state.json'
|
|
32
|
+
'artifacts/orchestration/powershell-orchestrator-state.json'
|
|
33
|
+
'artifacts/orchestration/csharp-orchestrator-state.json'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Both markers must appear in the field-scoped prompt for a delegation to qualify as a
|
|
37
|
+
# preparation-mode kickoff. The literals are reused verbatim from
|
|
38
|
+
# .claude/skills/parallel-plan/SKILL.md and .claude/skills/epic-plan/SKILL.md.
|
|
39
|
+
$script:PreparationModeMarkers = @(
|
|
40
|
+
'Preparation mode: true.'
|
|
41
|
+
'route_id: preparation.'
|
|
42
|
+
)
|
|
43
|
+
|
|
15
44
|
function ConvertFrom-CheckpointJson {
|
|
16
45
|
[CmdletBinding()]
|
|
17
46
|
param([Parameter(Mandatory)][string] $Json)
|
|
@@ -38,7 +67,13 @@ function Test-FeatureDocumentationOrEvidencePath {
|
|
|
38
67
|
[OutputType([bool])]
|
|
39
68
|
param([Parameter(Mandatory)][string] $NormalizedPath)
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
# The segment anchor (^|/) admits both the repo-relative spelling and an
|
|
71
|
+
# absolute spelling of the same feature document, which the Write tool
|
|
72
|
+
# supplies by contract. -cmatch is deliberate and must not be normalized into
|
|
73
|
+
# -match: String.StartsWith is case-sensitive, so the case-sensitive operator
|
|
74
|
+
# is what preserves the previous semantics exactly. PowerShell -match is
|
|
75
|
+
# case-insensitive and would widen this predicate.
|
|
76
|
+
return $NormalizedPath -cmatch '(^|/)docs/features/active/'
|
|
42
77
|
}
|
|
43
78
|
|
|
44
79
|
function Test-ImplementationPath {
|
|
@@ -49,8 +84,33 @@ function Test-ImplementationPath {
|
|
|
49
84
|
if (Test-FeatureDocumentationOrEvidencePath -NormalizedPath $NormalizedPath) {
|
|
50
85
|
return $false
|
|
51
86
|
}
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
# Segment-anchored and end-anchored, so an absolute spelling of a checkpoint is
|
|
88
|
+
# exempt exactly as its repo-relative spelling already was. -match is
|
|
89
|
+
# deliberate here and must not be narrowed into -cmatch: -contains was
|
|
90
|
+
# case-insensitive, so the case-insensitive operator is what preserves the
|
|
91
|
+
# previous semantics exactly.
|
|
92
|
+
#
|
|
93
|
+
# Accepted widening: this also exempts a path OUTSIDE the workspace whose tail
|
|
94
|
+
# is an artifacts/orchestration/ segment followed by one of the seven names.
|
|
95
|
+
# Measured exposure in this repository is one matching file, the real
|
|
96
|
+
# checkpoint; there is no nested or vendored second copy. The same widening is
|
|
97
|
+
# already accepted for the identical literal in four other hooks. Resolving a
|
|
98
|
+
# workspace root instead would reintroduce every root-resolution failure mode
|
|
99
|
+
# (8.3 short names, drive-letter case, symlinks, linked worktrees), and a strip
|
|
100
|
+
# that failed to match would leave the path absolute and deny, reinstating the
|
|
101
|
+
# reported defect in a subtler form.
|
|
102
|
+
#
|
|
103
|
+
# Known deliberate miss: a path reaching a checkpoint name only through a '..'
|
|
104
|
+
# hop stays denied. The Write tool does not emit '..' segments, and a
|
|
105
|
+
# canonicalizer would reintroduce filesystem dependence for no measured gain.
|
|
106
|
+
#
|
|
107
|
+
# Idempotence for the apply_patch call site: (^|/) matches at ^, so a
|
|
108
|
+
# repo-relative path harvested from a file marker by Test-ImplementationCommand
|
|
109
|
+
# classifies exactly as it does today.
|
|
110
|
+
foreach ($checkpoint in $script:CheckpointPaths) {
|
|
111
|
+
if ($NormalizedPath -match ('(^|/)' + [regex]::Escape($checkpoint) + '$')) {
|
|
112
|
+
return $false
|
|
113
|
+
}
|
|
54
114
|
}
|
|
55
115
|
return $NormalizedPath -match '\.(py|ps1|psm1|ts|tsx|js|jsx|cs|json|yml|yaml)$'
|
|
56
116
|
}
|
|
@@ -86,14 +146,60 @@ function Test-ImplementationCommand {
|
|
|
86
146
|
'(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
|
|
87
147
|
)
|
|
88
148
|
|
|
89
|
-
|
|
90
|
-
if ($normalizedCommand -
|
|
91
|
-
|
|
149
|
+
for ($index = 0; $index -lt $implementationCommandPatterns.Count; $index++) {
|
|
150
|
+
if ($normalizedCommand -notmatch $implementationCommandPatterns[$index]) {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
# Allow-side only (issue #539). Index 0 is the git staging trigger, whose pattern
|
|
154
|
+
# text is unchanged. It is the sole leg the orchestration-bookkeeping exemption may
|
|
155
|
+
# clear, and only when no other implementation pattern matches the same line: the
|
|
156
|
+
# loop continues rather than returning, so a chained line carrying any non-git
|
|
157
|
+
# implementation segment still classifies as implementation. The apply_patch marker
|
|
158
|
+
# legs above are upstream of this loop and are unmodified.
|
|
159
|
+
if ($index -eq 0 -and (Test-ExemptOrchestrationStagingCommand -CommandText $normalizedCommand)) {
|
|
160
|
+
continue
|
|
92
161
|
}
|
|
162
|
+
return $true
|
|
93
163
|
}
|
|
94
164
|
return $false
|
|
95
165
|
}
|
|
96
166
|
|
|
167
|
+
function Test-PreparationModeDelegation {
|
|
168
|
+
<#
|
|
169
|
+
.SYNOPSIS
|
|
170
|
+
Identifies an orchestrator delegation that is a preparation-mode kickoff.
|
|
171
|
+
.DESCRIPTION
|
|
172
|
+
Returns true only when all three conjuncts hold: the payload is present, the
|
|
173
|
+
delegated agent is exactly 'orchestrator', and the field-scoped prompt carries
|
|
174
|
+
both preparation markers. The prompt is read as a named field via this file's
|
|
175
|
+
own Get-StringProperty helper rather than from the serialized payload, so that
|
|
176
|
+
marker text planted in an unrelated field cannot exempt an implementation
|
|
177
|
+
delegation.
|
|
178
|
+
.OUTPUTS
|
|
179
|
+
System.Boolean
|
|
180
|
+
#>
|
|
181
|
+
[CmdletBinding()]
|
|
182
|
+
[OutputType([bool])]
|
|
183
|
+
param([Parameter(Mandatory)][AllowNull()] $ToolInput)
|
|
184
|
+
|
|
185
|
+
if ($null -eq $ToolInput) {
|
|
186
|
+
return $false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
$subagentType = Get-StringProperty -Value $ToolInput -Name 'subagent_type'
|
|
190
|
+
if ($subagentType -ne 'orchestrator') {
|
|
191
|
+
return $false
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
$prompt = Get-StringProperty -Value $ToolInput -Name 'prompt'
|
|
195
|
+
foreach ($marker in $script:PreparationModeMarkers) {
|
|
196
|
+
if (-not $prompt.Contains($marker)) {
|
|
197
|
+
return $false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return $true
|
|
201
|
+
}
|
|
202
|
+
|
|
97
203
|
function Test-ImplementationDelegation {
|
|
98
204
|
[CmdletBinding()]
|
|
99
205
|
[OutputType([bool])]
|
|
@@ -103,6 +209,17 @@ function Test-ImplementationDelegation {
|
|
|
103
209
|
return $false
|
|
104
210
|
}
|
|
105
211
|
|
|
212
|
+
try {
|
|
213
|
+
if (Test-PreparationModeDelegation -ToolInput $ToolInput) {
|
|
214
|
+
return $false
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
# An envelope the field reader cannot probe falls through to the unchanged
|
|
218
|
+
# whole-payload regex below. An extraction failure must never become an
|
|
219
|
+
# exemption, so the gate stays closed on the stricter classifier.
|
|
220
|
+
Write-Debug "Preparation-mode probe failed: $($_.Exception.Message)"
|
|
221
|
+
}
|
|
222
|
+
|
|
106
223
|
$payloadText = ($ToolInput | ConvertTo-Json -Depth 20 -Compress)
|
|
107
224
|
return $payloadText -match '(python-typed-engineer|powershell-typed-engineer|typescript-engineer|csharp-typed-engineer|atomic-executor|implementation|execute)'
|
|
108
225
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
".codex/hooks/enforce-epic-root-invocation.ps1",
|
|
38
38
|
".codex/hooks/enforce-epic-wave-barrier.ps1",
|
|
39
39
|
".codex/hooks/enforce-epic-worktree-removal-gate.ps1",
|
|
40
|
+
".codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1",
|
|
40
41
|
".codex/hooks/record-subagent-routing-attestation.ps1",
|
|
41
42
|
".codex/hooks/validate-codex-subagent-routing.ps1",
|
|
42
43
|
".codex/scripts/epic-child-launch-contract.ps1",
|