@rmrdeveloper/sideroom-pi 8.14.0 → 9.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.
Files changed (37) hide show
  1. package/README.md +19 -9
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
  3. package/assets/visual-context/browser/INSTALL.md +11 -2
  4. package/assets/visual-context/browser/background.js +72 -60
  5. package/assets/visual-context/browser/content.js +45 -7
  6. package/assets/visual-context/browser/destinations.js +40 -0
  7. package/assets/visual-context/browser/manifest.json +1 -1
  8. package/extensions/guidelines/catalog.ts +9 -0
  9. package/extensions/guidelines/execute.ts +26 -0
  10. package/extensions/guidelines/guard.ts +46 -14
  11. package/extensions/guidelines/index.ts +31 -0
  12. package/extensions/guidelines/prompt.ts +2 -2
  13. package/extensions/guidelines/review.ts +1 -1
  14. package/extensions/jev/model.ts +13 -0
  15. package/extensions/visual-context/broker.ts +14 -6
  16. package/extensions/visual-context/compiler-config.ts +103 -0
  17. package/extensions/visual-context/configure.ts +421 -0
  18. package/extensions/visual-context/dialogs.ts +95 -0
  19. package/extensions/visual-context/files.ts +139 -0
  20. package/extensions/visual-context/index.ts +48 -56
  21. package/extensions/visual-context/match.ts +48 -16
  22. package/extensions/visual-context/model.ts +29 -3
  23. package/extensions/visual-context/projects.ts +226 -0
  24. package/extensions/visual-context/session.ts +150 -17
  25. package/extensions/visual-context/setup.ts +146 -0
  26. package/extensions/visual-context/source.ts +203 -100
  27. package/extensions/visual-context/ui.ts +5 -1
  28. package/package.json +5 -5
  29. package/skills/sideroom-guidelines/SKILL.md +8 -5
  30. package/skills/sideroom-guidelines/references/languages/go.md +35 -1
  31. package/skills/sideroom-guidelines/references/languages/java.md +34 -1
  32. package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
  33. package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
  34. package/skills/sideroom-guidelines/references/languages/python.md +30 -1
  35. package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
  36. package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
  37. package/skills/sideroom-visual-setup/SKILL.md +49 -0
@@ -6,7 +6,13 @@ import {
6
6
  isReadToolResult,
7
7
  isToolCallEventType,
8
8
  } from '@earendil-works/pi-coding-agent';
9
- import { GUIDELINE_SKILL_PATH, LANGUAGE_GUIDES } from './catalog.ts';
9
+ import {
10
+ GUIDELINE_SKILL_PATH,
11
+ GUIDELINES_TOOL_NAME,
12
+ LANGUAGE_GUIDES,
13
+ languageGuideFor,
14
+ } from './catalog.ts';
15
+ import type { GuidelinesToolResult } from './execute.ts';
10
16
 
11
17
  export { GUIDELINE_SKILL_PATH } from './catalog.ts';
12
18
 
@@ -19,21 +25,54 @@ interface ReadInput {
19
25
  export interface GuidelineReadState {
20
26
  skillRead: boolean;
21
27
  readonly languageGuidesRead: Set<string>;
28
+ readonly pendingLoads: Map<string, GuidelinesToolResult>;
22
29
  }
23
30
 
24
31
  export function createGuidelineReadState(): GuidelineReadState {
25
- return { skillRead: false, languageGuidesRead: new Set() };
32
+ return {
33
+ skillRead: false,
34
+ languageGuidesRead: new Set(),
35
+ pendingLoads: new Map(),
36
+ };
26
37
  }
27
38
 
28
39
  export function resetGuidelineReadState(state: GuidelineReadState): void {
29
40
  state.skillRead = false;
30
41
  state.languageGuidesRead.clear();
42
+ state.pendingLoads.clear();
31
43
  }
32
44
 
33
45
  export function registerGuidelineGuard(
34
46
  pi: ExtensionAPI,
35
47
  state: GuidelineReadState,
36
48
  ): void {
49
+ // This notification follows every tool_result handler, including redactors.
50
+ pi.on('tool_execution_end', (event, ctx) => {
51
+ if (event.toolName !== GUIDELINES_TOOL_NAME) {
52
+ return;
53
+ }
54
+ const delivered = state.pendingLoads.get(event.toolCallId);
55
+ state.pendingLoads.delete(event.toolCallId);
56
+ const deliveredResult: Partial<GuidelinesToolResult> | undefined =
57
+ event.result;
58
+ if (
59
+ event.isError ||
60
+ event.parentToolCallId !== undefined ||
61
+ delivered === undefined ||
62
+ !Array.isArray(deliveredResult?.content) ||
63
+ deliveredResult.content.length !== delivered.content.length ||
64
+ !deliveredResult.content.every(
65
+ (part, index) =>
66
+ part.type === 'text' && part.text === delivered.content[index]?.text,
67
+ )
68
+ ) {
69
+ return;
70
+ }
71
+ for (const path of delivered.details.paths) {
72
+ recordGuidelineRead(state, path, ctx.cwd);
73
+ }
74
+ });
75
+
37
76
  pi.on('tool_result', (event, ctx) => {
38
77
  if (!isReadToolResult(event) || event.isError) {
39
78
  return;
@@ -62,7 +101,9 @@ export function registerGuidelineGuard(
62
101
  const reason = mutationBlockReason(
63
102
  state,
64
103
  event.input.path,
65
- pi.getActiveTools().includes('read'),
104
+ pi
105
+ .getActiveTools()
106
+ .some((name) => name === 'read' || name === GUIDELINES_TOOL_NAME),
66
107
  );
67
108
  if (reason === undefined) {
68
109
  return;
@@ -113,19 +154,10 @@ export function mutationBlockReason(
113
154
  return undefined;
114
155
  }
115
156
  if (!readToolAvailable) {
116
- return `Blocked ${targetPath}: the read tool is inactive, so the guidelines gate cannot be satisfied. Enable read, then read ${requiredPaths.join(' and ')} before retrying the mutation.`;
157
+ return `Blocked ${targetPath}: the read tool is inactive and sideroom_guidelines is inactive. Enable either tool, then load ${requiredPaths.join(' and ')} before retrying the mutation.`;
117
158
  }
118
159
 
119
- return `Blocked ${targetPath}: read ${requiredPaths.join(' and ')} with the read tool in this agent run, then retry the mutation.`;
120
- }
121
-
122
- function languageGuideFor(path: string): string | undefined {
123
- const normalizedPath = stripLeadingAt(path)
124
- .replaceAll('\\', '/')
125
- .toLowerCase();
126
- return LANGUAGE_GUIDES.find(({ extensions }) =>
127
- extensions.some((extension) => normalizedPath.endsWith(extension)),
128
- )?.path;
160
+ return `Blocked ${targetPath}: call sideroom_guidelines with path ${JSON.stringify(targetPath)}, or read ${requiredPaths.join(' and ')} in full with the read tool in this agent run, then retry the mutation.`;
129
161
  }
130
162
 
131
163
  const canonicalCatalogPaths = new Map<string, string>();
@@ -1,4 +1,7 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { Type } from 'typebox';
3
+ import { GUIDELINES_TOOL_NAME } from './catalog.ts';
4
+ import { executeGuidelines } from './execute.ts';
2
5
  import {
3
6
  createGuidelineReadState,
4
7
  registerGuidelineGuard,
@@ -16,6 +19,34 @@ export default function registerGuidelines(pi: ExtensionAPI): void {
16
19
  const readState = createGuidelineReadState();
17
20
  registerGuidelineGuard(pi, readState);
18
21
 
22
+ pi.registerTool({
23
+ name: GUIDELINES_TOOL_NAME,
24
+ label: 'Sideroom Guidelines',
25
+ description:
26
+ 'Before write or edit, load the complete Sideroom coding skill and the language guide for the target path. Unsupported languages load only the general skill. Call directly, not from another tool. Reuse loaded guides in this agent run; reload after compaction or a new run. Full reads of the packaged documents through read remain supported.',
27
+ parameters: Type.Object({
28
+ path: Type.String({
29
+ minLength: 1,
30
+ description:
31
+ 'Path of the file you will change; the file need not exist.',
32
+ }),
33
+ }),
34
+ exposure: 'model-only',
35
+ executionMode: 'sequential',
36
+ async execute(toolCallId, { path }, signal) {
37
+ signal?.throwIfAborted();
38
+ const loaded = executeGuidelines(path);
39
+ signal?.throwIfAborted();
40
+ // Keep an independent receipt: changed or partial tool results cannot
41
+ // authorize mutations, and resets discard receipts still awaiting delivery.
42
+ readState.pendingLoads.set(toolCallId, {
43
+ content: loaded.content.map((part) => ({ ...part })),
44
+ details: { paths: [...loaded.details.paths] },
45
+ });
46
+ return loaded;
47
+ },
48
+ });
49
+
19
50
  const reviewState = createReviewState();
20
51
  registerReviewGuard(pi, reviewState);
21
52
  pi.on('session_start', () => {
@@ -12,10 +12,10 @@ const LANGUAGE_GUIDE_INDEX = LANGUAGE_GUIDES.map(
12
12
  ).join('; ');
13
13
 
14
14
  export const GUIDELINES_REMINDER = `${GUIDELINES_REMINDER_HEADING}
15
- Before the first write or edit in each agent run, use the read tool without offset or limit to load these files in full:
15
+ Before write or edit, call sideroom_guidelines with the target path. Reuse loaded guides in this run; reload after compaction. Alternatively, use the read tool without offset or limit to load in full:
16
16
  - ${GUIDELINE_SKILL_PATH}
17
17
  - for the extension of the file you will change, one of: ${LANGUAGE_GUIDE_INDEX}
18
- A required read counts only when it succeeds without truncation. Until all required reads succeed, do not call write or edit and do not mutate files through bash or another tool; the extension blocks those mutations.`;
18
+ Only successful loads without truncation count. Until all required documents are loaded, do not call write or edit and do not mutate files through bash or another tool; the extension blocks those mutations.`;
19
19
 
20
20
  export function appendGuidelinesReminder(systemPrompt: string): string {
21
21
  if (systemPrompt.includes(GUIDELINES_REMINDER_HEADING)) {
@@ -14,7 +14,7 @@ import {
14
14
  export const REVIEW_STEER_TYPE = 'sideroom-guidelines-review';
15
15
 
16
16
  export const REVIEW_STEER =
17
- 'Finish any remaining requested implementation before reviewing. Review the changes this run introduced against the loaded Sideroom language guide: fix only demonstrated violations caused by those changes, preserving all requested requirements and existing behavior. KISS and YAGNI do not justify removing required error handling, compatibility, or tests. Do not refactor unrelated code. Then run the relevant formatter, linter, type checks, and tests. If the guide is no longer in context, re-read it in full first.';
17
+ 'Finish any remaining requested implementation before reviewing. Review the changes this run introduced against the loaded Sideroom language guide: fix only demonstrated violations caused by those changes, preserving all requested requirements and existing behavior. KISS and YAGNI do not justify removing required error handling, compatibility, or tests. Include rule 20, Decomposition by intent: names and contracts should separate orchestration from coherent implementation details, even with one caller. Do not split solely for line count or separate tightly coupled steps into shared mutable state, excessive parameters, or functions that must be read together. Do not refactor unrelated code. Then run the relevant formatter, linter, type checks, and tests. If the guide is no longer in context, re-read it in full first.';
18
18
 
19
19
  export const WORKFLOW_REVIEW_STEER =
20
20
  'Verify the semantic notes below against the visible evidence and the questionnaire or task contract. Correct only demonstrated ambiguity or contradictions; respect user answers and ask again only when clarification is necessary. Missing evidence is not proof of unfinished work. Do not create unrelated implementation work. Run project checks only if code changes.';
@@ -186,6 +186,19 @@ export const JEV_RULES: readonly JevRule[] = [
186
186
  },
187
187
  },
188
188
  },
189
+ {
190
+ id: 'decomposition-by-intent',
191
+ question: {
192
+ type: NOUL_TYPE,
193
+ instructions:
194
+ 'In `change.addedLines`, does the changed function obscure orchestration with a coherent implementation operation that a named function with a clear contract would make understandable without reading its implementation, or does a new split make tightly coupled steps harder to understand? Require the operation and its surrounding orchestration or helper caller to be visible in `file.body` or `context.files`; do not infer unseen bodies. One caller is sufficient for a useful extraction. Never flag function length alone or require a split that introduces shared mutable state, excessive parameters, or functions that must be read together.',
195
+ criteria: {
196
+ true: 'Visible bodies show added implementation details obscuring the caller flow, with a coherent extractable operation and a clear independent contract, or a new split demonstrably harms comprehension by separating tightly coupled steps',
197
+ false:
198
+ 'Names and contracts already convey the flow, coupled steps belong together, length is the only signal, or evidence is insufficient',
199
+ },
200
+ },
201
+ },
189
202
  {
190
203
  id: 'unnecessary-abstraction',
191
204
  question: {
@@ -106,6 +106,7 @@ export async function startBroker({
106
106
  publish();
107
107
  });
108
108
  socket.on('message', (bytes, binary) => {
109
+ let selectionId: string | undefined;
109
110
  try {
110
111
  const message = parseEnvelope(bytes.toString(), binary);
111
112
  if (!peer.role) {
@@ -191,10 +192,14 @@ export async function startBroker({
191
192
  }
192
193
  throw new Error('Invalid Pi message.');
193
194
  }
194
- if (
195
- message.type !== MESSAGE_TYPE.selection ||
196
- !Check(SessionDescriptorSchema, message.target)
197
- ) {
195
+ if (message.type !== MESSAGE_TYPE.selection) {
196
+ throw new Error(
197
+ 'Choose a destination session in the browser companion.',
198
+ );
199
+ }
200
+ const selection = parseSelection(message.selection);
201
+ selectionId = selection.id;
202
+ if (!Check(SessionDescriptorSchema, message.target)) {
198
203
  throw new Error(
199
204
  'Choose a destination session in the browser companion.',
200
205
  );
@@ -218,7 +223,6 @@ export async function startBroker({
218
223
  'Pi is still processing selections. Try again shortly.',
219
224
  );
220
225
  }
221
- const selection = parseSelection(message.selection);
222
226
  peer.lastSelectionAt = Date.now();
223
227
  const requestId = randomUUID();
224
228
  const timer = setTimeout(() => {
@@ -249,7 +253,11 @@ export async function startBroker({
249
253
  cause instanceof Error
250
254
  ? cause
251
255
  : new Error('Cannot route browser selection.', { cause });
252
- send(socket, { type: MESSAGE_TYPE.error, message: error.message });
256
+ send(socket, {
257
+ type: MESSAGE_TYPE.error,
258
+ id: selectionId,
259
+ message: error.message,
260
+ });
253
261
  if (!peer.role) {
254
262
  socket.close(1008, 'Invalid pairing');
255
263
  }
@@ -0,0 +1,103 @@
1
+ export const CONFIG_KINDS = [
2
+ 'astro',
3
+ 'nuxt',
4
+ 'next',
5
+ 'vite',
6
+ 'webpack',
7
+ 'rspack',
8
+ ] as const;
9
+ export type ConfigKind = (typeof CONFIG_KINDS)[number];
10
+ export const INSPECTOR_PACKAGE = 'code-inspector-plugin';
11
+ export const INSPECTOR_VERSION = '2.0.9';
12
+
13
+ export const COMPILER_IDENTIFIER = 'sideroomSourceLocations';
14
+ export const CORE_PACKAGE = '@code-inspector/core';
15
+ export const LOADER_PACKAGE = '@code-inspector/webpack';
16
+ export const METADATA_LOADER = `${LOADER_PACKAGE}/dist/loader.js`;
17
+
18
+ export function compilerPackage(kind: ConfigKind): string {
19
+ return kind === 'vite' || kind === 'astro' || kind === 'nuxt'
20
+ ? CORE_PACKAGE
21
+ : LOADER_PACKAGE;
22
+ }
23
+
24
+ interface CompilerIntegrationInput {
25
+ readonly kind: ConfigKind;
26
+ readonly commonjs: boolean;
27
+ readonly typescript: boolean;
28
+ }
29
+
30
+ export function compilerIntegration({
31
+ kind,
32
+ commonjs,
33
+ typescript,
34
+ }: CompilerIntegrationInput): string {
35
+ const importBinding = (binding: string, packageName: string): string =>
36
+ commonjs
37
+ ? `const { ${binding.replaceAll(' as ', ': ')} } = require('${packageName}');`
38
+ : `import { ${binding} } from '${packageName}';`;
39
+ if (compilerPackage(kind) === CORE_PACKAGE) {
40
+ const positionTypes = typescript
41
+ ? 'source: string, id: string'
42
+ : 'source, id';
43
+ const constant = typescript ? ' as const' : '';
44
+ const astroLoad =
45
+ kind === 'astro'
46
+ ? `
47
+ async load(${typescript ? 'id: string' : 'id'}) {
48
+ if (!id.endsWith('.astro') || id.includes('/node_modules/')) {
49
+ return;
50
+ }
51
+ return sideroomTransform({ content: await sideroomReadFile(id, 'utf8'), filePath: id, fileType: 'astro', escapeTags: [/^[A-Z]/, /[.:-]/], pathType: 'absolute' });
52
+ },`
53
+ : '';
54
+ return `${importBinding('transformCode as sideroomTransform', CORE_PACKAGE)}
55
+ ${kind === 'astro' ? importBinding('readFile as sideroomReadFile', 'node:fs/promises') : ''}
56
+
57
+ function ${COMPILER_IDENTIFIER}() {
58
+ return {
59
+ name: 'sideroom-source-locations',
60
+ enforce: 'pre'${constant},
61
+ apply: 'serve'${constant},
62
+ async transform(${positionTypes}) {
63
+ const [file, query] = id.split('?');
64
+ if (!file || query || file.includes('/node_modules/')) {
65
+ return;
66
+ }
67
+ const fileType = /\\.(jsx|tsx|js|ts|mjs|mts)$/.test(file) ? 'jsx' : file.endsWith('.vue') ? 'vue' : file.endsWith('.svelte') ? 'svelte' : undefined;
68
+ if (!fileType) {
69
+ return;
70
+ }
71
+ return sideroomTransform({ content: source, filePath: file, fileType, escapeTags: [/^[A-Z]/, /[.:-]/], pathType: 'absolute' });
72
+ },${astroLoad}
73
+ };
74
+ }
75
+ `;
76
+ }
77
+ const imports = commonjs
78
+ ? ''
79
+ : `${importBinding('createRequire as sideroomCreateRequire', 'node:module')}
80
+ const sideroomRequire = sideroomCreateRequire(import.meta.url);`;
81
+ const compilerType = typescript
82
+ ? 'compiler: { options: { mode?: string; module?: { rules?: unknown[] } } }'
83
+ : 'compiler';
84
+ return `${imports}
85
+ function ${COMPILER_IDENTIFIER}() {
86
+ return {
87
+ apply(${compilerType}) {
88
+ if (compiler.options.mode !== 'development') {
89
+ return;
90
+ }
91
+ compiler.options.module ??= {};
92
+ compiler.options.module.rules ??= [];
93
+ compiler.options.module.rules.unshift({
94
+ test: /\\.(jsx|tsx|js|ts|mjs|mts|vue|svelte)$/,
95
+ exclude: /node_modules/,
96
+ enforce: 'pre',
97
+ use: [{ loader: ${commonjs ? 'require' : 'sideroomRequire'}.resolve('${METADATA_LOADER}'), options: { pathType: 'absolute' } }],
98
+ });
99
+ },
100
+ };
101
+ }
102
+ `;
103
+ }