@rmrdeveloper/sideroom-pi 8.10.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.
- package/README.md +33 -26
- package/extensions/ask/index.ts +3 -2
- package/extensions/jev/client.ts +6 -2
- package/extensions/jev/context.ts +327 -0
- package/extensions/jev/guard.ts +8 -5
- package/extensions/jev/model.ts +91 -17
- package/extensions/persona/catalog.ts +2 -2
- package/extensions/persona/prompt.ts +2 -2
- package/package.json +1 -1
- package/skills/sideroom-architecture/SKILL.md +12 -5
- package/skills/sideroom-grill/SKILL.md +10 -2
- package/skills/sideroom-persona/SKILL.md +28 -21
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ Sideroom answers each one with a small, opinionated surface:
|
|
|
55
55
|
| Invisible monorepo skills | `monorepo-skills` — trusted child folders join Pi's available skill list |
|
|
56
56
|
| Vague plans | `sideroom-grill` — an interview that settles the words before the work |
|
|
57
57
|
| Unapplied guidelines | `sideroom_rules` — mechanical checks that block or flag the lines you add |
|
|
58
|
-
| Unchecked judgment rules | `jev` — asks a decision model about
|
|
59
|
-
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of
|
|
58
|
+
| Unchecked judgment rules | `jev` — asks a decision model about guide rules with visible evidence |
|
|
59
|
+
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of unnecessary technical words |
|
|
60
60
|
| Premature completion | `sideroom_done` — steers back to the project's check command before finishing |
|
|
61
61
|
| Work left unexplained | `explain` — offers a walkthrough and how to test it once the work settles |
|
|
62
62
|
|
|
@@ -140,9 +140,13 @@ project are labelled `external`. A new session starts clean.
|
|
|
140
140
|
|
|
141
141
|
An on-demand skill for fuzzy plans. It interviews you in `sideroom_ask`
|
|
142
142
|
rounds — reading the codebase first so it never asks what the code already
|
|
143
|
-
says — until you share one understanding.
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
says — until you share one understanding. Its recommendations favor choices
|
|
144
|
+
that are easy to maintain and meet known growth needs over the cheapest initial
|
|
145
|
+
build. It names the upfront cost; if a missing fact could change the choice,
|
|
146
|
+
it asks for that fact before recommending a solution. Resolved terms land in
|
|
147
|
+
`CONTEXT.md` the moment they resolve. Only hard-to-reverse, surprising choices
|
|
148
|
+
with a real trade-off qualify for a record under `docs/adr/`; the user is asked
|
|
149
|
+
first. Everything else stays in the conversation, ready to become a
|
|
146
150
|
spec or an implementation. Vocabulary sharpening during the rounds follows
|
|
147
151
|
the companion `sideroom-domain-modeling` skill, which also runs on its own
|
|
148
152
|
whenever the words — not the plan — are the problem. And when there is no
|
|
@@ -189,17 +193,18 @@ written, no config read: the catalog ships with the package.
|
|
|
189
193
|
|
|
190
194
|
### Jev — judgment where no check reaches
|
|
191
195
|
|
|
192
|
-
Reading the guide covers the mechanical rules. `jev`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
null handling, immutability,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
interrupting each edit. READMEs, JSON,
|
|
199
|
-
outside the project are never sent.
|
|
200
|
-
|
|
201
|
-
It never blocks and never enters the system prompt.
|
|
202
|
-
|
|
196
|
+
Reading the guide covers the mechanical rules. `jev` makes one optional call
|
|
197
|
+
to TypeSafe's decision model about the changed file and, when available, up to
|
|
198
|
+
four directly related source files. It checks guard clauses, fail fast,
|
|
199
|
+
command/query separation, null handling, immutability, repeated validation,
|
|
200
|
+
responsibility, dependency direction, and comments only when the supplied
|
|
201
|
+
files support the question. An answer above the cutoff travels with the
|
|
202
|
+
end-of-run guidelines review instead of interrupting each edit. READMEs, JSON,
|
|
203
|
+
lockfiles, deletion-only edits, and files outside the project are never sent.
|
|
204
|
+
|
|
205
|
+
It never blocks and never enters the system prompt. No conversation or session
|
|
206
|
+
content leaves the machine. Related source excludes ignored, hidden, sensitive-
|
|
207
|
+
named and symlinked paths; a normal source file can still contain a secret.
|
|
203
208
|
The footer counts the calls the session made, and `F10` shows the same count
|
|
204
209
|
beside the key source. Without a key nothing is called, and when Jev runs out of
|
|
205
210
|
quota, rejects the key,
|
|
@@ -214,12 +219,12 @@ without ever writing it to the session.
|
|
|
214
219
|
`sideroom_persona` is the agent's voice, and there is exactly one of it. A
|
|
215
220
|
short reminder rides the system prompt on every turn, so it survives
|
|
216
221
|
compaction; the full guide lives in the on-demand `sideroom-persona` skill.
|
|
217
|
-
The voice is direct and dry, free of filler
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
adds block the mutation; flattering
|
|
221
|
-
AI meta-commentary are answered
|
|
222
|
-
no profile selection to store.
|
|
222
|
+
The voice is direct and dry, free of filler. It uses everyday words even when
|
|
223
|
+
you use technical terms. It keeps a technical term only when needed to
|
|
224
|
+
understand or act, and explains it the first time. Emojis and decorative
|
|
225
|
+
symbols in the lines a `write`/`edit` adds block the mutation; flattering
|
|
226
|
+
openers, hedging, automatic apologies, and AI meta-commentary are answered
|
|
227
|
+
with a capped steer. No profiles to switch and no profile selection to store.
|
|
223
228
|
|
|
224
229
|
### Done — done means green
|
|
225
230
|
|
|
@@ -359,10 +364,12 @@ itself.
|
|
|
359
364
|
arrive after the review fired; nothing blocks and nothing reaches the system
|
|
360
365
|
prompt.
|
|
361
366
|
- One request per successful `write`/`edit` of a project file in a supported
|
|
362
|
-
language that added lines, with
|
|
363
|
-
|
|
364
|
-
-
|
|
365
|
-
|
|
367
|
+
language that added lines, with applicable `noul` questions on the complete
|
|
368
|
+
changed file and up to four directly related source files.
|
|
369
|
+
- The official 32k-token state-plus-longest-question and 64k-token request
|
|
370
|
+
limits are observed conservatively using 32,000 and 64,000 UTF-8 bytes.
|
|
371
|
+
Oversized neighbours are omitted whole; if the changed file cannot fit,
|
|
372
|
+
the call is skipped. An answer at or above `0.8` becomes a note.
|
|
366
373
|
- The request follows the run's abort signal; a cancelled request is not a
|
|
367
374
|
failure.
|
|
368
375
|
- Quota (`402`) and a rejected key (`401`, `403`) stop the calls immediately. A
|
package/extensions/ask/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ export const ASK_PROMPT_SNIPPET =
|
|
|
15
15
|
'Ask the user one or more questions with a recommended option, in their language.';
|
|
16
16
|
|
|
17
17
|
export const ASK_PROMPT_GUIDELINES = [
|
|
18
|
-
'
|
|
19
|
-
'
|
|
18
|
+
'Follow the sideroom-grill skill before non-trivial work without a settled spec. One sideroom_ask call is one batch; call again for another round.',
|
|
19
|
+
'Read the repository first: reuse a consistent project fact. An architectural decision (boundaries, dependencies, cost, security, operations, persistence, public contracts, future change difficulty) is never settled silently: never assume it. Follow the sideroom-architecture skill; ask through sideroom_ask with practical consequences and a one-line justification for one recommendation. Name inconsistent patterns in the prompt. Take minor decisions yourself.',
|
|
20
|
+
'Recommend evidence-backed long-term quality: maintainability and growth for known needs over initial speed. State the upfront cost; avoid speculative capacity. If decisive facts or figures are missing, ask for them first through sideroom_ask; recommend gathering evidence, not an unsupported solution.',
|
|
20
21
|
'Write sideroom_ask prompts, tab labels, option labels, and option descriptions in the language the user is speaking; keep ids and option values in English.',
|
|
21
22
|
'sideroom_ask always adds Out of scope and a write-your-own answer; never send them yourself. A single-selection question needs recommendationIndex; a multiple-selection question needs recommendedIndices and never recommendationIndex.',
|
|
22
23
|
'sideroom_ask only works in the interactive TUI; it errors in print, JSON, and RPC modes.',
|
package/extensions/jev/client.ts
CHANGED
|
@@ -105,7 +105,8 @@ export async function askJev(
|
|
|
105
105
|
file: JevFileState,
|
|
106
106
|
signal: AbortSignal | undefined,
|
|
107
107
|
): Promise<JevAttempt> {
|
|
108
|
-
const
|
|
108
|
+
const requestBody = buildRequestBody(file);
|
|
109
|
+
const body = JSON.stringify(requestBody);
|
|
109
110
|
let response: JevHttpResponse;
|
|
110
111
|
try {
|
|
111
112
|
response = await transport({ apiKey, body, signal });
|
|
@@ -130,7 +131,10 @@ export async function askJev(
|
|
|
130
131
|
return { findings: [], model: undefined, failure: undefined };
|
|
131
132
|
}
|
|
132
133
|
return {
|
|
133
|
-
findings: readFindings(
|
|
134
|
+
findings: readFindings(
|
|
135
|
+
envelope,
|
|
136
|
+
new Set(Object.keys(requestBody.questions)),
|
|
137
|
+
),
|
|
134
138
|
model: envelope.model,
|
|
135
139
|
failure: undefined,
|
|
136
140
|
};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, join, posix, relative, sep } from 'node:path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
import { LANGUAGE, languageForPath } from '../rules/catalog.ts';
|
|
5
|
+
import {
|
|
6
|
+
buildFileState,
|
|
7
|
+
type JevFileState,
|
|
8
|
+
type JevRelatedFile,
|
|
9
|
+
stateFitsBudget,
|
|
10
|
+
} from './model.ts';
|
|
11
|
+
|
|
12
|
+
const MAX_RELATED_FILES = 4;
|
|
13
|
+
const MAX_SCAN_FILES = 1_024;
|
|
14
|
+
const MAX_SCAN_DEPTH = 10;
|
|
15
|
+
const MAX_CANDIDATE_BYTES = 32_000;
|
|
16
|
+
const IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'] as const;
|
|
17
|
+
const SENSITIVE_NAME =
|
|
18
|
+
/secret|credential|password|token|private.?key|api.?key|auth.?key|(?:^|[._-])(?:env|private|key)(?:[._-]|$)/iu;
|
|
19
|
+
const SKIP_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'coverage']);
|
|
20
|
+
|
|
21
|
+
interface Candidate {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly body: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ScanResult {
|
|
27
|
+
readonly candidates: readonly Candidate[];
|
|
28
|
+
readonly partial: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Keep the edited file complete. Other files are included whole or not at all.
|
|
32
|
+
export function buildContextualState(
|
|
33
|
+
cwd: string,
|
|
34
|
+
path: string,
|
|
35
|
+
body: string,
|
|
36
|
+
kind: string,
|
|
37
|
+
lines: Parameters<typeof buildFileState>[3],
|
|
38
|
+
): JevFileState | undefined {
|
|
39
|
+
const base = buildFileState(path, body, kind, lines);
|
|
40
|
+
if (!stateFitsBudget(base)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const scan = scanSourceFiles(cwd, path);
|
|
45
|
+
const imported = references(path, body);
|
|
46
|
+
const related: JevRelatedFile[] = [];
|
|
47
|
+
let partial = scan.partial;
|
|
48
|
+
const matches = scan.candidates
|
|
49
|
+
.flatMap((candidate) => {
|
|
50
|
+
const importedByTarget = matchesReference(imported, candidate.path);
|
|
51
|
+
const consumesTarget = matchesReference(
|
|
52
|
+
references(candidate.path, candidate.body),
|
|
53
|
+
path,
|
|
54
|
+
);
|
|
55
|
+
if (!importedByTarget && !consumesTarget) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
...candidate,
|
|
61
|
+
relation: importedByTarget ? 'imported' : 'consumer',
|
|
62
|
+
} as const,
|
|
63
|
+
];
|
|
64
|
+
})
|
|
65
|
+
.sort((left, right) => {
|
|
66
|
+
if (left.relation !== right.relation) {
|
|
67
|
+
return left.relation === 'imported' ? -1 : 1;
|
|
68
|
+
}
|
|
69
|
+
return compareText(left.path, right.path);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
for (const candidate of matches) {
|
|
73
|
+
if (related.length >= MAX_RELATED_FILES) {
|
|
74
|
+
partial = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const next = [...related, candidate];
|
|
78
|
+
if (!stateFitsBudget(buildFileState(path, body, kind, lines, next))) {
|
|
79
|
+
partial = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
related.push(candidate);
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
[...imported].some(
|
|
86
|
+
(name) =>
|
|
87
|
+
!related.some((file) => matchesReference(new Set([name]), file.path)),
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
partial = true;
|
|
91
|
+
}
|
|
92
|
+
while (
|
|
93
|
+
related.length > 0 &&
|
|
94
|
+
!stateFitsBudget(buildFileState(path, body, kind, lines, related, partial))
|
|
95
|
+
) {
|
|
96
|
+
related.pop();
|
|
97
|
+
partial = true;
|
|
98
|
+
}
|
|
99
|
+
const state = buildFileState(path, body, kind, lines, related, partial);
|
|
100
|
+
return stateFitsBudget(state) ? state : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scanSourceFiles(cwd: string, editedPath: string): ScanResult {
|
|
104
|
+
const root = realpathSync(cwd);
|
|
105
|
+
const candidates: Candidate[] = [];
|
|
106
|
+
let visited = 0;
|
|
107
|
+
let partial = false;
|
|
108
|
+
|
|
109
|
+
function visit(
|
|
110
|
+
directory: string,
|
|
111
|
+
depth: number,
|
|
112
|
+
patterns: readonly string[],
|
|
113
|
+
): void {
|
|
114
|
+
if (depth > MAX_SCAN_DEPTH || visited >= MAX_SCAN_FILES) {
|
|
115
|
+
partial = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const localPatterns = [...patterns, ...readIgnorePatterns(directory, root)];
|
|
119
|
+
const matcher = ignore().add(localPatterns);
|
|
120
|
+
const entries = readdirSync(directory, { withFileTypes: true }).sort(
|
|
121
|
+
(a, b) => compareText(a.name, b.name),
|
|
122
|
+
);
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (visited >= MAX_SCAN_FILES) {
|
|
125
|
+
partial = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const absolute = join(directory, entry.name);
|
|
129
|
+
const path = relative(root, absolute).split(sep).join('/');
|
|
130
|
+
if (entry.name.startsWith('.') || SENSITIVE_NAME.test(entry.name)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
if (!SKIP_DIRECTORIES.has(entry.name) && !matcher.ignores(`${path}/`)) {
|
|
135
|
+
visit(absolute, depth + 1, localPatterns);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
!entry.isFile() ||
|
|
141
|
+
matcher.ignores(path) ||
|
|
142
|
+
path === editedPath ||
|
|
143
|
+
languageForPath(path) === LANGUAGE.generic
|
|
144
|
+
) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
visited += 1;
|
|
148
|
+
if (statSync(absolute).size > MAX_CANDIDATE_BYTES) {
|
|
149
|
+
partial = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
candidates.push({ path, body: readFileSync(absolute, 'utf8') });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
visit(root, 0, []);
|
|
157
|
+
return { candidates, partial };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readIgnorePatterns(directory: string, root: string): string[] {
|
|
161
|
+
const prefix = relative(root, directory).split(sep).join('/');
|
|
162
|
+
const rules: string[] = [];
|
|
163
|
+
for (const name of IGNORE_FILES) {
|
|
164
|
+
const file = join(directory, name);
|
|
165
|
+
if (!exists(file)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/u)) {
|
|
169
|
+
const trimmed = line.trim();
|
|
170
|
+
if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('\\#'))) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const negative = line.startsWith('!');
|
|
174
|
+
const pattern = (negative ? line.slice(1) : line).replace(/^\//u, '');
|
|
175
|
+
rules.push(
|
|
176
|
+
`${negative ? '!' : ''}${prefix ? `${prefix}/` : ''}${pattern}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return rules;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function exists(path: string): boolean {
|
|
184
|
+
try {
|
|
185
|
+
statSync(path);
|
|
186
|
+
return true;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function modulePath(path: string): string {
|
|
196
|
+
const extension = extname(path);
|
|
197
|
+
const withoutExtension = extension ? path.slice(0, -extension.length) : path;
|
|
198
|
+
return withoutExtension.endsWith('/index')
|
|
199
|
+
? withoutExtension.slice(0, -'/index'.length)
|
|
200
|
+
: withoutExtension;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function references(path: string, body: string): Set<string> {
|
|
204
|
+
const names = new Set<string>();
|
|
205
|
+
const language = languageForPath(path);
|
|
206
|
+
const code = body.replace(/\/\*[\s\S]*?\*\/|^[ \t]*\/\/[^\n]*$/gmu, '');
|
|
207
|
+
if (language === LANGUAGE.typescript || language === LANGUAGE.javascript) {
|
|
208
|
+
const imports = [
|
|
209
|
+
/^[ \t]*(?:import|export)\s+(?:\{[^}]*\}|[^\n;]+)\s*from\s*['"](\.[^'"]+)['"]/gmu,
|
|
210
|
+
/^[ \t]*import\s*['"](\.[^'"]+)['"]/gmu,
|
|
211
|
+
/^[ \t]*(?:(?:const|let|var)\s+[\w{} ,]+\s*=\s*)?require\s*\(\s*['"](\.[^'"]+)['"]/gmu,
|
|
212
|
+
];
|
|
213
|
+
for (const pattern of imports) {
|
|
214
|
+
for (const match of code.matchAll(pattern)) {
|
|
215
|
+
const specifier = match[1];
|
|
216
|
+
if (specifier) {
|
|
217
|
+
names.add(
|
|
218
|
+
modulePath(
|
|
219
|
+
posix.normalize(posix.join(posix.dirname(path), specifier)),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (language === LANGUAGE.python) {
|
|
227
|
+
for (const match of code.matchAll(
|
|
228
|
+
/^[ \t]*from\s+(\.+)([\w.]*)\s+import\b/gmu,
|
|
229
|
+
)) {
|
|
230
|
+
const dots = match[1]?.length ?? 0;
|
|
231
|
+
const specifier = match[2]?.replaceAll('.', '/') ?? '';
|
|
232
|
+
const parent = posix.resolve(
|
|
233
|
+
'/',
|
|
234
|
+
posix.dirname(path),
|
|
235
|
+
...Array(dots - 1).fill('..'),
|
|
236
|
+
);
|
|
237
|
+
names.add(modulePath(posix.join(parent.slice(1), specifier)));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (language === LANGUAGE.rust) {
|
|
241
|
+
let sourceRoot = posix.dirname(path);
|
|
242
|
+
if (path.startsWith('src/')) {
|
|
243
|
+
sourceRoot = 'src';
|
|
244
|
+
} else if (path.includes('/src/')) {
|
|
245
|
+
sourceRoot = path.slice(0, path.indexOf('/src/') + '/src'.length);
|
|
246
|
+
}
|
|
247
|
+
for (const match of code.matchAll(
|
|
248
|
+
/^[ \t]*(?:pub\s+)?(?:use|mod)\s+(crate::)?([\w:]+)\s*[;{]/gmu,
|
|
249
|
+
)) {
|
|
250
|
+
const specifier = match[2]?.replaceAll('::', '/');
|
|
251
|
+
if (specifier) {
|
|
252
|
+
const from = match[1] ? sourceRoot : posix.dirname(path);
|
|
253
|
+
names.add(modulePath(posix.join(from, specifier)));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (language === LANGUAGE.php) {
|
|
258
|
+
for (const match of code.matchAll(
|
|
259
|
+
/^[ \t]*(?:require|include)(?:_once)?\s*(?:\(?\s*__DIR__\s*\.\s*)?['"](\.[^'"]+|\/[^'"]+)['"]/gmu,
|
|
260
|
+
)) {
|
|
261
|
+
const specifier = match[1];
|
|
262
|
+
if (specifier) {
|
|
263
|
+
names.add(
|
|
264
|
+
modulePath(
|
|
265
|
+
posix.normalize(posix.join(posix.dirname(path), specifier)),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (language === LANGUAGE.java) {
|
|
272
|
+
for (const match of code.matchAll(
|
|
273
|
+
/^[ \t]*import\s+(?:static\s+)?([\w.]+)\s*;/gmu,
|
|
274
|
+
)) {
|
|
275
|
+
const specifier = match[1]?.replaceAll('.', '/');
|
|
276
|
+
if (specifier) {
|
|
277
|
+
names.add(specifier);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (language === LANGUAGE.go) {
|
|
282
|
+
for (const declaration of code.matchAll(
|
|
283
|
+
/^[ \t]*import\s*(?:\(([^)]*)\)|"([^"\n]+)")/gmu,
|
|
284
|
+
)) {
|
|
285
|
+
const quoted = declaration[1] ?? `"${declaration[2] ?? ''}"`;
|
|
286
|
+
for (const match of quoted.matchAll(/"([^"\n]+)"/gu)) {
|
|
287
|
+
if (match[1]) {
|
|
288
|
+
names.add(match[1]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return names;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function compareText(left: string, right: string): number {
|
|
297
|
+
if (left < right) {
|
|
298
|
+
return -1;
|
|
299
|
+
}
|
|
300
|
+
if (left > right) {
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function matchesReference(names: ReadonlySet<string>, path: string): boolean {
|
|
307
|
+
const target = modulePath(path);
|
|
308
|
+
if (names.has(target)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
for (const name of names) {
|
|
312
|
+
if (path.endsWith('.java') && target.endsWith(`/${name}`)) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (path.endsWith('.go')) {
|
|
316
|
+
const packageParts = name.split('/');
|
|
317
|
+
const packageSuffix = packageParts.slice(-2).join('/');
|
|
318
|
+
if (
|
|
319
|
+
packageParts.length >= 2 &&
|
|
320
|
+
posix.dirname(path).endsWith(`/${packageSuffix}`)
|
|
321
|
+
) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
package/extensions/jev/guard.ts
CHANGED
|
@@ -19,16 +19,15 @@ import {
|
|
|
19
19
|
type JevFailure,
|
|
20
20
|
type JevTransport,
|
|
21
21
|
} from './client.ts';
|
|
22
|
+
import { buildContextualState } from './context.ts';
|
|
22
23
|
import {
|
|
23
24
|
type AddedLine,
|
|
24
25
|
addedLinesForEdit,
|
|
25
26
|
addedLinesForWrite,
|
|
26
|
-
buildFileState,
|
|
27
27
|
findingKey,
|
|
28
28
|
formatFindings,
|
|
29
29
|
isReviewablePath,
|
|
30
30
|
type JevFinding,
|
|
31
|
-
stateFitsBudget,
|
|
32
31
|
} from './model.ts';
|
|
33
32
|
|
|
34
33
|
export const JEV_TRANSIENT_FAILURE_LIMIT = 3;
|
|
@@ -177,13 +176,17 @@ export function registerJevGuard(
|
|
|
177
176
|
if (body === undefined) {
|
|
178
177
|
return undefined;
|
|
179
178
|
}
|
|
180
|
-
|
|
179
|
+
if (ctx.signal?.aborted === true) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const fileState = buildContextualState(
|
|
183
|
+
ctx.cwd,
|
|
181
184
|
pending.relativePath,
|
|
182
185
|
body,
|
|
183
186
|
pending.kind,
|
|
184
187
|
pending.lines,
|
|
185
188
|
);
|
|
186
|
-
if (
|
|
189
|
+
if (fileState === undefined) {
|
|
187
190
|
return undefined;
|
|
188
191
|
}
|
|
189
192
|
|
|
@@ -269,7 +272,7 @@ function registerSuccess(state: JevGuardState): void {
|
|
|
269
272
|
state.transientFailures = 0;
|
|
270
273
|
}
|
|
271
274
|
|
|
272
|
-
// A deletion-only edit leaves nothing for the
|
|
275
|
+
// A deletion-only edit leaves nothing for the questions to read, and only
|
|
273
276
|
// the project-relative path of a supported language may leave the machine.
|
|
274
277
|
function isWorthAsking(mutation: PendingMutation): boolean {
|
|
275
278
|
return mutation.lines.length > 0 && isReviewablePath(mutation.relativePath);
|
package/extensions/jev/model.ts
CHANGED
|
@@ -11,12 +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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
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;
|
|
20
19
|
|
|
21
20
|
const NOUL_TYPE = 'noul';
|
|
22
21
|
|
|
@@ -31,8 +30,7 @@ export interface JevRule {
|
|
|
31
30
|
readonly question: JevNoulQuestion;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
//
|
|
35
|
-
// 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.
|
|
36
34
|
export const JEV_RULES: readonly JevRule[] = [
|
|
37
35
|
{
|
|
38
36
|
id: 'guard-clauses',
|
|
@@ -65,10 +63,11 @@ export const JEV_RULES: readonly JevRule[] = [
|
|
|
65
63
|
question: {
|
|
66
64
|
type: NOUL_TYPE,
|
|
67
65
|
instructions:
|
|
68
|
-
'In `change.addedLines`, does a function
|
|
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.',
|
|
69
67
|
criteria: {
|
|
70
|
-
true: 'A function
|
|
71
|
-
false:
|
|
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',
|
|
72
71
|
},
|
|
73
72
|
},
|
|
74
73
|
},
|
|
@@ -102,15 +101,60 @@ export const JEV_RULES: readonly JevRule[] = [
|
|
|
102
101
|
question: {
|
|
103
102
|
type: NOUL_TYPE,
|
|
104
103
|
instructions:
|
|
105
|
-
'
|
|
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.',
|
|
106
105
|
criteria: {
|
|
107
|
-
true: '
|
|
108
|
-
false:
|
|
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.',
|
|
118
|
+
criteria: {
|
|
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',
|
|
109
147
|
},
|
|
110
148
|
},
|
|
111
149
|
},
|
|
112
150
|
];
|
|
113
151
|
|
|
152
|
+
export interface JevRelatedFile {
|
|
153
|
+
readonly path: string;
|
|
154
|
+
readonly body: string;
|
|
155
|
+
readonly relation: 'imported' | 'consumer';
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
export interface JevFileState {
|
|
115
159
|
readonly file: {
|
|
116
160
|
readonly path: string;
|
|
@@ -121,6 +165,10 @@ export interface JevFileState {
|
|
|
121
165
|
readonly kind: string;
|
|
122
166
|
readonly addedLines: readonly string[];
|
|
123
167
|
};
|
|
168
|
+
readonly context: {
|
|
169
|
+
readonly files: readonly JevRelatedFile[];
|
|
170
|
+
readonly partial: boolean;
|
|
171
|
+
};
|
|
124
172
|
}
|
|
125
173
|
|
|
126
174
|
export interface JevRequestBody {
|
|
@@ -148,10 +196,13 @@ export function buildFileState(
|
|
|
148
196
|
body: string,
|
|
149
197
|
kind: string,
|
|
150
198
|
lines: readonly AddedLine[],
|
|
199
|
+
relatedFiles: readonly JevRelatedFile[] = [],
|
|
200
|
+
partial = false,
|
|
151
201
|
): JevFileState {
|
|
152
202
|
return {
|
|
153
203
|
file: { path, language: languageForPath(path), body },
|
|
154
204
|
change: { kind, addedLines: lines.map((line) => line.text) },
|
|
205
|
+
context: { files: relatedFiles, partial },
|
|
155
206
|
};
|
|
156
207
|
}
|
|
157
208
|
|
|
@@ -168,19 +219,38 @@ export function addedLinesForEdit(
|
|
|
168
219
|
return edits.flatMap((edit) => addedLines(edit.oldText, edit.newText));
|
|
169
220
|
}
|
|
170
221
|
|
|
171
|
-
//
|
|
172
|
-
// spend a call on answers that mean nothing.
|
|
222
|
+
// Questions are about code structure; a README or a lockfile has no useful state.
|
|
173
223
|
export function isReviewablePath(path: string): boolean {
|
|
174
224
|
return languageForPath(path) !== LANGUAGE.generic;
|
|
175
225
|
}
|
|
176
226
|
|
|
177
227
|
export function stateFitsBudget(state: JevFileState): boolean {
|
|
178
|
-
|
|
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
|
+
);
|
|
179
239
|
}
|
|
180
240
|
|
|
181
241
|
export function buildRequestBody(file: JevFileState): JevRequestBody {
|
|
182
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');
|
|
183
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
|
+
}
|
|
184
254
|
questions[rule.id] = rule.question;
|
|
185
255
|
}
|
|
186
256
|
return { model: JEV_MODEL, state: file, questions };
|
|
@@ -197,6 +267,7 @@ export function parseEnvelope(text: string): JevResponseEnvelope | undefined {
|
|
|
197
267
|
|
|
198
268
|
export function readFindings(
|
|
199
269
|
envelope: JevResponseEnvelope,
|
|
270
|
+
askedRuleIds: ReadonlySet<string> = new Set(JEV_RULES.map((rule) => rule.id)),
|
|
200
271
|
): readonly JevFinding[] {
|
|
201
272
|
const answers = envelope.answers;
|
|
202
273
|
if (answers === undefined) {
|
|
@@ -204,6 +275,9 @@ export function readFindings(
|
|
|
204
275
|
}
|
|
205
276
|
const findings: JevFinding[] = [];
|
|
206
277
|
for (const rule of JEV_RULES) {
|
|
278
|
+
if (!askedRuleIds.has(rule.id)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
207
281
|
const answer = answers[rule.id];
|
|
208
282
|
if (answer === undefined || answer.noul < VIOLATION_PROBABILITY) {
|
|
209
283
|
continue;
|
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
22
|
-
"Do:
|
|
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}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "8.
|
|
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": [
|
|
@@ -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
|
-
|
|
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
|
|
81
|
-
`sideroom_ask` round: situation,
|
|
82
|
-
|
|
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
|
|
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 | `
|
|
26
|
-
| 2 | Direct and dry | `The
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- Do: `
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
|
@@ -90,3 +92,8 @@ At most three steers are sent per agent run.
|
|
|
90
92
|
- Nothing is written to the target repository.
|
|
91
93
|
- The persona governs wording. It never relaxes the coding guidelines, the
|
|
92
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.
|