@rmrdeveloper/sideroom-pi 8.1.1 → 8.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 +33 -9
- package/extensions/ask/index.ts +12 -2
- package/extensions/ask/model.ts +116 -12
- package/extensions/ask/selection.ts +47 -0
- package/extensions/ask/ui.ts +141 -34
- package/package.json +27 -6
- package/skills/sideroom-grill/SKILL.md +5 -4
package/README.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
|
|
5
5
|
[](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
|
|
6
6
|
|
|
7
|
+
**Asks before it guesses. Shows the plan. Enforces the style. Refuses to
|
|
8
|
+
call it done in red.**
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pi install npm:@rmrdeveloper/sideroom-pi
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
7
16
|
> Coding agents fail in predictable ways: they guess instead of asking, they
|
|
8
17
|
> bury progress in chat scroll, they litter your repo with `TODO.md` files,
|
|
9
18
|
> every session writes code in a slightly different style, and they declare
|
|
@@ -54,9 +63,10 @@ wall of tool calls, it:
|
|
|
54
63
|
|
|
55
64
|
- **Asks like a senior would.** A clean TUI questionnaire appears — tabs for
|
|
56
65
|
several questions, a simple list for one. Every question carries a
|
|
57
|
-
recommendation,
|
|
58
|
-
|
|
59
|
-
batch. No guessing, no twenty
|
|
66
|
+
recommendation, and a plural one lets you tick several answers at once. You
|
|
67
|
+
can always answer *Out of scope* or write your own, and everything is written
|
|
68
|
+
in your language. Escape cancels the whole batch. No guessing, no twenty
|
|
69
|
+
follow-up clarifications in chat.
|
|
60
70
|
- **Works in the open.** A compact board sits above the editor:
|
|
61
71
|
`pending → in_progress → completed`. Exactly one step is active at a
|
|
62
72
|
time; the agent completes the current step and starts the next one in the
|
|
@@ -86,9 +96,11 @@ wall of tool calls, it:
|
|
|
86
96
|
### `sideroom_ask` — decisions, not interrogations
|
|
87
97
|
|
|
88
98
|
One call is one 1–N question batch. Each question needs an `id`, a `prompt`,
|
|
89
|
-
at least two `{ value, label }` options, and a
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
at least two `{ value, label }` options, and a recommendation:
|
|
100
|
+
`recommendationIndex` for the default single answer, or
|
|
101
|
+
`selectionMode: "multiple"` with `recommendedIndices` when the question accepts
|
|
102
|
+
several. The UI adds the rest: the recommended mark, the always-on *Out of
|
|
103
|
+
scope* row, and a custom answer.
|
|
92
104
|
|
|
93
105
|
TUI-only by design. In print, JSON, or RPC modes it fails fast with
|
|
94
106
|
`UI not available` instead of hanging.
|
|
@@ -185,9 +197,10 @@ pi update --extension npm:@rmrdeveloper/sideroom-pi
|
|
|
185
197
|
### Questionnaire contract
|
|
186
198
|
|
|
187
199
|
Each batch accepts one to four questions, with two to four caller-provided
|
|
188
|
-
options per question.
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
options per question. A question takes one answer unless it declares
|
|
201
|
+
`selectionMode: "multiple"`, which requires `recommendedIndices` and at least
|
|
202
|
+
one selection. Tab labels accept at most 16 characters and option labels at
|
|
203
|
+
most 60. Inputs that exceed a limit are rejected rather than truncated.
|
|
191
204
|
|
|
192
205
|
Write prompts, tab labels, and option copy in the language the user is
|
|
193
206
|
speaking; keep ids, option values, and TUI chrome in English. Do not send
|
|
@@ -250,6 +263,17 @@ Single-session scope only. Files are created lazily: nothing exists until
|
|
|
250
263
|
the first term or decision crystallises. A session with a sharper glossary
|
|
251
264
|
and zero ADRs is working as designed.
|
|
252
265
|
|
|
266
|
+
### Coexisting with other grilling skills
|
|
267
|
+
|
|
268
|
+
Pi routes skills by description, not by name, so several grilling skills can
|
|
269
|
+
be installed at once. `sideroom-grill` claims non-trivial plans before
|
|
270
|
+
implementation and states precedence over other grilling, interview, and spec
|
|
271
|
+
skills. If a third-party skill still wins, remove it from routing by adding
|
|
272
|
+
`disable-model-invocation: true` to its frontmatter, or exclude its path with
|
|
273
|
+
`-path` in `settings.skills`. A literal name collision keeps the first skill
|
|
274
|
+
found: project `.pi/skills`, then project `.agents/skills`, then
|
|
275
|
+
`~/.pi/agent/skills`, then `~/.agents/skills`, then packages.
|
|
276
|
+
|
|
253
277
|
## Documentation
|
|
254
278
|
|
|
255
279
|
Maintainer reference lives in [`docs/`](docs/README.md): the
|
package/extensions/ask/index.ts
CHANGED
|
@@ -9,16 +9,18 @@ import {
|
|
|
9
9
|
} from './model.ts';
|
|
10
10
|
|
|
11
11
|
export const ASK_DESCRIPTION =
|
|
12
|
-
'Ask the user one or more questions in the language they are speaking. Use for clarifying requirements, getting preferences, or confirming decisions. Each question must include a recommended option. For a single question, shows a simple option list. For multiple questions, shows a tab-based interface.';
|
|
12
|
+
'Ask the user one or more questions in the language they are speaking. Use for clarifying requirements, getting preferences, or confirming decisions. Each question must include a recommended option, or several recommended options when it accepts multiple selections. For a single question, shows a simple option list. For multiple questions, shows a tab-based interface.';
|
|
13
13
|
|
|
14
14
|
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
18
|
'Use sideroom_ask to clarify requirements, preferences, or decisions with the user.',
|
|
19
|
+
'When a non-trivial request is not yet a settled spec, follow the sideroom-grill skill before implementing; it owns the interview and drives sideroom_ask rounds.',
|
|
19
20
|
'Each sideroom_ask question needs an id, a prompt, at least two options, and a recommendationIndex pointing at the recommended option.',
|
|
20
21
|
'Write every sideroom_ask prompt, tab label, and option label or description in the language the user is speaking. Keep ids, option values, and tool code in English.',
|
|
21
|
-
'sideroom_ask always adds Out of scope and a custom answer; do not include those options yourself.',
|
|
22
|
+
'sideroom_ask always adds Out of scope and a custom answer; do not include those options yourself, and in a multiple-selection question they replace the selections.',
|
|
23
|
+
'For a genuinely plural sideroom_ask question, set selectionMode "multiple" with recommendedIndices (at least one) instead of recommendationIndex; never send both fields, and keep the single default otherwise.',
|
|
22
24
|
'One sideroom_ask call is one batch; call it again if another round of questions is needed.',
|
|
23
25
|
'sideroom_ask only works in the interactive TUI; it returns an error in print, JSON, or RPC modes.',
|
|
24
26
|
];
|
|
@@ -73,6 +75,14 @@ export default function registerAsk(pi: ExtensionAPI): void {
|
|
|
73
75
|
if (answer.wasCustom) {
|
|
74
76
|
return `${mark}${id}: ${theme.fg('muted', '(wrote) ')}${answer.label}`;
|
|
75
77
|
}
|
|
78
|
+
if (answer.selections !== undefined && answer.selections.length > 0) {
|
|
79
|
+
const picked = answer.selections
|
|
80
|
+
.map(
|
|
81
|
+
(selection) => `${String(selection.index)}. ${selection.label}`,
|
|
82
|
+
)
|
|
83
|
+
.join(', ');
|
|
84
|
+
return `${mark}${id}: ${picked}`;
|
|
85
|
+
}
|
|
76
86
|
if (answer.index !== undefined) {
|
|
77
87
|
return `${mark}${id}: ${String(answer.index)}. ${answer.label}`;
|
|
78
88
|
}
|
package/extensions/ask/model.ts
CHANGED
|
@@ -9,6 +9,13 @@ export const CUSTOM_LABEL = 'Write a custom answer';
|
|
|
9
9
|
export const UI_UNAVAILABLE =
|
|
10
10
|
'Error: UI not available (running in non-interactive mode)';
|
|
11
11
|
|
|
12
|
+
export const SINGLE_SELECTION_MODE = 'single';
|
|
13
|
+
export const MULTIPLE_SELECTION_MODE = 'multiple';
|
|
14
|
+
|
|
15
|
+
export type SelectionMode =
|
|
16
|
+
| typeof SINGLE_SELECTION_MODE
|
|
17
|
+
| typeof MULTIPLE_SELECTION_MODE;
|
|
18
|
+
|
|
12
19
|
const MAX_QUESTIONS = 4;
|
|
13
20
|
const MAX_OPTIONS = 4;
|
|
14
21
|
const MAX_TAB_LABEL_LENGTH = 16;
|
|
@@ -39,10 +46,32 @@ const QuestionSchema = Type.Object({
|
|
|
39
46
|
maxItems: MAX_OPTIONS,
|
|
40
47
|
description: `Two to ${String(MAX_OPTIONS)} mutually exclusive options`,
|
|
41
48
|
}),
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
selectionMode: Type.Optional(
|
|
50
|
+
Type.Union(
|
|
51
|
+
[
|
|
52
|
+
Type.Literal(SINGLE_SELECTION_MODE),
|
|
53
|
+
Type.Literal(MULTIPLE_SELECTION_MODE),
|
|
54
|
+
],
|
|
55
|
+
{
|
|
56
|
+
description:
|
|
57
|
+
"'single' (default) answers with one option; 'multiple' answers with one or more",
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
recommendationIndex: Type.Optional(
|
|
62
|
+
Type.Integer({
|
|
63
|
+
minimum: 0,
|
|
64
|
+
description:
|
|
65
|
+
"Required for a 'single' question: index into options pointing at the recommended option",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
recommendedIndices: Type.Optional(
|
|
69
|
+
Type.Array(Type.Integer({ minimum: 0 }), {
|
|
70
|
+
minItems: 1,
|
|
71
|
+
description:
|
|
72
|
+
"Required for a 'multiple' question: indexes of every recommended option, at least one",
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
46
75
|
});
|
|
47
76
|
|
|
48
77
|
export const AskParamsSchema = Type.Object({
|
|
@@ -61,7 +90,14 @@ export interface AskQuestion {
|
|
|
61
90
|
readonly label: string;
|
|
62
91
|
readonly prompt: string;
|
|
63
92
|
readonly options: readonly QuestionOption[];
|
|
64
|
-
readonly
|
|
93
|
+
readonly selectionMode: SelectionMode;
|
|
94
|
+
readonly recommendedIndices: readonly number[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface AskSelection {
|
|
98
|
+
readonly value: string;
|
|
99
|
+
readonly label: string;
|
|
100
|
+
readonly index: number;
|
|
65
101
|
}
|
|
66
102
|
|
|
67
103
|
export interface AskAnswer {
|
|
@@ -71,6 +107,7 @@ export interface AskAnswer {
|
|
|
71
107
|
readonly wasCustom: boolean;
|
|
72
108
|
readonly outOfScope: boolean;
|
|
73
109
|
readonly index?: number;
|
|
110
|
+
readonly selections?: readonly AskSelection[];
|
|
74
111
|
}
|
|
75
112
|
|
|
76
113
|
export interface AskResult {
|
|
@@ -208,11 +245,9 @@ export function parseAskParams(value: unknown): ParseAskResult {
|
|
|
208
245
|
};
|
|
209
246
|
}
|
|
210
247
|
seen.add(question.id);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
message: `Error: recommendationIndex ${String(question.recommendationIndex)} is out of range for question '${question.id}'`,
|
|
215
|
-
};
|
|
248
|
+
const failure = recommendationError(question);
|
|
249
|
+
if (failure !== undefined) {
|
|
250
|
+
return { ok: false, message: failure };
|
|
216
251
|
}
|
|
217
252
|
}
|
|
218
253
|
|
|
@@ -224,6 +259,7 @@ export function normalizeQuestions(
|
|
|
224
259
|
): AskQuestion[] {
|
|
225
260
|
return questions.map((question, index) => {
|
|
226
261
|
const label = question.label;
|
|
262
|
+
const selectionMode = selectionModeOf(question);
|
|
227
263
|
return {
|
|
228
264
|
id: question.id,
|
|
229
265
|
label:
|
|
@@ -232,15 +268,67 @@ export function normalizeQuestions(
|
|
|
232
268
|
: `Q${String(index + 1)}`,
|
|
233
269
|
prompt: question.prompt,
|
|
234
270
|
options: question.options,
|
|
235
|
-
|
|
271
|
+
selectionMode,
|
|
272
|
+
recommendedIndices: recommendedIndicesFor(question, selectionMode),
|
|
236
273
|
};
|
|
237
274
|
});
|
|
238
275
|
}
|
|
239
276
|
|
|
277
|
+
function selectionModeOf(
|
|
278
|
+
question: AskParams['questions'][number],
|
|
279
|
+
): SelectionMode {
|
|
280
|
+
return question.selectionMode === MULTIPLE_SELECTION_MODE
|
|
281
|
+
? MULTIPLE_SELECTION_MODE
|
|
282
|
+
: SINGLE_SELECTION_MODE;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function recommendedIndicesFor(
|
|
286
|
+
question: AskParams['questions'][number],
|
|
287
|
+
selectionMode: SelectionMode,
|
|
288
|
+
): readonly number[] {
|
|
289
|
+
if (selectionMode === MULTIPLE_SELECTION_MODE) {
|
|
290
|
+
return question.recommendedIndices ?? [];
|
|
291
|
+
}
|
|
292
|
+
if (question.recommendationIndex === undefined) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
return [question.recommendationIndex];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function recommendationError(
|
|
299
|
+
question: AskParams['questions'][number],
|
|
300
|
+
): string | undefined {
|
|
301
|
+
if (selectionModeOf(question) === SINGLE_SELECTION_MODE) {
|
|
302
|
+
if (question.recommendationIndex === undefined) {
|
|
303
|
+
return `Error: Question '${question.id}' requires recommendationIndex`;
|
|
304
|
+
}
|
|
305
|
+
if (question.recommendedIndices !== undefined) {
|
|
306
|
+
return `Error: Question '${question.id}' is a single-selection question; remove recommendedIndices`;
|
|
307
|
+
}
|
|
308
|
+
if (question.recommendationIndex >= question.options.length) {
|
|
309
|
+
return `Error: recommendationIndex ${String(question.recommendationIndex)} is out of range for question '${question.id}'`;
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
if (question.recommendedIndices === undefined) {
|
|
314
|
+
return `Error: Question '${question.id}' is a multiple-selection question and requires recommendedIndices`;
|
|
315
|
+
}
|
|
316
|
+
if (question.recommendationIndex !== undefined) {
|
|
317
|
+
return `Error: Question '${question.id}' is a multiple-selection question; remove recommendationIndex`;
|
|
318
|
+
}
|
|
319
|
+
const outOfRange = question.recommendedIndices.find(
|
|
320
|
+
(optionIndex) => optionIndex >= question.options.length,
|
|
321
|
+
);
|
|
322
|
+
if (outOfRange !== undefined) {
|
|
323
|
+
return `Error: recommendedIndices ${String(outOfRange)} is out of range for question '${question.id}'`;
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
240
328
|
export function renderOptions(question: AskQuestion): RenderOption[] {
|
|
241
329
|
const options: RenderOption[] = question.options.map((option, index) => ({
|
|
242
330
|
...option,
|
|
243
|
-
isRecommended:
|
|
331
|
+
isRecommended: question.recommendedIndices.includes(index),
|
|
244
332
|
}));
|
|
245
333
|
options.push({
|
|
246
334
|
value: OUT_OF_SCOPE_VALUE,
|
|
@@ -268,6 +356,13 @@ export function formatAnswerLines(
|
|
|
268
356
|
if (answer.wasCustom) {
|
|
269
357
|
return `${questionLabel}: user wrote: ${answer.label}`;
|
|
270
358
|
}
|
|
359
|
+
const selections = answer.selections;
|
|
360
|
+
if (selections !== undefined && selections.length > 0) {
|
|
361
|
+
const picked = selections
|
|
362
|
+
.map((selection) => `${String(selection.index)}. ${selection.label}`)
|
|
363
|
+
.join(', ');
|
|
364
|
+
return `${questionLabel}: user selected: ${picked}`;
|
|
365
|
+
}
|
|
271
366
|
if (answer.index !== undefined) {
|
|
272
367
|
return `${questionLabel}: user selected: ${String(answer.index)}. ${answer.label}`;
|
|
273
368
|
}
|
|
@@ -302,6 +397,15 @@ function schemaErrorMessage(value: unknown): string {
|
|
|
302
397
|
}
|
|
303
398
|
}
|
|
304
399
|
|
|
400
|
+
const recommendedMatch = /\/questions\/(\d+)\/recommendedIndices$/.exec(
|
|
401
|
+
error.instancePath,
|
|
402
|
+
);
|
|
403
|
+
if (recommendedMatch !== null && error.keyword === 'minItems') {
|
|
404
|
+
const questionIndex = Number(recommendedMatch[1]);
|
|
405
|
+
const id = questionId(questions, questionIndex);
|
|
406
|
+
return `Error: Question '${id}' must mark at least one recommended option`;
|
|
407
|
+
}
|
|
408
|
+
|
|
305
409
|
const tabLabelMatch = /\/questions\/(\d+)\/label$/.exec(error.instancePath);
|
|
306
410
|
if (error.keyword === 'maxLength' && tabLabelMatch !== null) {
|
|
307
411
|
const questionIndex = Number(tabLabelMatch[1]);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AskAnswer, AskQuestion, AskSelection } from './model.ts';
|
|
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
|
+
export function toggleSelection(
|
|
8
|
+
selected: readonly number[],
|
|
9
|
+
optionIndex: number,
|
|
10
|
+
): number[] {
|
|
11
|
+
if (selected.includes(optionIndex)) {
|
|
12
|
+
return selected.filter((index) => index !== optionIndex);
|
|
13
|
+
}
|
|
14
|
+
return [...selected, optionIndex].sort((left, right) => left - right);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function answerFromSelections(
|
|
18
|
+
question: AskQuestion,
|
|
19
|
+
selected: readonly number[],
|
|
20
|
+
): AskAnswer {
|
|
21
|
+
const selections = selected.map((optionIndex) =>
|
|
22
|
+
toSelection(question, optionIndex),
|
|
23
|
+
);
|
|
24
|
+
const [first] = selections;
|
|
25
|
+
if (first === undefined) {
|
|
26
|
+
throw new Error(`Question '${question.id}' needs at least one selection`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
id: question.id,
|
|
30
|
+
value: first.value,
|
|
31
|
+
label: first.label,
|
|
32
|
+
index: first.index,
|
|
33
|
+
wasCustom: false,
|
|
34
|
+
outOfScope: false,
|
|
35
|
+
selections,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toSelection(question: AskQuestion, optionIndex: number): AskSelection {
|
|
40
|
+
const option = question.options[optionIndex];
|
|
41
|
+
if (option === undefined) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unknown option index ${String(optionIndex)} for question '${question.id}'`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return { value: option.value, label: option.label, index: optionIndex + 1 };
|
|
47
|
+
}
|
package/extensions/ask/ui.ts
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
type AskAnswer,
|
|
12
12
|
type AskQuestion,
|
|
13
13
|
type AskResult,
|
|
14
|
+
MULTIPLE_SELECTION_MODE,
|
|
14
15
|
type RenderOption,
|
|
15
16
|
renderOptions,
|
|
16
17
|
} from './model.ts';
|
|
18
|
+
import { answerFromSelections, toggleSelection } from './selection.ts';
|
|
17
19
|
|
|
18
20
|
export async function runAskUi(
|
|
19
21
|
ui: Pick<ExtensionUIContext, 'custom'>,
|
|
@@ -25,8 +27,7 @@ export async function runAskUi(
|
|
|
25
27
|
return ui.custom<AskResult>((tui, theme, _keybindings, done) => {
|
|
26
28
|
let currentTab = 0;
|
|
27
29
|
const firstQuestion = questions[0];
|
|
28
|
-
let optionIndex =
|
|
29
|
-
firstQuestion === undefined ? 0 : firstQuestion.recommendationIndex;
|
|
30
|
+
let optionIndex = firstQuestion?.recommendedIndices[0] ?? 0;
|
|
30
31
|
let inputMode = false;
|
|
31
32
|
let inputQuestionId: string | null = null;
|
|
32
33
|
let cachedLines: string[] | undefined;
|
|
@@ -82,11 +83,10 @@ export async function runAskUi(
|
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
const saved = answers.get(question.id);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
86
|
+
optionIndex =
|
|
87
|
+
saved?.index !== undefined
|
|
88
|
+
? saved.index - 1
|
|
89
|
+
: (question.recommendedIndices[0] ?? 0);
|
|
90
90
|
refresh();
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -106,6 +106,45 @@ export async function runAskUi(
|
|
|
106
106
|
answers.set(answer.id, answer);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function selectedIndices(question: AskQuestion): readonly number[] {
|
|
110
|
+
const saved = answers.get(question.id);
|
|
111
|
+
if (saved?.selections === undefined) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return saved.selections.map((selection) => selection.index - 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function answerSummary(answer: AskAnswer): string {
|
|
118
|
+
if (answer.selections === undefined || answer.selections.length === 0) {
|
|
119
|
+
return answer.label;
|
|
120
|
+
}
|
|
121
|
+
return answer.selections.map((selection) => selection.label).join(', ');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isReservedOption(opt: RenderOption): boolean {
|
|
125
|
+
return opt.isOther === true || opt.isOutOfScope === true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function activateReservedOption(
|
|
129
|
+
question: AskQuestion,
|
|
130
|
+
opt: RenderOption,
|
|
131
|
+
index: number,
|
|
132
|
+
): void {
|
|
133
|
+
if (opt.isOther === true) {
|
|
134
|
+
openCustomEditor(question);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
saveAnswer({
|
|
138
|
+
id: question.id,
|
|
139
|
+
value: opt.value,
|
|
140
|
+
label: opt.label,
|
|
141
|
+
wasCustom: false,
|
|
142
|
+
outOfScope: true,
|
|
143
|
+
index: index + 1,
|
|
144
|
+
});
|
|
145
|
+
advanceAfterAnswer();
|
|
146
|
+
}
|
|
147
|
+
|
|
109
148
|
function openCustomEditor(question: AskQuestion): void {
|
|
110
149
|
inputMode = true;
|
|
111
150
|
inputQuestionId = question.id;
|
|
@@ -127,20 +166,8 @@ export async function runAskUi(
|
|
|
127
166
|
if (opt === undefined) {
|
|
128
167
|
return;
|
|
129
168
|
}
|
|
130
|
-
if (opt
|
|
131
|
-
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (opt.isOutOfScope === true) {
|
|
135
|
-
saveAnswer({
|
|
136
|
-
id: question.id,
|
|
137
|
-
value: opt.value,
|
|
138
|
-
label: opt.label,
|
|
139
|
-
wasCustom: false,
|
|
140
|
-
outOfScope: true,
|
|
141
|
-
index: index + 1,
|
|
142
|
-
});
|
|
143
|
-
advanceAfterAnswer();
|
|
169
|
+
if (isReservedOption(opt)) {
|
|
170
|
+
activateReservedOption(question, opt, index);
|
|
144
171
|
return;
|
|
145
172
|
}
|
|
146
173
|
const original = question.options[index];
|
|
@@ -155,6 +182,49 @@ export async function runAskUi(
|
|
|
155
182
|
advanceAfterAnswer();
|
|
156
183
|
}
|
|
157
184
|
|
|
185
|
+
function toggleMultipleOption(
|
|
186
|
+
question: AskQuestion,
|
|
187
|
+
opts: readonly RenderOption[],
|
|
188
|
+
index: number,
|
|
189
|
+
): void {
|
|
190
|
+
const opt = opts[index];
|
|
191
|
+
if (opt === undefined) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (isReservedOption(opt)) {
|
|
195
|
+
activateReservedOption(question, opt, index);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const next = toggleSelection(selectedIndices(question), index);
|
|
199
|
+
if (next.length === 0) {
|
|
200
|
+
answers.delete(question.id);
|
|
201
|
+
refresh();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
answers.set(question.id, answerFromSelections(question, next));
|
|
205
|
+
refresh();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function confirmMultipleSelection(
|
|
209
|
+
question: AskQuestion,
|
|
210
|
+
opts: readonly RenderOption[],
|
|
211
|
+
index: number,
|
|
212
|
+
): void {
|
|
213
|
+
const opt = opts[index];
|
|
214
|
+
if (opt === undefined) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (isReservedOption(opt)) {
|
|
218
|
+
activateReservedOption(question, opt, index);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (selectedIndices(question).length === 0) {
|
|
222
|
+
refresh();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
advanceAfterAnswer();
|
|
226
|
+
}
|
|
227
|
+
|
|
158
228
|
editor.onSubmit = (value) => {
|
|
159
229
|
if (inputQuestionId === null) {
|
|
160
230
|
return;
|
|
@@ -232,15 +302,31 @@ export async function runAskUi(
|
|
|
232
302
|
return;
|
|
233
303
|
}
|
|
234
304
|
|
|
305
|
+
if (
|
|
306
|
+
question?.selectionMode === MULTIPLE_SELECTION_MODE &&
|
|
307
|
+
matchesKey(data, Key.space)
|
|
308
|
+
) {
|
|
309
|
+
toggleMultipleOption(question, opts, optionIndex);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
235
313
|
const digit = optionIndexFromDigit(data, opts.length);
|
|
236
314
|
if (digit !== undefined && question !== undefined) {
|
|
237
315
|
optionIndex = digit;
|
|
238
|
-
|
|
316
|
+
if (question.selectionMode === MULTIPLE_SELECTION_MODE) {
|
|
317
|
+
toggleMultipleOption(question, opts, digit);
|
|
318
|
+
} else {
|
|
319
|
+
confirmOption(question, opts, digit);
|
|
320
|
+
}
|
|
239
321
|
return;
|
|
240
322
|
}
|
|
241
323
|
|
|
242
324
|
if (matchesKey(data, Key.enter) && question !== undefined) {
|
|
243
|
-
|
|
325
|
+
if (question.selectionMode === MULTIPLE_SELECTION_MODE) {
|
|
326
|
+
confirmMultipleSelection(question, opts, optionIndex);
|
|
327
|
+
} else {
|
|
328
|
+
confirmOption(question, opts, optionIndex);
|
|
329
|
+
}
|
|
244
330
|
return;
|
|
245
331
|
}
|
|
246
332
|
|
|
@@ -310,7 +396,10 @@ export async function runAskUi(
|
|
|
310
396
|
lines.push('');
|
|
311
397
|
}
|
|
312
398
|
|
|
313
|
-
function renderOptionList(): void {
|
|
399
|
+
function renderOptionList(questionForList: AskQuestion): void {
|
|
400
|
+
const isMultipleAnswer =
|
|
401
|
+
questionForList.selectionMode === MULTIPLE_SELECTION_MODE;
|
|
402
|
+
const selected = selectedIndices(questionForList);
|
|
314
403
|
for (let index = 0; index < opts.length; index += 1) {
|
|
315
404
|
const opt = opts[index];
|
|
316
405
|
if (opt === undefined) {
|
|
@@ -319,10 +408,16 @@ export async function runAskUi(
|
|
|
319
408
|
if (opt.isOutOfScope === true) {
|
|
320
409
|
lines.push('');
|
|
321
410
|
}
|
|
322
|
-
const
|
|
323
|
-
const prefix =
|
|
324
|
-
const color =
|
|
325
|
-
let line =
|
|
411
|
+
const focused = index === optionIndex;
|
|
412
|
+
const prefix = focused ? theme.fg('accent', '> ') : ' ';
|
|
413
|
+
const color = focused ? 'accent' : 'text';
|
|
414
|
+
let line = '';
|
|
415
|
+
if (isMultipleAnswer && !isReservedOption(opt)) {
|
|
416
|
+
line += selected.includes(index)
|
|
417
|
+
? theme.fg('success', '[x] ')
|
|
418
|
+
: theme.fg('dim', '[ ] ');
|
|
419
|
+
}
|
|
420
|
+
line += theme.fg(color, `${String(index + 1)}. ${opt.label}`);
|
|
326
421
|
if (opt.isRecommended === true) {
|
|
327
422
|
line = `${line} ${theme.fg('dim', 'recommended')}`;
|
|
328
423
|
}
|
|
@@ -367,7 +462,7 @@ export async function runAskUi(
|
|
|
367
462
|
}
|
|
368
463
|
addWrappedWithPrefix(
|
|
369
464
|
' ',
|
|
370
|
-
`${name}${theme.fg('dim', kind)}${theme.fg('text', answer
|
|
465
|
+
`${name}${theme.fg('dim', kind)}${theme.fg('text', answerSummary(answer))}`,
|
|
371
466
|
);
|
|
372
467
|
}
|
|
373
468
|
lines.push('');
|
|
@@ -395,19 +490,31 @@ export async function runAskUi(
|
|
|
395
490
|
if (saved !== undefined) {
|
|
396
491
|
addWrappedWithPrefix(
|
|
397
492
|
' ',
|
|
398
|
-
theme.fg('muted', `Current: ${saved
|
|
493
|
+
theme.fg('muted', `Current: ${answerSummary(saved)}`),
|
|
399
494
|
);
|
|
400
495
|
}
|
|
401
496
|
lines.push('');
|
|
402
|
-
renderOptionList();
|
|
497
|
+
renderOptionList(question);
|
|
403
498
|
}
|
|
404
499
|
|
|
405
500
|
if (!inputMode && currentTab !== questions.length) {
|
|
406
501
|
lines.push('');
|
|
502
|
+
if (
|
|
503
|
+
question?.selectionMode === MULTIPLE_SELECTION_MODE &&
|
|
504
|
+
selectedIndices(question).length === 0
|
|
505
|
+
) {
|
|
506
|
+
addWrappedWithPrefix(
|
|
507
|
+
' ',
|
|
508
|
+
theme.fg('warning', 'Select at least one option'),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
407
511
|
const count = String(opts.length);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
512
|
+
let help = `↑↓ or 1–${count} · Enter select · Esc cancel`;
|
|
513
|
+
if (question?.selectionMode === MULTIPLE_SELECTION_MODE) {
|
|
514
|
+
help = `↑↓ or 1–${count} toggle · Space toggle · Enter confirm · Esc cancel`;
|
|
515
|
+
} else if (isMulti) {
|
|
516
|
+
help = `↑↓ or 1–${count} · Enter · Tab next · Esc cancel`;
|
|
517
|
+
}
|
|
411
518
|
addWrappedWithPrefix(' ', theme.fg('dim', help));
|
|
412
519
|
}
|
|
413
520
|
lines.push(theme.fg('accent', '─'.repeat(renderWidth)));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "8.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "8.2.0",
|
|
4
|
+
"description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"extensions",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"lint:fix": "biome lint --write extensions scripts commitlint.config.mjs",
|
|
22
22
|
"format": "biome format --write extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|
|
23
23
|
"format:check": "biome format extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|
|
24
|
+
"preview": "node scripts/render-preview.mjs",
|
|
24
25
|
"check:if-blocks": "node scripts/check-if-blocks.mjs",
|
|
25
26
|
"check": "npm run types && npm run check:if-blocks && biome check extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
|
|
26
27
|
},
|
|
@@ -31,13 +32,31 @@
|
|
|
31
32
|
"pi",
|
|
32
33
|
"pi-package",
|
|
33
34
|
"pi-extension",
|
|
35
|
+
"pi-skill",
|
|
36
|
+
"coding-agent",
|
|
37
|
+
"questions",
|
|
38
|
+
"clarifying-questions",
|
|
34
39
|
"questionnaire",
|
|
40
|
+
"planning",
|
|
41
|
+
"spec",
|
|
35
42
|
"todo",
|
|
43
|
+
"task-board",
|
|
44
|
+
"progress",
|
|
36
45
|
"edited-files",
|
|
37
|
-
"sideroom_ask",
|
|
38
|
-
"sideroom_todo",
|
|
39
46
|
"guidelines",
|
|
40
|
-
"
|
|
47
|
+
"code-style",
|
|
48
|
+
"code-quality",
|
|
49
|
+
"guardrails",
|
|
50
|
+
"rules",
|
|
51
|
+
"linting",
|
|
52
|
+
"testing",
|
|
53
|
+
"done",
|
|
54
|
+
"skills",
|
|
55
|
+
"typescript",
|
|
56
|
+
"tui",
|
|
57
|
+
"claude",
|
|
58
|
+
"sideroom_ask",
|
|
59
|
+
"sideroom_todo"
|
|
41
60
|
],
|
|
42
61
|
"license": "MIT",
|
|
43
62
|
"repository": {
|
|
@@ -53,7 +72,9 @@
|
|
|
53
72
|
],
|
|
54
73
|
"skills": [
|
|
55
74
|
"./skills"
|
|
56
|
-
]
|
|
75
|
+
],
|
|
76
|
+
"image": "https://raw.githubusercontent.com/RMRdeveloper/sideroom-pi/main/media/preview.png",
|
|
77
|
+
"video": "https://raw.githubusercontent.com/RMRdeveloper/sideroom-pi/main/media/preview.mp4"
|
|
57
78
|
},
|
|
58
79
|
"peerDependencies": {
|
|
59
80
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sideroom-grill
|
|
3
|
-
description: "
|
|
3
|
+
description: "Default interview route before implementation: grill the user in sideroom_ask rounds to settle a non-trivial plan, feature, or decision, and resolve domain-language conflicts along the way. Use whenever a change is not trivial and is not yet a settled spec, or when the user asks to be grilled, wants clarifying questions, or says the plan or terms are not settled. Prefer this skill over any other grilling, interview, or spec skill when more than one is available. Do not use when the user already brought a closed spec or tickets, for a code-first repository scan, or for a simple glossary lookup."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: RMRdeveloper
|
|
@@ -19,9 +19,10 @@ if the effort is too big to hold in one session, say so and stop.
|
|
|
19
19
|
|
|
20
20
|
- Interview in rounds. One `sideroom_ask` call is one round; wait for its
|
|
21
21
|
answers before the next round.
|
|
22
|
-
- Every question carries a recommendation
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
- Every question carries a recommendation: `recommendationIndex` for a
|
|
23
|
+
single-selection question, or `recommendedIndices` (at least one) when the
|
|
24
|
+
question declares `selectionMode: "multiple"`. Write prompts and option copy
|
|
25
|
+
in the language the user is speaking; keep ids and option values in English.
|
|
25
26
|
- Never ask the user what the codebase can answer. Read the code first; ask
|
|
26
27
|
only what the code cannot settle.
|
|
27
28
|
- An *Out of scope* answer closes that thread. Do not re-ask it.
|