@danmoisan/drm-copilot-mcp 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +2 -41
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +26 -5
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +485 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +243 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +7 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +2 -0
- package/resources/claude-customizations/pack-manifests/core.json +3 -1
- package/resources/config/orchestration-routing.json +1 -1
package/package.json
CHANGED
|
@@ -46,46 +46,7 @@ param()
|
|
|
46
46
|
$script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
|
|
47
47
|
$script:OrchestratorStateCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
<#
|
|
51
|
-
.SYNOPSIS
|
|
52
|
-
Runs the orchestrator-state validator against the checkpoint and reports pass/fail.
|
|
53
|
-
.DESCRIPTION
|
|
54
|
-
Mirrors Invoke-RoutingContractValidation (.claude/hooks/validate-orchestrator-output.ps1):
|
|
55
|
-
an injectable subprocess scriptblock seam defaults to ``python -m
|
|
56
|
-
scripts.dev_tools.validate_orchestration_artifacts orchestrator-state <CheckpointPath>
|
|
57
|
-
--require-pr-creation-ready``. A missing checkpoint or --require-pr-creation-ready failure
|
|
58
|
-
both surface via the validator's non-zero exit/stderr text; no separate file-existence check
|
|
59
|
-
is made, validating pre-PR-creation readiness (steps 5-8, blocked_reason) not full completion.
|
|
60
|
-
.OUTPUTS
|
|
61
|
-
System.Collections.Hashtable with keys HasErrors (bool) and ErrorText (string).
|
|
62
|
-
#>
|
|
63
|
-
[CmdletBinding()]
|
|
64
|
-
[OutputType([hashtable])]
|
|
65
|
-
param(
|
|
66
|
-
[Parameter(Mandatory = $false)]
|
|
67
|
-
[string] $CheckpointPath = $script:OrchestratorStateCheckpointPath,
|
|
68
|
-
|
|
69
|
-
[Parameter(Mandatory = $false)]
|
|
70
|
-
[scriptblock] $Invoker = {
|
|
71
|
-
param($Path)
|
|
72
|
-
$output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
|
|
73
|
-
orchestrator-state $Path --require-pr-creation-ready 2>&1
|
|
74
|
-
[pscustomobject]@{
|
|
75
|
-
ExitCode = $LASTEXITCODE
|
|
76
|
-
Output = ($output | Out-String)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
$result = & $Invoker $CheckpointPath
|
|
82
|
-
$exitCode = 0
|
|
83
|
-
if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'ExitCode')) { $exitCode = [int]$result.ExitCode }
|
|
84
|
-
$outputText = ''
|
|
85
|
-
if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Output')) { $outputText = ([string]$result.Output).Trim() }
|
|
86
|
-
|
|
87
|
-
return @{ HasErrors = ($exitCode -ne 0); ErrorText = $outputText }
|
|
88
|
-
}
|
|
49
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force
|
|
89
50
|
|
|
90
51
|
function Get-PrContextArtifactExistence {
|
|
91
52
|
<#
|
|
@@ -359,7 +320,7 @@ function Get-PrAuthorBypassReason {
|
|
|
359
320
|
# Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
|
|
360
321
|
# bypassed by invoking gh pr create/edit directly) before receipt verification.
|
|
361
322
|
if ($hasBodyFile -and $ContextExists) {
|
|
362
|
-
$preflightResult = Invoke-OrchestratorStatePreflight
|
|
323
|
+
$preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $script:OrchestratorStateCheckpointPath
|
|
363
324
|
if ($preflightResult.HasErrors) {
|
|
364
325
|
$preflightSummary = if ([string]::IsNullOrWhiteSpace($preflightResult.ErrorText)) {
|
|
365
326
|
"checkpoint missing at $script:OrchestratorStateCheckpointPath"
|
|
@@ -38,6 +38,8 @@ param(
|
|
|
38
38
|
Set-StrictMode -Version Latest
|
|
39
39
|
$ErrorActionPreference = 'Stop'
|
|
40
40
|
|
|
41
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force
|
|
42
|
+
|
|
41
43
|
function Get-CheckpointFileContent {
|
|
42
44
|
<#
|
|
43
45
|
.SYNOPSIS
|
|
@@ -179,11 +181,30 @@ function Invoke-RoutingContractValidation {
|
|
|
179
181
|
[Parameter(Mandatory = $false)]
|
|
180
182
|
[scriptblock] $Invoker = {
|
|
181
183
|
param($Path, $Type)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
# Capability detection: use the authoritative Python CLI when
|
|
185
|
+
# scripts.dev_tools is importable (drm-copilot); otherwise fall back to
|
|
186
|
+
# the portable PowerShell completion module that travels with the
|
|
187
|
+
# pushed-down pack. The portable path performs the presence-level
|
|
188
|
+
# required-once-delegated existence gate and still fails closed.
|
|
189
|
+
if (Test-PythonOrchestratorValidatorAvailable) {
|
|
190
|
+
$output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
|
|
191
|
+
$Type $Path --require-complete --require-model-routing 2>&1
|
|
192
|
+
[pscustomobject]@{
|
|
193
|
+
ExitCode = $LASTEXITCODE
|
|
194
|
+
Output = ($output | Out-String)
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
# Import the portable completion module only when its function is not
|
|
198
|
+
# already available, so a repeated call (or a test that pre-imports and
|
|
199
|
+
# mocks the function) does not reload the module and reset the seam.
|
|
200
|
+
if (-not (Get-Command -Name Test-OrchestratorStateCompletionReadiness -ErrorAction SilentlyContinue)) {
|
|
201
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force
|
|
202
|
+
}
|
|
203
|
+
$portable = Test-OrchestratorStateCompletionReadiness -CheckpointPath $Path
|
|
204
|
+
[pscustomobject]@{
|
|
205
|
+
ExitCode = $portable.ExitCode
|
|
206
|
+
Output = $portable.Output
|
|
207
|
+
}
|
|
187
208
|
}
|
|
188
209
|
}
|
|
189
210
|
)
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Portable orchestrator-state checkpoint checks for pushed-down enforcement hooks.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Provides a self-contained PowerShell implementation of the pushed-down-relevant
|
|
7
|
+
orchestrator-state checkpoint validations so the `.claude` enforcement hooks work
|
|
8
|
+
in consumer repositories that do not ship `scripts/dev_tools` (the authoritative
|
|
9
|
+
Python validator). This module mirrors the portable pattern of
|
|
10
|
+
`.claude/lib/model-routing/ModelRouting.psm1`.
|
|
11
|
+
|
|
12
|
+
It implements PR-creation-readiness parity with
|
|
13
|
+
`scripts/dev_tools/_orchestrator_state_pr_creation_readiness.py` and the base
|
|
14
|
+
checkpoint-presence checks (required keys, step-status validity, blocked_reason
|
|
15
|
+
validity) from `scripts/dev_tools/validate_orchestrator_state.py`. The base
|
|
16
|
+
constants below are pinned to `REQUIRED_STATE_KEYS`, `STEP_STATUS_KEYS`,
|
|
17
|
+
`VALID_STEP_STATUS`, and `VALID_BLOCKED_REASONS` in that validator.
|
|
18
|
+
|
|
19
|
+
Every public function FAILS CLOSED: a missing checkpoint file, invalid JSON, a
|
|
20
|
+
missing required key, an invalid step status, or an unmet readiness condition all
|
|
21
|
+
yield a non-zero ExitCode with a non-empty Output message. The Python validator
|
|
22
|
+
remains the authoritative reference; this module is the destination-runtime
|
|
23
|
+
mirror used only when the Python module is not importable.
|
|
24
|
+
|
|
25
|
+
This module also hosts the capability-detection probe
|
|
26
|
+
(Test-PythonOrchestratorValidatorAvailable), shared by both pushed-down hooks
|
|
27
|
+
(.claude/hooks/enforce-pr-author-skill.ps1 and
|
|
28
|
+
.claude/hooks/validate-orchestrator-output.ps1) so neither hook duplicates it
|
|
29
|
+
locally, and the PR-creation preflight orchestration helper
|
|
30
|
+
(Invoke-OrchestratorStatePreflight) consumed by enforce-pr-author-skill.ps1.
|
|
31
|
+
#>
|
|
32
|
+
|
|
33
|
+
Set-StrictMode -Version Latest
|
|
34
|
+
|
|
35
|
+
# The canonical top-level checkpoint keys required by the primary validator.
|
|
36
|
+
# Pinned to REQUIRED_STATE_KEYS in scripts/dev_tools/validate_orchestrator_state.py.
|
|
37
|
+
$script:REQUIRED_STATE_KEYS = @(
|
|
38
|
+
'objective',
|
|
39
|
+
'change_budget_estimate',
|
|
40
|
+
'path_selected',
|
|
41
|
+
'promotion-type',
|
|
42
|
+
'short-name',
|
|
43
|
+
'relativeFile',
|
|
44
|
+
'long-name',
|
|
45
|
+
'issue-num',
|
|
46
|
+
'feature-folder',
|
|
47
|
+
'work-mode',
|
|
48
|
+
'plan-path',
|
|
49
|
+
'completed_steps',
|
|
50
|
+
'next_step',
|
|
51
|
+
'last_updated',
|
|
52
|
+
'step5_status',
|
|
53
|
+
'step6_status',
|
|
54
|
+
'step7_status',
|
|
55
|
+
'step8_status',
|
|
56
|
+
'step9_status',
|
|
57
|
+
'step10_status',
|
|
58
|
+
'delegation_receipts',
|
|
59
|
+
'blocked_reason'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# The lifecycle step-status keys whose value, when present, must be a member of
|
|
63
|
+
# VALID_STEP_STATUS. Pinned to STEP_STATUS_KEYS in the primary validator.
|
|
64
|
+
$script:STEP_STATUS_KEYS = @(
|
|
65
|
+
'step5_status',
|
|
66
|
+
'step6_status',
|
|
67
|
+
'step7_status',
|
|
68
|
+
'step8_status',
|
|
69
|
+
'step9_status',
|
|
70
|
+
'step10_status'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# The allowed step-status vocabulary. Pinned to VALID_STEP_STATUS in the primary
|
|
74
|
+
# validator.
|
|
75
|
+
$script:VALID_STEP_STATUS = @(
|
|
76
|
+
'not-applicable',
|
|
77
|
+
'pending',
|
|
78
|
+
'delegated',
|
|
79
|
+
'verified',
|
|
80
|
+
'blocked',
|
|
81
|
+
'not_started',
|
|
82
|
+
'in_progress',
|
|
83
|
+
'completed'
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# The allowed blocked_reason vocabulary. Pinned to VALID_BLOCKED_REASONS in the
|
|
87
|
+
# primary validator.
|
|
88
|
+
$script:VALID_BLOCKED_REASONS = @(
|
|
89
|
+
'none',
|
|
90
|
+
'spawn_agent_unavailable',
|
|
91
|
+
'delegation_launch_failed',
|
|
92
|
+
'delegate_no_receipt',
|
|
93
|
+
'delegate_contract_incomplete',
|
|
94
|
+
'validator_failed',
|
|
95
|
+
'user_requested_stop'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# The upstream steps that must not be pending/blocked before the first PR creation
|
|
99
|
+
# of a branch. Pinned to PR_CREATION_READY_STEP_KEYS in
|
|
100
|
+
# _orchestrator_state_pr_creation_readiness.py (deliberately narrower than the full
|
|
101
|
+
# step set: steps 9-10 can only populate after PR creation and CI have run).
|
|
102
|
+
$script:PR_CREATION_READY_STEP_KEYS = @(
|
|
103
|
+
'step5_status',
|
|
104
|
+
'step6_status',
|
|
105
|
+
'step7_status',
|
|
106
|
+
'step8_status'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# The checkpoint list fields that must be empty (or absent) before the first PR
|
|
110
|
+
# creation. Pinned to PR_CREATION_READY_EMPTY_LIST_KEYS in the Python reference.
|
|
111
|
+
$script:PR_CREATION_READY_EMPTY_LIST_KEYS = @(
|
|
112
|
+
'local_execution_overrides',
|
|
113
|
+
'delegation_bypasses'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
function Get-OrchestratorStateCheckpoint {
|
|
118
|
+
<#
|
|
119
|
+
.SYNOPSIS
|
|
120
|
+
Load and parse an orchestrator-state checkpoint, failing closed on error.
|
|
121
|
+
.DESCRIPTION
|
|
122
|
+
Private load helper. Reads the checkpoint at -CheckpointPath and parses it as
|
|
123
|
+
JSON. It never throws to the caller: a missing file or unparseable/invalid
|
|
124
|
+
JSON is reported through a structured result with Ok = $false and a non-empty
|
|
125
|
+
Error string, so callers can translate the failure into a fail-closed
|
|
126
|
+
non-zero ExitCode.
|
|
127
|
+
.PARAMETER CheckpointPath
|
|
128
|
+
The path to the orchestrator-state checkpoint JSON file.
|
|
129
|
+
.OUTPUTS
|
|
130
|
+
System.Collections.Hashtable with keys:
|
|
131
|
+
- Ok (bool): $true when the file exists and parsed to a JSON object.
|
|
132
|
+
- State (object): the parsed PSCustomObject on success, otherwise $null.
|
|
133
|
+
- Error (string): a non-empty failure message on failure, otherwise ''.
|
|
134
|
+
#>
|
|
135
|
+
[CmdletBinding()]
|
|
136
|
+
[OutputType([hashtable])]
|
|
137
|
+
param(
|
|
138
|
+
[Parameter(Mandatory = $true)]
|
|
139
|
+
[string] $CheckpointPath
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# A missing checkpoint file is a fail-closed condition: there is no state to
|
|
143
|
+
# validate, so readiness cannot be established.
|
|
144
|
+
if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
|
|
145
|
+
return @{
|
|
146
|
+
Ok = $false
|
|
147
|
+
State = $null
|
|
148
|
+
Error = "Checkpoint file '$CheckpointPath' does not exist."
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
$raw = Get-Content -LiteralPath $CheckpointPath -Raw -ErrorAction Stop
|
|
153
|
+
|
|
154
|
+
# An empty checkpoint carries no state; treat it as fail-closed rather than
|
|
155
|
+
# letting ConvertFrom-Json return $null silently.
|
|
156
|
+
if ([string]::IsNullOrWhiteSpace($raw)) {
|
|
157
|
+
return @{
|
|
158
|
+
Ok = $false
|
|
159
|
+
State = $null
|
|
160
|
+
Error = "Checkpoint file '$CheckpointPath' is empty."
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Parse the checkpoint text. Invalid JSON is a fail-closed condition, caught here
|
|
165
|
+
# so the caller receives a message instead of a terminating error.
|
|
166
|
+
try {
|
|
167
|
+
$state = $raw | ConvertFrom-Json -ErrorAction Stop
|
|
168
|
+
} catch {
|
|
169
|
+
return @{
|
|
170
|
+
Ok = $false
|
|
171
|
+
State = $null
|
|
172
|
+
Error = "Checkpoint file '$CheckpointPath' is not valid JSON: $($_.Exception.Message)"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# A JSON scalar or array root is not a checkpoint object; require an object so
|
|
177
|
+
# the presence checks can inspect named fields.
|
|
178
|
+
if ($state -isnot [System.Management.Automation.PSCustomObject]) {
|
|
179
|
+
return @{
|
|
180
|
+
Ok = $false
|
|
181
|
+
State = $null
|
|
182
|
+
Error = "Checkpoint file '$CheckpointPath' root must be a JSON object."
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return @{ Ok = $true; State = $state; Error = '' }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function Get-OrchestratorStateField {
|
|
190
|
+
<#
|
|
191
|
+
.SYNOPSIS
|
|
192
|
+
Read a checkpoint field, distinguishing an absent key from a null value.
|
|
193
|
+
.DESCRIPTION
|
|
194
|
+
Private accessor that safely reads a named property from the parsed
|
|
195
|
+
checkpoint object under Set-StrictMode, where accessing an undefined property
|
|
196
|
+
would otherwise throw. Returns whether the key is present and its value,
|
|
197
|
+
mirroring the semantics of Python's ``dict.get`` (absent and null both read
|
|
198
|
+
as "no value" for the readiness checks).
|
|
199
|
+
.PARAMETER State
|
|
200
|
+
The parsed checkpoint PSCustomObject.
|
|
201
|
+
.PARAMETER Name
|
|
202
|
+
The property name to read.
|
|
203
|
+
.OUTPUTS
|
|
204
|
+
System.Collections.Hashtable with keys Present (bool) and Value (object).
|
|
205
|
+
#>
|
|
206
|
+
[CmdletBinding()]
|
|
207
|
+
[OutputType([hashtable])]
|
|
208
|
+
param(
|
|
209
|
+
[Parameter(Mandatory = $true)]
|
|
210
|
+
[psobject] $State,
|
|
211
|
+
|
|
212
|
+
[Parameter(Mandatory = $true)]
|
|
213
|
+
[string] $Name
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
$names = @($State.PSObject.Properties.Name)
|
|
217
|
+
if ($names -contains $Name) {
|
|
218
|
+
return @{ Present = $true; Value = $State.$Name }
|
|
219
|
+
}
|
|
220
|
+
return @{ Present = $false; Value = $null }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function Get-OrchestratorStateBasePresenceError {
|
|
224
|
+
<#
|
|
225
|
+
.SYNOPSIS
|
|
226
|
+
Return the base checkpoint-presence errors, mirroring the primary validator.
|
|
227
|
+
.DESCRIPTION
|
|
228
|
+
Private base check. Emits one error string per missing required key, one per
|
|
229
|
+
step5_status..step10_status value outside VALID_STEP_STATUS, and one when
|
|
230
|
+
blocked_reason is present with a value outside VALID_BLOCKED_REASONS. This
|
|
231
|
+
mirrors the base block of scripts/dev_tools/validate_orchestrator_state.py
|
|
232
|
+
(required keys, step-status validity, blocked_reason validity) that runs
|
|
233
|
+
before any mode-specific gate.
|
|
234
|
+
.PARAMETER State
|
|
235
|
+
The parsed checkpoint PSCustomObject.
|
|
236
|
+
.OUTPUTS
|
|
237
|
+
System.String[] - zero or more error strings; empty when the base shape is valid.
|
|
238
|
+
#>
|
|
239
|
+
[CmdletBinding()]
|
|
240
|
+
[OutputType([string[]])]
|
|
241
|
+
param(
|
|
242
|
+
[Parameter(Mandatory = $true)]
|
|
243
|
+
[psobject] $State
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
247
|
+
$names = @($State.PSObject.Properties.Name)
|
|
248
|
+
|
|
249
|
+
# Require every canonical top-level field; a missing key is reported individually
|
|
250
|
+
# so the operator sees exactly which fields are absent.
|
|
251
|
+
foreach ($key in $script:REQUIRED_STATE_KEYS) {
|
|
252
|
+
if ($names -notcontains $key) {
|
|
253
|
+
$errors.Add("Checkpoint missing required key: $key")
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Every present step status must be a member of the allowed vocabulary; an absent
|
|
258
|
+
# step key contributes no error (mirrors the primary validator's None guard).
|
|
259
|
+
foreach ($key in $script:STEP_STATUS_KEYS) {
|
|
260
|
+
$field = Get-OrchestratorStateField -State $State -Name $key
|
|
261
|
+
if ($field.Present -and $null -ne $field.Value -and
|
|
262
|
+
($script:VALID_STEP_STATUS -notcontains [string]$field.Value)) {
|
|
263
|
+
$errors.Add("Checkpoint has invalid $key`: $($field.Value)")
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# A present, non-null blocked_reason must be a member of the allowed vocabulary.
|
|
268
|
+
$blocked = Get-OrchestratorStateField -State $State -Name 'blocked_reason'
|
|
269
|
+
if ($blocked.Present -and $null -ne $blocked.Value -and
|
|
270
|
+
($script:VALID_BLOCKED_REASONS -notcontains [string]$blocked.Value)) {
|
|
271
|
+
$errors.Add("Checkpoint has invalid blocked_reason: $($blocked.Value)")
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return $errors.ToArray()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function Get-OrchestratorStatePrCreationReadinessError {
|
|
278
|
+
<#
|
|
279
|
+
.SYNOPSIS
|
|
280
|
+
Return the PR-creation-readiness errors, parity with the Python reference.
|
|
281
|
+
.DESCRIPTION
|
|
282
|
+
Private readiness check mirroring
|
|
283
|
+
validate_orchestrator_state_pr_creation_readiness in
|
|
284
|
+
_orchestrator_state_pr_creation_readiness.py: steps 5-8 must not be
|
|
285
|
+
pending/blocked; blocked_reason must be `none` or absent; and the
|
|
286
|
+
local_execution_overrides / delegation_bypasses lists must be empty when
|
|
287
|
+
present. It does not enforce completion, CI, PR, or routing-contract gates.
|
|
288
|
+
.PARAMETER State
|
|
289
|
+
The parsed checkpoint PSCustomObject.
|
|
290
|
+
.OUTPUTS
|
|
291
|
+
System.String[] - zero or more error strings; empty when ready for PR creation.
|
|
292
|
+
#>
|
|
293
|
+
[CmdletBinding()]
|
|
294
|
+
[OutputType([string[]])]
|
|
295
|
+
param(
|
|
296
|
+
[Parameter(Mandatory = $true)]
|
|
297
|
+
[psobject] $State
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
301
|
+
|
|
302
|
+
# Reject an upstream step recorded as pending or blocked; steps 5-8 must have
|
|
303
|
+
# finished before the first PR of a branch is created.
|
|
304
|
+
foreach ($key in $script:PR_CREATION_READY_STEP_KEYS) {
|
|
305
|
+
$field = Get-OrchestratorStateField -State $State -Name $key
|
|
306
|
+
if ($field.Present -and ($field.Value -eq 'pending' -or $field.Value -eq 'blocked')) {
|
|
307
|
+
$errors.Add("Checkpoint PR-creation readiness validation failed: $key is $($field.Value).")
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# blocked_reason must read as clear: absent, null, or the literal 'none'. Any
|
|
312
|
+
# other recorded reason means the branch is not ready for PR creation.
|
|
313
|
+
$blocked = Get-OrchestratorStateField -State $State -Name 'blocked_reason'
|
|
314
|
+
if ($blocked.Present -and $null -ne $blocked.Value -and ([string]$blocked.Value -ne 'none')) {
|
|
315
|
+
$errors.Add('Checkpoint PR-creation readiness validation failed: blocked_reason is not `none`.')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# Each override list must be empty when present: a present non-list value, or a
|
|
319
|
+
# present non-empty list, means overrides/bypasses were recorded (mirrors the
|
|
320
|
+
# Python `value is not None and (not isinstance(value, list) or value)` guard).
|
|
321
|
+
foreach ($key in $script:PR_CREATION_READY_EMPTY_LIST_KEYS) {
|
|
322
|
+
$field = Get-OrchestratorStateField -State $State -Name $key
|
|
323
|
+
if ($field.Present -and $null -ne $field.Value) {
|
|
324
|
+
$isList = $field.Value -is [System.Array]
|
|
325
|
+
if (-not $isList -or @($field.Value).Count -gt 0) {
|
|
326
|
+
$errors.Add("Checkpoint PR-creation readiness validation failed: $key must be an empty list when present.")
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return $errors.ToArray()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function Test-PythonOrchestratorValidatorAvailable {
|
|
335
|
+
<#
|
|
336
|
+
.SYNOPSIS
|
|
337
|
+
Probe whether the authoritative Python orchestrator-state validator is importable.
|
|
338
|
+
.DESCRIPTION
|
|
339
|
+
Capability-detection seam. Returns $true only when
|
|
340
|
+
``python -c "import scripts.dev_tools.validate_orchestration_artifacts"`` exits 0,
|
|
341
|
+
indicating the authoritative Python validator ships in this repository (drm-copilot).
|
|
342
|
+
Returns $false on any non-zero exit or error, so a consumer repository that received
|
|
343
|
+
only the pushed-down `.claude` pack (no `scripts/dev_tools`) routes to the portable
|
|
344
|
+
PowerShell module. Any probe failure routes to the portable path, which itself fails
|
|
345
|
+
closed on bad checkpoints, preserving fail-closed semantics in both branches. Tests
|
|
346
|
+
mock this seam directly; they never mock `python`.
|
|
347
|
+
.OUTPUTS
|
|
348
|
+
System.Boolean
|
|
349
|
+
#>
|
|
350
|
+
[CmdletBinding()]
|
|
351
|
+
[OutputType([bool])]
|
|
352
|
+
param()
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
& python -c 'import scripts.dev_tools.validate_orchestration_artifacts' 2>&1 | Out-Null
|
|
356
|
+
return ($LASTEXITCODE -eq 0)
|
|
357
|
+
} catch {
|
|
358
|
+
return $false
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function Test-OrchestratorStatePrCreationReadiness {
|
|
363
|
+
<#
|
|
364
|
+
.SYNOPSIS
|
|
365
|
+
Validate a checkpoint is ready for the first `gh pr create` of a branch.
|
|
366
|
+
.DESCRIPTION
|
|
367
|
+
Public entry point used by the pushed-down enforce-pr-author-skill hook when
|
|
368
|
+
the authoritative Python validator is not importable. Loads the checkpoint
|
|
369
|
+
(fail-closed on missing file / invalid JSON), runs the base-presence check
|
|
370
|
+
(required keys, step-status validity, blocked_reason validity), then runs the
|
|
371
|
+
PR-creation-readiness parity check. Returns a hashtable compatible with the
|
|
372
|
+
hook's existing invoker contract: ExitCode is 1 whenever any error is present,
|
|
373
|
+
and Output carries the newline-joined error text (empty on success).
|
|
374
|
+
.PARAMETER CheckpointPath
|
|
375
|
+
The path to the orchestrator-state checkpoint JSON file.
|
|
376
|
+
.OUTPUTS
|
|
377
|
+
System.Collections.Hashtable with keys ExitCode (int, 0 or 1) and Output (string).
|
|
378
|
+
#>
|
|
379
|
+
[CmdletBinding()]
|
|
380
|
+
[OutputType([hashtable])]
|
|
381
|
+
param(
|
|
382
|
+
[Parameter(Mandatory = $true)]
|
|
383
|
+
[string] $CheckpointPath
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Fail closed when the checkpoint cannot be loaded: the load error is the whole
|
|
387
|
+
# output and ExitCode is 1.
|
|
388
|
+
$loaded = Get-OrchestratorStateCheckpoint -CheckpointPath $CheckpointPath
|
|
389
|
+
if (-not $loaded.Ok) {
|
|
390
|
+
return @{ ExitCode = 1; Output = $loaded.Error }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Accumulate base-presence errors and readiness errors; any error yields a
|
|
394
|
+
# non-zero ExitCode so the hook blocks PR creation.
|
|
395
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
396
|
+
$errors.AddRange([string[]]@(Get-OrchestratorStateBasePresenceError -State $loaded.State))
|
|
397
|
+
$errors.AddRange([string[]]@(Get-OrchestratorStatePrCreationReadinessError -State $loaded.State))
|
|
398
|
+
|
|
399
|
+
if ($errors.Count -gt 0) {
|
|
400
|
+
return @{ ExitCode = 1; Output = ($errors -join [System.Environment]::NewLine) }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return @{ ExitCode = 0; Output = '' }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function Invoke-OrchestratorStatePreflight {
|
|
407
|
+
<#
|
|
408
|
+
.SYNOPSIS
|
|
409
|
+
Runs the orchestrator-state validator against the checkpoint and reports pass/fail.
|
|
410
|
+
.DESCRIPTION
|
|
411
|
+
Shared by the pushed-down enforce-pr-author-skill hook. Mirrors
|
|
412
|
+
Invoke-RoutingContractValidation (.claude/hooks/validate-orchestrator-output.ps1):
|
|
413
|
+
an injectable subprocess scriptblock seam defaults to ``python -m
|
|
414
|
+
scripts.dev_tools.validate_orchestration_artifacts orchestrator-state <CheckpointPath>
|
|
415
|
+
--require-pr-creation-ready``. A missing checkpoint or --require-pr-creation-ready failure
|
|
416
|
+
both surface via the validator's non-zero exit/stderr text; no separate file-existence check
|
|
417
|
+
is made, validating pre-PR-creation readiness (steps 5-8, blocked_reason) not full completion.
|
|
418
|
+
.PARAMETER CheckpointPath
|
|
419
|
+
The path to the orchestrator-state checkpoint JSON file. Callers pass their own
|
|
420
|
+
checkpoint-path variable explicitly; the default below is only used when a caller omits
|
|
421
|
+
the parameter.
|
|
422
|
+
.OUTPUTS
|
|
423
|
+
System.Collections.Hashtable with keys HasErrors (bool) and ErrorText (string).
|
|
424
|
+
#>
|
|
425
|
+
[CmdletBinding()]
|
|
426
|
+
[OutputType([hashtable])]
|
|
427
|
+
param(
|
|
428
|
+
[Parameter(Mandatory = $false)]
|
|
429
|
+
[string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json',
|
|
430
|
+
|
|
431
|
+
[Parameter(Mandatory = $false)]
|
|
432
|
+
[scriptblock] $Invoker = {
|
|
433
|
+
param($Path)
|
|
434
|
+
# Capability detection: use the authoritative Python CLI when
|
|
435
|
+
# scripts.dev_tools is importable (drm-copilot); otherwise fall back to
|
|
436
|
+
# the portable PowerShell function that lives alongside this one in the
|
|
437
|
+
# pushed-down pack.
|
|
438
|
+
if (Test-PythonOrchestratorValidatorAvailable) {
|
|
439
|
+
$output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
|
|
440
|
+
orchestrator-state $Path --require-pr-creation-ready 2>&1
|
|
441
|
+
[pscustomobject]@{
|
|
442
|
+
ExitCode = $LASTEXITCODE
|
|
443
|
+
Output = ($output | Out-String)
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
$portable = Test-OrchestratorStatePrCreationReadiness -CheckpointPath $Path
|
|
447
|
+
[pscustomobject]@{
|
|
448
|
+
ExitCode = $portable.ExitCode
|
|
449
|
+
Output = $portable.Output
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
$result = & $Invoker $CheckpointPath
|
|
456
|
+
# Under this module's Set-StrictMode -Version Latest, member-enumerating .Name directly over
|
|
457
|
+
# a zero-property PSCustomObject throws (a PowerShell strict-mode gotcha not present in the
|
|
458
|
+
# hook's un-strict scope this code was moved from), so the property collection is counted
|
|
459
|
+
# before .Name is ever accessed.
|
|
460
|
+
$resultPropertyNames = @()
|
|
461
|
+
if ($null -ne $result -and @($result.PSObject.Properties).Count -gt 0) {
|
|
462
|
+
$resultPropertyNames = @($result.PSObject.Properties.Name)
|
|
463
|
+
}
|
|
464
|
+
$exitCode = 0
|
|
465
|
+
if ($resultPropertyNames -contains 'ExitCode') { $exitCode = [int]$result.ExitCode }
|
|
466
|
+
$outputText = ''
|
|
467
|
+
if ($resultPropertyNames -contains 'Output') { $outputText = ([string]$result.Output).Trim() }
|
|
468
|
+
|
|
469
|
+
return @{ HasErrors = ($exitCode -ne 0); ErrorText = $outputText }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Export the public readiness entry point plus the reusable load, field-accessor,
|
|
473
|
+
# and base-presence primitives so the sibling OrchestratorStateCompletion module can
|
|
474
|
+
# consume them via Import-Module without duplicating the shared parsing and
|
|
475
|
+
# base-check logic. Test-PythonOrchestratorValidatorAvailable and
|
|
476
|
+
# Invoke-OrchestratorStatePreflight are exported so both pushed-down hooks
|
|
477
|
+
# (enforce-pr-author-skill.ps1, validate-orchestrator-output.ps1) can consume them
|
|
478
|
+
# without duplicating the capability probe or the PR-creation preflight orchestration.
|
|
479
|
+
Export-ModuleMember -Function `
|
|
480
|
+
Test-OrchestratorStatePrCreationReadiness, `
|
|
481
|
+
Get-OrchestratorStateCheckpoint, `
|
|
482
|
+
Get-OrchestratorStateField, `
|
|
483
|
+
Get-OrchestratorStateBasePresenceError, `
|
|
484
|
+
Test-PythonOrchestratorValidatorAvailable, `
|
|
485
|
+
Invoke-OrchestratorStatePreflight
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Portable completion-gate presence checks for the orchestrator-state checkpoint.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Provides the destination-runtime PowerShell mirror of the completion-gate
|
|
7
|
+
presence checks the pushed-down validate-orchestrator-output hook needs when the
|
|
8
|
+
authoritative Python validator (`scripts/dev_tools`) is not importable. It
|
|
9
|
+
reuses the shared load, field-accessor, and base-presence primitives from the
|
|
10
|
+
sibling `OrchestratorState.psm1` and imports `.claude/lib/model-routing/ModelRouting.psm1`
|
|
11
|
+
so per-receipt model formulas are available where practical.
|
|
12
|
+
|
|
13
|
+
The single public function `Test-OrchestratorStateCompletionReadiness` fails
|
|
14
|
+
closed on a missing checkpoint file, invalid JSON, or an invalid base shape, then
|
|
15
|
+
applies the model-routing "required once delegated" existence gate - the
|
|
16
|
+
delegated-agent set (derived from `delegation_receipts[].agent_name` plus a
|
|
17
|
+
delegating `next_step`) must be a subset of `model_routing_receipts[].agent` -
|
|
18
|
+
mirroring `scripts/dev_tools/_orchestrator_state_model_routing_gate.py`. Deep
|
|
19
|
+
per-receipt routing-contract correctness that requires full Python authority is a
|
|
20
|
+
documented Non-Goal for the portable path; the gate performs the presence-level
|
|
21
|
+
existence check and reports missing receipts with error text containing the
|
|
22
|
+
literal token `model_routing_receipts`, so the completion hook maps a failure to
|
|
23
|
+
its `MODEL_ROUTING_BLOCKED:` block reason. The Python validator remains
|
|
24
|
+
authoritative; this module is the fallback mirror only.
|
|
25
|
+
#>
|
|
26
|
+
|
|
27
|
+
Set-StrictMode -Version Latest
|
|
28
|
+
|
|
29
|
+
# Import the sibling shared module and the portable model-routing module, resolved
|
|
30
|
+
# relative to this module's directory so the imports travel with the pushed-down
|
|
31
|
+
# pack regardless of the consumer repository's working directory.
|
|
32
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorState.psm1') -Force
|
|
33
|
+
$script:ModelRoutingModulePath = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..') -ChildPath (Join-Path -Path 'model-routing' -ChildPath 'ModelRouting.psm1')
|
|
34
|
+
Import-Module $script:ModelRoutingModulePath -Force
|
|
35
|
+
|
|
36
|
+
# The subagent types delegated via the Agent tool that can be named by a delegating
|
|
37
|
+
# next_step. Pinned to _DELEGATING_AGENTS in
|
|
38
|
+
# scripts/dev_tools/_orchestrator_state_model_routing_gate.py. The `orchestrator`
|
|
39
|
+
# type is deliberately excluded: it is the caller, never a routing-receipt target.
|
|
40
|
+
$script:DELEGATING_AGENTS = @(
|
|
41
|
+
'atomic-planner',
|
|
42
|
+
'atomic-executor',
|
|
43
|
+
'feature-review',
|
|
44
|
+
'task-researcher',
|
|
45
|
+
'prd-feature',
|
|
46
|
+
'pr-author'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Checkpoint keys the gate reads to derive the delegated-agent and receipt-agent sets.
|
|
50
|
+
$script:DELEGATION_RECEIPTS_KEY = 'delegation_receipts'
|
|
51
|
+
$script:MODEL_ROUTING_RECEIPTS_KEY = 'model_routing_receipts'
|
|
52
|
+
$script:NEXT_STEP_KEY = 'next_step'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
function Get-OrchestratorStateDelegatedAgent {
|
|
56
|
+
<#
|
|
57
|
+
.SYNOPSIS
|
|
58
|
+
Derive the set of agents a checkpoint has delegated (or is about to).
|
|
59
|
+
.DESCRIPTION
|
|
60
|
+
Private helper mirroring ``_delegated_agents`` in the Python gate. Collects
|
|
61
|
+
each well-formed ``delegation_receipts[]`` entry's non-empty ``agent_name``
|
|
62
|
+
plus the agent implied by a ``next_step`` that names a recognized delegating
|
|
63
|
+
agent. The list form of ``delegation_receipts`` is the authoritative "a
|
|
64
|
+
delegation happened" record; the namespaced (promotion) object form carries
|
|
65
|
+
no agent_name list and contributes no delegated agents.
|
|
66
|
+
.PARAMETER State
|
|
67
|
+
The parsed checkpoint PSCustomObject.
|
|
68
|
+
.OUTPUTS
|
|
69
|
+
System.String[] - the delegated-agent names (may be empty).
|
|
70
|
+
#>
|
|
71
|
+
[CmdletBinding()]
|
|
72
|
+
[OutputType([string[]])]
|
|
73
|
+
param(
|
|
74
|
+
[Parameter(Mandatory = $true)]
|
|
75
|
+
[psobject] $State
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
$agents = [System.Collections.Generic.HashSet[string]]::new()
|
|
79
|
+
|
|
80
|
+
# Collect each list-form delegation receipt's non-empty agent_name. A non-list
|
|
81
|
+
# (namespaced) delegation_receipts value contributes nothing here.
|
|
82
|
+
$receiptsField = Get-OrchestratorStateField -State $State -Name $script:DELEGATION_RECEIPTS_KEY
|
|
83
|
+
if ($receiptsField.Present -and ($receiptsField.Value -is [System.Array])) {
|
|
84
|
+
foreach ($receipt in $receiptsField.Value) {
|
|
85
|
+
if ($receipt -is [System.Management.Automation.PSCustomObject]) {
|
|
86
|
+
$nameField = Get-OrchestratorStateField -State $receipt -Name 'agent_name'
|
|
87
|
+
if ($nameField.Present -and $null -ne $nameField.Value -and
|
|
88
|
+
-not [string]::IsNullOrWhiteSpace([string]$nameField.Value)) {
|
|
89
|
+
[void]$agents.Add([string]$nameField.Value)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# A delegating next_step names the upcoming delegation that may not yet have a
|
|
96
|
+
# receipt; include it only when it matches a recognized delegating agent so a
|
|
97
|
+
# non-delegating label (for example "complete") never triggers the gate.
|
|
98
|
+
$nextStepField = Get-OrchestratorStateField -State $State -Name $script:NEXT_STEP_KEY
|
|
99
|
+
if ($nextStepField.Present -and $null -ne $nextStepField.Value -and
|
|
100
|
+
($script:DELEGATING_AGENTS -contains [string]$nextStepField.Value)) {
|
|
101
|
+
[void]$agents.Add([string]$nextStepField.Value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [string[]]@($agents)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Get-OrchestratorStateRoutingReceiptAgent {
|
|
108
|
+
<#
|
|
109
|
+
.SYNOPSIS
|
|
110
|
+
Collect the set of agents that carry a model-routing receipt.
|
|
111
|
+
.DESCRIPTION
|
|
112
|
+
Private helper mirroring the receipt-agent harvest in the Python gate. Reads
|
|
113
|
+
the checkpoint's ``model_routing_receipts[]`` array and returns the set of
|
|
114
|
+
non-empty ``agent`` values present on well-formed receipt objects. A non-list
|
|
115
|
+
value contributes no agents (the existence gate then reports every delegated
|
|
116
|
+
agent as unreceipted, preserving fail-closed semantics).
|
|
117
|
+
.PARAMETER State
|
|
118
|
+
The parsed checkpoint PSCustomObject.
|
|
119
|
+
.OUTPUTS
|
|
120
|
+
System.String[] - the receipt-agent names (may be empty).
|
|
121
|
+
#>
|
|
122
|
+
[CmdletBinding()]
|
|
123
|
+
[OutputType([string[]])]
|
|
124
|
+
param(
|
|
125
|
+
[Parameter(Mandatory = $true)]
|
|
126
|
+
[psobject] $State
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
$agents = [System.Collections.Generic.HashSet[string]]::new()
|
|
130
|
+
|
|
131
|
+
$receiptsField = Get-OrchestratorStateField -State $State -Name $script:MODEL_ROUTING_RECEIPTS_KEY
|
|
132
|
+
if ($receiptsField.Present -and ($receiptsField.Value -is [System.Array])) {
|
|
133
|
+
# Record each well-formed receipt's non-empty agent so the existence gate can
|
|
134
|
+
# test the delegated-agent set against it.
|
|
135
|
+
foreach ($receipt in $receiptsField.Value) {
|
|
136
|
+
if ($receipt -is [System.Management.Automation.PSCustomObject]) {
|
|
137
|
+
$agentField = Get-OrchestratorStateField -State $receipt -Name 'agent'
|
|
138
|
+
if ($agentField.Present -and $null -ne $agentField.Value -and
|
|
139
|
+
-not [string]::IsNullOrWhiteSpace([string]$agentField.Value)) {
|
|
140
|
+
[void]$agents.Add([string]$agentField.Value)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return [string[]]@($agents)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function Get-OrchestratorStateModelRoutingGateError {
|
|
150
|
+
<#
|
|
151
|
+
.SYNOPSIS
|
|
152
|
+
Return the required-once-delegated existence-gate errors.
|
|
153
|
+
.DESCRIPTION
|
|
154
|
+
Private gate mirroring ``validate_model_routing_gate`` at the presence level:
|
|
155
|
+
it fires only when the checkpoint has delegated (or is about to delegate to)
|
|
156
|
+
at least one agent, then reports one error per delegated agent that lacks a
|
|
157
|
+
matching ``model_routing_receipts[]`` entry. A delegation-free checkpoint
|
|
158
|
+
contributes zero errors, preserving backward compatibility. Each error names
|
|
159
|
+
the literal token ``model_routing_receipts`` so the completion hook routes a
|
|
160
|
+
failure to ``MODEL_ROUTING_BLOCKED:``.
|
|
161
|
+
.PARAMETER State
|
|
162
|
+
The parsed checkpoint PSCustomObject.
|
|
163
|
+
.OUTPUTS
|
|
164
|
+
System.String[] - zero or more error strings; empty when the gate is satisfied
|
|
165
|
+
or does not fire.
|
|
166
|
+
#>
|
|
167
|
+
[CmdletBinding()]
|
|
168
|
+
[OutputType([string[]])]
|
|
169
|
+
param(
|
|
170
|
+
[Parameter(Mandatory = $true)]
|
|
171
|
+
[psobject] $State
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
175
|
+
|
|
176
|
+
# Backward-compat gate: a delegation-free checkpoint imposes no routing-receipt
|
|
177
|
+
# requirement, so return early with no errors.
|
|
178
|
+
$delegated = @(Get-OrchestratorStateDelegatedAgent -State $State)
|
|
179
|
+
if ($delegated.Count -eq 0) {
|
|
180
|
+
return $errors.ToArray()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
$receiptAgents = @(Get-OrchestratorStateRoutingReceiptAgent -State $State)
|
|
184
|
+
|
|
185
|
+
# Existence invariant: the routing-receipt agent set must be a superset of the
|
|
186
|
+
# delegated-agent set. Report each delegated agent with no receipt, sorted for
|
|
187
|
+
# deterministic error ordering.
|
|
188
|
+
$missing = $delegated | Where-Object { $receiptAgents -notcontains $_ } | Sort-Object
|
|
189
|
+
foreach ($agent in $missing) {
|
|
190
|
+
$errors.Add("Checkpoint model_routing_receipts is missing a receipt for delegated agent: $agent.")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return $errors.ToArray()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function Test-OrchestratorStateCompletionReadiness {
|
|
197
|
+
<#
|
|
198
|
+
.SYNOPSIS
|
|
199
|
+
Validate a checkpoint satisfies the portable completion-gate presence checks.
|
|
200
|
+
.DESCRIPTION
|
|
201
|
+
Public entry point used by the pushed-down validate-orchestrator-output hook
|
|
202
|
+
when the authoritative Python validator is not importable. Loads the
|
|
203
|
+
checkpoint (fail-closed on missing file / invalid JSON / invalid base shape),
|
|
204
|
+
runs the base-presence check (required keys, step-status validity,
|
|
205
|
+
blocked_reason validity), then applies the model-routing required-once-
|
|
206
|
+
delegated existence gate. Returns a hashtable compatible with the hook's
|
|
207
|
+
invoker contract: ExitCode is 1 whenever any error is present, and Output
|
|
208
|
+
carries the newline-joined error text (empty on success). A missing routing
|
|
209
|
+
receipt yields error text containing ``model_routing_receipts`` so the hook
|
|
210
|
+
surfaces it under ``MODEL_ROUTING_BLOCKED:``.
|
|
211
|
+
.PARAMETER CheckpointPath
|
|
212
|
+
The path to the orchestrator-state checkpoint JSON file.
|
|
213
|
+
.OUTPUTS
|
|
214
|
+
System.Collections.Hashtable with keys ExitCode (int, 0 or 1) and Output (string).
|
|
215
|
+
#>
|
|
216
|
+
[CmdletBinding()]
|
|
217
|
+
[OutputType([hashtable])]
|
|
218
|
+
param(
|
|
219
|
+
[Parameter(Mandatory = $true)]
|
|
220
|
+
[string] $CheckpointPath
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Fail closed when the checkpoint cannot be loaded: the load error is the whole
|
|
224
|
+
# output and ExitCode is 1.
|
|
225
|
+
$loaded = Get-OrchestratorStateCheckpoint -CheckpointPath $CheckpointPath
|
|
226
|
+
if (-not $loaded.Ok) {
|
|
227
|
+
return @{ ExitCode = 1; Output = $loaded.Error }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Accumulate base-presence errors and existence-gate errors; any error yields a
|
|
231
|
+
# non-zero ExitCode so the completion hook blocks DONE.
|
|
232
|
+
$errors = [System.Collections.Generic.List[string]]::new()
|
|
233
|
+
$errors.AddRange([string[]]@(Get-OrchestratorStateBasePresenceError -State $loaded.State))
|
|
234
|
+
$errors.AddRange([string[]]@(Get-OrchestratorStateModelRoutingGateError -State $loaded.State))
|
|
235
|
+
|
|
236
|
+
if ($errors.Count -gt 0) {
|
|
237
|
+
return @{ ExitCode = 1; Output = ($errors -join [System.Environment]::NewLine) }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return @{ ExitCode = 0; Output = '' }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Export-ModuleMember -Function Test-OrchestratorStateCompletionReadiness
|
|
@@ -119,6 +119,13 @@ reference implementations are `.claude/lib/model-routing/ModelRouting.psm1`
|
|
|
119
119
|
(`Get-ComplexityFloor`) and `.claude/lib/model-routing/ModelRouting.psm1`
|
|
120
120
|
(`Resolve-DelegationModel`). Default `fable_policy` is `disabled` when the marker is absent.
|
|
121
121
|
|
|
122
|
+
When `epic-orchestrator` itself spawns `Agent(orchestrator)` or `Agent(pr-author)`, it applies
|
|
123
|
+
the same per-delegation resolution and passes `model` equal to the routing receipt's `model` on
|
|
124
|
+
the spawn call. It MUST NOT omit `model` (an omitted `model` falls back to the delegate's
|
|
125
|
+
frontmatter default — `opus` for these workers — which suppresses a `fable` resolution) and
|
|
126
|
+
MUST NOT hard-code `model=opus` in a way that overrides the resolved routing model, mirroring
|
|
127
|
+
step 5 of `## Model Selection` in `.claude/skills/orchestrate/SKILL.md`.
|
|
128
|
+
|
|
122
129
|
`route` is never an input to model selection; `route` remains file-count driven and governs only
|
|
123
130
|
agents, skills, and MCP tools. A skill whose frontmatter `context` field holds the value `fork`
|
|
124
131
|
inherits the parent model and ignores a model override, so model selection applies to agent
|
|
@@ -95,6 +95,8 @@ End-to-end procedure:
|
|
|
95
95
|
3. **Run the per-delegation selection order.** For each delegation, resolve the model as `resolve_delegation_model(agent, complexity_band, fable_policy)`: the `table_model` is the `preferred` overlay value when (`fable_policy == "preferred"` and the agent is in the overlay set `{atomic-planner, prd-feature, feature-review, task-researcher}` and `band == "C3"`), otherwise the base `complexity_to_model[band]`. Under `fable_policy == "disabled"`, a `fable` `table_model` clamps to `model = "opus"` with `clamped_from = "fable"`. `atomic-executor` and `pr-author` C3 cells stay `opus` under every policy.
|
|
96
96
|
4. **Emit a routing receipt.** Record a `model_routing_receipts[]` entry `{ agent, phase, complexity_band, fable_policy, table_model, clamped_from | null, model }`. `table_model` is the pre-clamp lookup; `model` is the post-clamp result.
|
|
97
97
|
|
|
98
|
+
5. **Delegate with the resolved model.** Pass `model` equal to the receipt's `model` on the `Agent(...)` spawn call for that delegation. The orchestrator MUST NOT omit `model` on the spawn (an omitted `model` falls back to the delegate's frontmatter default — `opus` for most workers — which suppresses a `fable` or `sonnet` resolution), and MUST NOT hard-code `model=opus` in a way that overrides the resolved routing model. This applies to every fresh delegation, mirroring the resume-path rule ("delegate with `model` equal to the receipt's `model`") so both paths bind the spawn model to the routing receipt.
|
|
99
|
+
|
|
98
100
|
The `complexity_assessments[]` and `model_routing_receipts[]` invariants are enforced by `scripts/dev_tools/validate_orchestrator_state.py` per `.claude/rules/orchestrator-state.md`; both arrays remain additive (a checkpoint that predates model routing stays valid).
|
|
99
101
|
|
|
100
102
|
### Required-once-delegated invariant (`require_model_routing` mode)
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
".claude/skills/skill-canonical-location-audit/SKILL.md",
|
|
71
71
|
".claude/skills/translate-copilot-to-claude/SKILL.md",
|
|
72
72
|
".claude/skills/update-status/SKILL.md",
|
|
73
|
-
".claude/lib/model-routing/ModelRouting.psm1"
|
|
73
|
+
".claude/lib/model-routing/ModelRouting.psm1",
|
|
74
|
+
".claude/lib/orchestrator-state/OrchestratorState.psm1",
|
|
75
|
+
".claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1"
|
|
74
76
|
]
|
|
75
77
|
}
|
|
@@ -163,6 +163,6 @@
|
|
|
163
163
|
},
|
|
164
164
|
"model_budget": {
|
|
165
165
|
"description": "Session-level model budget. fable_policy is a three-way switch controlling whether the fable tier is disabled (removed and clamped to opus), available (used as-is), or preferred (applies the preferred_overlay).",
|
|
166
|
-
"fable_policy": "
|
|
166
|
+
"fable_policy": "preferred"
|
|
167
167
|
}
|
|
168
168
|
}
|