ytdb-slate 0.6.0 → 0.6.1

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
@@ -69,6 +69,8 @@ The option has prompt cost. Each orchestrator system prompt contains the doctrin
69
69
 
70
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
71
 
72
+ 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`.
73
+
72
74
  ## Install
73
75
 
74
76
  Install into a project (pinned, project-scoped — recorded in `.pi/settings.json`):
@@ -95,6 +97,8 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
95
97
  | `workerPromptDocs` | string[] | `[]` | Project markdown files whose **contents** are appended to every worker-thread system prompt. |
96
98
  | `workflow.draftPRs` | boolean | `false` | Enable umbrella draft-PR publishing for tracks. |
97
99
  | `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). |
100
+ | `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). |
101
+ | `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
102
  | `doctrineExtraPath` | string | — | Project markdown whose **content** is appended to the orchestrator doctrine (project-specific workflow additions). |
99
103
  | `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
104
  | `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). |
@@ -112,7 +116,11 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
112
116
  "orchestratorPromptDocs": ["docs/agents/orchestrator-guidelines.md"],
113
117
  "workerPromptDocs": ["docs/agents/thread-guidelines.md"],
114
118
  "workflow": { "draftPRs": true },
115
- "writing": { "check": true },
119
+ "writing": {
120
+ "check": true,
121
+ "remind": false,
122
+ "remindPercent": 10
123
+ },
116
124
  "doctrineExtraPath": "docs/agents/workflow-additions.md",
117
125
  "reviewPerspectivesPath": "docs/agents/review-perspectives.md",
118
126
  "modelFailover": { "anthropic/claude-sonnet-5": "openai/gpt-5.2" },
@@ -120,6 +128,8 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
120
128
  }
121
129
  ```
122
130
 
131
+ Older Slate releases warn about these reminder keys and ignore them. Use a Slate release whose documentation includes these keys.
132
+
123
133
  > **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
134
 
125
135
  **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:
@@ -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.
@@ -1,15 +1,14 @@
1
1
  # Writing guidance and the prose checker
2
2
 
3
- Slate ships one writing convention and one command that measures
4
- prose against a mechanical proxy for it. Both are opt-in: with
5
- `writing.check` absent or false nothing about them is loaded,
6
- rendered or paid for.
3
+ Slate ships an optional writing convention and a command that measures
4
+ prose against a mechanical proxy. With `writing.check` absent or false,
5
+ Slate loads no writing guidance. The command remains available.
7
6
 
8
7
  This document is reference documentation, not workflow doctrine. It
9
8
  is the definition of record for the rule set, the severity classes,
10
- the command line and the caps. The always-loaded doctrine rule states
11
- the convention in a few lines and cites this file for everything
12
- else, so that a session pays for the detail only when it reads it.
9
+ the command line and the caps. The enabled doctrine rule states the
10
+ convention in a few lines and cites this file for everything else.
11
+ A session pays for the detail only when it reads this file.
13
12
 
14
13
  ## What the checker is, and what it is not
15
14
 
@@ -52,25 +51,37 @@ accuracy, structure or completeness at all.
52
51
 
53
52
  ## Enabling it
54
53
 
55
- One boolean in the project's `.pi/slate.json`:
54
+ Configure writing features in the project's `.pi/slate.json`:
56
55
 
57
56
  ```json
58
- { "writing": { "check": true } }
57
+ {
58
+ "writing": {
59
+ "check": true,
60
+ "remind": false,
61
+ "remindPercent": 10
62
+ }
63
+ }
59
64
  ```
60
65
 
61
- The default is false. An absent `writing` key is silent. A malformed
62
- one warns once and leaves the feature off, as does a non-boolean
63
- `check`; unknown keys under `writing` warn and are ignored. The
64
- validator is `sanitizeWritingConfig` in `extension/writing.ts`.
66
+ All three values are optional. `check` and `remind` default to false.
67
+ `remindPercent` defaults to 10. `remind` is inert unless `check` is
68
+ true.
65
69
 
66
- Slate reads project config only for a TRUSTED project. Both
67
- prompt-visible sites also re-check trust when they inject the doctrine
68
- rule or worker preamble. The worker check is defense in depth for a
69
- future direct call to `openWorkerSession`; the current config path is
70
- already gated in `extension/index.ts`. An untrusted project therefore
71
- behaves as if the switch were off, whatever its `slate.json` says.
70
+ A malformed `writing` value warns once and uses all defaults. A
71
+ non-boolean `check` or `remind` value warns and uses its default.
72
+ Unknown keys under `writing` also warn and are ignored.
72
73
 
73
- The switch turns on three things and nothing else:
74
+ `remindPercent` must be a finite number in `(0, 100]`. An invalid
75
+ value warns and defaults to 10. The validator is
76
+ `sanitizeWritingConfig` in `extension/writing.ts`.
77
+
78
+ Slate reads project config only for a TRUSTED project. The doctrine,
79
+ worker preamble, and reminder gates also check trust at their injection
80
+ points. These checks provide defense in depth because
81
+ `extension/index.ts` already gates the current config path. An
82
+ untrusted project behaves as if the switches were off.
83
+
84
+ The `check` switch turns on three things:
74
85
 
75
86
  1. **The doctrine rule.** One numbered rule is appended to the
76
87
  orchestrator's system prompt while orchestrator mode is on. It
@@ -80,26 +91,136 @@ The switch turns on three things and nothing else:
80
91
  preamble. With the switch off the preamble is byte-identical to
81
92
  its pre-feature text.
82
93
  3. **The turn status line.** In an interactive session, the Slate
83
- status line gains a `writing <n>/<m>` counter: `m` turns of the
84
- orchestrator's own prose were measured and `n` of them carried at
85
- least one `fail` class match.
94
+ status line gains a `writing <n>/<m>` counter. `m` counts measured
95
+ prose turns. `n` counts turns with at least one `fail` finding.
96
+
97
+ The separate `remind` switch adds a hidden, context-paced message.
98
+ Context-paced means Slate waits for configured context growth between
99
+ reminders. It works only while `check` remains on. The next section
100
+ defines this channel and its gates.
101
+
102
+ Running the command by hand needs no config. It is a plain Node
103
+ command and works whether either switch is on.
104
+
105
+ ## Writing requirements and reminders
106
+
107
+ The doctrine includes these six requirements in this order:
108
+
109
+ - Avoid idioms.
110
+ - Replace bare-reference openers with the subject they reference.
111
+ - Explain each project-specific term at first use.
112
+ - Define each abbreviation at first use.
113
+ - Express one idea in each sentence.
114
+ - Use one term for each concept.
115
+
116
+ These project-authored summaries came from the investigation for
117
+ [issue #96](https://github.com/JetBrains/ytdb-slate/issues/96). ASD-STE100
118
+ informed that work. Slate copied no standard text or controlled
119
+ vocabulary, and it claims no conformance.
120
+
121
+ A reminder repeats five lines. It omits this doctrine-only line:
122
+
123
+ `Express one idea in each sentence.`
124
+
125
+ The sentence-length rules provide a nearby mechanical signal. They do
126
+ not test idea count. The reminder then includes this exact scope guard:
127
+
128
+ `Exclude research logs, worker task text, and the project's own agent instruction file.`
129
+
130
+ This guard prevents the five requirements from applying to those
131
+ excluded artifacts.
132
+
133
+ These requirements are not mechanical checker findings. The shipped
134
+ checker has no rule for idioms or bare-reference openers. It also has
135
+ no rule for project-term explanations or abbreviation definitions.
136
+ It cannot enforce one term per concept.
137
+
138
+ After an eligible tool result, Slate queues a hidden custom message
139
+ with active `steer`. The message has `display: false` and arrives
140
+ before the next assistant response.
86
141
 
87
- Running the command by hand needs no config at all. It is a plain
88
- Node command and it works whether or not the switch is on.
142
+ The normal UI has no reminder indicator. It does not appear in the
143
+ normal TUI or tool panel. A tool-free round receives no reminder.
144
+
145
+ To diagnose delivery, inspect the session JSONL through pi. A delivered
146
+ reminder has entry type `custom_message`, `customType` set to
147
+ `slate-writing-reminder`, and `display` set to false. It also enters
148
+ later model context. This diagnostic does not depend on a stable
149
+ session-file path.
150
+
151
+ Every reminder gate must be open:
152
+
153
+ - orchestrator mode is on
154
+ - the project is trusted
155
+ - `writing.check` and `writing.remind` are true
156
+ - Slate is not paused
157
+ - the context threshold is reached, or a handoff forces the next reminder
158
+ - no reminder has been sent in the current assistant response round
159
+
160
+ Slate permits at most one reminder for each assistant response round.
161
+ Parallel tool results therefore cannot produce repeated reminders in
162
+ one round.
163
+
164
+ The interval is `remindPercent` of Slate's current effective context
165
+ budget. Slate rounds down to whole tokens, then applies an 8,192-token
166
+ floor. It applies no interval cap.
167
+
168
+ The default interval is 10 percent. The configured range is greater
169
+ than zero through 100, inclusive. The effective budget reflects the
170
+ live model, configured budget, context window, and Slate's handoff
171
+ headroom.
172
+
173
+ The 8,192-token floor makes sufficiently small percentages equivalent.
174
+ At 100 percent, normal cadence reaches its threshold at the effective
175
+ pause boundary. These endpoints remain valid. A forced post-handoff
176
+ reminder bypasses the cadence threshold.
177
+
178
+ After a reminder, Slate marks the current context usage. Another
179
+ reminder needs one full interval of growth. When context usage shrinks,
180
+ Slate lowers the mark to the new usage before measuring growth again.
181
+
182
+ A trusted handoff reloads the doctrine in the fresh session. It also
183
+ forces a reminder after the first eligible tool result. This forced
184
+ reminder does not need context usage or a reached threshold.
185
+
186
+ A representative 256,000-token budget gives a 25,600-token default
187
+ interval. This example only illustrates cadence. Slate has no
188
+ controlled comparison or claim that reminders improve prose.
189
+
190
+ ## Keeping the requirement text synchronized
191
+
192
+ `WRITING_REQUIREMENTS` and `WRITING_SCOPE_EXCLUSION` in
193
+ `extension/writing-reminder.ts` export the authoritative wording.
194
+ The complete six-line roster has three manual copies: this guide,
195
+ AGENTS.md's roster, and resolver exact fixtures. The integration canary
196
+ manually copies only the five reminder-eligible lines.
197
+
198
+ The scope exclusion has three exact copies: this guide, resolver exact
199
+ fixtures, and the integration canary. AGENTS.md carries equivalent
200
+ scope prose, not an exact copy. All applicable copies must update in
201
+ one commit.
202
+
203
+ `extension/mode.ts` renders the authoritative exports directly. The
204
+ shell harness reads expected text from canary evidence. Neither file
205
+ holds a manual wording copy.
89
206
 
90
207
  ## What it costs when it is on
91
208
 
92
209
  The doctrine rule is part of every orchestrator system prompt, so its
93
210
  cost is paid on every turn. The worker addition is paid once per
94
- worker dispatch. `context-budget.md` defines the portable-character
95
- measure, records the whole-doctrine sizes and owns the size budget.
211
+ worker dispatch. Each sent reminder also enters later model context.
212
+ `context-budget.md` defines the portable-character measure. It records
213
+ the whole-doctrine sizes and owns the size budget.
96
214
 
97
215
  An interactive session also runs the checker synchronously after each
98
216
  completed assistant message. The hook refuses text over 16 KiB and
99
217
  reports `writing skipped (message too large)`. This TUI bound is
100
- separate from the command's 1 MiB input cap. The hook is human-only
101
- telemetry: it does not change the model input, and a checker failure
102
- reports `writing unavailable` rather than failing the turn.
218
+ separate from the command's 1 MiB input cap.
219
+
220
+ The checker hook is human-only telemetry. It does not change model
221
+ input. A checker failure reports `writing unavailable` instead of
222
+ failing the turn. The optional reminder uses the separate model-visible
223
+ channel described above.
103
224
 
104
225
  ## What counts as prose
105
226
 
@@ -312,8 +433,9 @@ These are settled decisions. Read them as scope, not as a backlog.
312
433
  - **No verdict.** No rule class is a defect by itself, and no clean
313
434
  run is a pass. `review-rules.md` owns the mapping from a match to a
314
435
  severity, and a reviewer owns the judgement.
315
- - **Not a gate.** The turn hook is telemetry. It returns nothing to
316
- the model, changes no prompt, and cannot fail a turn.
436
+ - **Not a gate.** The checker hook is telemetry. It changes no model
437
+ input and cannot fail a turn. The optional reminder is guidance,
438
+ not a checker verdict.
317
439
  - **Not universal.** The convention covers prose written for people:
318
440
  documents, README text, pull request and commit text, issues,
319
441
  review comments, release notes and messages to the user. Research
@@ -92,6 +92,7 @@ interface PendingHandoff {
92
92
 
93
93
  export interface SlateHandoffHooks {
94
94
  startHandoff(ctx: ExtensionCommandContext, focus?: string): Promise<void>;
95
+ effectiveContextBudget(contextWindow: number, ctx: ExtensionContext): number | undefined;
95
96
  }
96
97
 
97
98
  function pendingFile(cwd: string): string {
@@ -224,6 +225,24 @@ function resolveBudgetTokens(budget: ContextBudgetObject | undefined, modelSpec:
224
225
  return DEFAULT_CONTEXT_BUDGET_TOKENS;
225
226
  }
226
227
 
228
+ /** Apply Slate's context-window clamp to one configured budget. */
229
+ export function effectiveContextBudgetTokens(configured: number, contextWindow: number, reserveTokens: number): number {
230
+ return Math.min(
231
+ configured,
232
+ Math.max(contextWindow - reserveTokens - BRIEF_HEADROOM_TOKENS, Math.ceil(contextWindow / 2)),
233
+ );
234
+ }
235
+
236
+ /** Resolve one model's configured budget and apply the shared window clamp. */
237
+ export function effectiveContextBudgetForModel(
238
+ budget: ContextBudgetObject | undefined,
239
+ modelSpec: string,
240
+ contextWindow: number,
241
+ reserveTokens: number,
242
+ ): number {
243
+ return effectiveContextBudgetTokens(resolveBudgetTokens(budget, modelSpec), contextWindow, reserveTokens);
244
+ }
245
+
227
246
  /** Last assistant message text on the current branch (the handoff brief). */
228
247
  function lastAssistantText(ctx: ExtensionCommandContext): string {
229
248
  let text = "";
@@ -315,6 +334,17 @@ export function registerSlateHandoff(
315
334
  return cachedReserveTokens;
316
335
  };
317
336
 
337
+ const effectiveContextBudget = (contextWindow: number, ctx: ExtensionContext): number | undefined => {
338
+ if (!ctx.model) return undefined;
339
+ const budget = getConfig().contextBudget;
340
+ return effectiveContextBudgetForModel(
341
+ typeof budget === "number" ? { tokens: budget } : budget,
342
+ `${ctx.model.provider}/${ctx.model.id}`,
343
+ contextWindow,
344
+ reserveTokens(ctx),
345
+ );
346
+ };
347
+
318
348
  /** Handoff instructions shared by both pause sites (turn check + compaction intercept). */
319
349
  const pauseInstructions = (headline: string) =>
320
350
  [
@@ -357,15 +387,12 @@ export function registerSlateHandoff(
357
387
  typeof budget === "number" ? { tokens: budget } : budget,
358
388
  `${ctx.model.provider}/${ctx.model.id}`,
359
389
  );
360
- // Clamp to the window: the pause must fire BEFORE pi's compaction point
361
- // (contextWindow − reserveTokens) with room to write the brief — but
362
- // never below half the window, so a small-window model still gets a
363
- // usable orchestrator instead of a near-zero effective budget.
364
- // (usage.contextWindow > 0 is guaranteed: getContextUsage returns
365
- // undefined when no window is known.)
366
- const effective = Math.min(
367
- configured,
368
- Math.max(usage.contextWindow - reserveTokens(ctx) - BRIEF_HEADROOM_TOKENS, Math.ceil(usage.contextWindow / 2)),
390
+ // Use the same effective budget exposed to context-cadenced features.
391
+ const effective = effectiveContextBudgetForModel(
392
+ typeof budget === "number" ? { tokens: budget } : budget,
393
+ `${ctx.model.provider}/${ctx.model.id}`,
394
+ usage.contextWindow,
395
+ reserveTokens(ctx),
369
396
  );
370
397
  if (usage.tokens < effective) return;
371
398
  const used = usage.tokens.toLocaleString("en-US");
@@ -472,6 +499,8 @@ export function registerSlateHandoff(
472
499
  const matches = !!pending.parentSession && pending.parentSession === ctx.sessionManager.getHeader()?.parentSession;
473
500
  if (!matches) return;
474
501
  store.adoptSnapshot(pending.snapshot, ctx);
502
+ store.writingReminder.forceNext = true;
503
+ store.writingReminder.adoptedThisSessionStart = true;
475
504
  store.paused = false;
476
505
  store.save();
477
506
  rmSync(file, { force: true });
@@ -686,5 +715,5 @@ export function registerSlateHandoff(
686
715
  }
687
716
  };
688
717
 
689
- return { startHandoff };
718
+ return { startHandoff, effectiveContextBudget };
690
719
  }
package/extension/mode.ts CHANGED
@@ -32,6 +32,20 @@ import {
32
32
  import { loadPromptDocs } from "./prompt-docs.ts";
33
33
  import { THINKING_LEVELS } from "./route.ts";
34
34
  import { orchestratorCostUsd, type SlateConfig, type SlateStore } from "./state.ts";
35
+ import {
36
+ claimWritingReminder,
37
+ commitWritingReminder,
38
+ decideWritingReminder,
39
+ rearmWritingReminder,
40
+ renderWritingDoctrineRequirements,
41
+ renderWritingReminderMessage,
42
+ renderWritingScopeExclusion,
43
+ resetWritingReminderSession,
44
+ WRITING_REMINDER_CUSTOM_TYPE,
45
+ writingReminderDeliveryDetails,
46
+ writingReminderGateOpen,
47
+ writingReminderInterval,
48
+ } from "./writing-reminder.ts";
35
49
  import { measureWritingTurn, type WritingChecker, type WritingCounters } from "./writing.ts";
36
50
  import type { WorkerExtensionSet, WorkerExtensionUnit } from "./worker-extensions.ts";
37
51
 
@@ -406,12 +420,14 @@ ${n}. Check all user-facing prose before delivery. Use short, active sentences a
406
420
  A sentence over 20 words warns. Shorten it when meaning stays clear.
407
421
 
408
422
  Do not use semicolons or contractions. Keep exact technical terms. The check
409
- does not test vocabulary.
423
+ does not test vocabulary. Follow these writing requirements:
424
+
425
+ ${renderWritingDoctrineRequirements(" ")}
410
426
 
411
- Apply it to README text, documentation, code comments, and pull request text.
412
- Apply it to commit bodies, issues, review comments, release notes, and messages
413
- to the user. Exclude research logs, worker task text, and the project's own agent
414
- instruction file.
427
+ Apply these requirements to README text, documentation, code comments, and
428
+ pull request text. Apply them to commit bodies, issues, review comments,
429
+ release notes, and messages to the user.
430
+ ${renderWritingScopeExclusion(" ")}
415
431
 
416
432
  Rules, limits and the checker command:
417
433
  ${WRITING_GUIDANCE_DOC}
@@ -662,6 +678,71 @@ export function registerSlateMode(
662
678
  return { systemPrompt: event.systemPrompt + parts.join("") };
663
679
  });
664
680
 
681
+ // Each assistant response opens one reminder slot for its following tools.
682
+ // A prior queue claim that never started delivery is retryable in this slot.
683
+ pi.on("message_end", (event) => {
684
+ if (event.message.role === "assistant") {
685
+ Object.assign(store.writingReminder, rearmWritingReminder(store.writingReminder));
686
+ }
687
+ });
688
+
689
+ // message_start proves that pi began delivering our custom steer.
690
+ pi.on("message_start", (event) => {
691
+ if (event.message.role === "custom" && event.message.customType === WRITING_REMINDER_CUSTOM_TYPE) {
692
+ Object.assign(
693
+ store.writingReminder,
694
+ commitWritingReminder(store.writingReminder, event.message.details, event.message.content),
695
+ );
696
+ }
697
+ });
698
+
699
+ // Claim the round before sendMessage can synchronously trigger other hooks.
700
+ pi.on("tool_result", (_event, ctx) => {
701
+ const config = getConfig().writing;
702
+ const runtime = store.writingReminder;
703
+ if (
704
+ !writingReminderGateOpen(
705
+ {
706
+ orchestratorMode: store.orchestratorMode,
707
+ trusted: ctx.isProjectTrusted(),
708
+ check: config?.check === true,
709
+ remind: config?.remind === true,
710
+ paused: store.paused,
711
+ },
712
+ runtime.sentThisRound,
713
+ )
714
+ ) {
715
+ return;
716
+ }
717
+ const usage = ctx.getContextUsage();
718
+ const effectiveBudget = usage ? hooks.effectiveContextBudget(usage.contextWindow, ctx) : undefined;
719
+ const interval =
720
+ effectiveBudget === undefined ? undefined : writingReminderInterval(effectiveBudget, config?.remindPercent ?? 10);
721
+ const decision = decideWritingReminder(runtime.markTokens, usage?.tokens, interval, runtime.forceNext);
722
+ const reminderContent = renderWritingReminderMessage();
723
+ Object.assign(runtime, claimWritingReminder(runtime, decision, reminderContent));
724
+ if (!decision.send) return;
725
+ const deliveryId = runtime.pending?.deliveryId;
726
+ if (deliveryId === undefined) {
727
+ Object.assign(runtime, rearmWritingReminder(runtime));
728
+ return;
729
+ }
730
+ try {
731
+ pi.sendMessage(
732
+ {
733
+ customType: WRITING_REMINDER_CUSTOM_TYPE,
734
+ content: reminderContent,
735
+ display: false,
736
+ details: writingReminderDeliveryDetails(deliveryId),
737
+ },
738
+ { deliverAs: "steer" },
739
+ );
740
+ } catch {
741
+ // A synchronous queue failure leaves force and cadence retryable.
742
+ if (runtime.pending) Object.assign(runtime, rearmWritingReminder(runtime));
743
+ }
744
+ });
745
+
665
746
  // The writing checker reads only the completed assistant message from
666
747
  // turn_end. It returns no hook result, so it is human-only telemetry and
667
748
  // cannot alter what the model receives.
@@ -704,9 +785,11 @@ export function registerSlateMode(
704
785
  writingCounters.findingTurns = 0;
705
786
  writingCheckerPromise = undefined;
706
787
  writingStatus = "fresh";
707
- // store.restore() (index.ts) and pending-handoff adoption (handoff.ts)
708
- // ran before this handler in registration order; re-apply the persisted
709
- // mode to the fresh runtime.
788
+ Object.assign(store.writingReminder, resetWritingReminderSession(store.writingReminder));
789
+ // Preserve force only when the earlier handoff handler marked this cycle.
790
+ // The reset consumes that marker, so a later generic session_start clears
791
+ // stale force. Registration order remains index restore, handoff, then mode.
792
+ // Re-apply the persisted mode to the fresh runtime.
710
793
  //
711
794
  // Config-driven default: seed orchestrator mode ON for a genuinely FRESH
712
795
  // interactive session. Running AFTER restore and handoff adoption means
@@ -18,6 +18,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
18
18
  // below on why that matters).
19
19
  import type { ThinkingLevel } from "./model-profiles.ts";
20
20
  import { sanitizeForNotify } from "./notify.ts";
21
+ import { createWritingReminderRuntime, type WritingReminderRuntime } from "./writing-reminder.ts";
21
22
 
22
23
  /**
23
24
  * ADDITIVE TOLERANCE (the persistence model has no migration hook): the
@@ -481,9 +482,11 @@ export interface RouterConfig {
481
482
  allowUnmeasuredEffort?: boolean;
482
483
  }
483
484
 
484
- /** Optional writing checks. Off unless `check` is explicitly true. */
485
+ /** Optional writing checks and context-cadenced reminders. */
485
486
  export interface WritingConfig {
486
487
  check?: boolean;
488
+ remind?: boolean;
489
+ remindPercent?: number;
487
490
  }
488
491
 
489
492
  export interface SlateConfig {
@@ -519,6 +522,8 @@ export class SlateStore {
519
522
  workerCostUsd = 0;
520
523
  /** Orchestrator spend inherited from ancestor sessions across handoffs. */
521
524
  carriedCostUsd = 0;
525
+ /** Session-instance reminder state. It is never part of a snapshot. */
526
+ readonly writingReminder: WritingReminderRuntime = createWritingReminderRuntime();
522
527
  /** Invoked after every save/restore; used by mode.ts to refresh the widget. */
523
528
  onDidChange?: () => void;
524
529
 
@@ -0,0 +1,184 @@
1
+ /** One writing requirement and the channels that render it. */
2
+ export interface WritingRequirement {
3
+ text: string;
4
+ reminder: boolean;
5
+ }
6
+
7
+ /** The ordered source of truth for writing doctrine and reminders. */
8
+ export const WRITING_REQUIREMENTS: readonly WritingRequirement[] = Object.freeze([
9
+ Object.freeze({ text: "Avoid idioms.", reminder: true }),
10
+ Object.freeze({ text: "Replace bare-reference openers with the subject they reference.", reminder: true }),
11
+ Object.freeze({ text: "Explain each project-specific term at first use.", reminder: true }),
12
+ Object.freeze({ text: "Define each abbreviation at first use.", reminder: true }),
13
+ Object.freeze({ text: "Express one idea in each sentence.", reminder: false }),
14
+ Object.freeze({ text: "Use one term for each concept.", reminder: true }),
15
+ ]);
16
+
17
+ /** The shared exclusion guard for doctrine and hidden reminders. */
18
+ export const WRITING_SCOPE_EXCLUSION =
19
+ "Exclude research logs, worker task text, and the project's own agent instruction file.";
20
+
21
+ export const WRITING_REMINDER_CUSTOM_TYPE = "slate-writing-reminder";
22
+
23
+ export interface WritingReminderDeliveryDetails {
24
+ deliveryId: number;
25
+ }
26
+
27
+ export interface PendingWritingReminder {
28
+ deliveryId: number;
29
+ nextMarkTokens: number;
30
+ consumeForce: boolean;
31
+ expectedContent: string;
32
+ }
33
+
34
+ export interface WritingReminderRuntime {
35
+ markTokens: number;
36
+ sentThisRound: boolean;
37
+ forceNext: boolean;
38
+ deliverySequence: number;
39
+ adoptedThisSessionStart: boolean;
40
+ pending?: PendingWritingReminder;
41
+ }
42
+
43
+ export interface WritingReminderGate {
44
+ orchestratorMode: boolean;
45
+ trusted: boolean;
46
+ check: boolean;
47
+ remind: boolean;
48
+ paused: boolean;
49
+ }
50
+
51
+ export interface WritingReminderDecision {
52
+ send: boolean;
53
+ nextMarkTokens: number;
54
+ }
55
+
56
+ export function createWritingReminderRuntime(): WritingReminderRuntime {
57
+ return {
58
+ markTokens: 0,
59
+ sentThisRound: false,
60
+ forceNext: false,
61
+ deliverySequence: 0,
62
+ adoptedThisSessionStart: false,
63
+ };
64
+ }
65
+
66
+ /** Convert a configured percentage of the effective budget to a token cadence. */
67
+ export function writingReminderInterval(effectiveBudgetTokens: number, remindPercent: number): number {
68
+ return Math.max(8_192, Math.floor((effectiveBudgetTokens * remindPercent) / 100));
69
+ }
70
+
71
+ /** Decide whether policy gates permit a reminder before checking its cadence. */
72
+ export function writingReminderGateOpen(gate: WritingReminderGate, sentThisRound: boolean): boolean {
73
+ return gate.orchestratorMode && gate.trusted && gate.check && gate.remind && !gate.paused && !sentThisRound;
74
+ }
75
+
76
+ /**
77
+ * Decide one cadence step. A smaller trustworthy usage lowers the mark.
78
+ * Force sends without usage and leaves the mark unchanged.
79
+ */
80
+ export function decideWritingReminder(
81
+ markTokens: number,
82
+ usageTokens: number | null | undefined,
83
+ intervalTokens: number | undefined,
84
+ forceNext: boolean,
85
+ ): WritingReminderDecision {
86
+ const trustworthyUsage =
87
+ typeof usageTokens === "number" && Number.isFinite(usageTokens) && usageTokens >= 0 ? usageTokens : undefined;
88
+ const loweredMark = trustworthyUsage === undefined ? markTokens : Math.min(markTokens, trustworthyUsage);
89
+ if (forceNext) return { send: true, nextMarkTokens: trustworthyUsage ?? loweredMark };
90
+ if (trustworthyUsage === undefined || intervalTokens === undefined) {
91
+ return { send: false, nextMarkTokens: loweredMark };
92
+ }
93
+ const send = trustworthyUsage >= loweredMark + intervalTokens;
94
+ return { send, nextMarkTokens: send ? trustworthyUsage : loweredMark };
95
+ }
96
+
97
+ /** Claim a round before queueing. Cadence and force remain uncommitted. */
98
+ export function claimWritingReminder(
99
+ runtime: WritingReminderRuntime,
100
+ decision: WritingReminderDecision,
101
+ expectedContent: string,
102
+ ): WritingReminderRuntime {
103
+ if (!decision.send) return { ...runtime, markTokens: decision.nextMarkTokens };
104
+ const deliveryId = runtime.deliverySequence + 1;
105
+ return {
106
+ ...runtime,
107
+ sentThisRound: true,
108
+ deliverySequence: deliveryId,
109
+ pending: { deliveryId, nextMarkTokens: decision.nextMarkTokens, consumeForce: runtime.forceNext, expectedContent },
110
+ };
111
+ }
112
+
113
+ export function writingReminderDeliveryDetails(deliveryId: number): WritingReminderDeliveryDetails {
114
+ return { deliveryId };
115
+ }
116
+
117
+ /** Match pi's hidden details and exact content to the current queue claim. */
118
+ export function writingReminderDeliveryMatches(
119
+ runtime: WritingReminderRuntime,
120
+ details: unknown,
121
+ content: unknown,
122
+ ): boolean {
123
+ if (!runtime.pending || typeof details !== "object" || details === null) return false;
124
+ try {
125
+ return (
126
+ (details as { deliveryId?: unknown }).deliveryId === runtime.pending.deliveryId &&
127
+ content === runtime.pending.expectedContent
128
+ );
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ /** Commit cadence and force when pi starts the matching custom message. */
135
+ export function commitWritingReminder(
136
+ runtime: WritingReminderRuntime,
137
+ details: unknown,
138
+ content: unknown,
139
+ ): WritingReminderRuntime {
140
+ if (!writingReminderDeliveryMatches(runtime, details, content)) return runtime;
141
+ return {
142
+ ...runtime,
143
+ markTokens: runtime.pending?.nextMarkTokens ?? runtime.markTokens,
144
+ forceNext: runtime.pending?.consumeForce ? false : runtime.forceNext,
145
+ pending: undefined,
146
+ };
147
+ }
148
+
149
+ /** Open the next round and discard any queue claim that never started delivery. */
150
+ export function rearmWritingReminder(runtime: WritingReminderRuntime): WritingReminderRuntime {
151
+ return { ...runtime, sentThisRound: false, pending: undefined };
152
+ }
153
+
154
+ /** Reset session cadence and preserve force only for this cycle's adoption. */
155
+ export function resetWritingReminderSession(runtime: WritingReminderRuntime): WritingReminderRuntime {
156
+ return {
157
+ markTokens: 0,
158
+ sentThisRound: false,
159
+ forceNext: runtime.adoptedThisSessionStart && runtime.forceNext,
160
+ deliverySequence: runtime.deliverySequence,
161
+ adoptedThisSessionStart: false,
162
+ pending: undefined,
163
+ };
164
+ }
165
+
166
+ export function renderWritingDoctrineRequirements(indent = ""): string {
167
+ return WRITING_REQUIREMENTS.map((requirement) => `${indent}- ${requirement.text}`).join("\n");
168
+ }
169
+
170
+ export function renderWritingScopeExclusion(indent = ""): string {
171
+ return `${indent}${WRITING_SCOPE_EXCLUSION}`;
172
+ }
173
+
174
+ export function renderWritingReminder(): string {
175
+ const requirements = WRITING_REQUIREMENTS.filter((requirement) => requirement.reminder)
176
+ .map((requirement) => `- ${requirement.text}`)
177
+ .join("\n");
178
+ return `${requirements}\n\n${WRITING_SCOPE_EXCLUSION}`;
179
+ }
180
+
181
+ /** The complete hidden custom-message content. */
182
+ export function renderWritingReminderMessage(): string {
183
+ return `[slate] Writing reminder:\n${renderWritingReminder()}`;
184
+ }
@@ -71,21 +71,17 @@ export function measureWritingTurn(
71
71
  }
72
72
 
73
73
  /** The known `writing` keys. Report anything else as a likely typo. */
74
- const WRITING_KEYS = ["check"];
74
+ const WRITING_KEYS = ["check", "remind", "remindPercent"];
75
75
 
76
- /**
77
- * Validate the raw `writing` config value. An absent value silently returns the
78
- * default. A wrong shape warns once and returns the default. Unknown keys and a
79
- * wrong-typed `check` value warn, while valid input is copied into a new object.
80
- */
76
+ /** Validate the raw `writing` config value and apply independent defaults. */
81
77
  export function sanitizeWritingConfig(raw: unknown, warn: (msg: string) => void): Required<WritingConfig> {
82
- const defaults: Required<WritingConfig> = { check: false };
78
+ const defaults: Required<WritingConfig> = { check: false, remind: false, remindPercent: 10 };
83
79
  if (raw === undefined) return defaults;
84
80
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
85
81
  warn('slate: ignoring writing — expected an object like { "check": true }');
86
82
  return defaults;
87
83
  }
88
- const value = raw as { check?: unknown };
84
+ const value = raw as { check?: unknown; remind?: unknown; remindPercent?: unknown };
89
85
 
90
86
  const unknownKeys = Object.keys(value).filter((key) => !WRITING_KEYS.includes(key));
91
87
  if (unknownKeys.length > 0) {
@@ -96,22 +92,36 @@ export function sanitizeWritingConfig(raw: unknown, warn: (msg: string) => void)
96
92
  );
97
93
  }
98
94
 
99
- let rawCheck: unknown;
100
- if (Object.prototype.hasOwnProperty.call(value, "check")) {
95
+ const read = (key: keyof typeof value): unknown => {
96
+ if (!Object.prototype.hasOwnProperty.call(value, key)) return undefined;
101
97
  try {
102
- rawCheck = value.check;
98
+ return value[key];
103
99
  } catch {
104
- warn("slate: ignoring writing.check — could not read the value (defaulting to false)");
100
+ const fallback = key === "remindPercent" ? 10 : false;
101
+ warn(`slate: ignoring writing.${key} — could not read the value (defaulting to ${fallback})`);
102
+ return undefined;
105
103
  }
106
- }
107
- let check = false;
108
- if (rawCheck !== undefined) {
109
- if (typeof rawCheck !== "boolean") {
110
- warn("slate: ignoring writing.check — expected true or false (defaulting to false)");
104
+ };
105
+ const booleanValue = (key: "check" | "remind", fallback: boolean): boolean => {
106
+ const candidate = read(key);
107
+ if (candidate === undefined) return fallback;
108
+ if (typeof candidate === "boolean") return candidate;
109
+ warn(`slate: ignoring writing.${key} — expected true or false (defaulting to ${fallback})`);
110
+ return fallback;
111
+ };
112
+
113
+ const check = booleanValue("check", defaults.check);
114
+ const requestedRemind = booleanValue("remind", defaults.remind);
115
+ const rawPercent = read("remindPercent");
116
+ let remindPercent = defaults.remindPercent;
117
+ if (rawPercent !== undefined) {
118
+ if (typeof rawPercent === "number" && Number.isFinite(rawPercent) && rawPercent > 0 && rawPercent <= 100) {
119
+ remindPercent = rawPercent;
111
120
  } else {
112
- check = rawCheck;
121
+ warn("slate: ignoring writing.remindPercent — expected a finite number in (0, 100] (defaulting to 10)");
113
122
  }
114
123
  }
115
124
 
116
- return { check };
125
+ // A reminder without the checker is disabled at the config boundary.
126
+ return { check, remind: check && requestedRemind, remindPercent };
117
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ytdb-slate",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Slate: thread-weaving orchestration for the pi coding agent — bounded worker actions, compressed episodes, mandatory research/design/adversarial/track review workflow, configurable model failover, and orchestrator context-budget handoff.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {