@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.12

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 (52) hide show
  1. package/out/mcp-server.js +366 -159
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
  4. package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
  5. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  6. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
  8. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  9. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  11. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
  13. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
  14. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
  15. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  16. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  18. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  19. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  20. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  21. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  22. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  23. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  24. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  25. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
  26. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  27. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
  28. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
  29. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
  30. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  31. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  32. package/resources/claude-customizations/pack-manifests/core.json +9 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  34. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
  37. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  38. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  39. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  40. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  41. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  42. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  43. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  44. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  45. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  46. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  47. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  48. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  49. package/resources/codex-and-agents-customizations/pack-manifests/core.json +18 -1
  50. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  51. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  52. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +26 -0
@@ -0,0 +1,475 @@
1
+ # Pure validation and profile parsing for the worktree-bound epic-child launcher.
2
+
3
+ function ConvertFrom-CodexChildLaunchJson {
4
+ param([Parameter(Mandatory)][string] $Raw, [Parameter(Mandatory)][string] $Name)
5
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
6
+ throw "EPIC_CHILD_LAUNCH_BLOCKED: $Name is empty."
7
+ }
8
+ try {
9
+ return $Raw | ConvertFrom-Json -Depth 32 -ErrorAction Stop
10
+ } catch {
11
+ throw "EPIC_CHILD_LAUNCH_BLOCKED: $Name is malformed JSON: $_"
12
+ }
13
+ }
14
+
15
+ function Get-CodexChildSha256 {
16
+ [OutputType([string])]
17
+ param([Parameter(Mandatory)][string] $Value)
18
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
19
+ return [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
20
+ }
21
+
22
+ function Get-CodexChildCanonicalPath {
23
+ [OutputType([string])]
24
+ param([Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][string] $BasePath)
25
+ $candidate = if ([System.IO.Path]::IsPathFullyQualified($Path)) {
26
+ $Path
27
+ } else {
28
+ Join-Path $BasePath $Path
29
+ }
30
+ return [System.IO.Path]::GetFullPath($candidate).TrimEnd(
31
+ [System.IO.Path]::DirectorySeparatorChar,
32
+ [System.IO.Path]::AltDirectorySeparatorChar
33
+ )
34
+ }
35
+
36
+ function Get-CodexChildEffectiveMaximum {
37
+ [OutputType([int])]
38
+ param(
39
+ [Parameter(Mandatory)][ValidateRange(1, 8)][int] $RequestedMaximum,
40
+ [Parameter(Mandatory)][ValidateRange(1, 8)][int] $SpecMaximum
41
+ )
42
+ return [Math]::Min($RequestedMaximum, $SpecMaximum)
43
+ }
44
+
45
+ function Get-CodexChildProfileKey {
46
+ [OutputType([string])]
47
+ param(
48
+ [Parameter(Mandatory)][string] $WorktreePath,
49
+ [Parameter(Mandatory)][string] $AgentName,
50
+ [Parameter(Mandatory)][string] $RepositoryRoot
51
+ )
52
+ $worktree = Get-CodexChildCanonicalPath -Path $WorktreePath -BasePath $RepositoryRoot
53
+ return "$worktree`n$AgentName"
54
+ }
55
+
56
+ function Test-CodexChildIssueEqual {
57
+ [OutputType([bool])]
58
+ param([AllowNull()] $Left, [AllowNull()] $Right)
59
+ return ($Left | ConvertTo-Json -Compress) -ceq ($Right | ConvertTo-Json -Compress)
60
+ }
61
+
62
+ function Test-CodexChildPositiveInteger {
63
+ [OutputType([bool])]
64
+ param([AllowNull()] $Value)
65
+ $isInteger = $Value -is [byte] -or $Value -is [sbyte] -or
66
+ $Value -is [int16] -or $Value -is [uint16] -or
67
+ $Value -is [int32] -or $Value -is [uint32] -or
68
+ $Value -is [int64] -or $Value -is [uint64]
69
+ return $isInteger -and [decimal]$Value -gt 0
70
+ }
71
+
72
+ function Get-CodexChildFeatureKey {
73
+ [OutputType([string])]
74
+ param([Parameter(Mandatory)] $Feature)
75
+ $issue = $Feature.issue_num | ConvertTo-Json -Compress
76
+ return "$issue`n$([string]$Feature.feature_folder)"
77
+ }
78
+
79
+ function Test-CodexChildActiveFeatureFolder {
80
+ [OutputType([bool])]
81
+ param([Parameter(Mandatory)][string] $Path)
82
+ $normalized = $Path.Replace('\', '/')
83
+ if ([System.IO.Path]::IsPathFullyQualified($Path) -or
84
+ $normalized -notmatch '^docs/features/active/[^/<>]+$') {
85
+ return $false
86
+ }
87
+ $leaf = ($normalized -split '/')[-1]
88
+ if ($leaf -in @('.', '..') -or $leaf.Trim() -cne $leaf) {
89
+ return $false
90
+ }
91
+ return $leaf -notmatch '(?i)(?:^|[-_.])(?:placeholder|pending|tbd|todo|unknown|none|null|temp|temporary|draft)(?:$|[-_.])'
92
+ }
93
+
94
+ function ConvertTo-CodexChildReceiptTimestamp {
95
+ [OutputType([datetimeoffset])]
96
+ param([Parameter(Mandatory)][AllowNull()] $Value)
97
+ if ($null -eq $Value -or [string]::IsNullOrWhiteSpace([string]$Value)) {
98
+ throw 'receipt timestamp is missing.'
99
+ }
100
+ try {
101
+ return [datetimeoffset]$Value
102
+ } catch {
103
+ throw "receipt timestamp is invalid: $_"
104
+ }
105
+ }
106
+
107
+ function Get-CodexChildActiveReceiptErrorList {
108
+ [OutputType([string[]])]
109
+ param(
110
+ [Parameter(Mandatory)] $Receipt,
111
+ [AllowEmptyString()][string] $ExpectedSessionId = '',
112
+ [datetimeoffset] $Now = [datetimeoffset]::UtcNow
113
+ )
114
+ $errors = [System.Collections.Generic.List[string]]::new()
115
+ if ([int]$Receipt.schema_version -ne 2 -or [string]$Receipt.state -cne 'active') {
116
+ $errors.Add('launch receipt must use schema_version 2 and state active.')
117
+ }
118
+ $sessionId = [string]$Receipt.codex_session_id
119
+ if ([string]::IsNullOrWhiteSpace($sessionId) -or
120
+ (-not [string]::IsNullOrWhiteSpace($ExpectedSessionId) -and $sessionId -cne $ExpectedSessionId)) {
121
+ $errors.Add('launch receipt session id is missing or does not match the active Codex session.')
122
+ }
123
+ $timestampsValid = $true
124
+ try {
125
+ $boundAt = ConvertTo-CodexChildReceiptTimestamp -Value $Receipt.session_bound_at
126
+ $expiresAt = ConvertTo-CodexChildReceiptTimestamp -Value $Receipt.expires_at
127
+ } catch {
128
+ $timestampsValid = $false
129
+ }
130
+ if (-not $timestampsValid -or $boundAt -gt $Now -or $expiresAt -le $Now -or $expiresAt -le $boundAt) {
131
+ $errors.Add('launch receipt session timestamps are invalid or expired.')
132
+ }
133
+ foreach ($name in @(
134
+ 'launch_id', 'delegation_id', 'feature_folder', 'deployment_agent', 'model',
135
+ 'model_reasoning_effort', 'execution_context', 'worktree_path', 'branch_name',
136
+ 'receipt_path', 'status_path', 'spec_path', 'profile_path', 'codex_home_path',
137
+ 'trusted_repository_root', 'checkpoint_kind', 'wave_lock_path'
138
+ )) {
139
+ if ([string]::IsNullOrWhiteSpace([string]$Receipt.$name)) {
140
+ $errors.Add("launch receipt is missing $name.")
141
+ }
142
+ }
143
+ return $errors.ToArray()
144
+ }
145
+ function Get-CodexChildTerminalReceiptErrorList {
146
+ [OutputType([string[]])]
147
+ param([Parameter(Mandatory)] $Receipt)
148
+ $errors = [System.Collections.Generic.List[string]]::new()
149
+ if ([int]$Receipt.schema_version -ne 2 -or [string]$Receipt.state -cnotin @('completed', 'failed')) {
150
+ $errors.Add('terminal launch receipt must use schema_version 2 and state completed or failed.')
151
+ }
152
+ if ([string]::IsNullOrWhiteSpace([string]$Receipt.codex_session_id)) {
153
+ $errors.Add('terminal launch receipt must preserve the bound Codex session id.')
154
+ }
155
+ $timestampsValid = $true
156
+ try {
157
+ $boundAt = ConvertTo-CodexChildReceiptTimestamp -Value $Receipt.session_bound_at
158
+ $terminalAt = if ([string]$Receipt.state -eq 'completed') {
159
+ ConvertTo-CodexChildReceiptTimestamp -Value $Receipt.completed_at
160
+ } else {
161
+ ConvertTo-CodexChildReceiptTimestamp -Value $Receipt.failed_at
162
+ }
163
+ } catch {
164
+ $timestampsValid = $false
165
+ }
166
+ if ([string]$Receipt.state -ceq 'completed') {
167
+ if ([int]$Receipt.exit_code -ne 0 -or [string]::IsNullOrWhiteSpace([string]$Receipt.completed_at)) {
168
+ $errors.Add('completed launch receipt requires exit_code 0 and completed_at.')
169
+ }
170
+ } elseif ([string]$Receipt.state -ceq 'failed' -and
171
+ [string]::IsNullOrWhiteSpace([string]$Receipt.failed_at)) {
172
+ $errors.Add('failed launch receipt requires failed_at.')
173
+ }
174
+ if (-not $timestampsValid -or $terminalAt -lt $boundAt) {
175
+ $errors.Add('terminal launch receipt timestamps are invalid or out of order.')
176
+ }
177
+ return $errors.ToArray()
178
+ }
179
+
180
+ function ConvertFrom-CodexAgentProfile {
181
+ [CmdletBinding()]
182
+ param([Parameter(Mandatory)][string] $ProfileRaw)
183
+ $values = [ordered]@{}
184
+ foreach ($key in @('name', 'model', 'model_reasoning_effort', 'default_permissions')) {
185
+ $pattern = '(?m)^{0}\s*=\s*"([^"]+)"\s*$' -f [regex]::Escape($key)
186
+ $match = [regex]::Match($ProfileRaw, $pattern)
187
+ if (-not $match.Success) {
188
+ throw "EPIC_CHILD_LAUNCH_BLOCKED: generated profile is missing $key."
189
+ }
190
+ $values[$key] = $match.Groups[1].Value
191
+ }
192
+ $instructions = [regex]::Match(
193
+ $ProfileRaw,
194
+ "(?ms)^developer_instructions\s*=\s*'''(.*?)'''\s*$"
195
+ )
196
+ $skillsSection = [regex]::Match($ProfileRaw, '(?ms)^\[skills\]\s*(.*)$')
197
+ $skills = if ($skillsSection.Success) {
198
+ [regex]::Match($skillsSection.Groups[1].Value, '(?ms)^config\s*=\s*(\[.*\])\s*$')
199
+ } else {
200
+ $null
201
+ }
202
+ if (-not $instructions.Success -or $null -eq $skills -or -not $skills.Success) {
203
+ throw 'EPIC_CHILD_LAUNCH_BLOCKED: generated profile must define developer_instructions and skills.config.'
204
+ }
205
+ $values.developer_instructions = $instructions.Groups[1].Value
206
+ $values.skills_config = $skills.Groups[1].Value
207
+ return [pscustomobject]$values
208
+ }
209
+
210
+ function Find-CodexChildFeature {
211
+ [CmdletBinding()]
212
+ param([Parameter(Mandatory)] $Checkpoint, [Parameter(Mandatory)] $Entry)
213
+ foreach ($feature in @($Checkpoint.features)) {
214
+ if ($null -ne $feature -and
215
+ (Test-CodexChildIssueEqual -Left $feature.issue_num -Right $Entry.issue_num) -and
216
+ [string]$feature.feature_folder -ceq [string]$Entry.feature_folder) {
217
+ return $feature
218
+ }
219
+ }
220
+ return $null
221
+ }
222
+
223
+ function Find-CodexChildDelegationReceipt {
224
+ [CmdletBinding()]
225
+ param(
226
+ [Parameter(Mandatory)] $Checkpoint,
227
+ [Parameter(Mandatory)] $Feature,
228
+ [Parameter(Mandatory)] $Entry
229
+ )
230
+ $featureReceipt = $null
231
+ if (@($Feature.PSObject.Properties.Name) -contains 'delegation_receipt') {
232
+ $featureReceipt = $Feature.delegation_receipt
233
+ $receipts = @($featureReceipt)
234
+ } else {
235
+ $receipts = @()
236
+ }
237
+ if (@($Checkpoint.PSObject.Properties.Name) -contains 'delegation_receipts') {
238
+ $receipts += @($Checkpoint.delegation_receipts)
239
+ }
240
+ foreach ($receipt in $receipts) {
241
+ if ($null -eq $receipt) {
242
+ continue
243
+ }
244
+ $names = @($receipt.PSObject.Properties.Name)
245
+ $receiptId = if ($names -contains 'delegation_id') {
246
+ [string]$receipt.delegation_id
247
+ } elseif ($names -contains 'id') {
248
+ [string]$receipt.id
249
+ } else {
250
+ ''
251
+ }
252
+ $isExact = [string]$receipt.feature_folder -ceq [string]$Entry.feature_folder -and
253
+ (Test-CodexChildIssueEqual -Left $receipt.issue_num -Right $Entry.issue_num) -and
254
+ $receiptId -ceq [string]$Entry.delegation_id -and
255
+ [string]$receipt.agent_name -ceq [string]$Entry.deployment_agent
256
+ if ($isExact) {
257
+ return $receipt
258
+ }
259
+ }
260
+ return $null
261
+ }
262
+
263
+ function Find-CodexChildModelReceipt {
264
+ [CmdletBinding()]
265
+ param(
266
+ [Parameter(Mandatory)] $Checkpoint,
267
+ [Parameter(Mandatory)] $Feature,
268
+ [Parameter(Mandatory)] $Entry
269
+ )
270
+ $featureReceipt = $null
271
+ if (@($Feature.PSObject.Properties.Name) -contains 'model_routing_receipt') {
272
+ $featureReceipt = $Feature.model_routing_receipt
273
+ $receipts = @($featureReceipt)
274
+ } else {
275
+ $receipts = @()
276
+ }
277
+ if (@($Checkpoint.PSObject.Properties.Name) -contains 'codex_model_routing_receipts') {
278
+ $receipts += @($Checkpoint.codex_model_routing_receipts)
279
+ }
280
+ foreach ($receipt in $receipts) {
281
+ if ($null -ne $receipt -and
282
+ [string]$receipt.delegation_id -ceq [string]$Entry.delegation_id -and
283
+ [string]$receipt.deployment_agent -ceq [string]$Entry.deployment_agent -and
284
+ [string]$receipt.model -ceq [string]$Entry.model -and
285
+ [string]$receipt.model_reasoning_effort -ceq [string]$Entry.model_reasoning_effort -and
286
+ [string]$receipt.execution_context -ceq [string]$Entry.execution_context) {
287
+ return $receipt
288
+ }
289
+ }
290
+ return $null
291
+ }
292
+
293
+ function Test-CodexChildLaunchSpec {
294
+ [CmdletBinding()]
295
+ [OutputType([string[]])]
296
+ param(
297
+ [Parameter(Mandatory)] $Spec,
298
+ [Parameter(Mandatory)] $Checkpoint,
299
+ [Parameter(Mandatory)][string] $RepositoryRoot,
300
+ [Parameter(Mandatory)][System.Collections.IDictionary] $ProfilesByKey,
301
+ [Parameter(Mandatory)][System.Collections.IDictionary] $LiveBranchesByWorktree
302
+ )
303
+ $errors = [System.Collections.Generic.List[string]]::new()
304
+ if ($Spec.schema_version -ne 1 -or [string]$Checkpoint.integration_branch -cne [string]$Spec.integration_branch) {
305
+ $errors.Add('schema_version must be 1 and integration_branch must match the epic checkpoint.')
306
+ }
307
+ $specMaximum = [int]$Spec.max_parallel_features
308
+ if ($specMaximum -lt 1 -or $specMaximum -gt 8 -or
309
+ $specMaximum -ne [int]$Checkpoint.max_parallel_features) {
310
+ $errors.Add('max_parallel_features must match the checkpoint and be between 1 and 8.')
311
+ }
312
+ if ([string]$Spec.wave_id -cnotmatch '^[a-z0-9][a-z0-9._-]{0,79}$') {
313
+ $errors.Add('wave_id must be a safe lowercase artifact identifier.')
314
+ }
315
+ if ([string]$Spec.checkpoint_kind -notin @('epic-planner', 'epic-orchestrator')) {
316
+ $errors.Add('checkpoint_kind must be epic-planner or epic-orchestrator.')
317
+ }
318
+ $expectedCheckpointName = "$($Spec.checkpoint_kind)-state.json"
319
+ if ((Split-Path ([string]$Spec.checkpoint_path) -Leaf) -ne $expectedCheckpointName) {
320
+ $errors.Add("checkpoint_path must end with $expectedCheckpointName.")
321
+ }
322
+ $expectedContext = if ([string]$Spec.checkpoint_kind -eq 'epic-planner') {
323
+ 'epic_preparation_child'
324
+ } else {
325
+ 'epic_execution_child'
326
+ }
327
+ $isPlanner = [string]$Spec.checkpoint_kind -eq 'epic-planner'
328
+ if ($isPlanner -and [int]$Spec.wave_number -ne 0) {
329
+ $errors.Add('epic-planner preparation batches must use wave_number 0.')
330
+ }
331
+ $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
332
+ $seenDelegations = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
333
+ $worktreeComparer = if ($IsWindows) { [System.StringComparer]::OrdinalIgnoreCase } else { [System.StringComparer]::Ordinal }
334
+ $seenWorktrees = [System.Collections.Generic.HashSet[string]]::new($worktreeComparer)
335
+ $expectedFeatures = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
336
+ $checkpointFeatures = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
337
+ $terminalStatuses = @('blocked_conflict_loop_limit', 'merged', 'worktree_removed')
338
+ foreach ($feature in @($Checkpoint.features)) {
339
+ if ($null -eq $feature) {
340
+ continue
341
+ }
342
+ $checkpointFeatureKey = Get-CodexChildFeatureKey -Feature $feature
343
+ if (-not $checkpointFeatures.Add($checkpointFeatureKey)) {
344
+ $errors.Add('checkpoint features must use unique issue_num and feature_folder identities.')
345
+ }
346
+ $featureWave = if (@($feature.PSObject.Properties.Name) -contains 'wave_number') {
347
+ [int]$feature.wave_number
348
+ } else {
349
+ [int]$feature.wave
350
+ }
351
+ $eligible = $isPlanner -or ($featureWave -eq [int]$Spec.wave_number -and
352
+ [string]$feature.merge_status -notin $terminalStatuses)
353
+ if ($eligible) {
354
+ $expectedFeatures.Add($checkpointFeatureKey) | Out-Null
355
+ }
356
+ }
357
+ $launchedFeatures = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
358
+ foreach ($entry in @($Spec.launches)) {
359
+ $required = @(
360
+ 'launch_id', 'delegation_id', 'feature_folder', 'issue_num', 'deployment_agent',
361
+ 'model', 'model_reasoning_effort', 'permissions', 'execution_context', 'worktree_path',
362
+ 'branch_name', 'prompt'
363
+ )
364
+ foreach ($name in $required) {
365
+ if (@($entry.PSObject.Properties.Name) -notcontains $name -or
366
+ [string]::IsNullOrWhiteSpace([string]$entry.$name)) {
367
+ $errors.Add("launch entry is missing $name.")
368
+ }
369
+ }
370
+ if ([string]$entry.launch_id -cnotmatch '^[a-z0-9][a-z0-9._-]{0,79}$') {
371
+ $errors.Add('launch_id must be a safe lowercase artifact identifier.')
372
+ }
373
+ if ([string]$entry.deployment_agent -notmatch '^[a-z0-9][a-z0-9._-]{0,79}$') {
374
+ $errors.Add("launch '$($entry.launch_id)' deployment_agent must be a safe generated profile name.")
375
+ }
376
+ if (-not $seen.Add([string]$entry.launch_id)) {
377
+ $errors.Add("duplicate launch_id: $($entry.launch_id)")
378
+ }
379
+ if (-not $seenDelegations.Add([string]$entry.delegation_id)) {
380
+ $errors.Add("duplicate delegation_id: $($entry.delegation_id)")
381
+ }
382
+ $feature = Find-CodexChildFeature -Checkpoint $Checkpoint -Entry $entry
383
+ if ($null -eq $feature) {
384
+ $errors.Add("launch '$($entry.launch_id)' has no exact checkpoint feature match.")
385
+ continue
386
+ }
387
+ $featureKey = Get-CodexChildFeatureKey -Feature $feature
388
+ if (-not $expectedFeatures.Contains($featureKey)) {
389
+ $errors.Add("launch '$($entry.launch_id)' targets a feature that is not eligible for this batch or wave.")
390
+ }
391
+ if (-not $launchedFeatures.Add($featureKey)) {
392
+ $errors.Add("launch '$($entry.launch_id)' duplicates a checkpoint feature in this batch or wave.")
393
+ }
394
+ $featureWave = if (@($feature.PSObject.Properties.Name) -contains 'wave_number') {
395
+ [int]$feature.wave_number
396
+ } else {
397
+ [int]$feature.wave
398
+ }
399
+ $issueIsPositive = (Test-CodexChildPositiveInteger -Value $entry.issue_num) -and
400
+ (Test-CodexChildPositiveInteger -Value $feature.issue_num)
401
+ if (-not $issueIsPositive) {
402
+ $errors.Add("launch '$($entry.launch_id)' requires a final positive integer issue_num.")
403
+ }
404
+ if (-not (Test-CodexChildActiveFeatureFolder -Path ([string]$entry.feature_folder)) -or
405
+ -not (Test-CodexChildActiveFeatureFolder -Path ([string]$feature.feature_folder))) {
406
+ $errors.Add("launch '$($entry.launch_id)' requires a final docs/features/active feature_folder without placeholders.")
407
+ }
408
+ if ((-not $isPlanner -and $featureWave -ne [int]$Spec.wave_number) -or
409
+ [string]$feature.branch_name -cne [string]$entry.branch_name) {
410
+ $errors.Add("launch '$($entry.launch_id)' wave or branch differs from its checkpoint feature.")
411
+ }
412
+ $worktree = Get-CodexChildCanonicalPath -Path ([string]$entry.worktree_path) -BasePath $RepositoryRoot
413
+ $featureWorktree = Get-CodexChildCanonicalPath -Path ([string]$feature.worktree_path) -BasePath $RepositoryRoot
414
+ if (-not [System.IO.Path]::IsPathFullyQualified([string]$entry.worktree_path) -or
415
+ [string]$entry.worktree_path -cne $worktree -or
416
+ $worktree -ne $featureWorktree -or
417
+ [string]$LiveBranchesByWorktree[$worktree] -cne [string]$entry.branch_name) {
418
+ $errors.Add("launch '$($entry.launch_id)' worktree is not canonical or its live branch does not match.")
419
+ }
420
+ if (-not $seenWorktrees.Add($worktree)) {
421
+ $errors.Add("duplicate worktree_path: $worktree")
422
+ }
423
+ if ([string]$entry.execution_context -cne $expectedContext) {
424
+ $errors.Add("launch '$($entry.launch_id)' has the wrong epic execution context.")
425
+ }
426
+ $prompt = [string]$entry.prompt
427
+ if ($isPlanner) {
428
+ if (-not $prompt.Contains('Preparation mode: true', [System.StringComparison]::Ordinal)) {
429
+ $errors.Add("launch '$($entry.launch_id)' planner prompt is missing 'Preparation mode: true'.")
430
+ }
431
+ } else {
432
+ $requiredPromptValues = @(
433
+ 'Epic mode: true.',
434
+ "epic_feature_folder: $($Checkpoint.epic_feature_folder)",
435
+ "integration_branch: $($Checkpoint.integration_branch)",
436
+ "--base $($Checkpoint.integration_branch)"
437
+ )
438
+ foreach ($requiredPromptValue in $requiredPromptValues) {
439
+ if (-not $prompt.Contains($requiredPromptValue, [System.StringComparison]::Ordinal)) {
440
+ $errors.Add("launch '$($entry.launch_id)' execution prompt is missing: $requiredPromptValue")
441
+ }
442
+ }
443
+ if (-not [string]::IsNullOrWhiteSpace([string]$feature.plan_path) -and
444
+ (-not $prompt.Contains([string]$feature.plan_path, [System.StringComparison]::Ordinal) -or
445
+ $prompt -notmatch '(?i)resumes? at atomic execution')) {
446
+ $errors.Add("launch '$($entry.launch_id)' prepared execution prompt must include plan_path and atomic-execution resume text.")
447
+ }
448
+ }
449
+ if ($null -eq (Find-CodexChildDelegationReceipt -Checkpoint $Checkpoint -Feature $feature -Entry $entry)) {
450
+ $errors.Add("launch '$($entry.launch_id)' has no matching delegation receipt.")
451
+ }
452
+ if ($null -eq (Find-CodexChildModelReceipt -Checkpoint $Checkpoint -Feature $feature -Entry $entry)) {
453
+ $errors.Add("launch '$($entry.launch_id)' has no matching model-routing receipt.")
454
+ }
455
+ $profileKey = Get-CodexChildProfileKey -WorktreePath $worktree `
456
+ -AgentName ([string]$entry.deployment_agent) -RepositoryRoot $RepositoryRoot
457
+ $agentProfile = $ProfilesByKey[$profileKey]
458
+ if ($null -eq $agentProfile -or
459
+ [string]$agentProfile.name -cne [string]$entry.deployment_agent -or
460
+ [string]$agentProfile.model -cne [string]$entry.model -or
461
+ [string]$agentProfile.model_reasoning_effort -cne [string]$entry.model_reasoning_effort -or
462
+ [string]$agentProfile.default_permissions -cne [string]$entry.permissions) {
463
+ $errors.Add("launch '$($entry.launch_id)' differs from its checked-in generated profile.")
464
+ }
465
+ }
466
+ if (@($Spec.launches).Count -eq 0) {
467
+ $errors.Add('launches must contain at least one child.')
468
+ }
469
+ foreach ($featureKey in $expectedFeatures) {
470
+ if (-not $launchedFeatures.Contains($featureKey)) {
471
+ $errors.Add('launches must cover every eligible checkpoint feature exactly once.')
472
+ }
473
+ }
474
+ return $errors.ToArray()
475
+ }