routing: replace the spend ladder with a rigor ladder, deleting the profile matrix, the tier bump, and escalate_effort_variant #63
Labels
No labels
already-shipped
bug
documentation
duplicate
enhancement
good first issue
help wanted
in progress
invalid
needs-decision
proposal
question
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: crenshawdev/cadence#63
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What you want Cadence to do
Delete three mechanisms and replace them with one.
Out: the
fast/balanced/qualityprofile matrix, the difficulty-driven tier bump, andescalate_effort_variantwith its runtime-read shim inagents/cad-plan-checker-high.md. Three machines currently decide how much a dispatch gets, and all three express the same thing in the currency of model price.In: one dial.
effort, expressing how much rigor a piece of work deserves.Start with the ladder, because everything else falls out of it
fast/balanced/qualityis a spend ladder. It asks the user how much they are willing to pay.Replace it with a rigor ladder:
draftfor spikes and throwaway work where turnaround matters,standard, andhardenedfor anything consequential. It asks how careful this work needs to be.Those are different questions, and only the second one is worth asking per phase. That reframe is the proposal. Read the rest as consequences of it, because taken on their own they each look like something they are not.
The model axis collapses
Once rigor is the live dial, the model axis has nothing left to express. This is not a separate decision to argue; it is what falls out.
Set
modelto opus everywhere and keep the field as an escape hatch. No role needs to be haiku or sonnet for any reason except latency.fablestays reachable only throughmodel.overrides.<role>, and its rationale strengthens: a ladder that is not about models has an even clearer reason not to place fable on a rung.The rung table
cad-assumptions-analyzerxhighcad-plannerxhighcad-executorxhighmax_tokenstruncation question that used to gate this row is settled: see "the truncation constraint, settled" below.cad-verifierxhighcad-plan-checkerhighINTERNALS.mdcurrently justifieslowwith "because it is a cheap gate."cad-reviewermedium/highfordiff,xhighforplan,risk_surface,pre_ship. Same change #64 asks for.The reviewer exception is structural rather than a saving. Review accuracy holds at lower effort on Opus 5, which supports a fast pass at review time and a thorough pass later, and that is the gate split Cadence already ships.
The payoff: rigor set from signals Cadence already computes
The opinionated version is not a user preference at all.
Cadence already detects risk surfaces (auth/authz, DB schema, money, concurrency, destructive ops, secrets/crypto, public API contracts, untrusted-input parsing) in order to fire the blocking review trigger. Those same signals can set rigor automatically, alongside the difficulty signals
autoalready reads.This is the argument that only exists after the reframe, and it is the strongest one here. You cannot auto-set a spend level from a risk signal without it being absurd: touching auth is no reason to become willing to pay more. You can absolutely auto-set a rigor level from one. A spend ladder structurally cannot express "this phase touches money, so verify harder," and that sentence is the whole point.
Compute the reachable rung set from which roles each rigor level can actually dispatch, not a full cross product.
What the effort docs establish on Opus 5
low,medium,high,xhigh,max. The API default ishigh, and passing"high"is identical to omitting the parameter.xhighis the named setting for demanding coding and agentic work.xhighormax(400 error); it is accepted athighor below.effortis a documented subagent frontmatter field that overrides session effort. It is frozen per file on the Agent/Task dispatch path Cadence uses, but not universally: the Workflow tool'sagent()call takes a per-dispatcheffortoption. Do not state the constraint more broadly than it holds.Materializing the rungs
@-includes do not work in agent bodies; that limitation is documented for CLAUDE.md only, and it is why thecad-plan-checker-high.mdruntime-read shim exists. The mechanism is theskills:frontmatter field. Per the subagents reference, listed skills are preloaded into the subagent's context at startup and the full skill content is injected, not only the description. No tool call, no compliance risk.Retire the
cad-plan-checker-high.mdshim in the same change.Two failure modes to guard. A skill with
disable-model-invocation: truecannot be preloaded. A missing or disabled skill is skipped silently with only a debug-log warning, so a typo produces an agent running with no contract rather than an error. Whatever ships needs a self-verify CONTRACTS entry asserting every referenced contract skill resolves.Why the default no is wrong here
The current placement is answering the wrong question, and there is now evidence
Under the shipped
balanceddefault, only heavy-tier roles land on opus.cadence-core/route-table.jsonputscad-executorat tierstandardandcad-plan-checkerat tierlight, andbalancedmaps those to sonnet and haiku respectively.Both placements are cost decisions wearing capability clothes. And in practice, moving an executor to Opus 5 produced fewer loops and fewer bugs in observed use. A looping executor burns more than the model it saved, so the cheap placement was not cheap; it was answering "how much will I spend per dispatch" when the question that mattered was "how many dispatches will this take to get right."
That is the reframe in one concrete case. It is also why the model collapse cannot be argued on its own: "put the executor on opus" reads as a spending increase, while "the executor's work is consequential, so it runs at hardened rigor" reads as what it is.
Corollary worth writing down: until this lands, effort is frozen per agent file and one value has to be correct for haiku, sonnet, and opus at once. Opus-5-specific guidance should not be applied to roles that are not routed to opus, which today includes the executor and the plan checker.
The MANIFESTO objection, met rather than avoided
The stated principle is value per dispatch, not minimize spend. Heavy-to-judgment and cheap-to-rote was that principle implemented when model was the only usable dial. Effort is a better instrument for the same principle, because it varies depth and tool-call count within one model instead of trading capability for price. The principle is unchanged; the dial improved.
The variant-files objection, which is the harder one
Eighteen agent rung files look exactly like the GSD sin the MANIFESTO names by name, "namespace variants of workflows shelved next to the workflows they vary."
They are not that. A GSD namespace variant is a behavioral fork: two files that do different things, drifting apart, each carrying its own maintenance. These are one single-sourced contract materialized at N rungs, because the host freezes
effortper file on the Agent/Task dispatch path. The contract lives in exactly one skill. The rung files are boilerplate that name a rung and point at it. If a rung file ever contains behavior, this proposal has failed and the reviewer should say so.On cost framing
This is not a cost optimization and should not be argued as one. Cadence targets Max subscribers, not API metering. The subscription allowance is a ceiling rather than a budget: it matters only if the cycle actually stops. Measured usage on a heavy day (2026-07-27) was 44M billed-equivalent tokens across 11.8 active hours with no weekly cap hit.
What it costs
The truncation constraint, settled. The effort docs say to set a large
max_tokens(start around 64k) atxhigh/max, because it caps thinking and response text together. The subagent frontmatter field list hasmaxTurnsand nomax_tokens, and the spawn-agent seam has no way to set one. That constraint is real and stays written down:max_tokensis unsettable and unobservable for a subagent. Cadence controls(agent_name, prompt, model?)and nothing about the token budget.What does not follow from it is the failure mode this issue originally claimed. An earlier draft said a truncated executor is a partial commit and a corrupted phase, and made that the gate on the
cad-executorrow. That inverts what truncation does.max_tokensbounds one API response; a phase is many responses, each its own request. Commits are Bash tool calls, so a truncated response is one whose tool call never issued. The tree is not half-written, the turn simply stops, and the fix is a re-dispatch. The recoverable failure was written up as the unrecoverable one, and that is the only reason it gated the proposal.And the cap is nowhere near binding. Measured rather than argued (spike write-up):
tool_use. The call issued intact.end_turn. It stopped because it was finished.stop_reason: "max_tokens"occurs zero times.So the cap is at least 36k output tokens and no real workload in this project's history has ever reached it. The exact ceiling is still unknown, and cannot be read off a corpus of untruncated responses, but the decision does not need it. The
cad-executorrow lands.What else can now fail. A silently-skipped contract skill. A rung file that accumulates behavior and becomes a real variant. A rigor level auto-set from risk signals that fires on the wrong surface and makes a cheap phase expensive.
What does not cost what it looks like. The effort docs warn that changing effort between requests invalidates cached prefixes. That targets an architecture Cadence does not have. Cadence's cache scopes are the orchestrator session and each subagent run, and effort is constant in both by construction: frozen within a run, irrelevant between roles because they share no prefix, unchanged between same-rung dispatches, and deliberately different only on a post-failure re-dispatch. Do not sell frozen effort as a discipline protecting against this; the fresh-context dispatch model already eliminates it.
A correction to carry forward. Lower effort producing terser visible output is not reliable; the Opus 5 prompting guide states that effort controls thinking rather than response length. The claim that survives, and the one the rung table rests on, is fewer tool calls at lower effort.
Half the burn is out of reach. The orchestrator is the user's own session and Cadence cannot configure it. It was 50.9% of measured burn, so no routing change here touches half the spend. Any claim this proposal makes about totals has to say that out loud.
Measurements that would validate or kill this
AnDone. No truncation, and zeroxhighexecutor against a real phase, checked for truncation. Kills the executor row if it truncates.max_tokensstops across the whole transcript corpus. The row survives. Re-running the corpus scan is the cheap standing check if the executor's appetite ever grows.highversusxhigh. If coverage does not move, the highest-leverage row in the table is unfounded.Three things to verify rather than assume, all measurable from the burn script in
/projects/cadence-token-analysis/, which captures per-request timestamps and agent IDs:xhighrun takes minutes, so sequential same-role dispatches are probably past the window, which would meanseams.md's stable-first cache-discipline advice rarely collects.execute_parallelfires concurrently, and a cache entry is readable only once the first response starts streaming, so simultaneous dispatches all pay the write./clearis correct and should not be relaxed. Cache reads were 318M of 329M raw tokens on the measured day, roughly 110k per request and 72% of billed-equivalent, and that term scales with context carried. The 1M window is not an argument to stop clearing.Why this belongs in
mainand not a forkBecause the thing being deleted is in
main, and it is wrong for everyone.The profile matrix asks every user how much they want to spend and gives none of them a way to answer the question they actually have. A fork could pin its own rungs, but a fork cannot fix the fact that Cadence's shipped default routes the executor to sonnet and justifies its last pre-execution gate at
lowon price grounds. Those decisions are inroute-table.jsonandINTERNALS.mdinmain.The auto-rigor argument is the load-bearing one for shared ownership. Cadence already computes the risk signals. Every user running a phase that touches auth or money or destructive ops benefits from those signals raising rigor, and none of them should have to fork to get it. Every fork would rediscover the same mapping from the same signals.
The escape hatches stay, so a user who disagrees is not stuck:
model.overrides.<role>keeps pinning a model,fablestays reachable, and the rigor ladder has adraftrung for exactly the throwaway work where none of this should apply.Related
effortdefect, and it was fixed in 1.5.0 by scoping the key rather than wiring it, because wiring it is not possible without the rung files. Thecad-reviewerrow above remains the real fix and it lands here.tierandeffortuser-configurable. This deletes the tier ladder #54 would expose. One closes the other.skills:preload mechanism. If both land, neither pays for it alone.Files this touches
cadence-core/route-table.json,cadence-core/bin/route.mjsandroute.test.mjs,agents/(rung stubs),skills/(one contract skill per role),agents/cad-plan-checker-high.md(retire the shim),cadence-core/references/seams.md,cadence-core/references/review-triggers.md,cadence-core/config.schema.json,INTERNALS.md"Model routing" (a rewrite rather than an edit, since its narrative is that model is a live lever and effort is not),DESIGN.md(narrow the frozen-effort argument to the Agent/Task path).Cross-links now that numbers exist:
effortdefect. Thecad-reviewerrow in the rung table above is its fix. Fix #64 regardless of what happens to this proposal.tierandeffortuser-configurable in the config layer. This proposal deletes the tier ladder that #54 wants to expose. One of the two closes the other, and that decision comes before either is scheduled.skills:preload mechanism described here. If both land, neither pays for it alone.Rewritten. The original filing presented the rung table and the model collapse as two independent sections, which was wrong in a way that mattered: read separately, each one reads as a spending decision and invites the obvious objection. They are consequences of the ladder reframe, so the body now leads with that and lets them fall out of it.
Also restored from the source analysis and missing from the first version: the governing caveat (the shipped
balanceddefault routescad-executorto sonnet andcad-plan-checkerto haiku, and Opus-5-specific guidance should not be applied to roles not routed to opus), the note that the orchestrator is roughly half of measured burn and out of Cadence's reach entirely, and the effort-docs facts, with the coverage-dial point promoted to the load-bearing premise it actually is rather than a hedge.Added the observed result that motivated the rewrite: moving an executor to Opus 5 produced fewer loops and fewer bugs. That is the measurement this proposal said it lacked, and it lands on the half that was buried. It needs a recorded version, which is now in the measurement list.
Spike: the
max_tokensquestion is settled, and the framing above was wrongRan the truncation spike that gated the
cad-executorrow. Verdict: invalidated. Body updated accordingly.The constraint holds.
max_tokensreally is unsettable and unobservable for a subagent. Sixteen supported frontmatter fields, none of them an output cap; no Agent-tool parameter, no settings key, no env var (MAX_THINKING_TOKENSis the only token variable and it is inert on Opus 5's adaptive reasoning). And the cap does bind thinking and response text together, with no way to disable thinking atxhighormax. That part of the issue was accurate as written.The consequence did not follow. The issue said a truncated executor is a partial commit and a corrupted phase.
max_tokensbounds one API response, not a phase, and commits are Bash tool calls, so a truncated response is one whose tool call never issued. That is a stalled turn you re-dispatch, not a half-written tree. The recoverable failure had been written up as the unrecoverable one, which is the only reason it gated the whole proposal.Measurements. Two probes at
xhigh, then a corpus scan:node --checkclean, 90/90 self-tests, exactly oneexport function tokenize, so no split-and-retry)stop_reason: tool_usestop_reason: end_turnstop_reason: "max_tokens"0 timesThe corpus scan is the one worth keeping. Truncation has an unambiguous signature in the transcript record, so rather than manufacture a truncation event you can just count every truncation that has ever happened. Zero, across everything. The largest naturally-terminated response in the whole corpus is the 36,015-token probe itself.
That bounds the cap at >=36k output tokens against a realistic executor turn costing 26.5k. It does not give the exact ceiling, and a corpus of untruncated responses never will, but the decision does not need one.
Two things the spike turned up that were not in the issue:
maxTurnsis a supported frontmatter field that Cadence sets on no agent. Not a truncation mitigation, since truncation is per-response and turns are not, but it is the only runaway-loop guard the host offers and the repo leaves it unused. Filing separately.Standing check if the executor's appetite ever grows: re-run the corpus scan over
~/.claude/projects/**/*.jsonlcountingstop_reason: "max_tokens". Seconds against real traffic, no probe agent needed. Zero hits means the ceiling still has not been touched.