@danmoisan/drm-copilot-mcp 1.0.9 → 1.0.11

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.
@@ -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": "disabled"
166
+ "fable_policy": "preferred"
167
167
  }
168
168
  }