@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.
Files changed (37) hide show
  1. package/README.md +63 -28
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
  3. package/extensions/ask/index.ts +3 -2
  4. package/extensions/done/guard.ts +42 -37
  5. package/extensions/done/index.ts +3 -2
  6. package/extensions/done/model.ts +1 -5
  7. package/extensions/explain/guard.ts +13 -11
  8. package/extensions/explain/model.ts +2 -25
  9. package/extensions/guidelines/review.ts +31 -13
  10. package/extensions/jev/client.ts +27 -2
  11. package/extensions/jev/context.ts +360 -0
  12. package/extensions/jev/guard.ts +116 -34
  13. package/extensions/jev/model.ts +175 -23
  14. package/extensions/jev/workflow.ts +188 -0
  15. package/extensions/persona/catalog.ts +2 -2
  16. package/extensions/persona/execute.ts +4 -6
  17. package/extensions/persona/index.ts +3 -2
  18. package/extensions/persona/model.ts +1 -31
  19. package/extensions/persona/prompt.ts +5 -3
  20. package/extensions/shared/settle.ts +63 -13
  21. package/package.json +1 -1
  22. package/skills/sideroom-architecture/SKILL.md +17 -8
  23. package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
  24. package/skills/sideroom-codebase-design/SKILL.md +151 -0
  25. package/skills/sideroom-codebase-design/references/growth.md +62 -0
  26. package/skills/sideroom-domain-modeling/SKILL.md +1 -1
  27. package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
  28. package/skills/sideroom-grill/SKILL.md +16 -6
  29. package/skills/sideroom-guidelines/SKILL.md +9 -7
  30. package/skills/sideroom-guidelines/references/languages/go.md +3 -3
  31. package/skills/sideroom-guidelines/references/languages/java.md +3 -3
  32. package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
  33. package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
  34. package/skills/sideroom-guidelines/references/languages/python.md +3 -3
  35. package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
  36. package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
  37. package/skills/sideroom-persona/SKILL.md +43 -25
@@ -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
- // The serialized state must stay well under the two documented budgets (64k
15
- // tokens for the whole request, 32k for the state plus the longest question). A
16
- // new file travels twice, as the body and as its added lines. A state that does
17
- // not fit is skipped, never truncated: half a file reads as a distorted picture
18
- // and Jev answers it with the same confidence.
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
- // The six guide rules that a single file can answer and that no mechanical
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 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.',
69
67
  criteria: {
70
- true: 'A function that returns data also mutates state, or a getter mutates',
71
- 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',
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
- '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.',
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: 'A rule already validated at the boundary is checked again deeper in the code',
108
- false: 'Validation happens once, at the boundary that owns the input',
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: JevFileState;
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
- // The six questions are about code structure; a README or a lockfile would
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 JSON.stringify(state).length <= MAX_STATE_CHARACTERS;
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 rule of JEV_RULES) {
207
- const answer = answers[rule.id];
208
- if (answer === undefined || answer.noul < VIOLATION_PROBABILITY) {
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: rule.id, probability: answer.noul });
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 that the added lines break this rule. Verify against the loaded guide before changing anything.`,
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: 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',
@@ -1,8 +1,6 @@
1
- import {
2
- formatPersonaDetail,
3
- type PersonaToolDetails,
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: formatPersonaDetail() }],
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
- "Read Sideroom's voice rules and hard prohibitions for user-facing answers.";
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 = 'Read the active Sideroom persona.';
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: 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}
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
- Full guide: call ${PERSONA_TOOL_NAME} or read ${PERSONA_SKILL_PATH}.`;
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)) {