@rmrdeveloper/sideroom-pi 7.1.0 → 8.0.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 +32 -15
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +193 -413
- package/extensions/ask/model.ts +60 -15
- package/extensions/guidelines/guard.ts +136 -0
- package/extensions/guidelines/index.ts +9 -0
- package/extensions/guidelines/prompt.ts +5 -4
- package/package.json +1 -1
- package/skills/sideroom-guidelines/SKILL.md +12 -8
- package/skills/sideroom-guidelines/references/languages/go.md +369 -19
- package/skills/sideroom-guidelines/references/languages/java.md +249 -16
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +242 -20
- package/skills/sideroom-guidelines/references/languages/python.md +312 -12
- package/skills/sideroom-guidelines/references/languages/rust.md +244 -12
- package/skills/sideroom-guidelines/references/languages/typescript.md +248 -29
package/extensions/ask/model.ts
CHANGED
|
@@ -9,9 +9,17 @@ export const CUSTOM_LABEL = 'Write a custom answer';
|
|
|
9
9
|
export const UI_UNAVAILABLE =
|
|
10
10
|
'Error: UI not available (running in non-interactive mode)';
|
|
11
11
|
|
|
12
|
+
const MAX_QUESTIONS = 4;
|
|
13
|
+
const MAX_OPTIONS = 4;
|
|
14
|
+
const MAX_TAB_LABEL_LENGTH = 16;
|
|
15
|
+
const MAX_OPTION_LABEL_LENGTH = 60;
|
|
16
|
+
|
|
12
17
|
const QuestionOptionSchema = Type.Object({
|
|
13
18
|
value: Type.String({ description: 'The value returned when selected' }),
|
|
14
|
-
label: Type.String({
|
|
19
|
+
label: Type.String({
|
|
20
|
+
maxLength: MAX_OPTION_LABEL_LENGTH,
|
|
21
|
+
description: `Display label for the option, with at most ${String(MAX_OPTION_LABEL_LENGTH)} characters`,
|
|
22
|
+
}),
|
|
15
23
|
description: Type.Optional(
|
|
16
24
|
Type.String({ description: 'Optional description shown below the label' }),
|
|
17
25
|
),
|
|
@@ -21,14 +29,15 @@ const QuestionSchema = Type.Object({
|
|
|
21
29
|
id: Type.String({ description: 'Unique identifier for this question' }),
|
|
22
30
|
label: Type.Optional(
|
|
23
31
|
Type.String({
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
maxLength: MAX_TAB_LABEL_LENGTH,
|
|
33
|
+
description: `Short contextual label of at most ${String(MAX_TAB_LABEL_LENGTH)} characters for the tab bar, e.g. 'Scope' (defaults to Q1, Q2)`,
|
|
26
34
|
}),
|
|
27
35
|
),
|
|
28
36
|
prompt: Type.String({ description: 'The full question text to display' }),
|
|
29
37
|
options: Type.Array(QuestionOptionSchema, {
|
|
30
38
|
minItems: 2,
|
|
31
|
-
|
|
39
|
+
maxItems: MAX_OPTIONS,
|
|
40
|
+
description: `Two to ${String(MAX_OPTIONS)} mutually exclusive options`,
|
|
32
41
|
}),
|
|
33
42
|
recommendationIndex: Type.Integer({
|
|
34
43
|
minimum: 0,
|
|
@@ -39,7 +48,8 @@ const QuestionSchema = Type.Object({
|
|
|
39
48
|
export const AskParamsSchema = Type.Object({
|
|
40
49
|
questions: Type.Array(QuestionSchema, {
|
|
41
50
|
minItems: 1,
|
|
42
|
-
|
|
51
|
+
maxItems: MAX_QUESTIONS,
|
|
52
|
+
description: `One to ${String(MAX_QUESTIONS)} questions to ask the user in this batch`,
|
|
43
53
|
}),
|
|
44
54
|
});
|
|
45
55
|
|
|
@@ -270,23 +280,58 @@ function schemaErrorMessage(value: unknown): string {
|
|
|
270
280
|
if (error === undefined) {
|
|
271
281
|
return 'Error: Invalid sideroom_ask parameters';
|
|
272
282
|
}
|
|
273
|
-
if (error.
|
|
274
|
-
|
|
283
|
+
if (error.instancePath === '/questions') {
|
|
284
|
+
if (error.keyword === 'minItems') {
|
|
285
|
+
return 'Error: No questions provided';
|
|
286
|
+
}
|
|
287
|
+
if (error.keyword === 'maxItems') {
|
|
288
|
+
return `Error: A questionnaire batch may include at most ${String(MAX_QUESTIONS)} questions`;
|
|
289
|
+
}
|
|
275
290
|
}
|
|
291
|
+
|
|
292
|
+
const questions = asQuestions(value);
|
|
276
293
|
const optionsMatch = /\/questions\/(\d+)\/options$/.exec(error.instancePath);
|
|
277
|
-
if (
|
|
294
|
+
if (optionsMatch !== null) {
|
|
278
295
|
const questionIndex = Number(optionsMatch[1]);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
296
|
+
const id = questionId(questions, questionIndex);
|
|
297
|
+
if (error.keyword === 'minItems') {
|
|
298
|
+
return `Error: Question '${id}' must include at least two options`;
|
|
299
|
+
}
|
|
300
|
+
if (error.keyword === 'maxItems') {
|
|
301
|
+
return `Error: Question '${id}' may include at most ${String(MAX_OPTIONS)} options`;
|
|
302
|
+
}
|
|
286
303
|
}
|
|
304
|
+
|
|
305
|
+
const tabLabelMatch = /\/questions\/(\d+)\/label$/.exec(error.instancePath);
|
|
306
|
+
if (error.keyword === 'maxLength' && tabLabelMatch !== null) {
|
|
307
|
+
const questionIndex = Number(tabLabelMatch[1]);
|
|
308
|
+
const id = questionId(questions, questionIndex);
|
|
309
|
+
return `Error: Question '${id}' label may contain at most ${String(MAX_TAB_LABEL_LENGTH)} characters`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const optionLabelMatch = /\/questions\/(\d+)\/options\/(\d+)\/label$/.exec(
|
|
313
|
+
error.instancePath,
|
|
314
|
+
);
|
|
315
|
+
if (error.keyword === 'maxLength' && optionLabelMatch !== null) {
|
|
316
|
+
const questionIndex = Number(optionLabelMatch[1]);
|
|
317
|
+
const optionIndex = Number(optionLabelMatch[2]);
|
|
318
|
+
const id = questionId(questions, questionIndex);
|
|
319
|
+
return `Error: Option ${String(optionIndex + 1)} for question '${id}' may contain at most ${String(MAX_OPTION_LABEL_LENGTH)} characters`;
|
|
320
|
+
}
|
|
321
|
+
|
|
287
322
|
return `Error: Invalid sideroom_ask parameters`;
|
|
288
323
|
}
|
|
289
324
|
|
|
325
|
+
function questionId(
|
|
326
|
+
questions: readonly Record<string, unknown>[],
|
|
327
|
+
index: number,
|
|
328
|
+
): string {
|
|
329
|
+
const question = questions[index];
|
|
330
|
+
return question !== undefined && typeof question.id === 'string'
|
|
331
|
+
? question.id
|
|
332
|
+
: String(index);
|
|
333
|
+
}
|
|
334
|
+
|
|
290
335
|
function asQuestions(value: unknown): readonly Record<string, unknown>[] {
|
|
291
336
|
if (typeof value !== 'object' || value === null) {
|
|
292
337
|
return [];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import {
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
isReadToolResult,
|
|
6
|
+
isToolCallEventType,
|
|
7
|
+
} from '@earendil-works/pi-coding-agent';
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
10
|
+
export const GUIDELINE_SKILL_PATH = join(
|
|
11
|
+
PACKAGE_ROOT,
|
|
12
|
+
'skills/sideroom-guidelines/SKILL.md',
|
|
13
|
+
);
|
|
14
|
+
const LANGUAGE_GUIDE_DIRECTORY = join(
|
|
15
|
+
PACKAGE_ROOT,
|
|
16
|
+
'skills/sideroom-guidelines/references/languages',
|
|
17
|
+
);
|
|
18
|
+
const LANGUAGE_GUIDE_BY_EXTENSION: Readonly<Record<string, string>> = {
|
|
19
|
+
'.go': join(LANGUAGE_GUIDE_DIRECTORY, 'go.md'),
|
|
20
|
+
'.java': join(LANGUAGE_GUIDE_DIRECTORY, 'java.md'),
|
|
21
|
+
'.php': join(LANGUAGE_GUIDE_DIRECTORY, 'php-laravel.md'),
|
|
22
|
+
'.py': join(LANGUAGE_GUIDE_DIRECTORY, 'python.md'),
|
|
23
|
+
'.rs': join(LANGUAGE_GUIDE_DIRECTORY, 'rust.md'),
|
|
24
|
+
'.ts': join(LANGUAGE_GUIDE_DIRECTORY, 'typescript.md'),
|
|
25
|
+
'.tsx': join(LANGUAGE_GUIDE_DIRECTORY, 'typescript.md'),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface ReadInput {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly offset?: number;
|
|
31
|
+
readonly limit?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GuidelineReadState {
|
|
35
|
+
skillRead: boolean;
|
|
36
|
+
readonly languageGuidesRead: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createGuidelineReadState(): GuidelineReadState {
|
|
40
|
+
return { skillRead: false, languageGuidesRead: new Set() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resetGuidelineReadState(state: GuidelineReadState): void {
|
|
44
|
+
state.skillRead = false;
|
|
45
|
+
state.languageGuidesRead.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function registerGuidelineGuard(
|
|
49
|
+
pi: ExtensionAPI,
|
|
50
|
+
state: GuidelineReadState,
|
|
51
|
+
): void {
|
|
52
|
+
pi.on('tool_result', (event) => {
|
|
53
|
+
if (!isReadToolResult(event) || event.isError) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// SAFETY: Pi validates built-in read arguments before emitting tool_result.
|
|
58
|
+
const input = event.input as unknown as ReadInput;
|
|
59
|
+
if (input.offset !== undefined || input.limit !== undefined) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
recordGuidelineRead(state, input.path);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
pi.on('tool_call', (event) => {
|
|
66
|
+
if (
|
|
67
|
+
!isToolCallEventType('edit', event) &&
|
|
68
|
+
!isToolCallEventType('write', event)
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const reason = mutationBlockReason(state, event.input.path);
|
|
74
|
+
if (reason === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
return { block: true, reason };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function recordGuidelineRead(
|
|
82
|
+
state: GuidelineReadState,
|
|
83
|
+
path: string | undefined,
|
|
84
|
+
): void {
|
|
85
|
+
if (path === undefined) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalizedPath = normalizePath(path);
|
|
90
|
+
if (normalizedPath === normalizePath(GUIDELINE_SKILL_PATH)) {
|
|
91
|
+
state.skillRead = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const guidePath of new Set(Object.values(LANGUAGE_GUIDE_BY_EXTENSION))) {
|
|
96
|
+
if (normalizedPath === normalizePath(guidePath)) {
|
|
97
|
+
state.languageGuidesRead.add(guidePath);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function mutationBlockReason(
|
|
104
|
+
state: GuidelineReadState,
|
|
105
|
+
targetPath: string,
|
|
106
|
+
): string | undefined {
|
|
107
|
+
const requiredPaths: string[] = [];
|
|
108
|
+
if (!state.skillRead) {
|
|
109
|
+
requiredPaths.push(GUIDELINE_SKILL_PATH);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const guidePath = languageGuideFor(targetPath);
|
|
113
|
+
if (guidePath !== undefined && !state.languageGuidesRead.has(guidePath)) {
|
|
114
|
+
requiredPaths.push(guidePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (requiredPaths.length === 0) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `Blocked ${targetPath}: read ${requiredPaths.join(' and ')} with the read tool in this agent run, then retry the edit.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function languageGuideFor(path: string): string | undefined {
|
|
125
|
+
const normalizedPath = normalizePath(path).toLowerCase();
|
|
126
|
+
const extension = Object.keys(LANGUAGE_GUIDE_BY_EXTENSION).find((candidate) =>
|
|
127
|
+
normalizedPath.endsWith(candidate),
|
|
128
|
+
);
|
|
129
|
+
return extension === undefined
|
|
130
|
+
? undefined
|
|
131
|
+
: LANGUAGE_GUIDE_BY_EXTENSION[extension];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizePath(path: string): string {
|
|
135
|
+
return path.replace(/^@/, '').replaceAll('\\', '/');
|
|
136
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import {
|
|
3
|
+
createGuidelineReadState,
|
|
4
|
+
registerGuidelineGuard,
|
|
5
|
+
resetGuidelineReadState,
|
|
6
|
+
} from './guard.ts';
|
|
2
7
|
import { appendGuidelinesReminder } from './prompt.ts';
|
|
3
8
|
|
|
4
9
|
// Pi loads extensions/*/index.ts through export default.
|
|
5
10
|
export default function registerGuidelines(pi: ExtensionAPI): void {
|
|
11
|
+
const readState = createGuidelineReadState();
|
|
12
|
+
registerGuidelineGuard(pi, readState);
|
|
13
|
+
|
|
6
14
|
pi.on('before_agent_start', (event) => {
|
|
15
|
+
resetGuidelineReadState(readState);
|
|
7
16
|
const systemPrompt = appendGuidelinesReminder(event.systemPrompt);
|
|
8
17
|
if (systemPrompt === event.systemPrompt) {
|
|
9
18
|
return;
|
|
@@ -2,11 +2,12 @@ export const GUIDELINES_REMINDER_HEADING =
|
|
|
2
2
|
'Sideroom coding guidelines (always-on reminder):';
|
|
3
3
|
|
|
4
4
|
export const GUIDELINES_REMINDER = `${GUIDELINES_REMINDER_HEADING}
|
|
5
|
-
Before write or edit, read the sideroom-guidelines
|
|
6
|
-
|
|
7
|
-
Do not
|
|
5
|
+
Before the first write or edit in each agent run, use the read tool without offset or limit on the absolute SKILL.md path shown for sideroom-guidelines in Available Skills.
|
|
6
|
+
For .java, .php, .ts/.tsx, .py, .go, or .rs targets, also fully read exactly the matching guide under that skill's references/languages/ directory.
|
|
7
|
+
The extension blocks write/edit until those exact full-file reads succeed. Do not bypass the gate with bash or another mutation tool.
|
|
8
|
+
Before finishing, review changed code against the loaded guide and run the relevant formatter, linter, type checks, and tests.`;
|
|
8
9
|
|
|
9
|
-
export const
|
|
10
|
+
export const LANGUAGE_GUIDE_FILES = [
|
|
10
11
|
'go.md',
|
|
11
12
|
'java.md',
|
|
12
13
|
'php-laravel.md',
|
package/package.json
CHANGED
|
@@ -11,18 +11,20 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Activation Contract
|
|
13
13
|
|
|
14
|
-
Load this skill before `write` or `edit
|
|
14
|
+
Load this whole skill with `read`, without `offset` or `limit`, before the first `write` or `edit` in each agent run. Use the absolute skill path shown in Available Skills. For each supported target language, also fully load its exact guide from `references/languages/`.
|
|
15
15
|
|
|
16
16
|
## Hard Rules
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
- Do not
|
|
18
|
+
- Treat every rule in the loaded language guide as mandatory for changed code.
|
|
19
|
+
- Do not write or edit until exact full-file reads of the required packaged paths succeed; partial reads and same-named files do not count.
|
|
20
|
+
- Do not bypass the gate with Bash, a subprocess, or another mutation tool.
|
|
21
|
+
- Do not load language guides unrelated to files being changed.
|
|
20
22
|
- Fail fast, keep control flow flat, and validate once at the boundary.
|
|
21
23
|
|
|
22
24
|
## Decision Gates
|
|
23
25
|
|
|
24
26
|
| Path | Read |
|
|
25
|
-
|
|
27
|
+
| ------ | ------ |
|
|
26
28
|
| `.java` | `references/languages/java.md` |
|
|
27
29
|
| `.php` | `references/languages/php-laravel.md` |
|
|
28
30
|
| `.ts` `.tsx` | `references/languages/typescript.md` |
|
|
@@ -33,9 +35,11 @@ Load this skill before `write` or `edit`. Load at most one file from `references
|
|
|
33
35
|
|
|
34
36
|
## Execution Steps
|
|
35
37
|
|
|
36
|
-
1.
|
|
37
|
-
2.
|
|
38
|
-
3.
|
|
38
|
+
1. Read this complete file from its packaged path before any mutation.
|
|
39
|
+
2. Fully read the matching packaged language guide, if the target has a supported extension.
|
|
40
|
+
3. Implement the smallest change that satisfies the current requirement.
|
|
41
|
+
4. Review every changed unit against the loaded guide.
|
|
42
|
+
5. Run the relevant formatter, linter, type checks, and tests before finishing.
|
|
39
43
|
|
|
40
44
|
## Do / Don't
|
|
41
45
|
|
|
@@ -65,7 +69,7 @@ Load this skill before `write` or `edit`. Load at most one file from `references
|
|
|
65
69
|
|
|
66
70
|
## Output Contract
|
|
67
71
|
|
|
68
|
-
Changed code follows
|
|
72
|
+
Changed code follows every applicable rule in the loaded complete guide. The final response names the validation run and any check that could not run. No extra files, layers, comments, or bypass mutations unless the requirement needs them.
|
|
69
73
|
|
|
70
74
|
## References
|
|
71
75
|
|