@rmrdeveloper/sideroom-pi 8.10.0 → 8.12.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 +63 -28
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
- package/extensions/ask/index.ts +3 -2
- package/extensions/done/guard.ts +42 -37
- package/extensions/done/index.ts +3 -2
- package/extensions/done/model.ts +1 -5
- package/extensions/explain/guard.ts +13 -11
- package/extensions/explain/model.ts +2 -25
- package/extensions/guidelines/review.ts +31 -13
- package/extensions/jev/client.ts +27 -2
- package/extensions/jev/context.ts +360 -0
- package/extensions/jev/guard.ts +116 -34
- package/extensions/jev/model.ts +175 -23
- package/extensions/jev/workflow.ts +188 -0
- package/extensions/persona/catalog.ts +2 -2
- package/extensions/persona/execute.ts +4 -6
- package/extensions/persona/index.ts +3 -2
- package/extensions/persona/model.ts +1 -31
- package/extensions/persona/prompt.ts +5 -3
- package/extensions/shared/settle.ts +63 -13
- package/package.json +1 -1
- package/skills/sideroom-architecture/SKILL.md +17 -8
- package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
- package/skills/sideroom-codebase-design/SKILL.md +151 -0
- package/skills/sideroom-codebase-design/references/growth.md +62 -0
- package/skills/sideroom-domain-modeling/SKILL.md +1 -1
- package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
- package/skills/sideroom-grill/SKILL.md +16 -6
- package/skills/sideroom-guidelines/SKILL.md +9 -7
- package/skills/sideroom-guidelines/references/languages/go.md +3 -3
- package/skills/sideroom-guidelines/references/languages/java.md +3 -3
- package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
- package/skills/sideroom-guidelines/references/languages/python.md +3 -3
- package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
- package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
- package/skills/sideroom-persona/SKILL.md +43 -25
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,118 @@ 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.',
|
|
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.',
|
|
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',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'testability',
|
|
152
|
+
question: {
|
|
153
|
+
type: NOUL_TYPE,
|
|
154
|
+
instructions:
|
|
155
|
+
'Do the added lines hardcode clock, network or randomness inside business logic, making deterministic tests difficult? Do not flag infrastructure adapters or entry points merely for using their own APIs.',
|
|
156
|
+
criteria: {
|
|
157
|
+
true: 'Business behavior directly depends on an uncontrollable external dependency with no visible injection point',
|
|
158
|
+
false:
|
|
159
|
+
'Dependencies are supplied, or the code is the adapter/composition boundary itself',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'contract-compatibility',
|
|
165
|
+
question: {
|
|
166
|
+
type: NOUL_TYPE,
|
|
167
|
+
instructions:
|
|
168
|
+
'Do added lines change a function signature, return shape or behavior in a way that demonstrably conflicts with a supplied non-test consumer? Require a concrete mismatch in visible code; do not speculate about unseen callers.',
|
|
169
|
+
criteria: {
|
|
170
|
+
true: 'A supplied consumer calls or interprets the changed contract incompatibly',
|
|
171
|
+
false:
|
|
172
|
+
'Visible consumers remain compatible, or there is insufficient evidence',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'test-quality',
|
|
178
|
+
question: {
|
|
179
|
+
type: NOUL_TYPE,
|
|
180
|
+
instructions:
|
|
181
|
+
'For behavior touched by the added lines, do supplied related tests have a demonstrable blind spot: assertions that cannot detect that behavior failing, or an explicit changed error branch left untested? Use both production and test bodies. Never infer absent repository coverage from missing files or partial context. Do not request tests that mirror implementation or cover unrelated code.',
|
|
182
|
+
criteria: {
|
|
183
|
+
true: 'Visible relevant tests fail to assert the changed observable behavior or a visible changed error case, with concrete evidence',
|
|
184
|
+
false:
|
|
185
|
+
'Assertions cover the behavior, the change is low-impact, or evidence is incomplete',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'unnecessary-abstraction',
|
|
191
|
+
question: {
|
|
192
|
+
type: NOUL_TYPE,
|
|
193
|
+
instructions:
|
|
194
|
+
'Do the added lines introduce a demonstrably redundant layer or combine visibly different domain concepts solely for similar syntax? Require evidence in supplied bodies. A single caller, delegation, injection, compatibility or a clear responsibility is not by itself a violation. Never infer requirements are absent or recommend removing required behavior.',
|
|
106
195
|
criteria: {
|
|
107
|
-
true: '
|
|
108
|
-
false:
|
|
196
|
+
true: 'The added abstraction duplicates an existing role without adding a contract, responsibility or testability, or mixes visibly distinct concepts',
|
|
197
|
+
false:
|
|
198
|
+
'It serves a present responsibility or contract, or evidence is insufficient',
|
|
109
199
|
},
|
|
110
200
|
},
|
|
111
201
|
},
|
|
112
202
|
];
|
|
113
203
|
|
|
204
|
+
export function isTestPath(path: string): boolean {
|
|
205
|
+
return /(?:^|\/)(?:__tests__|tests?|specs?)\/|(?:[._-](?:test|spec)|_test)\.[^/]+$|(?:^|\/)test_[^/]+$|Test\.java$/u.test(
|
|
206
|
+
path,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface JevRelatedFile {
|
|
211
|
+
readonly path: string;
|
|
212
|
+
readonly body: string;
|
|
213
|
+
readonly relation: 'imported' | 'consumer' | 'test';
|
|
214
|
+
}
|
|
215
|
+
|
|
114
216
|
export interface JevFileState {
|
|
115
217
|
readonly file: {
|
|
116
218
|
readonly path: string;
|
|
@@ -121,11 +223,15 @@ export interface JevFileState {
|
|
|
121
223
|
readonly kind: string;
|
|
122
224
|
readonly addedLines: readonly string[];
|
|
123
225
|
};
|
|
226
|
+
readonly context: {
|
|
227
|
+
readonly files: readonly JevRelatedFile[];
|
|
228
|
+
readonly partial: boolean;
|
|
229
|
+
};
|
|
124
230
|
}
|
|
125
231
|
|
|
126
232
|
export interface JevRequestBody {
|
|
127
233
|
readonly model: string;
|
|
128
|
-
readonly state:
|
|
234
|
+
readonly state: unknown;
|
|
129
235
|
readonly questions: Readonly<Record<string, JevNoulQuestion>>;
|
|
130
236
|
}
|
|
131
237
|
|
|
@@ -148,10 +254,13 @@ export function buildFileState(
|
|
|
148
254
|
body: string,
|
|
149
255
|
kind: string,
|
|
150
256
|
lines: readonly AddedLine[],
|
|
257
|
+
relatedFiles: readonly JevRelatedFile[] = [],
|
|
258
|
+
partial = false,
|
|
151
259
|
): JevFileState {
|
|
152
260
|
return {
|
|
153
261
|
file: { path, language: languageForPath(path), body },
|
|
154
262
|
change: { kind, addedLines: lines.map((line) => line.text) },
|
|
263
|
+
context: { files: relatedFiles, partial },
|
|
155
264
|
};
|
|
156
265
|
}
|
|
157
266
|
|
|
@@ -168,19 +277,56 @@ export function addedLinesForEdit(
|
|
|
168
277
|
return edits.flatMap((edit) => addedLines(edit.oldText, edit.newText));
|
|
169
278
|
}
|
|
170
279
|
|
|
171
|
-
//
|
|
172
|
-
// spend a call on answers that mean nothing.
|
|
280
|
+
// Questions are about code structure; a README or a lockfile has no useful state.
|
|
173
281
|
export function isReviewablePath(path: string): boolean {
|
|
174
282
|
return languageForPath(path) !== LANGUAGE.generic;
|
|
175
283
|
}
|
|
176
284
|
|
|
177
285
|
export function stateFitsBudget(state: JevFileState): boolean {
|
|
178
|
-
return
|
|
286
|
+
return requestFitsBudget(buildRequestBody(state));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function requestFitsBudget(request: JevRequestBody): boolean {
|
|
290
|
+
const longestQuestion = Object.values(request.questions).reduce(
|
|
291
|
+
(longest, question) =>
|
|
292
|
+
Math.max(longest, Buffer.byteLength(JSON.stringify(question))),
|
|
293
|
+
0,
|
|
294
|
+
);
|
|
295
|
+
return (
|
|
296
|
+
Buffer.byteLength(JSON.stringify(request.state)) + longestQuestion <=
|
|
297
|
+
MAX_STATE_BYTES &&
|
|
298
|
+
Buffer.byteLength(JSON.stringify(request)) <= MAX_REQUEST_BYTES
|
|
299
|
+
);
|
|
179
300
|
}
|
|
180
301
|
|
|
181
302
|
export function buildRequestBody(file: JevFileState): JevRequestBody {
|
|
182
303
|
const questions: Record<string, JevNoulQuestion> = {};
|
|
304
|
+
const hasImportedFile = file.context.files.some(
|
|
305
|
+
(related) => related.relation === 'imported',
|
|
306
|
+
);
|
|
307
|
+
const hasConsumer = file.context.files.some(
|
|
308
|
+
(related) => related.relation === 'consumer' && !isTestPath(related.path),
|
|
309
|
+
);
|
|
310
|
+
const hasTest =
|
|
311
|
+
isTestPath(file.file.path) ||
|
|
312
|
+
file.context.files.some((related) => isTestPath(related.path));
|
|
313
|
+
const hasProduction =
|
|
314
|
+
!isTestPath(file.file.path) ||
|
|
315
|
+
file.context.files.some((related) => !isTestPath(related.path));
|
|
316
|
+
const addedText = file.change.addedLines.join('\n');
|
|
183
317
|
for (const rule of JEV_RULES) {
|
|
318
|
+
if (rule.id === 'dependency-direction' && !hasImportedFile) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (rule.id === 'comments' && !/(?:\/\/|\/\*|#)/u.test(addedText)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (rule.id === 'contract-compatibility' && !hasConsumer) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (rule.id === 'test-quality' && (!hasTest || !hasProduction)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
184
330
|
questions[rule.id] = rule.question;
|
|
185
331
|
}
|
|
186
332
|
return { model: JEV_MODEL, state: file, questions };
|
|
@@ -197,18 +343,24 @@ export function parseEnvelope(text: string): JevResponseEnvelope | undefined {
|
|
|
197
343
|
|
|
198
344
|
export function readFindings(
|
|
199
345
|
envelope: JevResponseEnvelope,
|
|
346
|
+
askedRuleIds: ReadonlySet<string> = new Set(JEV_RULES.map((rule) => rule.id)),
|
|
200
347
|
): readonly JevFinding[] {
|
|
201
348
|
const answers = envelope.answers;
|
|
202
349
|
if (answers === undefined) {
|
|
203
350
|
return [];
|
|
204
351
|
}
|
|
205
352
|
const findings: JevFinding[] = [];
|
|
206
|
-
for (const
|
|
207
|
-
const answer = answers[
|
|
208
|
-
if (
|
|
353
|
+
for (const ruleId of askedRuleIds) {
|
|
354
|
+
const answer = answers[ruleId];
|
|
355
|
+
if (
|
|
356
|
+
answer === undefined ||
|
|
357
|
+
!Number.isFinite(answer.noul) ||
|
|
358
|
+
answer.noul > 1 ||
|
|
359
|
+
answer.noul < VIOLATION_PROBABILITY
|
|
360
|
+
) {
|
|
209
361
|
continue;
|
|
210
362
|
}
|
|
211
|
-
findings.push({ ruleId
|
|
363
|
+
findings.push({ ruleId, probability: answer.noul });
|
|
212
364
|
}
|
|
213
365
|
return findings;
|
|
214
366
|
}
|
|
@@ -225,7 +377,7 @@ export function formatFindings(
|
|
|
225
377
|
`Sideroom Jev review flagged ${path}:`,
|
|
226
378
|
...findings.map(
|
|
227
379
|
(finding) =>
|
|
228
|
-
`- [${finding.ruleId}] Jev scored ${finding.probability.toFixed(2)} probability
|
|
380
|
+
`- [${finding.ruleId}] Jev scored ${finding.probability.toFixed(2)} probability of a review concern. Verify the supplied evidence and applicable contract against the loaded guide before changing anything. Missing context is not proof of a violation.`,
|
|
229
381
|
),
|
|
230
382
|
].join('\n');
|
|
231
383
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { type AskParams, parseAskParams } from '../ask/model.ts';
|
|
2
|
+
import {
|
|
3
|
+
parseTodoParams,
|
|
4
|
+
TODO_STATUS,
|
|
5
|
+
type TodoItem,
|
|
6
|
+
type TodoToolParameters,
|
|
7
|
+
} from '../todo/model.ts';
|
|
8
|
+
import {
|
|
9
|
+
JEV_MODEL,
|
|
10
|
+
type JevFileState,
|
|
11
|
+
type JevNoulQuestion,
|
|
12
|
+
type JevRequestBody,
|
|
13
|
+
requestFitsBudget,
|
|
14
|
+
} from './model.ts';
|
|
15
|
+
|
|
16
|
+
const MAX_EVIDENCE_FILES = 4;
|
|
17
|
+
|
|
18
|
+
type ChangedFileEvidence = Pick<JevFileState, 'file' | 'change'>;
|
|
19
|
+
|
|
20
|
+
export interface WorkflowEvidence {
|
|
21
|
+
readonly files: Map<string, ChangedFileEvidence>;
|
|
22
|
+
readonly completed: Map<string, string>;
|
|
23
|
+
partial: boolean;
|
|
24
|
+
checks: 'unknown' | 'passed' | 'failed' | 'stale';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createWorkflowEvidence(): WorkflowEvidence {
|
|
28
|
+
return {
|
|
29
|
+
files: new Map(),
|
|
30
|
+
completed: new Map(),
|
|
31
|
+
partial: false,
|
|
32
|
+
checks: 'unknown',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resetWorkflowEvidence(evidence: WorkflowEvidence): void {
|
|
37
|
+
evidence.files.clear();
|
|
38
|
+
evidence.completed.clear();
|
|
39
|
+
evidence.partial = false;
|
|
40
|
+
evidence.checks = 'unknown';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function recordFileEvidence(
|
|
44
|
+
evidence: WorkflowEvidence,
|
|
45
|
+
file: JevFileState,
|
|
46
|
+
): void {
|
|
47
|
+
evidence.checks = 'stale';
|
|
48
|
+
evidence.files.delete(file.file.path);
|
|
49
|
+
evidence.files.set(file.file.path, { file: file.file, change: file.change });
|
|
50
|
+
if (evidence.files.size > MAX_EVIDENCE_FILES) {
|
|
51
|
+
const oldest = evidence.files.keys().next().value;
|
|
52
|
+
if (oldest !== undefined) {
|
|
53
|
+
evidence.files.delete(oldest);
|
|
54
|
+
}
|
|
55
|
+
evidence.partial = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function question(instructions: string, violation: string): JevNoulQuestion {
|
|
60
|
+
return {
|
|
61
|
+
type: 'noul',
|
|
62
|
+
instructions,
|
|
63
|
+
criteria: {
|
|
64
|
+
true: violation,
|
|
65
|
+
false:
|
|
66
|
+
'No concrete contradiction is visible, or the supplied evidence is insufficient',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildAskReview(input: unknown): JevRequestBody | undefined {
|
|
72
|
+
const parsed = parseAskParams(input as AskParams);
|
|
73
|
+
if (!parsed.ok) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const request: JevRequestBody = {
|
|
77
|
+
model: JEV_MODEL,
|
|
78
|
+
state: { kind: 'questionnaire', questions: parsed.questions },
|
|
79
|
+
questions: {
|
|
80
|
+
'question-options': question(
|
|
81
|
+
'Do the supplied questions contain overlapping single-selection options or bundle independent decisions so no option can express a reasonable combination? Multiple selections may intentionally overlap. Judge only the question text, not unseen requirements.',
|
|
82
|
+
'The wording visibly prevents a clear independent choice',
|
|
83
|
+
),
|
|
84
|
+
'question-recommendation': question(
|
|
85
|
+
'Does a recommended option claim factual certainty or decisive cost/performance figures without support in the supplied question, or contradict its stated constraints? A preference alone need not include proof. Do not assume unseen project evidence is absent; flag only a demonstrably unsupported assertion or contradiction in the wording.',
|
|
86
|
+
'The recommendation visibly contradicts stated constraints or asserts an unsupported decisive fact',
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return requestFitsBudget(request) ? request : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function completedItems(
|
|
94
|
+
details: unknown,
|
|
95
|
+
): readonly TodoItem[] | undefined {
|
|
96
|
+
if (typeof details !== 'object' || details === null || 'error' in details) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const parsed = parseTodoParams({
|
|
100
|
+
action: 'propose',
|
|
101
|
+
items: 'items' in details ? details.items : undefined,
|
|
102
|
+
} as TodoToolParameters);
|
|
103
|
+
if (!parsed.ok || parsed.params.action !== 'propose') {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return parsed.params.items.filter(
|
|
107
|
+
(item) => item.status === TODO_STATUS.completed,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildBoardReview({
|
|
112
|
+
items,
|
|
113
|
+
evidence,
|
|
114
|
+
changedIds,
|
|
115
|
+
}: {
|
|
116
|
+
readonly items: readonly TodoItem[];
|
|
117
|
+
readonly evidence: WorkflowEvidence;
|
|
118
|
+
readonly changedIds?: ReadonlySet<string>;
|
|
119
|
+
}): JevRequestBody | undefined {
|
|
120
|
+
const newlyCompleted = items.filter(
|
|
121
|
+
(item) =>
|
|
122
|
+
(changedIds === undefined || changedIds.has(item.id)) &&
|
|
123
|
+
evidence.completed.get(item.id) !== item.content,
|
|
124
|
+
);
|
|
125
|
+
evidence.completed.clear();
|
|
126
|
+
for (const item of items) {
|
|
127
|
+
evidence.completed.set(item.id, item.content);
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
newlyCompleted.length === 0 ||
|
|
131
|
+
(evidence.files.size === 0 &&
|
|
132
|
+
evidence.checks !== 'passed' &&
|
|
133
|
+
evidence.checks !== 'failed')
|
|
134
|
+
) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const files: ChangedFileEvidence[] = [];
|
|
138
|
+
const state = {
|
|
139
|
+
kind: 'completion',
|
|
140
|
+
items: newlyCompleted,
|
|
141
|
+
evidence: { files, checks: evidence.checks, partial: evidence.partial },
|
|
142
|
+
};
|
|
143
|
+
const request: JevRequestBody = {
|
|
144
|
+
model: JEV_MODEL,
|
|
145
|
+
state,
|
|
146
|
+
questions: {
|
|
147
|
+
'board-completion': question(
|
|
148
|
+
'Do the newly completed tasks make a concrete claim contradicted by the supplied code changes or check status? Check status concerns only the detected project check in this user turn and becomes stale after a mutation. Missing evidence, unknown/stale checks and partial files do not prove unfinished work. Do not require every task to involve code or tests. Flag only an explicit mismatch, such as claiming checks passed when the observed latest checks failed.',
|
|
149
|
+
'A completed task claims behavior or a check result demonstrably contradicted by visible evidence',
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
for (const file of evidence.files.values()) {
|
|
154
|
+
files.push(file);
|
|
155
|
+
if (!requestFitsBudget(request)) {
|
|
156
|
+
files.pop();
|
|
157
|
+
state.evidence.partial = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return (files.length > 0 ||
|
|
161
|
+
evidence.checks === 'passed' ||
|
|
162
|
+
evidence.checks === 'failed') &&
|
|
163
|
+
requestFitsBudget(request)
|
|
164
|
+
? request
|
|
165
|
+
: undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function completionIds(input: unknown): ReadonlySet<string> {
|
|
169
|
+
const parsed = parseTodoParams(input as TodoToolParameters);
|
|
170
|
+
if (!parsed.ok) {
|
|
171
|
+
return new Set();
|
|
172
|
+
}
|
|
173
|
+
if (parsed.params.action === 'propose') {
|
|
174
|
+
return new Set(
|
|
175
|
+
parsed.params.items
|
|
176
|
+
.filter((item) => item.status === TODO_STATUS.completed)
|
|
177
|
+
.map((item) => item.id),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return new Set(
|
|
181
|
+
parsed.params.patches
|
|
182
|
+
.filter(
|
|
183
|
+
(patch) =>
|
|
184
|
+
patch.status === TODO_STATUS.completed || patch.content !== undefined,
|
|
185
|
+
)
|
|
186
|
+
.map((patch) => patch.id),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -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',
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
personaToolDetails,
|
|
5
|
-
} from './model.ts';
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { PERSONA_SKILL_PATH } from './catalog.ts';
|
|
3
|
+
import { type PersonaToolDetails, personaToolDetails } from './model.ts';
|
|
6
4
|
|
|
7
5
|
export interface PersonaToolResult {
|
|
8
6
|
readonly content: { readonly type: 'text'; readonly text: string }[];
|
|
@@ -11,7 +9,7 @@ export interface PersonaToolResult {
|
|
|
11
9
|
|
|
12
10
|
export function executePersona(): PersonaToolResult {
|
|
13
11
|
return {
|
|
14
|
-
content: [{ type: 'text', text:
|
|
12
|
+
content: [{ type: 'text', text: readFileSync(PERSONA_SKILL_PATH, 'utf8') }],
|
|
15
13
|
details: personaToolDetails(),
|
|
16
14
|
};
|
|
17
15
|
}
|
|
@@ -15,9 +15,10 @@ import { personaStatusLabel } from './model.ts';
|
|
|
15
15
|
import { appendPersonaReminder } from './prompt.ts';
|
|
16
16
|
|
|
17
17
|
export const PERSONA_DESCRIPTION =
|
|
18
|
-
|
|
18
|
+
'Load the complete Sideroom writing guide before a substantial explanation, findings report, implementation summary, or user-facing documentation; also use when asked for clearer, shorter, or less technical wording. Reuse while in context.';
|
|
19
19
|
|
|
20
|
-
export const PERSONA_PROMPT_SNIPPET =
|
|
20
|
+
export const PERSONA_PROMPT_SNIPPET =
|
|
21
|
+
'Load the complete writing guide before substantial user-facing prose.';
|
|
21
22
|
|
|
22
23
|
// Pi loads extensions/*/index.ts through export default.
|
|
23
24
|
export default function registerPersona(pi: ExtensionAPI): void {
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PERSONA_SKILL_PATH,
|
|
3
|
-
PERSONA_TOOL_NAME,
|
|
4
|
-
PROHIBITIONS,
|
|
5
|
-
type Prohibition,
|
|
6
|
-
TEXT_SCOPE,
|
|
7
|
-
VOICE_RULES,
|
|
8
|
-
} from './catalog.ts';
|
|
1
|
+
import { PERSONA_TOOL_NAME, PROHIBITIONS, VOICE_RULES } from './catalog.ts';
|
|
9
2
|
import type { ProhibitionViolation } from './checks.ts';
|
|
10
3
|
|
|
11
4
|
// Pi narrows these blocks before handing the message over; the shape is
|
|
@@ -45,22 +38,6 @@ export function personaToolDetails(): PersonaToolDetails {
|
|
|
45
38
|
};
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
export function formatPersonaDetail(): string {
|
|
49
|
-
return [
|
|
50
|
-
'Sideroom persona: one always-on voice; no profiles or switching.',
|
|
51
|
-
'',
|
|
52
|
-
'Voice rules',
|
|
53
|
-
...VOICE_RULES.map((rule) => `- ${rule.instruction}`),
|
|
54
|
-
'',
|
|
55
|
-
'Hard prohibitions',
|
|
56
|
-
...PROHIBITIONS.map(
|
|
57
|
-
(prohibition) => `- ${prohibition.rule} (${enforcementOf(prohibition)})`,
|
|
58
|
-
),
|
|
59
|
-
'',
|
|
60
|
-
`Complete guide: ${PERSONA_SKILL_PATH}`,
|
|
61
|
-
].join('\n');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
41
|
export function formatBlockReason(
|
|
65
42
|
path: string,
|
|
66
43
|
violations: readonly ProhibitionViolation[],
|
|
@@ -112,10 +89,3 @@ export function assistantMessageText(content: readonly TextBlock[]): string {
|
|
|
112
89
|
}
|
|
113
90
|
return parts.join('\n');
|
|
114
91
|
}
|
|
115
|
-
|
|
116
|
-
function enforcementOf(prohibition: Prohibition): string {
|
|
117
|
-
if (prohibition.scopes.includes(TEXT_SCOPE.artifact)) {
|
|
118
|
-
return 'blocked on write and edit';
|
|
119
|
-
}
|
|
120
|
-
return 'corrected with a steer';
|
|
121
|
-
}
|
|
@@ -18,17 +18,19 @@ 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}
|
|
26
|
+
Apply this voice to progress messages, final answers, and user-facing documentation.
|
|
26
27
|
Voice: ${VOICE_SUMMARY}
|
|
27
28
|
Never: ${PROHIBITION_SUMMARY}
|
|
28
29
|
${ENFORCEMENT_SUMMARY}
|
|
29
30
|
Examples:
|
|
30
31
|
${PERSONA_EXAMPLES.join('\n')}
|
|
31
|
-
|
|
32
|
+
Before the first substantial explanation or report, call ${PERSONA_TOOL_NAME} or read ${PERSONA_SKILL_PATH}; reuse the complete guide while it remains in context.
|
|
33
|
+
Before sending prose, check: outcome first, necessary terms explained, no repetition or invented labels. Preserve evidence and requested detail.`;
|
|
32
34
|
|
|
33
35
|
export function appendPersonaReminder(systemPrompt: string): string {
|
|
34
36
|
if (systemPrompt.includes(PERSONA_REMINDER_HEADING)) {
|