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 +11 -1
- package/docs/context-budget.md +96 -33
- package/docs/writing-guidance.md +154 -32
- package/extension/handoff.ts +39 -10
- package/extension/mode.ts +91 -8
- package/extension/state.ts +6 -1
- package/extension/writing-reminder.ts +184 -0
- package/extension/writing.ts +29 -19
- package/package.json +1 -1
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": {
|
|
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:
|
package/docs/context-budget.md
CHANGED
|
@@ -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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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,
|
|
189
|
+
| off | — | off | on | 4 | 2,999 | 60 |
|
|
162
190
|
| off | — | on | off | 4 | 1,948 | 38 |
|
|
163
|
-
| off | — | on | on | 5 |
|
|
164
|
-
| on |
|
|
165
|
-
| on |
|
|
166
|
-
| on |
|
|
167
|
-
| on |
|
|
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,
|
|
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,
|
|
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
|
|
178
|
-
whole doctrine, and adds one embedded path
|
|
179
|
-
writing rule is
|
|
180
|
-
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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.
|
package/docs/writing-guidance.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# Writing guidance and the prose checker
|
|
2
2
|
|
|
3
|
-
Slate ships
|
|
4
|
-
prose against a mechanical proxy
|
|
5
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
54
|
+
Configure writing features in the project's `.pi/slate.json`:
|
|
56
55
|
|
|
57
56
|
```json
|
|
58
|
-
{
|
|
57
|
+
{
|
|
58
|
+
"writing": {
|
|
59
|
+
"check": true,
|
|
60
|
+
"remind": false,
|
|
61
|
+
"remindPercent": 10
|
|
62
|
+
}
|
|
63
|
+
}
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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.
|
|
95
|
-
|
|
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.
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
316
|
-
|
|
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
|
package/extension/handoff.ts
CHANGED
|
@@ -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
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
412
|
-
Apply
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
708
|
-
//
|
|
709
|
-
//
|
|
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
|
package/extension/state.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/extension/writing.ts
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
95
|
+
const read = (key: keyof typeof value): unknown => {
|
|
96
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) return undefined;
|
|
101
97
|
try {
|
|
102
|
-
|
|
98
|
+
return value[key];
|
|
103
99
|
} catch {
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
-
|
|
121
|
+
warn("slate: ignoring writing.remindPercent — expected a finite number in (0, 100] (defaulting to 10)");
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
|
|
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.
|
|
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": {
|