@rmrdeveloper/sideroom-pi 9.0.0 → 9.1.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 (62) hide show
  1. package/README.md +23 -28
  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 +20 -0
  8. package/extensions/done/index.ts +0 -1
  9. package/extensions/done/model.ts +1 -3
  10. package/extensions/explain/guard.ts +11 -21
  11. package/extensions/explain/index.ts +0 -1
  12. package/extensions/explain/model.ts +0 -5
  13. package/extensions/guidelines/execute.ts +16 -2
  14. package/extensions/guidelines/guard.ts +1 -2
  15. package/extensions/guidelines/index.ts +7 -7
  16. package/extensions/guidelines/prompt.ts +0 -2
  17. package/extensions/guidelines/review.ts +9 -10
  18. package/extensions/jev/client.ts +0 -7
  19. package/extensions/jev/context.ts +0 -2
  20. package/extensions/jev/guard.ts +78 -44
  21. package/extensions/jev/index.ts +0 -3
  22. package/extensions/jev/key.ts +0 -3
  23. package/extensions/jev/model.ts +0 -8
  24. package/extensions/jev/queue.ts +125 -0
  25. package/extensions/jev/ui.ts +0 -14
  26. package/extensions/modified-files/index.ts +0 -1
  27. package/extensions/monorepo-skills/index.ts +0 -1
  28. package/extensions/persona/checks.ts +0 -4
  29. package/extensions/persona/guard.ts +45 -6
  30. package/extensions/persona/index.ts +4 -2
  31. package/extensions/persona/model.ts +0 -2
  32. package/extensions/persona/prompt.ts +0 -2
  33. package/extensions/rules/checks.ts +0 -5
  34. package/extensions/rules/index.ts +0 -1
  35. package/extensions/rules/lexer.ts +1 -1
  36. package/extensions/shared/check-status.ts +35 -0
  37. package/extensions/shared/file-path.ts +0 -3
  38. package/extensions/shared/read-file.ts +0 -2
  39. package/extensions/shared/settle.ts +78 -13
  40. package/extensions/todo/guards.ts +40 -84
  41. package/extensions/todo/index.ts +1 -2
  42. package/extensions/todo/model.ts +6 -48
  43. package/extensions/todo/session.ts +1 -4
  44. package/extensions/visual-context/broker.ts +10 -10
  45. package/extensions/visual-context/configure.ts +4 -4
  46. package/extensions/visual-context/files.ts +0 -3
  47. package/extensions/visual-context/index.ts +8 -9
  48. package/extensions/visual-context/literal.ts +0 -1
  49. package/extensions/visual-context/markup.ts +3 -5
  50. package/extensions/visual-context/model.ts +2 -2
  51. package/extensions/visual-context/pairing.ts +1 -2
  52. package/extensions/visual-context/parse-astro.ts +9 -12
  53. package/extensions/visual-context/parse-jsx.ts +2 -2
  54. package/extensions/visual-context/parse-vue.ts +1 -1
  55. package/extensions/visual-context/setup.ts +8 -8
  56. package/extensions/visual-context/source.ts +0 -3
  57. package/package.json +15 -9
  58. package/skills/sideroom-guidelines/SKILL.md +1 -1
  59. package/skills/sideroom-persona/SKILL.md +8 -6
  60. package/skills/sideroom-user-stories/SKILL.md +78 -0
  61. package/skills/sideroom-user-stories/references/review.md +84 -0
  62. package/skills/sideroom-user-stories/references/templates.md +131 -0
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Sideroom Pi — the side room your agent was missing
1
+ # Sideroom Pi: make "done" mean done
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://raw.githubusercontent.com/RMRdeveloper/sideroom-pi/main/media/logo.png" alt="The Sideroom Pi mark" width="640">
@@ -8,8 +8,9 @@
8
8
  [![npm monthly downloads](https://img.shields.io/npm/dm/@rmrdeveloper/sideroom-pi?label=downloads&logo=npm)](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
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
- **Asks before it guesses. Shows the plan. Enforces the style. Refuses to
12
- call it done in red.**
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.
13
14
 
14
15
  ```bash
15
16
  pi install npm:@rmrdeveloper/sideroom-pi
@@ -17,18 +18,16 @@ pi install npm:@rmrdeveloper/sideroom-pi
17
18
 
18
19
  ![Sideroom asking a multi-question batch and keeping the live board below it](https://raw.githubusercontent.com/RMRdeveloper/sideroom-pi/main/media/preview.png)
19
20
 
20
- > Coding agents fail in predictable ways: they guess instead of asking, they
21
- > bury progress in chat scroll, they litter your repo with `TODO.md` files,
22
- > every session writes code in a slightly different style, and they declare
23
- > victory while the project checks are still red.
21
+ > "Done, everything works." You open the project. The tests are red.
24
22
  >
25
- > **Sideroom fixes all of it — without touching your repository.**
23
+ > Sideroom is for people who use Pi on real work and are tired of reviewing
24
+ > and redoing what the agent delivered. Same freedom and price as Pi; the
25
+ > discipline you expect from a senior review.
26
26
 
27
- Sideroom Pi is a global [Pi package](https://pi.dev/docs/latest/packages)
28
- that gives the parent agent a *side room* next to your code: a place to ask
29
- sharp questions, show live progress, track what changed, and stay honest
30
- about code quality. The room lives in the Pi session, not in your repo. No
31
- `.pi` config, no task graph, no Sideroom files left behind.
27
+ For developers who chose Pi for control or cost and need the result to be
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.
32
31
 
33
32
  ## Why Sideroom exists
34
33
 
@@ -285,16 +284,13 @@ nothing is detectable, the gate does nothing.
285
284
 
286
285
  ### Explain — the walkthrough offer
287
286
 
288
- When an implementation settles, `explain` steers the agent to offer a
289
- walkthrough through `sideroom_ask`: explain the changes only, how to test them
290
- only, both, or nothing. It fires on `agent_before_settle`, after every retry
291
- and compaction recovery, so the offer never lands on work that is about to be
292
- redone, and it stays inside the same run. It waits one boundary so the
293
- guidelines review runs first; both use the same event, and Pi would otherwise
294
- order them by extension load. It stays quiet after Escape, after an error, and
295
- while a message you typed is waiting. Once per turn, only when the turn touched at least five distinct
296
- files, and only in the TUI, because `sideroom_ask` cannot run anywhere else. No
297
- tool, no widget, no persisted state.
287
+ When a review, style correction, or check already requires a closing
288
+ continuation, `explain` can add a walkthrough offer through `sideroom_ask`:
289
+ changes only, test steps only, both, or nothing. The instruction asks the agent
290
+ to finish the work, review and checks before offering. It never opens a turn
291
+ solely for an offer. Once per user turn, only after at least five distinct
292
+ files changed, and only in the TUI. Aborts, errors and queued messages postpone
293
+ it. No tool, widget or persisted state.
298
294
 
299
295
  ## Quick path
300
296
 
@@ -467,14 +463,13 @@ itself.
467
463
  since the last idle user prompt, in TUI mode, on a completed run with no user
468
464
  message pending. File identity follows Pi's path aliases and canonicalizes
469
465
  existing paths.
470
- - One boundary of delay: the guidelines review steers on the same event, and Pi
471
- runs both handlers in extension load order, so `explain` offers at the next
472
- boundary.
466
+ - Delivery: joins an existing necessary settle continuation through the shared
467
+ supplement event, in any extension load order. If none is needed, stays silent.
473
468
  - The steer names the intent; the agent writes the question in the user's
474
469
  language with four options, one of them a decline, and one recommendation.
475
470
  `sideroom_ask` still adds *Out of scope* and the custom answer.
476
- - At most one offer per turn. The flag is set before the steer, so the turn the
477
- offer triggers cannot re-trigger it.
471
+ - At most one offer per turn. The flag is set only when the supplement is
472
+ appended; the offer never triggers its own continuation.
478
473
  - No green-check requirement: red checks still produce the offer.
479
474
 
480
475
  ### Grill contract
@@ -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,8 +6,10 @@ import {
6
6
  isToolCallEventType,
7
7
  isWriteToolResult,
8
8
  } from '@earendil-works/pi-coding-agent';
9
+ import { registerCheckStatus } from '../shared/check-status.ts';
9
10
  import {
10
11
  registerSettleGuard,
12
+ registerSettleSupplement,
11
13
  SETTLE_PRIORITY,
12
14
  withHiddenSteer,
13
15
  } from '../shared/settle.ts';
@@ -46,6 +48,13 @@ export function registerDoneGuard({
46
48
  const checkRuns = new Set<string>();
47
49
  const mutationPaths = new Map<string, string>();
48
50
 
51
+ registerCheckStatus(pi, (ctx) => {
52
+ const check = resolveCommand(ctx);
53
+ return check === undefined
54
+ ? undefined
55
+ : { command: check.display, green: state.checksGreen };
56
+ });
57
+
49
58
  pi.on('tool_call', (event, ctx) => {
50
59
  if (
51
60
  isToolCallEventType('write', event) ||
@@ -86,6 +95,17 @@ export function registerDoneGuard({
86
95
  }
87
96
  });
88
97
 
98
+ registerSettleSupplement(pi, (ctx) => {
99
+ if (!shouldNoticeMissingTest(state, resolveTestSetup(ctx))) {
100
+ return undefined;
101
+ }
102
+ state.noticedMissingTest = true;
103
+ return {
104
+ customType: MISSING_TEST_NOTICE_TYPE,
105
+ content: formatMissingTestNotice(),
106
+ };
107
+ });
108
+
89
109
  registerSettleGuard(pi, {
90
110
  priority: SETTLE_PRIORITY.done,
91
111
  pending: (ctx) =>
@@ -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>();
@@ -15,8 +15,6 @@ export const MISSING_TEST_NOTICE_TYPE = 'sideroom-missing-test-notice';
15
15
 
16
16
  const TEST_DIRECTORY = /(?:^|\/)(?:__tests__|tests?|specs?)(?:\/|$)/;
17
17
 
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
18
  const TEST_FILE =
21
19
  /(?:^|\/)(?:test_[\w.-]*\.py|[\w.-]*[._](?:test|spec)\.[\w.-]+|[\w.-]*(?:Test|Tests)\.java)$/;
22
20
 
@@ -82,5 +80,5 @@ export function formatDoneSteer(display: string): string {
82
80
  }
83
81
 
84
82
  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.';
83
+ 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
84
  }
@@ -3,12 +3,7 @@ import {
3
3
  isEditToolResult,
4
4
  isWriteToolResult,
5
5
  } from '@earendil-works/pi-coding-agent';
6
- import {
7
- registerSettleGuard,
8
- SETTLE_PRIORITY,
9
- startsUserTurn,
10
- withHiddenSteer,
11
- } from '../shared/settle.ts';
6
+ import { registerSettleSupplement, startsUserTurn } from '../shared/settle.ts';
12
7
  import {
13
8
  applyOfferDecision,
14
9
  decideOffer,
@@ -43,7 +38,7 @@ export function registerExplainGuard(
43
38
  if (!isEditToolResult(event) && !isWriteToolResult(event)) {
44
39
  return;
45
40
  }
46
- // SAFETY: Pi validates built-in file-tool inputs before emitting tool_result.
41
+
47
42
  const input = event.input as unknown as FileToolInput;
48
43
  const pathKey = mutationPathKey(ctx.cwd, input.path);
49
44
  if (pathKey === undefined) {
@@ -52,19 +47,14 @@ export function registerExplainGuard(
52
47
  recordMutation(state, pathKey);
53
48
  });
54
49
 
55
- // agent_before_settle runs after retries and compaction recovery, so the
56
- // walkthrough is never offered over reworked work, and its continuation
57
- // stays inside the same run instead of starting a second one.
58
- registerSettleGuard(pi, {
59
- priority: SETTLE_PRIORITY.explain,
60
- pending: (ctx) => decideOffer(state, ctx.mode) === EXPLAIN_DECISION.send,
61
- handle: (event, ctx) => {
62
- applyOfferDecision(state, decideOffer(state, ctx.mode));
63
- return withHiddenSteer({
64
- event,
65
- customType: EXPLAIN_OFFER_TYPE,
66
- content: EXPLAIN_OFFER,
67
- });
68
- },
50
+ registerSettleSupplement(pi, (ctx) => {
51
+ if (decideOffer(state, ctx.mode) !== EXPLAIN_DECISION.send) {
52
+ return undefined;
53
+ }
54
+ applyOfferDecision(state, EXPLAIN_DECISION.send);
55
+ return {
56
+ customType: EXPLAIN_OFFER_TYPE,
57
+ content: `After finishing the requested work, review, and checks: ${EXPLAIN_OFFER}`,
58
+ };
69
59
  });
70
60
  }
@@ -2,7 +2,6 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  import { registerExplainGuard } from './guard.ts';
3
3
  import { createExplainState, resetExplainTurn } from './model.ts';
4
4
 
5
- // Pi loads extensions/*/index.ts through export default.
6
5
  export default function registerExplain(pi: ExtensionAPI): void {
7
6
  const state = createExplainState();
8
7
  registerExplainGuard(pi, state);
@@ -2,8 +2,6 @@ import { resolveFileToolPath } from '../shared/file-path.ts';
2
2
 
3
3
  export const EXPLAIN_OFFER_TYPE = 'sideroom-explain-offer';
4
4
 
5
- // Five is the point where a turn stopped being an adjustment and became work
6
- // worth walking the user through.
7
5
  export const EXPLAIN_FILE_THRESHOLD = 5;
8
6
 
9
7
  const TUI_MODE = 'tui';
@@ -36,8 +34,6 @@ export function resetExplainTurn(state: ExplainState): void {
36
34
  state.offeredThisTurn = false;
37
35
  }
38
36
 
39
- // Pi reports the path as the model wrote it, so use the built-in file-tool
40
- // semantics and canonicalize existing aliases before counting the file.
41
37
  export function mutationPathKey(cwd: string, path: string): string | undefined {
42
38
  return resolveFileToolPath(cwd, path);
43
39
  }
@@ -46,7 +42,6 @@ export function recordMutation(state: ExplainState, pathKey: string): void {
46
42
  state.mutatedFilesThisTurn.add(pathKey);
47
43
  }
48
44
 
49
- // The shared settle guard gives review and checks priority over the offer.
50
45
  export function decideOffer(
51
46
  state: ExplainState,
52
47
  mode: string,
@@ -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) ||