@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 (Codex 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
|
+
}
|
|
@@ -15,6 +15,12 @@ param()
|
|
|
15
15
|
# the headroom-split precedent already used on this side by enforce-completion-helpers.ps1.
|
|
16
16
|
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-helpers.ps1')
|
|
17
17
|
|
|
18
|
+
# Pure mode dispatch and per-mode readiness predicates for issue #554. A new sibling
|
|
19
|
+
# rather than an addition to the helpers file above, whose header declares a different
|
|
20
|
+
# normative contract and which lacks headroom; leaving that file byte-untouched is the
|
|
21
|
+
# proof the issue #539 exemption is behaviourally unchanged.
|
|
22
|
+
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-modes.ps1')
|
|
23
|
+
|
|
18
24
|
# The readiness checkpoint this gate reads and names in its block message.
|
|
19
25
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
20
26
|
|
|
@@ -200,6 +206,19 @@ function Test-PreparationModeDelegation {
|
|
|
200
206
|
return $true
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
# Classifies an Agent delegation as implementation by STRUCTURE (issue #554). Both
|
|
210
|
+
# reads are field-scoped through this file's own Get-StringProperty, and the
|
|
211
|
+
# whole-payload serialization scan this function used to perform is removed: it let any
|
|
212
|
+
# field, and two ordinary English words, decide the outcome, so marker text planted
|
|
213
|
+
# outside 'prompt' changed the classification and rewording a prompt changed the
|
|
214
|
+
# decision. Neither can happen now. An allow-listed subagent_type is implementation
|
|
215
|
+
# whatever the prompt says; any other non-orchestrator subagent_type is not; an
|
|
216
|
+
# orchestrator whose resolved mode is preparation is not, and every other one is.
|
|
217
|
+
#
|
|
218
|
+
# Get-StringProperty TRIMS where the Claude-side reader does not. Every marker test
|
|
219
|
+
# introduced here is a containment test over the prompt, which is insensitive to
|
|
220
|
+
# leading and trailing whitespace, so that pre-existing divergence between the two
|
|
221
|
+
# surfaces cannot change any decision this change introduces.
|
|
203
222
|
function Test-ImplementationDelegation {
|
|
204
223
|
[CmdletBinding()]
|
|
205
224
|
[OutputType([bool])]
|
|
@@ -209,19 +228,16 @@ function Test-ImplementationDelegation {
|
|
|
209
228
|
return $false
|
|
210
229
|
}
|
|
211
230
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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)"
|
|
231
|
+
$subagentType = Get-StringProperty -Value $ToolInput -Name 'subagent_type'
|
|
232
|
+
if (Test-OrchestrationImplementationAgent -SubagentType $subagentType) {
|
|
233
|
+
return $true
|
|
234
|
+
}
|
|
235
|
+
if ($subagentType -ne 'orchestrator') {
|
|
236
|
+
return $false
|
|
221
237
|
}
|
|
222
238
|
|
|
223
|
-
$
|
|
224
|
-
return $
|
|
239
|
+
$prompt = Get-StringProperty -Value $ToolInput -Name 'prompt'
|
|
240
|
+
return ((Resolve-OrchestrationDelegationMode -Prompt $prompt) -ne 'preparation')
|
|
225
241
|
}
|
|
226
242
|
|
|
227
243
|
function Test-OrchestrationReady {
|
|
@@ -265,6 +281,33 @@ function Get-CheckpointContent {
|
|
|
265
281
|
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
266
282
|
}
|
|
267
283
|
|
|
284
|
+
# The two per-mode read seams (issue #554). Each takes its path from the fixed mode
|
|
285
|
+
# table and never from a delegation's own text; an absent file returns an empty
|
|
286
|
+
# string, which the readiness predicate then treats as a deny.
|
|
287
|
+
function Get-EpicCheckpointContent {
|
|
288
|
+
[CmdletBinding()]
|
|
289
|
+
[OutputType([string])]
|
|
290
|
+
param()
|
|
291
|
+
|
|
292
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
|
|
293
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
294
|
+
return ''
|
|
295
|
+
}
|
|
296
|
+
return Get-Content -Raw -LiteralPath $path
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function Get-ParallelCheckpointContent {
|
|
300
|
+
[CmdletBinding()]
|
|
301
|
+
[OutputType([string])]
|
|
302
|
+
param()
|
|
303
|
+
|
|
304
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
|
|
305
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
306
|
+
return ''
|
|
307
|
+
}
|
|
308
|
+
return Get-Content -Raw -LiteralPath $path
|
|
309
|
+
}
|
|
310
|
+
|
|
268
311
|
function Get-OrchestrationPreimplementationGateAllowDecision {
|
|
269
312
|
[CmdletBinding()]
|
|
270
313
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -295,12 +338,43 @@ function Get-OrchestrationPreimplementationGateBlockDecision {
|
|
|
295
338
|
}
|
|
296
339
|
}
|
|
297
340
|
|
|
341
|
+
# Builds a mode-specific deny reason naming the checkpoint actually consulted and the
|
|
342
|
+
# predicate that failed, behind the unchanged PREIMPLEMENTATION_GATE_BLOCKED prefix
|
|
343
|
+
# that downstream reason-matching reads.
|
|
344
|
+
function Get-OrchestrationModeDenyReason {
|
|
345
|
+
[CmdletBinding()]
|
|
346
|
+
[OutputType([string])]
|
|
347
|
+
param(
|
|
348
|
+
[Parameter(Mandatory)][string] $Mode,
|
|
349
|
+
[Parameter(Mandatory)][string] $Failure
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode $Mode
|
|
353
|
+
return ("PREIMPLEMENTATION_GATE_BLOCKED: this $Mode-mode delegation was evaluated against " +
|
|
354
|
+
"$path, and the failed readiness predicate is '$Failure'. Implementation operations " +
|
|
355
|
+
'require that checkpoint to satisfy every readiness predicate before implementation begins.')
|
|
356
|
+
}
|
|
357
|
+
|
|
298
358
|
function Invoke-OrchestrationPreimplementationGateDecision {
|
|
299
359
|
[CmdletBinding()]
|
|
300
360
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
301
361
|
param(
|
|
302
362
|
[string] $ToolInputRaw,
|
|
303
|
-
[string] $CheckpointRaw
|
|
363
|
+
[string] $CheckpointRaw,
|
|
364
|
+
|
|
365
|
+
# The two per-mode injection parameters (issue #554, decision D2). Each
|
|
366
|
+
# overrides its read seam whenever the caller BINDS it, decided with
|
|
367
|
+
# ContainsKey and never with a truthiness test, so an explicitly supplied
|
|
368
|
+
# empty string suppresses the seam instead of falling through to disk. The
|
|
369
|
+
# two parameters above keep their existing names, positions, attributes,
|
|
370
|
+
# and truthiness-based fall-through exactly.
|
|
371
|
+
[AllowNull()]
|
|
372
|
+
[AllowEmptyString()]
|
|
373
|
+
[string] $EpicCheckpointRaw,
|
|
374
|
+
|
|
375
|
+
[AllowNull()]
|
|
376
|
+
[AllowEmptyString()]
|
|
377
|
+
[string] $ParallelCheckpointRaw
|
|
304
378
|
)
|
|
305
379
|
|
|
306
380
|
if (-not $ToolInputRaw) {
|
|
@@ -313,6 +387,12 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
313
387
|
}
|
|
314
388
|
|
|
315
389
|
$requiresReadyCheckpoint = $false
|
|
390
|
+
# The path and command legs are single-feature by construction; only the
|
|
391
|
+
# delegation leg carries a mode marker, so only it can move the mode off the
|
|
392
|
+
# default. Both other legs therefore keep the default readiness source and the
|
|
393
|
+
# default wording they have today.
|
|
394
|
+
$mode = $script:OrchestrationDelegationDefaultMode
|
|
395
|
+
$prompt = ''
|
|
316
396
|
$filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
|
|
317
397
|
if ($filePath) {
|
|
318
398
|
$normalized = ([string]$filePath) -replace '\\', '/'
|
|
@@ -323,6 +403,10 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
323
403
|
$requiresReadyCheckpoint = Test-ImplementationCommand -Command $command
|
|
324
404
|
} else {
|
|
325
405
|
$requiresReadyCheckpoint = Test-ImplementationDelegation -ToolInput $toolInput
|
|
406
|
+
if ($requiresReadyCheckpoint) {
|
|
407
|
+
$prompt = Get-StringProperty -Value $toolInput -Name 'prompt'
|
|
408
|
+
$mode = Resolve-OrchestrationDelegationMode -Prompt $prompt
|
|
409
|
+
}
|
|
326
410
|
}
|
|
327
411
|
}
|
|
328
412
|
|
|
@@ -330,6 +414,35 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
330
414
|
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
331
415
|
}
|
|
332
416
|
|
|
417
|
+
# A prompt-declared checkpoint path is a cross-check operand only and never
|
|
418
|
+
# selects a source: a delegation that named its own readiness file would choose
|
|
419
|
+
# its own gate. Disagreement with the mode's canonical path is a deny.
|
|
420
|
+
if (-not (Test-OrchestrationDelegationDeclaredCheckpointPath -Prompt $prompt -Mode $mode)) {
|
|
421
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
|
|
422
|
+
Get-OrchestrationModeDenyReason -Mode $mode -Failure 'declared-checkpoint-path')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if ($mode -eq 'epic' -or $mode -eq 'parallel') {
|
|
426
|
+
$isEpic = ($mode -eq 'epic')
|
|
427
|
+
$injected = if ($isEpic) { 'EpicCheckpointRaw' } else { 'ParallelCheckpointRaw' }
|
|
428
|
+
$modeRaw = if ($PSBoundParameters.ContainsKey($injected)) {
|
|
429
|
+
[string]$PSBoundParameters[$injected]
|
|
430
|
+
} elseif ($isEpic) { Get-EpicCheckpointContent } else { Get-ParallelCheckpointContent }
|
|
431
|
+
try {
|
|
432
|
+
$modeCheckpoint = ConvertFrom-CheckpointJson -Json ([string]$modeRaw)
|
|
433
|
+
} catch { $modeCheckpoint = $null }
|
|
434
|
+
$folder = Find-OrchestrationDelegationTargetFolder -Prompt $prompt
|
|
435
|
+
$issue = Find-OrchestrationDelegationIssueNumber -Prompt $prompt
|
|
436
|
+
$failure = if ($isEpic) {
|
|
437
|
+
Get-EpicOrchestrationReadinessFailure -Checkpoint $modeCheckpoint -TargetFolder $folder -IssueNumber $issue
|
|
438
|
+
} else {
|
|
439
|
+
Get-ParallelOrchestrationReadinessFailure -Checkpoint $modeCheckpoint -TargetFolder $folder -IssueNumber $issue
|
|
440
|
+
}
|
|
441
|
+
if (-not $failure) { return Get-OrchestrationPreimplementationGateAllowDecision }
|
|
442
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason (
|
|
443
|
+
Get-OrchestrationModeDenyReason -Mode $mode -Failure $failure)
|
|
444
|
+
}
|
|
445
|
+
|
|
333
446
|
if (-not $CheckpointRaw) {
|
|
334
447
|
$CheckpointRaw = Get-CheckpointContent
|
|
335
448
|
}
|