ytdb-slate 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ytdb-slate
2
2
 
3
+ [![CI status](https://github.com/JetBrains/ytdb-slate/actions/workflows/tier-1.yml/badge.svg?branch=main)](https://github.com/JetBrains/ytdb-slate/actions/workflows/tier-1.yml)
4
+
3
5
  Slate is a thread-weaving orchestration extension for the [pi coding agent](https://pi.dev).
4
6
 
5
7
  The orchestrator (your main pi session) dispatches **bounded actions** to persistent **worker threads**. Each completed action is compressed by an LLM into an **episode** — a durable, structured record (intent, actions, findings, artifacts, open issues, handoff notes) that the orchestrator composes into further dispatches instead of re-reading raw transcripts. On top of that, Slate injects a mandatory workflow doctrine: research before design, design review, adversarial review, and track review — with optional umbrella **draft-PR publishing** for tracks. An opt-in **model-failover** map adds high availability: when a model API fails, the orchestrator, worker threads, and episode compression each retry once on a configured equal-quality alternative. A second opt-in, **action-level model routing**, gives each dispatched action a model and an effort level chosen to be up to the task and no more, so cost is bounded per action instead of per session.
@@ -57,6 +59,16 @@ Slate can pick the model and the effort level per dispatched action rather than
57
59
 
58
60
  What the code enforces is deliberately narrower than what the routing data advises. The dispatch guards refuse an unlisted model, a level off the target model's ladder and a level the provider rejects outright; they warn about — and mark — an unmeasured level, refusing it only when `router.allowUnmeasuredEffort` is `false`; and the context-window guard substitutes a wider listed model instead of ever blocking an action. The obligations that come from the profile data itself — keeping review and gate actions on measured levels, and honoring a REFUSE such as `anthropic/claude-fable-5`'s for zero-data-retention work — are stated in the doctrine for the orchestrator to honor; they are **not** code-enforced guards. Full semantics: the `router.*` rows in [Configuration](#configuration) and the shipped [`docs/model-routing.md`](docs/model-routing.md) — if they disagree, that document wins.
59
61
 
62
+ ## Optional writing guidance
63
+
64
+ Set `writing.check` to `true` to add writing guidance and human-only telemetry. In orchestrator mode, Slate adds a doctrine rule that states the convention. It also adds a status value such as `writing 1/4`. This value means that one of four measured prose turns had a fail-level finding. Each worker gets a shorter preamble reminder about sentence length, semicolons, and contractions.
65
+
66
+ The option has prompt cost. Each orchestrator system prompt contains the doctrine rule. You pay for this text on every turn. Each worker session also gets a gated preamble addition. Both additions are absent when the option is off. The worker preamble then stays byte-identical to its pre-feature form.
67
+
68
+ > **No conformance claim:** This is a dictionary-free proxy. It embeds no controlled vocabulary and claims no ASD-STE100 conformance.
69
+
70
+ The shipped CLI checks plain files, JSONL records, and unified diffs. See [`docs/writing-guidance.md`](docs/writing-guidance.md) for its rules, limits, output, and command examples.
71
+
60
72
  ## Install
61
73
 
62
74
  Install into a project (pinned, project-scoped — recorded in `.pi/settings.json`):
@@ -82,6 +94,7 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
82
94
  | `orchestratorPromptDocs` | string[] | `[]` | Project markdown files (paths relative to the project root) whose **contents** are appended to the orchestrator system prompt. |
83
95
  | `workerPromptDocs` | string[] | `[]` | Project markdown files whose **contents** are appended to every worker-thread system prompt. |
84
96
  | `workflow.draftPRs` | boolean | `false` | Enable umbrella draft-PR publishing for tracks. |
97
+ | `writing.check` | boolean | `false` | Add the writing doctrine rule, per-turn status, and worker guidance. An absent option is silently off. A malformed `writing` value, or a non-boolean `check` value, warns and defaults to off. Unknown `writing` keys warn and are ignored. See [`docs/writing-guidance.md`](docs/writing-guidance.md). |
85
98
  | `doctrineExtraPath` | string | — | Project markdown whose **content** is appended to the orchestrator doctrine (project-specific workflow additions). |
86
99
  | `reviewPerspectivesPath` | string | — | Project review charters, each declaring its own finding-ID prefix. The doctrine references this **path**; the orchestrator reads the file alongside the shipped review rules. |
87
100
  | `modelFailover` | object (string → string) | — (empty, failover off) | Map of `provider/id` → equal-quality alternative model; on a model API failure the affected site retries once on the mapped model (worker/orchestrator sites only after pi's own retries are exhausted; episode compression has no pi retry loop). Read once at session start — see [`docs/model-failover.md`](docs/model-failover.md). |
@@ -99,6 +112,7 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
99
112
  "orchestratorPromptDocs": ["docs/agents/orchestrator-guidelines.md"],
100
113
  "workerPromptDocs": ["docs/agents/thread-guidelines.md"],
101
114
  "workflow": { "draftPRs": true },
115
+ "writing": { "check": true },
102
116
  "doctrineExtraPath": "docs/agents/workflow-additions.md",
103
117
  "reviewPerspectivesPath": "docs/agents/review-perspectives.md",
104
118
  "modelFailover": { "anthropic/claude-sonnet-5": "openai/gpt-5.2" },
@@ -143,6 +157,7 @@ In orchestrator mode, Slate appends a short **doctrine** (a block of numbered ru
143
157
  - `docs/design-principles.md` — Slate's own design rationale
144
158
  - `docs/model-failover.md` — the opt-in `modelFailover` map (**reference documentation** — unlike the entries above it is not workflow doctrine and is not cited by the doctrine)
145
159
  - `docs/context-budget.md` — the orchestrator `contextBudget`: defaults, per-model overrides, the window clamp, and the pricing rationale (also **reference documentation**, not cited by the doctrine)
160
+ - `docs/writing-guidance.md` — the optional writing convention, `writing.check` behavior, status line, and checker CLI
146
161
  - `docs/model-routing.md` — action-level model routing: the two `router` keys, how a model becomes routable, how an omitted effort level resolves, the dispatch guards, and the warnings a first routed session emits. Unlike the two entries above it **is** cited by the doctrine, by absolute path, but only while the routing rule renders — it is the extra embedded path that appears with the rule, the fourth normally and the fifth when `workflow.draftPRs` is also on, which the size arithmetic in `docs/context-budget.md` accounts for. The routable model list itself is never read from this file: the rule renders it live from the session's own resolution, because that set depends on your registry and credentials.
147
162
 
148
163
  Project-specific additions layer on top — they extend, not replace, the shipped doctrine — via two distinct mechanisms:
@@ -129,69 +129,93 @@ you pay its input tokens every turn.
129
129
 
130
130
  **Its size depends on where the package is installed.** That is not
131
131
  obvious, and it catches anyone who measures: the doctrine cites the
132
- shipped docs by ABSOLUTE path, so three to five full filesystem
133
- paths are embedded in the block — three at minimum
132
+ shipped docs by ABSOLUTE path, so three to six full filesystem paths
133
+ are embedded in the block — three at minimum
134
134
  (`track-workflow.md`, `review-rules.md`, `design-principles.md`),
135
135
  plus `pr-publishing.md` when `workflow.draftPRs` is on, plus
136
- `model-routing.md` when the routing rule renders. Every additional
137
- character in the installed `docs/` directory therefore costs 3–5
136
+ `model-routing.md` when the routing rule renders, plus
137
+ `writing-guidance.md` when `writing.check` is on. Every additional
138
+ character in the installed `docs/` directory therefore costs 3–6
138
139
  characters of doctrine.
139
140
 
140
- So the figures below are **portable characters**, defined exactly as
141
- Slate's own automated size check defines them: the block with each
142
- occurrence of the installed `docs/` DIRECTORY removed, keeping the
143
- filename. That makes them install-invariant by construction —
144
- verified by measuring at a 12-character docs path and at this repo's
145
- 58-character one, which give identical portable counts — and it
146
- makes the arithmetic one multiplication:
141
+ The figures below are **portable characters**, defined exactly as
142
+ Slate's own automated `doctrine-budget` check defines them: the
143
+ block with each occurrence of the installed `docs/` DIRECTORY
144
+ removed, keeping the filename. That makes them install-invariant by
145
+ construction and makes the arithmetic one multiplication:
147
146
 
148
147
  > rendered characters = portable + (embedded paths × length of your
149
148
  > installed `docs/` directory)
150
149
 
151
- Every row below states its full basis, because router state, model
152
- count and `draftPRs` each move the number:
150
+ `doctrine-budget` in `verification/resolver-checks.mjs` is the
151
+ definition of record for this convention and its enforced bounds.
152
+ This table is a measured snapshot, not a second authority. It uses
153
+ the same real shipped-profile fixture as that check; the six-model
154
+ rows use the six configured models in this repository's documented
155
+ example. Every row states its full basis, because router state,
156
+ model count, `draftPRs`, and `writing.check` each move the number:
153
157
 
154
- | router | models | `draftPRs` | paths | portable | lines | rendered at a 58-char docs dir |
158
+ | router | models | `draftPRs` | `writing.check` | paths | portable | lines |
155
159
  | --- | --- | --- | --- | --- | --- | --- |
156
- | off | — | off | 3 | 1,929 | 38 | 2,103 |
157
- | off | — | on | 4 | 1,948 | 38 | 2,180 |
158
- | on | 6 configured | off | 4 | 3,870 | 58 | 4,102 |
159
- | on | 6 configured | on | 5 | 3,889 | 58 | 4,179 |
160
- | on | all 9 shipped | off | 4 | 4,448 | 61 | 4,680 |
161
- | on | all 9 shipped | on | 5 | 4,467 | 61 | 4,757 |
160
+ | off | — | off | off | 3 | 1,929 | 38 |
161
+ | off | — | off | on | 4 | 2,683 | 53 |
162
+ | off | — | on | off | 4 | 1,948 | 38 |
163
+ | off | — | on | on | 5 | 2,702 | 53 |
164
+ | on | 6 configured | off | off | 4 | 3,879 | 58 |
165
+ | on | 6 configured | off | on | 5 | 4,633 | 73 |
166
+ | on | 6 configured | on | off | 5 | 3,898 | 58 |
167
+ | on | 6 configured | on | on | 6 | 4,652 | 73 |
168
+ | on | all 9 shipped | off | off | 4 | 4,434 | 61 |
169
+ | on | all 9 shipped | off | on | 5 | 5,188 | 76 |
170
+ | on | all 9 shipped | on | off | 5 | 4,453 | 61 |
171
+ | on | all 9 shipped | on | on | 6 | 5,207 | 76 |
162
172
 
163
173
  An untrusted project reads the first row whatever its `slate.json`
164
- says: no project config is loaded, so neither optional rule renders.
165
- Line counts do not vary with the install path. Enabling `draftPRs`
166
- costs 19 portable characters plus one more embedded path.
174
+ says: no project config is loaded, so no optional rule renders. Line
175
+ counts do not vary with the install path. Enabling `draftPRs` costs
176
+ 19 portable characters plus one embedded path. Enabling
177
+ `writing.check` costs 754 portable characters, adds 15 lines to the
178
+ whole doctrine, and adds one embedded path; considered alone, the
179
+ writing rule is 754 portable characters / 16 lines because its
180
+ leading newline becomes the separator when appended.
167
181
 
168
182
  Two parts of the block grow with configuration rather than with the
169
183
  path:
170
184
 
171
185
  - The routing rule is a live table with ONE ROW PER ROUTABLE MODEL,
172
186
  so what renders is the models you CONFIGURE — not the nine Slate
173
- ships profiles for. It is 1,941 portable characters / 20 lines for
174
- six configured models and 2,519 / 23 for all nine. A model ROW
175
- costs 154–186 characters; the legend adds a one-off clause per
176
- marker it has to explain, so listing the first cheap-tier model
177
- costs ~50 characters more than its row (two new clauses). That is
178
- why stepping from six to nine averages 193 per model where the
179
- first six average 171.
187
+ ships profiles for. In this snapshot it is 1,950 portable
188
+ characters and adds 20 doctrine lines for six configured models;
189
+ for all nine it is 2,505 characters and adds 23 lines. The six
190
+ model rows are 146–181 characters; all nine are 146–183. The
191
+ legend adds a one-off clause per marker it has to explain, so
192
+ growth is not only the sum of new rows.
180
193
  - The worker-extension rule grows per whitelisted extension and per
181
194
  tool that extension contributes, so it has no fixed size; one
182
195
  extension contributing two tools measured 373 portable characters
183
196
  / 7 lines.
184
197
 
185
- Against a 256,000-token budget none of this is material — as a rough
186
- estimate, at 4 characters per token (no tokenizer was run, and the
187
- table is denser than prose, so treat it as a floor) the whole block
188
- is ≈480–525 tokens with both optional rules off and ≈970–1,025 with
189
- six models routed, each range spanning portable to as-rendered-here.
190
- That is under half a percent of the default budget, though it is
191
- re-sent on every request rather than paid once. It is listed here
192
- because it is context Slate itself puts in front of the model on
193
- every turn, and because it is the number to check before assuming
194
- the budget's headroom is all conversation.
198
+ Against a 256,000-token budget none of this is material. At four
199
+ characters per token as a rough estimate (no tokenizer was run, and
200
+ the routing table is denser than prose), the portable block ranges
201
+ from about 482 tokens at the baseline to about 1,302 with all nine
202
+ models, `draftPRs`, and writing enabled. That is close to half a
203
+ percent of the default budget at the largest measured configuration,
204
+ though it is re-sent on every request rather than paid once. It is
205
+ listed here because it is context Slate itself puts in front of the
206
+ model on every turn, and because it is the number to check before
207
+ assuming the budget's headroom is all conversation.
208
+
209
+ ### Worker writing preamble
210
+
211
+ The doctrine table does not include the worker preamble. It belongs
212
+ to each worker session's system prompt, not to the orchestrator's
213
+ per-turn doctrine. The feature-off preamble is 226 bytes. With
214
+ `writing.check` on it is 384 bytes, a 158-byte increase (the
215
+ 157-byte guidance plus its separating space). These are UTF-8 byte
216
+ counts; the current text is ASCII, so its character counts happen to
217
+ be identical. This separate figure states the worker-session cost
218
+ without presenting it as orchestrator doctrine.
195
219
 
196
220
  ## Worker threads: the routing window guard
197
221
 
@@ -447,14 +447,13 @@ model's ladder is unreadable.
447
447
  - **In the orchestrator's own system prompt, every turn:** the
448
448
  doctrine gains a routing rule — a table with one row per routable
449
449
  model, plus the rules for reading it. This is the surface you do
450
- not see, and it is the router's standing cost: 1,941 characters /
451
- 20 lines for six configured models, 2,519 / 23 for all nine — a
452
- model row costs 154–186 characters, plus a one-off legend clause
453
- for each marker it introduces (listing the first cheap-tier model
454
- costs ~50 characters beyond its row, which is why six→nine
455
- averages 193 per model against the first six's 171). Configuring
456
- the router roughly doubles Slate's always-loaded block (1,929
457
- characters with routing off). Those are PORTABLE characters — the
450
+ not see, and it is the router's standing cost: 1,950 characters /
451
+ 20 added doctrine lines for six configured models, 2,505 / 23 for
452
+ all nine. In the current snapshot a model row costs 146–183
453
+ characters, plus a one-off legend clause for each marker it
454
+ introduces. Configuring the router roughly doubles Slate's
455
+ always-loaded block (1,929 characters with routing and writing
456
+ off). Those are PORTABLE characters — the
458
457
  doctrine with each occurrence of the installed `docs/` directory
459
458
  removed, filenames kept — because the doctrine embeds absolute doc
460
459
  paths and its raw size therefore depends on where the package is
@@ -37,14 +37,45 @@ automatically).
37
37
 
38
38
  ## 3. Choose perspectives by what the change touches
39
39
 
40
- There is no fixed roster — compose perspectives per change. Each
41
- bullet below is that perspective's charter:
40
+ There is no fixed roster — compose perspectives per change. Every
41
+ perspective has this first step before its specific charter: run
42
+ `node /path/to/slate/extension/writing-check.mjs --diff PATH --format text`
43
+ over the review diff, using the package root that contains this document.
44
+ Report fail-level matches in changed prose governed by the reviewed
45
+ project's writing convention. Diff mode deliberately selects only prose
46
+ file types; inspect user-facing strings and comments in source files
47
+ directly. For this package, [writing-guidance.md](writing-guidance.md)
48
+ defines the convention, its scope and its exclusions.
49
+
50
+ The checker is diagnostic, not authoritative. It embeds no controlled
51
+ vocabulary and establishes no ASD-STE100 conformance. A clean run does
52
+ not prove that writing is good, and a match is not automatically a
53
+ defect. Apply the project's convention scope before grading output:
54
+
55
+ - `fail`: in convention-governed changed text, normally `should-fix`;
56
+ use `blocker` only when the underlying defect prevents safe or correct
57
+ delivery. In excluded text, it is advisory only.
58
+ - `warning`: normally at most a `suggestion`; raise it only when
59
+ independent evidence establishes a more consequential defect.
60
+ - `house-style`: at most a `suggestion` when the applicable convention
61
+ calls for the change; otherwise report nothing.
62
+ - `advisory`: never a defect by itself. File a finding only when
63
+ independent review establishes an underlying defect, graded on its
64
+ own consequences.
65
+
66
+ Reviewer judgment governs every class. The checker cannot decide
67
+ meaning, factual accuracy, claim truth, document structure, topic unity,
68
+ or instruction completeness; those usually matter more than a
69
+ mechanical match. Each bullet below is the remainder of that
70
+ perspective's charter:
42
71
 
43
72
  - **Code baseline** (any production/test code change): correctness &
44
73
  bugs (logic, null safety, resource leaks, lifecycle); code quality
45
74
  (readability, duplication, error handling); test quality (do tests
46
75
  verify behavior, cover the change, and would they fail on
47
- regression?).
76
+ regression?). Verify documentation claims about the code under review
77
+ against the implementation already being read. A contradiction is a
78
+ code-baseline finding, not work to leave for a prose specialist.
48
79
  - **Specialists**, added when the change touches their domain:
49
80
  - concurrency — any defect requiring reasoning about thread
50
81
  interleavings (locks, shared state, atomicity);
@@ -60,14 +91,26 @@ bullet below is that perspective's charter:
60
91
  domain specialists). Each charter in that file MUST declare its own
61
92
  stable finding-ID prefix (uniqueness rules in §4). Compose them per
62
93
  change exactly like the built-in perspectives above.
63
- - **Non-code artifacts** (docs, prompts, process rules, extension
64
- guidance like this file) get their own perspectives INSTEAD of the
65
- code baseline: internal consistency & cross-references; instruction
66
- completeness (can a reader execute this without guessing?); context
67
- budget (what becomes always-loaded vs on-demand); writing style;
68
- and safety of any scripts/hooks/config touched.
69
- - **Mixed diffs**: scope each reviewer to its in-scope files. Code
70
- reviewers never review process/docs files, and vice versa.
94
+ - **Non-code baseline** (docs, prompts, process rules, extension
95
+ guidance like this file): internal consistency & cross-references;
96
+ factual and instruction completeness (can a reader execute this
97
+ without guessing?); context budget (what becomes always-loaded vs
98
+ on-demand); and safety of any scripts/hooks/config touched. This
99
+ baseline applies instead of the code baseline unless a mixed diff
100
+ also changes code.
101
+ - **Text specialist**, added when a change ships user-facing or
102
+ licensing-adjacent prose: licensing and provenance exposure;
103
+ coherence with the project's writing convention; proportionality and
104
+ duplication; register, audience fit, structure, and accuracy as
105
+ prose. User documentation, command help, release notes, prompts, and
106
+ public-facing messages normally trigger it. Do not dispatch it solely
107
+ for a short internal code comment, test name, fixture string, or
108
+ mechanical label unless the text is licensing-adjacent or its prose
109
+ risk independently warrants review.
110
+ - **Mixed diffs**: scope each reviewer to its charter. Code reviewers own
111
+ claims that describe their in-scope code even when those claims live
112
+ in documentation. They do not take on the rest of the non-code or text
113
+ charter, and non-code reviewers do not take on code correctness.
71
114
  - Selection is a judgment call, not a rigid filter — when in doubt,
72
115
  include the perspective. Risk/complexity changes how deep iteration
73
116
  goes (§6), never which perspectives run.
@@ -156,13 +199,19 @@ review dispatch:
156
199
  library code instead of assuming its semantics.
157
200
  - Enumerate cases exhaustively along the changed execution paths
158
201
  (branches, error paths, boundary values); mark each case as checked
159
- or explicitly out of scope.
202
+ or explicitly out of scope. For prose, enumerate the affected
203
+ audiences, reader tasks, claims, definitions, cross-references,
204
+ examples, exceptions, and boundary conditions instead of execution
205
+ paths.
160
206
  - Back every defect claim (blocker or should-fix) with a concrete
161
207
  counterexample: the input, state, or interleaving that triggers the
162
208
  defect, traced through the code.
163
209
  - Back every correctness claim ("no issue here") with a justification
164
210
  for why no counterexample exists — not merely the absence of one
165
- found.
211
+ found. For prose, proof of absence means a bounded, reproducible check
212
+ of the relevant set. Examples include checker output, targeted
213
+ searches, re-resolved references, and comparison with authoritative
214
+ sources. State explicitly what those checks cannot establish.
166
215
  - Before finalizing, run an alternative-hypothesis check: "if the
167
216
  opposite verdict were true, what evidence would exist?" — then look
168
217
  for that evidence.
@@ -170,3 +219,7 @@ review dispatch:
170
219
  sought → confirmed / refuted / refined) instead of wandering.
171
220
  - Derive the final verdict explicitly from the stated definitions and
172
221
  claims, not from overall impression.
222
+ - Treat a fix series as a changed artifact that needs review. Verify the
223
+ addressed finding, then review the fix diff and the cumulative result
224
+ for new paths, claims, and regressions. Clearing the original finding
225
+ does not clear defects introduced by its fix.
@@ -0,0 +1,322 @@
1
+ # Writing guidance and the prose checker
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.
7
+
8
+ This document is reference documentation, not workflow doctrine. It
9
+ 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.
13
+
14
+ ## What the checker is, and what it is not
15
+
16
+ `extension/writing-check.mjs` is a dependency-free Node command. It
17
+ reads text, strips the parts that are not prose, splits what remains
18
+ into blocks and sentences, and reports surface facts about them:
19
+ sentence length, sentence count per paragraph, and matches for a
20
+ closed list of twelve patterns. It emits per-rule counts, length
21
+ distributions and one finding record per match, each with a source
22
+ offset and a framed excerpt.
23
+
24
+ **It is a proxy, and the proxy is dictionary-free.** It embeds no
25
+ controlled vocabulary and consults none. It therefore cannot decide
26
+ whether a word is approved, whether a word is used in an approved
27
+ sense or part of speech, or whether a technical term is legitimate.
28
+ It reports those limits in its own output, under `NOT CHECKED` — see
29
+ [What the checker declares unchecked](#what-the-checker-declares-unchecked).
30
+
31
+ **It establishes no conformance with any writing standard.** ASD-STE100
32
+ inspired the choice of what to measure. Slate claims no conformance
33
+ with it. No text, example or dictionary entry from the standard is
34
+ reproduced here or in the checker. TechScribe's
35
+ [public analysis](https://www.simplified-english.co.uk/analysis.html#write-short-sentences)
36
+ is the source for the 20- and 25-word figures. Boeing's
37
+ [public checker page](https://www.boeing.com/company/simplified-english-checker)
38
+ also publishes that pair and is the source for the six-sentence figure.
39
+ The source repository keeps the citation trail in its non-published
40
+ `research/writing-threshold-provenance.md`. Slate defines the severity
41
+ classes, patterns and suppression lists, and applies one band uniformly
42
+ to all prose rather than selecting a limit by text type. A clean run
43
+ is evidence about surface form only. It is not a certificate.
44
+
45
+ **It is not an authority over a reviewer.** A match is a place to
46
+ look, not a verdict. `review-rules.md` governs how a text reviewer
47
+ turns checker output into findings and severities, and it is the
48
+ document to follow when reviewing prose. The short version: a
49
+ `fail` class match deserves inspection, an `advisory` class match is
50
+ never a defect on its own, and the checker cannot see meaning,
51
+ accuracy, structure or completeness at all.
52
+
53
+ ## Enabling it
54
+
55
+ One boolean in the project's `.pi/slate.json`:
56
+
57
+ ```json
58
+ { "writing": { "check": true } }
59
+ ```
60
+
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`.
65
+
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.
72
+
73
+ The switch turns on three things and nothing else:
74
+
75
+ 1. **The doctrine rule.** One numbered rule is appended to the
76
+ orchestrator's system prompt while orchestrator mode is on. It
77
+ states the convention and cites this file by absolute path.
78
+ 2. **The worker preamble sentence.** Every worker thread dispatched
79
+ while the switch is on gets one extra sentence of guidance in its
80
+ preamble. With the switch off the preamble is byte-identical to
81
+ its pre-feature text.
82
+ 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.
86
+
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.
89
+
90
+ ## What it costs when it is on
91
+
92
+ The doctrine rule is part of every orchestrator system prompt, so its
93
+ 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.
96
+
97
+ An interactive session also runs the checker synchronously after each
98
+ completed assistant message. The hook refuses text over 16 KiB and
99
+ 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.
103
+
104
+ ## What counts as prose
105
+
106
+ Rules run over blocks, and only some blocks are prose. Before any
107
+ block is formed, the normalizer BLANKS the spans that are not prose,
108
+ keeping the source length so that every reported offset still points
109
+ into the original text:
110
+
111
+ - fenced code blocks, including an unterminated final fence;
112
+ - indented code (four or more leading spaces followed by a character
113
+ that is not a list marker);
114
+ - unified-diff lines (`diff --git`, `@@`, `---`, `+++`);
115
+ - timestamped log lines (an ISO-8601 timestamp followed by a level
116
+ such as `INFO` or `ERROR`);
117
+ - HTML comments;
118
+ - inline code spans;
119
+ - autolinks (`<https://…>`, `<mailto:…>`) and bare URLs.
120
+
121
+ What remains is split into lines and classified: `table-row`,
122
+ `heading`, `blockquote`, `list-item` or `paragraph`. Consecutive
123
+ paragraph lines join into one block with a single space, so a sentence
124
+ wrapped across source lines is one sentence. The block keeps a map
125
+ from its joined text back to source offsets, so findings on a
126
+ multi-line paragraph report the right characters.
127
+
128
+ Rules and prose statistics apply to `paragraph`, `list-item` and
129
+ `blockquote` blocks. Headings and table rows are parsed and counted as
130
+ blocks, but no rule fires in them and they contribute no words or
131
+ sentences to the statistics. `PARA6` is narrower still: it applies to
132
+ `paragraph` blocks only.
133
+
134
+ Sentences are split at `.`, `!` or `?` when followed by whitespace or
135
+ the end of the text. A period is treated as internal, and does not
136
+ split, when it is doubled, sits between two digits, ends one of a
137
+ small fixed abbreviation list (`e.g.`, `i.e.`, `etc.`, `vs.`, `dr.`,
138
+ `mr.`, `mrs.`, `ms.`, `prof.`, `sr.`, `jr.`, `no.`, `fig.`,
139
+ `approx.`, `dept.`, `inc.`, `ltd.`, `st.`), or closes an initialism
140
+ such as `U.S.`. An ellipsis is one punctuation run, not three empty
141
+ sentences. A candidate sentence with no letter in it is dropped.
142
+
143
+ A word is a run of letters and digits, with internal apostrophes and
144
+ hyphens kept, that contains at least one letter. So a hyphenated
145
+ compound counts as one word and a bare number counts as none.
146
+
147
+ Four of the twelve rules ignore identifier-shaped text. `PASSIVE`,
148
+ `INGFORM`, `NOUNCLUSTER` and `MULTICMD` run over a filtered token
149
+ list with path-like tokens, dotted names, `snake_case`, `camelCase`
150
+ and em-dash-bearing quotations removed, so a file name or a symbol
151
+ cannot trip them. The length, punctuation and parenthetical rules see
152
+ the unfiltered text.
153
+
154
+ ## The rules
155
+
156
+ Twelve rules in four severity classes. The class is the checker's own
157
+ label, reported with every finding and in the per-rule summary.
158
+
159
+ **`fail` — a mechanical violation of the convention.**
160
+
161
+ | rule | what it flags |
162
+ | --- | --- |
163
+ | `SENT25` | a sentence of more than 25 words |
164
+ | `PARA6` | a paragraph block of more than 6 sentences |
165
+ | `SEMICOLON` | each `;` in a prose block |
166
+ | `CONTRACTION` | a contracted form. Two shapes match: a pronoun or one of `that`, `there`, `here`, `what`, `who`, `how`, `where`, `when`, `why`, `let` carrying `n't` or `'re`, `'ll`, `'ve`, `'d`, `'m`, `'s`; and a negated auxiliary such as `don't`, `won't` or `shan't`. Straight and curly apostrophes both match |
167
+
168
+ **`warning` — over the comfortable length, under the failing one.**
169
+
170
+ | rule | what it flags |
171
+ | --- | --- |
172
+ | `SENT20` | a sentence of more than 20 words and at most 25 |
173
+
174
+ A sentence reports one length result at most: `SENT25` when it is
175
+ over 25 words, `SENT20` when it is over 20, and nothing otherwise.
176
+
177
+ **`house-style` — legitimate constructions this convention avoids.**
178
+
179
+ | rule | what it flags |
180
+ | --- | --- |
181
+ | `PARENTHETICAL_PAREN` | a parenthesis pair whose content looks like a clause rather than an aside. The test is a surface one: an auxiliary or `be` form, a word ending in `ed` or `es`, or a non-initial word ending in `s` |
182
+ | `PARENTHETICAL_DASH` | a paired em dash, or a spaced en dash pair, enclosing text. A pair inside a quotation that itself contains an em dash is suppressed |
183
+ | `SLASHED` | a letters-only `word/word` construction such as `and/or`. Paths and URLs are already blanked, and a longer slash run does not match |
184
+
185
+ **`advisory` — noisy heuristics, never a defect on their own.**
186
+
187
+ | rule | what it flags |
188
+ | --- | --- |
189
+ | `PASSIVE` | a `be` form, optionally followed by an `-ly` adverb, then a likely participle: a word ending in `ed`, or one of a fixed irregular-participle list. A fixed list of state-like participles (`connected`, `installed`, `configured` and others) is suppressed, which lowers noise and can hide a real passive |
190
+ | `INGFORM` | a word ending in `ing` that is not preceded by a `be` form. A first token of a multi-token sentence is skipped, deliberately, because it is usually a gerund subject or a heading-like phrase |
191
+ | `NOUNCLUSTER` | a run of four or more words with no function word between them |
192
+ | `MULTICMD` | a token shape: a sentence starts with a non-function word and later has `and`, `and then` or `, then` followed by another non-function word. It approximates multiple commands but can match ordinary coordination such as “Files and folders exist.” |
193
+
194
+ Only the `fail` class moves the turn status counter. A `warning`,
195
+ `house-style` or `advisory` match is reported and counted, but the
196
+ counter treats the turn as clean.
197
+
198
+ Two properties are worth knowing before reading a report. Rule ids
199
+ and classes come from one closed table in the module, so a report can
200
+ never name a rule the table does not define. Every finding carries a
201
+ source offset range, a block index, a sentence index where the rule
202
+ is sentence-scoped, and an excerpt framed in `⟦…⟧` with the framing
203
+ characters removed from the content, so the frame is always the
204
+ checker's and never the text's.
205
+
206
+ ## The command line
207
+
208
+ ```
209
+ node extension/writing-check.mjs --input records.jsonl [--format json|text]
210
+ node extension/writing-check.mjs --file PATH [--file PATH ...] [--format json|text]
211
+ node extension/writing-check.mjs --diff changes.diff [--format json|text]
212
+ ```
213
+
214
+ Exactly one input mode per run. `--format` is `json` by default and
215
+ `text` for a human-readable report. `--help` prints the usage.
216
+ `--input` and `--diff` take one path each; `--file` repeats.
217
+
218
+ Every input must be a REGULAR FILE. The command refuses a symlink, a
219
+ directory, a FIFO or a device. It checks the opened descriptor and
220
+ rejects a special file substituted between the initial check and the
221
+ open. It also detects growth while reading. It does not detect every
222
+ race or a same-size in-place modification.
223
+
224
+ ### `--input`: JSONL records
225
+
226
+ One JSON object per line, each with a string `text` field. A missing
227
+ or non-string field is rejected. `id` names the record in the report;
228
+ with no `id` the record is named from its `session` and `line` fields,
229
+ else from its position. Blank lines are skipped. This is the mode for
230
+ text that is not a file on disk — an extracted message, a comment
231
+ body, a release note draft.
232
+
233
+ ### `--file`: whole files
234
+
235
+ One record per path, named by the path. This is the mode for a
236
+ complete prose artifact: a document, a README, a release note.
237
+
238
+ ### `--diff`: added lines of a unified diff
239
+
240
+ Reads a unified diff and checks the ADDED lines only. Consecutive
241
+ added lines form one record, named `path:firstAddedLine`, so a
242
+ sentence added across several lines is checked as one sentence. It
243
+ parses hunk headers strictly and fails loudly on a malformed diff
244
+ rather than checking a mis-attributed line.
245
+
246
+ Diff mode is for PROSE review, and its file filter is deliberately
247
+ narrow. It includes `.md`, `.markdown`, `.mdx`, `.txt`, `.rst`,
248
+ `.adoc` and `.asciidoc`, plus a small closed set of extensionless
249
+ conventional names (`readme`, `changelog`, `changes`, `contributing`,
250
+ `release_notes`).
251
+
252
+ It therefore EXCLUDES source files entirely, and that exclusion is
253
+ the mode's main limitation: prose that lives inside code — a
254
+ user-facing string, a comment, a help text, a commit message — is not
255
+ reviewed by `--diff`. Inspect those directly, or extract the text and
256
+ pass it through `--input`. Git's quoted-path encoding is decoded
257
+ before a path is classified or reported; a path that is not valid
258
+ UTF-8 is skipped and named in a `DIFF FILES SKIPPED` line rather than
259
+ being checked under a false name.
260
+
261
+ ### Caps
262
+
263
+ Every cap is a hard bound with a reported consequence, never a silent
264
+ truncation.
265
+
266
+ | cap | value | applies to |
267
+ | --- | --- | --- |
268
+ | input bytes | 1,048,576 | one record, and the total across a run's records or files |
269
+ | records | 10,000 | a JSONL input, a `--file` list, and a diff's added-line runs |
270
+ | findings per record | 1,000 | one record's reported findings |
271
+ | findings per run | 20,000 | the whole run, shared as an EQUAL per-record allowance |
272
+ | excerpt characters | 2,000 | one finding's excerpt, framing characters included |
273
+ | reported stripped spans | 5,000 | the reported non-prose span list |
274
+ | reported block details | 5,000 | the reported per-block detail list |
275
+
276
+ The per-run budget is divided equally rather than first-come, so a
277
+ record's position never decides what it keeps, and the floor is one
278
+ finding per record so that a large run still shows every offending
279
+ record. When the run budget binds, the report states it BEFORE any
280
+ finding line, so a reduced report cannot read as a complete one. A
281
+ record that loses findings reports its own shortfall. The two
282
+ 5,000-span caps bound the REPORT only: blanking and counting are
283
+ always complete.
284
+
285
+ ## What the checker declares unchecked
286
+
287
+ The report ends with a `NOT CHECKED` list. It is part of the output on
288
+ purpose: the limits are a result, not a footnote.
289
+
290
+ | id | why not |
291
+ | --- | --- |
292
+ | `APPROVED_WORD` | approved-word membership needs an authorized controlled dictionary |
293
+ | `APPROVED_MEANING_POS` | approved meaning and part of speech need authorized dictionary data and contextual review |
294
+ | `NOUNCLUSTER_CORRECTNESS` | the token-run heuristic cannot decide whether a noun cluster is wrong or is an approved technical term |
295
+ | `TOPIC_UNITY` | topic unity and topic-sentence adequacy need semantic review |
296
+ | `WARNING_CAUTION_CONTENT` | risk level, placement, command adequacy and consequences need structured metadata and human review |
297
+
298
+ ## What this feature deliberately does not do
299
+
300
+ These are settled decisions. Read them as scope, not as a backlog.
301
+
302
+ - **No controlled vocabulary, and no plan for one.** Word approval
303
+ needs an authorized dictionary. Slate ships none and embeds none.
304
+ - **No conformance claim.** ASD-STE100 inspires the choice of what to
305
+ measure. Slate claims no conformance. No text, example or dictionary
306
+ entry from the standard is reproduced here or in the code.
307
+ - **No vocabulary or terminology enforcement.** The convention keeps
308
+ exact technical terms, so the checker must not push a writer off
309
+ them. Nothing in it rewrites, replaces or grades a word choice.
310
+ - **No broad prose rewriting.** The checker reports and never edits.
311
+ Every fix is a human or agent decision.
312
+ - **No verdict.** No rule class is a defect by itself, and no clean
313
+ run is a pass. `review-rules.md` owns the mapping from a match to a
314
+ 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.
317
+ - **Not universal.** The convention covers prose written for people:
318
+ documents, README text, pull request and commit text, issues,
319
+ review comments, release notes and messages to the user. Research
320
+ logs, worker task text and this repository's own agent instruction
321
+ file are excluded, because a dense exact register serves them
322
+ better.