@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.
Files changed (38) hide show
  1. package/out/mcp-server.js +335 -24
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +163 -18
  4. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +477 -0
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +120 -12
  6. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +11 -0
  7. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +1 -0
  8. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +131 -2
  9. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +37 -0
  10. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +136 -4
  11. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
  12. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +18 -9
  13. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +8 -1
  14. package/resources/claude-customizations/.claude/skills/pr-context-artifacts/SKILL.md +22 -0
  15. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +10 -0
  16. package/resources/claude-customizations/config/orchestration-routing.json +1 -0
  17. package/resources/claude-customizations/pack-manifests/core.json +1 -0
  18. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +10 -0
  19. package/resources/codex-and-agents-customizations/.agents/skills/pr-context-artifacts/SKILL.md +22 -0
  20. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c1.toml +22 -0
  21. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c2.toml +22 -0
  22. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3-elevated.toml +22 -0
  23. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3.toml +22 -0
  24. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c4.toml +22 -0
  25. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward.toml +2 -0
  26. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +7 -0
  27. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +7 -0
  28. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +7 -0
  29. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +7 -0
  30. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +7 -0
  31. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +7 -0
  32. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  33. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +477 -0
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +125 -12
  35. package/resources/codex-and-agents-customizations/pack-manifests/core.json +8 -1
  36. package/resources/config/orchestration-routing.json +1 -0
  37. package/resources/customizations/.github/skills/pr-context-artifacts/SKILL.md +22 -0
  38. 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
- try {
213
- if (Test-PreparationModeDelegation -ToolInput $ToolInput) {
214
- return $false
215
- }
216
- } catch {
217
- # An envelope the field reader cannot probe falls through to the unchanged
218
- # whole-payload regex below. An extraction failure must never become an
219
- # exemption, so the gate stays closed on the stricter classifier.
220
- Write-Debug "Preparation-mode probe failed: $($_.Exception.Message)"
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
- $payloadText = ($ToolInput | ConvertTo-Json -Depth 20 -Compress)
224
- return $payloadText -match '(python-typed-engineer|powershell-typed-engineer|typescript-engineer|csharp-typed-engineer|atomic-executor|implementation|execute)'
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
  }