@danmoisan/drm-copilot-mcp 1.1.4 → 1.1.6
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 +335 -24
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +163 -18
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +477 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +120 -12
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +11 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +1 -0
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +131 -2
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +37 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +136 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +18 -9
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +8 -1
- package/resources/claude-customizations/.claude/skills/pr-context-artifacts/SKILL.md +22 -0
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +10 -0
- package/resources/claude-customizations/config/orchestration-routing.json +1 -0
- package/resources/claude-customizations/pack-manifests/core.json +1 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +10 -0
- package/resources/codex-and-agents-customizations/.agents/skills/pr-context-artifacts/SKILL.md +22 -0
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c1.toml +22 -0
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c2.toml +22 -0
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3-elevated.toml +22 -0
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3.toml +22 -0
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c4.toml +22 -0
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward.toml +2 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +7 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +7 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +7 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +7 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +7 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +7 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +477 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +125 -12
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +8 -1
- package/resources/config/orchestration-routing.json +1 -0
- package/resources/customizations/.github/skills/pr-context-artifacts/SKILL.md +22 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +10 -2
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Mode dispatch and per-mode readiness predicates for the orchestration
|
|
4
|
+
preimplementation gate (Claude surface).
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Normative contract: the issue #554 mode dispatch and readiness predicates.
|
|
7
|
+
This file owns the fixed mode table, the canonical checkpoint-path map, the
|
|
8
|
+
implementation-agent allow-list, mode resolution, the target-token finder, the
|
|
9
|
+
prompt-declared-path cross-check, and the epic and parallel readiness
|
|
10
|
+
predicates. It owns nothing else.
|
|
11
|
+
|
|
12
|
+
The readiness source is resolved from the recognized mode marker through the
|
|
13
|
+
fixed table below, and NEVER from a path parsed out of a prompt: a delegation
|
|
14
|
+
that named its own readiness file would choose its own gate. A prompt-declared
|
|
15
|
+
path is a cross-check operand only. The posture follows the shipped precedents
|
|
16
|
+
enforce-epic-wave-barrier.ps1 and enforce-parallel-cohort-barrier.ps1.
|
|
17
|
+
|
|
18
|
+
PURITY. This file is pure string and object logic, with no filesystem,
|
|
19
|
+
process, network, or environment access: it opens no file, probes no path,
|
|
20
|
+
issues no web request, launches no executable, and imports no module. Every
|
|
21
|
+
readiness predicate accepts an ALREADY-PARSED checkpoint object, or $null; the
|
|
22
|
+
per-mode read seams live in the main gate hook. It is a new sibling rather than
|
|
23
|
+
an addition to the issue #539 helpers file, whose header declares a different
|
|
24
|
+
normative contract and which lacks headroom under the 500-line cap; leaving
|
|
25
|
+
that file byte-untouched is the proof the #539 exemption is unchanged.
|
|
26
|
+
#>
|
|
27
|
+
[CmdletBinding()]
|
|
28
|
+
param()
|
|
29
|
+
|
|
30
|
+
# --- Constant table 1: the fixed mode table --------------------------------------
|
|
31
|
+
# Markers are reused verbatim from shipped contracts and hooks; do not invent them.
|
|
32
|
+
# The trailing-period asymmetry is deliberate: preparation markers are matched WITH
|
|
33
|
+
# their periods as the shipped gate hook does and an existing test pins, epic and
|
|
34
|
+
# parallel WITHOUT, as the two barrier hooks do, which makes the three hooks on the
|
|
35
|
+
# same Agent matcher agree. MatchCase carries the same asymmetry. Both forms are
|
|
36
|
+
# containment tests over the prompt, so neither is sensitive to edge whitespace.
|
|
37
|
+
# Rows evaluate in order, so preparation is first and exempts; all markers on a row
|
|
38
|
+
# must be present for that row to match.
|
|
39
|
+
$script:OrchestrationDelegationModeTable = @(
|
|
40
|
+
[pscustomobject]@{
|
|
41
|
+
Mode = 'preparation'
|
|
42
|
+
Markers = @('Preparation mode: true.', 'route_id: preparation.')
|
|
43
|
+
MatchCase = $true
|
|
44
|
+
}
|
|
45
|
+
[pscustomobject]@{ Mode = 'epic'; Markers = @('Epic mode: true'); MatchCase = $false }
|
|
46
|
+
[pscustomobject]@{ Mode = 'parallel'; Markers = @('Parallel mode: true'); MatchCase = $false }
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# The mode a prompt carrying no recognized marker resolves to.
|
|
50
|
+
$script:OrchestrationDelegationDefaultMode = 'single-feature'
|
|
51
|
+
|
|
52
|
+
# --- Constant table 2: the canonical checkpoint-path map -------------------------
|
|
53
|
+
# Preparation is exempt and has no readiness source, expressed as an empty string
|
|
54
|
+
# so callers test it with one truthiness check. No value here is ever derived from
|
|
55
|
+
# prompt text.
|
|
56
|
+
$script:OrchestrationDelegationCheckpointPathMap = [ordered]@{
|
|
57
|
+
'preparation' = ''
|
|
58
|
+
'epic' = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
59
|
+
'parallel' = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
60
|
+
'single-feature' = 'artifacts/orchestration/orchestrator-state.json'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# --- Constant table 3: the implementation-agent allow-list -----------------------
|
|
64
|
+
# Exactly five members: the agent tokens carried over from the replaced seven-token
|
|
65
|
+
# regex, dropping only the two free-text tokens. Retaining atomic-executor and the
|
|
66
|
+
# four typed-engineer names is a hard invariant; pre-existing cases supply two of
|
|
67
|
+
# them and assert deny.
|
|
68
|
+
$script:OrchestrationImplementationAgentAllowList = @(
|
|
69
|
+
'python-typed-engineer'
|
|
70
|
+
'powershell-typed-engineer'
|
|
71
|
+
'typescript-engineer'
|
|
72
|
+
'csharp-typed-engineer'
|
|
73
|
+
'atomic-executor'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
function Get-OrchestrationModeProperty {
|
|
77
|
+
<#
|
|
78
|
+
.SYNOPSIS
|
|
79
|
+
Reads a named property off an already-parsed object, or $null. The single
|
|
80
|
+
field-access seam here; never throws.
|
|
81
|
+
#>
|
|
82
|
+
[CmdletBinding()]
|
|
83
|
+
[OutputType([object])]
|
|
84
|
+
param(
|
|
85
|
+
[Parameter(Mandatory)][AllowNull()] $Value,
|
|
86
|
+
[Parameter(Mandatory)][string] $Name
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if ($null -eq $Value) { return $null }
|
|
90
|
+
$properties = $null
|
|
91
|
+
try {
|
|
92
|
+
$properties = $Value.PSObject.Properties
|
|
93
|
+
} catch {
|
|
94
|
+
Write-Debug "Property probe failed for '$Name': $($_.Exception.Message)"
|
|
95
|
+
return $null
|
|
96
|
+
}
|
|
97
|
+
if ($null -eq $properties -or -not ($properties.Name -contains $Name)) { return $null }
|
|
98
|
+
return $properties[$Name].Value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function Get-OrchestrationModeString {
|
|
102
|
+
<#
|
|
103
|
+
.SYNOPSIS
|
|
104
|
+
Reads a named property as a trimmed string, or an empty string. The name is
|
|
105
|
+
distinct from the gate hooks' Get-StringProperty so it cannot shadow it.
|
|
106
|
+
#>
|
|
107
|
+
[CmdletBinding()]
|
|
108
|
+
[OutputType([string])]
|
|
109
|
+
param(
|
|
110
|
+
[Parameter(Mandatory)][AllowNull()] $Value,
|
|
111
|
+
[Parameter(Mandatory)][string] $Name
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
$raw = Get-OrchestrationModeProperty -Value $Value -Name $Name
|
|
115
|
+
if ($null -eq $raw) { return '' }
|
|
116
|
+
return ([string]$raw).Trim()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Get-OrchestrationModeCollection {
|
|
120
|
+
<#
|
|
121
|
+
.SYNOPSIS
|
|
122
|
+
Reads a named property as an array, or an empty array.
|
|
123
|
+
#>
|
|
124
|
+
[CmdletBinding()]
|
|
125
|
+
[OutputType([object[]])]
|
|
126
|
+
param(
|
|
127
|
+
[Parameter(Mandatory)][AllowNull()] $Value,
|
|
128
|
+
[Parameter(Mandatory)][string] $Name
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
$raw = Get-OrchestrationModeProperty -Value $Value -Name $Name
|
|
132
|
+
if ($null -eq $raw) { return @() }
|
|
133
|
+
return @($raw)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function Get-OrchestrationModeFolderBasename {
|
|
137
|
+
<#
|
|
138
|
+
.SYNOPSIS
|
|
139
|
+
Normalizes a feature_folder value to its bare basename. A record's value
|
|
140
|
+
may be a full path with a lifecycle prefix or a bare basename, so both
|
|
141
|
+
sides of every comparison are normalized, following the cohort-barrier hook.
|
|
142
|
+
#>
|
|
143
|
+
[CmdletBinding()]
|
|
144
|
+
[OutputType([string])]
|
|
145
|
+
param([AllowNull()][AllowEmptyString()][string] $Path)
|
|
146
|
+
|
|
147
|
+
if (-not $Path) { return '' }
|
|
148
|
+
$normalized = ($Path -replace '\\', '/').TrimEnd('/')
|
|
149
|
+
if (-not $normalized) { return '' }
|
|
150
|
+
return ($normalized -split '/')[-1]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function Resolve-OrchestrationDelegationMode {
|
|
154
|
+
<#
|
|
155
|
+
.SYNOPSIS
|
|
156
|
+
Resolves a delegation prompt to one of the four mode names. Reads nothing
|
|
157
|
+
but the supplied string; evaluates preparation, then epic, then parallel,
|
|
158
|
+
then the default. Null and empty prompts resolve to the default. No
|
|
159
|
+
checkpoint path is ever read out of a prompt.
|
|
160
|
+
#>
|
|
161
|
+
[CmdletBinding()]
|
|
162
|
+
[OutputType([string])]
|
|
163
|
+
param([AllowNull()][AllowEmptyString()][string] $Prompt)
|
|
164
|
+
|
|
165
|
+
if (-not $Prompt) { return $script:OrchestrationDelegationDefaultMode }
|
|
166
|
+
|
|
167
|
+
foreach ($row in $script:OrchestrationDelegationModeTable) {
|
|
168
|
+
$allPresent = $true
|
|
169
|
+
foreach ($marker in $row.Markers) {
|
|
170
|
+
$present = if ($row.MatchCase) {
|
|
171
|
+
$Prompt.Contains($marker)
|
|
172
|
+
} else {
|
|
173
|
+
$Prompt -like ('*' + $marker + '*')
|
|
174
|
+
}
|
|
175
|
+
if (-not $present) {
|
|
176
|
+
$allPresent = $false
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if ($allPresent) { return $row.Mode }
|
|
181
|
+
}
|
|
182
|
+
return $script:OrchestrationDelegationDefaultMode
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function Get-OrchestrationDelegationCheckpointPath {
|
|
186
|
+
<#
|
|
187
|
+
.SYNOPSIS
|
|
188
|
+
Returns the canonical readiness source for a mode name, from the fixed
|
|
189
|
+
table and nowhere else. Preparation returns an empty string, as does an
|
|
190
|
+
unrecognized name, so no caller can manufacture a source.
|
|
191
|
+
#>
|
|
192
|
+
[CmdletBinding()]
|
|
193
|
+
[OutputType([string])]
|
|
194
|
+
param([AllowNull()][AllowEmptyString()][string] $Mode)
|
|
195
|
+
|
|
196
|
+
if (-not $Mode -or -not $script:OrchestrationDelegationCheckpointPathMap.Contains($Mode)) {
|
|
197
|
+
return ''
|
|
198
|
+
}
|
|
199
|
+
return [string]$script:OrchestrationDelegationCheckpointPathMap[$Mode]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function Test-OrchestrationImplementationAgent {
|
|
203
|
+
<#
|
|
204
|
+
.SYNOPSIS
|
|
205
|
+
Tests a subagent_type value against the implementation-agent allow-list.
|
|
206
|
+
#>
|
|
207
|
+
[CmdletBinding()]
|
|
208
|
+
[OutputType([bool])]
|
|
209
|
+
param([AllowNull()][AllowEmptyString()][string] $SubagentType)
|
|
210
|
+
|
|
211
|
+
if (-not $SubagentType) { return $false }
|
|
212
|
+
return ($script:OrchestrationImplementationAgentAllowList -contains $SubagentType)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function Find-OrchestrationDelegationTargetFolder {
|
|
216
|
+
<#
|
|
217
|
+
.SYNOPSIS
|
|
218
|
+
Resolves the target feature-folder basename out of a delegation prompt,
|
|
219
|
+
reusing the wave-barrier technique in shape: scan for slash-separated
|
|
220
|
+
docs/features/active/ tokens, longest unique match wins, a Markdown match
|
|
221
|
+
resolves to its parent, and the basename is returned. $null when no token
|
|
222
|
+
resolves, which the caller treats as a deny.
|
|
223
|
+
#>
|
|
224
|
+
[CmdletBinding()]
|
|
225
|
+
[OutputType([string])]
|
|
226
|
+
param([AllowNull()][AllowEmptyString()][string] $Prompt)
|
|
227
|
+
|
|
228
|
+
if (-not $Prompt) { return $null }
|
|
229
|
+
|
|
230
|
+
$pattern = 'docs[\\/]+features[\\/]+active[\\/]+[^\s"''`]+'
|
|
231
|
+
$matchList = [regex]::Matches($Prompt, $pattern)
|
|
232
|
+
if ($matchList.Count -eq 0) { return $null }
|
|
233
|
+
|
|
234
|
+
$unique = [ordered]@{}
|
|
235
|
+
foreach ($item in $matchList) { $unique[$item.Value] = $true }
|
|
236
|
+
$candidates = @(@($unique.Keys) | Sort-Object -Property Length -Descending)
|
|
237
|
+
$best = [string]$candidates[0]
|
|
238
|
+
|
|
239
|
+
# Sentence punctuation trails a bare path token in every shipped kickoff
|
|
240
|
+
# contract. A trailing period is stripped only when it does not form the
|
|
241
|
+
# Markdown extension the next branch depends on.
|
|
242
|
+
$best = $best.TrimEnd(',', ';', ':')
|
|
243
|
+
while ($best.EndsWith('.') -and -not $best.EndsWith('.md')) {
|
|
244
|
+
$best = $best.Substring(0, $best.Length - 1)
|
|
245
|
+
}
|
|
246
|
+
if ($best -match '\.md$') { $best = $best -replace '[\\/][^\\/]+\.md$', '' }
|
|
247
|
+
|
|
248
|
+
$basename = Get-OrchestrationModeFolderBasename -Path $best
|
|
249
|
+
if (-not $basename) { return $null }
|
|
250
|
+
return $basename
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function Find-OrchestrationDelegationIssueNumber {
|
|
254
|
+
<#
|
|
255
|
+
.SYNOPSIS
|
|
256
|
+
Resolves an issue number out of a delegation prompt, as a string. The
|
|
257
|
+
alternative target resolution of decision D3, issue_num being the primary
|
|
258
|
+
key on both checkpoints. The keyed form is preferred over the bare hash
|
|
259
|
+
form; $null when neither resolves. Accepted widening: a bare hash form such
|
|
260
|
+
as a pull-request reference can supply a number that is not the target's,
|
|
261
|
+
which widens the SEARCH only - an unmatched number yields no record and
|
|
262
|
+
denies, so deny-by-default is preserved.
|
|
263
|
+
#>
|
|
264
|
+
[CmdletBinding()]
|
|
265
|
+
[OutputType([string])]
|
|
266
|
+
param([AllowNull()][AllowEmptyString()][string] $Prompt)
|
|
267
|
+
|
|
268
|
+
if (-not $Prompt) { return $null }
|
|
269
|
+
|
|
270
|
+
$keyed = [regex]::Match($Prompt, 'issue[_-]?num(?:ber)?\s*[:=]\s*#?(\d+)', 'IgnoreCase')
|
|
271
|
+
if ($keyed.Success) { return $keyed.Groups[1].Value }
|
|
272
|
+
$hashForm = [regex]::Match($Prompt, '(?:^|\s)#(\d+)\b')
|
|
273
|
+
if ($hashForm.Success) { return $hashForm.Groups[1].Value }
|
|
274
|
+
return $null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function Test-OrchestrationDelegationDeclaredCheckpointPath {
|
|
278
|
+
<#
|
|
279
|
+
.SYNOPSIS
|
|
280
|
+
Cross-checks a prompt-declared checkpoint path against the mode's canonical
|
|
281
|
+
path. True only when the prompt declares none for the mode, or declares one
|
|
282
|
+
equal to the canonical value. The declared value is a cross-check operand
|
|
283
|
+
ONLY and never selects a source; a disagreement is a deny. A mode with no
|
|
284
|
+
declared-path key returns true.
|
|
285
|
+
#>
|
|
286
|
+
[CmdletBinding()]
|
|
287
|
+
[OutputType([bool])]
|
|
288
|
+
param(
|
|
289
|
+
[AllowNull()][AllowEmptyString()][string] $Prompt,
|
|
290
|
+
[AllowNull()][AllowEmptyString()][string] $Mode
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if ($Mode -ne 'epic' -and $Mode -ne 'parallel') { return $true }
|
|
294
|
+
if (-not $Prompt) { return $true }
|
|
295
|
+
|
|
296
|
+
$canonical = Get-OrchestrationDelegationCheckpointPath -Mode $Mode
|
|
297
|
+
$key = $Mode + '_checkpoint_path'
|
|
298
|
+
$declaredMatch = [regex]::Match(
|
|
299
|
+
$Prompt, [regex]::Escape($key) + '\s*[:=]\s*([^\s"''`]+)', 'IgnoreCase')
|
|
300
|
+
if (-not $declaredMatch.Success) { return $true }
|
|
301
|
+
|
|
302
|
+
$declared = $declaredMatch.Groups[1].Value.TrimEnd(',', ';', ':')
|
|
303
|
+
while ($declared.EndsWith('.') -and -not $declared.EndsWith('.json')) {
|
|
304
|
+
$declared = $declared.Substring(0, $declared.Length - 1)
|
|
305
|
+
}
|
|
306
|
+
$declared = $declared -replace '\\', '/'
|
|
307
|
+
return ($declared -eq $canonical)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function Test-OrchestrationModeTerminalMergeStatus {
|
|
311
|
+
<#
|
|
312
|
+
.SYNOPSIS
|
|
313
|
+
Tests whether a target record's merge_status is terminal-merged (decision
|
|
314
|
+
D8). The two terminal members are merged and worktree_removed, the same two
|
|
315
|
+
the barrier hooks treat as terminal-safe. Every other member, including the
|
|
316
|
+
failure members, is pre-merge here: re-delegation after a blocked or
|
|
317
|
+
conflicted state is legitimate remediation and must not be gated off. An
|
|
318
|
+
ABSENT merge_status is treated as not_started, per parallel invariant 7.
|
|
319
|
+
This predicate CONSUMES the existing member sets and extends neither.
|
|
320
|
+
#>
|
|
321
|
+
[CmdletBinding()]
|
|
322
|
+
[OutputType([bool])]
|
|
323
|
+
param([Parameter(Mandatory)][AllowNull()] $Record)
|
|
324
|
+
|
|
325
|
+
if ($null -eq $Record) { return $false }
|
|
326
|
+
$status = Get-OrchestrationModeString -Value $Record -Name 'merge_status'
|
|
327
|
+
if (-not $status) { return $false }
|
|
328
|
+
return (@('merged', 'worktree_removed') -contains $status)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function Find-OrchestrationModeRecord {
|
|
332
|
+
<#
|
|
333
|
+
.SYNOPSIS
|
|
334
|
+
Finds the target record in a checkpoint's feature or item collection,
|
|
335
|
+
matching the normalized feature_folder basename first and issue_num second.
|
|
336
|
+
$null when neither resolves, which is a failed conjunct.
|
|
337
|
+
#>
|
|
338
|
+
[CmdletBinding()]
|
|
339
|
+
[OutputType([object])]
|
|
340
|
+
param(
|
|
341
|
+
[Parameter(Mandatory)][AllowNull()] $Records,
|
|
342
|
+
[AllowNull()][AllowEmptyString()][string] $TargetFolder,
|
|
343
|
+
[AllowNull()][AllowEmptyString()][string] $IssueNumber
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if ($null -eq $Records) { return $null }
|
|
347
|
+
foreach ($record in @($Records)) {
|
|
348
|
+
if ($null -eq $record) { continue }
|
|
349
|
+
if ($TargetFolder) {
|
|
350
|
+
$folder = Get-OrchestrationModeString -Value $record -Name 'feature_folder'
|
|
351
|
+
$basename = Get-OrchestrationModeFolderBasename -Path $folder
|
|
352
|
+
if ($basename -and $basename -eq $TargetFolder) { return $record }
|
|
353
|
+
}
|
|
354
|
+
if ($IssueNumber) {
|
|
355
|
+
$issue = Get-OrchestrationModeString -Value $record -Name 'issue_num'
|
|
356
|
+
if ($issue -and $issue -eq $IssueNumber) { return $record }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return $null
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function Get-EpicOrchestrationReadinessFailure {
|
|
363
|
+
<#
|
|
364
|
+
.SYNOPSIS
|
|
365
|
+
Names the first failed epic readiness conjunct, or returns an empty string.
|
|
366
|
+
Accepts an already-parsed checkpoint object or $null and enforces, in
|
|
367
|
+
order: route_id exactly epic; non-empty epic_feature_folder; non-empty
|
|
368
|
+
epic_manifest_path under docs/features/epics/; non-empty
|
|
369
|
+
integration_branch; present and non-empty features; the resolved target
|
|
370
|
+
present as a record in features; and that record's merge_status neither
|
|
371
|
+
merged nor worktree_removed. The epic_manifest_path conjunct deliberately
|
|
372
|
+
tightens relative to validate_epic_orchestrator_state.py, whose
|
|
373
|
+
required-key set omits it, but is not stricter than the producing skill's
|
|
374
|
+
contract, which mandates it; a false deny names the failed conjunct.
|
|
375
|
+
#>
|
|
376
|
+
[CmdletBinding()]
|
|
377
|
+
[OutputType([string])]
|
|
378
|
+
param(
|
|
379
|
+
[Parameter(Mandatory)][AllowNull()] $Checkpoint,
|
|
380
|
+
[AllowNull()][AllowEmptyString()][string] $TargetFolder,
|
|
381
|
+
[AllowNull()][AllowEmptyString()][string] $IssueNumber
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if ($null -eq $Checkpoint) { return 'checkpoint-absent' }
|
|
385
|
+
if ((Get-OrchestrationModeString -Value $Checkpoint -Name 'route_id') -ne 'epic') {
|
|
386
|
+
return 'route_id'
|
|
387
|
+
}
|
|
388
|
+
if (-not (Get-OrchestrationModeString -Value $Checkpoint -Name 'epic_feature_folder')) {
|
|
389
|
+
return 'epic_feature_folder'
|
|
390
|
+
}
|
|
391
|
+
$manifest = (Get-OrchestrationModeString -Value $Checkpoint -Name 'epic_manifest_path') -replace '\\', '/'
|
|
392
|
+
if (-not $manifest -or $manifest -notmatch '(^|/)docs/features/epics/') {
|
|
393
|
+
return 'epic_manifest_path'
|
|
394
|
+
}
|
|
395
|
+
if (-not (Get-OrchestrationModeString -Value $Checkpoint -Name 'integration_branch')) {
|
|
396
|
+
return 'integration_branch'
|
|
397
|
+
}
|
|
398
|
+
$features = Get-OrchestrationModeCollection -Value $Checkpoint -Name 'features'
|
|
399
|
+
if ($features.Count -eq 0) { return 'features' }
|
|
400
|
+
$record = Find-OrchestrationModeRecord -Records $features -TargetFolder $TargetFolder -IssueNumber $IssueNumber
|
|
401
|
+
if ($null -eq $record) { return 'target-record' }
|
|
402
|
+
if (Test-OrchestrationModeTerminalMergeStatus -Record $record) { return 'merge_status' }
|
|
403
|
+
return ''
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function Test-EpicOrchestrationReady {
|
|
407
|
+
<#
|
|
408
|
+
.SYNOPSIS
|
|
409
|
+
Boolean wrapper over Get-EpicOrchestrationReadinessFailure.
|
|
410
|
+
#>
|
|
411
|
+
[CmdletBinding()]
|
|
412
|
+
[OutputType([bool])]
|
|
413
|
+
param(
|
|
414
|
+
[Parameter(Mandatory)][AllowNull()] $Checkpoint,
|
|
415
|
+
[AllowNull()][AllowEmptyString()][string] $TargetFolder,
|
|
416
|
+
[AllowNull()][AllowEmptyString()][string] $IssueNumber
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
$failure = Get-EpicOrchestrationReadinessFailure -Checkpoint $Checkpoint `
|
|
420
|
+
-TargetFolder $TargetFolder -IssueNumber $IssueNumber
|
|
421
|
+
return (-not $failure)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function Get-ParallelOrchestrationReadinessFailure {
|
|
425
|
+
<#
|
|
426
|
+
.SYNOPSIS
|
|
427
|
+
Names the first failed parallel readiness conjunct, or an empty string.
|
|
428
|
+
Accepts an already-parsed checkpoint object or $null and enforces, in
|
|
429
|
+
order: route_id exactly parallel; non-empty parallel_slug; non-empty
|
|
430
|
+
parallel_manifest_path; present and non-empty items; the resolved target
|
|
431
|
+
present as a record in items; and that record's merge_status neither merged
|
|
432
|
+
nor worktree_removed. It consumes the parallel item-state and merge-status
|
|
433
|
+
member sets and adds no member to either.
|
|
434
|
+
#>
|
|
435
|
+
[CmdletBinding()]
|
|
436
|
+
[OutputType([string])]
|
|
437
|
+
param(
|
|
438
|
+
[Parameter(Mandatory)][AllowNull()] $Checkpoint,
|
|
439
|
+
[AllowNull()][AllowEmptyString()][string] $TargetFolder,
|
|
440
|
+
[AllowNull()][AllowEmptyString()][string] $IssueNumber
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if ($null -eq $Checkpoint) { return 'checkpoint-absent' }
|
|
444
|
+
if ((Get-OrchestrationModeString -Value $Checkpoint -Name 'route_id') -ne 'parallel') {
|
|
445
|
+
return 'route_id'
|
|
446
|
+
}
|
|
447
|
+
if (-not (Get-OrchestrationModeString -Value $Checkpoint -Name 'parallel_slug')) {
|
|
448
|
+
return 'parallel_slug'
|
|
449
|
+
}
|
|
450
|
+
if (-not (Get-OrchestrationModeString -Value $Checkpoint -Name 'parallel_manifest_path')) {
|
|
451
|
+
return 'parallel_manifest_path'
|
|
452
|
+
}
|
|
453
|
+
$items = Get-OrchestrationModeCollection -Value $Checkpoint -Name 'items'
|
|
454
|
+
if ($items.Count -eq 0) { return 'items' }
|
|
455
|
+
$record = Find-OrchestrationModeRecord -Records $items -TargetFolder $TargetFolder -IssueNumber $IssueNumber
|
|
456
|
+
if ($null -eq $record) { return 'target-record' }
|
|
457
|
+
if (Test-OrchestrationModeTerminalMergeStatus -Record $record) { return 'merge_status' }
|
|
458
|
+
return ''
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function Test-ParallelOrchestrationReady {
|
|
462
|
+
<#
|
|
463
|
+
.SYNOPSIS
|
|
464
|
+
Boolean wrapper over Get-ParallelOrchestrationReadinessFailure.
|
|
465
|
+
#>
|
|
466
|
+
[CmdletBinding()]
|
|
467
|
+
[OutputType([bool])]
|
|
468
|
+
param(
|
|
469
|
+
[Parameter(Mandatory)][AllowNull()] $Checkpoint,
|
|
470
|
+
[AllowNull()][AllowEmptyString()][string] $TargetFolder,
|
|
471
|
+
[AllowNull()][AllowEmptyString()][string] $IssueNumber
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
$failure = Get-ParallelOrchestrationReadinessFailure -Checkpoint $Checkpoint `
|
|
475
|
+
-TargetFolder $TargetFolder -IssueNumber $IssueNumber
|
|
476
|
+
return (-not $failure)
|
|
477
|
+
}
|
|
@@ -13,6 +13,12 @@ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -
|
|
|
13
13
|
# the enforce-pr-author-skill.ps1 headroom-split precedent.
|
|
14
14
|
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-helpers.ps1')
|
|
15
15
|
|
|
16
|
+
# Pure mode dispatch and per-mode readiness predicates for issue #554. A new sibling
|
|
17
|
+
# rather than an addition to the helpers file above, whose header declares a different
|
|
18
|
+
# normative contract and which lacks headroom; leaving that file byte-untouched is the
|
|
19
|
+
# proof the issue #539 exemption is behaviourally unchanged.
|
|
20
|
+
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-modes.ps1')
|
|
21
|
+
|
|
16
22
|
# The readiness checkpoint this gate reads and names in its block message.
|
|
17
23
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
18
24
|
|
|
@@ -179,6 +185,14 @@ function Test-PreparationModeDelegation {
|
|
|
179
185
|
return $true
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
# Classifies an Agent delegation as implementation by STRUCTURE (issue #554). Both
|
|
189
|
+
# reads are field-scoped through Get-ClaudeHookToolInputString, and the whole-payload
|
|
190
|
+
# serialization scan this function used to perform is removed: it let any field, and
|
|
191
|
+
# two ordinary English words, decide the outcome, so marker text planted outside
|
|
192
|
+
# 'prompt' changed the classification and rewording a prompt changed the decision.
|
|
193
|
+
# Neither can happen now. An allow-listed subagent_type is implementation whatever the
|
|
194
|
+
# prompt says; any other non-orchestrator subagent_type is not; an orchestrator whose
|
|
195
|
+
# resolved mode is preparation is not, and every other orchestrator is.
|
|
182
196
|
function Test-ImplementationDelegation {
|
|
183
197
|
[CmdletBinding()]
|
|
184
198
|
[OutputType([bool])]
|
|
@@ -188,19 +202,16 @@ function Test-ImplementationDelegation {
|
|
|
188
202
|
return $false
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
# whole-payload regex below. An extraction failure must never become an
|
|
198
|
-
# exemption, so the gate stays closed on the stricter classifier.
|
|
199
|
-
Write-Debug "Preparation-mode probe failed: $($_.Exception.Message)"
|
|
205
|
+
$subagentType = Get-ClaudeHookToolInputString -ToolInput $ToolInput -Name 'subagent_type'
|
|
206
|
+
if (Test-OrchestrationImplementationAgent -SubagentType $subagentType) {
|
|
207
|
+
return $true
|
|
208
|
+
}
|
|
209
|
+
if ($subagentType -ne 'orchestrator') {
|
|
210
|
+
return $false
|
|
200
211
|
}
|
|
201
212
|
|
|
202
|
-
$
|
|
203
|
-
return $
|
|
213
|
+
$prompt = Get-ClaudeHookToolInputString -ToolInput $ToolInput -Name 'prompt'
|
|
214
|
+
return ((Resolve-OrchestrationDelegationMode -Prompt $prompt) -ne 'preparation')
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
function Test-OrchestrationReady {
|
|
@@ -244,6 +255,33 @@ function Get-CheckpointContent {
|
|
|
244
255
|
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
245
256
|
}
|
|
246
257
|
|
|
258
|
+
# The two per-mode read seams (issue #554). Each takes its path from the fixed mode
|
|
259
|
+
# table and never from a delegation's own text; an absent file returns an empty
|
|
260
|
+
# string, which the readiness predicate then treats as a deny.
|
|
261
|
+
function Get-EpicCheckpointContent {
|
|
262
|
+
[CmdletBinding()]
|
|
263
|
+
[OutputType([string])]
|
|
264
|
+
param()
|
|
265
|
+
|
|
266
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
|
|
267
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
268
|
+
return ''
|
|
269
|
+
}
|
|
270
|
+
return Get-Content -Raw -LiteralPath $path
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function Get-ParallelCheckpointContent {
|
|
274
|
+
[CmdletBinding()]
|
|
275
|
+
[OutputType([string])]
|
|
276
|
+
param()
|
|
277
|
+
|
|
278
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
|
|
279
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
280
|
+
return ''
|
|
281
|
+
}
|
|
282
|
+
return Get-Content -Raw -LiteralPath $path
|
|
283
|
+
}
|
|
284
|
+
|
|
247
285
|
function Get-OrchestrationPreimplementationGateAllowDecision {
|
|
248
286
|
[CmdletBinding()]
|
|
249
287
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -274,6 +312,23 @@ function Get-OrchestrationPreimplementationGateBlockDecision {
|
|
|
274
312
|
}
|
|
275
313
|
}
|
|
276
314
|
|
|
315
|
+
# Builds a mode-specific deny reason naming the checkpoint actually consulted and the
|
|
316
|
+
# predicate that failed, behind the unchanged PREIMPLEMENTATION_GATE_BLOCKED prefix
|
|
317
|
+
# that downstream reason-matching reads.
|
|
318
|
+
function Get-OrchestrationModeDenyReason {
|
|
319
|
+
[CmdletBinding()]
|
|
320
|
+
[OutputType([string])]
|
|
321
|
+
param(
|
|
322
|
+
[Parameter(Mandatory)][string] $Mode,
|
|
323
|
+
[Parameter(Mandatory)][string] $Failure
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode $Mode
|
|
327
|
+
return ("PREIMPLEMENTATION_GATE_BLOCKED: this $Mode-mode delegation was evaluated against " +
|
|
328
|
+
"$path, and the failed readiness predicate is '$Failure'. Implementation operations " +
|
|
329
|
+
'require that checkpoint to satisfy every readiness predicate before implementation begins.')
|
|
330
|
+
}
|
|
331
|
+
|
|
277
332
|
function Invoke-OrchestrationPreimplementationGateDecision {
|
|
278
333
|
[CmdletBinding()]
|
|
279
334
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -282,7 +337,21 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
282
337
|
[AllowEmptyString()]
|
|
283
338
|
[string] $ToolInputRaw,
|
|
284
339
|
|
|
285
|
-
[string] $CheckpointRaw
|
|
340
|
+
[string] $CheckpointRaw,
|
|
341
|
+
|
|
342
|
+
# The two per-mode injection parameters (issue #554, decision D2). Each
|
|
343
|
+
# overrides its read seam whenever the caller BINDS it, decided with
|
|
344
|
+
# ContainsKey and never with a truthiness test, so an explicitly supplied
|
|
345
|
+
# empty string suppresses the seam instead of falling through to disk. The
|
|
346
|
+
# two parameters above keep their existing names, positions, attributes,
|
|
347
|
+
# and truthiness-based fall-through exactly.
|
|
348
|
+
[AllowNull()]
|
|
349
|
+
[AllowEmptyString()]
|
|
350
|
+
[string] $EpicCheckpointRaw,
|
|
351
|
+
|
|
352
|
+
[AllowNull()]
|
|
353
|
+
[AllowEmptyString()]
|
|
354
|
+
[string] $ParallelCheckpointRaw
|
|
286
355
|
)
|
|
287
356
|
|
|
288
357
|
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
@@ -296,6 +365,12 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
296
365
|
$toolInput = $payload.Value
|
|
297
366
|
|
|
298
367
|
$requiresReadyCheckpoint = $false
|
|
368
|
+
# The path and command legs are single-feature by construction; only the
|
|
369
|
+
# delegation leg carries a mode marker, so only it can move the mode off the
|
|
370
|
+
# default. Both other legs therefore keep the default readiness source and the
|
|
371
|
+
# default wording they have today.
|
|
372
|
+
$mode = $script:OrchestrationDelegationDefaultMode
|
|
373
|
+
$prompt = ''
|
|
299
374
|
$filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
|
|
300
375
|
if ($filePath) {
|
|
301
376
|
$normalized = ([string]$filePath) -replace '\\', '/'
|
|
@@ -306,6 +381,10 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
306
381
|
$requiresReadyCheckpoint = Test-ImplementationCommand -Command $command
|
|
307
382
|
} else {
|
|
308
383
|
$requiresReadyCheckpoint = Test-ImplementationDelegation -ToolInput $toolInput
|
|
384
|
+
if ($requiresReadyCheckpoint) {
|
|
385
|
+
$prompt = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'prompt'
|
|
386
|
+
$mode = Resolve-OrchestrationDelegationMode -Prompt $prompt
|
|
387
|
+
}
|
|
309
388
|
}
|
|
310
389
|
}
|
|
311
390
|
|
|
@@ -313,6 +392,35 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
313
392
|
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
314
393
|
}
|
|
315
394
|
|
|
395
|
+
# A prompt-declared checkpoint path is a cross-check operand only and never
|
|
396
|
+
# selects a source: a delegation that named its own readiness file would choose
|
|
397
|
+
# its own gate. Disagreement with the mode's canonical path is a deny.
|
|
398
|
+
if (-not (Test-OrchestrationDelegationDeclaredCheckpointPath -Prompt $prompt -Mode $mode)) {
|
|
399
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
|
|
400
|
+
Get-OrchestrationModeDenyReason -Mode $mode -Failure 'declared-checkpoint-path')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if ($mode -eq 'epic' -or $mode -eq 'parallel') {
|
|
404
|
+
$isEpic = ($mode -eq 'epic')
|
|
405
|
+
$injected = if ($isEpic) { 'EpicCheckpointRaw' } else { 'ParallelCheckpointRaw' }
|
|
406
|
+
$modeRaw = if ($PSBoundParameters.ContainsKey($injected)) {
|
|
407
|
+
[string]$PSBoundParameters[$injected]
|
|
408
|
+
} elseif ($isEpic) { Get-EpicCheckpointContent } else { Get-ParallelCheckpointContent }
|
|
409
|
+
try {
|
|
410
|
+
$modeCheckpoint = ConvertFrom-CheckpointJson -Json ([string]$modeRaw)
|
|
411
|
+
} catch { $modeCheckpoint = $null }
|
|
412
|
+
$folder = Find-OrchestrationDelegationTargetFolder -Prompt $prompt
|
|
413
|
+
$issue = Find-OrchestrationDelegationIssueNumber -Prompt $prompt
|
|
414
|
+
$failure = if ($isEpic) {
|
|
415
|
+
Get-EpicOrchestrationReadinessFailure -Checkpoint $modeCheckpoint -TargetFolder $folder -IssueNumber $issue
|
|
416
|
+
} else {
|
|
417
|
+
Get-ParallelOrchestrationReadinessFailure -Checkpoint $modeCheckpoint -TargetFolder $folder -IssueNumber $issue
|
|
418
|
+
}
|
|
419
|
+
if (-not $failure) { return Get-OrchestrationPreimplementationGateAllowDecision }
|
|
420
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
|
|
421
|
+
Get-OrchestrationModeDenyReason -Mode $mode -Failure $failure)
|
|
422
|
+
}
|
|
423
|
+
|
|
316
424
|
if (-not $CheckpointRaw) {
|
|
317
425
|
$CheckpointRaw = Get-CheckpointContent
|
|
318
426
|
}
|
|
@@ -428,6 +428,17 @@ function Test-BlastRadiusConflict {
|
|
|
428
428
|
.OUTPUTS
|
|
429
429
|
System.Collections.Hashtable. Keys conflict (a boolean) and reasons (an
|
|
430
430
|
array of hashtables with keys kind and detail).
|
|
431
|
+
|
|
432
|
+
Read the verdict from the conflict key of the returned hashtable.
|
|
433
|
+
Do not test the returned object itself: the hashtable is
|
|
434
|
+
unconditionally truthy under PowerShell boolean coercion, so
|
|
435
|
+
'if ($result)' treats every pair as contending.
|
|
436
|
+
System.Collections.Hashtable implements IDictionary and ICollection but
|
|
437
|
+
not IList, and the count-based truthiness rule applies only to IList
|
|
438
|
+
implementations, so a hashtable falls under the rule for any other
|
|
439
|
+
non-collection type and is always $true. The Python port agrees with its
|
|
440
|
+
own verdict; this mirror provably cannot, because PowerShell exposes no
|
|
441
|
+
hook by which a type can decline or change the conversion.
|
|
431
442
|
#>
|
|
432
443
|
[CmdletBinding()]
|
|
433
444
|
[OutputType([hashtable])]
|