@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.
- package/README.md +35 -40
- package/assets/visual-context/browser/background.js +2 -2
- package/assets/visual-context/browser/popup.js +1 -2
- package/extensions/ask/index.ts +0 -1
- package/extensions/ask/model.ts +1 -1
- package/extensions/ask/selection.ts +0 -4
- package/extensions/done/guard.ts +18 -35
- package/extensions/done/index.ts +0 -1
- package/extensions/done/model.ts +2 -31
- package/extensions/explain/index.ts +4 -7
- package/extensions/explain/prompt.ts +13 -0
- package/extensions/guidelines/execute.ts +16 -2
- package/extensions/guidelines/guard.ts +1 -2
- package/extensions/guidelines/index.ts +7 -7
- package/extensions/guidelines/prompt.ts +0 -2
- package/extensions/guidelines/review.ts +9 -10
- package/extensions/jev/client.ts +0 -7
- package/extensions/jev/context.ts +0 -2
- package/extensions/jev/guard.ts +78 -44
- package/extensions/jev/index.ts +0 -3
- package/extensions/jev/key.ts +0 -3
- package/extensions/jev/model.ts +0 -8
- package/extensions/jev/queue.ts +125 -0
- package/extensions/jev/ui.ts +0 -14
- package/extensions/modified-files/index.ts +0 -1
- package/extensions/monorepo-skills/index.ts +0 -1
- package/extensions/persona/checks.ts +0 -4
- package/extensions/persona/guard.ts +45 -6
- package/extensions/persona/index.ts +4 -2
- package/extensions/persona/model.ts +0 -2
- package/extensions/persona/prompt.ts +0 -2
- package/extensions/rules/checks.ts +0 -5
- package/extensions/rules/index.ts +0 -1
- package/extensions/rules/lexer.ts +1 -1
- package/extensions/shared/check-status.ts +35 -0
- package/extensions/shared/file-path.ts +0 -3
- package/extensions/shared/read-file.ts +0 -2
- package/extensions/shared/settle.ts +78 -13
- package/extensions/todo/guards.ts +40 -84
- package/extensions/todo/index.ts +1 -2
- package/extensions/todo/model.ts +6 -48
- package/extensions/todo/session.ts +1 -4
- package/extensions/visual-context/broker.ts +10 -10
- package/extensions/visual-context/configure.ts +4 -4
- package/extensions/visual-context/files.ts +0 -3
- package/extensions/visual-context/index.ts +8 -9
- package/extensions/visual-context/literal.ts +0 -1
- package/extensions/visual-context/markup.ts +3 -5
- package/extensions/visual-context/model.ts +2 -2
- package/extensions/visual-context/pairing.ts +1 -2
- package/extensions/visual-context/parse-astro.ts +9 -12
- package/extensions/visual-context/parse-jsx.ts +2 -2
- package/extensions/visual-context/parse-vue.ts +1 -1
- package/extensions/visual-context/setup.ts +8 -8
- package/extensions/visual-context/source.ts +0 -3
- package/package.json +15 -9
- package/skills/sideroom-guidelines/SKILL.md +1 -1
- package/skills/sideroom-persona/SKILL.md +9 -7
- package/skills/sideroom-user-stories/SKILL.md +78 -0
- package/skills/sideroom-user-stories/references/review.md +84 -0
- package/skills/sideroom-user-stories/references/templates.md +131 -0
- package/extensions/explain/guard.ts +0 -70
- package/extensions/explain/model.ts +0 -71
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
[](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
|
|
13
|
-
|
|
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
|
|
30
|
-
|
|
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 |
|
|
65
|
-
| Work left unexplained | `explain` —
|
|
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
|
-
- **
|
|
105
|
-
|
|
106
|
-
|
|
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 —
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|
package/extensions/ask/index.ts
CHANGED
|
@@ -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,
|
package/extensions/ask/model.ts
CHANGED
|
@@ -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
|
|
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,
|
package/extensions/done/guard.ts
CHANGED
|
@@ -6,22 +6,16 @@ import {
|
|
|
6
6
|
isToolCallEventType,
|
|
7
7
|
isWriteToolResult,
|
|
8
8
|
} from '@earendil-works/pi-coding-agent';
|
|
9
|
-
import {
|
|
10
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
package/extensions/done/index.ts
CHANGED
|
@@ -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>();
|
package/extensions/done/model.ts
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
});
|
package/extensions/jev/client.ts
CHANGED
|
@@ -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) ||
|