@danmoisan/drm-copilot-mcp 1.0.8 → 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/model-routing/ModelRouting.psm1 +209 -0
- 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 +10 -3
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +8 -4
- package/resources/claude-customizations/pack-manifests/core.json +4 -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,209 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Model-routing reference formulas for the orchestrator, ported from the Python references.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Provides the destination-runtime PowerShell ports of the two self-contained,
|
|
7
|
+
pure model-routing formulas that the `orchestrate` skill instructs the
|
|
8
|
+
orchestrator to run:
|
|
9
|
+
|
|
10
|
+
- Get-ComplexityFloor port of scripts/dev_tools/compute_complexity_floor.py
|
|
11
|
+
- Resolve-DelegationModel port of scripts/dev_tools/resolve_delegation_model.py
|
|
12
|
+
|
|
13
|
+
Both functions are pure and deterministic: they read no file at runtime and
|
|
14
|
+
encode only the fixed band ordering, the base complexity-to-model table, the
|
|
15
|
+
preferred overlay, and the disabled-mode clamp as module-scope constants.
|
|
16
|
+
Those literals are pinned to config/orchestration-routing.json (model_policy /
|
|
17
|
+
model_budget) by a static config-parity Pester test, and the Python modules
|
|
18
|
+
remain the validator's authoritative reference. This module is one half of a
|
|
19
|
+
two-language mirror; it never imports validator logic.
|
|
20
|
+
#>
|
|
21
|
+
|
|
22
|
+
Set-StrictMode -Version Latest
|
|
23
|
+
|
|
24
|
+
# The fixed complexity-band vocabulary, ordered from lowest to highest rigor.
|
|
25
|
+
# The array order defines "higher" and "lower" band comparisons used by the
|
|
26
|
+
# floor computation, mirroring BAND_ORDER in compute_complexity_floor.py.
|
|
27
|
+
$script:BAND_ORDER = @('C1', 'C2', 'C3', 'C4')
|
|
28
|
+
|
|
29
|
+
# The lowest band, returned when no floor signal is present (LOWEST_BAND).
|
|
30
|
+
$script:LOWEST_BAND = 'C1'
|
|
31
|
+
|
|
32
|
+
# Every present floor signal contributes this uniform candidate band, per the
|
|
33
|
+
# model_policy.complexity contract (each [floor] signal contributes C3).
|
|
34
|
+
$script:FLOOR_CANDIDATE_BAND = 'C3'
|
|
35
|
+
|
|
36
|
+
# Floors never exceed this ceiling; C4 is judgment-only and never floor-forced,
|
|
37
|
+
# so the computed floor is clamped to at most C3 (FLOOR_CEILING_BAND).
|
|
38
|
+
$script:FLOOR_CEILING_BAND = 'C3'
|
|
39
|
+
|
|
40
|
+
# The three session-level fable policies (model_budget.fable_policy).
|
|
41
|
+
$script:DISABLED_POLICY = 'disabled'
|
|
42
|
+
$script:PREFERRED_POLICY = 'preferred'
|
|
43
|
+
|
|
44
|
+
# The model tier removed from consideration under the disabled policy, the tier
|
|
45
|
+
# a disabled-mode fable cell clamps down to, and the recorded clamp reason.
|
|
46
|
+
$script:FABLE_MODEL = 'fable'
|
|
47
|
+
$script:DISABLED_CLAMP_MODEL = 'opus'
|
|
48
|
+
$script:DISABLED_CLAMP_REASON = 'fable_disabled'
|
|
49
|
+
|
|
50
|
+
# The base complexity-to-model table applied uniformly across delegated agents
|
|
51
|
+
# (BASE_COMPLEXITY_TO_MODEL). Pinned to model_policy.complexity_to_model.
|
|
52
|
+
$script:BASE_COMPLEXITY_TO_MODEL = @{
|
|
53
|
+
C1 = 'haiku'
|
|
54
|
+
C2 = 'sonnet'
|
|
55
|
+
C3 = 'opus'
|
|
56
|
+
C4 = 'fable'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# The agents whose C3 cell the preferred overlay redirects to fable. No other
|
|
60
|
+
# agent and no other band is affected (PREFERRED_OVERLAY_AGENTS).
|
|
61
|
+
$script:PREFERRED_OVERLAY_AGENTS = @(
|
|
62
|
+
'atomic-planner',
|
|
63
|
+
'prd-feature',
|
|
64
|
+
'feature-review',
|
|
65
|
+
'task-researcher'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# The single band and target model the preferred overlay applies.
|
|
69
|
+
$script:PREFERRED_OVERLAY_BAND = 'C3'
|
|
70
|
+
$script:PREFERRED_OVERLAY_MODEL = 'fable'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function Get-ComplexityFloor {
|
|
74
|
+
<#
|
|
75
|
+
.SYNOPSIS
|
|
76
|
+
Compute the deterministic complexity-band floor from present floor signals.
|
|
77
|
+
|
|
78
|
+
.DESCRIPTION
|
|
79
|
+
Faithful PowerShell port of compute_complexity_floor
|
|
80
|
+
(scripts/dev_tools/compute_complexity_floor.py). Returns the deterministic
|
|
81
|
+
lower-bound complexity band implied by the set of present floor signals:
|
|
82
|
+
each present floor signal contributes a candidate band of C3, the floor is
|
|
83
|
+
the maximum triggered candidate band, and the floor never exceeds C3
|
|
84
|
+
(C4 is never floor-forced). With no floor signal present the floor is the
|
|
85
|
+
lowest band C1. The function is pure: it reads no file and does not mutate
|
|
86
|
+
its input, and the result is independent of input ordering.
|
|
87
|
+
|
|
88
|
+
.PARAMETER SignalsPresent
|
|
89
|
+
The names of the present signals flagged [floor] in the
|
|
90
|
+
model_policy.complexity catalog. Every element is treated as a triggered
|
|
91
|
+
floor signal contributing the candidate band C3. An empty collection means
|
|
92
|
+
no floor signal is present.
|
|
93
|
+
|
|
94
|
+
.OUTPUTS
|
|
95
|
+
System.String. The floor band: C1 when no floor signal is present,
|
|
96
|
+
otherwise the maximum triggered candidate band clamped to at most C3.
|
|
97
|
+
C4 is never returned.
|
|
98
|
+
#>
|
|
99
|
+
[CmdletBinding()]
|
|
100
|
+
[OutputType([string])]
|
|
101
|
+
param(
|
|
102
|
+
[Parameter(Mandatory = $true)]
|
|
103
|
+
[AllowEmptyCollection()]
|
|
104
|
+
[string[]] $SignalsPresent
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# With no present floor signal there is no candidate band to raise the floor
|
|
108
|
+
# above the lowest band, so the floor is C1 (mirrors the empty-input guard).
|
|
109
|
+
if (-not $SignalsPresent -or $SignalsPresent.Count -eq 0) {
|
|
110
|
+
return $script:LOWEST_BAND
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Each present floor signal contributes the uniform candidate band; the floor
|
|
114
|
+
# is the maximum triggered candidate rank across all of them. Because every
|
|
115
|
+
# signal contributes the same candidate band, the max equals that rank.
|
|
116
|
+
$candidateRank = $script:BAND_ORDER.IndexOf($script:FLOOR_CANDIDATE_BAND)
|
|
117
|
+
$highestRank = $candidateRank
|
|
118
|
+
|
|
119
|
+
# Clamp with the ceiling rank so the floor can never exceed C3; this is what
|
|
120
|
+
# keeps C4 from ever being floor-forced regardless of how many signals exist.
|
|
121
|
+
$ceilingRank = $script:BAND_ORDER.IndexOf($script:FLOOR_CEILING_BAND)
|
|
122
|
+
$floorRank = [Math]::Min($highestRank, $ceilingRank)
|
|
123
|
+
return $script:BAND_ORDER[$floorRank]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Resolve-DelegationModel {
|
|
127
|
+
<#
|
|
128
|
+
.SYNOPSIS
|
|
129
|
+
Resolve the delegation model tier for an agent, band, and fable policy.
|
|
130
|
+
|
|
131
|
+
.DESCRIPTION
|
|
132
|
+
Faithful PowerShell port of resolve_delegation_model
|
|
133
|
+
(scripts/dev_tools/resolve_delegation_model.py). Applies the model_policy
|
|
134
|
+
selection formula to a single delegation: it computes the pre-clamp
|
|
135
|
+
table_model (the base complexity_to_model table plus any preferred overlay)
|
|
136
|
+
and the post-clamp model, recording the clamp provenance. The preferred
|
|
137
|
+
overlay redirects only the C3 cell to fable and only for the four overlay
|
|
138
|
+
agents; atomic-executor and pr-author C3 cells stay opus under every
|
|
139
|
+
policy. Under the disabled policy a fable table cell clamps to opus with
|
|
140
|
+
clamped_from = fable and clamp_reason = fable_disabled. The function is
|
|
141
|
+
pure: it reads no file and mutates no input.
|
|
142
|
+
|
|
143
|
+
.PARAMETER Agent
|
|
144
|
+
The target delegate agent name (for example atomic-planner). Only
|
|
145
|
+
participates in preferred-overlay eligibility.
|
|
146
|
+
|
|
147
|
+
.PARAMETER Band
|
|
148
|
+
The assessed complexity band, one of C1..C4. Used as the key into the
|
|
149
|
+
base complexity_to_model table. A band outside the table is the PowerShell
|
|
150
|
+
analog of the Python KeyError and causes a terminating error (throw).
|
|
151
|
+
|
|
152
|
+
.PARAMETER FablePolicy
|
|
153
|
+
The session fable policy, one of disabled, available, or preferred.
|
|
154
|
+
|
|
155
|
+
.OUTPUTS
|
|
156
|
+
System.Collections.Hashtable. A hashtable with keys table_model (the
|
|
157
|
+
pre-clamp table lookup, including any overlay), model (the post-clamp
|
|
158
|
+
result), clamped_from (fable when a clamp occurred, else $null), and
|
|
159
|
+
clamp_reason (fable_disabled when a clamp occurred, else $null).
|
|
160
|
+
#>
|
|
161
|
+
[CmdletBinding()]
|
|
162
|
+
[OutputType([hashtable])]
|
|
163
|
+
param(
|
|
164
|
+
[Parameter(Mandatory = $true)]
|
|
165
|
+
[string] $Agent,
|
|
166
|
+
[Parameter(Mandatory = $true)]
|
|
167
|
+
[string] $Band,
|
|
168
|
+
[Parameter(Mandatory = $true)]
|
|
169
|
+
[string] $FablePolicy
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# The preferred overlay redirects only the C3 cell to fable, and only for the
|
|
173
|
+
# overlay agents; every other case reads the base table unchanged. The three
|
|
174
|
+
# conditions (policy, agent membership, band) must all hold for the overlay.
|
|
175
|
+
if ($FablePolicy -eq $script:PREFERRED_POLICY -and
|
|
176
|
+
$script:PREFERRED_OVERLAY_AGENTS -contains $Agent -and
|
|
177
|
+
$Band -eq $script:PREFERRED_OVERLAY_BAND) {
|
|
178
|
+
$tableModel = $script:PREFERRED_OVERLAY_MODEL
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
# A band outside the base table is the PowerShell analog of the Python
|
|
182
|
+
# KeyError: fail fast rather than return a silently wrong value.
|
|
183
|
+
if (-not $script:BASE_COMPLEXITY_TO_MODEL.ContainsKey($Band)) {
|
|
184
|
+
throw "Unknown complexity band '$Band'; expected one of $($script:BAND_ORDER -join ', ')."
|
|
185
|
+
}
|
|
186
|
+
$tableModel = $script:BASE_COMPLEXITY_TO_MODEL[$Band]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Under the disabled policy, fable is removed from consideration: a fable
|
|
190
|
+
# table cell clamps down to opus and records the clamp provenance.
|
|
191
|
+
if ($FablePolicy -eq $script:DISABLED_POLICY -and $tableModel -eq $script:FABLE_MODEL) {
|
|
192
|
+
return @{
|
|
193
|
+
table_model = $tableModel
|
|
194
|
+
model = $script:DISABLED_CLAMP_MODEL
|
|
195
|
+
clamped_from = $script:FABLE_MODEL
|
|
196
|
+
clamp_reason = $script:DISABLED_CLAMP_REASON
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# No clamp applies: the resolved model is the table model verbatim.
|
|
201
|
+
return @{
|
|
202
|
+
table_model = $tableModel
|
|
203
|
+
model = $tableModel
|
|
204
|
+
clamped_from = $null
|
|
205
|
+
clamp_reason = $null
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Export-ModuleMember -Function Get-ComplexityFloor, Resolve-DelegationModel
|
|
@@ -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
|
|
@@ -115,9 +115,16 @@ The child's own `orchestrator` reads this line and applies the two-axis model-se
|
|
|
115
115
|
documented in `.claude/skills/orchestrate/SKILL.md` (`## Model Selection`): it assesses a
|
|
116
116
|
judgment-based `complexity_band`, records `complexity_assessments[]` and `model_routing_receipts[]`,
|
|
117
117
|
and resolves each delegation's model tier under the given `fable_policy`. The two canonical, tested
|
|
118
|
-
reference implementations are
|
|
119
|
-
(`
|
|
120
|
-
(`
|
|
118
|
+
reference implementations are `.claude/lib/model-routing/ModelRouting.psm1`
|
|
119
|
+
(`Get-ComplexityFloor`) and `.claude/lib/model-routing/ModelRouting.psm1`
|
|
120
|
+
(`Resolve-DelegationModel`). Default `fable_policy` is `disabled` when the marker is absent.
|
|
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`.
|
|
121
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`
|
|
@@ -29,9 +29,9 @@ On every invocation, the main session must:
|
|
|
29
29
|
Because model selection is required once delegation occurs (see `## Model Selection`), a resuming orchestrator must repair a missing model choice deterministically before delegating at a delegating `next_step`. When the resumed `next_step` is a delegating step:
|
|
30
30
|
|
|
31
31
|
a. **Preflight the checkpoint.** Run the orchestrator-state validator with `--require-model-routing` (via `mcp__drm-copilot__validate_orchestration_artifacts` or the local CLI) against `artifacts/orchestration/orchestrator-state.json` before the first delegation. Record the result in a `model_routing_preflight` block `{ status ("pass"|"fail"), checked_at (ISO-8601 UTC), validator_command, output_summary }`.
|
|
32
|
-
b. **Recompute the floor.** For the upcoming phase, recompute the complexity floor with `
|
|
32
|
+
b. **Recompute the floor.** For the upcoming phase, recompute the complexity floor with `Get-ComplexityFloor -SignalsPresent <names>` (`.claude/lib/model-routing/ModelRouting.psm1`); do not reimplement the formula.
|
|
33
33
|
c. **Record the assessment.** Write a `complexity_assessments[]` entry `{ phase, band, floor, signals_present[], rationale, assessed_at }` with `floor` equal to the recomputed value and `band >= floor`.
|
|
34
|
-
d. **Resolve and record the receipt.** Resolve the model with `
|
|
34
|
+
d. **Resolve and record the receipt.** Resolve the model with `Resolve-DelegationModel -Agent <agent> -Band <complexity_band> -FablePolicy <fable_policy>` (`.claude/lib/model-routing/ModelRouting.psm1`) and write a `model_routing_receipts[]` entry `{ agent, phase, complexity_band, fable_policy, table_model, clamped_from | null, model }`.
|
|
35
35
|
e. **Persist and delegate.** Persist the checkpoint, then delegate with `model` equal to the receipt's `model`.
|
|
36
36
|
|
|
37
37
|
The orchestrator MUST NOT delegate at a delegating `next_step` while `model_routing_preflight` status is `fail`; it repairs the missing choice (steps b-e) and re-preflights until the status is `pass`.
|
|
@@ -83,8 +83,10 @@ Model selection is a second axis, strictly separate from `route`. `route` (`smal
|
|
|
83
83
|
|
|
84
84
|
The two canonical, tested reference implementations express the formulas the orchestrator applies by judgment:
|
|
85
85
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
86
|
+
- `.claude/lib/model-routing/ModelRouting.psm1` (`Get-ComplexityFloor`) — the deterministic complexity-floor formula. Each present `[floor]` signal contributes a candidate band of `C3`; the floor is the maximum triggered candidate band; the floor never exceeds `C3`. C4 is never floor-forced; it is reached only by judgment.
|
|
87
|
+
- `.claude/lib/model-routing/ModelRouting.psm1` (`Resolve-DelegationModel`) — the delegation-model selection formula (base `complexity_to_model` table, the `preferred` overlay, and the `disabled` clamp).
|
|
88
|
+
|
|
89
|
+
The runnable reference the destination runtime applies is the `.claude`-resident PowerShell module above; the repository validator remains the Python authority (`scripts/dev_tools/compute_complexity_floor.py` and `scripts/dev_tools/resolve_delegation_model.py`), pinned to the same `config/orchestration-routing.json` truth table by a static config-parity test.
|
|
88
90
|
|
|
89
91
|
End-to-end procedure:
|
|
90
92
|
|
|
@@ -93,6 +95,8 @@ End-to-end procedure:
|
|
|
93
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.
|
|
94
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.
|
|
95
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
|
+
|
|
96
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).
|
|
97
101
|
|
|
98
102
|
### Required-once-delegated invariant (`require_model_routing` mode)
|
|
@@ -69,6 +69,9 @@
|
|
|
69
69
|
".claude/skills/review-staged/SKILL.md",
|
|
70
70
|
".claude/skills/skill-canonical-location-audit/SKILL.md",
|
|
71
71
|
".claude/skills/translate-copilot-to-claude/SKILL.md",
|
|
72
|
-
".claude/skills/update-status/SKILL.md"
|
|
72
|
+
".claude/skills/update-status/SKILL.md",
|
|
73
|
+
".claude/lib/model-routing/ModelRouting.psm1",
|
|
74
|
+
".claude/lib/orchestrator-state/OrchestratorState.psm1",
|
|
75
|
+
".claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1"
|
|
73
76
|
]
|
|
74
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
|
}
|