@danmoisan/drm-copilot-mcp 1.1.4 → 1.1.5

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.
Files changed (27) hide show
  1. package/out/mcp-server.js +286 -10
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +477 -0
  4. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +120 -12
  5. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +131 -2
  6. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +9 -0
  7. package/resources/claude-customizations/config/orchestration-routing.json +1 -0
  8. package/resources/claude-customizations/pack-manifests/core.json +1 -0
  9. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +10 -0
  10. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c1.toml +22 -0
  11. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c2.toml +22 -0
  12. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3-elevated.toml +22 -0
  13. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3.toml +22 -0
  14. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c4.toml +22 -0
  15. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward.toml +2 -0
  16. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +7 -0
  17. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +7 -0
  18. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +7 -0
  19. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +7 -0
  20. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +7 -0
  21. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +7 -0
  22. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  23. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +477 -0
  24. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +125 -12
  25. package/resources/codex-and-agents-customizations/pack-manifests/core.json +8 -1
  26. package/resources/config/orchestration-routing.json +1 -0
  27. 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
- try {
192
- if (Test-PreparationModeDelegation -ToolInput $ToolInput) {
193
- return $false
194
- }
195
- } catch {
196
- # An envelope the field reader cannot probe falls through to the unchanged
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
- $payloadText = ($ToolInput | ConvertTo-Json -Depth 20 -Compress)
203
- return $payloadText -match '(python-typed-engineer|powershell-typed-engineer|typescript-engineer|csharp-typed-engineer|atomic-executor|implementation|execute)'
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
  }