@rmrdeveloper/sideroom-pi 8.10.0 → 8.12.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 +63 -28
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
- package/extensions/ask/index.ts +3 -2
- package/extensions/done/guard.ts +42 -37
- package/extensions/done/index.ts +3 -2
- package/extensions/done/model.ts +1 -5
- package/extensions/explain/guard.ts +13 -11
- package/extensions/explain/model.ts +2 -25
- package/extensions/guidelines/review.ts +31 -13
- package/extensions/jev/client.ts +27 -2
- package/extensions/jev/context.ts +360 -0
- package/extensions/jev/guard.ts +116 -34
- package/extensions/jev/model.ts +175 -23
- package/extensions/jev/workflow.ts +188 -0
- package/extensions/persona/catalog.ts +2 -2
- package/extensions/persona/execute.ts +4 -6
- package/extensions/persona/index.ts +3 -2
- package/extensions/persona/model.ts +1 -31
- package/extensions/persona/prompt.ts +5 -3
- package/extensions/shared/settle.ts +63 -13
- package/package.json +1 -1
- package/skills/sideroom-architecture/SKILL.md +17 -8
- package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
- package/skills/sideroom-codebase-design/SKILL.md +151 -0
- package/skills/sideroom-codebase-design/references/growth.md +62 -0
- package/skills/sideroom-domain-modeling/SKILL.md +1 -1
- package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
- package/skills/sideroom-grill/SKILL.md +16 -6
- package/skills/sideroom-guidelines/SKILL.md +9 -7
- package/skills/sideroom-guidelines/references/languages/go.md +3 -3
- package/skills/sideroom-guidelines/references/languages/java.md +3 -3
- package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
- package/skills/sideroom-guidelines/references/languages/python.md +3 -3
- package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
- package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
- package/skills/sideroom-persona/SKILL.md +43 -25
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Sideroom Pi — the side room your agent was missing
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/RMRdeveloper/sideroom-pi/main/media/logo.png" alt="The Sideroom Pi mark" width="640">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
[](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
|
|
4
8
|
[](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
|
|
5
9
|
[](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
|
|
@@ -54,9 +58,10 @@ Sideroom answers each one with a small, opinionated surface:
|
|
|
54
58
|
| Style drift | A pre-edit gate + the on-demand `sideroom-guidelines` skill + an end-of-turn review steer |
|
|
55
59
|
| Invisible monorepo skills | `monorepo-skills` — trusted child folders join Pi's available skill list |
|
|
56
60
|
| Vague plans | `sideroom-grill` — an interview that settles the words before the work |
|
|
61
|
+
| Costly growth and refactors | `sideroom-codebase-design` — evidenced improvements to module design, tests, and long-term scaling |
|
|
57
62
|
| Unapplied guidelines | `sideroom_rules` — mechanical checks that block or flag the lines you add |
|
|
58
|
-
| Unchecked judgment rules | `jev` — asks a decision model about
|
|
59
|
-
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of
|
|
63
|
+
| Unchecked judgment rules | `jev` — asks a decision model about guide rules with visible evidence |
|
|
64
|
+
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of unnecessary technical words |
|
|
60
65
|
| Premature completion | `sideroom_done` — steers back to the project's check command before finishing |
|
|
61
66
|
| Work left unexplained | `explain` — offers a walkthrough and how to test it once the work settles |
|
|
62
67
|
|
|
@@ -140,9 +145,13 @@ project are labelled `external`. A new session starts clean.
|
|
|
140
145
|
|
|
141
146
|
An on-demand skill for fuzzy plans. It interviews you in `sideroom_ask`
|
|
142
147
|
rounds — reading the codebase first so it never asks what the code already
|
|
143
|
-
says — until you share one understanding.
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
says — until you share one understanding. Its recommendations favor choices
|
|
149
|
+
that are easy to maintain and meet known growth needs over the cheapest initial
|
|
150
|
+
build. It names the upfront cost; if a missing fact could change the choice,
|
|
151
|
+
it asks for that fact before recommending a solution. Resolved terms land in
|
|
152
|
+
`CONTEXT.md` the moment they resolve. Only hard-to-reverse, surprising choices
|
|
153
|
+
with a real trade-off qualify for a record under `docs/adr/`; the user is asked
|
|
154
|
+
first. Everything else stays in the conversation, ready to become a
|
|
146
155
|
spec or an implementation. Vocabulary sharpening during the rounds follows
|
|
147
156
|
the companion `sideroom-domain-modeling` skill, which also runs on its own
|
|
148
157
|
whenever the words — not the plan — are the problem. And when there is no
|
|
@@ -150,6 +159,18 @@ one to interview at all, `sideroom-domain-scaffold` reads the central
|
|
|
150
159
|
domain code as source of truth, builds or completes `CONTEXT.md` on its
|
|
151
160
|
own, and grills the conflicting terms once, at the end.
|
|
152
161
|
|
|
162
|
+
### Codebase design — improve how the code grows
|
|
163
|
+
|
|
164
|
+
`sideroom-codebase-design` reviews existing code, identifies where changes or
|
|
165
|
+
growth create friction, and designs improvements with small interfaces and
|
|
166
|
+
clear responsibilities. It considers growing features, teams, traffic, and
|
|
167
|
+
data, using known needs and evidence rather than hypothetical infrastructure.
|
|
168
|
+
Each recommendation explains the problem, benefit, cost, confidence, migration,
|
|
169
|
+
and verification in text. It produces no diagrams or HTML reports. A review
|
|
170
|
+
stays a review; when implementation is requested, it follows the authorized
|
|
171
|
+
scope and preserves behavior and compatibility. See
|
|
172
|
+
[skill activation](docs/skills.md) for routing and examples.
|
|
173
|
+
|
|
153
174
|
### Guidelines — a gate, not a novel
|
|
154
175
|
|
|
155
176
|
A short system-prompt contract points at the packaged
|
|
@@ -189,17 +210,23 @@ written, no config read: the catalog ships with the package.
|
|
|
189
210
|
|
|
190
211
|
### Jev — judgment where no check reaches
|
|
191
212
|
|
|
192
|
-
Reading the guide covers the mechanical rules. `jev`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
null handling, immutability,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
213
|
+
Reading the guide covers the mechanical rules. `jev` makes one optional call
|
|
214
|
+
to TypeSafe's decision model about the changed file and, when available, up to
|
|
215
|
+
four directly related source files. It checks guard clauses, fail fast,
|
|
216
|
+
command/query separation, null handling, immutability, repeated validation,
|
|
217
|
+
responsibility, dependency direction, comments, testability, redundant
|
|
218
|
+
abstractions, consumer compatibility and test quality only when the supplied
|
|
219
|
+
files support the question. An answer above the cutoff travels with the
|
|
220
|
+
end-of-run guidelines review instead of interrupting each edit. READMEs, JSON,
|
|
221
|
+
lockfiles, deletion-only edits, and files outside the project are never sent.
|
|
222
|
+
|
|
223
|
+
It never blocks and never enters the system prompt. No conversation or session
|
|
224
|
+
history leaves the machine. Successful questionnaires also send their question
|
|
225
|
+
wording and options, without user answers. Newly completed tasks may send their
|
|
226
|
+
descriptions, up to four observed changed files and a check status, without
|
|
227
|
+
commands or logs. These additional calls review explicit contradictions and
|
|
228
|
+
ambiguous choices. Related source excludes ignored, hidden, sensitive-
|
|
229
|
+
named and symlinked paths; a normal source file can still contain a secret.
|
|
203
230
|
The footer counts the calls the session made, and `F10` shows the same count
|
|
204
231
|
beside the key source. Without a key nothing is called, and when Jev runs out of
|
|
205
232
|
quota, rejects the key,
|
|
@@ -214,12 +241,12 @@ without ever writing it to the session.
|
|
|
214
241
|
`sideroom_persona` is the agent's voice, and there is exactly one of it. A
|
|
215
242
|
short reminder rides the system prompt on every turn, so it survives
|
|
216
243
|
compaction; the full guide lives in the on-demand `sideroom-persona` skill.
|
|
217
|
-
The voice is direct and dry, free of filler
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
adds block the mutation; flattering
|
|
221
|
-
AI meta-commentary are answered
|
|
222
|
-
no profile selection to store.
|
|
244
|
+
The voice is direct and dry, free of filler. It uses everyday words even when
|
|
245
|
+
you use technical terms. It keeps a technical term only when needed to
|
|
246
|
+
understand or act, and explains it the first time. Emojis and decorative
|
|
247
|
+
symbols in the lines a `write`/`edit` adds block the mutation; flattering
|
|
248
|
+
openers, hedging, automatic apologies, and AI meta-commentary are answered
|
|
249
|
+
with a capped steer. No profiles to switch and no profile selection to store.
|
|
223
250
|
|
|
224
251
|
### Done — done means green
|
|
225
252
|
|
|
@@ -359,10 +386,17 @@ itself.
|
|
|
359
386
|
arrive after the review fired; nothing blocks and nothing reaches the system
|
|
360
387
|
prompt.
|
|
361
388
|
- One request per successful `write`/`edit` of a project file in a supported
|
|
362
|
-
language that added lines, with
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
language that added lines, with applicable `noul` questions on the complete
|
|
390
|
+
changed file and up to four directly related source files, prioritizing tests
|
|
391
|
+
and production consumers before imports.
|
|
392
|
+
- One additional request per successful, non-cancelled questionnaire and per
|
|
393
|
+
eligible completion update. Only validated question wording or targeted
|
|
394
|
+
completion descriptions and bounded observed evidence travel; no answers,
|
|
395
|
+
conversation history or command output. All requests share the same limits.
|
|
396
|
+
- The official 32k-token state-plus-longest-question and 64k-token request
|
|
397
|
+
limits are observed conservatively using 32,000 and 64,000 UTF-8 bytes.
|
|
398
|
+
Oversized neighbours are omitted whole; if the changed file cannot fit,
|
|
399
|
+
the call is skipped. An answer at or above `0.8` becomes a note.
|
|
366
400
|
- The request follows the run's abort signal; a cancelled request is not a
|
|
367
401
|
failure.
|
|
368
402
|
- Quota (`402`) and a rejected key (`401`, `403`) stop the calls immediately. A
|
|
@@ -426,8 +460,9 @@ and zero ADRs is working as designed.
|
|
|
426
460
|
|
|
427
461
|
### Coexisting with other grilling skills
|
|
428
462
|
|
|
429
|
-
|
|
430
|
-
|
|
463
|
+
See [skill activation](docs/skills.md) for selection boundaries and discovery
|
|
464
|
+
diagnostics. Pi routes skills by description, not by name, so several grilling
|
|
465
|
+
skills can be installed at once. `sideroom-grill` claims non-trivial plans before
|
|
431
466
|
implementation and states precedence over other grilling, interview, and spec
|
|
432
467
|
skills. If a third-party skill still wins, remove it from routing by adding
|
|
433
468
|
`disable-model-invocation: true` to its frontmatter, or exclude its path with
|
|
@@ -181,7 +181,7 @@ async function createUser(data) {
|
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
### 12. DRY (real duplication only)
|
|
184
|
-
RULE: Extract when at least 2 of 3 hold: (a) the logic appears 3+ times, (b) a future rule change would need to touch all occurrences at once, (c) the copies represent the same domain concept. Do not extract on the first or second occurrence, and do not extract look-alike code that belongs to different concepts (a user's email validation vs a vendor's) — they will diverge.
|
|
184
|
+
RULE: Extract when at least 2 of 3 hold: (a) the logic appears 3+ times, (b) a future rule change would need to touch all occurrences at once, (c) the copies represent the same domain concept. Do not extract solely to anticipate reuse on the first or second occurrence, and do not extract look-alike code that belongs to different concepts (a user's email validation vs a vendor's) — they will diverge. Extraction for a present responsibility, clear contract, or testability is valid even with one caller.
|
|
185
185
|
```ts
|
|
186
186
|
// bad — extracted after one real use
|
|
187
187
|
function formatName(x: string) { return x.trim().toUpperCase(); }
|
|
@@ -190,7 +190,7 @@ function normalizeTicketStatus(status: string): TicketStatus { /* ... */ }
|
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
### 13. KISS
|
|
193
|
-
RULE:
|
|
193
|
+
RULE: Choose the simplest design that fully satisfies the current requirements and preserves required behavior. Avoid configurability, hooks, or layers without a demonstrated need; do not remove necessary structure just to reduce code size.
|
|
194
194
|
```js
|
|
195
195
|
// bad
|
|
196
196
|
function formatPrice(value, { currency = 'USD', locale = 'en-US', showSymbol = true } = {}) { /* ... */ }
|
|
@@ -199,7 +199,7 @@ function formatPrice(value) { return `$${value.toFixed(2)}`; }
|
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
### 14. YAGNI
|
|
202
|
-
RULE:
|
|
202
|
+
RULE: Avoid capabilities for hypothetical future use cases without evidence. User requirements, established project contracts, required error handling, compatibility, and meaningful tests are current needs, not speculation. Never use YAGNI to omit them or remove existing supported behavior. Apply it to the proposed change, not as a reason to refactor unrelated code.
|
|
203
203
|
```js
|
|
204
204
|
// bad
|
|
205
205
|
function createInvoice(order, { supportsRecurring = false, supportsMultiCurrency = false } = {}) { /* ... */ }
|
package/extensions/ask/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ export const ASK_PROMPT_SNIPPET =
|
|
|
15
15
|
'Ask the user one or more questions with a recommended option, in their language.';
|
|
16
16
|
|
|
17
17
|
export const ASK_PROMPT_GUIDELINES = [
|
|
18
|
-
'
|
|
19
|
-
'
|
|
18
|
+
'Follow the sideroom-grill skill before non-trivial work without a settled spec. One sideroom_ask call is one batch; call again for another round.',
|
|
19
|
+
'Read the repository first: reuse a consistent project fact. An architectural decision (boundaries, dependencies, cost, security, operations, persistence, public contracts, future change difficulty) is never settled silently: never assume it. Follow the sideroom-architecture skill; ask through sideroom_ask with practical consequences and a one-line justification for one recommendation. Name inconsistent patterns in the prompt. Take minor decisions yourself.',
|
|
20
|
+
'Recommend evidence-backed long-term quality: maintainability and growth for known needs over initial speed. State the upfront cost; avoid speculative capacity. If decisive facts or figures are missing, ask for them first through sideroom_ask; recommend gathering evidence, not an unsupported solution.',
|
|
20
21
|
'Write sideroom_ask prompts, tab labels, option labels, and option descriptions in the language the user is speaking; keep ids and option values in English.',
|
|
21
22
|
'sideroom_ask always adds Out of scope and a write-your-own answer; never send them yourself. A single-selection question needs recommendationIndex; a multiple-selection question needs recommendedIndices and never recommendationIndex.',
|
|
22
23
|
'sideroom_ask only works in the interactive TUI; it errors in print, JSON, and RPC modes.',
|
package/extensions/done/guard.ts
CHANGED
|
@@ -6,6 +6,11 @@ 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
14
|
import { type CheckCommand, commandMatches } from './detect.ts';
|
|
10
15
|
import {
|
|
11
16
|
DONE_GATE_TYPE,
|
|
@@ -25,19 +30,22 @@ export type ResolveCommand = (
|
|
|
25
30
|
|
|
26
31
|
export type ResolveTestSetup = (ctx: Pick<ExtensionContext, 'cwd'>) => boolean;
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
pi: ExtensionAPI
|
|
30
|
-
state: DoneState
|
|
31
|
-
resolveCommand: ResolveCommand
|
|
32
|
-
resolveTestSetup: ResolveTestSetup
|
|
33
|
-
|
|
33
|
+
interface DoneGuardOptions {
|
|
34
|
+
readonly pi: ExtensionAPI;
|
|
35
|
+
readonly state: DoneState;
|
|
36
|
+
readonly resolveCommand: ResolveCommand;
|
|
37
|
+
readonly resolveTestSetup: ResolveTestSetup;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerDoneGuard({
|
|
41
|
+
pi,
|
|
42
|
+
state,
|
|
43
|
+
resolveCommand,
|
|
44
|
+
resolveTestSetup,
|
|
45
|
+
}: DoneGuardOptions): void {
|
|
34
46
|
const checkRuns = new Set<string>();
|
|
35
47
|
const mutationPaths = new Map<string, string>();
|
|
36
48
|
|
|
37
|
-
pi.on('turn_start', () => {
|
|
38
|
-
state.steeredThisTurn = false;
|
|
39
|
-
});
|
|
40
|
-
|
|
41
49
|
pi.on('tool_call', (event, ctx) => {
|
|
42
50
|
if (
|
|
43
51
|
isToolCallEventType('write', event) ||
|
|
@@ -72,41 +80,38 @@ export function registerDoneGuard(
|
|
|
72
80
|
}
|
|
73
81
|
if (isBashToolResult(event)) {
|
|
74
82
|
const wasCheck = checkRuns.delete(event.toolCallId);
|
|
75
|
-
if (wasCheck
|
|
76
|
-
state.checksGreen =
|
|
83
|
+
if (wasCheck) {
|
|
84
|
+
state.checksGreen = !event.isError;
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
});
|
|
80
88
|
|
|
81
|
-
pi
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
state
|
|
85
|
-
state
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
88
100
|
customType: DONE_GATE_TYPE,
|
|
89
101
|
content: formatDoneSteer(check.display),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
state.noticedMissingTest = true;
|
|
101
|
-
state.steerCount += 1;
|
|
102
|
-
pi.sendMessage(
|
|
103
|
-
{
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (!shouldNoticeMissingTest(state, resolveTestSetup(ctx))) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
state.noticedMissingTest = true;
|
|
108
|
+
state.steerCount += 1;
|
|
109
|
+
return withHiddenSteer({
|
|
110
|
+
event,
|
|
104
111
|
customType: MISSING_TEST_NOTICE_TYPE,
|
|
105
112
|
content: formatMissingTestNotice(),
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{ triggerTurn: true, deliverAs: 'steer' },
|
|
109
|
-
);
|
|
113
|
+
});
|
|
114
|
+
},
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
117
|
|
package/extensions/done/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { startsUserTurn } from '../shared/settle.ts';
|
|
5
6
|
import {
|
|
6
7
|
type CheckCommand,
|
|
7
8
|
detectCheckCommand,
|
|
@@ -41,10 +42,10 @@ export default function registerDone(pi: ExtensionAPI): void {
|
|
|
41
42
|
return hasTests;
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
registerDoneGuard(pi, state, resolveCommand, resolveTestSetup);
|
|
45
|
+
registerDoneGuard({ pi, state, resolveCommand, resolveTestSetup });
|
|
45
46
|
|
|
46
47
|
pi.on('input', (event) => {
|
|
47
|
-
if (event
|
|
48
|
+
if (startsUserTurn(event)) {
|
|
48
49
|
resetDoneRun(state);
|
|
49
50
|
}
|
|
50
51
|
});
|
package/extensions/done/model.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export interface DoneState {
|
|
2
2
|
mutated: boolean;
|
|
3
3
|
checksGreen: boolean;
|
|
4
|
-
steeredThisTurn: boolean;
|
|
5
4
|
steerCount: number;
|
|
6
5
|
mutatedSource: boolean;
|
|
7
6
|
mutatedTest: boolean;
|
|
@@ -27,7 +26,6 @@ export function createDoneState(): DoneState {
|
|
|
27
26
|
return {
|
|
28
27
|
mutated: false,
|
|
29
28
|
checksGreen: false,
|
|
30
|
-
steeredThisTurn: false,
|
|
31
29
|
steerCount: 0,
|
|
32
30
|
mutatedSource: false,
|
|
33
31
|
mutatedTest: false,
|
|
@@ -38,7 +36,6 @@ export function createDoneState(): DoneState {
|
|
|
38
36
|
export function resetDoneRun(state: DoneState): void {
|
|
39
37
|
state.mutated = false;
|
|
40
38
|
state.checksGreen = false;
|
|
41
|
-
state.steeredThisTurn = false;
|
|
42
39
|
state.steerCount = 0;
|
|
43
40
|
state.mutatedSource = false;
|
|
44
41
|
state.mutatedTest = false;
|
|
@@ -63,7 +60,6 @@ export function shouldSteerDone(
|
|
|
63
60
|
hasCommand &&
|
|
64
61
|
state.mutated &&
|
|
65
62
|
!state.checksGreen &&
|
|
66
|
-
!state.steeredThisTurn &&
|
|
67
63
|
state.steerCount < MAX_DONE_STEERS
|
|
68
64
|
);
|
|
69
65
|
}
|
|
@@ -86,5 +82,5 @@ export function formatDoneSteer(display: string): string {
|
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
export function formatMissingTestNotice(): string {
|
|
89
|
-
return 'This run changed code files and no test file.
|
|
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.';
|
|
90
86
|
}
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
isWriteToolResult,
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
registerSettleGuard,
|
|
8
|
+
SETTLE_PRIORITY,
|
|
8
9
|
startsUserTurn,
|
|
9
10
|
withHiddenSteer,
|
|
10
11
|
} from '../shared/settle.ts';
|
|
@@ -54,15 +55,16 @@ export function registerExplainGuard(
|
|
|
54
55
|
// agent_before_settle runs after retries and compaction recovery, so the
|
|
55
56
|
// walkthrough is never offered over reworked work, and its continuation
|
|
56
57
|
// stays inside the same run instead of starting a second one.
|
|
57
|
-
pi
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
},
|
|
67
69
|
});
|
|
68
70
|
}
|
|
@@ -13,7 +13,6 @@ export const EXPLAIN_OFFER =
|
|
|
13
13
|
|
|
14
14
|
export const EXPLAIN_DECISION = {
|
|
15
15
|
send: 'send',
|
|
16
|
-
defer: 'defer',
|
|
17
16
|
none: 'none',
|
|
18
17
|
} as const;
|
|
19
18
|
|
|
@@ -22,24 +21,18 @@ export type ExplainDecision =
|
|
|
22
21
|
|
|
23
22
|
export interface ExplainState {
|
|
24
23
|
mutatedFilesThisTurn: Set<string>;
|
|
25
|
-
mutatedSinceSettle: boolean;
|
|
26
|
-
pendingOffer: boolean;
|
|
27
24
|
offeredThisTurn: boolean;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export function createExplainState(): ExplainState {
|
|
31
28
|
return {
|
|
32
29
|
mutatedFilesThisTurn: new Set(),
|
|
33
|
-
mutatedSinceSettle: false,
|
|
34
|
-
pendingOffer: false,
|
|
35
30
|
offeredThisTurn: false,
|
|
36
31
|
};
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
export function resetExplainTurn(state: ExplainState): void {
|
|
40
35
|
state.mutatedFilesThisTurn.clear();
|
|
41
|
-
state.mutatedSinceSettle = false;
|
|
42
|
-
state.pendingOffer = false;
|
|
43
36
|
state.offeredThisTurn = false;
|
|
44
37
|
}
|
|
45
38
|
|
|
@@ -51,13 +44,9 @@ export function mutationPathKey(cwd: string, path: string): string | undefined {
|
|
|
51
44
|
|
|
52
45
|
export function recordMutation(state: ExplainState, pathKey: string): void {
|
|
53
46
|
state.mutatedFilesThisTurn.add(pathKey);
|
|
54
|
-
state.mutatedSinceSettle = true;
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
// The
|
|
58
|
-
// and both handlers run in an extension load order Sideroom does not control.
|
|
59
|
-
// Explain holds its steer back one boundary so the review always runs first;
|
|
60
|
-
// the review's continuation is what brings the next boundary.
|
|
49
|
+
// The shared settle guard gives review and checks priority over the offer.
|
|
61
50
|
export function decideOffer(
|
|
62
51
|
state: ExplainState,
|
|
63
52
|
mode: string,
|
|
@@ -66,15 +55,9 @@ export function decideOffer(
|
|
|
66
55
|
mode === TUI_MODE &&
|
|
67
56
|
!state.offeredThisTurn &&
|
|
68
57
|
state.mutatedFilesThisTurn.size >= EXPLAIN_FILE_THRESHOLD;
|
|
69
|
-
if (
|
|
58
|
+
if (thresholdReached) {
|
|
70
59
|
return EXPLAIN_DECISION.send;
|
|
71
60
|
}
|
|
72
|
-
if (
|
|
73
|
-
thresholdReached ||
|
|
74
|
-
(mode === TUI_MODE && state.mutatedSinceSettle && !state.offeredThisTurn)
|
|
75
|
-
) {
|
|
76
|
-
return EXPLAIN_DECISION.defer;
|
|
77
|
-
}
|
|
78
61
|
return EXPLAIN_DECISION.none;
|
|
79
62
|
}
|
|
80
63
|
|
|
@@ -84,11 +67,5 @@ export function applyOfferDecision(
|
|
|
84
67
|
): void {
|
|
85
68
|
if (decision === EXPLAIN_DECISION.send) {
|
|
86
69
|
state.offeredThisTurn = true;
|
|
87
|
-
state.pendingOffer = false;
|
|
88
|
-
} else if (decision === EXPLAIN_DECISION.defer) {
|
|
89
|
-
state.pendingOffer = true;
|
|
90
|
-
} else {
|
|
91
|
-
state.pendingOffer = false;
|
|
92
70
|
}
|
|
93
|
-
state.mutatedSinceSettle = false;
|
|
94
71
|
}
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
6
|
import { isReviewNote, REVIEW_NOTE_EVENT } from '../shared/review-note.ts';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
registerSettleGuard,
|
|
9
|
+
SETTLE_PRIORITY,
|
|
9
10
|
startsUserTurn,
|
|
10
11
|
withHiddenSteer,
|
|
11
12
|
} from '../shared/settle.ts';
|
|
@@ -13,10 +14,13 @@ import {
|
|
|
13
14
|
export const REVIEW_STEER_TYPE = 'sideroom-guidelines-review';
|
|
14
15
|
|
|
15
16
|
export const REVIEW_STEER =
|
|
16
|
-
'Review
|
|
17
|
+
'Finish any remaining requested implementation before reviewing. Review the changes this run introduced against the loaded Sideroom language guide: fix only demonstrated violations caused by those changes, preserving all requested requirements and existing behavior. KISS and YAGNI do not justify removing required error handling, compatibility, or tests. Do not refactor unrelated code. Then run the relevant formatter, linter, type checks, and tests. If the guide is no longer in context, re-read it in full first.';
|
|
18
|
+
|
|
19
|
+
export const WORKFLOW_REVIEW_STEER =
|
|
20
|
+
'Verify the semantic notes below against the visible evidence and the questionnaire or task contract. Correct only demonstrated ambiguity or contradictions; respect user answers and ask again only when clarification is necessary. Missing evidence is not proof of unfinished work. Do not create unrelated implementation work. Run project checks only if code changes.';
|
|
17
21
|
|
|
18
22
|
export const REVIEW_NOTES_STEER =
|
|
19
|
-
'
|
|
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.';
|
|
20
24
|
|
|
21
25
|
export interface ReviewState {
|
|
22
26
|
mutated: boolean;
|
|
@@ -42,21 +46,27 @@ export function resetReviewTurn(state: ReviewState): void {
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export function shouldReview(state: ReviewState): boolean {
|
|
45
|
-
return state.mutated && !state.steeredThisTurn;
|
|
49
|
+
return (state.mutated || state.notes.length > 0) && !state.steeredThisTurn;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
// Notes that arrive after the review fired get one follow-up per turn, so a
|
|
49
53
|
// fix that draws new notes cannot keep the run going.
|
|
50
54
|
export function pendingReviewSteer(state: ReviewState): string | undefined {
|
|
51
55
|
if (shouldReview(state)) {
|
|
52
|
-
return withNotes(
|
|
56
|
+
return withNotes(
|
|
57
|
+
state.mutated ? REVIEW_STEER : WORKFLOW_REVIEW_STEER,
|
|
58
|
+
state.notes,
|
|
59
|
+
);
|
|
53
60
|
}
|
|
54
61
|
if (
|
|
55
62
|
state.steeredThisTurn &&
|
|
56
63
|
!state.notesSteeredThisTurn &&
|
|
57
64
|
state.notes.length > 0
|
|
58
65
|
) {
|
|
59
|
-
return withNotes(
|
|
66
|
+
return withNotes(
|
|
67
|
+
state.mutated ? REVIEW_NOTES_STEER : WORKFLOW_REVIEW_STEER,
|
|
68
|
+
state.notes,
|
|
69
|
+
);
|
|
60
70
|
}
|
|
61
71
|
return undefined;
|
|
62
72
|
}
|
|
@@ -104,12 +114,20 @@ export function registerReviewGuard(
|
|
|
104
114
|
// continuation stays inside the same run. agent_settled is notification-only:
|
|
105
115
|
// a steer sent there starts a second run after the user already saw the
|
|
106
116
|
// agent stop, and it fires even when the user pressed Escape.
|
|
107
|
-
pi
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
registerSettleGuard(pi, {
|
|
118
|
+
priority: SETTLE_PRIORITY.review,
|
|
119
|
+
pending: () => pendingReviewSteer(state) !== undefined,
|
|
120
|
+
handle: (event) => {
|
|
121
|
+
const steer = pendingReviewSteer(state);
|
|
122
|
+
if (steer === undefined) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
markReviewSteered(state);
|
|
126
|
+
return withHiddenSteer({
|
|
127
|
+
event,
|
|
128
|
+
customType: REVIEW_STEER_TYPE,
|
|
129
|
+
content: steer,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
114
132
|
});
|
|
115
133
|
}
|
package/extensions/jev/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
JEV_ENDPOINT,
|
|
4
4
|
type JevFileState,
|
|
5
5
|
type JevFinding,
|
|
6
|
+
type JevRequestBody,
|
|
6
7
|
parseEnvelope,
|
|
7
8
|
readFindings,
|
|
8
9
|
} from './model.ts';
|
|
@@ -105,7 +106,28 @@ export async function askJev(
|
|
|
105
106
|
file: JevFileState,
|
|
106
107
|
signal: AbortSignal | undefined,
|
|
107
108
|
): Promise<JevAttempt> {
|
|
108
|
-
|
|
109
|
+
return askJevRequest({
|
|
110
|
+
transport,
|
|
111
|
+
apiKey,
|
|
112
|
+
requestBody: buildRequestBody(file),
|
|
113
|
+
signal,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface JevRequestInput {
|
|
118
|
+
readonly transport: JevTransport;
|
|
119
|
+
readonly apiKey: string;
|
|
120
|
+
readonly requestBody: JevRequestBody;
|
|
121
|
+
readonly signal: AbortSignal | undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function askJevRequest({
|
|
125
|
+
transport,
|
|
126
|
+
apiKey,
|
|
127
|
+
requestBody,
|
|
128
|
+
signal,
|
|
129
|
+
}: JevRequestInput): Promise<JevAttempt> {
|
|
130
|
+
const body = JSON.stringify(requestBody);
|
|
109
131
|
let response: JevHttpResponse;
|
|
110
132
|
try {
|
|
111
133
|
response = await transport({ apiKey, body, signal });
|
|
@@ -130,7 +152,10 @@ export async function askJev(
|
|
|
130
152
|
return { findings: [], model: undefined, failure: undefined };
|
|
131
153
|
}
|
|
132
154
|
return {
|
|
133
|
-
findings: readFindings(
|
|
155
|
+
findings: readFindings(
|
|
156
|
+
envelope,
|
|
157
|
+
new Set(Object.keys(requestBody.questions)),
|
|
158
|
+
),
|
|
134
159
|
model: envelope.model,
|
|
135
160
|
failure: undefined,
|
|
136
161
|
};
|