@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.
- package/README.md +29 -0
- package/dist/index.js +2 -0
- package/dist/remote/bridge.d.ts +18 -0
- package/dist/remote/bridge.js +2 -0
- package/dist/remote/protocol.d.ts +9 -0
- package/dist/remote/ui-script.js +20 -0
- package/dist/task/accept-debt.d.ts +51 -2
- package/dist/task/accept-debt.js +140 -7
- package/dist/task/auto-orchestrator.d.ts +1 -0
- package/dist/task/auto-orchestrator.js +40 -2
- package/dist/task/final-gate-fix.d.ts +22 -1
- package/dist/task/final-gate-fix.js +53 -6
- package/dist/task/final-gate.d.ts +54 -1
- package/dist/task/final-gate.js +115 -3
- package/dist/task/gate-deps.d.ts +41 -2
- package/dist/task/gate-deps.js +141 -3
- package/dist/task/plan-io.d.ts +55 -0
- package/dist/task/plan-io.js +94 -0
- package/dist/task/plan-orchestrator.d.ts +52 -0
- package/dist/task/plan-orchestrator.js +234 -0
- package/dist/task/plan-prompts.d.ts +40 -0
- package/dist/task/plan-prompts.js +138 -0
- package/dist/task/plan-session.d.ts +184 -0
- package/dist/task/plan-session.js +373 -0
- package/dist/task/question-box.d.ts +8 -0
- package/dist/task/question-box.js +1 -1
- package/dist/task/spec-validation.d.ts +14 -0
- package/dist/task/spec-validation.js +21 -1
- package/dist/task/widget.d.ts +4 -0
- package/dist/task/widget.js +2 -2
- package/dist/task/write-guard.d.ts +42 -0
- package/dist/task/write-guard.js +108 -0
- package/package.json +1 -1
|
@@ -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.`;
|