@rmrdeveloper/sideroom-pi 8.9.0 → 8.11.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.
@@ -1,4 +1,4 @@
1
- import { languageForPath } from '../rules/catalog.ts';
1
+ import { LANGUAGE, languageForPath } from '../rules/catalog.ts';
2
2
  import { type AddedLine, addedLines } from '../rules/model.ts';
3
3
 
4
4
  export type { AddedLine } from '../rules/model.ts';
@@ -11,11 +11,11 @@ export const JEV_MODEL = 'jev-latest';
11
11
  // noise on every edit.
12
12
  export const VIOLATION_PROBABILITY = 0.8;
13
13
 
14
- // The state must stay well under the two documented budgets (64k tokens for the
15
- // whole request, 32k for the state plus the longest question). A file that does
16
- // not fit is skipped, never truncated: half a file reads as a distorted picture
17
- // and Jev answers it with the same confidence.
18
- export const MAX_STATE_CHARACTERS = 60_000;
14
+ // TypeSafe documents 32k tokens for state plus the longest question and 64k
15
+ // for the full request. JSON UTF-8 bytes are a deliberately conservative
16
+ // upper bound; no third-party tokenizer can promise the model's exact count.
17
+ export const MAX_STATE_BYTES = 32_000;
18
+ export const MAX_REQUEST_BYTES = 64_000;
19
19
 
20
20
  const NOUL_TYPE = 'noul';
21
21
 
@@ -30,8 +30,7 @@ export interface JevRule {
30
30
  readonly question: JevNoulQuestion;
31
31
  }
32
32
 
33
- // The six guide rules that a single file can answer and that no mechanical
34
- // check covers. Rules 11, 12, 15, 16, 17 and 18 need the module's neighbours.
33
+ // Questions must only use evidence in the file and the selected direct neighbours.
35
34
  export const JEV_RULES: readonly JevRule[] = [
36
35
  {
37
36
  id: 'guard-clauses',
@@ -64,10 +63,11 @@ export const JEV_RULES: readonly JevRule[] = [
64
63
  question: {
65
64
  type: NOUL_TYPE,
66
65
  instructions:
67
- 'In `change.addedLines`, does a function both change state and return a value, or does something that reads like a query have a side effect?',
66
+ 'In `change.addedLines`, does a function return a value while changing a parameter, shared state or an external resource, or does a getter/query cause such a side effect? Ignore changes to local collections created inside the function to build its result.',
68
67
  criteria: {
69
- true: 'A function that returns data also mutates state, or a getter mutates',
70
- false: 'Functions either change state or return data, never both',
68
+ true: 'A function returns data while changing caller-visible state, or a getter/query mutates that state',
69
+ false:
70
+ 'Local accumulation and read-only file access do not change caller-visible state',
71
71
  },
72
72
  },
73
73
  },
@@ -101,15 +101,60 @@ export const JEV_RULES: readonly JevRule[] = [
101
101
  question: {
102
102
  type: NOUL_TYPE,
103
103
  instructions:
104
- 'In `change.addedLines`, is the same required, type or range check repeated after a boundary already owns that input?',
104
+ 'Does `change.addedLines` repeat a required, type or range check that is visibly performed at the input boundary in `file.body` or `context.files`? Do not infer unseen validation.',
105
+ criteria: {
106
+ true: 'The same check is visible both at the boundary and in the added lines',
107
+ false:
108
+ 'The boundary check is not visible, or the added lines do not repeat it',
109
+ },
110
+ },
111
+ },
112
+ {
113
+ id: 'single-responsibility',
114
+ question: {
115
+ type: NOUL_TYPE,
116
+ instructions:
117
+ 'Do the added lines give the changed function or class two unrelated reasons to change, visible in its body? Do not flag a function merely for coordinating focused collaborators.',
105
118
  criteria: {
106
- true: 'A rule already validated at the boundary is checked again deeper in the code',
107
- false: 'Validation happens once, at the boundary that owns the input',
119
+ true: 'The changed unit itself performs two unrelated responsibilities with visible evidence',
120
+ false:
121
+ 'The unit has one responsibility or only coordinates separate focused units',
122
+ },
123
+ },
124
+ },
125
+ {
126
+ id: 'dependency-direction',
127
+ question: {
128
+ type: NOUL_TYPE,
129
+ instructions:
130
+ 'Do the added lines introduce an import from business/domain code to infrastructure, HTTP, or a framework? Use paths and bodies in `context.files` as evidence; never infer a layer from an unavailable file.',
131
+ criteria: {
132
+ true: 'A new inward-to-outward dependency is visible in the import and its related file',
133
+ false:
134
+ 'No such dependency is visible, or the roles of the files are unclear',
135
+ },
136
+ },
137
+ },
138
+ {
139
+ id: 'comments',
140
+ question: {
141
+ type: NOUL_TYPE,
142
+ instructions:
143
+ 'Does a comment in `change.addedLines` merely narrate obvious code instead of explaining non-obvious intent, a trade-off or a hazard?',
144
+ criteria: {
145
+ true: 'A newly added comment repeats what the adjacent code already says',
146
+ false: 'No comment was added, or it explains non-obvious intent',
108
147
  },
109
148
  },
110
149
  },
111
150
  ];
112
151
 
152
+ export interface JevRelatedFile {
153
+ readonly path: string;
154
+ readonly body: string;
155
+ readonly relation: 'imported' | 'consumer';
156
+ }
157
+
113
158
  export interface JevFileState {
114
159
  readonly file: {
115
160
  readonly path: string;
@@ -120,6 +165,10 @@ export interface JevFileState {
120
165
  readonly kind: string;
121
166
  readonly addedLines: readonly string[];
122
167
  };
168
+ readonly context: {
169
+ readonly files: readonly JevRelatedFile[];
170
+ readonly partial: boolean;
171
+ };
123
172
  }
124
173
 
125
174
  export interface JevRequestBody {
@@ -147,10 +196,13 @@ export function buildFileState(
147
196
  body: string,
148
197
  kind: string,
149
198
  lines: readonly AddedLine[],
199
+ relatedFiles: readonly JevRelatedFile[] = [],
200
+ partial = false,
150
201
  ): JevFileState {
151
202
  return {
152
203
  file: { path, language: languageForPath(path), body },
153
204
  change: { kind, addedLines: lines.map((line) => line.text) },
205
+ context: { files: relatedFiles, partial },
154
206
  };
155
207
  }
156
208
 
@@ -167,13 +219,38 @@ export function addedLinesForEdit(
167
219
  return edits.flatMap((edit) => addedLines(edit.oldText, edit.newText));
168
220
  }
169
221
 
170
- export function stateFitsBudget(body: string): boolean {
171
- return body.length <= MAX_STATE_CHARACTERS;
222
+ // Questions are about code structure; a README or a lockfile has no useful state.
223
+ export function isReviewablePath(path: string): boolean {
224
+ return languageForPath(path) !== LANGUAGE.generic;
225
+ }
226
+
227
+ export function stateFitsBudget(state: JevFileState): boolean {
228
+ const request = buildRequestBody(state);
229
+ const longestQuestion = Object.values(request.questions).reduce(
230
+ (longest, question) =>
231
+ Math.max(longest, Buffer.byteLength(JSON.stringify(question))),
232
+ 0,
233
+ );
234
+ return (
235
+ Buffer.byteLength(JSON.stringify(state)) + longestQuestion <=
236
+ MAX_STATE_BYTES &&
237
+ Buffer.byteLength(JSON.stringify(request)) <= MAX_REQUEST_BYTES
238
+ );
172
239
  }
173
240
 
174
241
  export function buildRequestBody(file: JevFileState): JevRequestBody {
175
242
  const questions: Record<string, JevNoulQuestion> = {};
243
+ const hasImportedFile = file.context.files.some(
244
+ (related) => related.relation === 'imported',
245
+ );
246
+ const addedText = file.change.addedLines.join('\n');
176
247
  for (const rule of JEV_RULES) {
248
+ if (rule.id === 'dependency-direction' && !hasImportedFile) {
249
+ continue;
250
+ }
251
+ if (rule.id === 'comments' && !/(?:\/\/|\/\*|#)/u.test(addedText)) {
252
+ continue;
253
+ }
177
254
  questions[rule.id] = rule.question;
178
255
  }
179
256
  return { model: JEV_MODEL, state: file, questions };
@@ -190,6 +267,7 @@ export function parseEnvelope(text: string): JevResponseEnvelope | undefined {
190
267
 
191
268
  export function readFindings(
192
269
  envelope: JevResponseEnvelope,
270
+ askedRuleIds: ReadonlySet<string> = new Set(JEV_RULES.map((rule) => rule.id)),
193
271
  ): readonly JevFinding[] {
194
272
  const answers = envelope.answers;
195
273
  if (answers === undefined) {
@@ -197,6 +275,9 @@ export function readFindings(
197
275
  }
198
276
  const findings: JevFinding[] = [];
199
277
  for (const rule of JEV_RULES) {
278
+ if (!askedRuleIds.has(rule.id)) {
279
+ continue;
280
+ }
200
281
  const answer = answers[rule.id];
201
282
  if (answer === undefined || answer.noul < VIOLATION_PROBABILITY) {
202
283
  continue;
@@ -30,6 +30,7 @@ const STATUS_LABEL: Readonly<Record<JevStatus, string>> = {
30
30
  [JEV_STATUS.noKey]: 'jev: no key (F10)',
31
31
  [JEV_STATUS.quota]: 'jev: out of quota (F10)',
32
32
  [JEV_STATUS.auth]: 'jev: key rejected (F10)',
33
+ [JEV_STATUS.rateLimited]: 'jev: rate limited',
33
34
  [JEV_STATUS.unavailable]: 'jev: unreachable',
34
35
  };
35
36
 
@@ -35,7 +35,7 @@ export const VOICE_RULES: readonly VoiceRule[] = [
35
35
  {
36
36
  id: 'plain-language',
37
37
  instruction:
38
- 'Explain in plain words: name the subject and goal before the detail, prefer everyday terms over concept jargon (hook, pipeline, registry, resolver, guardrail, invariant) unless the user used them, define any necessary term in one line, and be explicit without repetition. Never write tangled chains such as "the registry resolves the provider through the auth resolver".',
38
+ 'Explain in plain words: say what the user needs to know or do before details. Use everyday words even if the user uses technical terms. Keep a technical term only when needed to understand or act; explain it at first use in one short sentence. Do not repeat yourself or chain technical labels.',
39
39
  },
40
40
  {
41
41
  id: 'direct',
@@ -49,7 +49,7 @@ export const VOICE_RULES: readonly VoiceRule[] = [
49
49
  {
50
50
  id: 'no-invented-terms',
51
51
  instruction:
52
- 'Name things as they are. Coin no intermediate term, abbreviation, codename, or technical concept word the user did not ask for.',
52
+ "Do not coin intermediate terms, abbreviations, or codenames. Use the user's terms only when needed to answer; otherwise explain in everyday words.",
53
53
  },
54
54
  {
55
55
  id: 'short-prose',
@@ -18,8 +18,8 @@ const ENFORCEMENT_SUMMARY =
18
18
  // Static Do/Don't pairs. Literals only: no per-turn data, so the reminder
19
19
  // stays byte-identical across turns and the provider prompt cache holds.
20
20
  export const PERSONA_EXAMPLES = [
21
- "Do: Enable the flag to skip cache (enableCache). Don't: The registry resolves the provider through the auth resolver pipeline.",
22
- "Do: This runs when the session closes (session_save). Don't: The lifecycle hook tears down session-scoped resources via the cleanup path.",
21
+ "Do: Your work list stays with this conversation, not in project files. Don't: Session-scoped board state persists outside the repository.",
22
+ "Do: To get a fresh answer, turn off the cache (stored results). Don't: Disable the cache invalidation pipeline.",
23
23
  ] as const;
24
24
 
25
25
  export const PERSONA_REMINDER = `${PERSONA_REMINDER_HEADING}
@@ -1,6 +1,6 @@
1
1
  import { realpathSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { isAbsolute, join, resolve } from 'node:path';
3
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  const FILE_URL_PREFIX = 'file://';
@@ -29,6 +29,31 @@ export function resolveFileToolPath(
29
29
  return realpathOr(absolutePath);
30
30
  }
31
31
 
32
+ // A file that does not exist yet keeps its unresolved path, so it is matched
33
+ // against the literal working directory as well as the canonical one.
34
+ export function projectRelativePath(
35
+ cwd: string,
36
+ absolutePath: string,
37
+ ): string | undefined {
38
+ const literalRoot = resolve(cwd);
39
+ for (const root of new Set([realpathOr(literalRoot), literalRoot])) {
40
+ const relativePath = relative(root, absolutePath);
41
+ if (isInsideRoot(relativePath)) {
42
+ return relativePath;
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ function isInsideRoot(relativePath: string): boolean {
49
+ return (
50
+ relativePath.length > 0 &&
51
+ relativePath !== '..' &&
52
+ !relativePath.startsWith(`..${sep}`) &&
53
+ !isAbsolute(relativePath)
54
+ );
55
+ }
56
+
32
57
  function normalizeFileToolPath(path: string): string {
33
58
  const normalizedSpaces = path.replace(UNICODE_SPACES, ' ');
34
59
  const withoutAtPrefix = normalizedSpaces.startsWith('@')
@@ -0,0 +1,14 @@
1
+ export const REVIEW_NOTE_EVENT = 'sideroom:review-note';
2
+
3
+ export interface ReviewNote {
4
+ readonly text: string;
5
+ }
6
+
7
+ export function isReviewNote(value: unknown): value is ReviewNote {
8
+ return (
9
+ typeof value === 'object' &&
10
+ value !== null &&
11
+ 'text' in value &&
12
+ typeof value.text === 'string'
13
+ );
14
+ }
@@ -0,0 +1,50 @@
1
+ import type {
2
+ AgentBeforeSettleEvent,
3
+ AgentBeforeSettleEventResult,
4
+ InputEvent,
5
+ InputSource,
6
+ } from '@earendil-works/pi-coding-agent';
7
+
8
+ const COMPLETED_OUTCOME = 'completed';
9
+ const USER_ROLE = 'user';
10
+ const CUSTOM_MESSAGE_ENTRY = 'custom_message';
11
+ const USER_INPUT_SOURCES: ReadonlySet<InputSource> = new Set([
12
+ 'interactive',
13
+ 'rpc',
14
+ ]);
15
+
16
+ // A message typed while the agent runs joins the current run as a steer or
17
+ // follow-up; only input that arrives idle opens a new turn.
18
+ export function startsUserTurn(event: InputEvent): boolean {
19
+ return (
20
+ USER_INPUT_SOURCES.has(event.source) &&
21
+ event.streamingBehavior === undefined
22
+ );
23
+ }
24
+
25
+ // An aborted run is the user stopping the agent, an errored run has nothing
26
+ // worth reviewing, and a pending user message already decides what comes next.
27
+ export function acceptsSettleSteer(event: AgentBeforeSettleEvent): boolean {
28
+ if (event.outcome !== COMPLETED_OUTCOME) {
29
+ return false;
30
+ }
31
+ return !event.context.pendingMessages.some(
32
+ (message) => message.role === USER_ROLE,
33
+ );
34
+ }
35
+
36
+ // Handlers chain on agent_before_settle, so keep the entries earlier handlers
37
+ // proposed; replacing them would drop another extension's steer.
38
+ export function withHiddenSteer(
39
+ event: AgentBeforeSettleEvent,
40
+ customType: string,
41
+ content: string,
42
+ ): AgentBeforeSettleEventResult {
43
+ return {
44
+ entries: [
45
+ ...event.entries,
46
+ { type: CUSTOM_MESSAGE_ENTRY, customType, content, display: false },
47
+ ],
48
+ continue: true,
49
+ };
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmrdeveloper/sideroom-pi",
3
- "version": "8.9.0",
3
+ "version": "8.11.0",
4
4
  "description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
5
5
  "type": "module",
6
6
  "files": [
@@ -89,8 +89,8 @@
89
89
  "@changesets/cli": "^3.0.2",
90
90
  "@commitlint/cli": "^21.2.2",
91
91
  "@commitlint/config-conventional": "^21.2.2",
92
- "@earendil-works/pi-coding-agent": "0.84.4",
93
- "@earendil-works/pi-tui": "0.84.4",
92
+ "@earendil-works/pi-coding-agent": "0.87.1",
93
+ "@earendil-works/pi-tui": "0.87.1",
94
94
  "@types/node": "^22.20.1",
95
95
  "typebox": "1.3.7",
96
96
  "typescript": "^5.6.0"
@@ -50,7 +50,14 @@ impact), or for a word conflict — that is `sideroom-domain-modeling`.
50
50
  technology label.
51
51
  - Present the decision as the current situation, the alternatives by their
52
52
  practical consequences, and one recommendation with a one-line justification.
53
- Never reduce it to technology names.
53
+ Prefer the most maintainable option that can meet known growth needs over the
54
+ cheapest initial build; name its upfront cost and the cost of later changes.
55
+ Do not recommend maximum capacity merely for hypothetical growth. Never reduce
56
+ the alternatives to technology names.
57
+ - If a missing figure could change which option is best, ask for it before
58
+ recommending an architecture. The preliminary question still needs a
59
+ justified recommended option (such as obtaining the figure); never invent
60
+ a target or recommend a solution without evidence.
54
61
  - One open architectural dimension blocks implementation. `Out of scope` closes
55
62
  a thread, is recorded, and is not re-asked.
56
63
 
@@ -77,10 +84,10 @@ an explicit `Out of scope` on each one you pick.
77
84
  codebase, and note any inconsistent pattern to name in a prompt.
78
85
  2. List the architectural decisions the change forces. Drop the ones the code
79
86
  already settles.
80
- 3. For each remaining decision, pick the dimensions it touches and run one
81
- `sideroom_ask` round: situation, alternatives by consequence, one
82
- recommendation, each option with a `description`. Force a number or an
83
- explicit `Out of scope`.
87
+ 3. For each remaining decision, pick the dimensions it touches. Ask for any
88
+ decisive missing figures first, then run a `sideroom_ask` round: situation,
89
+ alternatives by consequence, one evidence-backed recommendation, each option
90
+ with a `description`. Force a number or an explicit `Out of scope`.
84
91
  4. Repeat rounds until no architectural dimension is open. Do not implement
85
92
  with one open.
86
93
  5. Record an accepted decision as an ADR under `docs/adr/` only when it clears
@@ -34,6 +34,14 @@ if the effort is too big to hold in one session, say so and stop.
34
34
  by their practical consequences, and one recommendation with a one-line
35
35
  justification. Never reduce it to technology names. Name a repository
36
36
  inconsistency in the prompt instead of picking one.
37
+ - Recommend for sustainable quality: prefer maintainability and room to grow
38
+ for known needs over the cheapest initial build. Explain the upfront cost and
39
+ future change cost; do not mistake speculative maximum capacity for quality.
40
+ Apply this to minor decisions too, without asking the user about them.
41
+ - If a missing fact or figure could change the recommendation, ask for it in
42
+ a preliminary round before recommending a solution. That round still needs
43
+ a defensible recommended option, such as gathering the missing evidence;
44
+ never invent a number or mark an unsupported solution as recommended.
37
45
  - An *Out of scope* answer closes that thread. Do not re-ask it.
38
46
  - Everything a round settles is recorded through `sideroom-domain-modeling`:
39
47
  inline glossary entries, the ADR three-gate offer, lazy file creation.
@@ -47,8 +55,8 @@ if the effort is too big to hold in one session, say so and stop.
47
55
  1. Read the code around the change. Settle from the codebase everything the
48
56
  codebase settles.
49
57
  2. Run one `sideroom_ask` round on what remains genuinely open: terms first,
50
- then the architectural decisions. Minor decisions are yours; do not spend a
51
- round on them.
58
+ then missing facts that could change a recommendation, then the architectural
59
+ decisions. Minor decisions are yours; do not spend a round on them.
52
60
  3. After each round, write resolved terms to `CONTEXT.md` and qualifying
53
61
  decisions to `docs/adr/`. Offer the ADR for an accepted architectural
54
62
  decision only when all three gates pass.
@@ -22,37 +22,39 @@ needs the full persona instead of the injected reminder.
22
22
 
23
23
  | # | Rule | Do | Don't |
24
24
  | --- | --- | --- | --- |
25
- | 1 | Plain, never in doubt | `Enable the flag to skip cache (enableCache).` | `The registry resolves the provider through the auth resolver pipeline.` |
26
- | 2 | Direct and dry | `The guard blocks the write. Fix the emoji and retry.` | `I'd be happy to help with that! Let's take a look together.` |
25
+ | 1 | Plain, never in doubt | `Your work list stays with this conversation, not in project files.` | `Session-scoped board state persists outside the repository.` |
26
+ | 2 | Direct and dry | `The file was not saved. Remove the symbol and try again.` | `I'd be happy to help with that! Let's take a look together.` |
27
27
  | 3 | No filler | Start with the answer. | `Before we dive in…`, `As I mentioned earlier…`, `To summarize what I just said…` |
28
- | 4 | No invented terms | `the file the mutation adds` | `the artifact delta` (when the user never asked for that name) |
28
+ | 4 | No invented terms | `the file you changed` | `the artifact delta` (when the user never asked for that name) |
29
29
  | 5 | Short prose | Prose for one or two points. | A three-bullet list for a single sentence. |
30
30
  | 6 | User's language | Answer in the language and variant the user wrote in. | A Spanish question answered in English. |
31
31
 
32
32
  ### Plain means everyday words, not more words
33
33
 
34
34
  Rule 1 outranks brevity for clarity, and rule 3 outranks rule 1 for length.
35
- Concretely: name the subject and the goal **before** the detail, use everyday
36
- words instead of concept jargon (`hook`, `pipeline`, `registry`, `resolver`,
37
- `guardrail`, `invariant`) unless the user already used them, define any
38
- necessary term in one line, then stop. Being explicit is not a licence to
39
- repeat yourself, restate the question, or explain the same point twice.
40
-
41
- - Do: `This runs when the session closes. In code: \`session_save\`.`
42
- - Don't: `The lifecycle hook tears down session-scoped resources via the
43
- cleanup path.`
44
- - Do: `The persona reminder is injected on every turn, so it survives
45
- compaction. That is why switching profiles would be pointless here.`
46
- - Don't: `What I'm saying is that the reminder gets injected, and since it's
47
- injected every turn, it survives compaction — which means it comes back after
48
- compaction, so profile switching has no effect.`
35
+ Say what the user needs to know or do before explaining how. Choose words a
36
+ student can understand without knowing the code. Even if the user uses a
37
+ technical term, do not repeat it unless they need it to understand the answer
38
+ or take the next step. If they do, explain it at first use in one short
39
+ sentence. Keep exact commands, file names, and code names when needed to act;
40
+ explain their purpose in everyday words. Plain words must stay accurate.
41
+ Do not repeat the question or explain the same point twice.
42
+
43
+ - User: `Where is the session-scoped work board?`
44
+ - Do: `Your work list stays with this conversation, not in project files.`
45
+ - Don't: `Session-scoped board state persists outside the repository.`
46
+ - Do: `To get a fresh answer, turn off the cache (stored results).`
47
+ - Don't: `Disable the cache invalidation pipeline.`
48
+ - Do: `The answer is saved with this conversation. You can read it again later.`
49
+ - Don't: `The response is persisted to the session, so the session stores the
50
+ response for future access.`
49
51
 
50
52
  ### Names the user did not ask for
51
53
 
52
- Rule 4 bans coining vocabulary mid-conversation. Do not rename a concept the
53
- user already named, do not invent abbreviations (`the PRF layer`), and do not
54
- use an internal field name as if it were the domain word. Use the user's word
55
- until they ask for another.
54
+ Rule 4 bans coining vocabulary mid-conversation. Do not invent abbreviations
55
+ (`the PRF layer`) or use an internal field name as if it were the domain word.
56
+ Replace a user's unnecessary technical term with everyday words, not another
57
+ new label. Keep exact names only when needed to act.
56
58
 
57
59
  ## Hard prohibitions
58
60
 
@@ -77,9 +79,12 @@ dead-locked.
77
79
  ### What steering means
78
80
 
79
81
  The extension sends a corrective steer after a degraded artifact block or a
80
- prohibited assistant message. Steers use `triggerTurn: true` so they reach the
81
- model even when the run is already idle. At most three steers are sent per
82
- agent run.
82
+ prohibited assistant message. Both moments happen while the agent is working, so
83
+ the steer travels through `pi.sendMessage` with `{ triggerTurn: true, deliverAs:
84
+ 'steer' }` and joins the current run. A steer sent after the run ended belongs
85
+ to the guidelines review and the walkthrough offer, which append a
86
+ `custom_message` entry on `agent_before_settle` instead.
87
+ At most three steers are sent per agent run.
83
88
 
84
89
  ## Boundaries
85
90
 
@@ -87,3 +92,8 @@ agent run.
87
92
  - Nothing is written to the target repository.
88
93
  - The persona governs wording. It never relaxes the coding guidelines, the
89
94
  added-line rules, or the check-before-finish gate.
95
+
96
+ ## References
97
+
98
+ - [Digital.gov: Avoid jargon](https://digital.gov/guides/plain-language/principles/avoid-jargon): keep only necessary specialist words and explain them.
99
+ - [GOV.UK: Use clear language](https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/clear-language/): write plainly even for specialists.