@mjasnikovs/pi-task 0.29.3 → 0.31.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * PLAN-file I/O & transcript formatting for /task-plan.
3
+ *
4
+ * A TASK_PLAN_NNNN.md is a normal task file — same front matter, same section
5
+ * machinery (task-io.ts) — whose body holds the task prompt and the planning
6
+ * transcript. This mirrors auto-io.ts, which does exactly the same thing for
7
+ * /task-auto's TASK_AUTO_NNNN.md; nothing about the file format is new here.
8
+ *
9
+ * The transcript is the whole point of the command: it is what rides into /task
10
+ * as the handoff prompt, and it is what a human reads afterwards to see which
11
+ * decisions were made and who made them.
12
+ */
13
+ import * as fsp from 'node:fs/promises';
14
+ import { tasksDir, ensureTasksDir } from './task-io.js';
15
+ const PLAN_FILE_RE = /^(TASK_PLAN_\d{4,})\.md$/;
16
+ /** Next free TASK_PLAN_NNNN id. Mirrors allocateAutoId. */
17
+ export async function allocatePlanId(cwd) {
18
+ await ensureTasksDir(cwd);
19
+ const entries = await fsp.readdir(tasksDir(cwd));
20
+ let max = 0;
21
+ for (const e of entries) {
22
+ const m = PLAN_FILE_RE.exec(e);
23
+ if (m) {
24
+ const n = parseInt(m[1].slice('TASK_PLAN_'.length), 10);
25
+ if (n > max)
26
+ max = n;
27
+ }
28
+ }
29
+ return `TASK_PLAN_${String(max + 1).padStart(4, '0')}`;
30
+ }
31
+ const SOURCE_STAMP = {
32
+ chosen: '',
33
+ accepted: ' (accepted recommendation)',
34
+ typed: '',
35
+ skipped: '',
36
+ yolo: ' (YOLO)'
37
+ };
38
+ /**
39
+ * The transcript as the MODEL sees it — fed back as `priorQA` on every next
40
+ * question, and prepended to the handoff prompt. Decisions are numbered so a
41
+ * later question can refer to one; notes and stated decisions are labelled by who
42
+ * said them, because a model answer the user merely READ must not be mistaken for
43
+ * a decision the user MADE.
44
+ */
45
+ export function formatPlanTranscript(entries) {
46
+ const lines = [];
47
+ let n = 0;
48
+ for (const e of entries) {
49
+ if (e.kind === 'decision') {
50
+ n++;
51
+ lines.push(`Q${n}: ${e.question}`);
52
+ lines.push(`A${n}: ${e.answer}${SOURCE_STAMP[e.source]}`);
53
+ }
54
+ else if (e.kind === 'stated') {
55
+ lines.push(`DECIDED BY USER: ${e.text}`);
56
+ }
57
+ else {
58
+ lines.push(`THE USER ASKED: ${e.question}`);
59
+ lines.push(`YOU ANSWERED: ${e.answer}`);
60
+ }
61
+ }
62
+ return lines.join('\n');
63
+ }
64
+ /** Body of a fresh plan file, before any entry is recorded. */
65
+ export function buildPlanBody(task) {
66
+ return `\n## task prompt\n\n${task.trim() || '(none)'}\n\n## decisions\n\n(none yet)\n`;
67
+ }
68
+ /**
69
+ * The `## decisions` section: the human-readable transcript. Same content as
70
+ * {@link formatPlanTranscript} — the model and the reader see the same record, so
71
+ * there is no hidden channel.
72
+ */
73
+ export function formatPlanDecisions(entries) {
74
+ return entries.length === 0 ? '(none yet)' : formatPlanTranscript(entries);
75
+ }
76
+ /**
77
+ * The prompt handed to /task when the user proceeds to execution.
78
+ *
79
+ * The task prompt leads, exactly as a bare `/task <prompt>` would, so refine sees
80
+ * a normal task description first; the decisions follow as an authoritative block.
81
+ * Anything the user did NOT settle is simply absent — /task's own grill phase asks
82
+ * about what is left, which is why this block never invents a decision to fill a
83
+ * gap.
84
+ */
85
+ export function buildHandoffPrompt(task, entries) {
86
+ const decisions = entries.filter(e => e.kind !== 'note');
87
+ if (decisions.length === 0)
88
+ return task.trim();
89
+ return (`${task.trim()}\n\n`
90
+ + `PLANNING DECISIONS — these were settled with the user before this task was started. `
91
+ + `They are authoritative: implement them as written, and do not re-open or contradict `
92
+ + `them. They may not cover everything; decide anything they leave open as usual.\n\n`
93
+ + `${formatPlanTranscript(decisions)}`);
94
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * /task-plan — plan ONE task with the model, then hand it to /task.
3
+ *
4
+ * The command is a thin wiring layer. Everything it does is already in the
5
+ * codebase and is reused as-is:
6
+ *
7
+ * • the adaptive question loop, the duplicate backstop, the boxed picker and
8
+ * the A/B answer mapping → plan-session.ts (which reuses parsers.ts,
9
+ * question-dedup.ts, inline-markdown.ts,
10
+ * question-box.ts, yolo.ts)
11
+ * • the child process, its loop/leak/stall guards and retries → child-runner.ts
12
+ * • local TUI + remote browser prompt fan-out → remote/bridge.ts
13
+ * • the status widget → widget.ts
14
+ * • the plan file (.pi-tasks/TASK_PLAN_NNNN.md) → task-io.ts
15
+ * • the handoff itself → orchestrator.ts
16
+ *
17
+ * The handoff is deliberately the SAME call /task makes for a typed prompt —
18
+ * gated when `verify work` / `enforce guidelines` is on, fire-and-forget
19
+ * otherwise — so a planned task is not a second kind of task. The only thing
20
+ * /task receives that a bare /task would not is the decisions block.
21
+ */
22
+ import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
23
+ import { type PlanSessionDeps, type PlanOutcome } from './plan-session.js';
24
+ import { type PlanEntry } from './plan-io.js';
25
+ /**
26
+ * Build the session deps around a live command ctx: children with a status
27
+ * loader, prompts fanned out to the terminal AND any remote viewer, and the plan
28
+ * file rewritten after every entry.
29
+ */
30
+ export declare function buildPlanDeps(ctx: ExtensionCommandContext, cwd: string, planId: string, task: string, signal: AbortSignal): PlanSessionDeps;
31
+ /**
32
+ * Write the transcript into the plan file. The decisions and the model's answers
33
+ * to the user's questions live in separate sections: only the decisions are
34
+ * authoritative for the implementation, and the file must not blur that.
35
+ */
36
+ export declare function persistEntries(cwd: string, planId: string, entries: readonly PlanEntry[]): Promise<void>;
37
+ /**
38
+ * Seams the command flow needs from the outside world. Defaulted to the real
39
+ * thing in {@link handleTaskPlan}; injected in tests so the whole flow — plan
40
+ * file, transcript, handoff prompt — can be exercised without a model or a
41
+ * session replacement.
42
+ */
43
+ export interface PlanCommandDeps {
44
+ /** Build the session deps (children + dialogs) for this run. */
45
+ session(ctx: ExtensionCommandContext, cwd: string, planId: string, task: string, signal: AbortSignal): PlanSessionDeps;
46
+ /** Run the loop. */
47
+ run(deps: PlanSessionDeps): Promise<PlanOutcome>;
48
+ /** Hand the composed prompt to /task. Returns the produced task id, if any. */
49
+ handoff(ctx: ExtensionCommandContext, cwd: string, prompt: string): Promise<string | undefined>;
50
+ }
51
+ export declare function handleTaskPlan(args: string, ctx: ExtensionCommandContext, commandDeps?: PlanCommandDeps): Promise<void>;
52
+ export declare function registerTaskPlan(pi: ExtensionAPI): void;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * /task-plan — plan ONE task with the model, then hand it to /task.
3
+ *
4
+ * The command is a thin wiring layer. Everything it does is already in the
5
+ * codebase and is reused as-is:
6
+ *
7
+ * • the adaptive question loop, the duplicate backstop, the boxed picker and
8
+ * the A/B answer mapping → plan-session.ts (which reuses parsers.ts,
9
+ * question-dedup.ts, inline-markdown.ts,
10
+ * question-box.ts, yolo.ts)
11
+ * • the child process, its loop/leak/stall guards and retries → child-runner.ts
12
+ * • local TUI + remote browser prompt fan-out → remote/bridge.ts
13
+ * • the status widget → widget.ts
14
+ * • the plan file (.pi-tasks/TASK_PLAN_NNNN.md) → task-io.ts
15
+ * • the handoff itself → orchestrator.ts
16
+ *
17
+ * The handoff is deliberately the SAME call /task makes for a typed prompt —
18
+ * gated when `verify work` / `enforce guidelines` is on, fire-and-forget
19
+ * otherwise — so a planned task is not a second kind of task. The only thing
20
+ * /task receives that a bare /task would not is the decisions block.
21
+ */
22
+ import * as path from 'node:path';
23
+ import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
24
+ import { PLAN_QUESTION_PROMPT, PLAN_ANSWER_PROMPT } from './plan-prompts.js';
25
+ import { runPlanSession, ASK_TITLE } from './plan-session.js';
26
+ import { allocatePlanId, buildPlanBody, buildHandoffPrompt, formatPlanDecisions } from './plan-io.js';
27
+ import { writeTaskFile, setTaskSection, updateTaskFrontMatter, tasksDir } from './task-io.js';
28
+ import { deriveTitle } from './parsers.js';
29
+ import { renderInlineMarkdown } from './inline-markdown.js';
30
+ import { expandFeatureMentions } from './auto-orchestrator.js';
31
+ import { runSingleTask, runGatedTask } from './orchestrator.js';
32
+ import { startAutoLoader } from './widget.js';
33
+ import { SessionUI, publishViewer, publishNotify, publishLifecycleNotice, registerBridgeCommand } from '../remote/bridge.js';
34
+ import { getConfig } from '../config/config.js';
35
+ import { isYoloMode } from './yolo.js';
36
+ import { gateDebugWriter } from './debug-log.js';
37
+ import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
38
+ import * as fsp from 'node:fs/promises';
39
+ /** Loader labels for the two planning children. */
40
+ const PLAN_STEPS = {
41
+ 'plan-question': 'question',
42
+ 'plan-answer': 'answering you'
43
+ };
44
+ /**
45
+ * Build the session deps around a live command ctx: children with a status
46
+ * loader, prompts fanned out to the terminal AND any remote viewer, and the plan
47
+ * file rewritten after every entry.
48
+ */
49
+ export function buildPlanDeps(ctx, cwd, planId, task, signal) {
50
+ const ui = new SessionUI(ctx);
51
+ const theme = ctx.ui.theme;
52
+ let lastLine;
53
+ let status;
54
+ let contextUsage;
55
+ const parentContextWindow = getParentContextWindow(ctx);
56
+ const title = deriveTitle(task);
57
+ const logDebug = gateDebugWriter((msg) => {
58
+ const line = `${new Date().toISOString()} ${msg}\n`;
59
+ void fsp.appendFile(path.join(tasksDir(cwd), `${planId}-debug.log`), line).catch(() => { });
60
+ });
61
+ const phaseDeps = {
62
+ cwd,
63
+ taskId: planId,
64
+ signal,
65
+ onChildOutput: line => {
66
+ lastLine = line;
67
+ },
68
+ onContextUsage: snapshot => {
69
+ contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
70
+ },
71
+ ...(logDebug && { logDebug })
72
+ };
73
+ /** Run a planning child under the shared /task-auto-style loader. */
74
+ const child = async (name, prompt) => {
75
+ lastLine = undefined;
76
+ contextUsage = undefined;
77
+ const startedAt = Date.now();
78
+ const stopLoader = startAutoLoader(ctx, () => ({
79
+ command: '/task-plan',
80
+ title,
81
+ step: status ?? PLAN_STEPS[name] ?? name,
82
+ stepNum: 1,
83
+ stepTotal: 1,
84
+ startedAt,
85
+ lastLine,
86
+ contextUsage
87
+ }));
88
+ try {
89
+ return await runPhaseChild(phaseDeps, name, 'read', prompt);
90
+ }
91
+ finally {
92
+ stopLoader();
93
+ }
94
+ };
95
+ return {
96
+ generateQuestion: (priorQA, hint) => child('plan-question', prependHint(hint, PLAN_QUESTION_PROMPT(task, priorQA))),
97
+ answerUserQuestion: (priorQA, question) => child('plan-answer', PLAN_ANSWER_PROMPT(task, priorQA, question)),
98
+ ask: (spec) => ui.ask(spec),
99
+ promptText: (localTitle, question) => ui.ask({
100
+ localTitle,
101
+ question,
102
+ // No recommendation and no options: both surfaces fall back to
103
+ // their plain text input, which is exactly what "type your
104
+ // question" needs. Skip = changed my mind.
105
+ allowSkip: true
106
+ }),
107
+ showAnswer: async (question, answer) => {
108
+ const text = `You asked:\n${question}\n\n${answer}\n`;
109
+ publishViewer(ASK_TITLE, text);
110
+ await ctx.ui.editor(ASK_TITLE, text);
111
+ },
112
+ onEntries: async (entries) => {
113
+ await persistEntries(cwd, planId, entries);
114
+ },
115
+ renderMarkdown: (s) => renderInlineMarkdown(s, theme),
116
+ setStatus: line => {
117
+ status = line;
118
+ },
119
+ yolo: isYoloMode(),
120
+ ...(logDebug && { logDebug })
121
+ };
122
+ }
123
+ /**
124
+ * Write the transcript into the plan file. The decisions and the model's answers
125
+ * to the user's questions live in separate sections: only the decisions are
126
+ * authoritative for the implementation, and the file must not blur that.
127
+ */
128
+ export async function persistEntries(cwd, planId, entries) {
129
+ const decisions = entries.filter(e => e.kind !== 'note');
130
+ const notes = entries.filter(e => e.kind === 'note');
131
+ await setTaskSection(cwd, planId, 'decisions', formatPlanDecisions(decisions));
132
+ if (notes.length > 0) {
133
+ await setTaskSection(cwd, planId, 'notes', notes
134
+ .map(n => (n.kind === 'note' ? `Q: ${n.question}\nA: ${n.answer}` : ''))
135
+ .join('\n\n'));
136
+ }
137
+ }
138
+ /**
139
+ * The default handoff: EXACTLY what `/task <prompt>` does with a typed prompt —
140
+ * gated when `verify work` / `enforce guidelines` is on, fire-and-forget
141
+ * otherwise. A planned task must not be a second kind of task.
142
+ */
143
+ async function defaultHandoff(ctx, cwd, prompt) {
144
+ const cfg = getConfig();
145
+ if (cfg.verifyWork || cfg.enforceGuidelines) {
146
+ // runGatedTask owns the whole gated sequence and reports its own outcome;
147
+ // it does not surface the inner task id, so the plan file records the
148
+ // handoff without one.
149
+ await runGatedTask(ctx, cwd, prompt);
150
+ return undefined;
151
+ }
152
+ const { taskId, sessionCancelled } = await runSingleTask(ctx, cwd, prompt, { notifyFinish: true });
153
+ if (sessionCancelled) {
154
+ ctx.ui.notify('Could not start a fresh session for /task-plan.', 'warning');
155
+ return undefined;
156
+ }
157
+ return taskId || undefined;
158
+ }
159
+ const DEFAULT_COMMAND_DEPS = {
160
+ session: buildPlanDeps,
161
+ run: runPlanSession,
162
+ handoff: defaultHandoff
163
+ };
164
+ export async function handleTaskPlan(args, ctx, commandDeps = DEFAULT_COMMAND_DEPS) {
165
+ await ctx.waitForIdle();
166
+ const cwd = ctx.cwd;
167
+ const raw = args.trim();
168
+ if (raw.length === 0) {
169
+ ctx.ui.setEditorText('/task-plan ');
170
+ ctx.ui.notify('Describe the task after /task-plan (use @ for file completion).', 'info');
171
+ return;
172
+ }
173
+ // Inline any @file the user referenced, exactly as /task-auto's planner does:
174
+ // a one-line "Implement @spec.md" reads as trivial to a model that cannot see
175
+ // the file, and the first question comes out useless.
176
+ const task = await expandFeatureMentions(cwd, raw);
177
+ const planId = await allocatePlanId(cwd);
178
+ const now = new Date().toISOString();
179
+ const fm = {
180
+ id: planId,
181
+ state: 'in_progress',
182
+ // Plan files have no phase pipeline of their own — same as TASK_AUTO_*,
183
+ // which parks at 'done' so the phase progress bar never claims otherwise.
184
+ phase: 'done',
185
+ created_at: now,
186
+ updated_at: now,
187
+ title: deriveTitle(raw)
188
+ };
189
+ await writeTaskFile(cwd, fm, buildPlanBody(task));
190
+ const abort = new AbortController();
191
+ let outcome;
192
+ try {
193
+ outcome = await commandDeps.run(commandDeps.session(ctx, cwd, planId, task, abort.signal));
194
+ }
195
+ catch (err) {
196
+ const msg = err instanceof Error ? err.message : String(err);
197
+ await updateTaskFrontMatter(cwd, planId, {
198
+ state: msg === USER_CANCELLED ? 'cancelled' : 'failed',
199
+ reason: msg.slice(0, 200)
200
+ }).catch(() => { });
201
+ const line = `${planId} stopped — ${msg.slice(0, 160)}`;
202
+ ctx.ui.notify(line, 'error');
203
+ publishLifecycleNotice(line, 'error');
204
+ return;
205
+ }
206
+ if (outcome.kind === 'cancelled') {
207
+ // Nothing is handed to /task, but whatever WAS decided stays on disk —
208
+ // a cancelled plan is a record, not a rollback.
209
+ await updateTaskFrontMatter(cwd, planId, { state: 'cancelled' }).catch(() => { });
210
+ const line = outcome.entries.length === 0 ?
211
+ `${planId} cancelled — nothing planned.`
212
+ : `${planId} cancelled — ${outcome.entries.length} entr${outcome.entries.length === 1 ? 'y' : 'ies'} kept in .pi-tasks/${planId}.md`;
213
+ ctx.ui.notify(line, 'warning');
214
+ publishNotify(line, 'warning');
215
+ return;
216
+ }
217
+ const prompt = buildHandoffPrompt(task, outcome.entries);
218
+ const decisions = outcome.entries.filter(e => e.kind !== 'note').length;
219
+ await setTaskSection(cwd, planId, 'handoff', `handoff_at: ${new Date().toISOString()}\ndecisions: ${decisions}`).catch(() => { });
220
+ await updateTaskFrontMatter(cwd, planId, { state: 'completed' }).catch(() => { });
221
+ const taskId = await commandDeps.handoff(ctx, cwd, prompt);
222
+ if (taskId) {
223
+ // Stamped after the fact: this is the one link from the plan to the task
224
+ // it produced, and /task allocates the id only once its own run starts.
225
+ await setTaskSection(cwd, planId, 'handoff', `handoff_at: ${new Date().toISOString()}\ndecisions: ${decisions}\ntask: ${taskId}`).catch(() => { });
226
+ }
227
+ }
228
+ export function registerTaskPlan(pi) {
229
+ registerBridgeCommand(pi, 'task-plan', {
230
+ description: 'Plan one task with the model — it asks, you answer (or ask it something, or '
231
+ + 'proceed) — then hand the decisions to /task. Usage: /task-plan <prompt>',
232
+ handler: handleTaskPlan
233
+ });
234
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Prompts for /task-plan's two planning children.
3
+ *
4
+ * /task-plan plans ONE task interactively before any spec work happens. Its
5
+ * question generator is deliberately the SAME SHAPE as /task-auto's clarify head
6
+ * (`AUTO_CLARIFY_PROMPT` in auto-prompts.ts): one numbered question, a required
7
+ * `SUGGESTED:` line, an optional `ALT:` line for a binary fork, the literal token
8
+ * NONE when nothing remains. That is not a coincidence — it means
9
+ * {@link parseClarifyList} parses this output UNCHANGED, and the boxed picker,
10
+ * the duplicate backstop and the YOLO picker all work here with no new code.
11
+ *
12
+ * What differs is the JOB. /task-auto's clarify asks what changes how a feature is
13
+ * SPLIT INTO TASKS; every question it asks is about plan shape, ordering, and which
14
+ * subsystems are in or out. /task-plan is planning a single unit of work that /task
15
+ * will implement in one run, so a "how do we split this" question is off-topic here
16
+ * and the prompt below rules it out explicitly.
17
+ */
18
+ /**
19
+ * Ask the SINGLE most important remaining question about ONE task.
20
+ *
21
+ * `priorQA` carries every decision made so far — model questions the user
22
+ * answered, answers the user volunteered, and the Q&A from questions the user
23
+ * asked the model — so each next question adapts to them. Output MUST match
24
+ * parseClarifyList.
25
+ */
26
+ export declare const PLAN_QUESTION_PROMPT: (task: string, priorQA: string) => string;
27
+ /**
28
+ * Answer a question the USER asked the model during planning.
29
+ *
30
+ * This is the one channel /task-plan adds that no existing phase has: everywhere
31
+ * else in pi-task the model asks and the user answers. Here the user asks. The
32
+ * answer is advisory — it is recorded in the plan file as a note, and it does NOT
33
+ * by itself decide anything; the user still answers the model's own questions.
34
+ *
35
+ * The abstention rule matters more here than anywhere else in the pipeline: a
36
+ * planning answer that invents a file, a flag, or an API reads exactly like a
37
+ * grounded one, and the user is asking BECAUSE they do not know. Saying "I could
38
+ * not confirm this" is a correct answer; a confident guess is not.
39
+ */
40
+ export declare const PLAN_ANSWER_PROMPT: (task: string, priorQA: string, question: string) => string;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Prompts for /task-plan's two planning children.
3
+ *
4
+ * /task-plan plans ONE task interactively before any spec work happens. Its
5
+ * question generator is deliberately the SAME SHAPE as /task-auto's clarify head
6
+ * (`AUTO_CLARIFY_PROMPT` in auto-prompts.ts): one numbered question, a required
7
+ * `SUGGESTED:` line, an optional `ALT:` line for a binary fork, the literal token
8
+ * NONE when nothing remains. That is not a coincidence — it means
9
+ * {@link parseClarifyList} parses this output UNCHANGED, and the boxed picker,
10
+ * the duplicate backstop and the YOLO picker all work here with no new code.
11
+ *
12
+ * What differs is the JOB. /task-auto's clarify asks what changes how a feature is
13
+ * SPLIT INTO TASKS; every question it asks is about plan shape, ordering, and which
14
+ * subsystems are in or out. /task-plan is planning a single unit of work that /task
15
+ * will implement in one run, so a "how do we split this" question is off-topic here
16
+ * and the prompt below rules it out explicitly.
17
+ */
18
+ /**
19
+ * Ask the SINGLE most important remaining question about ONE task.
20
+ *
21
+ * `priorQA` carries every decision made so far — model questions the user
22
+ * answered, answers the user volunteered, and the Q&A from questions the user
23
+ * asked the model — so each next question adapts to them. Output MUST match
24
+ * parseClarifyList.
25
+ */
26
+ export const PLAN_QUESTION_PROMPT = (task, priorQA) => `You are helping a user plan ONE implementation task before it is built, one clarifying question at a time.
27
+
28
+ TASK:
29
+ ${task.trim()}
30
+
31
+ DECISIONS SO FAR:
32
+ ${priorQA.trim() || '(none yet)'}
33
+
34
+ READ FIRST. Use the read tool on the repo and any referenced docs before you
35
+ decide what to ask — open the files this task would touch. A question you could
36
+ have asked without opening anything is almost always the wrong one: the good
37
+ question is the fork you can only see once you know what is already there, and
38
+ the SUGGESTED default has to name real, existing things to be worth accepting.
39
+ Reading is expected and costs you nothing; only your REPLY is short.
40
+
41
+ Output the SINGLE most important question that REMAINS — the one whose answer
42
+ would most change HOW THIS ONE TASK IS BUILT: what is in and out of its scope,
43
+ which approach or data shape it commits to, which existing code it changes versus
44
+ leaves alone, how it behaves at the edges the request does not pin down. Account
45
+ for the decisions so far:
46
+ - Never re-ask something already decided above.
47
+ - If a decision introduced a new fork or contradicts an assumption in the request
48
+ (for example, the user picked an approach the request did not anticipate), ask
49
+ about the most important consequence of that choice next.
50
+ - Drop questions the decisions have made irrelevant.
51
+
52
+ SCOPE RULES — read carefully:
53
+ - This is ONE task, implemented in ONE run. Do NOT ask how the work should be
54
+ split, sequenced, phased, or broken into separate tasks, milestones, or PRs —
55
+ that decision is not on the table here.
56
+ - Questions must clarify the EXISTING request. Do NOT propose new deliverables,
57
+ enhancements, migrations, or "while we're here" cleanups.
58
+ - Skip anything the implementer will naturally resolve by reading the code
59
+ (where a file lives, what a function is currently called, which test runner the
60
+ repo uses).
61
+ - Ask about decisions that are costly to reverse once the code is written.
62
+
63
+ YOU MUST propose a default answer for the question — every question you emit
64
+ carries exactly one SUGGESTED line. Never omit it, never leave it blank, never
65
+ refuse. Infer the most sensible, concrete, decisive default from the request, the
66
+ repo, and any stated constraints; it is shown to the user as a recommendation they
67
+ accept or override. When the question is a genuine binary "A or B?" fork, also give
68
+ the single best alternative as an ALT line; otherwise emit only the one SUGGESTED.
69
+
70
+ OUTPUT FORMAT (exact) — read as much as you like, but your written REPLY is 2 or
71
+ 3 lines and nothing else:
72
+ - Do NOT report what you read. No preamble, no analysis, no findings, no numbered
73
+ notes about files. The FIRST line of your reply is the question itself.
74
+ (Measured failure: a reply that opened with a numbered observation about a file
75
+ was read as the question.) What you learned belongs INSIDE the question and its
76
+ SUGGESTED line, as concrete names — not in a summary of your investigation.
77
+ - One question as a single numbered line: "1. ...".
78
+ - On the NEXT line (never inline), a line that begins with "SUGGESTED: <your recommended default>". This line is REQUIRED for every question.
79
+ - If your question names two alternatives — ANY question of the form "should it be X or Y?" — you MUST add a third line beginning with "ALT: <the option your SUGGESTED did not take>". Only a question with no second alternative (a genuinely open "what should X be?") omits the ALT line. Do not offer a choice in the question and then leave the user only one card.
80
+ - Put the core question in **bold**, followed by a short one-line rationale in plain prose. Backticks around code/identifiers are fine. Avoid other markdown (headings, bullet lists, links).
81
+ - Only when nothing decision-changing is left to ask — the request and the decisions above already pin down how this task is built — output exactly the single token NONE on its own line (and no SUGGESTED line).
82
+
83
+ EXAMPLES (format only — your wording will differ):
84
+
85
+ Open-ended question:
86
+ 1. **Which existing callers must keep working unchanged?** This decides whether the change can alter the exported signature or must add a new one alongside it.
87
+ SUGGESTED: keep every current caller working — add the new option with a default that preserves today's behaviour
88
+
89
+ Binary "A or B?" fork:
90
+ 1. **Should the retry live in the client wrapper or in each call site?** This decides whether one shared code path owns the backoff or every caller repeats it.
91
+ SUGGESTED: put it in the client wrapper so every call site inherits the same backoff
92
+ ALT: retry at each call site, so a caller can opt out
93
+
94
+ No question remains:
95
+ NONE`;
96
+ /**
97
+ * Answer a question the USER asked the model during planning.
98
+ *
99
+ * This is the one channel /task-plan adds that no existing phase has: everywhere
100
+ * else in pi-task the model asks and the user answers. Here the user asks. The
101
+ * answer is advisory — it is recorded in the plan file as a note, and it does NOT
102
+ * by itself decide anything; the user still answers the model's own questions.
103
+ *
104
+ * The abstention rule matters more here than anywhere else in the pipeline: a
105
+ * planning answer that invents a file, a flag, or an API reads exactly like a
106
+ * grounded one, and the user is asking BECAUSE they do not know. Saying "I could
107
+ * not confirm this" is a correct answer; a confident guess is not.
108
+ */
109
+ export const PLAN_ANSWER_PROMPT = (task, priorQA, question) => `You are helping a user plan ONE implementation task. The user has asked YOU a question about it. Answer it.
110
+
111
+ TASK:
112
+ ${task.trim()}
113
+
114
+ DECISIONS SO FAR:
115
+ ${priorQA.trim() || '(none yet)'}
116
+
117
+ THE USER'S QUESTION:
118
+ ${question.trim()}
119
+
120
+ Use the read tool to check the repo before you answer. Ground the answer in what
121
+ is actually there.
122
+
123
+ RULES — read carefully:
124
+ - Answer the question that was asked. Do not answer a different, easier one.
125
+ - Name only files, symbols, commands, and options you have VERIFIED exist — by
126
+ reading them, or because they appear in the task above. Never name a path or an
127
+ API from memory.
128
+ - If you cannot confirm the answer from the repo, say so plainly and say what you
129
+ would need to check. "I could not confirm X" is a correct answer here; a
130
+ confident guess is not.
131
+ - If the question asks for a recommendation, give ONE, and say in a sentence what
132
+ it costs.
133
+ - Do not start implementing, do not write code blocks longer than a few lines, and
134
+ do not restate the task back to the user.
135
+
136
+ OUTPUT:
137
+ - Plain prose, at most 8 short lines. No preamble, no headings, no bullet lists
138
+ longer than 4 items, no code fences.`;