@rmrdeveloper/sideroom-pi 9.1.0 → 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.
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,24 +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.
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.
284
285
 
285
- ### Explain — the walkthrough offer
286
+ ### Explain — the walkthrough invitation
286
287
 
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.
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.
294
295
 
295
296
  ## Quick path
296
297
 
@@ -455,22 +456,20 @@ itself.
455
456
  `cargo test`, or `make check`. Detection is cached per working directory.
456
457
  - Green means that command ran with a zero exit in this run. A later
457
458
  successful `write`/`edit` clears green again.
458
- - 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.
459
461
 
460
462
  ### Explain contract
461
463
 
462
- - Trigger: `agent_before_settle` with at least five distinct files mutated
463
- since the last idle user prompt, in TUI mode, on a completed run with no user
464
- message pending. File identity follows Pi's path aliases and canonicalizes
465
- existing paths.
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.
468
- - The steer names the intent; the agent writes the question in the user's
469
- language with four options, one of them a decline, and one recommendation.
470
- `sideroom_ask` still adds *Out of scope* and the custom answer.
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.
473
- - 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.
474
473
 
475
474
  ### Grill contract
476
475
 
@@ -7,23 +7,15 @@ import {
7
7
  isWriteToolResult,
8
8
  } from '@earendil-works/pi-coding-agent';
9
9
  import { registerCheckStatus } from '../shared/check-status.ts';
10
- import {
11
- registerSettleGuard,
12
- registerSettleSupplement,
13
- SETTLE_PRIORITY,
14
- withHiddenSteer,
15
- } from '../shared/settle.ts';
10
+ import { registerSettleSupplement } from '../shared/settle.ts';
16
11
  import { type CheckCommand, commandMatches } from './detect.ts';
17
12
  import {
18
- DONE_GATE_TYPE,
19
13
  type DoneState,
20
- formatDoneSteer,
21
14
  formatMissingTestNotice,
22
15
  isSourcePath,
23
16
  isTestPath,
24
17
  MISSING_TEST_NOTICE_TYPE,
25
18
  shouldNoticeMissingTest,
26
- shouldSteerDone,
27
19
  } from './model.ts';
28
20
 
29
21
  export type ResolveCommand = (
@@ -82,7 +74,6 @@ export function registerDoneGuard({
82
74
  if (event.isError) {
83
75
  return;
84
76
  }
85
- state.mutated = true;
86
77
  state.checksGreen = false;
87
78
  recordMutationPath(state, path);
88
79
  return;
@@ -105,34 +96,6 @@ export function registerDoneGuard({
105
96
  content: formatMissingTestNotice(),
106
97
  };
107
98
  });
108
-
109
- registerSettleGuard(pi, {
110
- priority: SETTLE_PRIORITY.done,
111
- pending: (ctx) =>
112
- shouldSteerDone(state, resolveCommand(ctx) !== undefined) ||
113
- shouldNoticeMissingTest(state, resolveTestSetup(ctx)),
114
- handle: (event, ctx) => {
115
- const check = resolveCommand(ctx);
116
- if (check !== undefined && shouldSteerDone(state, true)) {
117
- state.steerCount += 1;
118
- return withHiddenSteer({
119
- event,
120
- customType: DONE_GATE_TYPE,
121
- content: formatDoneSteer(check.display),
122
- });
123
- }
124
- if (!shouldNoticeMissingTest(state, resolveTestSetup(ctx))) {
125
- return;
126
- }
127
- state.noticedMissingTest = true;
128
- state.steerCount += 1;
129
- return withHiddenSteer({
130
- event,
131
- customType: MISSING_TEST_NOTICE_TYPE,
132
- content: formatMissingTestNotice(),
133
- });
134
- },
135
- });
136
99
  }
137
100
 
138
101
  function recordMutationPath(state: DoneState, path: string | undefined): void {
@@ -1,16 +1,10 @@
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?)(?:\/|$)/;
@@ -22,9 +16,7 @@ const SOURCE_FILE = /\.(?:[cm]?js|jsx|ts|tsx|go|java|php|py|rs)$/i;
22
16
 
23
17
  export function createDoneState(): DoneState {
24
18
  return {
25
- mutated: false,
26
19
  checksGreen: false,
27
- steerCount: 0,
28
20
  mutatedSource: false,
29
21
  mutatedTest: false,
30
22
  noticedMissingTest: false,
@@ -32,9 +24,7 @@ export function createDoneState(): DoneState {
32
24
  }
33
25
 
34
26
  export function resetDoneRun(state: DoneState): void {
35
- state.mutated = false;
36
27
  state.checksGreen = false;
37
- state.steerCount = 0;
38
28
  state.mutatedSource = false;
39
29
  state.mutatedTest = false;
40
30
  state.noticedMissingTest = false;
@@ -50,18 +40,6 @@ export function isSourcePath(path: string): boolean {
50
40
  return SOURCE_FILE.test(normalized);
51
41
  }
52
42
 
53
- export function shouldSteerDone(
54
- state: DoneState,
55
- hasCommand: boolean,
56
- ): boolean {
57
- return (
58
- hasCommand &&
59
- state.mutated &&
60
- !state.checksGreen &&
61
- state.steerCount < MAX_DONE_STEERS
62
- );
63
- }
64
-
65
43
  export function shouldNoticeMissingTest(
66
44
  state: DoneState,
67
45
  hasTests: boolean,
@@ -70,15 +48,10 @@ export function shouldNoticeMissingTest(
70
48
  hasTests &&
71
49
  state.mutatedSource &&
72
50
  !state.mutatedTest &&
73
- !state.noticedMissingTest &&
74
- state.steerCount < MAX_DONE_STEERS
51
+ !state.noticedMissingTest
75
52
  );
76
53
  }
77
54
 
78
- export function formatDoneSteer(display: string): string {
79
- return `Run \`${display}\` and make it pass before finishing.`;
80
- }
81
-
82
55
  export function formatMissingTestNotice(): string {
83
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.';
84
57
  }
@@ -1,10 +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
4
  export default function registerExplain(pi: ExtensionAPI): void {
6
- const state = createExplainState();
7
- registerExplainGuard(pi, state);
8
-
9
- pi.on('session_start', () => resetExplainTurn(state));
5
+ pi.on('before_agent_start', (event) => ({
6
+ systemPrompt: appendExplainReminder(event.systemPrompt),
7
+ }));
10
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
+ }
@@ -39,7 +39,7 @@ async function waitForSettle(pi: ExtensionAPI, ctx: ExtensionContext): Promise<v
39
39
  await Promise.all(request.waits);
40
40
  }
41
41
 
42
- export const SETTLE_PRIORITY = { review: 0, persona: 1, done: 2 } as const;
42
+ export const SETTLE_PRIORITY = { review: 0, persona: 1 } as const;
43
43
 
44
44
  interface SettleSupplement {
45
45
  readonly customType: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rmrdeveloper/sideroom-pi",
3
- "version": "9.1.0",
4
- "description": "Make \"done\" mean done for Pi. Asks before it guesses, enforces your coding rules, and refuses to finish while your project's checks are red.",
3
+ "version": "9.2.0",
4
+ "description": "Make \"done\" mean done for Pi. Asks before it guesses, enforces your coding rules, and reviews changes before finishing.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "extensions",
@@ -104,7 +104,7 @@ At most three steers are sent per agent run.
104
104
  - No persisted profile state, no profiles, no switching: the persona ships with the package.
105
105
  - Nothing is written to the target repository.
106
106
  - The persona governs wording. It never relaxes the coding guidelines, the
107
- added-line rules, or the check-before-finish gate.
107
+ added-line rules, or the project's verification requirements.
108
108
 
109
109
  ## References
110
110
 
@@ -1,60 +0,0 @@
1
- import {
2
- type ExtensionAPI,
3
- isEditToolResult,
4
- isWriteToolResult,
5
- } from '@earendil-works/pi-coding-agent';
6
- import { registerSettleSupplement, startsUserTurn } from '../shared/settle.ts';
7
- import {
8
- applyOfferDecision,
9
- decideOffer,
10
- EXPLAIN_DECISION,
11
- EXPLAIN_OFFER,
12
- EXPLAIN_OFFER_TYPE,
13
- type ExplainState,
14
- mutationPathKey,
15
- recordMutation,
16
- resetExplainTurn,
17
- } from './model.ts';
18
-
19
- interface FileToolInput {
20
- readonly path: string;
21
- }
22
-
23
- export function registerExplainGuard(
24
- pi: ExtensionAPI,
25
- state: ExplainState,
26
- ): void {
27
- pi.on('input', (event) => {
28
- if (!startsUserTurn(event)) {
29
- return;
30
- }
31
- resetExplainTurn(state);
32
- });
33
-
34
- pi.on('tool_result', (event, ctx) => {
35
- if (event.isError) {
36
- return;
37
- }
38
- if (!isEditToolResult(event) && !isWriteToolResult(event)) {
39
- return;
40
- }
41
-
42
- const input = event.input as unknown as FileToolInput;
43
- const pathKey = mutationPathKey(ctx.cwd, input.path);
44
- if (pathKey === undefined) {
45
- return;
46
- }
47
- recordMutation(state, pathKey);
48
- });
49
-
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
- };
59
- });
60
- }
@@ -1,66 +0,0 @@
1
- import { resolveFileToolPath } from '../shared/file-path.ts';
2
-
3
- export const EXPLAIN_OFFER_TYPE = 'sideroom-explain-offer';
4
-
5
- export const EXPLAIN_FILE_THRESHOLD = 5;
6
-
7
- const TUI_MODE = 'tui';
8
-
9
- export const EXPLAIN_OFFER =
10
- "Use sideroom_ask for one question in the user's language with four parallel options: changes only, test steps only, both, or no explanation. Recommend changes only.";
11
-
12
- export const EXPLAIN_DECISION = {
13
- send: 'send',
14
- none: 'none',
15
- } as const;
16
-
17
- export type ExplainDecision =
18
- (typeof EXPLAIN_DECISION)[keyof typeof EXPLAIN_DECISION];
19
-
20
- export interface ExplainState {
21
- mutatedFilesThisTurn: Set<string>;
22
- offeredThisTurn: boolean;
23
- }
24
-
25
- export function createExplainState(): ExplainState {
26
- return {
27
- mutatedFilesThisTurn: new Set(),
28
- offeredThisTurn: false,
29
- };
30
- }
31
-
32
- export function resetExplainTurn(state: ExplainState): void {
33
- state.mutatedFilesThisTurn.clear();
34
- state.offeredThisTurn = false;
35
- }
36
-
37
- export function mutationPathKey(cwd: string, path: string): string | undefined {
38
- return resolveFileToolPath(cwd, path);
39
- }
40
-
41
- export function recordMutation(state: ExplainState, pathKey: string): void {
42
- state.mutatedFilesThisTurn.add(pathKey);
43
- }
44
-
45
- export function decideOffer(
46
- state: ExplainState,
47
- mode: string,
48
- ): ExplainDecision {
49
- const thresholdReached =
50
- mode === TUI_MODE &&
51
- !state.offeredThisTurn &&
52
- state.mutatedFilesThisTurn.size >= EXPLAIN_FILE_THRESHOLD;
53
- if (thresholdReached) {
54
- return EXPLAIN_DECISION.send;
55
- }
56
- return EXPLAIN_DECISION.none;
57
- }
58
-
59
- export function applyOfferDecision(
60
- state: ExplainState,
61
- decision: ExplainDecision,
62
- ): void {
63
- if (decision === EXPLAIN_DECISION.send) {
64
- state.offeredThisTurn = true;
65
- }
66
- }