@rmrdeveloper/sideroom-pi 9.0.1 → 9.2.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 (63) hide show
  1. package/README.md +35 -40
  2. package/assets/visual-context/browser/background.js +2 -2
  3. package/assets/visual-context/browser/popup.js +1 -2
  4. package/extensions/ask/index.ts +0 -1
  5. package/extensions/ask/model.ts +1 -1
  6. package/extensions/ask/selection.ts +0 -4
  7. package/extensions/done/guard.ts +18 -35
  8. package/extensions/done/index.ts +0 -1
  9. package/extensions/done/model.ts +2 -31
  10. package/extensions/explain/index.ts +4 -7
  11. package/extensions/explain/prompt.ts +13 -0
  12. package/extensions/guidelines/execute.ts +16 -2
  13. package/extensions/guidelines/guard.ts +1 -2
  14. package/extensions/guidelines/index.ts +7 -7
  15. package/extensions/guidelines/prompt.ts +0 -2
  16. package/extensions/guidelines/review.ts +9 -10
  17. package/extensions/jev/client.ts +0 -7
  18. package/extensions/jev/context.ts +0 -2
  19. package/extensions/jev/guard.ts +78 -44
  20. package/extensions/jev/index.ts +0 -3
  21. package/extensions/jev/key.ts +0 -3
  22. package/extensions/jev/model.ts +0 -8
  23. package/extensions/jev/queue.ts +125 -0
  24. package/extensions/jev/ui.ts +0 -14
  25. package/extensions/modified-files/index.ts +0 -1
  26. package/extensions/monorepo-skills/index.ts +0 -1
  27. package/extensions/persona/checks.ts +0 -4
  28. package/extensions/persona/guard.ts +45 -6
  29. package/extensions/persona/index.ts +4 -2
  30. package/extensions/persona/model.ts +0 -2
  31. package/extensions/persona/prompt.ts +0 -2
  32. package/extensions/rules/checks.ts +0 -5
  33. package/extensions/rules/index.ts +0 -1
  34. package/extensions/rules/lexer.ts +1 -1
  35. package/extensions/shared/check-status.ts +35 -0
  36. package/extensions/shared/file-path.ts +0 -3
  37. package/extensions/shared/read-file.ts +0 -2
  38. package/extensions/shared/settle.ts +78 -13
  39. package/extensions/todo/guards.ts +40 -84
  40. package/extensions/todo/index.ts +1 -2
  41. package/extensions/todo/model.ts +6 -48
  42. package/extensions/todo/session.ts +1 -4
  43. package/extensions/visual-context/broker.ts +10 -10
  44. package/extensions/visual-context/configure.ts +4 -4
  45. package/extensions/visual-context/files.ts +0 -3
  46. package/extensions/visual-context/index.ts +8 -9
  47. package/extensions/visual-context/literal.ts +0 -1
  48. package/extensions/visual-context/markup.ts +3 -5
  49. package/extensions/visual-context/model.ts +2 -2
  50. package/extensions/visual-context/pairing.ts +1 -2
  51. package/extensions/visual-context/parse-astro.ts +9 -12
  52. package/extensions/visual-context/parse-jsx.ts +2 -2
  53. package/extensions/visual-context/parse-vue.ts +1 -1
  54. package/extensions/visual-context/setup.ts +8 -8
  55. package/extensions/visual-context/source.ts +0 -3
  56. package/package.json +15 -9
  57. package/skills/sideroom-guidelines/SKILL.md +1 -1
  58. package/skills/sideroom-persona/SKILL.md +9 -7
  59. package/skills/sideroom-user-stories/SKILL.md +78 -0
  60. package/skills/sideroom-user-stories/references/review.md +84 -0
  61. package/skills/sideroom-user-stories/references/templates.md +131 -0
  62. package/extensions/explain/guard.ts +0 -70
  63. package/extensions/explain/model.ts +0 -71
package/README.md CHANGED
@@ -9,8 +9,8 @@
9
9
  [![npm license](https://img.shields.io/npm/l/@rmrdeveloper/sideroom-pi?label=license)](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
10
10
 
11
11
  **The open-source quality layer for Pi.** Your agent asks before it guesses,
12
- follows your code rules on the lines it writes, and cannot call a task
13
- finished while your project's own checks are still red.
12
+ follows your code rules on the lines it writes, and reviews its changes
13
+ before finishing.
14
14
 
15
15
  ```bash
16
16
  pi install npm:@rmrdeveloper/sideroom-pi
@@ -26,8 +26,8 @@ pi install npm:@rmrdeveloper/sideroom-pi
26
26
 
27
27
  For developers who chose Pi for control or cost and need the result to be
28
28
  actually ready: Sideroom is the quality layer that makes Pi follow your
29
- rules, ask when there are two reasonable paths, and not say "done" until
30
- your project passes its own checks.
29
+ rules, ask when there are two reasonable paths, and review its changes
30
+ against the loaded guide before finishing.
31
31
 
32
32
  ## Why Sideroom exists
33
33
 
@@ -61,8 +61,8 @@ Sideroom answers each one with a small, opinionated surface:
61
61
  | Unapplied guidelines | `sideroom_rules` — mechanical checks that block or flag the lines you add |
62
62
  | Unchecked judgment rules | `jev` — asks a decision model about guide rules with visible evidence |
63
63
  | Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of unnecessary technical words |
64
- | Premature completion | `sideroom_done` — steers back to the project's check command before finishing |
65
- | Work left unexplained | `explain` — offers a walkthrough and how to test it once the work settles |
64
+ | Premature completion | Guidelines review — asks for relevant checks and reuses valid results |
65
+ | Work left unexplained | `explain` — invites a walkthrough in the final response after meaningful work |
66
66
 
67
67
  ## What it feels like
68
68
 
@@ -101,9 +101,9 @@ wall of tool calls, it:
101
101
  conditionals and swallowed errors block the mutation; vague names, stale
102
102
  `TODO`s, commented-out code, and debug output are appended as notes to the
103
103
  result. A rule that keeps firing degrades instead of trapping the agent.
104
- - **Does not call it done in red.** When files changed and the project's own
105
- check command has not passed, the agent is steered back to run it before
106
- finishing. If no check command is detectable, the gate stays out of the way.
104
+ - **Reviews before finishing.** The guidelines review asks for missing,
105
+ failed or stale checks and reuses valid results. Check observation never
106
+ forces another response just to run a command.
107
107
 
108
108
  ## What's inside
109
109
 
@@ -273,27 +273,25 @@ symbols in the lines a `write`/`edit` adds block the mutation; flattering
273
273
  openers, hedging, automatic apologies, and AI meta-commentary are answered
274
274
  with a capped steer. No profiles to switch and no profile selection to store.
275
275
 
276
- ### Done — done means green
276
+ ### Done — passive check observation
277
277
 
278
278
  `sideroom_done` detects the project's check command — `package.json` scripts
279
279
  (`check` → `test` → `lint` → `typecheck` → `types`) with the right
280
280
  package manager, `pytest`, `go test ./...`, `cargo test`, or a `make check`
281
- target — and watches for it to pass. If files changed without a green run, the
282
- agent is steered once per turn, up to a cap, to run it before finishing. When
283
- nothing is detectable, the gate does nothing.
284
-
285
- ### Explain — the walkthrough offer
286
-
287
- When an implementation settles, `explain` steers the agent to offer a
288
- walkthrough through `sideroom_ask`: explain the changes only, how to test them
289
- only, both, or nothing. It fires on `agent_before_settle`, after every retry
290
- and compaction recovery, so the offer never lands on work that is about to be
291
- redone, and it stays inside the same run. It waits one boundary so the
292
- guidelines review runs first; both use the same event, and Pi would otherwise
293
- order them by extension load. It stays quiet after Escape, after an error, and
294
- while a message you typed is waiting. Once per turn, only when the turn touched at least five distinct
295
- files, and only in the TUI, because `sideroom_ask` cannot run anywhere else. No
296
- tool, no widget, no persisted state.
281
+ target — and observes checks the agent chooses to run. It shares successful
282
+ results with guidelines review, so that review can reuse them. It never requests
283
+ a continuation to run checks. A coverage notice can join an existing review or
284
+ style continuation, but never opens one on its own.
285
+
286
+ ### Explain — the walkthrough invitation
287
+
288
+ After meaningful changes, review and checks are complete, `explain` lets the
289
+ agent end its final response with a short invitation to explain the changes
290
+ or how to test them. It skips trivial work and explanations already given.
291
+ There is no automatic questionnaire, file-count threshold or extra model turn.
292
+ Only after you accept, the agent may clarify the scope through `sideroom_ask`
293
+ in the TUI; if your request is clear, it explains directly. A fixed reminder
294
+ keeps the system prompt stable across turns. No tool, widget or persisted state.
297
295
 
298
296
  ## Quick path
299
297
 
@@ -458,23 +456,20 @@ itself.
458
456
  `cargo test`, or `make check`. Detection is cached per working directory.
459
457
  - Green means that command ran with a zero exit in this run. A later
460
458
  successful `write`/`edit` clears green again.
461
- - Steering: once per turn, at most twice per run, then it stops.
459
+ - Observation only: no standalone check or coverage continuation. Coverage
460
+ notices join another necessary continuation at most once per user turn.
462
461
 
463
462
  ### Explain contract
464
463
 
465
- - Trigger: `agent_before_settle` with at least five distinct files mutated
466
- since the last idle user prompt, in TUI mode, on a completed run with no user
467
- message pending. File identity follows Pi's path aliases and canonicalizes
468
- existing paths.
469
- - One boundary of delay: the guidelines review steers on the same event, and Pi
470
- runs both handlers in extension load order, so `explain` offers at the next
471
- boundary.
472
- - The steer names the intent; the agent writes the question in the user's
473
- language with four options, one of them a decline, and one recommendation.
474
- `sideroom_ask` still adds *Out of scope* and the custom answer.
475
- - At most one offer per turn. The flag is set before the steer, so the turn the
476
- offer triggers cannot re-trigger it.
477
- - No green-check requirement: red checks still produce the offer.
464
+ - Delivery: a fixed, idempotent reminder on `before_agent_start`; no per-turn
465
+ state is interpolated into the system prompt.
466
+ - After meaningful work, review and checks, the agent may include one short
467
+ invitation in the final response, in the user's language. Trivial work,
468
+ explanations already provided and requests for no follow-up skip it.
469
+ - No file-count threshold, automatic questionnaire, closing hook or extra turn.
470
+ - After explicit acceptance, clarify scope through `sideroom_ask` only when
471
+ needed in the TUI. Otherwise explain directly, or clarify in ordinary
472
+ conversation outside the TUI.
478
473
 
479
474
  ### Grill contract
480
475
 
@@ -210,7 +210,7 @@ function scheduleReconnect(entry) {
210
210
  entry.retry = setTimeout(() => {
211
211
  void reconnect(entry);
212
212
  }, RETRY_MS);
213
- // Alarms wake the worker after it has been suspended while Pi is closed.
213
+
214
214
  void chrome.alarms.create(RETRY_ALARM, { periodInMinutes: 1 });
215
215
  }
216
216
 
@@ -284,7 +284,7 @@ async function openConnection(entry) {
284
284
  });
285
285
  return;
286
286
  }
287
- // Older brokers and connection failures can only identify the installation.
287
+
288
288
  for (const selectionId of [...receipts.keys()]) {
289
289
  await notifyReceipt({
290
290
  endpoint: entry.endpoint,
@@ -6,8 +6,7 @@ const disconnect = document.querySelector('#disconnect');
6
6
  const forget = document.querySelector('#forget');
7
7
  const destination = document.querySelector('#destination');
8
8
  const connect = document.querySelector('#connect');
9
- // Edge keeps the Chromium shortcut page under its own scheme; every other
10
- // Chromium browser the companion supports answers on chrome://.
9
+
11
10
  const SHORTCUT_PAGE = navigator.userAgent.includes('Edg/')
12
11
  ? 'edge://extensions/shortcuts'
13
12
  : 'chrome://extensions/shortcuts';
@@ -23,7 +23,6 @@ export const ASK_PROMPT_GUIDELINES = [
23
23
  'sideroom_ask only works in the interactive TUI; it errors in print, JSON, and RPC modes.',
24
24
  ];
25
25
 
26
- // Pi loads extensions/*/index.ts through export default.
27
26
  export default function registerAsk(pi: ExtensionAPI): void {
28
27
  pi.registerTool({
29
28
  name: TOOL_NAME,
@@ -423,7 +423,7 @@ function schemaErrorMessage(value: unknown): string {
423
423
  return `Error: Option ${String(optionIndex + 1)} for question '${id}' may contain at most ${String(MAX_OPTION_LABEL_LENGTH)} characters`;
424
424
  }
425
425
 
426
- return `Error: Invalid sideroom_ask parameters`;
426
+ return 'Error: Invalid sideroom_ask parameters';
427
427
  }
428
428
 
429
429
  function questionId(
@@ -1,9 +1,5 @@
1
1
  import type { AskAnswer, AskQuestion, AskSelection } from './model.ts';
2
2
 
3
- /**
4
- * Sorted ascending so the answer and its rendering stay stable no matter the
5
- * order the user toggled the options in.
6
- */
7
3
  export function toggleSelection(
8
4
  selected: readonly number[],
9
5
  optionIndex: number,
@@ -6,22 +6,16 @@ import {
6
6
  isToolCallEventType,
7
7
  isWriteToolResult,
8
8
  } from '@earendil-works/pi-coding-agent';
9
- import {
10
- registerSettleGuard,
11
- SETTLE_PRIORITY,
12
- withHiddenSteer,
13
- } from '../shared/settle.ts';
9
+ import { registerCheckStatus } from '../shared/check-status.ts';
10
+ import { registerSettleSupplement } from '../shared/settle.ts';
14
11
  import { type CheckCommand, commandMatches } from './detect.ts';
15
12
  import {
16
- DONE_GATE_TYPE,
17
13
  type DoneState,
18
- formatDoneSteer,
19
14
  formatMissingTestNotice,
20
15
  isSourcePath,
21
16
  isTestPath,
22
17
  MISSING_TEST_NOTICE_TYPE,
23
18
  shouldNoticeMissingTest,
24
- shouldSteerDone,
25
19
  } from './model.ts';
26
20
 
27
21
  export type ResolveCommand = (
@@ -46,6 +40,13 @@ export function registerDoneGuard({
46
40
  const checkRuns = new Set<string>();
47
41
  const mutationPaths = new Map<string, string>();
48
42
 
43
+ registerCheckStatus(pi, (ctx) => {
44
+ const check = resolveCommand(ctx);
45
+ return check === undefined
46
+ ? undefined
47
+ : { command: check.display, green: state.checksGreen };
48
+ });
49
+
49
50
  pi.on('tool_call', (event, ctx) => {
50
51
  if (
51
52
  isToolCallEventType('write', event) ||
@@ -73,7 +74,6 @@ export function registerDoneGuard({
73
74
  if (event.isError) {
74
75
  return;
75
76
  }
76
- state.mutated = true;
77
77
  state.checksGreen = false;
78
78
  recordMutationPath(state, path);
79
79
  return;
@@ -86,32 +86,15 @@ export function registerDoneGuard({
86
86
  }
87
87
  });
88
88
 
89
- registerSettleGuard(pi, {
90
- priority: SETTLE_PRIORITY.done,
91
- pending: (ctx) =>
92
- shouldSteerDone(state, resolveCommand(ctx) !== undefined) ||
93
- shouldNoticeMissingTest(state, resolveTestSetup(ctx)),
94
- handle: (event, ctx) => {
95
- const check = resolveCommand(ctx);
96
- if (check !== undefined && shouldSteerDone(state, true)) {
97
- state.steerCount += 1;
98
- return withHiddenSteer({
99
- event,
100
- customType: DONE_GATE_TYPE,
101
- content: formatDoneSteer(check.display),
102
- });
103
- }
104
- if (!shouldNoticeMissingTest(state, resolveTestSetup(ctx))) {
105
- return;
106
- }
107
- state.noticedMissingTest = true;
108
- state.steerCount += 1;
109
- return withHiddenSteer({
110
- event,
111
- customType: MISSING_TEST_NOTICE_TYPE,
112
- content: formatMissingTestNotice(),
113
- });
114
- },
89
+ registerSettleSupplement(pi, (ctx) => {
90
+ if (!shouldNoticeMissingTest(state, resolveTestSetup(ctx))) {
91
+ return undefined;
92
+ }
93
+ state.noticedMissingTest = true;
94
+ return {
95
+ customType: MISSING_TEST_NOTICE_TYPE,
96
+ content: formatMissingTestNotice(),
97
+ };
115
98
  });
116
99
  }
117
100
 
@@ -15,7 +15,6 @@ import {
15
15
  } from './guard.ts';
16
16
  import { createDoneState, resetDoneRun } from './model.ts';
17
17
 
18
- // Pi loads extensions/*/index.ts through export default.
19
18
  export default function registerDone(pi: ExtensionAPI): void {
20
19
  const state = createDoneState();
21
20
  const cache = new Map<string, CheckCommand | undefined>();
@@ -1,22 +1,14 @@
1
1
  export interface DoneState {
2
- mutated: boolean;
3
2
  checksGreen: boolean;
4
- steerCount: number;
5
3
  mutatedSource: boolean;
6
4
  mutatedTest: boolean;
7
5
  noticedMissingTest: boolean;
8
6
  }
9
7
 
10
- export const MAX_DONE_STEERS = 2;
11
-
12
- export const DONE_GATE_TYPE = 'sideroom-done-gate';
13
-
14
8
  export const MISSING_TEST_NOTICE_TYPE = 'sideroom-missing-test-notice';
15
9
 
16
10
  const TEST_DIRECTORY = /(?:^|\/)(?:__tests__|tests?|specs?)(?:\/|$)/;
17
11
 
18
- // Test paths are recognised by name only: the notice never claims the right
19
- // test exists, so a missed convention only costs a nudge, never a false pass.
20
12
  const TEST_FILE =
21
13
  /(?:^|\/)(?:test_[\w.-]*\.py|[\w.-]*[._](?:test|spec)\.[\w.-]+|[\w.-]*(?:Test|Tests)\.java)$/;
22
14
 
@@ -24,9 +16,7 @@ const SOURCE_FILE = /\.(?:[cm]?js|jsx|ts|tsx|go|java|php|py|rs)$/i;
24
16
 
25
17
  export function createDoneState(): DoneState {
26
18
  return {
27
- mutated: false,
28
19
  checksGreen: false,
29
- steerCount: 0,
30
20
  mutatedSource: false,
31
21
  mutatedTest: false,
32
22
  noticedMissingTest: false,
@@ -34,9 +24,7 @@ export function createDoneState(): DoneState {
34
24
  }
35
25
 
36
26
  export function resetDoneRun(state: DoneState): void {
37
- state.mutated = false;
38
27
  state.checksGreen = false;
39
- state.steerCount = 0;
40
28
  state.mutatedSource = false;
41
29
  state.mutatedTest = false;
42
30
  state.noticedMissingTest = false;
@@ -52,18 +40,6 @@ export function isSourcePath(path: string): boolean {
52
40
  return SOURCE_FILE.test(normalized);
53
41
  }
54
42
 
55
- export function shouldSteerDone(
56
- state: DoneState,
57
- hasCommand: boolean,
58
- ): boolean {
59
- return (
60
- hasCommand &&
61
- state.mutated &&
62
- !state.checksGreen &&
63
- state.steerCount < MAX_DONE_STEERS
64
- );
65
- }
66
-
67
43
  export function shouldNoticeMissingTest(
68
44
  state: DoneState,
69
45
  hasTests: boolean,
@@ -72,15 +48,10 @@ export function shouldNoticeMissingTest(
72
48
  hasTests &&
73
49
  state.mutatedSource &&
74
50
  !state.mutatedTest &&
75
- !state.noticedMissingTest &&
76
- state.steerCount < MAX_DONE_STEERS
51
+ !state.noticedMissingTest
77
52
  );
78
53
  }
79
54
 
80
- export function formatDoneSteer(display: string): string {
81
- return `Run \`${display}\` and make it pass before finishing.`;
82
- }
83
-
84
55
  export function formatMissingTestNotice(): string {
85
- return 'This run changed code files and no test file. Check whether existing tests cover the changed behavior; add or update a test if coverage is missing, then rerun the relevant checks before finishing.';
56
+ return 'This run changed code files and no test file. Check whether existing tests cover the changed behavior; add or update a test if coverage is missing, reuse relevant checks that already passed after the latest mutation, and run missing, failed, or stale checks before finishing. Any code or test edit makes earlier checks stale.';
86
57
  }
@@ -1,11 +1,8 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
- import { registerExplainGuard } from './guard.ts';
3
- import { createExplainState, resetExplainTurn } from './model.ts';
2
+ import { appendExplainReminder } from './prompt.ts';
4
3
 
5
- // Pi loads extensions/*/index.ts through export default.
6
4
  export default function registerExplain(pi: ExtensionAPI): void {
7
- const state = createExplainState();
8
- registerExplainGuard(pi, state);
9
-
10
- pi.on('session_start', () => resetExplainTurn(state));
5
+ pi.on('before_agent_start', (event) => ({
6
+ systemPrompt: appendExplainReminder(event.systemPrompt),
7
+ }));
11
8
  }
@@ -0,0 +1,13 @@
1
+ export const EXPLAIN_REMINDER_HEADING = 'Sideroom walkthrough invitation:';
2
+
3
+ export const EXPLAIN_REMINDER = `${EXPLAIN_REMINDER_HEADING}
4
+ After completing meaningful changes, review, and checks, you may end the final response with one short sentence in the user's language offering to explain the changes or how to test them. Offer only when it adds value; omit it for trivial work, when the requested explanation is already provided, or when the user asks for no follow-up. Do not ask an automatic question or call sideroom_ask just to offer a walkthrough. Only after the user explicitly accepts the invitation, use sideroom_ask in the TUI if clarification is needed about changes, test steps, or both; otherwise explain directly. Outside the TUI, clarify in ordinary conversation when needed.`;
5
+
6
+ export function appendExplainReminder(systemPrompt: string): string {
7
+ if (systemPrompt.includes(EXPLAIN_REMINDER_HEADING)) {
8
+ return systemPrompt;
9
+ }
10
+ return systemPrompt.length === 0
11
+ ? EXPLAIN_REMINDER
12
+ : `${systemPrompt}\n\n${EXPLAIN_REMINDER}`;
13
+ }
@@ -6,15 +6,29 @@ export interface GuidelinesToolResult {
6
6
  readonly details: { readonly paths: readonly string[] };
7
7
  }
8
8
 
9
+ interface GuidelinesLoadOptions {
10
+ readonly loadedPaths?: ReadonlySet<string>;
11
+ readonly readDocument?: (path: string) => string;
12
+ }
13
+
9
14
  export function executeGuidelines(
10
15
  path: string,
11
- readDocument: (path: string) => string = (path) => readFileSync(path, 'utf8'),
16
+ {
17
+ loadedPaths = new Set(),
18
+ readDocument = (path) => readFileSync(path, 'utf8'),
19
+ }: GuidelinesLoadOptions = {},
12
20
  ): GuidelinesToolResult {
13
21
  const guidePath = languageGuideFor(path);
14
22
  const paths = [
15
23
  GUIDELINE_SKILL_PATH,
16
24
  ...(guidePath === undefined ? [] : [guidePath]),
17
- ];
25
+ ].filter((documentPath) => !loadedPaths.has(documentPath));
26
+ if (paths.length === 0) {
27
+ return {
28
+ content: [{ type: 'text', text: 'The required complete guidelines are already loaded in this run.' }],
29
+ details: { paths },
30
+ };
31
+ }
18
32
  const content = paths.map((documentPath) => {
19
33
  const text = readDocument(documentPath);
20
34
  if (text.trim().length === 0) {
@@ -46,7 +46,7 @@ export function registerGuidelineGuard(
46
46
  pi: ExtensionAPI,
47
47
  state: GuidelineReadState,
48
48
  ): void {
49
- // This notification follows every tool_result handler, including redactors.
49
+
50
50
  pi.on('tool_execution_end', (event, ctx) => {
51
51
  if (event.toolName !== GUIDELINES_TOOL_NAME) {
52
52
  return;
@@ -78,7 +78,6 @@ export function registerGuidelineGuard(
78
78
  return;
79
79
  }
80
80
 
81
- // SAFETY: Pi validates built-in read arguments before emitting tool_result.
82
81
  const input = event.input as unknown as ReadInput;
83
82
  if (
84
83
  input.offset !== undefined ||
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  import { Type } from 'typebox';
3
- import { GUIDELINES_TOOL_NAME } from './catalog.ts';
3
+ import { GUIDELINE_SKILL_PATH, GUIDELINES_TOOL_NAME } from './catalog.ts';
4
4
  import { executeGuidelines } from './execute.ts';
5
5
  import {
6
6
  createGuidelineReadState,
@@ -14,7 +14,6 @@ import {
14
14
  resetReviewTurn,
15
15
  } from './review.ts';
16
16
 
17
- // Pi loads extensions/*/index.ts through export default.
18
17
  export default function registerGuidelines(pi: ExtensionAPI): void {
19
18
  const readState = createGuidelineReadState();
20
19
  registerGuidelineGuard(pi, readState);
@@ -35,10 +34,13 @@ export default function registerGuidelines(pi: ExtensionAPI): void {
35
34
  executionMode: 'sequential',
36
35
  async execute(toolCallId, { path }, signal) {
37
36
  signal?.throwIfAborted();
38
- const loaded = executeGuidelines(path);
37
+ const loadedPaths = new Set(readState.languageGuidesRead);
38
+ if (readState.skillRead) {
39
+ loadedPaths.add(GUIDELINE_SKILL_PATH);
40
+ }
41
+ const loaded = executeGuidelines(path, { loadedPaths });
39
42
  signal?.throwIfAborted();
40
- // Keep an independent receipt: changed or partial tool results cannot
41
- // authorize mutations, and resets discard receipts still awaiting delivery.
43
+
42
44
  readState.pendingLoads.set(toolCallId, {
43
45
  content: loaded.content.map((part) => ({ ...part })),
44
46
  details: { paths: [...loaded.details.paths] },
@@ -63,8 +65,6 @@ export default function registerGuidelines(pi: ExtensionAPI): void {
63
65
  return { systemPrompt };
64
66
  });
65
67
 
66
- // Compaction may summarize the loaded guides out of context mid-run; force
67
- // a fresh read instead of trusting a now-stale in-memory flag.
68
68
  pi.on('session_compact', () => {
69
69
  resetGuidelineReadState(readState);
70
70
  });
@@ -5,8 +5,6 @@ export { LANGUAGE_GUIDE_FILES } from './catalog.ts';
5
5
  export const GUIDELINES_REMINDER_HEADING =
6
6
  'Sideroom coding guidelines (always-on reminder):';
7
7
 
8
- // One dense index instead of one sentence per language: the same paths and the
9
- // same requirement, a fraction of the tokens every turn.
10
8
  const LANGUAGE_GUIDE_INDEX = LANGUAGE_GUIDES.map(
11
9
  ({ extensions, path }) => `${extensions.join('/')} ${path}`,
12
10
  ).join('; ');
@@ -3,6 +3,7 @@ import {
3
3
  isEditToolResult,
4
4
  isWriteToolResult,
5
5
  } from '@earendil-works/pi-coding-agent';
6
+ import { readCheckStatus } from '../shared/check-status.ts';
6
7
  import { isReviewNote, REVIEW_NOTE_EVENT } from '../shared/review-note.ts';
7
8
  import {
8
9
  registerSettleGuard,
@@ -14,13 +15,13 @@ import {
14
15
  export const REVIEW_STEER_TYPE = 'sideroom-guidelines-review';
15
16
 
16
17
  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. 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
+ '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. Reuse relevant checks that already passed in this user turn after the latest file mutation. Run only missing, failed, or stale formatter, linter, type checks, and tests; any further file mutation makes earlier checks stale. If the guide is no longer in context, re-read it in full first.';
18
19
 
19
20
  export const WORKFLOW_REVIEW_STEER =
20
21
  '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.';
21
22
 
22
23
  export const REVIEW_NOTES_STEER =
23
- 'Additional semantic notes arrived after the review. Verify each against the loaded Sideroom language guide, fix only demonstrated violations introduced by this run without removing required behavior or refactoring unrelated code, then rerun the relevant checks.';
24
+ 'Additional semantic notes arrived after the review. Verify each against the loaded Sideroom language guide, fix only demonstrated violations introduced by this run without removing required behavior or refactoring unrelated code, reuse checks that remain valid, and run only missing, failed, or stale checks after any corrective edit.';
24
25
 
25
26
  export interface ReviewState {
26
27
  mutated: boolean;
@@ -49,8 +50,6 @@ export function shouldReview(state: ReviewState): boolean {
49
50
  return (state.mutated || state.notes.length > 0) && !state.steeredThisTurn;
50
51
  }
51
52
 
52
- // Notes that arrive after the review fired get one follow-up per turn, so a
53
- // fix that draws new notes cannot keep the run going.
54
53
  export function pendingReviewSteer(state: ReviewState): string | undefined {
55
54
  if (shouldReview(state)) {
56
55
  return withNotes(
@@ -110,23 +109,23 @@ export function registerReviewGuard(
110
109
  }
111
110
  });
112
111
 
113
- // agent_before_settle runs after retries and compaction recovery, and its
114
- // continuation stays inside the same run. agent_settled is notification-only:
115
- // a steer sent there starts a second run after the user already saw the
116
- // agent stop, and it fires even when the user pressed Escape.
117
112
  registerSettleGuard(pi, {
118
113
  priority: SETTLE_PRIORITY.review,
119
114
  pending: () => pendingReviewSteer(state) !== undefined,
120
- handle: (event) => {
115
+ handle: (event, ctx) => {
121
116
  const steer = pendingReviewSteer(state);
122
117
  if (steer === undefined) {
123
118
  return;
124
119
  }
125
120
  markReviewSteered(state);
121
+ const check = readCheckStatus(pi, ctx);
122
+ const checkNote = check?.green
123
+ ? `\n\nThe detected project check \`${check.command}\` passed after the latest successful file mutation in this user turn. Reuse that result if this review changes no files; rerun it after any further mutation.`
124
+ : '';
126
125
  return withHiddenSteer({
127
126
  event,
128
127
  customType: REVIEW_STEER_TYPE,
129
- content: steer,
128
+ content: steer + checkNote,
130
129
  });
131
130
  },
132
131
  });
@@ -49,8 +49,6 @@ export interface JevAttempt {
49
49
  readonly failure: JevFailure | undefined;
50
50
  }
51
51
 
52
- // The endpoint is a package constant, never a value from the environment or the
53
- // session, so the outbound URL cannot be steered by the repository being edited.
54
52
  export function createFetchTransport(
55
53
  timeoutMs: number = JEV_REQUEST_TIMEOUT_MS,
56
54
  ): JevTransport {
@@ -68,8 +66,6 @@ export function createFetchTransport(
68
66
  };
69
67
  }
70
68
 
71
- // Pi waits for every tool_result handler before the result reaches the model,
72
- // so the user's Escape has to cut the request, not only the timeout.
73
69
  function requestSignal(
74
70
  timeoutMs: number,
75
71
  runSignal: AbortSignal | undefined,
@@ -97,9 +93,6 @@ export function classifyStatus(status: number): JevFailure | undefined {
97
93
  return undefined;
98
94
  }
99
95
 
100
- // A Jev failure is a missing note, never an error: the mutation already
101
- // succeeded and the mechanical rules already ran. So this returns a value
102
- // instead of throwing, and the caller only decides whether to keep calling.
103
96
  export async function askJev(
104
97
  transport: JevTransport,
105
98
  apiKey: string,
@@ -29,7 +29,6 @@ interface ScanResult {
29
29
  readonly partial: boolean;
30
30
  }
31
31
 
32
- // Keep the edited file complete. Other files are included whole or not at all.
33
32
  export function buildContextualState(
34
33
  cwd: string,
35
34
  path: string,
@@ -339,7 +338,6 @@ function matchesReference(names: ReadonlySet<string>, path: string): boolean {
339
338
  return false;
340
339
  }
341
340
 
342
- // Naming is evidence only for a sibling test, never for a repository-wide basename match.
343
341
  function isCompanionTest(source: string, candidate: string): boolean {
344
342
  if (
345
343
  isTestPath(source) ||