@hheei/pi-ask-user 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +68 -2
  3. package/dist/ask-prompt.d.ts +60 -0
  4. package/dist/ask-prompt.js +360 -0
  5. package/dist/batch-prompt.d.ts +42 -0
  6. package/dist/batch-prompt.js +192 -0
  7. package/dist/constants.d.ts +15 -0
  8. package/dist/constants.js +17 -0
  9. package/dist/custom-prompt.d.ts +20 -0
  10. package/dist/custom-prompt.js +56 -0
  11. package/dist/dialogs.d.ts +25 -0
  12. package/dist/dialogs.js +93 -0
  13. package/dist/events.d.ts +23 -0
  14. package/dist/events.js +22 -0
  15. package/dist/execute.d.ts +5 -0
  16. package/dist/execute.js +157 -0
  17. package/dist/format.d.ts +18 -0
  18. package/dist/format.js +84 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +6 -0
  21. package/dist/multi-select-list.d.ts +25 -0
  22. package/dist/multi-select-list.js +155 -0
  23. package/dist/option-rows.d.ts +33 -0
  24. package/dist/option-rows.js +89 -0
  25. package/dist/preferences.d.ts +8 -0
  26. package/dist/preferences.js +25 -0
  27. package/dist/presentation.d.ts +6 -0
  28. package/dist/presentation.js +129 -0
  29. package/dist/prompt-context.d.ts +26 -0
  30. package/dist/prompt-context.js +68 -0
  31. package/dist/prompt-help.d.ts +23 -0
  32. package/dist/prompt-help.js +50 -0
  33. package/dist/prompt-layout.d.ts +43 -0
  34. package/dist/prompt-layout.js +89 -0
  35. package/dist/prompt-overflow.d.ts +5 -0
  36. package/dist/prompt-overflow.js +59 -0
  37. package/dist/response.d.ts +46 -0
  38. package/dist/response.js +31 -0
  39. package/dist/schema.d.ts +43 -0
  40. package/dist/schema.js +87 -0
  41. package/dist/settings-provider.d.ts +15 -0
  42. package/dist/settings-provider.js +42 -0
  43. package/dist/shortcuts.d.ts +7 -0
  44. package/dist/shortcuts.js +21 -0
  45. package/dist/single-select-layout.d.ts +19 -0
  46. package/dist/single-select-layout.js +61 -0
  47. package/dist/single-select-list.d.ts +23 -0
  48. package/dist/single-select-list.js +99 -0
  49. package/dist/tool.d.ts +2 -0
  50. package/dist/tool.js +91 -0
  51. package/package.json +56 -3
  52. package/skills/ask-user/SKILL.md +189 -0
  53. package/skills/ask-user/references/ask-user-skill-extension-spec.md +106 -0
@@ -0,0 +1,23 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { Component, KeybindingsManager } from "@earendil-works/pi-tui";
3
+ import { type QuestionOption } from "./single-select-layout";
4
+ export declare class WrappedSingleSelectList implements Component {
5
+ private options;
6
+ private theme;
7
+ private keybindings;
8
+ private readonly rows;
9
+ private cachedWidth;
10
+ private cachedLines;
11
+ onCancel?: () => void;
12
+ onSubmit?: (result: string) => void;
13
+ onEnterFreeform?: () => void;
14
+ constructor(options: QuestionOption[], allowFreeform: boolean, theme: Theme, keybindings: KeybindingsManager);
15
+ setMaxVisibleRows(count: number): void;
16
+ invalidate(): void;
17
+ private styleListLine;
18
+ private buildLines;
19
+ /** Enter on the selected row: open the custom response, or choose the option. */
20
+ private confirm;
21
+ handleInput(data: string): void;
22
+ render(width: number): string[];
23
+ }
@@ -0,0 +1,99 @@
1
+ import { fitRow } from "@hheei/pi-ext-core";
2
+ import { OptionRows } from "./option-rows";
3
+ import { digitKey, matchesSelectDown, matchesSelectUp } from "./shortcuts";
4
+ import { renderSingleSelectRows, } from "./single-select-layout";
5
+ export class WrappedSingleSelectList {
6
+ options;
7
+ theme;
8
+ keybindings;
9
+ rows;
10
+ cachedWidth;
11
+ cachedLines;
12
+ onCancel;
13
+ onSubmit;
14
+ onEnterFreeform;
15
+ constructor(options, allowFreeform, theme, keybindings) {
16
+ this.options = options;
17
+ this.theme = theme;
18
+ this.keybindings = keybindings;
19
+ this.rows = new OptionRows(allowFreeform, 12);
20
+ }
21
+ setMaxVisibleRows(count) {
22
+ if (this.rows.setMaxVisibleRows(count))
23
+ this.invalidate();
24
+ }
25
+ invalidate() {
26
+ this.cachedWidth = undefined;
27
+ this.cachedLines = undefined;
28
+ }
29
+ styleListLine(row, width) {
30
+ const text = row.line;
31
+ if (row.kind === "indicator")
32
+ return fitRow(this.theme.fg("dim", text), width, "");
33
+ if (row.selected)
34
+ return fitRow(this.theme.fg("accent", this.theme.bold(text)), width, "");
35
+ if (row.kind === "description")
36
+ return fitRow(this.theme.fg("muted", text), width, "");
37
+ return fitRow(this.theme.fg("text", text), width, "");
38
+ }
39
+ buildLines(width) {
40
+ const count = this.rows.count(this.options.length);
41
+ if (count === 0)
42
+ return [fitRow(this.theme.fg("warning", "No options"), width, "")];
43
+ const optionRows = renderSingleSelectRows({
44
+ options: this.options,
45
+ selectedIndex: this.rows.selected,
46
+ width,
47
+ allowFreeform: this.rows.allowFreeform,
48
+ maxRows: this.rows.visibleRows,
49
+ });
50
+ return optionRows.map((row) => this.styleListLine(row, width));
51
+ }
52
+ /** Enter on the selected row: open the custom response, or choose the option. */
53
+ confirm() {
54
+ const index = this.rows.selected;
55
+ if (this.rows.isFreeformRow(index, this.options.length)) {
56
+ this.onEnterFreeform?.();
57
+ return;
58
+ }
59
+ const result = this.options[index]?.title;
60
+ if (result)
61
+ this.onSubmit?.(result);
62
+ else
63
+ this.onCancel?.();
64
+ }
65
+ handleInput(data) {
66
+ if (this.keybindings.matches(data, "tui.select.cancel")) {
67
+ this.onCancel?.();
68
+ return;
69
+ }
70
+ const count = this.rows.count(this.options.length);
71
+ if (matchesSelectUp(data, this.keybindings) && this.rows.moveSelection(-1, count)) {
72
+ this.invalidate();
73
+ return;
74
+ }
75
+ if (matchesSelectDown(data, this.keybindings) && this.rows.moveSelection(1, count)) {
76
+ this.invalidate();
77
+ return;
78
+ }
79
+ const digit = digitKey(data);
80
+ if (digit !== null && digit <= this.options.length) {
81
+ this.rows.selected = digit - 1;
82
+ this.invalidate();
83
+ return;
84
+ }
85
+ if (this.keybindings.matches(data, "tui.select.confirm") && count > 0) {
86
+ this.confirm();
87
+ }
88
+ }
89
+ render(width) {
90
+ if (this.cachedLines && this.cachedWidth === width)
91
+ return this.cachedLines;
92
+ const count = this.rows.count(this.options.length);
93
+ this.rows.clampSelection(count);
94
+ const lines = this.buildLines(width);
95
+ this.cachedWidth = width;
96
+ this.cachedLines = lines;
97
+ return lines;
98
+ }
99
+ }
package/dist/tool.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function registerAskUserTool(pi: ExtensionAPI): void;
package/dist/tool.js ADDED
@@ -0,0 +1,91 @@
1
+ import { Type } from "typebox";
2
+ import { BATCH_MAX_QUESTIONS, BATCH_MIN_QUESTIONS } from "./constants";
3
+ import { executeBatch, executeSingle } from "./execute";
4
+ import { createAskUserRenderers } from "./presentation";
5
+ import { StringEnum } from "./schema";
6
+ const ASK_DESCRIPTION = "Ask the user a question with optional multiple-choice answers, or 2-4 independent questions in one prompt. The turn blocks until the user answers.";
7
+ const ASK_PROMPT_SNIPPET = "Ask the user one focused question, or 2-4 independent ones together, with optional multiple-choice answers";
8
+ const ASK_PROMPT_GUIDELINES = [
9
+ "ask_user: gather context with tools (read/web/ref) first and put a short summary of findings in context; the user sees only what you send.",
10
+ "ask_user: use it when intent is ambiguous, when a decision needs explicit user input, or when several valid options exist.",
11
+ "ask_user: ask one focused question per call; never bundle numbered, multipart, or unrelated questions into one question.",
12
+ "ask_user: use questions (2-4 entries) only for independent decisions whose prerequisites are settled; anything that depends on an earlier answer belongs in a later call.",
13
+ "ask_user: with questions, put every question inside questions; a top-level question is only the batch heading and is never asked.",
14
+ "ask_user: keep option titles short and put the trade-off in description; allowFreeform is on by default, so do not invent an option for every possible answer.",
15
+ ];
16
+ const ASK_PARAMETER_DESCRIPTIONS = {
17
+ question: "The question to ask. With questions, it is only an optional heading for the batch; put every actual question inside questions.",
18
+ questions: "2-4 independent questions shown together, used instead of question. Set context, options, allowMultiple, and allowFreeform on each entry; the remaining parameters apply to the whole batch.",
19
+ batchQuestion: "One question in the batch",
20
+ context: "Relevant context shown before the question (summary of findings)",
21
+ options: "Options for the user to choose from",
22
+ allowMultiple: "Allow selecting multiple options (default: false)",
23
+ allowFreeform: "Add a freeform text option (default: true)",
24
+ displayMode: "'overlay' shows a centered modal, 'inline' renders in place (default: displayMode in the askUser section of ext_settings.json, otherwise 'overlay')",
25
+ timeoutMs: "Auto-dismiss after N milliseconds; the result is cancelled",
26
+ };
27
+ // Flat object shape: union item schemas get stripped or rejected by several
28
+ // providers/proxies (Google function calling, Codex-style backends, cmux),
29
+ // leaving the model to guess the shape and produce empty options. Plain
30
+ // strings are still accepted at runtime for older transcripts. See issue #22.
31
+ const optionSchema = Type.Object({
32
+ title: Type.String({ description: "Short title for this option" }),
33
+ description: Type.Optional(Type.String({ description: "Longer description explaining this option" })),
34
+ });
35
+ const questionEntrySchema = Type.Object({
36
+ question: Type.String({ description: ASK_PARAMETER_DESCRIPTIONS.batchQuestion }),
37
+ context: Type.Optional(Type.String({ description: ASK_PARAMETER_DESCRIPTIONS.context })),
38
+ options: Type.Optional(Type.Array(optionSchema, { description: ASK_PARAMETER_DESCRIPTIONS.options })),
39
+ allowMultiple: Type.Optional(Type.Boolean({ description: ASK_PARAMETER_DESCRIPTIONS.allowMultiple })),
40
+ allowFreeform: Type.Optional(Type.Boolean({ description: ASK_PARAMETER_DESCRIPTIONS.allowFreeform })),
41
+ });
42
+ export function registerAskUserTool(pi) {
43
+ pi.registerTool({
44
+ // Asks the user, so only the model may call it; codemode scripts cannot open prompts.
45
+ exposure: "model-only",
46
+ name: "ask_user",
47
+ label: "ask_user",
48
+ description: ASK_DESCRIPTION,
49
+ promptSnippet: ASK_PROMPT_SNIPPET,
50
+ promptGuidelines: ASK_PROMPT_GUIDELINES,
51
+ // Block other tool calls in the same assistant turn until the user answers,
52
+ // so the model cannot batch ask_user with bash/edit/write and let those run
53
+ // (potentially with side effects) before the user sees the prompt.
54
+ executionMode: "sequential",
55
+ parameters: Type.Object({
56
+ question: Type.Optional(Type.String({ description: ASK_PARAMETER_DESCRIPTIONS.question })),
57
+ questions: Type.Optional(Type.Array(questionEntrySchema, {
58
+ minItems: BATCH_MIN_QUESTIONS,
59
+ maxItems: BATCH_MAX_QUESTIONS,
60
+ description: ASK_PARAMETER_DESCRIPTIONS.questions,
61
+ })),
62
+ context: Type.Optional(Type.String({ description: ASK_PARAMETER_DESCRIPTIONS.context })),
63
+ options: Type.Optional(Type.Array(optionSchema, { description: ASK_PARAMETER_DESCRIPTIONS.options })),
64
+ allowMultiple: Type.Optional(Type.Boolean({ description: ASK_PARAMETER_DESCRIPTIONS.allowMultiple })),
65
+ allowFreeform: Type.Optional(Type.Boolean({ description: ASK_PARAMETER_DESCRIPTIONS.allowFreeform })),
66
+ displayMode: Type.Optional(StringEnum(["overlay", "inline"], {
67
+ description: ASK_PARAMETER_DESCRIPTIONS.displayMode,
68
+ })),
69
+ timeout_ms: Type.Optional(Type.Integer({
70
+ minimum: 1,
71
+ description: ASK_PARAMETER_DESCRIPTIONS.timeoutMs,
72
+ })),
73
+ }),
74
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
75
+ const ask = params;
76
+ const questions = ask.questions;
77
+ if (questions !== undefined && questions.length > 1) {
78
+ return executeBatch(pi, ask, signal, onUpdate, ctx);
79
+ }
80
+ // A one-entry batch is one question: models wrap a single question in questions often
81
+ // enough that rejecting it would cost the whole turn.
82
+ const only = questions?.[0];
83
+ if (only === undefined)
84
+ return executeSingle(pi, ask, signal, onUpdate, ctx);
85
+ const single = { ...ask, ...only };
86
+ delete single.questions;
87
+ return executeSingle(pi, single, signal, onUpdate, ctx);
88
+ },
89
+ ...createAskUserRenderers(pi),
90
+ });
91
+ }
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@hheei/pi-ask-user",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Interactive ask_user tool for pi-coding-agent: one question or 2-4 in one prompt, multiple choice, multi-select, and freeform answers.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "skills",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "pi": {
18
+ "extensions": [
19
+ "dist/index.js"
20
+ ],
21
+ "skills": [
22
+ "./skills"
23
+ ]
24
+ },
25
+ "keywords": [
26
+ "pi",
27
+ "pi-package",
28
+ "pi-extension",
29
+ "ask",
30
+ "ask_user",
31
+ "interactive"
32
+ ],
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/hheei/pi-mono.git",
37
+ "directory": "packages/pi-ask-user"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@hheei/pi-ext-core": "^0.8.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-ai": ">=1.2.0",
47
+ "@earendil-works/pi-coding-agent": ">=1.2.0",
48
+ "@earendil-works/pi-tui": ">=1.2.0",
49
+ "typebox": "*"
50
+ },
51
+ "devDependencies": {
52
+ "@earendil-works/pi-ai": "^1.4.0",
53
+ "@earendil-works/pi-coding-agent": "^1.4.0",
54
+ "@earendil-works/pi-tui": "^1.4.0"
55
+ },
56
+ "scripts": {
57
+ "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
58
+ }
6
59
  }
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: ask-user
3
+ description: "You MUST use this before high-stakes architectural decisions, irreversible changes, or when requirements are ambiguous. Runs a decision handshake with the ask_user tool: summarize context, present structured options, collect explicit user choice, then proceed."
4
+ metadata:
5
+ short-description: Decision gate for ambiguity and high-stakes choices
6
+ ---
7
+
8
+ # Ask User Decision Gate
9
+
10
+ Use this skill to force explicit user alignment before consequential decisions.
11
+
12
+ This skill is about **decision control**, not general chit-chat.
13
+
14
+ ## Non-negotiable rule
15
+
16
+ Invoke `ask_user` before proceeding when **any** of the following is true:
17
+
18
+ 1. The next step changes architecture, schema, API contracts, deployment strategy, or security posture.
19
+ 2. The work is costly to undo (large refactor, migration, destructive edit, production-facing behavior change).
20
+ 3. Requirements, constraints, or success criteria are unclear, conflicting, or missing.
21
+ 4. Multiple valid options exist and the trade-off is preference-dependent.
22
+ 5. You are about to assume something that can materially change implementation.
23
+
24
+ Do **not** skip this gate unless the user has already provided a clear, explicit decision for the exact trade-off.
25
+
26
+ ## Agent Protocol Handshake (required)
27
+
28
+ Follow this handshake in order.
29
+
30
+ ### 1) Detect boundary
31
+ Classify the current step as:
32
+ - `high_stakes`
33
+ - `ambiguous`
34
+ - `both`
35
+ - `clear` (no gate needed)
36
+
37
+ If classification is not `clear`, continue.
38
+
39
+ ### 2) Gather evidence first
40
+ Before asking, gather context from available tools (`read`, `grep`, `bash`, `web`, etc.).
41
+ Do not ask the user to decide blind.
42
+
43
+ ### 3) Synthesize context
44
+ Prepare a short neutral summary (3-7 bullets or short paragraph) covering:
45
+ - current state
46
+ - key constraints
47
+ - trade-offs
48
+ - recommendation (if any)
49
+
50
+ ### 4) Ask one focused question
51
+ Call `ask_user` with one decision at a time:
52
+ - `question`: concrete decision prompt
53
+ - `context`: synthesized summary
54
+ - `options`: 2-5 clear choices when possible
55
+ - `allowMultiple`: `false` unless independent selections are genuinely needed
56
+ - `allowFreeform`: usually `true`
57
+ - `displayMode` *(optional)*: `"overlay"` (default) or `"inline"`. Use `"inline"` when preceding assistant context (summary, trade-offs, recommendation) is essential to the decision and should remain visible — overlays cover the conversation underneath. The user may set a personal default via `displayMode` in the `askUser` section of `ext_settings.json`; only pass this when you intentionally want to override it for one call.
58
+ - `timeout_ms` *(optional)*: auto-dismiss after N milliseconds; the result comes back cancelled. Omit unless the decision is genuinely time-boxed.
59
+
60
+ When 2-4 decisions at the same boundary are independent of each other and their prerequisites are settled, you may ask them together with `questions` instead of `question`. Each entry carries its own `question`, `context`, `options`, `allowMultiple`, and `allowFreeform`. A top-level `question` sent with `questions` is only a heading, never asked. Never batch a decision whose options depend on another answer; ask it in a later call once that answer is known.
61
+
62
+ ### 5) Commit the decision
63
+ After response:
64
+ - restate the decision in plain language
65
+ - state what will be done next
66
+ - proceed with implementation
67
+
68
+ ### 6) Re-open only on new ambiguity
69
+ Ask again only if materially new uncertainty appears.
70
+ Avoid repetitive confirmation loops.
71
+
72
+ ## Anti-overasking guardrails (required)
73
+
74
+ Apply a strict question budget per decision boundary:
75
+
76
+ - **Max 1** `ask_user` call per decision boundary in normal cases.
77
+ - **Max 2** `ask_user` calls for the same boundary when first response is unclear/cancelled.
78
+ - Never ask the same trade-off again without new evidence.
79
+
80
+ Escalation ladder:
81
+
82
+ 1. **Attempt 1:** structured options + concise context.
83
+ 2. **Attempt 2 (only if needed):** narrower question with agent recommendation and explicit choices:
84
+ - `Proceed with recommended option`
85
+ - `Choose another option` (freeform)
86
+ - `Stop for now`
87
+
88
+ After attempt 2:
89
+
90
+ - If boundary is `high_stakes` or `both`: **stop and mark blocked**. Do not keep asking.
91
+ - If boundary is `ambiguous` only and user says “your call” or equivalent: proceed with the most reversible default and state assumptions explicitly.
92
+
93
+ ## `ask_user` payload quality standard
94
+
95
+ ### Question quality
96
+ Use:
97
+ - “Which option should we adopt for X?”
98
+ - “Do you want A (fast) or B (safer) for Y?”
99
+
100
+ Avoid:
101
+ - broad/open prompts with no decision boundary
102
+ - multiple unrelated decisions in one question
103
+ - questions that should be answered by reading code/docs first
104
+
105
+ ### Option quality
106
+ Options must be:
107
+ - mutually understandable
108
+ - short and outcome-oriented
109
+ - explicit on trade-offs
110
+
111
+ Good options include a short description when trade-offs are non-obvious.
112
+
113
+ ## Recommended patterns
114
+
115
+ ### Single-select architecture decision
116
+
117
+ ```json
118
+ {
119
+ "question": "Which caching strategy should we use for the first release?",
120
+ "context": "Current API has p95 latency issues. Redis is fastest but adds infra complexity; in-memory cache is simpler but not shared across instances.",
121
+ "options": [
122
+ { "title": "In-memory cache", "description": "Simpler rollout, weaker horizontal consistency" },
123
+ { "title": "Redis cache", "description": "Better consistency and scalability, more ops overhead" }
124
+ ],
125
+ "allowMultiple": false,
126
+ "allowFreeform": true
127
+ }
128
+ ```
129
+
130
+ ### Multi-select when decisions are independent
131
+
132
+ ```json
133
+ {
134
+ "question": "Select the first-wave hardening items to implement now.",
135
+ "context": "We can ship quickly with baseline controls, then add targeted hardening. Budget is limited to 1-2 days.",
136
+ "options": [
137
+ { "title": "Rate limiting" },
138
+ { "title": "Audit logging" },
139
+ { "title": "Input schema validation" },
140
+ { "title": "Secrets rotation" }
141
+ ],
142
+ "allowMultiple": true,
143
+ "allowFreeform": true
144
+ }
145
+ ```
146
+
147
+ ### Independent decisions at one checkpoint
148
+
149
+ ```json
150
+ {
151
+ "questions": [
152
+ {
153
+ "question": "Which logging backend should the service use?",
154
+ "context": "Both integrate with the existing middleware; only the hosted option needs a new vendor contract.",
155
+ "options": [
156
+ { "title": "Self-hosted Loki", "description": "No new vendor, more ops work" },
157
+ { "title": "Hosted Datadog", "description": "Fastest setup, recurring cost" }
158
+ ]
159
+ },
160
+ {
161
+ "question": "Should the first release include the admin dashboard?",
162
+ "options": [{ "title": "Include it" }, { "title": "Defer it" }],
163
+ "allowFreeform": false
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ ## Anti-patterns
170
+
171
+ - Asking `ask_user` without first gathering context
172
+ - Using it for trivial formatting choices
173
+ - Forcing options when freeform is clearly better
174
+ - Asking the same question repeatedly without new information
175
+ - Batching dependent decisions in `questions`, or using a batch to dodge the one-decision-per-question rule
176
+ - Proceeding with high-stakes implementation after unclear/cancelled answer
177
+
178
+ ## If user cancels or answer is unclear
179
+
180
+ Pause execution and explain what is blocked.
181
+ Use at most one narrower follow-up `ask_user` question (attempt 2).
182
+ After that, do not continue asking in a loop:
183
+ - for high-stakes decisions: remain blocked until explicit decision
184
+ - for ambiguity-only decisions: proceed only if user delegated the choice ("your call")
185
+
186
+ ## Additional reference
187
+
188
+ For full trigger matrix, UX conventions, and extension interaction details, read:
189
+ - `references/ask-user-skill-extension-spec.md`
@@ -0,0 +1,106 @@
1
+ # Ask User Skill × Extension Interaction Spec
2
+
3
+ ## Purpose
4
+
5
+ This document defines a minimal decision-gating protocol for using the `ask-user` skill with the `ask_user` tool.
6
+
7
+ Goal: require explicit user decisions at high-impact or ambiguous boundaries before implementation continues.
8
+
9
+ ---
10
+
11
+ ## 1) Trigger Matrix (When to Call `ask_user`)
12
+
13
+ | Scenario | Must Ask? | Why |
14
+ |---|---:|---|
15
+ | Architecture trade-off (e.g., queue vs cron, SQL vs KV) | Yes | Preference-sensitive, high blast radius |
16
+ | Data schema / migration path selection | Yes | Costly to reverse |
17
+ | Security/compliance posture trade-off | Yes | Risk ownership is human |
18
+ | Requirements conflict or ambiguity | Yes | Need explicit intent |
19
+ | Non-trivial scope cut/prioritization | Yes | Product decision, not purely technical |
20
+ | Purely local refactor with identical behavior | Usually no | No policy-level decision |
21
+ | Formatting-only edits | No | Trivial |
22
+ | User already gave explicit choice for exact trade-off | No (unless new ambiguity) | Decision already captured |
23
+
24
+ ---
25
+
26
+ ## 2) Decision Handshake
27
+
28
+ Use this protocol whenever the trigger matrix says to ask.
29
+
30
+ 1. **Detect boundary**
31
+ - classify as `high_stakes`, `ambiguous`, `both`, or `clear`
32
+ 2. **Gather evidence**
33
+ - read code/docs/logs first; do not ask blindly
34
+ 3. **Summarize context**
35
+ - prepare concise trade-off context (3–7 bullets or short paragraph)
36
+ 4. **Ask one focused question**
37
+ - call `ask_user` for one decision at a time; 2-4 independent decisions with settled prerequisites may go together in `questions`
38
+ 5. **Commit and proceed**
39
+ - restate chosen option and implement accordingly
40
+
41
+ ### Retry/cancel policy
42
+
43
+ - Max **2** `ask_user` attempts for the same decision boundary.
44
+ - Attempt 1: normal structured question.
45
+ - Attempt 2: narrower question with recommendation and explicit options.
46
+ - After attempt 2:
47
+ - `high_stakes` / `both`: stop and report blocked.
48
+ - `ambiguous` only: proceed only if user delegates (e.g., “your call”), using the most reversible default.
49
+
50
+ ---
51
+
52
+ ## 3) Example Payloads
53
+
54
+ ### Architecture decision
55
+
56
+ ```json
57
+ {
58
+ "question": "Which implementation path should we use for v1?",
59
+ "context": "Path A is faster to ship but less extensible. Path B takes longer but supports plugin-style growth. Existing deadline is 2 weeks.",
60
+ "options": [
61
+ { "title": "Path A (ship fast)", "description": "Lowest scope, revisit architecture later" },
62
+ { "title": "Path B (extensible)", "description": "Higher initial effort, cleaner long-term composition" }
63
+ ],
64
+ "allowMultiple": false,
65
+ "allowFreeform": true
66
+ }
67
+ ```
68
+
69
+ ### Display mode (optional)
70
+
71
+ The `ask_user` tool accepts an optional `displayMode` parameter:
72
+
73
+ - `"overlay"` *(default)*: centered modal; covers the conversation underneath.
74
+ - `"inline"`: rendered in the conversation flow; preceding messages stay visible.
75
+
76
+ Guidance:
77
+
78
+ - Omit `displayMode` to respect the user's configured preference (`displayMode` in the `askUser` section of `ext_settings.json`).
79
+ - Pass `"inline"` only when the immediately preceding assistant message (summary, trade-offs, recommendation) is the primary context for the decision and must remain visible.
80
+ - Pass `"overlay"` only to explicitly force the modal style (rare).
81
+
82
+ ### Batches (optional)
83
+
84
+ `questions` replaces `question` with 2-4 entries, each with its own `question`, `context`, `options`, `allowMultiple`, and `allowFreeform`. The user answers them on separate pages and submits from a review page, where unanswered questions can be submitted as skipped.
85
+
86
+ Guidance:
87
+
88
+ - Batch only decisions that are independent and whose prerequisites are settled.
89
+ - Treat each entry as its own decision boundary for the retry/cancel policy.
90
+ - Skipped entries come back as `{ "status": "skipped" }`; handle them like an unclear answer for that decision.
91
+
92
+ ### Requirement-priority decision
93
+
94
+ ```json
95
+ {
96
+ "question": "Which requirement should be prioritized first?",
97
+ "context": "Current request mixes performance tuning and UI redesign. Doing both now risks delaying delivery.",
98
+ "options": [
99
+ { "title": "Performance first" },
100
+ { "title": "UI redesign first" },
101
+ { "title": "Do a minimal pass on both" }
102
+ ],
103
+ "allowMultiple": false,
104
+ "allowFreeform": true
105
+ }
106
+ ```