routing: replace the spend ladder with a rigor ladder, deleting the profile matrix, the tier bump, and escalate_effort_variant #63

Open
opened 2026-07-28 17:41:20 +00:00 by crenshawdev · 3 comments
crenshawdev commented 2026-07-28 17:41:20 +00:00 (Migrated from github.com)

Scope note: this issue was split, and now tracks only the breaking remainder.

The argument below is unchanged and still makes the case for the whole reframe. What changed is delivery. Three pieces that break nothing were extracted so they can ship in v1.5.0 without waiting on a major release:

  • #74 — single-sourced agent contracts via skills: preload (the mechanism that materializes rungs)
  • #75 — route every role to opus, model stays an escape hatch
  • #64 — scopes the per-trigger effort key to the backend that can honour it. Be precise about what this is: an honesty fix, not the capability. Investigation under #64 confirmed effort cannot vary per dispatch on the claude-subagent path at all, so 1.5.0 stops the key lying rather than making it work. The cad-reviewer row of the table below still needs the rung files, and stays here.

What stays here for v2.0.0 is the part a user must act on: the fast/balanced/quality to draft/standard/hardened enum rename with no back-compat alias, the rung stub files themselves, and auto-rigor driven from the risk signals Cadence already computes. That last piece is the actual payoff and it is the one thing that genuinely needs the rename to exist first.

One flag rather than a quiet override: this issue argues the model collapse cannot be extracted, because "put the executor on opus" reads as a spending increase while "this work is consequential, so it runs at hardened rigor" reads as what it is. That objection is reproduced in full at the top of #75. It is a framing concern rather than a technical dependency, and if it turns out to be the deciding one, close #75 and fold it back here.

What you want Cadence to do

Delete three mechanisms and replace them with one.

Out: the fast/balanced/quality profile matrix, the difficulty-driven tier bump, and escalate_effort_variant with its runtime-read shim in agents/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/quality is a spend ladder. It asks the user how much they are willing to pay.

Replace it with a rigor ladder: draft for spikes and throwaway work where turnaround matters, standard, and hardened for 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 model to opus everywhere and keep the field as an escape hatch. No role needs to be haiku or sonnet for any reason except latency. fable stays reachable only through model.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

role base rung why
cad-assumptions-analyzer xhigh Coverage, not thinking. It reads 5-15 files and derives what the code supports; lower effort means fewer tool calls means fewer files read. Its output becomes the LOCKED decisions in CONTEXT.md that bind every downstream task. Highest leverage per token in the spine.
cad-planner xhigh Goal-backward derivation over a whole phase. Errors here cost the entire phase.
cad-executor xhigh Multi-file implementation with tools, the documented case for the rung. The max_tokens truncation question that used to gate this row is settled: see "the truncation constraint, settled" below.
cad-verifier xhigh Evidence gathering is tool-count-sensitive for the same reason as the analyzer. Its own prompt enumerates "how verifiers go soft"; do not hand it a dial that makes it look at less.
cad-plan-checker high Independently derives must-be-true statements, then checks the plan against its own derivation. Real reasoning, and the last gate before execution. INTERNALS.md currently justifies low with "because it is a cheap gate."
cad-reviewer per trigger medium/high for diff, xhigh for plan, 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 auto already 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

  • Five rungs: low, medium, high, xhigh, max. The API default is high, and passing "high" is identical to omitting the parameter.
  • Effort affects all tokens, not just thinking. Lower effort makes the model combine operations into fewer tool calls and make fewer tool calls overall. For read-heavy exploration roles this makes effort a coverage dial rather than a thinking dial. This is the load-bearing fact for the rung table above: the analyzer and verifier rows are about how many files get read, not about how hard the model thinks.
  • xhigh is the named setting for demanding coding and agentic work.
  • Thinking cannot be disabled at xhigh or max (400 error); it is accepted at high or below.
  • Effort and thinking are owned by different layers. As of v2.1.198 a subagent inherits the main conversation's extended-thinking configuration, and there is no per-subagent thinking setting. Effort is per-agent; thinking is session-scoped. Cadence controls how hard its agents think, not whether they think, and no rung file can change that.
  • Effort is a behavioral signal, not a token budget.
  • The minimum cacheable prefix drops to 512 tokens on Opus 5, down from 1024 on Opus 4.8.
  • effort is 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's agent() call takes a per-dispatch effort option. 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 the cad-plan-checker-high.md runtime-read shim exists. The mechanism is the skills: 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.

---
name: cad-verifier-xhigh
effort: xhigh
tools: Read, Bash, Grep, Glob
skills:
  - cad-verifier-contract
---
Follow the preloaded contract exactly.

Retire the cad-plan-checker-high.md shim in the same change.

Two failure modes to guard. A skill with disable-model-invocation: true cannot 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 balanced default, only heavy-tier roles land on opus. cadence-core/route-table.json puts cad-executor at tier standard and cad-plan-checker at tier light, and balanced maps 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

Tokens are a budget, every dispatch is a bet on whether the answer is worth what it burns. Cadence sends a heavy model to the judgment and a cheap one to the rote work.

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 effort per 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) at xhigh/max, because it caps thinking and response text together. The subagent frontmatter field list has maxTurns and no max_tokens, and the spawn-agent seam has no way to set one. That constraint is real and stays written down: max_tokens is 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-executor row. That inverts what truncation does. max_tokens bounds 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):

  • The realistic executor shape, a 1,687-line file written in a single tool call under genuine reasoning load, cost 26,521 output tokens and stopped on tool_use. The call issued intact.
  • A deliberately maximal response reached 36,015 output tokens and stopped on end_turn. It stopped because it was finished.
  • Across 33,165 assistant responses in 668 transcripts, spanning Opus 5, Opus 4.8 and Haiku 4.5, main sessions and subagents alike, 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-executor row 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

  • An xhigh executor against a real phase, checked for truncation. Kills the executor row if it truncates. Done. No truncation, and zero max_tokens stops 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.
  • Analyzer file-read counts at high versus xhigh. If coverage does not move, the highest-leverage row in the table is unfounded.
  • Verifier UNCERTAIN rate by rung. If abstentions do not fall at higher effort, the verifier row buys nothing.
  • Executor loop and retry counts by model, which is the measurement that already has an informal result behind it and deserves a recorded one.

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:

  1. Subagents build cache from cold on a five-minute TTL while the main conversation gets an hour. An xhigh run takes minutes, so sequential same-role dispatches are probably past the window, which would mean seams.md's stable-first cache-discipline advice rarely collects.
  2. execute_parallel fires concurrently, and a cache entry is readable only once the first response starts streaming, so simultaneous dispatches all pay the write.
  3. /clear is 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 main and not a fork

Because 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 low on price grounds. Those decisions are in route-table.json and INTERNALS.md in main.

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, fable stays reachable, and the rigor ladder has a draft rung for exactly the throwaway work where none of this should apply.

  • #64 is the per-trigger effort defect, 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. The cad-reviewer row above remains the real fix and it lands here.
  • #54 wants to make per-role tier and effort user-configurable. This deletes the tier ladder #54 would expose. One closes the other.
  • #69 needs the same skills: preload mechanism. If both land, neither pays for it alone.

Files this touches

cadence-core/route-table.json, cadence-core/bin/route.mjs and route.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).

> **Scope note: this issue was split, and now tracks only the breaking remainder.** > > The argument below is unchanged and still makes the case for the whole reframe. What changed is delivery. Three pieces that break nothing were extracted so they can ship in **v1.5.0** without waiting on a major release: > > - #74 — single-sourced agent contracts via `skills:` preload (the mechanism that materializes rungs) > - #75 — route every role to opus, `model` stays an escape hatch > - #64 — scopes the per-trigger `effort` key to the backend that can honour it. Be precise about what this is: an honesty fix, **not** the capability. Investigation under #64 confirmed effort cannot vary per dispatch on the `claude-subagent` path at all, so 1.5.0 stops the key lying rather than making it work. The `cad-reviewer` row of the table below still needs the rung files, and stays here. > > What stays here for **v2.0.0** is the part a user must act on: the `fast`/`balanced`/`quality` to `draft`/`standard`/`hardened` enum rename with no back-compat alias, the rung stub files themselves, and auto-rigor driven from the risk signals Cadence already computes. That last piece is the actual payoff and it is the one thing that genuinely needs the rename to exist first. > > One flag rather than a quiet override: this issue argues the model collapse *cannot* be extracted, because "put the executor on opus" reads as a spending increase while "this work is consequential, so it runs at hardened rigor" reads as what it is. That objection is reproduced in full at the top of #75. It is a framing concern rather than a technical dependency, and if it turns out to be the deciding one, close #75 and fold it back here. ## What you want Cadence to do Delete three mechanisms and replace them with one. Out: the `fast`/`balanced`/`quality` profile matrix, the difficulty-driven tier bump, and `escalate_effort_variant` with its runtime-read shim in `agents/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`/`quality` is a spend ladder. It asks the user how much they are willing to pay. Replace it with a rigor ladder: `draft` for spikes and throwaway work where turnaround matters, `standard`, and `hardened` for 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 `model` to opus everywhere and keep the field as an escape hatch. No role needs to be haiku or sonnet for any reason except latency. `fable` stays reachable only through `model.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 | role | base rung | why | |---|---|---| | `cad-assumptions-analyzer` | `xhigh` | Coverage, not thinking. It reads 5-15 files and derives what the code supports; lower effort means fewer tool calls means fewer files read. Its output becomes the LOCKED decisions in CONTEXT.md that bind every downstream task. Highest leverage per token in the spine. | | `cad-planner` | `xhigh` | Goal-backward derivation over a whole phase. Errors here cost the entire phase. | | `cad-executor` | `xhigh` | Multi-file implementation with tools, the documented case for the rung. The `max_tokens` truncation question that used to gate this row is settled: see "the truncation constraint, settled" below. | | `cad-verifier` | `xhigh` | Evidence gathering is tool-count-sensitive for the same reason as the analyzer. Its own prompt enumerates "how verifiers go soft"; do not hand it a dial that makes it look at less. | | `cad-plan-checker` | `high` | Independently derives must-be-true statements, then checks the plan against its own derivation. Real reasoning, and the last gate before execution. `INTERNALS.md` currently justifies `low` with "because it is a cheap gate." | | `cad-reviewer` | per trigger | `medium`/`high` for `diff`, `xhigh` for `plan`, `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 `auto` already 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 - Five rungs: `low`, `medium`, `high`, `xhigh`, `max`. The API default is `high`, and passing `"high"` is identical to omitting the parameter. - **Effort affects all tokens, not just thinking.** Lower effort makes the model combine operations into fewer tool calls and make fewer tool calls overall. For read-heavy exploration roles this makes effort a **coverage dial rather than a thinking dial**. This is the load-bearing fact for the rung table above: the analyzer and verifier rows are about how many files get read, not about how hard the model thinks. - `xhigh` is the named setting for demanding coding and agentic work. - Thinking cannot be disabled at `xhigh` or `max` (400 error); it is accepted at `high` or below. - **Effort and thinking are owned by different layers.** As of v2.1.198 a subagent inherits the main conversation's extended-thinking configuration, and there is no per-subagent thinking setting. Effort is per-agent; thinking is session-scoped. Cadence controls how hard its agents think, not whether they think, and no rung file can change that. - Effort is a behavioral signal, not a token budget. - The minimum cacheable prefix drops to 512 tokens on Opus 5, down from 1024 on Opus 4.8. - `effort` is 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's `agent()` call takes a per-dispatch `effort` option. 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 the `cad-plan-checker-high.md` runtime-read shim exists. The mechanism is the `skills:` frontmatter field. Per [the subagents reference](https://code.claude.com/docs/en/sub-agents), 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. ```yaml --- name: cad-verifier-xhigh effort: xhigh tools: Read, Bash, Grep, Glob skills: - cad-verifier-contract --- Follow the preloaded contract exactly. ``` Retire the `cad-plan-checker-high.md` shim in the same change. Two failure modes to guard. A skill with `disable-model-invocation: true` cannot 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 `balanced` default, only heavy-tier roles land on opus. `cadence-core/route-table.json` puts `cad-executor` at tier `standard` and `cad-plan-checker` at tier `light`, and `balanced` maps 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 > Tokens are a budget, every dispatch is a bet on whether the answer is worth what it burns. Cadence sends a heavy model to the judgment and a cheap one to the rote work. 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 `effort` per 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) at `xhigh`/`max`, because it caps thinking and response text together. The subagent frontmatter field list has `maxTurns` and no `max_tokens`, and the spawn-agent seam has no way to set one. That constraint is real and stays written down: `max_tokens` is 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-executor` row. That inverts what truncation does. `max_tokens` bounds 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](https://github.com/crenshawdev/cadence/blob/main/.planning/spikes/xhigh-executor-truncation/SPIKE.md)): - The realistic executor shape, a 1,687-line file written in a single tool call under genuine reasoning load, cost **26,521 output tokens** and stopped on `tool_use`. The call issued intact. - A deliberately maximal response reached **36,015 output tokens** and stopped on `end_turn`. It stopped because it was finished. - Across **33,165 assistant responses in 668 transcripts**, spanning Opus 5, Opus 4.8 and Haiku 4.5, main sessions and subagents alike, `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-executor` row 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 - ~~An `xhigh` executor against a real phase, checked for truncation. Kills the executor row if it truncates.~~ **Done.** No truncation, and zero `max_tokens` stops 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. - Analyzer file-read counts at `high` versus `xhigh`. If coverage does not move, the highest-leverage row in the table is unfounded. - Verifier UNCERTAIN rate by rung. If abstentions do not fall at higher effort, the verifier row buys nothing. - Executor loop and retry counts by model, which is the measurement that already has an informal result behind it and deserves a recorded one. 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: 1. Subagents build cache from cold on a five-minute TTL while the main conversation gets an hour. An `xhigh` run takes minutes, so sequential same-role dispatches are probably past the window, which would mean `seams.md`'s stable-first cache-discipline advice rarely collects. 2. `execute_parallel` fires concurrently, and a cache entry is readable only once the first response starts streaming, so simultaneous dispatches all pay the write. 3. `/clear` is 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 `main` and not a fork Because 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 `low` on price grounds. Those decisions are in `route-table.json` and `INTERNALS.md` in `main`. 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, `fable` stays reachable, and the rigor ladder has a `draft` rung for exactly the throwaway work where none of this should apply. ## Related - #64 is the per-trigger `effort` defect, 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. The `cad-reviewer` row above remains the real fix and it lands here. - #54 wants to make per-role `tier` and `effort` user-configurable. This deletes the tier ladder #54 would expose. One closes the other. - #69 needs the same `skills:` preload mechanism. If both land, neither pays for it alone. ### Files this touches `cadence-core/route-table.json`, `cadence-core/bin/route.mjs` and `route.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).
crenshawdev commented 2026-07-28 17:41:48 +00:00 (Migrated from github.com)

Cross-links now that numbers exist:

  • #64 is the per-trigger effort defect. The cad-reviewer row in the rung table above is its fix. Fix #64 regardless of what happens to this proposal.
  • #54 asks to make per-role tier and effort user-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.
  • #69 needs the same skills: preload mechanism described here. If both land, neither pays for it alone.
Cross-links now that numbers exist: - #64 is the per-trigger `effort` defect. The `cad-reviewer` row in the rung table above is its fix. Fix #64 regardless of what happens to this proposal. - #54 asks to make per-role `tier` and `effort` user-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. - #69 needs the same `skills:` preload mechanism described here. If both land, neither pays for it alone.
crenshawdev commented 2026-07-28 17:52:08 +00:00 (Migrated from github.com)

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 balanced default routes cad-executor to sonnet and cad-plan-checker to 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.

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 `balanced` default routes `cad-executor` to sonnet and `cad-plan-checker` to 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.
crenshawdev commented 2026-07-28 18:30:53 +00:00 (Migrated from github.com)

Spike: the max_tokens question is settled, and the framing above was wrong

Ran the truncation spike that gated the cad-executor row. Verdict: invalidated. Body updated accordingly.

The constraint holds. max_tokens really 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_TOKENS is 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 at xhigh or max. 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_tokens bounds 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:

probe shape measured
realistic executor turn 1,687-line tokenizer in one Write, verified independently (node --check clean, 90/90 self-tests, exactly one export function tokenize, so no split-and-retry) 26,521 output tokens, stop_reason: tool_use
deliberately maximal 12,000 lines emitted directly, told to run until physically cut off 36,015 output tokens, stop_reason: end_turn
corpus scan 668 transcripts, 33,165 assistant responses, Opus 5 / Opus 4.8 / Haiku 4.5, main sessions and subagents stop_reason: "max_tokens" 0 times

The 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:

  1. Effort and thinking belong to different layers. A subagent inherits the main conversation's extended-thinking configuration (v2.1.198) and there is no per-subagent thinking setting. Added to the effort-docs list above.
  2. maxTurns is 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/**/*.jsonl counting stop_reason: "max_tokens". Seconds against real traffic, no probe agent needed. Zero hits means the ceiling still has not been touched.

## Spike: the `max_tokens` question is settled, and the framing above was wrong Ran the truncation spike that gated the `cad-executor` row. Verdict: **invalidated**. Body updated accordingly. **The constraint holds.** `max_tokens` really 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_TOKENS` is 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 at `xhigh` or `max`. 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_tokens` bounds 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: | probe | shape | measured | |---|---|---| | realistic executor turn | 1,687-line tokenizer in one Write, verified independently (`node --check` clean, 90/90 self-tests, exactly one `export function tokenize`, so no split-and-retry) | 26,521 output tokens, `stop_reason: tool_use` | | deliberately maximal | 12,000 lines emitted directly, told to run until physically cut off | 36,015 output tokens, `stop_reason: end_turn` | | corpus scan | 668 transcripts, 33,165 assistant responses, Opus 5 / Opus 4.8 / Haiku 4.5, main sessions and subagents | `stop_reason: "max_tokens"` **0 times** | The 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:** 1. Effort and thinking belong to different layers. A subagent inherits the main conversation's extended-thinking configuration (v2.1.198) and there is no per-subagent thinking setting. Added to the effort-docs list above. 2. `maxTurns` is 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/**/*.jsonl` counting `stop_reason: "max_tokens"`. Seconds against real traffic, no probe agent needed. Zero hits means the ceiling still has not been touched.
Sign in to join this conversation.
No description provided.