@dreki-gg/pi-plan-mode 0.6.3 → 0.7.2
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/CHANGELOG.md +34 -0
- package/extensions/plan-mode/index.ts +203 -250
- package/extensions/plan-mode/plans-json.ts +1 -5
- package/extensions/plan-mode/tools/submit-plan.ts +144 -0
- package/extensions/plan-mode/tools/update-step.ts +143 -0
- package/extensions/plan-mode/types.ts +25 -0
- package/extensions/plan-mode/utils.test.ts +6 -0
- package/extensions/plan-mode/utils.ts +1 -82
- package/package.json +2 -7
- package/node_modules/@dreki-gg/pi-command-sandbox/dist/index.d.mts +0 -104
- package/node_modules/@dreki-gg/pi-command-sandbox/dist/index.mjs +0 -396
- package/node_modules/@dreki-gg/pi-command-sandbox/package.json +0 -42
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Plan Mode Extension
|
|
3
3
|
*
|
|
4
4
|
* Two-phase workflow:
|
|
5
|
-
* 1. PLAN phase — read-only tools
|
|
6
|
-
* Planner analyzes codebase,
|
|
7
|
-
* 2. EXECUTE phase — full tools +
|
|
8
|
-
* Executor works through
|
|
5
|
+
* 1. PLAN phase — read-only tools + submit_plan tool + medium thinking
|
|
6
|
+
* Planner analyzes codebase, calls submit_plan with structured data
|
|
7
|
+
* 2. EXECUTE phase — full tools + update_step tool + low thinking
|
|
8
|
+
* Executor works through plan steps, calling update_step for each
|
|
9
9
|
*
|
|
10
|
-
* Plans live in `.plans/<kebab-name>/
|
|
10
|
+
* Plans live in `.plans/<kebab-name>/plan.json` with structured steps and context.
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
13
|
* /plan [prompt] — enter plan mode (optionally with a starting prompt)
|
|
@@ -18,33 +18,27 @@
|
|
|
18
18
|
* --plan — start session in plan mode
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { AgentMessage } from '@earendil-works/pi-agent-core';
|
|
22
|
-
import type { AssistantMessage, TextContent } from '@earendil-works/pi-ai';
|
|
23
21
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
24
22
|
import { Key } from '@earendil-works/pi-tui';
|
|
25
|
-
import { mkdir,
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type PlansManifest,
|
|
32
|
-
} from './plans-json.js';
|
|
23
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
24
|
+
import { isSafeCommand } from './utils.js';
|
|
25
|
+
import { readPlansJson, serializePlansJson } from './plans-json.js';
|
|
26
|
+
import { registerSubmitPlanTool } from './tools/submit-plan.js';
|
|
27
|
+
import { registerUpdateStepTool } from './tools/update-step.js';
|
|
28
|
+
import type { PlanData, PersistedState } from './types.js';
|
|
33
29
|
|
|
34
30
|
// ── Tool sets ────────────────────────────────────────────────────────────────
|
|
35
|
-
// Plan phase: read-only + edit/write (for .plans/ files only, enforced by prompt)
|
|
36
31
|
const PLAN_TOOLS = [
|
|
37
32
|
'read',
|
|
38
33
|
'bash',
|
|
39
34
|
'grep',
|
|
40
35
|
'find',
|
|
41
36
|
'ls',
|
|
42
|
-
'
|
|
43
|
-
'write',
|
|
37
|
+
'submit_plan',
|
|
44
38
|
'questionnaire',
|
|
45
39
|
'search_skills',
|
|
46
40
|
];
|
|
47
|
-
const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'search_skills'];
|
|
41
|
+
const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'update_step', 'search_skills'];
|
|
48
42
|
|
|
49
43
|
// ── Model + thinking presets ─────────────────────────────────────────────────
|
|
50
44
|
const PLAN_MODEL = { provider: 'anthropic', id: 'claude-opus-4-6' } as const;
|
|
@@ -55,32 +49,13 @@ const EXEC_THINKING = 'low' as const;
|
|
|
55
49
|
|
|
56
50
|
type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
57
51
|
|
|
58
|
-
// ── Persisted state ──────────────────────────────────────────────────────────
|
|
59
|
-
interface PersistedState {
|
|
60
|
-
planEnabled: boolean;
|
|
61
|
-
executing: boolean;
|
|
62
|
-
planDir: string | undefined;
|
|
63
|
-
todos: TodoItem[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
67
|
-
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
68
|
-
return m.role === 'assistant' && Array.isArray(m.content);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getTextContent(message: AssistantMessage): string {
|
|
72
|
-
return message.content
|
|
73
|
-
.filter((b): b is TextContent => b.type === 'text')
|
|
74
|
-
.map((b) => b.text)
|
|
75
|
-
.join('\n');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
52
|
// ── Extension ────────────────────────────────────────────────────────────────
|
|
79
53
|
export default function planMode(pi: ExtensionAPI): void {
|
|
80
54
|
let planEnabled = false;
|
|
81
55
|
let executing = false;
|
|
82
56
|
let planDir: string | undefined;
|
|
83
|
-
let
|
|
57
|
+
let plan: PlanData | undefined;
|
|
58
|
+
let executionStartIdx: number | undefined;
|
|
84
59
|
let previousThinking: ThinkingLevel | undefined;
|
|
85
60
|
let previousModel: { provider: string; id: string } | undefined;
|
|
86
61
|
|
|
@@ -97,7 +72,8 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
97
72
|
planEnabled,
|
|
98
73
|
executing,
|
|
99
74
|
planDir,
|
|
100
|
-
|
|
75
|
+
plan,
|
|
76
|
+
executionStartIdx,
|
|
101
77
|
});
|
|
102
78
|
}
|
|
103
79
|
|
|
@@ -119,29 +95,43 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
119
95
|
};
|
|
120
96
|
|
|
121
97
|
await mkdir('.plans', { recursive: true });
|
|
122
|
-
|
|
123
|
-
|
|
98
|
+
await writeFile('.plans/plans.json', serializePlansJson(manifest), 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Save plan.json to disk ────────────────────────────────────────────────
|
|
102
|
+
async function savePlanToDisk(): Promise<void> {
|
|
103
|
+
if (!planDir || !plan) return;
|
|
104
|
+
await mkdir(planDir, { recursive: true });
|
|
105
|
+
await writeFile(`${planDir}/plan.json`, JSON.stringify(plan, null, 2) + '\n', 'utf-8');
|
|
124
106
|
}
|
|
125
107
|
|
|
126
108
|
// ── UI updates ────────────────────────────────────────────────────────────
|
|
127
109
|
function updateUI(ctx: ExtensionContext): void {
|
|
128
110
|
const { theme } = ctx.ui;
|
|
129
111
|
|
|
130
|
-
if (executing &&
|
|
131
|
-
const done =
|
|
132
|
-
|
|
112
|
+
if (executing && plan) {
|
|
113
|
+
const done = plan.steps.filter((s) => s.status === 'done').length;
|
|
114
|
+
const total = plan.steps.length;
|
|
115
|
+
ctx.ui.setStatus('plan-mode', theme.fg('accent', `📋 exec ${done}/${total}`));
|
|
133
116
|
} else if (planEnabled) {
|
|
134
117
|
ctx.ui.setStatus('plan-mode', theme.fg('warning', '📝 plan'));
|
|
135
118
|
} else {
|
|
136
119
|
ctx.ui.setStatus('plan-mode', undefined);
|
|
137
120
|
}
|
|
138
121
|
|
|
139
|
-
if (executing &&
|
|
140
|
-
const lines =
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
if (executing && plan) {
|
|
123
|
+
const lines = plan.steps.map((step, i) => {
|
|
124
|
+
const num = `${i + 1}. `;
|
|
125
|
+
switch (step.status) {
|
|
126
|
+
case 'done':
|
|
127
|
+
return theme.fg('success', '✓ ') + theme.fg('muted', theme.strikethrough(num + step.description));
|
|
128
|
+
case 'skipped':
|
|
129
|
+
return theme.fg('warning', '⊘ ') + theme.fg('muted', theme.strikethrough(num + step.description));
|
|
130
|
+
case 'blocked':
|
|
131
|
+
return theme.fg('error', '✗ ') + theme.fg('error', num + step.description);
|
|
132
|
+
default:
|
|
133
|
+
return theme.fg('muted', '☐ ') + (num + step.description);
|
|
143
134
|
}
|
|
144
|
-
return `${theme.fg('muted', '☐ ')}${item.text}`;
|
|
145
135
|
});
|
|
146
136
|
ctx.ui.setWidget('plan-todos', lines);
|
|
147
137
|
} else {
|
|
@@ -172,7 +162,7 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
172
162
|
planEnabled = true;
|
|
173
163
|
executing = false;
|
|
174
164
|
planDir = undefined;
|
|
175
|
-
|
|
165
|
+
plan = undefined;
|
|
176
166
|
previousThinking = pi.getThinkingLevel() as ThinkingLevel;
|
|
177
167
|
previousModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
178
168
|
pi.setActiveTools(PLAN_TOOLS);
|
|
@@ -190,7 +180,8 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
190
180
|
planEnabled = false;
|
|
191
181
|
executing = false;
|
|
192
182
|
planDir = undefined;
|
|
193
|
-
|
|
183
|
+
plan = undefined;
|
|
184
|
+
executionStartIdx = undefined;
|
|
194
185
|
pi.setActiveTools(EXEC_TOOLS);
|
|
195
186
|
if (previousModel) {
|
|
196
187
|
await switchModel(ctx, previousModel);
|
|
@@ -206,6 +197,8 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
206
197
|
async function startExecution(ctx: ExtensionContext): Promise<void> {
|
|
207
198
|
planEnabled = false;
|
|
208
199
|
executing = true;
|
|
200
|
+
// Record message count so the context filter can drop the planning conversation
|
|
201
|
+
executionStartIdx = ctx.sessionManager.getEntries().length;
|
|
209
202
|
pi.setActiveTools(EXEC_TOOLS);
|
|
210
203
|
await switchModel(ctx, EXEC_MODEL);
|
|
211
204
|
pi.setThinkingLevel(EXEC_THINKING);
|
|
@@ -225,6 +218,26 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
225
218
|
}
|
|
226
219
|
}
|
|
227
220
|
|
|
221
|
+
// ── Register tools ────────────────────────────────────────────────────────
|
|
222
|
+
registerSubmitPlanTool(pi, {
|
|
223
|
+
onPlanSubmitted: (dir, submittedPlan) => {
|
|
224
|
+
planDir = dir;
|
|
225
|
+
plan = submittedPlan;
|
|
226
|
+
persist();
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
registerUpdateStepTool(pi, {
|
|
231
|
+
getPlan: () => plan,
|
|
232
|
+
onStepUpdated: (step, status, notes) => {
|
|
233
|
+
if (!plan) return;
|
|
234
|
+
const stepIdx = step - 1;
|
|
235
|
+
plan.steps[stepIdx].status = status;
|
|
236
|
+
if (notes) plan.steps[stepIdx].notes = notes;
|
|
237
|
+
persist();
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
228
241
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
229
242
|
pi.registerCommand('plan', {
|
|
230
243
|
description: 'Enter plan mode, optionally with a starting prompt',
|
|
@@ -244,11 +257,14 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
244
257
|
pi.registerCommand('todos', {
|
|
245
258
|
description: 'Show current plan progress',
|
|
246
259
|
handler: async (_args, ctx) => {
|
|
247
|
-
if (
|
|
260
|
+
if (!plan || plan.steps.length === 0) {
|
|
248
261
|
ctx.ui.notify('No plan yet. Use /plan to start planning.', 'info');
|
|
249
262
|
return;
|
|
250
263
|
}
|
|
251
|
-
const
|
|
264
|
+
const statusIcon = { pending: '○', done: '✓', skipped: '⊘', blocked: '✗' };
|
|
265
|
+
const list = plan.steps
|
|
266
|
+
.map((s, i) => `${i + 1}. ${statusIcon[s.status]} ${s.description}`)
|
|
267
|
+
.join('\n');
|
|
252
268
|
ctx.ui.notify(`Plan Progress:\n${list}`, 'info');
|
|
253
269
|
},
|
|
254
270
|
});
|
|
@@ -262,7 +278,6 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
262
278
|
pi.on('tool_call', async (event) => {
|
|
263
279
|
if (!planEnabled) return;
|
|
264
280
|
|
|
265
|
-
// Block bash commands that aren't on the safe allowlist
|
|
266
281
|
if (event.toolName === 'bash') {
|
|
267
282
|
const command = event.input.command as string;
|
|
268
283
|
if (!isSafeCommand(command)) {
|
|
@@ -272,25 +287,25 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
272
287
|
};
|
|
273
288
|
}
|
|
274
289
|
}
|
|
275
|
-
|
|
276
|
-
// Block edit/write to paths outside .plans/
|
|
277
|
-
if (event.toolName === 'edit' || event.toolName === 'write') {
|
|
278
|
-
const path = (event.input as { path?: string }).path ?? '';
|
|
279
|
-
if (!path.startsWith('.plans/') && !path.startsWith('.plans\\')) {
|
|
280
|
-
return {
|
|
281
|
-
block: true,
|
|
282
|
-
reason: `Plan mode: file modifications are restricted to .plans/ directory.\nPath: ${path}`,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
290
|
});
|
|
287
291
|
|
|
288
|
-
// ── Filter
|
|
292
|
+
// ── Filter planning conversation when executing ──────────────────────────
|
|
289
293
|
pi.on('context', async (event) => {
|
|
290
294
|
if (planEnabled) return;
|
|
295
|
+
|
|
296
|
+
if (executing && executionStartIdx !== undefined) {
|
|
297
|
+
// During execution, drop everything from the planning phase.
|
|
298
|
+
// The executor gets its context from the before_agent_start injection.
|
|
299
|
+
const startIdx = executionStartIdx;
|
|
300
|
+
return {
|
|
301
|
+
messages: event.messages.filter((_m, i) => i >= startIdx),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Not executing — just strip stale plan-mode injected messages
|
|
291
306
|
return {
|
|
292
307
|
messages: event.messages.filter((m) => {
|
|
293
|
-
const msg = m as
|
|
308
|
+
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
294
309
|
if (msg.customType === 'plan-mode-context') return false;
|
|
295
310
|
if (msg.role !== 'user') return true;
|
|
296
311
|
const content = msg.content;
|
|
@@ -299,7 +314,8 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
299
314
|
}
|
|
300
315
|
if (Array.isArray(content)) {
|
|
301
316
|
return !content.some(
|
|
302
|
-
(c
|
|
317
|
+
(c: { type?: string; text?: string }) =>
|
|
318
|
+
c.type === 'text' && c.text?.includes('[PLAN MODE ACTIVE]'),
|
|
303
319
|
);
|
|
304
320
|
}
|
|
305
321
|
return true;
|
|
@@ -319,131 +335,134 @@ You are in plan mode — a planning phase with strict bash restrictions.
|
|
|
319
335
|
Restrictions:
|
|
320
336
|
- Available tools: ${PLAN_TOOLS.join(', ')}
|
|
321
337
|
- Bash is restricted to read-only commands (ls, grep, git status, etc.)
|
|
322
|
-
- edit and write are ONLY allowed for files inside the \`.plans/\` directory
|
|
323
338
|
|
|
324
339
|
Your task:
|
|
325
340
|
1. Analyze the codebase thoroughly using the available read-only tools
|
|
326
341
|
2. Ask clarifying questions if needed (use the questionnaire tool)
|
|
327
342
|
3. Produce a detailed, concrete plan
|
|
328
343
|
|
|
329
|
-
When you are ready to finalize the plan:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
<Brief description of what this plan accomplishes>
|
|
337
|
-
|
|
338
|
-
## Context
|
|
339
|
-
<Key findings from codebase analysis>
|
|
340
|
-
|
|
341
|
-
## Plan:
|
|
342
|
-
1. First step — what to change and where
|
|
343
|
-
2. Second step — what to change and where
|
|
344
|
-
...
|
|
344
|
+
When you are ready to finalize the plan, call the submit_plan tool with:
|
|
345
|
+
- name: a short kebab-case name (e.g. "add-auth-middleware")
|
|
346
|
+
- title: a human-readable plan title
|
|
347
|
+
- context: complete codebase context including relevant file paths, APIs, patterns, constraints, and gotchas — this must be thorough enough that an implementor with zero prior context can execute the plan
|
|
348
|
+
- steps: an array of steps, each with a short description (≤60 chars for display) and detailed implementation instructions
|
|
349
|
+
- risks: any open questions, assumptions, or concerns
|
|
345
350
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
\`\`\`
|
|
349
|
-
|
|
350
|
-
3. Create \`.plans/<plan-name>/START-PROMPT.md\` — a self-contained handoff prompt that a different model can use to execute the plan WITHOUT access to this conversation. It must include:
|
|
351
|
-
- Complete context about the codebase (relevant file paths, APIs, patterns)
|
|
352
|
-
- The full plan steps to execute
|
|
353
|
-
- Any critical constraints or gotchas
|
|
354
|
-
- Clear instructions to mark each step done with \`[DONE:n]\` tags
|
|
355
|
-
|
|
356
|
-
The START-PROMPT.md is critical — it must be thorough enough that an implementor with zero prior context can execute the plan correctly.
|
|
357
|
-
|
|
358
|
-
If you need supporting reference files for extra context (code snippets, diagrams, specs), place them alongside in the same \`.plans/<plan-name>/\` directory.
|
|
359
|
-
|
|
360
|
-
Do NOT attempt to make product code changes — only create planning artifacts in \`.plans/\`.`,
|
|
351
|
+
Do NOT attempt to make product code changes — only analyze and plan.
|
|
352
|
+
Do NOT write files manually — use submit_plan to finalize the plan.`,
|
|
361
353
|
display: false,
|
|
362
354
|
},
|
|
363
355
|
};
|
|
364
356
|
}
|
|
365
357
|
|
|
366
|
-
if (executing &&
|
|
367
|
-
const remaining =
|
|
368
|
-
|
|
358
|
+
if (executing && plan) {
|
|
359
|
+
const remaining = plan.steps
|
|
360
|
+
.map((s, i) => ({ ...s, num: i + 1 }))
|
|
361
|
+
.filter((s) => s.status === 'pending');
|
|
362
|
+
|
|
363
|
+
if (remaining.length === 0) return;
|
|
364
|
+
|
|
365
|
+
const stepList = remaining
|
|
366
|
+
.map((s) => `${s.num}. ${s.description}\n Details: ${s.details}`)
|
|
367
|
+
.join('\n\n');
|
|
368
|
+
|
|
369
369
|
return {
|
|
370
370
|
message: {
|
|
371
371
|
customType: 'plan-execution-context',
|
|
372
372
|
content: `[EXECUTING PLAN — Full tool access enabled]
|
|
373
373
|
|
|
374
|
+
Context:
|
|
375
|
+
${plan.context}
|
|
376
|
+
|
|
374
377
|
Remaining steps:
|
|
375
|
-
${
|
|
378
|
+
${stepList}
|
|
376
379
|
|
|
377
|
-
Execute each step in order.
|
|
380
|
+
Execute each step in order. After completing each step, call the update_step tool to mark it done before moving to the next.
|
|
381
|
+
If a step is unnecessary, call update_step with status "skipped".
|
|
382
|
+
If a step cannot be completed, call update_step with status "blocked" and explain why in the notes.`,
|
|
378
383
|
display: false,
|
|
379
384
|
},
|
|
380
385
|
};
|
|
381
386
|
}
|
|
382
387
|
});
|
|
383
388
|
|
|
384
|
-
// ──
|
|
385
|
-
pi.on('
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
389
|
+
// ── Handle blocked steps and plan completion ──────────────────────────────
|
|
390
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
391
|
+
// Check for blocked steps during execution
|
|
392
|
+
if (executing && plan) {
|
|
393
|
+
const blocked = plan.steps
|
|
394
|
+
.map((s, i) => ({ ...s, num: i + 1 }))
|
|
395
|
+
.filter((s) => s.status === 'blocked');
|
|
396
|
+
|
|
397
|
+
if (blocked.length > 0) {
|
|
398
|
+
const blockedStep = blocked[0];
|
|
399
|
+
const blockedInfo = blockedStep.notes
|
|
400
|
+
? `Step ${blockedStep.num}: ${blockedStep.description}\nReason: ${blockedStep.notes}`
|
|
401
|
+
: `Step ${blockedStep.num}: ${blockedStep.description}`;
|
|
402
|
+
|
|
403
|
+
const choice = await ctx.ui.select(`Step blocked — ${blockedInfo}\n\nWhat next?`, [
|
|
404
|
+
'Skip this step',
|
|
405
|
+
'Provide instructions',
|
|
406
|
+
'Re-plan',
|
|
407
|
+
'Abort execution',
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
if (choice === 'Skip this step') {
|
|
411
|
+
plan.steps[blockedStep.num - 1].status = 'skipped';
|
|
412
|
+
await savePlanToDisk();
|
|
413
|
+
updateUI(ctx);
|
|
414
|
+
persist();
|
|
415
|
+
|
|
416
|
+
// Check if there are more pending steps
|
|
417
|
+
const hasPending = plan.steps.some((s) => s.status === 'pending');
|
|
418
|
+
if (hasPending) {
|
|
419
|
+
pi.sendUserMessage('The blocked step has been skipped. Continue with the next step.', { deliverAs: 'followUp' });
|
|
420
|
+
}
|
|
421
|
+
// If no pending, fall through to completion check below
|
|
422
|
+
} else if (choice === 'Provide instructions') {
|
|
423
|
+
const instructions = await ctx.ui.editor('Instructions for the blocked step:', '');
|
|
424
|
+
if (instructions?.trim()) {
|
|
425
|
+
plan.steps[blockedStep.num - 1].status = 'pending';
|
|
426
|
+
plan.steps[blockedStep.num - 1].notes = undefined;
|
|
427
|
+
await savePlanToDisk();
|
|
428
|
+
updateUI(ctx);
|
|
429
|
+
persist();
|
|
430
|
+
pi.sendUserMessage(
|
|
431
|
+
`Retry step ${blockedStep.num} with these additional instructions: ${instructions.trim()}`,
|
|
432
|
+
{ deliverAs: 'followUp' },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
} else if (choice === 'Re-plan') {
|
|
437
|
+
await enterPlanMode(ctx);
|
|
438
|
+
pi.sendUserMessage(
|
|
439
|
+
`Step ${blockedStep.num} was blocked: ${blockedStep.notes ?? 'no details'}. Re-analyze and create a revised plan.`,
|
|
440
|
+
{ deliverAs: 'followUp' },
|
|
441
|
+
);
|
|
442
|
+
return;
|
|
443
|
+
} else if (choice === 'Abort execution') {
|
|
444
|
+
await exitPlanMode(ctx);
|
|
445
|
+
return;
|
|
419
446
|
}
|
|
420
447
|
}
|
|
421
|
-
await updatePlansManifest(planName, 'in-progress', title);
|
|
422
|
-
persist();
|
|
423
|
-
} else if (match && planDir && path.endsWith('PLAN.md')) {
|
|
424
|
-
// planDir already set but PLAN.md just written — update title
|
|
425
|
-
try {
|
|
426
|
-
const content = await readFile(path, 'utf-8');
|
|
427
|
-
const title = extractPlanTitle(content);
|
|
428
|
-
await updatePlansManifest(match[1], 'in-progress', title);
|
|
429
|
-
} catch {
|
|
430
|
-
// Fall through
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
448
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (executing && todos.length > 0) {
|
|
439
|
-
if (todos.every((t) => t.completed)) {
|
|
440
|
-
// Mark plan as done in plans.json
|
|
449
|
+
// Check if all steps are resolved (done or skipped)
|
|
450
|
+
const allResolved = plan.steps.every((s) => s.status === 'done' || s.status === 'skipped');
|
|
451
|
+
if (allResolved) {
|
|
441
452
|
if (planDir) {
|
|
442
453
|
const planName = planDir.replace(/^\.plans\//, '');
|
|
443
|
-
await updatePlansManifest(planName, 'done');
|
|
454
|
+
await updatePlansManifest(planName, 'done', plan.title);
|
|
455
|
+
await savePlanToDisk();
|
|
444
456
|
}
|
|
445
457
|
|
|
446
|
-
const list =
|
|
458
|
+
const list = plan.steps
|
|
459
|
+
.map((s) => {
|
|
460
|
+
if (s.status === 'done') return `~~${s.description}~~`;
|
|
461
|
+
if (s.status === 'skipped') return `⊘ ~~${s.description}~~`;
|
|
462
|
+
return s.description;
|
|
463
|
+
})
|
|
464
|
+
.join('\n');
|
|
465
|
+
|
|
447
466
|
pi.sendMessage(
|
|
448
467
|
{
|
|
449
468
|
customType: 'plan-complete',
|
|
@@ -452,9 +471,11 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
|
|
|
452
471
|
},
|
|
453
472
|
{ triggerTurn: false },
|
|
454
473
|
);
|
|
474
|
+
|
|
455
475
|
executing = false;
|
|
456
|
-
|
|
476
|
+
plan = undefined;
|
|
457
477
|
planDir = undefined;
|
|
478
|
+
executionStartIdx = undefined;
|
|
458
479
|
pi.setActiveTools(EXEC_TOOLS);
|
|
459
480
|
if (previousModel) {
|
|
460
481
|
await switchModel(ctx, previousModel);
|
|
@@ -464,16 +485,15 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
|
|
|
464
485
|
}
|
|
465
486
|
updateUI(ctx);
|
|
466
487
|
persist();
|
|
488
|
+
return;
|
|
467
489
|
}
|
|
468
490
|
return;
|
|
469
491
|
}
|
|
470
492
|
|
|
471
493
|
if (!planEnabled || !ctx.hasUI) return;
|
|
494
|
+
if (!planDir || !plan) return;
|
|
472
495
|
|
|
473
|
-
//
|
|
474
|
-
if (!planDir) return;
|
|
475
|
-
|
|
476
|
-
// Show menu
|
|
496
|
+
// Show menu after plan submission
|
|
477
497
|
const choice = await ctx.ui.select('Plan ready — what next?', [
|
|
478
498
|
'Execute Plan',
|
|
479
499
|
'Refine Plan',
|
|
@@ -482,60 +502,21 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
|
|
|
482
502
|
]);
|
|
483
503
|
|
|
484
504
|
if (choice === 'Execute Plan') {
|
|
485
|
-
// Read START-PROMPT.md for clean context handoff
|
|
486
|
-
const startPromptPath = `${planDir}/START-PROMPT.md`;
|
|
487
|
-
const planMdPath = `${planDir}/PLAN.md`;
|
|
488
|
-
|
|
489
|
-
// Read the plan to extract todos
|
|
490
|
-
let planContent = '';
|
|
491
|
-
try {
|
|
492
|
-
planContent = await readFile(planMdPath, 'utf-8');
|
|
493
|
-
} catch {
|
|
494
|
-
// Fall through — will use empty plan content
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const extracted = extractTodoItems(planContent);
|
|
498
|
-
if (extracted.length > 0) {
|
|
499
|
-
todos = extracted;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Read the start prompt for clean handoff
|
|
503
|
-
let startPrompt = '';
|
|
504
|
-
try {
|
|
505
|
-
startPrompt = (await readFile(startPromptPath, 'utf-8')).trim();
|
|
506
|
-
} catch {
|
|
507
|
-
// Fall through
|
|
508
|
-
}
|
|
509
|
-
|
|
510
505
|
await startExecution(ctx);
|
|
511
506
|
updateUI(ctx);
|
|
512
507
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
} else {
|
|
523
|
-
// Fallback: ask executor to read the plan
|
|
524
|
-
pi.sendMessage(
|
|
525
|
-
{
|
|
526
|
-
customType: 'plan-mode-execute',
|
|
527
|
-
content: `Execute the plan in ${planMdPath}. Read it first, then execute step by step. Mark each step with [DONE:n] before moving to the next.`,
|
|
528
|
-
display: true,
|
|
529
|
-
},
|
|
530
|
-
{ triggerTurn: true, deliverAs: 'followUp' },
|
|
531
|
-
);
|
|
532
|
-
}
|
|
508
|
+
// Build execution prompt from structured plan data
|
|
509
|
+
const stepList = plan.steps
|
|
510
|
+
.map((s, i) => `${i + 1}. ${s.description}`)
|
|
511
|
+
.join('\n');
|
|
512
|
+
|
|
513
|
+
pi.sendUserMessage(
|
|
514
|
+
`Execute the following plan: "${plan.title}"\n\nSteps:\n${stepList}\n\nStart with step 1. Call update_step after completing each step.`,
|
|
515
|
+
{ deliverAs: 'followUp' },
|
|
516
|
+
);
|
|
533
517
|
} else if (choice === 'Refine Plan') {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
{
|
|
537
|
-
customType: 'plan-mode-refine',
|
|
538
|
-
content: `Review the plan you just created in ${planDir}/PLAN.md with an adversarial lens. Challenge assumptions, find gaps, identify risks, and look for:
|
|
518
|
+
pi.sendUserMessage(
|
|
519
|
+
`Review the plan you just created with an adversarial lens. Challenge assumptions, find gaps, identify risks, and look for:
|
|
539
520
|
|
|
540
521
|
- Missing edge cases or error handling
|
|
541
522
|
- Incorrect assumptions about the codebase
|
|
@@ -543,10 +524,8 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
|
|
|
543
524
|
- Missing dependencies between steps
|
|
544
525
|
- Simpler alternatives that were overlooked
|
|
545
526
|
|
|
546
|
-
After your review,
|
|
547
|
-
|
|
548
|
-
},
|
|
549
|
-
{ triggerTurn: true, deliverAs: 'followUp' },
|
|
527
|
+
After your review, call submit_plan again with the improved plan.`,
|
|
528
|
+
{ deliverAs: 'followUp' },
|
|
550
529
|
);
|
|
551
530
|
} else if (choice === 'Follow up') {
|
|
552
531
|
const followUp = await ctx.ui.editor('Follow-up instructions for the planner:', '');
|
|
@@ -560,7 +539,6 @@ After your review, update PLAN.md and START-PROMPT.md with any improvements.`,
|
|
|
560
539
|
|
|
561
540
|
// ── Restore state on session start/resume ─────────────────────────────────
|
|
562
541
|
pi.on('session_start', async (_event, ctx) => {
|
|
563
|
-
// Check CLI flag
|
|
564
542
|
if (pi.getFlag('plan') === true) {
|
|
565
543
|
planEnabled = true;
|
|
566
544
|
}
|
|
@@ -578,33 +556,8 @@ After your review, update PLAN.md and START-PROMPT.md with any improvements.`,
|
|
|
578
556
|
planEnabled = saved.data.planEnabled ?? planEnabled;
|
|
579
557
|
executing = saved.data.executing ?? executing;
|
|
580
558
|
planDir = saved.data.planDir ?? planDir;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// Re-scan [DONE:n] markers on resume
|
|
585
|
-
if (executing && todos.length > 0) {
|
|
586
|
-
let execIdx = -1;
|
|
587
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
588
|
-
const entry = entries[i] as { type: string; customType?: string };
|
|
589
|
-
if (entry.customType === 'plan-mode-execute') {
|
|
590
|
-
execIdx = i;
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const messages: AssistantMessage[] = [];
|
|
596
|
-
for (let i = execIdx + 1; i < entries.length; i++) {
|
|
597
|
-
const entry = entries[i];
|
|
598
|
-
if (
|
|
599
|
-
entry.type === 'message' &&
|
|
600
|
-
'message' in entry &&
|
|
601
|
-
isAssistantMessage(entry.message as AgentMessage)
|
|
602
|
-
) {
|
|
603
|
-
messages.push(entry.message as AssistantMessage);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
const allText = messages.map(getTextContent).join('\n');
|
|
607
|
-
markCompletedSteps(allText, todos);
|
|
559
|
+
plan = saved.data.plan ?? plan;
|
|
560
|
+
executionStartIdx = saved.data.executionStartIdx ?? executionStartIdx;
|
|
608
561
|
}
|
|
609
562
|
|
|
610
563
|
// Apply tool restrictions, model, and thinking level
|