@dreki-gg/pi-plan-mode 0.2.0 → 0.3.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.
- package/CHANGELOG.md +20 -0
- package/README.md +44 -70
- package/extensions/plan-mode/index.ts +463 -548
- package/extensions/plan-mode/utils.ts +26 -11
- package/package.json +12 -4
|
@@ -1,664 +1,579 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Extension
|
|
3
|
+
*
|
|
4
|
+
* Two-phase workflow:
|
|
5
|
+
* 1. PLAN phase — read-only tools (+ edit/write for .plans/ only) + medium thinking
|
|
6
|
+
* Planner analyzes codebase, asks questions, writes PLAN.md + START-PROMPT.md
|
|
7
|
+
* 2. EXECUTE phase — full tools + low thinking, clean context from START-PROMPT.md
|
|
8
|
+
* Executor works through the plan step by step with [DONE:n] tracking
|
|
9
|
+
*
|
|
10
|
+
* Plans live in `.plans/<kebab-name>/PLAN.md` with a `START-PROMPT.md` sibling for clean handoff.
|
|
11
|
+
*
|
|
12
|
+
* Commands:
|
|
13
|
+
* /plan [prompt] — enter plan mode (optionally with a starting prompt)
|
|
14
|
+
* /todos — show current plan progress
|
|
15
|
+
* Ctrl+Alt+P — toggle plan mode (shortcut)
|
|
16
|
+
*
|
|
17
|
+
* Flag:
|
|
18
|
+
* --plan — start session in plan mode
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { AgentMessage } from '@earendil-works/pi-agent-core';
|
|
22
|
+
import type { AssistantMessage, TextContent } from '@earendil-works/pi-ai';
|
|
23
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
24
|
+
import { Key } from '@earendil-works/pi-tui';
|
|
3
25
|
import {
|
|
4
26
|
extractTodoItems,
|
|
5
|
-
formatTodoList,
|
|
6
27
|
isSafeCommand,
|
|
7
28
|
markCompletedSteps,
|
|
8
29
|
type TodoItem,
|
|
9
30
|
} from './utils.js';
|
|
10
31
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
phase: WorkflowPhase;
|
|
15
|
-
todos: TodoItem[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface BeforeAgentStartCompatEvent {
|
|
19
|
-
systemPromptOptions?: {
|
|
20
|
-
selectedTools?: string[];
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const STATE_ENTRY = 'plan-mode-state';
|
|
25
|
-
const CLEAR_VALUES = new Set(['', 'off', 'none', 'disable', 'exit']);
|
|
26
|
-
const PLANNING_TOOL_PATTERNS = [
|
|
32
|
+
// ── Tool sets ────────────────────────────────────────────────────────────────
|
|
33
|
+
// Plan phase: read-only + edit/write (for .plans/ files only, enforced by prompt)
|
|
34
|
+
const PLAN_TOOLS = [
|
|
27
35
|
'read',
|
|
28
36
|
'bash',
|
|
29
37
|
'grep',
|
|
30
38
|
'find',
|
|
31
39
|
'ls',
|
|
40
|
+
'edit',
|
|
41
|
+
'write',
|
|
32
42
|
'questionnaire',
|
|
33
|
-
'
|
|
34
|
-
]
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
function isAssistantMessage(message: unknown): message is {
|
|
38
|
-
role: 'assistant';
|
|
39
|
-
content: Array<{ type: string; text?: string }>;
|
|
40
|
-
} {
|
|
41
|
-
return (
|
|
42
|
-
typeof message === 'object' &&
|
|
43
|
-
message !== null &&
|
|
44
|
-
'role' in message &&
|
|
45
|
-
(message as { role?: unknown }).role === 'assistant' &&
|
|
46
|
-
'content' in message &&
|
|
47
|
-
Array.isArray((message as { content?: unknown }).content)
|
|
48
|
-
);
|
|
49
|
-
}
|
|
43
|
+
'search_skills',
|
|
44
|
+
];
|
|
45
|
+
const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'search_skills'];
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.filter(
|
|
55
|
-
(block): block is { type: 'text'; text: string } =>
|
|
56
|
-
block.type === 'text' && typeof block.text === 'string',
|
|
57
|
-
)
|
|
58
|
-
.map((block) => block.text)
|
|
59
|
-
.join('\n');
|
|
60
|
-
}
|
|
47
|
+
// ── Model + thinking presets ─────────────────────────────────────────────────
|
|
48
|
+
const PLAN_MODEL = { provider: 'anthropic', id: 'claude-opus-4-6' } as const;
|
|
49
|
+
const PLAN_THINKING = 'medium' as const;
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return name === pattern;
|
|
65
|
-
}
|
|
51
|
+
const EXEC_MODEL = { provider: 'openai', id: 'gpt-5.5' } as const;
|
|
52
|
+
const EXEC_THINKING = 'low' as const;
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
return allToolNames.filter((name) =>
|
|
69
|
-
patterns.some(
|
|
70
|
-
(pattern) =>
|
|
71
|
-
matchesToolPattern(name, pattern) ||
|
|
72
|
-
(pattern === 'context7_*' && name.startsWith('context7_')),
|
|
73
|
-
),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
54
|
+
type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const data = entry.data as PersistedState | undefined;
|
|
84
|
-
if (data && typeof data.phase === 'string' && Array.isArray(data.todos)) lastState = data;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return lastState;
|
|
56
|
+
// ── Persisted state ──────────────────────────────────────────────────────────
|
|
57
|
+
interface PersistedState {
|
|
58
|
+
planEnabled: boolean;
|
|
59
|
+
executing: boolean;
|
|
60
|
+
planDir: string | undefined;
|
|
61
|
+
todos: TodoItem[];
|
|
88
62
|
}
|
|
89
63
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
todos: TodoItem[],
|
|
94
|
-
): string {
|
|
95
|
-
const sections = [
|
|
96
|
-
'PLAN MODE ACTIVE.',
|
|
97
|
-
'Run a Cursor-style planning pass before implementation.',
|
|
98
|
-
`Enabled tools: ${activeTools.length > 0 ? activeTools.join(', ') : '(none)'}`,
|
|
99
|
-
'Rules:',
|
|
100
|
-
'- Stay read-only. Do not edit files or claim to have changed code.',
|
|
101
|
-
'- Inspect the real codebase before proposing work. Do not guess from filenames.',
|
|
102
|
-
'- If requirements are underspecified and the questionnaire tool is available, use it to ask 1-5 structured clarifying questions before finalizing the plan.',
|
|
103
|
-
'- Produce a concrete numbered plan under a `Plan:` header.',
|
|
104
|
-
'- Include open questions, assumptions, risks, and sequencing when relevant.',
|
|
105
|
-
'- If the user wants terminology hardened or the design pressure-tested, point them to the domain-model workflow.',
|
|
106
|
-
'- If the user wants self-contained handoff files, point them to the implementation-plan workflow.',
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
if (ctx.hasUI) {
|
|
110
|
-
sections.push(
|
|
111
|
-
'After the planning response, the extension may offer next-step choices like domain-model review, implementation-plan generation, or execution.',
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (todos.length > 0) {
|
|
116
|
-
sections.push(`Current plan draft:\n${formatTodoList(todos)}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return sections.join('\n');
|
|
64
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
65
|
+
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
66
|
+
return m.role === 'assistant' && Array.isArray(m.content);
|
|
120
67
|
}
|
|
121
68
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
'Do not implement product code in this phase.',
|
|
128
|
-
'Ground every plan file in the current codebase state.',
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
if (todos.length > 0) {
|
|
132
|
-
sections.push(`Current approved plan:\n${formatTodoList(todos)}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return sections.join('\n');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function buildExecutionInstructions(todos: TodoItem[]): string {
|
|
139
|
-
const sections = [
|
|
140
|
-
'PLAN EXECUTION MODE ACTIVE.',
|
|
141
|
-
'Execute the approved plan in small verified steps.',
|
|
142
|
-
'After completing a plan step, include a `[DONE:n]` tag in the response so progress can be tracked.',
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
if (todos.length > 0) {
|
|
146
|
-
sections.push(`Current checklist:\n${formatTodoList(todos)}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return sections.join('\n');
|
|
69
|
+
function getTextContent(message: AssistantMessage): string {
|
|
70
|
+
return message.content
|
|
71
|
+
.filter((b): b is TextContent => b.type === 'text')
|
|
72
|
+
.map((b) => b.text)
|
|
73
|
+
.join('\n');
|
|
150
74
|
}
|
|
151
75
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
let
|
|
156
|
-
let
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const query = argumentText.trim().toLowerCase();
|
|
162
|
-
const items: AutocompleteItem[] = [
|
|
163
|
-
{
|
|
164
|
-
value: 'status',
|
|
165
|
-
label: 'status',
|
|
166
|
-
description: `Show the current plan workflow state (currently ${phase})`,
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
value: 'domain',
|
|
170
|
-
label: 'domain',
|
|
171
|
-
description: 'Stress-test the current plan with a domain-model review',
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
value: 'plans',
|
|
175
|
-
label: 'plans',
|
|
176
|
-
description: 'Generate self-contained implementation plan files',
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
value: 'execute',
|
|
180
|
-
label: 'execute',
|
|
181
|
-
description: 'Leave read-only planning and execute the approved plan',
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
value: 'off',
|
|
185
|
-
label: 'off',
|
|
186
|
-
description: 'Disable plan mode',
|
|
187
|
-
},
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
const filtered = items.filter((item) => {
|
|
191
|
-
if (!query) return true;
|
|
192
|
-
return (
|
|
193
|
-
item.value.toLowerCase().startsWith(query) ||
|
|
194
|
-
item.label?.toLowerCase().includes(query) ||
|
|
195
|
-
item.description?.toLowerCase().includes(query)
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
return filtered.length > 0 ? filtered : null;
|
|
200
|
-
}
|
|
76
|
+
// ── Extension ────────────────────────────────────────────────────────────────
|
|
77
|
+
export default function planMode(pi: ExtensionAPI): void {
|
|
78
|
+
let planEnabled = false;
|
|
79
|
+
let executing = false;
|
|
80
|
+
let planDir: string | undefined;
|
|
81
|
+
let todos: TodoItem[] = [];
|
|
82
|
+
let previousThinking: ThinkingLevel | undefined;
|
|
83
|
+
let previousModel: { provider: string; id: string } | undefined;
|
|
201
84
|
|
|
85
|
+
// ── Flag ──────────────────────────────────────────────────────────────────
|
|
202
86
|
pi.registerFlag('plan', {
|
|
203
|
-
description: 'Start in plan mode (
|
|
87
|
+
description: 'Start in plan mode (read-only + medium thinking)',
|
|
204
88
|
type: 'boolean',
|
|
205
89
|
default: false,
|
|
206
90
|
});
|
|
207
91
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
92
|
+
// ── State persistence ─────────────────────────────────────────────────────
|
|
93
|
+
function persist(): void {
|
|
94
|
+
pi.appendEntry<PersistedState>('plan-mode', {
|
|
95
|
+
planEnabled,
|
|
96
|
+
executing,
|
|
97
|
+
planDir,
|
|
98
|
+
todos,
|
|
212
99
|
});
|
|
213
100
|
}
|
|
214
101
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
ctx.ui.setWidget('plan-mode-todos', undefined);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
102
|
+
// ── UI updates ────────────────────────────────────────────────────────────
|
|
103
|
+
function updateUI(ctx: ExtensionContext): void {
|
|
104
|
+
const { theme } = ctx.ui;
|
|
221
105
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
106
|
+
if (executing && todos.length > 0) {
|
|
107
|
+
const done = todos.filter((t) => t.completed).length;
|
|
108
|
+
ctx.ui.setStatus('plan-mode', theme.fg('accent', `📋 exec ${done}/${todos.length}`));
|
|
109
|
+
} else if (planEnabled) {
|
|
110
|
+
ctx.ui.setStatus('plan-mode', theme.fg('warning', '📝 plan'));
|
|
226
111
|
} else {
|
|
227
|
-
|
|
228
|
-
const total = todoItems.length;
|
|
229
|
-
ctx.ui.setStatus(
|
|
230
|
-
'plan-mode',
|
|
231
|
-
ctx.ui.theme.fg('success', total > 0 ? `plan:exec ${completed}/${total}` : 'plan:exec'),
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (todoItems.length === 0) {
|
|
236
|
-
ctx.ui.setWidget('plan-mode-todos', undefined);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const lines = todoItems.map((item) => {
|
|
241
|
-
if (item.completed) {
|
|
242
|
-
return ctx.ui.theme.fg('success', `☑ ${ctx.ui.theme.strikethrough(item.text)}`);
|
|
243
|
-
}
|
|
244
|
-
const marker = phase === 'executing' ? '☐' : '•';
|
|
245
|
-
return `${ctx.ui.theme.fg('muted', `${marker} `)}${item.text}`;
|
|
246
|
-
});
|
|
247
|
-
ctx.ui.setWidget('plan-mode-todos', lines);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function ensureRestoreTools() {
|
|
251
|
-
if (restoreToolNames) return;
|
|
252
|
-
restoreToolNames = [...pi.getActiveTools()];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function applyPhaseTools(targetPhase: WorkflowPhase) {
|
|
256
|
-
const allToolNames = pi.getAllTools().map((tool) => tool.name);
|
|
257
|
-
|
|
258
|
-
if (targetPhase === 'planning') {
|
|
259
|
-
const resolved = resolveToolNames(allToolNames, [...PLANNING_TOOL_PATTERNS, 'context7_*']);
|
|
260
|
-
if (resolved.length > 0) pi.setActiveTools(resolved);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (targetPhase === 'plan-files') {
|
|
265
|
-
const resolved = resolveToolNames(allToolNames, [...PLAN_FILE_TOOL_PATTERNS, 'context7_*']);
|
|
266
|
-
if (resolved.length > 0) pi.setActiveTools(resolved);
|
|
267
|
-
return;
|
|
112
|
+
ctx.ui.setStatus('plan-mode', undefined);
|
|
268
113
|
}
|
|
269
114
|
|
|
270
|
-
if (
|
|
271
|
-
|
|
115
|
+
if (executing && todos.length > 0) {
|
|
116
|
+
const lines = todos.map((item) => {
|
|
117
|
+
if (item.completed) {
|
|
118
|
+
return theme.fg('success', '☑ ') + theme.fg('muted', theme.strikethrough(item.text));
|
|
119
|
+
}
|
|
120
|
+
return `${theme.fg('muted', '☐ ')}${item.text}`;
|
|
121
|
+
});
|
|
122
|
+
ctx.ui.setWidget('plan-todos', lines);
|
|
123
|
+
} else {
|
|
124
|
+
ctx.ui.setWidget('plan-todos', undefined);
|
|
272
125
|
}
|
|
273
126
|
}
|
|
274
127
|
|
|
275
|
-
|
|
276
|
-
|
|
128
|
+
// ── Model switching ───────────────────────────────────────────────────────
|
|
129
|
+
async function switchModel(
|
|
277
130
|
ctx: ExtensionContext,
|
|
278
|
-
|
|
279
|
-
) {
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
} else {
|
|
285
|
-
applyPhaseTools(nextPhase);
|
|
131
|
+
preset: { provider: string; id: string },
|
|
132
|
+
): Promise<boolean> {
|
|
133
|
+
const model = ctx.modelRegistry.find(preset.provider, preset.id);
|
|
134
|
+
if (!model) {
|
|
135
|
+
ctx.ui.notify(`Model ${preset.provider}/${preset.id} not found`, 'error');
|
|
136
|
+
return false;
|
|
286
137
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (options?.notify) ctx.ui.notify(options.notify, 'info');
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function sendPlanningPrompt(prompt: string, ctx: ExtensionContext) {
|
|
296
|
-
const commandCtx = ctx as ExtensionContext & { waitForIdle?: () => Promise<void> };
|
|
297
|
-
if (typeof commandCtx.waitForIdle === 'function') {
|
|
298
|
-
await commandCtx.waitForIdle();
|
|
138
|
+
const ok = await pi.setModel(model);
|
|
139
|
+
if (!ok) {
|
|
140
|
+
ctx.ui.notify(`No API key for ${preset.provider}/${preset.id}`, 'error');
|
|
141
|
+
return false;
|
|
299
142
|
}
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function availableCommand(name: string): string | undefined {
|
|
304
|
-
const commands = pi.getCommands?.() ?? [];
|
|
305
|
-
return commands.find((command) => command.name === name)?.name;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function startPlanning(prompt: string | undefined, ctx: ExtensionContext) {
|
|
309
|
-
setPhase('planning', ctx, {
|
|
310
|
-
notify:
|
|
311
|
-
phase === 'off'
|
|
312
|
-
? 'Plan mode enabled. Ask pi to inspect, question, and plan before implementation.'
|
|
313
|
-
: undefined,
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const trimmed = prompt?.trim();
|
|
317
|
-
if (!trimmed) return;
|
|
318
|
-
await sendPlanningPrompt(trimmed, ctx);
|
|
143
|
+
return true;
|
|
319
144
|
}
|
|
320
145
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
await sendPlanningPrompt(`/${skillCommand} ${suffix}`.trim(), ctx);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
await sendPlanningPrompt(promptText, ctx);
|
|
146
|
+
// ── Phase transitions ─────────────────────────────────────────────────────
|
|
147
|
+
async function enterPlanMode(ctx: ExtensionContext): Promise<void> {
|
|
148
|
+
planEnabled = true;
|
|
149
|
+
executing = false;
|
|
150
|
+
planDir = undefined;
|
|
151
|
+
todos = [];
|
|
152
|
+
previousThinking = pi.getThinkingLevel() as ThinkingLevel;
|
|
153
|
+
previousModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
154
|
+
pi.setActiveTools(PLAN_TOOLS);
|
|
155
|
+
await switchModel(ctx, PLAN_MODEL);
|
|
156
|
+
pi.setThinkingLevel(PLAN_THINKING);
|
|
157
|
+
ctx.ui.notify(
|
|
158
|
+
`Plan mode ON — ${PLAN_MODEL.provider}/${PLAN_MODEL.id}:${PLAN_THINKING}`,
|
|
159
|
+
'info',
|
|
160
|
+
);
|
|
161
|
+
updateUI(ctx);
|
|
162
|
+
persist();
|
|
343
163
|
}
|
|
344
164
|
|
|
345
|
-
async function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
todoItems.length > 0 ? formatTodoList(todoItems) : 'No extracted plan steps yet.';
|
|
354
|
-
const trimmedArgs = args?.trim();
|
|
355
|
-
const promptText =
|
|
356
|
-
trimmedArgs && trimmedArgs.length > 0
|
|
357
|
-
? trimmedArgs
|
|
358
|
-
: `Create self-contained implementation plan files for the current approved plan. Ground every file in the real codebase, document exact APIs and file paths, and write the plans to docs/plans unless the repo or user prefers another location.\n\nCurrent plan:\n${planText}`;
|
|
359
|
-
const skillCommand = availableCommand('skill:create-implementation-plans');
|
|
360
|
-
|
|
361
|
-
if (skillCommand) {
|
|
362
|
-
const suffix =
|
|
363
|
-
trimmedArgs && trimmedArgs.length > 0
|
|
364
|
-
? trimmedArgs
|
|
365
|
-
: `Create self-contained implementation plan files for the current approved plan.\n\nCurrent plan:\n${planText}`;
|
|
366
|
-
await sendPlanningPrompt(`/${skillCommand} ${suffix}`.trim(), ctx);
|
|
367
|
-
return;
|
|
165
|
+
async function exitPlanMode(ctx: ExtensionContext): Promise<void> {
|
|
166
|
+
planEnabled = false;
|
|
167
|
+
executing = false;
|
|
168
|
+
planDir = undefined;
|
|
169
|
+
todos = [];
|
|
170
|
+
pi.setActiveTools(EXEC_TOOLS);
|
|
171
|
+
if (previousModel) {
|
|
172
|
+
await switchModel(ctx, previousModel);
|
|
368
173
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function startExecution(args: string | undefined, ctx: ExtensionContext) {
|
|
374
|
-
setPhase('executing', ctx, {
|
|
375
|
-
notify: 'Plan execution mode enabled. Full tool access restored.',
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const trimmedArgs = args?.trim();
|
|
379
|
-
if (trimmedArgs && trimmedArgs.length > 0) {
|
|
380
|
-
await sendPlanningPrompt(trimmedArgs, ctx);
|
|
381
|
-
return;
|
|
174
|
+
if (previousThinking) {
|
|
175
|
+
pi.setThinkingLevel(previousThinking);
|
|
382
176
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
? `Execute the approved plan in order.\n\nPlan:\n${formatTodoList(todoItems)}\n\nStart with step 1 and include [DONE:n] tags as each step is completed.`
|
|
387
|
-
: 'Execute the approved plan in small verified steps. Include [DONE:n] tags as each major plan step is completed.';
|
|
388
|
-
await sendPlanningPrompt(defaultPrompt, ctx);
|
|
177
|
+
ctx.ui.notify('Plan mode OFF — original model restored', 'info');
|
|
178
|
+
updateUI(ctx);
|
|
179
|
+
persist();
|
|
389
180
|
}
|
|
390
181
|
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
ctx
|
|
182
|
+
async function startExecution(ctx: ExtensionContext): Promise<void> {
|
|
183
|
+
planEnabled = false;
|
|
184
|
+
executing = true;
|
|
185
|
+
pi.setActiveTools(EXEC_TOOLS);
|
|
186
|
+
await switchModel(ctx, EXEC_MODEL);
|
|
187
|
+
pi.setThinkingLevel(EXEC_THINKING);
|
|
188
|
+
ctx.ui.notify(
|
|
189
|
+
`Executing plan — ${EXEC_MODEL.provider}/${EXEC_MODEL.id}:${EXEC_THINKING}`,
|
|
190
|
+
'info',
|
|
191
|
+
);
|
|
192
|
+
updateUI(ctx);
|
|
193
|
+
persist();
|
|
402
194
|
}
|
|
403
195
|
|
|
404
|
-
async function
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const choice = await ctx.ui.select('Plan mode', [
|
|
411
|
-
'Stay in planning mode',
|
|
412
|
-
'Stress-test with domain-model',
|
|
413
|
-
'Generate implementation plan files',
|
|
414
|
-
'Execute current plan',
|
|
415
|
-
'Show status',
|
|
416
|
-
'Disable plan mode',
|
|
417
|
-
]);
|
|
418
|
-
|
|
419
|
-
if (!choice || choice === 'Stay in planning mode') return;
|
|
420
|
-
if (choice === 'Stress-test with domain-model') {
|
|
421
|
-
await runDomainWorkflow(undefined, ctx);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
if (choice === 'Generate implementation plan files') {
|
|
425
|
-
await runPlanFileWorkflow(undefined, ctx);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (choice === 'Execute current plan') {
|
|
429
|
-
await startExecution(undefined, ctx);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (choice === 'Show status') {
|
|
433
|
-
showStatus(ctx);
|
|
434
|
-
return;
|
|
196
|
+
async function togglePlanMode(ctx: ExtensionContext): Promise<void> {
|
|
197
|
+
if (planEnabled || executing) {
|
|
198
|
+
await exitPlanMode(ctx);
|
|
199
|
+
} else {
|
|
200
|
+
await enterPlanMode(ctx);
|
|
435
201
|
}
|
|
436
|
-
|
|
437
|
-
setPhase('off', ctx, { notify: 'Plan mode disabled.' });
|
|
438
202
|
}
|
|
439
203
|
|
|
204
|
+
// ── Commands ──────────────────────────────────────────────────────────────
|
|
440
205
|
pi.registerCommand('plan', {
|
|
441
|
-
description: '
|
|
442
|
-
getArgumentCompletions: getPlanCommandCompletions,
|
|
206
|
+
description: 'Enter plan mode, optionally with a starting prompt',
|
|
443
207
|
handler: async (args, ctx) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (phase === 'off') {
|
|
447
|
-
await startPlanning(undefined, ctx);
|
|
448
|
-
} else {
|
|
449
|
-
await showPlanMenu(ctx);
|
|
450
|
-
}
|
|
208
|
+
if (planEnabled || executing) {
|
|
209
|
+
await togglePlanMode(ctx);
|
|
451
210
|
return;
|
|
452
211
|
}
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
return;
|
|
212
|
+
await enterPlanMode(ctx);
|
|
213
|
+
const prompt = args?.trim();
|
|
214
|
+
if (prompt) {
|
|
215
|
+
pi.sendUserMessage(prompt);
|
|
458
216
|
}
|
|
459
|
-
if (lower === 'status') {
|
|
460
|
-
showStatus(ctx);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (lower === 'domain') {
|
|
464
|
-
await runDomainWorkflow(undefined, ctx);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
if (lower === 'plans' || lower === 'files') {
|
|
468
|
-
await runPlanFileWorkflow(undefined, ctx);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (lower === 'execute' || lower === 'run') {
|
|
472
|
-
await startExecution(undefined, ctx);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
await startPlanning(raw, ctx);
|
|
477
217
|
},
|
|
478
218
|
});
|
|
479
219
|
|
|
480
|
-
pi.registerCommand('
|
|
481
|
-
description: 'Show current plan
|
|
220
|
+
pi.registerCommand('todos', {
|
|
221
|
+
description: 'Show current plan progress',
|
|
482
222
|
handler: async (_args, ctx) => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
pi.registerCommand('plan-plans', {
|
|
495
|
-
description: 'Generate self-contained implementation plan files for the current plan',
|
|
496
|
-
handler: async (args, ctx) => {
|
|
497
|
-
await runPlanFileWorkflow(args, ctx);
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
pi.registerCommand('plan-execute', {
|
|
502
|
-
description: 'Leave read-only planning and execute the current approved plan',
|
|
503
|
-
handler: async (args, ctx) => {
|
|
504
|
-
await startExecution(args, ctx);
|
|
223
|
+
if (todos.length === 0) {
|
|
224
|
+
ctx.ui.notify('No plan yet. Use /plan to start planning.', 'info');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const list = todos
|
|
228
|
+
.map((t, i) => `${i + 1}. ${t.completed ? '✓' : '○'} ${t.text}`)
|
|
229
|
+
.join('\n');
|
|
230
|
+
ctx.ui.notify(`Plan Progress:\n${list}`, 'info');
|
|
505
231
|
},
|
|
506
232
|
});
|
|
507
233
|
|
|
508
234
|
pi.registerShortcut(Key.ctrlAlt('p'), {
|
|
509
235
|
description: 'Toggle plan mode',
|
|
510
|
-
handler: async (ctx) =>
|
|
511
|
-
if (phase === 'off') setPhase('planning', ctx, { notify: 'Plan mode enabled.' });
|
|
512
|
-
else setPhase('off', ctx, { notify: 'Plan mode disabled.' });
|
|
513
|
-
},
|
|
236
|
+
handler: async (ctx) => togglePlanMode(ctx),
|
|
514
237
|
});
|
|
515
238
|
|
|
239
|
+
// ── Block destructive bash in plan mode ───────────────────────────────────
|
|
516
240
|
pi.on('tool_call', async (event) => {
|
|
517
|
-
if (
|
|
241
|
+
if (!planEnabled) return;
|
|
242
|
+
|
|
243
|
+
// Block bash commands that aren't on the safe allowlist
|
|
244
|
+
if (event.toolName === 'bash') {
|
|
245
|
+
const command = event.input.command as string;
|
|
246
|
+
if (!isSafeCommand(command)) {
|
|
247
|
+
return {
|
|
248
|
+
block: true,
|
|
249
|
+
reason: `Plan mode: command blocked. Use /plan to exit plan mode first.\nCommand: ${command}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
518
253
|
|
|
519
|
-
|
|
520
|
-
if (
|
|
254
|
+
// Block edit/write to paths outside .plans/
|
|
255
|
+
if (event.toolName === 'edit' || event.toolName === 'write') {
|
|
256
|
+
const path = (event.input as { path?: string }).path ?? '';
|
|
257
|
+
if (!path.startsWith('.plans/') && !path.startsWith('.plans\\')) {
|
|
258
|
+
return {
|
|
259
|
+
block: true,
|
|
260
|
+
reason: `Plan mode: file modifications are restricted to .plans/ directory.\nPath: ${path}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
521
265
|
|
|
266
|
+
// ── Filter stale plan context when not planning ───────────────────────────
|
|
267
|
+
pi.on('context', async (event) => {
|
|
268
|
+
if (planEnabled) return;
|
|
522
269
|
return {
|
|
523
|
-
|
|
524
|
-
|
|
270
|
+
messages: event.messages.filter((m) => {
|
|
271
|
+
const msg = m as AgentMessage & { customType?: string };
|
|
272
|
+
if (msg.customType === 'plan-mode-context') return false;
|
|
273
|
+
if (msg.role !== 'user') return true;
|
|
274
|
+
const content = msg.content;
|
|
275
|
+
if (typeof content === 'string') {
|
|
276
|
+
return !content.includes('[PLAN MODE ACTIVE]');
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(content)) {
|
|
279
|
+
return !content.some(
|
|
280
|
+
(c) => c.type === 'text' && (c as TextContent).text?.includes('[PLAN MODE ACTIVE]'),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}),
|
|
525
285
|
};
|
|
526
286
|
});
|
|
527
287
|
|
|
528
|
-
|
|
529
|
-
|
|
288
|
+
// ── Inject context for each phase ─────────────────────────────────────────
|
|
289
|
+
pi.on('before_agent_start', async () => {
|
|
290
|
+
if (planEnabled) {
|
|
291
|
+
return {
|
|
292
|
+
message: {
|
|
293
|
+
customType: 'plan-mode-context',
|
|
294
|
+
content: `[PLAN MODE ACTIVE]
|
|
295
|
+
You are in plan mode — a planning phase with strict bash restrictions.
|
|
530
296
|
|
|
531
|
-
|
|
532
|
-
|
|
297
|
+
Restrictions:
|
|
298
|
+
- Available tools: ${PLAN_TOOLS.join(', ')}
|
|
299
|
+
- Bash is restricted to read-only commands (ls, grep, git status, etc.)
|
|
300
|
+
- edit and write are ONLY allowed for files inside the \`.plans/\` directory
|
|
533
301
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
302
|
+
Your task:
|
|
303
|
+
1. Analyze the codebase thoroughly using the available read-only tools
|
|
304
|
+
2. Ask clarifying questions if needed (use the questionnaire tool)
|
|
305
|
+
3. Produce a detailed, concrete plan
|
|
306
|
+
|
|
307
|
+
When you are ready to finalize the plan:
|
|
308
|
+
1. Choose a short descriptive kebab-case name for the plan (e.g. "add-auth-middleware")
|
|
309
|
+
2. Create \`.plans/<plan-name>/PLAN.md\` with the full numbered plan under a \`Plan:\` header:
|
|
310
|
+
|
|
311
|
+
\`\`\`markdown
|
|
312
|
+
# <Plan Title>
|
|
313
|
+
|
|
314
|
+
<Brief description of what this plan accomplishes>
|
|
315
|
+
|
|
316
|
+
## Context
|
|
317
|
+
<Key findings from codebase analysis>
|
|
318
|
+
|
|
319
|
+
## Plan:
|
|
320
|
+
1. First step — what to change and where
|
|
321
|
+
2. Second step — what to change and where
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
## Risks / Open Questions
|
|
325
|
+
<Any concerns or assumptions>
|
|
326
|
+
\`\`\`
|
|
327
|
+
|
|
328
|
+
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:
|
|
329
|
+
- Complete context about the codebase (relevant file paths, APIs, patterns)
|
|
330
|
+
- The full plan steps to execute
|
|
331
|
+
- Any critical constraints or gotchas
|
|
332
|
+
- Clear instructions to mark each step done with \`[DONE:n]\` tags
|
|
333
|
+
|
|
334
|
+
The START-PROMPT.md is critical — it must be thorough enough that an implementor with zero prior context can execute the plan correctly.
|
|
335
|
+
|
|
336
|
+
If you need supporting reference files for extra context (code snippets, diagrams, specs), place them alongside in the same \`.plans/<plan-name>/\` directory.
|
|
337
|
+
|
|
338
|
+
Do NOT attempt to make product code changes — only create planning artifacts in \`.plans/\`.`,
|
|
339
|
+
display: false,
|
|
340
|
+
},
|
|
537
341
|
};
|
|
538
342
|
}
|
|
539
343
|
|
|
540
|
-
if (
|
|
344
|
+
if (executing && todos.length > 0) {
|
|
345
|
+
const remaining = todos.filter((t) => !t.completed);
|
|
346
|
+
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join('\n');
|
|
541
347
|
return {
|
|
542
|
-
|
|
348
|
+
message: {
|
|
349
|
+
customType: 'plan-execution-context',
|
|
350
|
+
content: `[EXECUTING PLAN — Full tool access enabled]
|
|
351
|
+
|
|
352
|
+
Remaining steps:
|
|
353
|
+
${todoList}
|
|
354
|
+
|
|
355
|
+
Execute each step in order. You MUST include [DONE:n] in your response after completing each step before moving to the next one.`,
|
|
356
|
+
display: false,
|
|
357
|
+
},
|
|
543
358
|
};
|
|
544
359
|
}
|
|
545
|
-
|
|
546
|
-
return {
|
|
547
|
-
systemPrompt: `${event.systemPrompt}\n\n${buildExecutionInstructions(todoItems)}`,
|
|
548
|
-
};
|
|
549
360
|
});
|
|
550
361
|
|
|
362
|
+
// ── Track [DONE:n] markers during execution ───────────────────────────────
|
|
551
363
|
pi.on('turn_end', async (event, ctx) => {
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
if (!text) return;
|
|
364
|
+
if (!executing || todos.length === 0) return;
|
|
365
|
+
if (!isAssistantMessage(event.message)) return;
|
|
555
366
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
367
|
+
const text = getTextContent(event.message);
|
|
368
|
+
if (markCompletedSteps(text, todos) > 0) {
|
|
369
|
+
updateUI(ctx);
|
|
559
370
|
}
|
|
371
|
+
persist();
|
|
560
372
|
});
|
|
561
373
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
374
|
+
// ── Detect plan directory from written files ──────────────────────────────
|
|
375
|
+
pi.on('tool_result', async (event) => {
|
|
376
|
+
if (!planEnabled) return;
|
|
377
|
+
if (event.toolName !== 'write' && event.toolName !== 'edit') return;
|
|
378
|
+
if (event.isError) return;
|
|
379
|
+
|
|
380
|
+
const path = (event.input as { path?: string }).path;
|
|
381
|
+
if (!path) return;
|
|
382
|
+
|
|
383
|
+
// Detect .plans/<name>/ directory from written files
|
|
384
|
+
const match = path.match(/\.plans\/([^/]+)\//);
|
|
385
|
+
if (match && !planDir) {
|
|
386
|
+
planDir = `.plans/${match[1]}`;
|
|
387
|
+
persist();
|
|
566
388
|
}
|
|
389
|
+
});
|
|
567
390
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
391
|
+
// ── After agent finishes: prompt for next action ──────────────────────────
|
|
392
|
+
pi.on('agent_end', async (event, ctx) => {
|
|
393
|
+
// Check execution completion
|
|
394
|
+
if (executing && todos.length > 0) {
|
|
395
|
+
if (todos.every((t) => t.completed)) {
|
|
396
|
+
const list = todos.map((t) => `~~${t.text}~~`).join('\n');
|
|
397
|
+
pi.sendMessage(
|
|
398
|
+
{
|
|
399
|
+
customType: 'plan-complete',
|
|
400
|
+
content: `**Plan Complete!** ✓\n\n${list}`,
|
|
401
|
+
display: true,
|
|
402
|
+
},
|
|
403
|
+
{ triggerTurn: false },
|
|
404
|
+
);
|
|
405
|
+
executing = false;
|
|
406
|
+
todos = [];
|
|
407
|
+
planDir = undefined;
|
|
408
|
+
pi.setActiveTools(EXEC_TOOLS);
|
|
409
|
+
if (previousModel) {
|
|
410
|
+
await switchModel(ctx, previousModel);
|
|
411
|
+
}
|
|
412
|
+
if (previousThinking) {
|
|
413
|
+
pi.setThinkingLevel(previousThinking);
|
|
414
|
+
}
|
|
415
|
+
updateUI(ctx);
|
|
416
|
+
persist();
|
|
417
|
+
}
|
|
577
418
|
return;
|
|
578
419
|
}
|
|
579
420
|
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
const lastAssistantMessage = [...event.messages]
|
|
583
|
-
.reverse()
|
|
584
|
-
.find((message) => isAssistantMessage(message));
|
|
585
|
-
if (!lastAssistantMessage) return;
|
|
586
|
-
|
|
587
|
-
const extracted = extractTodoItems(getMessageText(lastAssistantMessage));
|
|
588
|
-
if (extracted.length > 0) {
|
|
589
|
-
todoItems = extracted;
|
|
590
|
-
updateUi(ctx);
|
|
591
|
-
persistState();
|
|
592
|
-
}
|
|
421
|
+
if (!planEnabled || !ctx.hasUI) return;
|
|
593
422
|
|
|
594
|
-
if
|
|
423
|
+
// Check if plan files were created by looking for planDir
|
|
424
|
+
if (!planDir) return;
|
|
595
425
|
|
|
426
|
+
// Show menu
|
|
596
427
|
const choice = await ctx.ui.select('Plan ready — what next?', [
|
|
597
|
-
'
|
|
598
|
-
'
|
|
599
|
-
'
|
|
600
|
-
'
|
|
601
|
-
'Refine the plan',
|
|
602
|
-
'Disable plan mode',
|
|
428
|
+
'Execute Plan',
|
|
429
|
+
'Refine Plan',
|
|
430
|
+
'Follow up',
|
|
431
|
+
'Exit plan mode',
|
|
603
432
|
]);
|
|
604
433
|
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (refinement?.trim()) await sendPlanningPrompt(refinement, ctx);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
434
|
+
if (choice === 'Execute Plan') {
|
|
435
|
+
// Read START-PROMPT.md for clean context handoff
|
|
436
|
+
const startPromptPath = `${planDir}/START-PROMPT.md`;
|
|
437
|
+
const planMdPath = `${planDir}/PLAN.md`;
|
|
438
|
+
|
|
439
|
+
// Read the plan to extract todos
|
|
440
|
+
let planContent = '';
|
|
441
|
+
try {
|
|
442
|
+
const result = await pi.exec('cat', [planMdPath]);
|
|
443
|
+
if (result.code === 0) {
|
|
444
|
+
planContent = result.stdout;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// Fall through — will use empty plan content
|
|
448
|
+
}
|
|
623
449
|
|
|
624
|
-
|
|
625
|
-
|
|
450
|
+
const extracted = extractTodoItems(planContent);
|
|
451
|
+
if (extracted.length > 0) {
|
|
452
|
+
todos = extracted;
|
|
453
|
+
}
|
|
626
454
|
|
|
627
|
-
|
|
628
|
-
|
|
455
|
+
// Read the start prompt for clean handoff
|
|
456
|
+
let startPrompt = '';
|
|
457
|
+
try {
|
|
458
|
+
const result = await pi.exec('cat', [startPromptPath]);
|
|
459
|
+
if (result.code === 0) {
|
|
460
|
+
startPrompt = result.stdout.trim();
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
// Fall through
|
|
464
|
+
}
|
|
629
465
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
466
|
+
await startExecution(ctx);
|
|
467
|
+
updateUI(ctx);
|
|
468
|
+
|
|
469
|
+
if (startPrompt) {
|
|
470
|
+
pi.sendMessage(
|
|
471
|
+
{
|
|
472
|
+
customType: 'plan-mode-execute',
|
|
473
|
+
content: startPrompt,
|
|
474
|
+
display: true,
|
|
475
|
+
},
|
|
476
|
+
{ triggerTurn: true },
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
// Fallback: ask executor to read the plan
|
|
480
|
+
pi.sendMessage(
|
|
481
|
+
{
|
|
482
|
+
customType: 'plan-mode-execute',
|
|
483
|
+
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.`,
|
|
484
|
+
display: true,
|
|
485
|
+
},
|
|
486
|
+
{ triggerTurn: true },
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
} else if (choice === 'Refine Plan') {
|
|
490
|
+
// Adversarial review — planner critiques its own plan
|
|
491
|
+
pi.sendMessage(
|
|
492
|
+
{
|
|
493
|
+
customType: 'plan-mode-refine',
|
|
494
|
+
content: `Review the plan you just created in ${planDir}/PLAN.md with an adversarial lens. Challenge assumptions, find gaps, identify risks, and look for:
|
|
495
|
+
|
|
496
|
+
- Missing edge cases or error handling
|
|
497
|
+
- Incorrect assumptions about the codebase
|
|
498
|
+
- Steps that are too vague or could be misinterpreted
|
|
499
|
+
- Missing dependencies between steps
|
|
500
|
+
- Simpler alternatives that were overlooked
|
|
501
|
+
|
|
502
|
+
After your review, update PLAN.md and START-PROMPT.md with any improvements.`,
|
|
503
|
+
display: true,
|
|
504
|
+
},
|
|
505
|
+
{ triggerTurn: true },
|
|
506
|
+
);
|
|
507
|
+
} else if (choice === 'Follow up') {
|
|
508
|
+
const followUp = await ctx.ui.editor('Follow-up instructions for the planner:', '');
|
|
509
|
+
if (followUp?.trim()) {
|
|
510
|
+
pi.sendUserMessage(followUp.trim());
|
|
511
|
+
}
|
|
512
|
+
} else if (choice === 'Exit plan mode') {
|
|
513
|
+
await exitPlanMode(ctx);
|
|
637
514
|
}
|
|
515
|
+
});
|
|
638
516
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
517
|
+
// ── Restore state on session start/resume ─────────────────────────────────
|
|
518
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
519
|
+
// Check CLI flag
|
|
520
|
+
if (pi.getFlag('plan') === true) {
|
|
521
|
+
planEnabled = true;
|
|
644
522
|
}
|
|
645
523
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
524
|
+
// Restore persisted state
|
|
525
|
+
const entries = ctx.sessionManager.getEntries();
|
|
526
|
+
const saved = entries
|
|
527
|
+
.filter(
|
|
528
|
+
(e: { type: string; customType?: string }) =>
|
|
529
|
+
e.type === 'custom' && e.customType === 'plan-mode',
|
|
530
|
+
)
|
|
531
|
+
.pop() as { data?: PersistedState } | undefined;
|
|
532
|
+
|
|
533
|
+
if (saved?.data) {
|
|
534
|
+
planEnabled = saved.data.planEnabled ?? planEnabled;
|
|
535
|
+
executing = saved.data.executing ?? executing;
|
|
536
|
+
planDir = saved.data.planDir ?? planDir;
|
|
537
|
+
todos = saved.data.todos ?? todos;
|
|
652
538
|
}
|
|
653
539
|
|
|
654
|
-
|
|
655
|
-
|
|
540
|
+
// Re-scan [DONE:n] markers on resume
|
|
541
|
+
if (executing && todos.length > 0) {
|
|
542
|
+
let execIdx = -1;
|
|
543
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
544
|
+
const entry = entries[i] as { type: string; customType?: string };
|
|
545
|
+
if (entry.customType === 'plan-mode-execute') {
|
|
546
|
+
execIdx = i;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
656
550
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
551
|
+
const messages: AssistantMessage[] = [];
|
|
552
|
+
for (let i = execIdx + 1; i < entries.length; i++) {
|
|
553
|
+
const entry = entries[i];
|
|
554
|
+
if (
|
|
555
|
+
entry.type === 'message' &&
|
|
556
|
+
'message' in entry &&
|
|
557
|
+
isAssistantMessage(entry.message as AgentMessage)
|
|
558
|
+
) {
|
|
559
|
+
messages.push(entry.message as AssistantMessage);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const allText = messages.map(getTextContent).join('\n');
|
|
563
|
+
markCompletedSteps(allText, todos);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Apply tool restrictions, model, and thinking level
|
|
567
|
+
if (planEnabled) {
|
|
568
|
+
pi.setActiveTools(PLAN_TOOLS);
|
|
569
|
+
await switchModel(ctx, PLAN_MODEL);
|
|
570
|
+
pi.setThinkingLevel(PLAN_THINKING);
|
|
571
|
+
} else if (executing) {
|
|
572
|
+
pi.setActiveTools(EXEC_TOOLS);
|
|
573
|
+
await switchModel(ctx, EXEC_MODEL);
|
|
574
|
+
pi.setThinkingLevel(EXEC_THINKING);
|
|
575
|
+
}
|
|
660
576
|
|
|
661
|
-
|
|
662
|
-
await restoreState(ctx);
|
|
577
|
+
updateUI(ctx);
|
|
663
578
|
});
|
|
664
579
|
}
|