ytdb-slate 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ytdb-slate
2
2
 
3
+ [![CI status](https://github.com/JetBrains/ytdb-slate/actions/workflows/tier-1.yml/badge.svg?branch=main)](https://github.com/JetBrains/ytdb-slate/actions/workflows/tier-1.yml)
4
+
3
5
  Slate is a thread-weaving orchestration extension for the [pi coding agent](https://pi.dev).
4
6
 
5
7
  The orchestrator (your main pi session) dispatches **bounded actions** to persistent **worker threads**. Each completed action is compressed by an LLM into an **episode** — a durable, structured record (intent, actions, findings, artifacts, open issues, handoff notes) that the orchestrator composes into further dispatches instead of re-reading raw transcripts. On top of that, Slate injects a mandatory workflow doctrine: research before design, design review, adversarial review, and track review — with optional umbrella **draft-PR publishing** for tracks. An opt-in **model-failover** map adds high availability: when a model API fails, the orchestrator, worker threads, and episode compression each retry once on a configured equal-quality alternative. A second opt-in, **action-level model routing**, gives each dispatched action a model and an effort level chosen to be up to the task and no more, so cost is bounded per action instead of per session.
@@ -53,10 +55,20 @@ Slate can ride through model API outages. The opt-in `modelFailover` map in `.pi
53
55
 
54
56
  ## Action-level model routing
55
57
 
56
- Slate can pick the model and the effort level per dispatched action rather than running every action on whatever model the orchestrator happens to be on. `router.models` in `.pi/slate.json` (trusted projects only) is a closed list of `provider/id` specs; a dispatch then runs on the `model` and `effort` arguments of that action, else on the thread's own base model and a level DERIVED for the model that actually runs — the lowest level that model has a capability measurement at, never a fixed default. The list is empty by default, and empty means off: no list is enforced, nothing is seeded, and model selection is exactly what it was before the router existed. Only models Slate ships a benchmark profile for can be routed to; entries pi cannot serve, or has no profile for, are dropped with a warning naming them.
58
+ Slate can pick the model and the effort level per dispatched action rather than running every action on whatever model the orchestrator happens to be on. `router.models` in `.pi/slate.json` (trusted projects only) is a closed list of `provider/id` specs. Each action uses its `model` and `effort` arguments when present. Otherwise, it uses the thread's base model and a level DERIVED for the model it routes to. That level is the lowest one with a capability measurement, never a fixed default. The list is empty by default. An empty list disables candidate-list enforcement, base seeding, effort derivation, and the router's window, billing, and substitution paths. Per-action `model` and `effort` arguments stay active on both router states. Slate always checks an explicit effort against pi's vocabulary, and against the ladder of the model it routes to whenever it has that model's profile data. An action that omits `model` plans for the thread's pre-router pin, and a thread with no pin gives it no plan target. An action that omits `effort` resolves no level. In both cases the worker session returns to what it opened on. [`docs/model-routing.md`](docs/model-routing.md) holds the full contract, including the known cases where the model or level that runs differs. Only models Slate ships a benchmark profile for can be routed to; entries pi cannot serve, or has no profile for, are dropped with a warning naming them.
57
59
 
58
60
  What the code enforces is deliberately narrower than what the routing data advises. The dispatch guards refuse an unlisted model, a level off the target model's ladder and a level the provider rejects outright; they warn about — and mark — an unmeasured level, refusing it only when `router.allowUnmeasuredEffort` is `false`; and the context-window guard substitutes a wider listed model instead of ever blocking an action. The obligations that come from the profile data itself — keeping review and gate actions on measured levels, and honoring a REFUSE such as `anthropic/claude-fable-5`'s for zero-data-retention work — are stated in the doctrine for the orchestrator to honor; they are **not** code-enforced guards. Full semantics: the `router.*` rows in [Configuration](#configuration) and the shipped [`docs/model-routing.md`](docs/model-routing.md) — if they disagree, that document wins.
59
61
 
62
+ ## Optional writing guidance
63
+
64
+ Set `writing.check` to `true` to add writing guidance and human-only telemetry. In orchestrator mode, Slate adds a doctrine rule that states the convention. It also adds a status value such as `writing 1/4`. This value means that one of four measured prose turns had a fail-level finding. Each worker gets a shorter preamble reminder about sentence length, semicolons, and contractions.
65
+
66
+ The option has prompt cost. Each orchestrator system prompt contains the doctrine rule. You pay for this text on every turn. Each worker session also gets a gated preamble addition. Both additions are absent when the option is off. The worker preamble then stays byte-identical to its pre-feature form.
67
+
68
+ > **No conformance claim:** This is a dictionary-free proxy. It embeds no controlled vocabulary and claims no ASD-STE100 conformance.
69
+
70
+ The shipped CLI checks plain files, JSONL records, and unified diffs. See [`docs/writing-guidance.md`](docs/writing-guidance.md) for its rules, limits, output, and command examples.
71
+
60
72
  ## Install
61
73
 
62
74
  Install into a project (pinned, project-scoped — recorded in `.pi/settings.json`):
@@ -82,11 +94,12 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
82
94
  | `orchestratorPromptDocs` | string[] | `[]` | Project markdown files (paths relative to the project root) whose **contents** are appended to the orchestrator system prompt. |
83
95
  | `workerPromptDocs` | string[] | `[]` | Project markdown files whose **contents** are appended to every worker-thread system prompt. |
84
96
  | `workflow.draftPRs` | boolean | `false` | Enable umbrella draft-PR publishing for tracks. |
97
+ | `writing.check` | boolean | `false` | Add the writing doctrine rule, per-turn status, and worker guidance. An absent option is silently off. A malformed `writing` value, or a non-boolean `check` value, warns and defaults to off. Unknown `writing` keys warn and are ignored. See [`docs/writing-guidance.md`](docs/writing-guidance.md). |
85
98
  | `doctrineExtraPath` | string | — | Project markdown whose **content** is appended to the orchestrator doctrine (project-specific workflow additions). |
86
99
  | `reviewPerspectivesPath` | string | — | Project review charters, each declaring its own finding-ID prefix. The doctrine references this **path**; the orchestrator reads the file alongside the shipped review rules. |
87
100
  | `modelFailover` | object (string → string) | — (empty, failover off) | Map of `provider/id` → equal-quality alternative model; on a model API failure the affected site retries once on the mapped model (worker/orchestrator sites only after pi's own retries are exhausted; episode compression has no pi retry loop). Read once at session start — see [`docs/model-failover.md`](docs/model-failover.md). |
88
101
  | `preserveGlobalModelDefault` | boolean | `true` | Put pi's global model defaults (`defaultProvider`, `defaultModel`, `defaultThinkingLevel`) back after a Slate-initiated model switch — orchestrator failover and handoff adoption — so the switch stays session-scoped. `false` turns that restore off at both sites (the switches themselves are unaffected), leaving each one persisted in pi's global settings. Best-effort: Slate reverts only a value its own switch produced, and can stand down or give up — limits in [`docs/model-failover.md`](docs/model-failover.md). |
89
- | `router.models` | string[] | `[]` (empty — routing off) | Closed list of models (`provider/id`) an action may be routed to. **Empty or absent means the router is OFF and dispatch behaves exactly as it did before the router existed** — no model list is enforced and no thread gets a seeded base model. A listed model becomes routable only where four things intersect: your list, a benchmark profile Slate ships for that model, pi's model registry, and credentials pi actually has configured; anything that fails one of those is dropped with a warning naming it (if every entry drops, routing turns off with one summary warning). Read once at session start and resolved once per session — see [`docs/model-routing.md`](docs/model-routing.md). |
102
+ | `router.models` | string[] | `[]` (empty — routing off) | Closed list of models (`provider/id`) an action may use. Empty or absent means the router is OFF. Slate enforces no candidate list, seeds no thread base, derives no effort level, and runs no router-only window, billing, or substitution path. Per-action `model` and `effort` arguments stay active on both router states. Slate still checks explicit effort against pi's vocabulary and the routed model's ladder when it has that model's profile data. An action that omits `model` plans for the thread's pre-router pin, and a thread with no pin gives it no plan target. An action that omits `effort` resolves no level. In both cases the worker session returns to what it opened on. The known cases where the model or level that runs differs are listed in [`docs/model-routing.md`](docs/model-routing.md). A listed model becomes routable only if Slate has its benchmark profile, pi registers it, and credentials are configured. Slate drops other entries with a warning. If all entries drop, routing turns off with one summary warning. Slate reads and resolves the list once per session. See [`docs/model-routing.md`](docs/model-routing.md). |
90
103
  | `router.allowUnmeasuredEffort` | boolean | `true` | What happens when an explicitly requested effort level is on the target model's ladder but has no capability measurement in Slate's profiles: `true` dispatches it with a ⚠ notice and marks the episode `(unmeasured level)`; `false` refuses the dispatch. Either way a level that is off the model's ladder, or one the provider rejects outright, is refused, and a level Slate DERIVES for an action is always a measured one. |
91
104
 
92
105
  Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point them at markdown files that actually exist in **your** project):
@@ -99,6 +112,7 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
99
112
  "orchestratorPromptDocs": ["docs/agents/orchestrator-guidelines.md"],
100
113
  "workerPromptDocs": ["docs/agents/thread-guidelines.md"],
101
114
  "workflow": { "draftPRs": true },
115
+ "writing": { "check": true },
102
116
  "doctrineExtraPath": "docs/agents/workflow-additions.md",
103
117
  "reviewPerspectivesPath": "docs/agents/review-perspectives.md",
104
118
  "modelFailover": { "anthropic/claude-sonnet-5": "openai/gpt-5.2" },
@@ -143,6 +157,7 @@ In orchestrator mode, Slate appends a short **doctrine** (a block of numbered ru
143
157
  - `docs/design-principles.md` — Slate's own design rationale
144
158
  - `docs/model-failover.md` — the opt-in `modelFailover` map (**reference documentation** — unlike the entries above it is not workflow doctrine and is not cited by the doctrine)
145
159
  - `docs/context-budget.md` — the orchestrator `contextBudget`: defaults, per-model overrides, the window clamp, and the pricing rationale (also **reference documentation**, not cited by the doctrine)
160
+ - `docs/writing-guidance.md` — the optional writing convention, `writing.check` behavior, status line, and checker CLI
146
161
  - `docs/model-routing.md` — action-level model routing: the two `router` keys, how a model becomes routable, how an omitted effort level resolves, the dispatch guards, and the warnings a first routed session emits. Unlike the two entries above it **is** cited by the doctrine, by absolute path, but only while the routing rule renders — it is the extra embedded path that appears with the rule, the fourth normally and the fifth when `workflow.draftPRs` is also on, which the size arithmetic in `docs/context-budget.md` accounts for. The routable model list itself is never read from this file: the rule renders it live from the session's own resolution, because that set depends on your registry and credentials.
147
162
 
148
163
  Project-specific additions layer on top — they extend, not replace, the shipped doctrine — via two distinct mechanisms:
@@ -129,69 +129,93 @@ you pay its input tokens every turn.
129
129
 
130
130
  **Its size depends on where the package is installed.** That is not
131
131
  obvious, and it catches anyone who measures: the doctrine cites the
132
- shipped docs by ABSOLUTE path, so three to five full filesystem
133
- paths are embedded in the block — three at minimum
132
+ shipped docs by ABSOLUTE path, so three to six full filesystem paths
133
+ are embedded in the block — three at minimum
134
134
  (`track-workflow.md`, `review-rules.md`, `design-principles.md`),
135
135
  plus `pr-publishing.md` when `workflow.draftPRs` is on, plus
136
- `model-routing.md` when the routing rule renders. Every additional
137
- character in the installed `docs/` directory therefore costs 3–5
136
+ `model-routing.md` when the routing rule renders, plus
137
+ `writing-guidance.md` when `writing.check` is on. Every additional
138
+ character in the installed `docs/` directory therefore costs 3–6
138
139
  characters of doctrine.
139
140
 
140
- So the figures below are **portable characters**, defined exactly as
141
- Slate's own automated size check defines them: the block with each
142
- occurrence of the installed `docs/` DIRECTORY removed, keeping the
143
- filename. That makes them install-invariant by construction —
144
- verified by measuring at a 12-character docs path and at this repo's
145
- 58-character one, which give identical portable counts — and it
146
- makes the arithmetic one multiplication:
141
+ The figures below are **portable characters**, defined exactly as
142
+ Slate's own automated `doctrine-budget` check defines them: the
143
+ block with each occurrence of the installed `docs/` DIRECTORY
144
+ removed, keeping the filename. That makes them install-invariant by
145
+ construction and makes the arithmetic one multiplication:
147
146
 
148
147
  > rendered characters = portable + (embedded paths × length of your
149
148
  > installed `docs/` directory)
150
149
 
151
- Every row below states its full basis, because router state, model
152
- count and `draftPRs` each move the number:
150
+ `doctrine-budget` in `verification/resolver-checks.mjs` is the
151
+ definition of record for this convention and its enforced bounds.
152
+ This table is a measured snapshot, not a second authority. It uses
153
+ the same real shipped-profile fixture as that check; the six-model
154
+ rows use the six configured models in this repository's documented
155
+ example. Every row states its full basis, because router state,
156
+ model count, `draftPRs`, and `writing.check` each move the number:
153
157
 
154
- | router | models | `draftPRs` | paths | portable | lines | rendered at a 58-char docs dir |
158
+ | router | models | `draftPRs` | `writing.check` | paths | portable | lines |
155
159
  | --- | --- | --- | --- | --- | --- | --- |
156
- | off | — | off | 3 | 1,929 | 38 | 2,103 |
157
- | off | — | on | 4 | 1,948 | 38 | 2,180 |
158
- | on | 6 configured | off | 4 | 3,870 | 58 | 4,102 |
159
- | on | 6 configured | on | 5 | 3,889 | 58 | 4,179 |
160
- | on | all 9 shipped | off | 4 | 4,448 | 61 | 4,680 |
161
- | on | all 9 shipped | on | 5 | 4,467 | 61 | 4,757 |
160
+ | off | — | off | off | 3 | 1,929 | 38 |
161
+ | off | — | off | on | 4 | 2,683 | 53 |
162
+ | off | — | on | off | 4 | 1,948 | 38 |
163
+ | off | — | on | on | 5 | 2,702 | 53 |
164
+ | on | 6 configured | off | off | 4 | 3,879 | 58 |
165
+ | on | 6 configured | off | on | 5 | 4,633 | 73 |
166
+ | on | 6 configured | on | off | 5 | 3,898 | 58 |
167
+ | on | 6 configured | on | on | 6 | 4,652 | 73 |
168
+ | on | all 9 shipped | off | off | 4 | 4,434 | 61 |
169
+ | on | all 9 shipped | off | on | 5 | 5,188 | 76 |
170
+ | on | all 9 shipped | on | off | 5 | 4,453 | 61 |
171
+ | on | all 9 shipped | on | on | 6 | 5,207 | 76 |
162
172
 
163
173
  An untrusted project reads the first row whatever its `slate.json`
164
- says: no project config is loaded, so neither optional rule renders.
165
- Line counts do not vary with the install path. Enabling `draftPRs`
166
- costs 19 portable characters plus one more embedded path.
174
+ says: no project config is loaded, so no optional rule renders. Line
175
+ counts do not vary with the install path. Enabling `draftPRs` costs
176
+ 19 portable characters plus one embedded path. Enabling
177
+ `writing.check` costs 754 portable characters, adds 15 lines to the
178
+ whole doctrine, and adds one embedded path; considered alone, the
179
+ writing rule is 754 portable characters / 16 lines because its
180
+ leading newline becomes the separator when appended.
167
181
 
168
182
  Two parts of the block grow with configuration rather than with the
169
183
  path:
170
184
 
171
185
  - The routing rule is a live table with ONE ROW PER ROUTABLE MODEL,
172
186
  so what renders is the models you CONFIGURE — not the nine Slate
173
- ships profiles for. It is 1,941 portable characters / 20 lines for
174
- six configured models and 2,519 / 23 for all nine. A model ROW
175
- costs 154–186 characters; the legend adds a one-off clause per
176
- marker it has to explain, so listing the first cheap-tier model
177
- costs ~50 characters more than its row (two new clauses). That is
178
- why stepping from six to nine averages 193 per model where the
179
- first six average 171.
187
+ ships profiles for. In this snapshot it is 1,950 portable
188
+ characters and adds 20 doctrine lines for six configured models;
189
+ for all nine it is 2,505 characters and adds 23 lines. The six
190
+ model rows are 146–181 characters; all nine are 146–183. The
191
+ legend adds a one-off clause per marker it has to explain, so
192
+ growth is not only the sum of new rows.
180
193
  - The worker-extension rule grows per whitelisted extension and per
181
194
  tool that extension contributes, so it has no fixed size; one
182
195
  extension contributing two tools measured 373 portable characters
183
196
  / 7 lines.
184
197
 
185
- Against a 256,000-token budget none of this is material — as a rough
186
- estimate, at 4 characters per token (no tokenizer was run, and the
187
- table is denser than prose, so treat it as a floor) the whole block
188
- is ≈480–525 tokens with both optional rules off and ≈970–1,025 with
189
- six models routed, each range spanning portable to as-rendered-here.
190
- That is under half a percent of the default budget, though it is
191
- re-sent on every request rather than paid once. It is listed here
192
- because it is context Slate itself puts in front of the model on
193
- every turn, and because it is the number to check before assuming
194
- the budget's headroom is all conversation.
198
+ Against a 256,000-token budget none of this is material. At four
199
+ characters per token as a rough estimate (no tokenizer was run, and
200
+ the routing table is denser than prose), the portable block ranges
201
+ from about 482 tokens at the baseline to about 1,302 with all nine
202
+ models, `draftPRs`, and writing enabled. That is close to half a
203
+ percent of the default budget at the largest measured configuration,
204
+ though it is re-sent on every request rather than paid once. It is
205
+ listed here because it is context Slate itself puts in front of the
206
+ model on every turn, and because it is the number to check before
207
+ assuming the budget's headroom is all conversation.
208
+
209
+ ### Worker writing preamble
210
+
211
+ The doctrine table does not include the worker preamble. It belongs
212
+ to each worker session's system prompt, not to the orchestrator's
213
+ per-turn doctrine. The feature-off preamble is 226 bytes. With
214
+ `writing.check` on it is 384 bytes, a 158-byte increase (the
215
+ 157-byte guidance plus its separating space). These are UTF-8 byte
216
+ counts; the current text is ASCII, so its character counts happen to
217
+ be identical. This separate figure states the worker-session cost
218
+ without presenting it as orchestrator doctrine.
195
219
 
196
220
  ## Worker threads: the routing window guard
197
221
 
@@ -194,7 +194,9 @@ context budget, also decide per dispatch whether a worker thread still
194
194
  fits the model it is routed to, in which case the action moves to the
195
195
  widest listed model rather than being blocked.
196
196
 
197
- The feature is OFF until `router.models` lists something, and it is
197
+ Candidate routing is OFF until `router.models` lists something — the
198
+ per-action `model` and `effort` arguments, and a fallback a failover
199
+ is holding, stay active either way. The feature is
198
200
  built on a deliberate split that is worth holding onto when reasoning
199
201
  about the architecture: the code ENFORCES less than the routing data
200
202
  ADVISES. Dispatch guards refuse what is mechanically wrong — a model
@@ -89,11 +89,11 @@ Failover applies at three sites:
89
89
  model and the failed action is re-prompted once with a continuation
90
90
  nudge. The transcript is preserved; usage and cost accumulate across
91
91
  both attempts. The thread stays on the mapped model while its
92
- session is live and returns to its configured model when the session
93
- is reopened (e.g. after a pi restart). The `threads` listing marks a
94
- switched live session as `model=A ⇒B (live)` — the marker means a
95
- live failover switch happened (it is not a value comparison) and
96
- disappears when the session is reopened.
92
+ session is live, including across later model-less actions, and the
93
+ `threads` listing marks it `live=<model> (failover)`. The hold, the
94
+ marker and the retry inside the failing dispatch are cases M1, M6
95
+ and M2 of [Known cases where the model or level
96
+ differs](model-routing.md#known-cases-where-the-model-or-level-differs).
97
97
  - **Episode compression.** The compression call is retried once with
98
98
  the mapped compressor model — only if it is distinct from the
99
99
  original, resolvable, and authed. If the retry also fails, the
@@ -6,14 +6,19 @@ list of models an action may run on; every dispatch then resolves to
6
6
  one model from that list and one effort level from that model's own
7
7
  ladder — chosen to be up to the task and no more, so the bill is
8
8
  bounded by the action rather than by the session's current model.
9
- The list is empty by default: with no entries the router is OFF, no
10
- model list is enforced, no thread gets a seeded base, and a
11
- dispatch that names no model resolves exactly as it did before the
12
- router existed — the thread's pre-router pin, else the host
13
- session's model. (The per-action `model` and `effort` arguments
14
- themselves are honoured on both router states; see the accepted
15
- limitations.) This document is reference documentation, not
16
- workflow doctrine.
9
+ The list is empty by default. With no entries the router is OFF: no
10
+ model list is enforced, no thread gets a seeded base, no effort level
11
+ is derived, and the router's window, billing and substitution paths
12
+ are inert. A dispatch that names no model then plans for the thread's
13
+ pre-router pin, and for no model at all when the thread has none: a
14
+ new worker opens on the host session's model, and a reused one stays
15
+ on the model it opened on. The per-action `model` and `effort`
16
+ arguments are honoured on both router states. What a dispatch plans
17
+ is not always what it runs; the cases known to bite are collected
18
+ under [Known cases where the model or level
19
+ differs](#known-cases-where-the-model-or-level-differs).
20
+
21
+ This document is reference documentation, not workflow doctrine.
17
22
 
18
23
  A model can only be routed to if Slate ships a benchmark profile for
19
24
  it. An entry with no profile is named in a warning and excluded —
@@ -26,36 +31,44 @@ from](#where-the-numbers-come-from-and-how-stale-they-can-be).
26
31
 
27
32
  ## What routing decides, per action
28
33
 
29
- Two levers, both per dispatch — on a new thread and on a
34
+ Two route inputs are per dispatch — on a new thread and on a
30
35
  continuation alike:
31
36
 
32
37
  - **the model** — the `model` argument (`"provider/id"`), else the
33
- thread's base model (router ON), else the thread's pre-router pin
34
- or the host session's model (router OFF);
38
+ thread's base model (router ON), else the thread's pre-router pin,
39
+ else no plan target at all, in which case the session returns to
40
+ the model it opened on (router OFF; M1, M3, M7);
35
41
  - **the effort level** — the `effort` argument (one of pi's thinking
36
- levels), else a level Slate resolves for the model that actually
37
- runs (router ON) or the level the worker session opened on (router
42
+ levels), else a level Slate resolves for the model it routes to
43
+ (router ON) or the level the worker session opened on (router
38
44
  OFF). See [Effort levels](#effort-levels).
39
45
 
40
- Neither is sticky, and that is the guarantee to hold on to: an
41
- explicit `model` or `effort` governs its own action and no later
42
- one. An omitted `model` resolves to the thread's base. An omitted
43
- `effort` resolves differently on the two router states, and neither
44
- of them is "whatever the last action asked for":
46
+ The arguments themselves do not become thread defaults. An explicit
47
+ `effort` never governs a later action. An explicit `model` governs
48
+ only its own action too, outside M1, M3, M6 and M7. An omitted `effort`
49
+ is never "whatever the last action asked for":
45
50
 
46
51
  - **router on** — the thread's base effort, re-validated, or a level
47
- derived for the model that runs (see [Effort
52
+ derived for the model it routes to (see [Effort
48
53
  levels](#effort-levels)). A thread based on `luna@medium` whose
49
54
  previous action ran at `max` runs the next one at `medium`.
50
- - **router off** — nothing is resolved, so the worker session is put
51
- back to the level it opened on, which is pi's own settings default
55
+ - **router off** — the worker session is put back to the level it
56
+ opened on, which is pi's own settings default
52
57
  (`defaultThinkingLevel`, else pi's built-in default). Slate passes
53
58
  that level explicitly at every open, so it is re-clamped against
54
59
  the session's current model rather than inherited from the session
55
60
  file.
56
61
 
57
- A thread's base model and base effort are separate state, visible in
58
- the `threads` listing, and are never set from an action's own route.
62
+ The model axis carries live-session cases the effort axis does not,
63
+ M1 and M3 among them, and they predate action-level routing. The
64
+ effort axis shares only M7. All of them, with the code that
65
+ implements each, are under [Known cases where the model or level
66
+ differs](#known-cases-where-the-model-or-level-differs).
67
+
68
+ A thread's base model and base effort are separate planning state,
69
+ visible in the `threads` listing, and are never set from an action's
70
+ own route. A live failover marker, when present, identifies the
71
+ model that currently overrides that nominal base.
59
72
 
60
73
  Two things can come back instead of a normal episode:
61
74
 
@@ -68,6 +81,120 @@ Two things can come back instead of a normal episode:
68
81
  result — advisory: an evidence gap, a window substitution, a
69
82
  long-context billing cliff, a re-seeded base. The action ran.
70
83
 
84
+ ## Known cases where the model or level differs
85
+
86
+ The model and the level an action runs on are not always the ones it
87
+ asked for, or the ones its thread implies. The cases below are the
88
+ ones known to bite, each with the code that implements it. **This
89
+ list is not exhaustive.** `planRoute`, `decideModelSwitch` and
90
+ `decideEffortSwitch` in `extension/route.ts`, applied by `applyRoute`
91
+ in `extension/threads.ts`, are the authority: read them when you need
92
+ certainty rather than a map. This section is the reference for these
93
+ cases. Other sites may name or summarise them; if a summary disagrees,
94
+ this section governs, with the source functions above the ultimate
95
+ authority.
96
+
97
+ Why the MODEL can differ:
98
+
99
+ - **M1 — a live failover holds the thread.** After a successful
100
+ `modelFailover` the worker stays on its fallback, and a dispatch
101
+ that omits `model` keeps it: the revert to the session's opening
102
+ model stands down (BG16). The hold ends when the session is
103
+ disposed, and when a later non-`openOnly` route moves it — an
104
+ explicit `model` on either router state, or the thread's routed
105
+ base while the router is ON. An OFF pre-router pin is `openOnly`
106
+ and does not release it. A reopen ends it too, except in M6.
107
+ *Code:* the `failover-held` keep in `decideModelSwitch`
108
+ (`route.ts`); `applyRoute` clears the marker after a plan-driven
109
+ `setModel`, and `liveFailoverModel` reports it (`threads.ts`).
110
+ - **M2 — a failover fires inside the dispatch.** When the attempt
111
+ fails with a model-API error, Slate switches the live session to
112
+ the mapped model and re-prompts once. One episode covers both
113
+ attempts, and its `ran:` line names the model the action ended on
114
+ rather than the planned one. *Code:* the `isFailoverCandidate`
115
+ block in `runDispatchInner` (`threads.ts`) and `planFailoverSwitch`,
116
+ guard 7 (`route.ts`).
117
+ - **M3 — the revert was attempted and failed.** A dispatch that names
118
+ no model switches the session back to the model it opened on. When
119
+ that spec no longer resolves, or `setModel` throws, Slate warns and
120
+ leaves the session on the model the previous action left it on
121
+ (BG24). A PLAN-driven switch that fails is the opposite case: the
122
+ dispatch aborts, nothing runs and no episode is written. M7 is the
123
+ case where no revert is attempted at all. *Code:* `giveUp` inside
124
+ `applyRoute` (`threads.ts`).
125
+ - **M4 — the window guard substitutes** (router ON). A model that
126
+ cannot hold the thread's context is replaced by the widest listed
127
+ candidate, and only when one is STRICTLY wider than the model it
128
+ would replace. When nothing listed is wider, the action runs on the
129
+ original model and a warning says pi will compact. It never blocks
130
+ the action, and `substitutedFrom` records a replacement. *Code:*
131
+ `strictlyWider` in `planRoute`'s guard 5 (`route.ts`).
132
+ - **M5 — the thread's base was not routable** (router ON). A base
133
+ that is absent or has fallen off the list is re-seeded to a listed
134
+ candidate with a warning, so a model-less dispatch runs on the
135
+ seeded base and not on the value the record carried. *Code:* the
136
+ re-seed branch of `planRoute` with `defaultBase` (`route.ts`),
137
+ written back by `persistReseededBase` (`threads.ts`).
138
+ - **M6 — a reopened session restores a model from its file** (CQ3).
139
+ Slate passes the reopening model explicitly whenever the dispatch
140
+ resolves one, and that overrides the session file. When the
141
+ dispatch resolves no model at all and the host session has none
142
+ either, pi restores the file's last model change instead, which can
143
+ be a fallback M1 was holding. The `live=` marker is cleared on every
144
+ reopen, so in this one shape the fallback outlives its marker.
145
+ *Code:* `opts.model ? resolveModel(ctx, opts.model) : ctx.model` in
146
+ `openWorkerSession` (`worker.ts`); the marker delete in
147
+ `openWorkerFor` and the CQ3 note on `liveFailoverModel`
148
+ (`threads.ts`).
149
+ - **M7 — nothing was captured to return to.** When the worker session
150
+ reported no model as it opened, there is no revert target, and a
151
+ dispatch that names no model leaves the session where it is with no
152
+ switch attempted. The effort axis has the same keep when no opening
153
+ level was captured. *Code:* `NO_SESSION_BASELINE` and the
154
+ `no-baseline` keeps in `decideModelSwitch` and `decideEffortSwitch`,
155
+ fed by `captureSessionBaseline` (`route.ts`).
156
+
157
+ Why the LEVEL can differ. There is no counterpart to M1 here: a
158
+ dispatch that names no `effort` returns the session to the level it
159
+ opened on. M7 and E6 are known cases in which it does not.
160
+
161
+ - **E1 — pi clamps a level Slate cannot judge.** When the ladder for
162
+ the judged model is unreadable — no profile, a throwing lookup, a
163
+ non-array ladder — the guards stand down and the level goes to pi,
164
+ which clamps it to something the model offers. Slate reports
165
+ nothing, because it has no evidence to report. *Code:*
166
+ `checkEffortFor` and `guardEffort`'s `ladderKnown` (`route.ts`).
167
+ - **E2 — M4 moved the model under an explicit level.** A level that
168
+ is not valid on the substituted model is DROPPED with a warning
169
+ rather than refusing the action — a context size must never
170
+ hard-block a dispatch — and the session's opening level applies.
171
+ *Code:* the `soft` branch of `guardEffort` (`route.ts`).
172
+ - **E3 — a stored base level no longer holds.** A base effort is a
173
+ cached derivation over a table that ships with Slate. When today's
174
+ table no longer reads it as `ok`, it is silently re-derived for
175
+ that model: nobody asked for that level, so a stale cache is
176
+ Slate's to correct rather than news to report. *Code:* the stored
177
+ base-effort branch of `planRoute`, with `lowestMeasuredEffort`
178
+ (`route.ts`).
179
+ - **E4 — the action's model is not the base model.** An explicit
180
+ `model`, or M4, drops the inherited base level and derives the new
181
+ model's own lowest measured level instead. A level derived for one
182
+ model is never carried onto another. *Code:* the effort resolution
183
+ in `planRoute`, keyed on `judgedModel` (`route.ts`).
184
+ - **E5 — M2 re-clamps the level.** The failover route requests no
185
+ level, and pi re-clamps the session's current level for the
186
+ fallback model. *Code:* `session.setModel(mapped)` in the failover
187
+ block (`threads.ts`); `openWorkerSession` passes the settings
188
+ default explicitly at every open for the same reason (`worker.ts`).
189
+ - **E6 — the level was judged for one model and another model runs.**
190
+ When M1, M3 or M7 leaves the session on a model other than the plan
191
+ target, the level the plan resolved is still applied, and pi clamps
192
+ it to that model's own ladder with nothing reported.
193
+ `effortJudgedFor` names the model the judgement was about, which is
194
+ then not the model that ran. *Code:* the effort switch in
195
+ `applyRoute`, which runs after a model `keep` (`threads.ts`), and
196
+ `decideEffortSwitch` (`route.ts`).
197
+
71
198
  ## Configuration
72
199
 
73
200
  `router` lives in `.pi/slate.json` and, like the rest of that file,
@@ -196,7 +323,8 @@ The vocabulary is pi's ladder and nothing else: `off`, `minimal`,
196
323
  actually offers is per model — the shipped table records a ladder
197
324
  per model id, not a family rule.
198
325
 
199
- **An omitted `effort` never resolves to a fixed default.** In order:
326
+ **An omitted `effort` never resolves to a fixed default.** With the
327
+ router ON, in order:
200
328
 
201
329
  1. the thread's stored base effort — but only when the action runs
202
330
  on the thread's base model, and only while that stored level
@@ -205,8 +333,8 @@ per model id, not a family rule.
205
333
  the ladder, or onto the provider's rejection list) is silently
206
334
  re-derived; nobody asked for it, so a stale cache is Slate's
207
335
  problem to correct rather than news to report;
208
- 2. otherwise the **lowest measured level of the model that actually
209
- runs** — the lowest level on its ladder that carries a traced
336
+ 2. otherwise the **lowest measured level of the model it routes to**
337
+ — the lowest level on that model's ladder that carries a traced
210
338
  capability measurement. A level derived for one model is never
211
339
  carried onto another, so an explicit `model`, or a window
212
340
  substitution, re-derives it;
@@ -214,14 +342,19 @@ per model id, not a family rule.
214
342
  the level the worker session opened on applies, which is pi's own
215
343
  settings default — never a level another action left behind.
216
344
 
345
+ With the router OFF Slate resolves no level at all, so that same
346
+ opening level applies to every action that omits `effort`.
347
+
217
348
  So a higher level is only ever reached by naming it: pass `effort`
218
349
  explicitly on the dispatch. A derived level is measured by
219
350
  construction and therefore cannot trip the effort guards; only an
220
351
  explicit one can.
221
352
 
222
- An explicit level is judged against the model the action will
223
- actually run on, and the difference between a warning and a refusal
224
- is:
353
+ An explicit level is judged against the model the action routes to,
354
+ which `effortJudgedFor` names. [Known cases where the model or level
355
+ differs](#known-cases-where-the-model-or-level-differs) collects what
356
+ can move either afterwards. The difference between a warning and a
357
+ refusal is:
225
358
 
226
359
  - **evidence gap** (the level is on that model's ladder but no
227
360
  traced source reports a capability result there) — advisory. The
@@ -230,8 +363,8 @@ is:
230
363
  `router.allowUnmeasuredEffort: false` turns this one case into a
231
364
  hard refusal for the whole project;
232
365
  - **off the model's ladder** — refused, whenever the ladder is
233
- known. pi would otherwise clamp the level silently and the
234
- orchestrator would believe the action ran at a level the model
366
+ known, outside E2. pi would otherwise clamp the level silently and
367
+ the orchestrator would believe the action ran at a level the model
235
368
  never offered;
236
369
  - **rejected outright by the provider** — refused, always. Such a
237
370
  level is still on pi's ladder for the model (the table records the
@@ -300,26 +433,27 @@ model's ladder is unreadable.
300
433
  its decision to make), the same lines in the live progress output,
301
434
  and `details.ranModel` / `details.ranEffort` /
302
435
  `details.ranEffortUnmeasured` / `details.warnings` for a renderer.
303
- - **In the `threads` listing:** `base=<model>@<level>?` — what a
304
- dispatch that omits `model` will run on. The trailing `?` marks
436
+ - **In the `threads` listing:** `base=<model>@<level>?` — the nominal
437
+ plan target when a dispatch omits `model`. The trailing `?` marks
305
438
  the level, not the model, as provisional: it is a stored default,
306
- re-checked on every dispatch. Then `last=<model>@<level>` — what the thread's
307
- last action actually ran on, with `(unmeasured)` where that
308
- applies — and `live=<model> (failover)` while a failover holds the
309
- live session.
439
+ re-validated against the model's current capability data on every
440
+ dispatch and silently re-derived if it no longer holds. `last=` is
441
+ the model and level that the last action actually ran on, with
442
+ `(unmeasured)` where that applies. `live=<model> (failover)` means
443
+ a held fallback currently overrides the nominal base for the live
444
+ session.
310
445
  - **At the session level:** the resolution's own warnings, as UI
311
446
  notifications (console when no UI is attached).
312
447
  - **In the orchestrator's own system prompt, every turn:** the
313
448
  doctrine gains a routing rule — a table with one row per routable
314
449
  model, plus the rules for reading it. This is the surface you do
315
- not see, and it is the router's standing cost: 1,941 characters /
316
- 20 lines for six configured models, 2,519 / 23 for all nine — a
317
- model row costs 154–186 characters, plus a one-off legend clause
318
- for each marker it introduces (listing the first cheap-tier model
319
- costs ~50 characters beyond its row, which is why six→nine
320
- averages 193 per model against the first six's 171). Configuring
321
- the router roughly doubles Slate's always-loaded block (1,929
322
- characters with routing off). Those are PORTABLE characters — the
450
+ not see, and it is the router's standing cost: 1,950 characters /
451
+ 20 added doctrine lines for six configured models, 2,505 / 23 for
452
+ all nine. In the current snapshot a model row costs 146–183
453
+ characters, plus a one-off legend clause for each marker it
454
+ introduces. Configuring the router roughly doubles Slate's
455
+ always-loaded block (1,929 characters with routing and writing
456
+ off). Those are PORTABLE characters — the
323
457
  doctrine with each occurrence of the installed `docs/` directory
324
458
  removed, filenames kept — because the doctrine embeds absolute doc
325
459
  paths and its raw size therefore depends on where the package is
@@ -519,13 +653,16 @@ concern; a green suite says nothing about it.
519
653
  dispatch guard and anything else that consults the router agree.
520
654
  A key or provider added mid-session, and an edit to
521
655
  `slate.json`, both need a new pi session.
522
- - **Router OFF is not "no effort checking".** With an empty list
523
- there is no model list, no window guard, no billing notice, no
524
- seeded base and no substitution — but an explicit `effort` is
525
- still judged against the shipped profile for the model that runs,
526
- when pi's registry can serve that model. The `model` argument
527
- itself is passed through byte-for-byte, so a malformed spec still
528
- produces pi's own error.
656
+ - **Router OFF disables candidate routing, not action arguments or
657
+ failover.** With an empty list there is no model list, window
658
+ guard, billing notice, seeded base or candidate substitution, and
659
+ Slate derives no effort level. A model-less plan targets the
660
+ thread's pre-router pin, else nothing, so the session returns to the
661
+ model it opened on outside M1, M3 and M7, and to the level it opened
662
+ on outside M7 and E6. An explicit `effort` is still judged against
663
+ the shipped profile for the plan target when pi's registry can serve
664
+ that model. The `model` argument itself is passed through
665
+ byte-for-byte, so a malformed spec still produces pi's own error.
529
666
  - **A mid-thread model switch costs the prompt cache.** A long
530
667
  thread runs overwhelmingly on cache reads; routing a small action
531
668
  to a cheaper model on a cold cache can cost more than staying on