ytdb-slate 0.6.0 → 0.7.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,6 +1,6 @@
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)
3
+ [![CI status](https://github.com/JetBrains/ytdb-slate/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/JetBrains/ytdb-slate/actions/workflows/ci.yml)
4
4
 
5
5
  Slate is a thread-weaving orchestration extension for the [pi coding agent](https://pi.dev).
6
6
 
@@ -57,6 +57,14 @@ Slate can ride through model API outages. The opt-in `modelFailover` map in `.pi
57
57
 
58
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.
59
59
 
60
+ Router startup warnings have two classes. One class is a configuration fault. It reports project configuration that Slate ignored or dropped. A configuration fault also covers problems the user can stop by adding a model, credential, or failover entry.
61
+
62
+ The addition must go into their project configuration or pi credentials. Removing the model named by a warning does not meet this second test. Slate always shows configuration faults.
63
+
64
+ The other class is a model data note. It reports shipped research data that project configuration cannot correct. Slate hides those notes by default.
65
+
66
+ With this repository's six-model list and pi's stock registry, Slate hides eleven notes. Slate shows one discoverability line instead. A local registry override can change that count. The line names the hidden warning count and the `router.showWarnings` setting. A hidden note can still affect which model runs an action.
67
+
60
68
  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.
61
69
 
62
70
  ## Optional writing guidance
@@ -69,6 +77,8 @@ The option has prompt cost. Each orchestrator system prompt contains the doctrin
69
77
 
70
78
  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
79
 
80
+ Set `writing.remind` to `true` to add context-paced reminders after eligible tool results. Context-paced means Slate waits for configured context growth between reminders. The reminders are hidden from the normal TUI. They repeat five writing requirements and the scope exclusion before the next assistant response. This option remains off unless `writing.check` is also `true`.
81
+
72
82
  ## Install
73
83
 
74
84
  Install into a project (pinned, project-scoped — recorded in `.pi/settings.json`):
@@ -95,12 +105,15 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
95
105
  | `workerPromptDocs` | string[] | `[]` | Project markdown files whose **contents** are appended to every worker-thread system prompt. |
96
106
  | `workflow.draftPRs` | boolean | `false` | Enable umbrella draft-PR publishing for tracks. |
97
107
  | `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). |
108
+ | `writing.remind` | boolean | `false` | Send a hidden reminder after an eligible tool result. All gates must pass: orchestrator mode, project trust, `writing.check`, this option, no pause, cadence or handoff force, and one-per-round control. See [`docs/writing-guidance.md`](docs/writing-guidance.md). |
109
+ | `writing.remindPercent` | number | `10` | Set cadence as a percentage of Slate's current effective context budget. The value must be finite and in `(0, 100]`. The interval has an 8,192-token floor and no cap. A handoff force bypasses this threshold. See [`docs/writing-guidance.md`](docs/writing-guidance.md). |
98
110
  | `doctrineExtraPath` | string | — | Project markdown whose **content** is appended to the orchestrator doctrine (project-specific workflow additions). |
99
111
  | `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. |
100
112
  | `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). |
101
113
  | `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). |
102
114
  | `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). |
103
115
  | `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. |
116
+ | `router.showWarnings` | boolean | `false` | Reveal model data notes about Slate's shipped routing research. Configuration faults are never hidden. See [`docs/model-routing.md`](docs/model-routing.md). |
104
117
 
105
118
  Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point them at markdown files that actually exist in **your** project):
106
119
 
@@ -112,7 +125,11 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
112
125
  "orchestratorPromptDocs": ["docs/agents/orchestrator-guidelines.md"],
113
126
  "workerPromptDocs": ["docs/agents/thread-guidelines.md"],
114
127
  "workflow": { "draftPRs": true },
115
- "writing": { "check": true },
128
+ "writing": {
129
+ "check": true,
130
+ "remind": false,
131
+ "remindPercent": 10
132
+ },
116
133
  "doctrineExtraPath": "docs/agents/workflow-additions.md",
117
134
  "reviewPerspectivesPath": "docs/agents/review-perspectives.md",
118
135
  "modelFailover": { "anthropic/claude-sonnet-5": "openai/gpt-5.2" },
@@ -120,6 +137,8 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
120
137
  }
121
138
  ```
122
139
 
140
+ Older Slate releases warn about these reminder keys and ignore them. Use a Slate release whose documentation includes these keys.
141
+
123
142
  > **Silent skip:** the project-file keys fail silently — no error is shown. For the content-injected keys (`orchestratorPromptDocs`, `workerPromptDocs`, `doctrineExtraPath`) a missing, unreadable, or empty file is skipped and nothing is injected. For `reviewPerspectivesPath` the pointer is omitted only when the file is missing — the file is not read at injection time, so an unreadable or empty file is still cited. Verify your paths after copying the example.
124
143
 
125
144
  **Worker extensions (`workerExtensions`).** By default worker threads load no extensions. This key is a list of regex patterns that select extensions the **host session has already loaded** and load them into every worker too. Each pattern is matched **unanchored** (unlike `contextBudget.overrides`, which is anchored) against a load unit's recorded source spec (e.g. `npm:pi-web-search@1.3.1`), its load-unit path, or the entry path of any tool that unit contributes, so a bare package name matches:
@@ -158,7 +177,7 @@ In orchestrator mode, Slate appends a short **doctrine** (a block of numbered ru
158
177
  - `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)
159
178
  - `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
179
  - `docs/writing-guidance.md` — the optional writing convention, `writing.check` behavior, status line, and checker CLI
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.
180
+ - `docs/model-routing.md` — the action-level model routing reference. It covers the three `router` keys, model eligibility, omitted effort, and dispatch guards. It also covers first-session warnings. The doctrine cites this absolute path only while the routing rule renders. This path is fourth normally and fifth when `workflow.draftPRs` is on. The rule renders the live list because it depends on the session registry and credentials.
162
181
 
163
182
  Project-specific additions layer on top — they extend, not replace, the shipped doctrine — via two distinct mechanisms:
164
183
 
@@ -108,6 +108,32 @@ Rationale: the clamp guarantees the pause fires before pi's
108
108
  auto-compaction and leaves room to write the handoff brief; the
109
109
  half-window floor keeps small-window models usable.
110
110
 
111
+ ## Writing-reminder cadence
112
+
113
+ The effective budget also sets the optional hidden writing-reminder cadence.
114
+ When `writing.check` and `writing.remind` are both true, Slate computes:
115
+
116
+ ```
117
+ interval = max(8192, floor(effectiveBudget × remindPercent / 100))
118
+ ```
119
+
120
+ `remindPercent` defaults to 10 and accepts a finite value in `(0, 100]`. The
121
+ context-window clamp therefore shortens the cadence on a model whose configured
122
+ budget does not fit. Slate checks the cadence after tool results. One reminder
123
+ slot can fire after each assistant response.
124
+
125
+ A successful handoff adoption sets `forceNext`. The next eligible tool result
126
+ then bypasses token cadence and missing usage. It does not bypass trust,
127
+ orchestrator mode, `writing.check`, `writing.remind`, or pause gates. Slate
128
+ commits the cadence mark only when pi starts delivery of the custom message.
129
+
130
+ The current hidden message is 320 ASCII characters, including its `[slate]`
131
+ header, five reminder requirements, blank separator, and scope exclusion. That
132
+ is about 80 tokens at four characters per token. This count is for the exact
133
+ rendered production string, without JSONL framing or provider-role overhead. The
134
+ message enters conversation context only when a reminder fires. Later requests
135
+ resend it with the rest of the conversation.
136
+
111
137
  ## Compaction policy
112
138
 
113
139
  In orchestrator mode, for budget-driven configs only: a
@@ -150,34 +176,36 @@ construction and makes the arithmetic one multiplication:
150
176
  `doctrine-budget` in `verification/resolver-checks.mjs` is the
151
177
  definition of record for this convention and its enforced bounds.
152
178
  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:
179
+ the same shipped-profile fixture as that check. The six-model rows
180
+ use the six models in this repository's `.pi/slate.json`. Every row
181
+ states its full basis, because router state, model count, `draftPRs`,
182
+ and `writing.check` each move the number. Rows with the same fixture
183
+ and basis must agree with `verification/README.md`. A different basis
184
+ must be named instead of presented as the same measurement:
157
185
 
158
186
  | router | models | `draftPRs` | `writing.check` | paths | portable | lines |
159
187
  | --- | --- | --- | --- | --- | --- | --- |
160
188
  | off | — | off | off | 3 | 1,929 | 38 |
161
- | off | — | off | on | 4 | 2,683 | 53 |
189
+ | off | — | off | on | 4 | 2,999 | 60 |
162
190
  | 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 |
191
+ | off | — | on | on | 5 | 3,018 | 60 |
192
+ | on | `.pi/slate.json` six | off | off | 4 | 3,879 | 58 |
193
+ | on | `.pi/slate.json` six | off | on | 5 | 4,949 | 80 |
194
+ | on | `.pi/slate.json` six | on | off | 5 | 3,898 | 58 |
195
+ | on | `.pi/slate.json` six | on | on | 6 | 4,968 | 80 |
168
196
  | on | all 9 shipped | off | off | 4 | 4,434 | 61 |
169
- | on | all 9 shipped | off | on | 5 | 5,188 | 76 |
197
+ | on | all 9 shipped | off | on | 5 | 5,504 | 83 |
170
198
  | on | all 9 shipped | on | off | 5 | 4,453 | 61 |
171
- | on | all 9 shipped | on | on | 6 | 5,207 | 76 |
199
+ | on | all 9 shipped | on | on | 6 | 5,523 | 83 |
172
200
 
173
201
  An untrusted project reads the first row whatever its `slate.json`
174
202
  says: no project config is loaded, so no optional rule renders. Line
175
203
  counts do not vary with the install path. Enabling `draftPRs` costs
176
204
  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.
205
+ `writing.check` costs 1,070 portable characters, adds 22 lines to the
206
+ whole doctrine, and adds one embedded path. Considered alone, the
207
+ writing rule is 1,070 portable characters / 23 lines. Its leading
208
+ newline becomes the separator when appended.
181
209
 
182
210
  Two parts of the block grow with configuration rather than with the
183
211
  path:
@@ -191,20 +219,56 @@ path:
191
219
  legend adds a one-off clause per marker it has to explain, so
192
220
  growth is not only the sum of new rows.
193
221
  - The worker-extension rule grows per whitelisted extension and per
194
- tool that extension contributes, so it has no fixed size; one
222
+ tool that extension contributes, so it has no fixed size. One
195
223
  extension contributing two tools measured 373 portable characters
196
224
  / 7 lines.
197
225
 
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.
226
+ The table above isolates the shipped rules. Two representative bases include
227
+ worker extensions and support verification decisions:
228
+
229
+ | basis | models | worker extensions | paths | portable | lines | rough tokens |
230
+ | --- | ---: | --- | ---: | ---: | ---: | ---: |
231
+ | current `.pi/slate.json` dogfood config | 6 | real 2 units / 4 tools | 6 | 5,884 | 90 | ≈1,471 |
232
+ | stable maximal fixture | 9 | synthetic 2 units / 4 tools, every rendered field at its cap | 6 | 6,870 | 93 | ≈1,718 |
233
+
234
+ The dogfood row uses `workflow.draftPRs: true`, `writing.check: true`, and the
235
+ six models in `.pi/slate.json`. Its extension basis is
236
+ `pi-smart-fetch@0.3.12` plus `pi-web-search@1.3.1`. Those packages resolve to two
237
+ units and four tools. Their worker-extension rule is 916 portable characters /
238
+ 11 lines. Raw size remains symbolic: `portable + 6 × length(installed docs
239
+ directory)`. No maintainer checkout path belongs in this shipped document.
240
+
241
+ The stable maximal row uses all nine shipped profiles, draft PRs, and writing.
242
+ Its direct post-resolution worker fixture has two units and four tools. Unit
243
+ labels are 128 characters. Tool names are 64 characters. Descriptions are 140
244
+ characters.
245
+
246
+ Those are the renderer caps. The fixture uses safe ASCII letters. It is
247
+ independent of installed extension labels, descriptions, versions and other
248
+ prose. The worker rule measures 1,347 characters against its 1,600-character
249
+ verification budget.
250
+
251
+ `doctrine-budget` caps that representative maximal doctrine at 7,200 portable
252
+ characters. The measured 6,870 leaves 330 characters, or 4.6 percent of the
253
+ 7,200-character bound. A positive control adds one capped tool
254
+ and three copies of the largest measured model row. It measures 7,634 portable
255
+ characters and exceeds the bound by 434. These figures are verification budgets,
256
+ never runtime limits. Arbitrary user extension rosters can exceed them.
257
+
258
+ The exact values are maintenance tripwires. A deliberate doctrine wording,
259
+ requirement roster, renderer cap, or fixture change requires a fresh render
260
+ through `doctrine-budget`. Update its exact expectations and every published
261
+ figure in the same commit. Keep the positive control steps unchanged unless the
262
+ fixture design itself changes.
263
+
264
+ Against a 256,000-token context budget, these blocks remain small. At four
265
+ characters per token as a rough estimate, the shipped-rule table ranges from
266
+ about 482 tokens to about 1,381 tokens. The current dogfood basis is about 1,471
267
+ tokens. The representative maximum is about 1,718 tokens, or 0.67 percent of the
268
+ default budget.
269
+
270
+ No tokenizer was run, and tables are denser than prose. The block is re-sent on every request rather than paid once. These figures show how
271
+ much headroom Slate consumes before conversation content.
208
272
 
209
273
  ### Worker writing preamble
210
274
 
@@ -317,9 +381,8 @@ number.
317
381
  compaction can land in the same cycle and the handoff brief is
318
382
  written from compacted context — episodes and thread state survive
319
383
  on disk, so handoff still functions.
320
- - The doctrine sizes above cover the shipped rules only. A project
321
- that also injects `doctrineExtraPath` or `orchestratorPromptDocs`,
322
- or whitelists worker extensions, pays for those on top in the same
323
- always-loaded position, and Slate measures none of it against the
324
- budget separately — it simply arrives as context the budget then
325
- has less room for.
384
+ - The doctrine sizes above cover the stated fixtures only. A project that
385
+ injects `doctrineExtraPath` or `orchestratorPromptDocs` pays for those on top.
386
+ A larger worker-extension roster also adds context beyond the representative
387
+ fixtures. Slate imposes no runtime doctrine-size limit. This content simply
388
+ leaves less room under the context budget.
@@ -199,7 +199,7 @@ opened on. M7 and E6 are known cases in which it does not.
199
199
 
200
200
  `router` lives in `.pi/slate.json` and, like the rest of that file,
201
201
  is honored **only in trusted projects** — an untrusted project loads
202
- no project config, so the router stays off. There are exactly two
202
+ no project config, so the router stays off. There are exactly three
203
203
  keys:
204
204
 
205
205
  ```json
@@ -213,7 +213,8 @@ keys:
213
213
  "anthropic/claude-opus-5",
214
214
  "anthropic/claude-fable-5"
215
215
  ],
216
- "allowUnmeasuredEffort": true
216
+ "allowUnmeasuredEffort": true,
217
+ "showWarnings": false
217
218
  }
218
219
  }
219
220
  ```
@@ -222,6 +223,7 @@ keys:
222
223
  | --- | --- | --- | --- |
223
224
  | `models` | array of `"provider/id"` strings | `[]` | the closed candidate list; empty or absent = router OFF |
224
225
  | `allowUnmeasuredEffort` | boolean | `true` | `false` turns an evidence gap from a warning into a refusal |
226
+ | `showWarnings` | boolean | `false` | `true` shows model data notes in addition to the always-visible configuration faults |
225
227
 
226
228
  Validation, all of it at session start so a broken list surfaces
227
229
  before a dispatch is refused by it:
@@ -237,6 +239,9 @@ before a dispatch is refused by it:
237
239
  entries in the same list still apply.
238
240
  - A non-boolean `allowUnmeasuredEffort` is warned about and treated
239
241
  as `true`.
242
+ - A non-boolean `showWarnings` is warned about as a configuration
243
+ fault and treated as `false`. The fault remains visible despite
244
+ that fallback.
240
245
 
241
246
  The config is read once, at session start. Editing `slate.json`
242
247
  mid-session takes effect only in a new pi session.
@@ -249,24 +254,54 @@ the credentials pi actually has configured. Resolution happens once
249
254
  per session, lazily at the first consultation, and the answer is
250
255
  then frozen for that session.
251
256
 
252
- | what happens | outcome | warning |
253
- | --- | --- | --- |
254
- | entry is not a canonical `provider/id` | dropped | names the entry and the defect |
255
- | no profile in Slate's model profiles | dropped | "has no benchmark data … excluding it from routing" |
256
- | two entries resolve to the same profile (an alias or a case variant) | first kept, second dropped | names both spellings and the profile |
257
- | the same spec listed twice | first kept | none (silent) |
258
- | pi's model registry does not know it | dropped | "routing there could only produce billed failures" |
259
- | pi has no usable credentials configured for it | dropped | same wording, credentials variant |
260
- | every entry dropped | router turns OFF | one summary line naming the count |
261
- | no usable input price for today's date | kept, ordered last | "its cost cannot be compared" |
262
- | no usable effort ladder in the profile | kept | "every explicit effort level for it will read as off-ladder" |
263
- | profile's context window differs from the registry's | kept (registry value used) | see [Expected first-session warnings](#expected-first-session-warnings) |
264
- | profile names unknown routing-critical fields | kept | "routing decisions for it are provisional", naming the fields |
265
- | no `modelFailover` entry for a candidate | kept | one aggregate line naming every uncovered candidate |
266
-
267
- Every warning is emitted at most once per session and reaches the
268
- same channel as the other config warnings: a UI notification when a
269
- UI is attached, the console otherwise.
257
+ The class test has two independent conditions. A warning is a
258
+ **configuration fault** when EITHER condition holds:
259
+
260
+ 1. Slate ignored or dropped part of the user's configuration.
261
+ 2. The user can stop the warning by ADDING something to their own
262
+ project config or pi credentials, such as a model, credential or
263
+ failover entry. This differs from only removing the model named
264
+ by the warning.
265
+
266
+ Every other warning is a **model data note**. The single-part test
267
+ that asks only whether a user can stop a warning does not partition
268
+ the classes: removing a dropped `router.models` entry can silence its
269
+ warning, but removal is not the ADD remedy in condition 2. Condition
270
+ 1 catches every silently ignored or dropped config value first.
271
+
272
+ | what happens | outcome | warning fragment | class |
273
+ | --- | --- | --- | --- |
274
+ | entry is not a canonical `provider/id` | dropped | "It is not a canonical \"provider/id\" model spec. Reason:" | configuration fault |
275
+ | no profile in Slate's model profile table | dropped | "has no entry in slate's model profile table" | configuration fault |
276
+ | two entries resolve to the same profile (an alias or a case variant) | first kept, second dropped | "name the same profiled model" | configuration fault |
277
+ | the same spec is listed twice | first kept | none (silent) | — |
278
+ | pi's model registry does not know it | dropped | "is not in pi's model registry. Slate drops it from routing." | configuration fault |
279
+ | pi has no usable credentials configured for it | dropped | "has no usable credentials configured in pi. Slate drops it from routing." | configuration fault |
280
+ | every entry is dropped | router turns OFF | "survived validation. The warnings above name each dropped entry" | configuration fault |
281
+ | no usable input price exists for today's date | kept, ordered last | "Slate cannot compare its cost with the other models." | model data note |
282
+ | no usable effort ladder exists in the profile | kept | "Such a level passes through to pi, which clamps it" | model data note |
283
+ | profile context window differs from the registry window | kept, registry value used | "differs between two sources" | model data note |
284
+ | the first profile names unknown routing-critical fields | kept | "picks models from a research table shipped inside slate" | model data note |
285
+ | a profile names unknown routing-critical fields | kept | "model fact that slate could not trace to a source" or "model facts that slate could not trace to a source" | model data note |
286
+ | a reportable profile/registry context-window divergence exists, the registry figure is not the profile's recorded known-divergence figure, and the registry figure equals the model's own long-context billing threshold | kept | "context window equal to the model's own long-context billing threshold" | model data note |
287
+ | no `modelFailover` entry exists for a candidate | kept | "routable models have no modelFailover entry" | configuration fault |
288
+ | every configured candidate is marked non-preferred | cheapest candidate becomes the base | "profiles mark every configured model as one it must never pick by itself" | configuration fault |
289
+ | resolution throws | router turns OFF | "routing is disabled. The router could not resolve its model list" | configuration fault |
290
+
291
+ Every warning is emitted at most once per session. The complete
292
+ resolution retains every warning regardless of display filtering.
293
+ With `router.showWarnings: true`, every warning reaches the same
294
+ channel as the other config warnings: a UI notification when a UI is
295
+ attached, the console otherwise. With the default `false`, every
296
+ configuration fault still reaches that channel and model data notes
297
+ do not.
298
+
299
+ When at least one note is hidden, Slate emits one discoverability
300
+ line after resolution. It gives the hidden warning count, names
301
+ `router.showWarnings`, and says that a hidden warning can affect
302
+ which model runs an action. The count is warnings, not physical
303
+ display lines: one warning can wrap across several lines. No
304
+ discoverability line appears when nothing was hidden.
270
305
 
271
306
  Half a list is still a routing policy, so partial drops leave the
272
307
  router ON. Nothing surviving is not a policy: the router turns OFF
@@ -463,60 +498,78 @@ model's ladder is unreadable.
463
498
 
464
499
  ## Expected first-session warnings
465
500
 
466
- **A batch of warnings on the first routed session is expected UX,
467
- not a bug.** With a six-model list on a stock pi install, resolution
468
- emits eleven lines, broken down below. They are not repeated: every
469
- warning fires at most once per session, and the resolution is frozen
470
- after the first consultation.
471
-
472
- In orchestrator mode that first consultation is the DOCTRINE BUILD,
501
+ The stock count below is a render over this repository's
502
+ `.pi/slate.json`, the shipped profile table, and the provider data
503
+ inside the pinned pi 0.83.0 package. It does not read the live model
504
+ registry. The packaged OpenAI data at
505
+ `@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json`
506
+ reports a **272,000-token** context window for
507
+ `openai/gpt-5.6-luna`, `openai/gpt-5.6-terra` and
508
+ `openai/gpt-5.6-sol`. The packaged Anthropic data reports 1,000,000
509
+ for each configured Anthropic model. The render treated all six as
510
+ authenticated, matching this repository's configured-auth state.
511
+
512
+ The shipped profiles record 1,050,000 tokens for those three OpenAI
513
+ models. Each stock registry value therefore produces a context-window
514
+ divergence note. The same 272,000-token value equals each model's
515
+ long-context billing threshold, so one aggregate billing-pattern note
516
+ also fires.
517
+
518
+ Resolution emits **11 warnings: 0 configuration faults and 11 model
519
+ data notes**. The default `router.showWarnings: false` therefore
520
+ shows **0 of those warnings** and one discoverability line. That line
521
+ begins `slate: there are 11 hidden warnings in the model router.`
522
+ Enabling the option shows all 11 warnings and no discoverability
523
+ line. Each warning fires at most once per session, and the resolution
524
+ is frozen after the first consultation.
525
+
526
+ | warnings | class | condition keys | why | shown by default |
527
+ | --- | --- | --- | --- | --- |
528
+ | 1 | model data note | `w3-explainer` | explains the shipped research table before the first unknown-data warning | 0 |
529
+ | 6 | model data note | one `w3:string:<JSON spec>` for each configured model | each profile names model facts with no traced source, or conflicting figures with no adjudication | 0 |
530
+ | 3 | model data note | one `w1:string:<JSON spec>` for each configured OpenAI model | each stock registry window differs from its profile window | 0 |
531
+ | 1 | model data note | `w1-billing-pattern` | aggregates the three registry windows that equal their models' billing thresholds | 0 |
532
+ | **11** | **all model data notes** | — | stock measured total | **0 warnings; 1 discoverability line** |
533
+
534
+ The stock emission order is:
535
+
536
+ 1. `w1:string:"openai/gpt-5.6-luna"`
537
+ 2. `w3-explainer`
538
+ 3. `w3:string:"openai/gpt-5.6-luna"`
539
+ 4. `w1:string:"openai/gpt-5.6-terra"`
540
+ 5. `w3:string:"openai/gpt-5.6-terra"`
541
+ 6. `w1:string:"openai/gpt-5.6-sol"`
542
+ 7. `w3:string:"openai/gpt-5.6-sol"`
543
+ 8. `w3:string:"anthropic/claude-sonnet-5"`
544
+ 9. `w3:string:"anthropic/claude-opus-5"`
545
+ 10. `w3:string:"anthropic/claude-fable-5"`
546
+ 11. `w1-billing-pattern`
547
+
548
+ This count depends on pi's registry data and any local registry
549
+ override. This machine's `~/.pi/agent/models.json` overrides the
550
+ three OpenAI windows to 1,050,000 tokens. A live render here therefore
551
+ suppresses the three divergence notes and the billing-pattern note,
552
+ leaving **7 model data notes**. That seven-note result describes this
553
+ machine, not a stock install.
554
+
555
+ The configured failover map covers all six candidates, so the
556
+ failover-coverage condition does not fire in either render. Preferred
557
+ candidates remain in the list, so the non-preferred-base condition
558
+ does not fire either.
559
+
560
+ In orchestrator mode the first consultation is the DOCTRINE BUILD,
473
561
  not a dispatch: the orchestrator's system prompt carries the
474
- routable model table, and it is assembled when your first message
475
- starts a turn. So the whole batch can arrive in a session where you
476
- typed nothing but "hi" and dispatched nothing at all — that is the
477
- router reading its inputs for the session, not a dispatch
478
- misfiring. (Outside orchestrator mode no doctrine is built, so the
479
- first dispatch triggers it instead. The config-shape warnings above
480
- land earlier still, at session start.)
481
-
482
- The breakdown:
483
-
484
- | lines | class | why |
485
- | --- | --- | --- |
486
- | 6 | unknown routing data | each of the six profiles names routing-critical fields with no traced value, or with two published values and no adjudication, so routing decisions for them are provisional |
487
- | 3 | context-window divergence | the three `openai/gpt-5.6-*` models — see below |
488
- | 1 | billing-pattern note | one aggregate line naming those three models |
489
- | 1 | failover coverage | one aggregate line, when `modelFailover` covers none of them |
490
-
491
- The window divergence is real, and both sides are stated without
492
- adjudication. On a stock install pi's registry reports **272,000**
493
- tokens for `openai/gpt-5.6-luna`, `-terra` and `-sol`, while their
494
- profiles record **1,050,000** (with the profile's asOf date in the
495
- same line). Routing uses the REGISTRY figure — that is a fact about
496
- this code, and it is stated as such — but which figure is factually
497
- correct is not established by the router: it sees two numbers and no
498
- provenance for either.
499
-
500
- The registry's 272,000 is also exactly those models' long-context
501
- BILLING threshold, so one further line says so once, naming all
502
- three: a window equal to its own threshold would leave the
503
- long-context price tier unreachable, which is the
504
- billing-row-restated-as-a-capacity pattern. That is suggestive, not
505
- proof, which is why it is a pointer and not a verdict.
506
-
507
- **Why Slate does not "fix" this by changing its own number.** The
508
- research pass that produced this table explicitly withdrew 272,000
509
- as a capacity figure: in the source it appears only inside a pricing
510
- row, as a boundary on input tokens. Restating it as a window is the
511
- fabrication that pass caught and corrected. Editing the profile to
512
- match the registry would silence the warning by asserting something
513
- the evidence does not support, and would suppress a divergence that
514
- is currently true. The honest options are the two you already have:
515
- leave it (routing uses the registry number, which is the
516
- conservative one) or raise the registry window deliberately in pi's
517
- custom-models file, after which the warning stops on its own —
518
- `context-budget.md` documents that opt-in and the billing
519
- consequences of running above the threshold.
562
+ routable model table, and it is assembled when the first message
563
+ starts a turn. Outside orchestrator mode the first dispatch triggers
564
+ resolution instead. Config-shape warnings land earlier, at session
565
+ start.
566
+
567
+ A hidden warning can still alter which model runs an action. Context-
568
+ window divergence is the concrete case, and the stock registry emits
569
+ three such hidden notes for this list. Routing uses the registry
570
+ window, and that value drives the context-window substitution guard.
571
+ `router.showWarnings` changes display only; it does not change the
572
+ resolution or the guard input.
520
573
 
521
574
  ## What the router does NOT enforce
522
575