You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
orchestrate: validate_run_state rejects resolvedRouting.fallback: null — non-premium slices should omit it, but the contract is ambiguous #333
During a real /orchestrate run (orchestrate plugin v1.6.0), after freezing per-slice routing into run-state.json, writing the resolved-routing block for a non-premium slice (no route:* label, no model fallback) as:
caused validate_run_stateand all three render tools (render_dashboard, render_graph, render_report) to fail with:
run-state.json does not match the expected shape: slices.<N>.resolvedRouting.fallback: Expected object, received null
Why this is an easy trap
The SKILL spine describes freezing { model, variant, optional fallback }. "Optional" naturally reads as nullable — so writing "fallback": null for a slice with no fallback looks correct. But the schema is .optional(), which accepts an absent key yet rejects a literal null. The key must be omitted, not set to null, when there is no fallback.
Worse, the error only surfaces at the final render step — after the entire wave loop has run — rather than at the checkpoint write that introduced the bad shape. The whole run can complete and then fail to render.
Relationship to existing issues (read before deduping)
So this is a genuinely new surface, not a duplicate of #300/#315.
Suggested fix (options)
Relax the schema so resolvedRouting.fallback accepts null and treats it as absent (.nullish() / z.object(...).nullable().optional()), so both null and omitted validate. (Touches orchestrate-mcp/src + requires npm run build and a rebuilt committed dist/.)
New occurrence — orchestrate plugin v1.6.0, run prd1-20260621-011032
Hit again on a successful autonomous /orchestrate 1 run — confirms this trap is still live in v1.6.0.
What happened
After freezing each slice's resolvedRouting per the SKILL spine (section 3 step 2) for non-premium (standard-tier) slices with no route:* label, I wrote "fallback": null — the natural reading of "no fallback" (and resolve_routing returned fallbacks: []).
Failure timing (supports the "fail fast" half of the fix)
validate_run_statepassed at first-write time — but that checkpoint was written beforeresolvedRouting existed on the slices, so the bad shape was never in scope at that point.
At run end, all three render tools failed:
run-state.json does not match the expected shape: slices.<N>.resolvedRouting.fallback: Expected object, received null
Removing the fallback key entirely (omitting it) made validate_run_state pass and all renders succeed.
This is concrete evidence for suggested-fix option (b)'s second clause: validate_run_state should catch the null case at the first checkpoint write that contains resolvedRouting, not only at render time — otherwise a fully-successful run hits a hard error at the very last step and the operator must hand-edit run-state.json to strip fallback: null from every slice and re-validate before artifacts render.
Both existing suggested fixes still apply: (1) relax the schema to treat null === absent (.nullish()), and (2) document that resolvedRouting.fallback must be OMITTED (never null) for non-premium slices, showing the no-fallback shape in the reference.
Confirming recurrence — run prd23-20260621-184106 (PRD #23 in rodrigorjsf/ast-grep-for-agents)
Hit this again on a fresh /orchestrate run, orchestrate v1.6.0. Following references/slice-pipeline.md step 2 ("freeze it into resolvedRouting"), the natural checkpoint for a slice with no route:* label (the common case — resolve_routing returns an empty fallbacks array) is "fallback": null. That validates fine through validate_run_state but then all three render tools fail at end-of-run:
RUN_STATE_INVALID: slices.<id>.resolvedRouting.fallback: Expected object, received null
Note the two validators disagree: validate_run_state returns valid for the same file the render tools reject — which is exactly why the bad shape survives the whole wave loop and only blows up at render.
Workaround that worked here: OMIT the fallback key entirely from every slice (do not set it to null), then re-run the render tools.
Reinforces the suggested fix in the issue body. Concretely the most robust resolution is all of: (a) relax the Zod schema to treat fallback: null === absent, (b) make validate_run_state reject fallback: null too so the inconsistency surfaces at the checkpoint write instead of at final render, and (c) document in references/slice-pipeline.md step 2 that when fallbacks is empty the fallback key must be OMITTED, not set to null.
Confirming this is the exact run that surfaced it: `prd335-20260621-205440` (orchestrate plugin v1.6.0).
Two additional details from the same run worth folding into the fix:
Schema divergence between validate and render. The checkpoint was written with `"fallback": null` AND, for standard-tier slices, no `investigator` key. `validate_run_state` returned `valid` and the wave loop ran to completion — but `render_dashboard` / `render_graph` / `render_report` then ALL failed with `RUN_STATE_INVALID`:
```
slices.X.resolvedRouting.investigator: Required; slices.X.resolvedRouting.fallback: Expected object, received null
```
So beyond the null-vs-absent `fallback` trap, `validate_run_state` and the render tools enforce different schemas for `resolvedRouting`: a checkpoint that passes the fast-fail guard breaks at the final render step. The render path additionally requires `investigator` (which validate does not).
Manual recovery. The fix was to edit run-state to add `"investigator": null` to every standard slice AND remove every `"fallback": null` line (fallback must be absent, not null).
Suggested addition to the fix: make `validate_run_state` and the render tools share ONE canonical schema (ideally render calls the same validator), so a state that validates always renders. And clarify run-state.md that `investigator` is a required key (null for non-investigating tiers) while `fallback` must be omitted (never null).
Autonomous pre-grill triage — AI-generated insights to seed a future grilling session. Not a decision.
Evidence
The reported trap is real and still live in the schema. resolvedRoutingSchema.fallback is labelFallbackSchema.optional() (plugins/orchestrate/orchestrate-mcp/src/tools/render.ts:33). Zod .optional() accepts an absent key but rejects a literal null — so "fallback": null fails exactly as the issue states.
One of the issue's three claims is already obsolete. The June-22 comment's "two validators disagree / render additionally requires investigator" is no longer true: validate-run-state.ts:4 does import { runStateSchema } from "./render.js" and validateRunState parses against that exact exported schema (validate-run-state.ts:128). The file's own header comment (lines 11–15) explicitly says it "reuses the SAME exported runStateSchema the render tools validate against." Both paths now enforce one schema, including investigator: roleConfigSchemaV2.nullable() (render.ts:29, a required-but-nullable key). This consolidation shipped in PR orchestrate run prd310-20260611-162818 — PRD #310 model routing v2 (8/8 slices) #329 (commit 78ac377, model-routing v2), which is the v1.6.0 build the comments were filed against — so the divergence the operator observed was a real symptom of a now-merged-but-not-yet-cache-refreshed state, not a standing design gap.
The remaining true defect is timing, not schema divergence.validate_run_state runs "right after the first write and on resume" (validate-run-state.ts:14–15). But resolvedRouting is frozen per-slice during the wave loop, after that first write — so the bad fallback: null is never in scope when validate runs, and first surfaces at end-of-run render. The June-21 comment confirms exactly this: "validate_run_state passed at first-write time — but that checkpoint was written before resolvedRouting existed on the slices."
Doc gap is real and asymmetric.references/run-state.md gives subState an explicit "omit the key entirely; an explicit null is rejected" warning with the Zod rationale (lines 124–127), but fallback only gets "present when a label override carried a fallback, absent otherwise" (lines 148–149) — no "never null" warning. The JSON example (lines 72–79) shows a slice withfallback set; there is no example of the no-fallback shape an author would copy for the common case.
references/slice-pipeline.md:49–50 says an unrouted slice should "freeze no fallback" but does not say how (omit vs null). This is the line the operator was following when they wrote null.
Ambiguities
The example at run-state.md:72–79 shows investigator: null (a standard/unrouted slice) but alsofallback: {...} set — which contradicts the prose at slice-pipeline.md:50 that unrouted slices freeze no fallback. So the canonical example itself models an arguably-inconsistent combination, muddying what "correct" looks like.
The issue conflates three distinct fixes (relax schema, add a fast-fail at first resolvedRouting write, doc the omit-shape) without ranking them. Option (a) "treat null === absent via .nullish()" and option (b) "make validate rejectnull too" are in direct tension — one accepts null, the other hardens against it. The grill must pick a direction, not "do all of it."
"Non-premium slices should omit it" — but is fallback ever meaningfully present for a standard slice? If fallback only exists for premium/route:* slices, the whole field could move under the routed branch and the null case becomes structurally impossible.
Honest doubts — impact & real value delivery
Severity is low and the workaround is trivial. Every reported occurrence ended in a successful run; recovery was "omit the fallback key and re-render." No subagent work is lost, no merge is corrupted — only the final render artifacts are blocked, and only until a one-line hand-edit. This is a polish/ergonomics bug, not a correctness bug.
It is self-inflicted by the orchestrator author, not a tool bug. The MCP schema is doing exactly what an optional field should. The recurrences all stem from the human/agent-authored checkpoint writing null against documented guidance. A pure doc fix (mirroring the orchestrate: run-state.md subState wording — say 'omit the key' (an explicit null is rejected) #300subState treatment) may close 100% of real-world occurrences at near-zero cost — making the schema-relax + new-validator work arguably gold-plating.
Risk of relaxing to .nullish(): it permanently legitimizes two shapes for the same meaning, which is precisely the kind of "null === absent" ambiguity the rest of run-state.md works hard to forbid (see the subState wording). It trades an author-time error for a latent schema looseness.
Alternative approaches (>=3 distinct, numbered)
Doc-only, mirror orchestrate: run-state.md subState wording — say 'omit the key' (an explicit null is rejected) #300. Add to run-state.md a "fallback must be OMITTED, never null" warning identical in spirit to the subState block, add a no-fallback JSON example, and fix slice-pipeline.md:50 to say "omit the key." Zero code, zero build, matches the precedent that already worked for subState. Lowest cost, closes the observed occurrences.
Tighten the validator's timing, not its schema. Keep fallback strictly .optional(). Instead, call validate_run_state (or add a dedicated guard) at the first checkpoint write that contains resolvedRouting, so null fails in seconds at that write rather than at end-of-run render. This delivers the "fail fast" value the operator explicitly asked for without weakening the schema — and is the only option that addresses the actual root cause (timing), since schema divergence is already gone.
Restructure so the null case is unrepresentable. Move fallback/fallbackTaken into a discriminated sub-object that only exists on premium/routed slices (e.g. resolvedRouting.premiumLane?: { fallback, fallbackTaken }). A standard slice has no place to put fallback, so the trap cannot be written. Larger refactor, but eliminates the class of bug rather than patching one field. Pairs naturally with the ambiguity that fallback may be meaningless for standard slices.
Relax to .nullish() + doc (the issue's "do both"). Accept null and absent as equivalent and document the preferred shape. Most permissive; costs a dist/ rebuild; risks normalizing dual shapes (see doubts).
Gotchas
Any src/ change requires npm run build and committing the rebuilt dist/ — vitest tests src/, so green tests do NOT prove the shipped bundle is fixed (project memory orchestrate-mcp dist/ is committed).
Because validateRunState and render now share runStateSchema, any schema edit changes both call sites at once — good for consistency, but it means a .nullish() relaxation also silently makes render accept null, which may not be intended.
The issue's comments were filed against a v1.6.0 cache that pre-dates (or coincides with) the orchestrate run prd310-20260611-162818 — PRD #310 model routing v2 (8/8 slices) #329 shared-schema merge; a grill must re-verify against current development before accepting the "two validators disagree" framing — that premise is already stale in the repo I read.
fallbackTaken: z.boolean().default(false) (render.ts:34) means the field can be omitted entirely and defaults — worth confirming the freeze guidance doesn't also redundantly write defaults.
Points to clarify
Is fallback ever legitimately present on a non-premium / non-route:* slice? If never, option 3 (make it unrepresentable) is the principled fix.
Does the maintainer want null accepted (.nullish()) or rejected earlier (option 2)? These are mutually exclusive directions and the issue asks for both.
Should the canonical example at run-state.md:72–79 be split into a routed example (with fallback) and an unrouted example (no fallback, no investigator) to remove the current mixed-signal?
Cost-of-fix vs cost-of-bug lens: bug cost = one hand-edit at run end, rarely. Fix cost of options 2–4 = code + build + committed dist + review. The economics favor option 1 unless the fail-fast value is independently wanted.
Author-ergonomics lens: the deepest fix is making the guidance copyable — authors write null because the only example shows a populated field. A correct no-fallback example may outperform a schema change at preventing recurrence.
Suggested grilling agenda
Kill the stale premise first: confirm on current development that validate_run_state and render share runStateSchema (they do — validate-run-state.ts:4). Drop the "two validators disagree" framing; reframe the issue as timing + docs, not schema divergence.
Force the direction choice: accept null (.nullish) vs reject-earlier (option 2) vs unrepresentable (option 3). They are mutually exclusive; "do both" is incoherent.
Decide scope: patch fallback only, or audit all optional null-traps in the run-state schema in one pass (consistency lens).
Resolve the example contradiction (run-state.md:72–79 shows unrouted-with-fallback vs slice-pipeline.md:50 unrouted-has-no-fallback) — whichever fix wins must ship a coherent canonical example.
Recurred in orchestrate run prd352-20260803-015333 (PRD #352, twelve slices). I filed #382 before checking the backlog — that was a de-duplication failure on my part; closing it as a duplicate of this issue. Its evidence is worth keeping here.
What happened. The orchestrator wrote resolvedRouting.fallback: null into all twelve slices, following the spine's own wording — "the returned {model, variant, optional fallback}" — where null is the natural encoding of "there is no fallback", and reads as the companion to the sibling fallbackTaken: false.
How late it surfaced, and why that is the real cost. Nothing rejected it until the very end, when all three render tools failed at once with twelve identical errors:
slices.353.resolvedRouting.fallback: Expected object, received null
... (all 12)
Twelve slices' worth of work — every implementation, review and merge — had already landed on an invalid checkpoint. It only stayed harmless because nothing else parses run-state.json through the schema: finalize_slice mutates raw JSON surgically rather than round-tripping.
The structural reason it went unnoticed.validate_run_state's own description recommends calling it "right after writing the first run-state checkpoint … so a mis-shaped checkpoint fails in seconds rather than after expensive subagent work". That advice is exactly right — and the spine only wires it into the resume path. A fresh single-session run therefore never invokes it until something else happens to parse the file. Adding that call after the first slice's resolvedRouting is written would have turned a twelve-slice-late total failure into a two-second one.
So this issue's ambiguity has a second half worth capturing: it is not only that the contract is unclear about the empty case, it is that nothing checks the contract when it is first written.
Workaround used: deleted the fallback key wherever it was null, re-ran validate_run_state (valid), re-rendered all three artifacts successfully.
A second, distinct defect in the same schema, found by a cross-slice review of PR #381 (PRD #352). Adding it here rather than opening a separate issue, since it is the same file and the same object — but note it is not the same failure: this issue is a rejected value, the following is a missing key.
resolvedRoutingSchema does not enumerate slice-executor.
src/tools/routing.ts:862-864 — resolve_routing emits a slice-executor entry.
skills/orchestrate/references/run-state.md:159-161 — documents it as the first key of resolvedRouting.
src/tools/validate-run-state.ts:4,128 imports that same schema as canonical.
Why it matters. Zod strips unknown keys rather than rejecting, so a checkpoint whose resolvedRouting is missing the executor entry validates as valid. That is precisely the field the resume path re-spawns the executor from (SKILL.md:305-307), so the one gate that could catch a malformed checkpoint before a resume is blind to exactly the key delegation depends on. The dashboard also never renders the executor's routing.
Why it is not P0. Nothing re-serializes parsed run-state: validate-run-state.ts and render.ts only safeParse, and finalize-slice.ts:233-336 edits the raw JSON object — its own comment at :234 says it deliberately avoids parsing through runStateSchema. So no field is dropped from disk today; the exposure is that validation cannot detect the omission.
Suggested scope for this issue: if the fix here is going to touch resolvedRoutingSchema anyway to accept fallback: null, add the slice-executor key in the same pass. Both are the same class — the schema and its writers disagree about what a well-formed resolvedRouting looks like.
Activity
rodrigorjsf commentedon Jun 21, 2026
New occurrence — orchestrate plugin v1.6.0, run
prd1-20260621-011032Hit again on a successful autonomous
/orchestrate 1run — confirms this trap is still live in v1.6.0.What happened
After freezing each slice's
resolvedRoutingper the SKILL spine (section 3 step 2) for non-premium (standard-tier) slices with noroute:*label, I wrote"fallback": null— the natural reading of "no fallback" (andresolve_routingreturnedfallbacks: []).Failure timing (supports the "fail fast" half of the fix)
validate_run_statepassed at first-write time — but that checkpoint was written beforeresolvedRoutingexisted on the slices, so the bad shape was never in scope at that point.At run end, all three render tools failed:
Removing the
fallbackkey entirely (omitting it) madevalidate_run_statepass and all renders succeed.This is concrete evidence for suggested-fix option (b)'s second clause:
validate_run_stateshould catch thenullcase at the first checkpoint write that containsresolvedRouting, not only at render time — otherwise a fully-successful run hits a hard error at the very last step and the operator must hand-editrun-state.jsonto stripfallback: nullfrom every slice and re-validate before artifacts render.Both existing suggested fixes still apply: (1) relax the schema to treat
null === absent(.nullish()), and (2) document thatresolvedRouting.fallbackmust be OMITTED (nevernull) for non-premium slices, showing the no-fallback shape in the reference.rodrigorjsf commentedon Jun 21, 2026
Confirming recurrence — run
prd23-20260621-184106(PRD #23 inrodrigorjsf/ast-grep-for-agents)Hit this again on a fresh
/orchestraterun, orchestrate v1.6.0. Followingreferences/slice-pipeline.mdstep 2 ("freeze it intoresolvedRouting"), the natural checkpoint for a slice with noroute:*label (the common case —resolve_routingreturns an emptyfallbacksarray) is"fallback": null. That validates fine throughvalidate_run_statebut then all three render tools fail at end-of-run:Note the two validators disagree:
validate_run_statereturnsvalidfor the same file the render tools reject — which is exactly why the bad shape survives the whole wave loop and only blows up at render.Workaround that worked here: OMIT the
fallbackkey entirely from every slice (do not set it tonull), then re-run the render tools.Reinforces the suggested fix in the issue body. Concretely the most robust resolution is all of: (a) relax the Zod schema to treat
fallback: null=== absent, (b) makevalidate_run_staterejectfallback: nulltoo so the inconsistency surfaces at the checkpoint write instead of at final render, and (c) document inreferences/slice-pipeline.mdstep 2 that whenfallbacksis empty thefallbackkey must be OMITTED, not set tonull.rodrigorjsf commentedon Jun 22, 2026
Confirming this is the exact run that surfaced it: `prd335-20260621-205440` (orchestrate plugin v1.6.0).
Two additional details from the same run worth folding into the fix:
Schema divergence between validate and render. The checkpoint was written with `"fallback": null` AND, for standard-tier slices, no `investigator` key. `validate_run_state` returned `valid` and the wave loop ran to completion — but `render_dashboard` / `render_graph` / `render_report` then ALL failed with `RUN_STATE_INVALID`:
```
slices.X.resolvedRouting.investigator: Required; slices.X.resolvedRouting.fallback: Expected object, received null
```
So beyond the null-vs-absent `fallback` trap, `validate_run_state` and the render tools enforce different schemas for `resolvedRouting`: a checkpoint that passes the fast-fail guard breaks at the final render step. The render path additionally requires `investigator` (which validate does not).
Manual recovery. The fix was to edit run-state to add `"investigator": null` to every standard slice AND remove every `"fallback": null` line (fallback must be absent, not null).
Suggested addition to the fix: make `validate_run_state` and the render tools share ONE canonical schema (ideally render calls the same validator), so a state that validates always renders. And clarify run-state.md that `investigator` is a required key (null for non-investigating tiers) while `fallback` must be omitted (never null).
rodrigorjsf commentedon Jun 28, 2026
Autonomous pre-grill triage — AI-generated insights to seed a future grilling session. Not a decision.
Evidence
resolvedRoutingSchema.fallbackislabelFallbackSchema.optional()(plugins/orchestrate/orchestrate-mcp/src/tools/render.ts:33). Zod.optional()accepts an absent key but rejects a literalnull— so"fallback": nullfails exactly as the issue states.investigator" is no longer true:validate-run-state.ts:4doesimport { runStateSchema } from "./render.js"andvalidateRunStateparses against that exact exported schema (validate-run-state.ts:128). The file's own header comment (lines 11–15) explicitly says it "reuses the SAME exportedrunStateSchemathe render tools validate against." Both paths now enforce one schema, includinginvestigator: roleConfigSchemaV2.nullable()(render.ts:29, a required-but-nullable key). This consolidation shipped in PR orchestrate run prd310-20260611-162818 — PRD #310 model routing v2 (8/8 slices) #329 (commit78ac377, model-routing v2), which is the v1.6.0 build the comments were filed against — so the divergence the operator observed was a real symptom of a now-merged-but-not-yet-cache-refreshed state, not a standing design gap.validate_run_stateruns "right after the first write and on resume" (validate-run-state.ts:14–15). ButresolvedRoutingis frozen per-slice during the wave loop, after that first write — so the badfallback: nullis never in scope when validate runs, and first surfaces at end-of-run render. The June-21 comment confirms exactly this: "validate_run_state passed at first-write time — but that checkpoint was written before resolvedRouting existed on the slices."references/run-state.mdgivessubStatean explicit "omit the key entirely; an explicitnullis rejected" warning with the Zod rationale (lines 124–127), butfallbackonly gets "present when a label override carried a fallback, absent otherwise" (lines 148–149) — no "nevernull" warning. The JSON example (lines 72–79) shows a slice withfallbackset; there is no example of the no-fallback shape an author would copy for the common case.references/slice-pipeline.md:49–50says an unrouted slice should "freeze nofallback" but does not say how (omit vs null). This is the line the operator was following when they wrotenull.Ambiguities
investigator: null(a standard/unrouted slice) but alsofallback: {...}set — which contradicts the prose at slice-pipeline.md:50 that unrouted slices freeze no fallback. So the canonical example itself models an arguably-inconsistent combination, muddying what "correct" looks like.null=== absent via.nullish()" and option (b) "make validate rejectnulltoo" are in direct tension — one accepts null, the other hardens against it. The grill must pick a direction, not "do all of it."fallbackever meaningfully present for a standard slice? If fallback only exists for premium/route:*slices, the whole field could move under the routed branch and the null case becomes structurally impossible.Honest doubts — impact & real value delivery
fallbackkey and re-render." No subagent work is lost, no merge is corrupted — only the final render artifacts are blocked, and only until a one-line hand-edit. This is a polish/ergonomics bug, not a correctness bug.nullagainst documented guidance. A pure doc fix (mirroring the orchestrate: run-state.md subState wording — say 'omit the key' (an explicit null is rejected) #300subStatetreatment) may close 100% of real-world occurrences at near-zero cost — making the schema-relax + new-validator work arguably gold-plating.subState: null) was fixed doc-only and apparently has not recurred. If the same minimal fix worked there, the burden is on this issue to justify whyfallbackneeds schema surgery (build + committeddist/churn) whensubStatedid not..nullish(): it permanently legitimizes two shapes for the same meaning, which is precisely the kind of "null === absent" ambiguity the rest of run-state.md works hard to forbid (see the subState wording). It trades an author-time error for a latent schema looseness.Alternative approaches (>=3 distinct, numbered)
null" warning identical in spirit to the subState block, add a no-fallback JSON example, and fix slice-pipeline.md:50 to say "omit the key." Zero code, zero build, matches the precedent that already worked for subState. Lowest cost, closes the observed occurrences.fallbackstrictly.optional(). Instead, callvalidate_run_state(or add a dedicated guard) at the first checkpoint write that containsresolvedRouting, sonullfails in seconds at that write rather than at end-of-run render. This delivers the "fail fast" value the operator explicitly asked for without weakening the schema — and is the only option that addresses the actual root cause (timing), since schema divergence is already gone.fallback/fallbackTakeninto a discriminated sub-object that only exists on premium/routed slices (e.g.resolvedRouting.premiumLane?: { fallback, fallbackTaken }). A standard slice has no place to putfallback, so the trap cannot be written. Larger refactor, but eliminates the class of bug rather than patching one field. Pairs naturally with the ambiguity that fallback may be meaningless for standard slices..nullish()+ doc (the issue's "do both"). Acceptnulland absent as equivalent and document the preferred shape. Most permissive; costs adist/rebuild; risks normalizing dual shapes (see doubts).Gotchas
src/change requiresnpm run buildand committing the rebuiltdist/— vitest testssrc/, so green tests do NOT prove the shipped bundle is fixed (project memoryorchestrate-mcp dist/ is committed).validateRunStateand render now sharerunStateSchema, any schema edit changes both call sites at once — good for consistency, but it means a.nullish()relaxation also silently makes render acceptnull, which may not be intended.developmentbefore accepting the "two validators disagree" framing — that premise is already stale in the repo I read.fallbackTaken: z.boolean().default(false)(render.ts:34) means the field can be omitted entirely and defaults — worth confirming the freeze guidance doesn't also redundantly write defaults.Points to clarify
fallbackever legitimately present on a non-premium / non-route:*slice? If never, option 3 (make it unrepresentable) is the principled fix.nullaccepted (.nullish()) or rejected earlier (option 2)? These are mutually exclusive directions and the issue asks for both.Other analytical lenses
subStateandfallback— two structurally identical optional-rejects-null fields — with different documentation rigor. A one-pass audit of every.optional()field insliceSchema/resolvedRoutingSchemafor the same null-trap (subState ✓ documented, fallback ✗, investigator is nullable so safe) would prevent the next orchestrate: run-state.md subState wording — say 'omit the key' (an explicit null is rejected) #300/orchestrate: validate_run_state rejects resolvedRouting.fallback: null — non-premium slices should omit it, but the contract is ambiguous #333 instead of patching the third instance reactively.nullbecause the only example shows a populated field. A correct no-fallback example may outperform a schema change at preventing recurrence.Suggested grilling agenda
developmentthatvalidate_run_stateand render sharerunStateSchema(they do — validate-run-state.ts:4). Drop the "two validators disagree" framing; reframe the issue as timing + docs, not schema divergence.null(.nullish) vs reject-earlier (option 2) vs unrepresentable (option 3). They are mutually exclusive; "do both" is incoherent.dist/churn.fallbackonly, or audit all optional null-traps in the run-state schema in one pass (consistency lens).rodrigorjsf commentedon Aug 3, 2026
Recurred in orchestrate run
prd352-20260803-015333(PRD #352, twelve slices). I filed #382 before checking the backlog — that was a de-duplication failure on my part; closing it as a duplicate of this issue. Its evidence is worth keeping here.What happened. The orchestrator wrote
resolvedRouting.fallback: nullinto all twelve slices, following the spine's own wording — "the returned{model, variant, optional fallback}" — wherenullis the natural encoding of "there is no fallback", and reads as the companion to the siblingfallbackTaken: false.How late it surfaced, and why that is the real cost. Nothing rejected it until the very end, when all three render tools failed at once with twelve identical errors:
Twelve slices' worth of work — every implementation, review and merge — had already landed on an invalid checkpoint. It only stayed harmless because nothing else parses
run-state.jsonthrough the schema:finalize_slicemutates raw JSON surgically rather than round-tripping.The structural reason it went unnoticed.
validate_run_state's own description recommends calling it "right after writing the first run-state checkpoint … so a mis-shaped checkpoint fails in seconds rather than after expensive subagent work". That advice is exactly right — and the spine only wires it into the resume path. A fresh single-session run therefore never invokes it until something else happens to parse the file. Adding that call after the first slice'sresolvedRoutingis written would have turned a twelve-slice-late total failure into a two-second one.So this issue's ambiguity has a second half worth capturing: it is not only that the contract is unclear about the empty case, it is that nothing checks the contract when it is first written.
Workaround used: deleted the
fallbackkey wherever it wasnull, re-ranvalidate_run_state(valid), re-rendered all three artifacts successfully.rodrigorjsf commentedon Aug 3, 2026
A second, distinct defect in the same schema, found by a cross-slice review of PR #381 (PRD #352). Adding it here rather than opening a separate issue, since it is the same file and the same object — but note it is not the same failure: this issue is a rejected value, the following is a missing key.
resolvedRoutingSchemadoes not enumerateslice-executor.src/tools/routing.ts:862-864—resolve_routingemits aslice-executorentry.skills/orchestrate/references/run-state.md:159-161— documents it as the first key ofresolvedRouting.src/tools/render.ts:26-33—resolvedRoutingSchemadeclares onlyinvestigator,implementer,reviewer,conflict-resolver,fallback,fallbackTaken.src/tools/validate-run-state.ts:4,128imports that same schema as canonical.Why it matters. Zod strips unknown keys rather than rejecting, so a checkpoint whose
resolvedRoutingis missing the executor entry validates asvalid. That is precisely the field the resume path re-spawns the executor from (SKILL.md:305-307), so the one gate that could catch a malformed checkpoint before a resume is blind to exactly the key delegation depends on. The dashboard also never renders the executor's routing.Why it is not P0. Nothing re-serializes parsed run-state:
validate-run-state.tsandrender.tsonlysafeParse, andfinalize-slice.ts:233-336edits the raw JSON object — its own comment at:234says it deliberately avoids parsing throughrunStateSchema. So no field is dropped from disk today; the exposure is that validation cannot detect the omission.Suggested scope for this issue: if the fix here is going to touch
resolvedRoutingSchemaanyway to acceptfallback: null, add theslice-executorkey in the same pass. Both are the same class — the schema and its writers disagree about what a well-formedresolvedRoutinglooks like.