@dreki-gg/pi-plan-mode 0.7.2 → 0.13.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/CHANGELOG.md +99 -0
- package/extensions/plan-mode/__tests__/agent-end-safety.test.ts +125 -0
- package/extensions/plan-mode/__tests__/package-skills.test.ts +47 -0
- package/extensions/plan-mode/__tests__/prompts.test.ts +38 -0
- package/extensions/plan-mode/{utils.test.ts → __tests__/utils.test.ts} +1 -1
- package/extensions/plan-mode/constants.ts +34 -0
- package/extensions/plan-mode/context-filter.ts +32 -0
- package/extensions/plan-mode/index.ts +166 -411
- package/extensions/plan-mode/phase-transitions.ts +87 -0
- package/extensions/plan-mode/plan-storage.ts +80 -0
- package/extensions/plan-mode/prompts.ts +72 -0
- package/extensions/plan-mode/resume.ts +121 -0
- package/extensions/plan-mode/state.ts +47 -0
- package/extensions/plan-mode/tools/submit-plan.ts +10 -16
- package/extensions/plan-mode/tools/update-step.ts +3 -0
- package/extensions/plan-mode/types.ts +8 -2
- package/extensions/plan-mode/ui.ts +39 -0
- package/package.json +7 -2
- package/skills/technical-options/SKILL.md +89 -0
|
@@ -1,63 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan Mode Extension
|
|
2
|
+
* Plan Mode Extension — Thin orchestrator
|
|
3
3
|
*
|
|
4
4
|
* Two-phase workflow:
|
|
5
5
|
* 1. PLAN phase — read-only tools + submit_plan tool + medium thinking
|
|
6
|
-
* Planner analyzes codebase, calls submit_plan with structured data
|
|
7
6
|
* 2. EXECUTE phase — full tools + update_step tool + low thinking
|
|
8
|
-
* Executor works through plan steps, calling update_step for each
|
|
9
|
-
*
|
|
10
|
-
* Plans live in `.plans/<kebab-name>/plan.json` with structured steps and context.
|
|
11
7
|
*
|
|
12
8
|
* Commands:
|
|
13
|
-
* /plan [prompt] — enter plan mode
|
|
9
|
+
* /plan [prompt] — enter plan mode
|
|
10
|
+
* /plan resume — resume an in-progress plan from disk
|
|
11
|
+
* /plan-exec — execute the current plan in a clean session
|
|
14
12
|
* /todos — show current plan progress
|
|
15
|
-
* Ctrl+Alt+P — toggle plan mode
|
|
13
|
+
* Ctrl+Alt+P — toggle plan mode
|
|
16
14
|
*
|
|
17
15
|
* Flag:
|
|
18
16
|
* --plan — start session in plan mode
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
|
-
import type { ExtensionAPI
|
|
19
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
22
20
|
import { Key } from '@earendil-works/pi-tui';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
21
|
+
import { PLAN_TOOLS, EXEC_TOOLS, PLAN_MODEL, PLAN_THINKING, EXEC_MODEL, EXEC_THINKING } from './constants.js';
|
|
22
|
+
import type { ThinkingLevel } from './types.js';
|
|
23
|
+
import { PlanModeState } from './state.js';
|
|
24
|
+
import { savePlanToDisk, loadPlanFromDisk, readAndClearExecPending, updatePlansManifest } from './plan-storage.js';
|
|
25
|
+
import { updateUI } from './ui.js';
|
|
26
|
+
import { buildPlanModePrompt, buildExecutionPrompt } from './prompts.js';
|
|
27
|
+
import { filterExecutionMessages, filterStalePlanMessages } from './context-filter.js';
|
|
28
|
+
import { enterPlanMode, exitPlanMode, switchModel } from './phase-transitions.js';
|
|
29
|
+
import { resumePlan, executeInNewSession } from './resume.js';
|
|
26
30
|
import { registerSubmitPlanTool } from './tools/submit-plan.js';
|
|
27
31
|
import { registerUpdateStepTool } from './tools/update-step.js';
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
// ── Tool sets ────────────────────────────────────────────────────────────────
|
|
31
|
-
const PLAN_TOOLS = [
|
|
32
|
-
'read',
|
|
33
|
-
'bash',
|
|
34
|
-
'grep',
|
|
35
|
-
'find',
|
|
36
|
-
'ls',
|
|
37
|
-
'submit_plan',
|
|
38
|
-
'questionnaire',
|
|
39
|
-
'search_skills',
|
|
40
|
-
];
|
|
41
|
-
const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'update_step', 'search_skills'];
|
|
42
|
-
|
|
43
|
-
// ── Model + thinking presets ─────────────────────────────────────────────────
|
|
44
|
-
const PLAN_MODEL = { provider: 'anthropic', id: 'claude-opus-4-6' } as const;
|
|
45
|
-
const PLAN_THINKING = 'medium' as const;
|
|
46
|
-
|
|
47
|
-
const EXEC_MODEL = { provider: 'openai', id: 'gpt-5.5' } as const;
|
|
48
|
-
const EXEC_THINKING = 'low' as const;
|
|
49
|
-
|
|
50
|
-
type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
32
|
+
import { isSafeCommand } from './utils.js';
|
|
51
33
|
|
|
52
|
-
// ── Extension ────────────────────────────────────────────────────────────────
|
|
53
34
|
export default function planMode(pi: ExtensionAPI): void {
|
|
54
|
-
|
|
55
|
-
let executing = false;
|
|
56
|
-
let planDir: string | undefined;
|
|
57
|
-
let plan: PlanData | undefined;
|
|
58
|
-
let executionStartIdx: number | undefined;
|
|
59
|
-
let previousThinking: ThinkingLevel | undefined;
|
|
60
|
-
let previousModel: { provider: string; id: string } | undefined;
|
|
35
|
+
const state = new PlanModeState();
|
|
61
36
|
|
|
62
37
|
// ── Flag ──────────────────────────────────────────────────────────────────
|
|
63
38
|
pi.registerFlag('plan', {
|
|
@@ -66,203 +41,65 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
66
41
|
default: false,
|
|
67
42
|
});
|
|
68
43
|
|
|
69
|
-
// ──
|
|
70
|
-
function persist(): void {
|
|
71
|
-
pi.appendEntry<PersistedState>('plan-mode', {
|
|
72
|
-
planEnabled,
|
|
73
|
-
executing,
|
|
74
|
-
planDir,
|
|
75
|
-
plan,
|
|
76
|
-
executionStartIdx,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── plans.json tracking ───────────────────────────────────────────────────
|
|
81
|
-
async function updatePlansManifest(
|
|
82
|
-
planName: string,
|
|
83
|
-
status: 'in-progress' | 'done',
|
|
84
|
-
title?: string,
|
|
85
|
-
): Promise<void> {
|
|
86
|
-
const manifest = await readPlansJson();
|
|
87
|
-
const existing = manifest[planName];
|
|
88
|
-
const now = new Date().toISOString();
|
|
89
|
-
|
|
90
|
-
manifest[planName] = {
|
|
91
|
-
status,
|
|
92
|
-
title: title ?? existing?.title ?? 'Untitled plan',
|
|
93
|
-
created: existing?.created ?? now,
|
|
94
|
-
completed: status === 'done' ? now : null,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
await mkdir('.plans', { recursive: true });
|
|
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');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ── UI updates ────────────────────────────────────────────────────────────
|
|
109
|
-
function updateUI(ctx: ExtensionContext): void {
|
|
110
|
-
const { theme } = ctx.ui;
|
|
111
|
-
|
|
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}`));
|
|
116
|
-
} else if (planEnabled) {
|
|
117
|
-
ctx.ui.setStatus('plan-mode', theme.fg('warning', '📝 plan'));
|
|
118
|
-
} else {
|
|
119
|
-
ctx.ui.setStatus('plan-mode', undefined);
|
|
120
|
-
}
|
|
121
|
-
|
|
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);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
ctx.ui.setWidget('plan-todos', lines);
|
|
137
|
-
} else {
|
|
138
|
-
ctx.ui.setWidget('plan-todos', undefined);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── Model switching ───────────────────────────────────────────────────────
|
|
143
|
-
async function switchModel(
|
|
144
|
-
ctx: ExtensionContext,
|
|
145
|
-
preset: { provider: string; id: string },
|
|
146
|
-
): Promise<boolean> {
|
|
147
|
-
const model = ctx.modelRegistry.find(preset.provider, preset.id);
|
|
148
|
-
if (!model) {
|
|
149
|
-
ctx.ui.notify(`Model ${preset.provider}/${preset.id} not found`, 'error');
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
const ok = await pi.setModel(model);
|
|
153
|
-
if (!ok) {
|
|
154
|
-
ctx.ui.notify(`No API key for ${preset.provider}/${preset.id}`, 'error');
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Phase transitions ─────────────────────────────────────────────────────
|
|
161
|
-
async function enterPlanMode(ctx: ExtensionContext): Promise<void> {
|
|
162
|
-
planEnabled = true;
|
|
163
|
-
executing = false;
|
|
164
|
-
planDir = undefined;
|
|
165
|
-
plan = undefined;
|
|
166
|
-
previousThinking = pi.getThinkingLevel() as ThinkingLevel;
|
|
167
|
-
previousModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
168
|
-
pi.setActiveTools(PLAN_TOOLS);
|
|
169
|
-
await switchModel(ctx, PLAN_MODEL);
|
|
170
|
-
pi.setThinkingLevel(PLAN_THINKING);
|
|
171
|
-
ctx.ui.notify(
|
|
172
|
-
`Plan mode ON — ${PLAN_MODEL.provider}/${PLAN_MODEL.id}:${PLAN_THINKING}`,
|
|
173
|
-
'info',
|
|
174
|
-
);
|
|
175
|
-
updateUI(ctx);
|
|
176
|
-
persist();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function exitPlanMode(ctx: ExtensionContext): Promise<void> {
|
|
180
|
-
planEnabled = false;
|
|
181
|
-
executing = false;
|
|
182
|
-
planDir = undefined;
|
|
183
|
-
plan = undefined;
|
|
184
|
-
executionStartIdx = undefined;
|
|
185
|
-
pi.setActiveTools(EXEC_TOOLS);
|
|
186
|
-
if (previousModel) {
|
|
187
|
-
await switchModel(ctx, previousModel);
|
|
188
|
-
}
|
|
189
|
-
if (previousThinking) {
|
|
190
|
-
pi.setThinkingLevel(previousThinking);
|
|
191
|
-
}
|
|
192
|
-
ctx.ui.notify('Plan mode OFF — original model restored', 'info');
|
|
193
|
-
updateUI(ctx);
|
|
194
|
-
persist();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function startExecution(ctx: ExtensionContext): Promise<void> {
|
|
198
|
-
planEnabled = false;
|
|
199
|
-
executing = true;
|
|
200
|
-
// Record message count so the context filter can drop the planning conversation
|
|
201
|
-
executionStartIdx = ctx.sessionManager.getEntries().length;
|
|
202
|
-
pi.setActiveTools(EXEC_TOOLS);
|
|
203
|
-
await switchModel(ctx, EXEC_MODEL);
|
|
204
|
-
pi.setThinkingLevel(EXEC_THINKING);
|
|
205
|
-
ctx.ui.notify(
|
|
206
|
-
`Executing plan — ${EXEC_MODEL.provider}/${EXEC_MODEL.id}:${EXEC_THINKING}`,
|
|
207
|
-
'info',
|
|
208
|
-
);
|
|
209
|
-
updateUI(ctx);
|
|
210
|
-
persist();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function togglePlanMode(ctx: ExtensionContext): Promise<void> {
|
|
214
|
-
if (planEnabled || executing) {
|
|
215
|
-
await exitPlanMode(ctx);
|
|
216
|
-
} else {
|
|
217
|
-
await enterPlanMode(ctx);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── Register tools ────────────────────────────────────────────────────────
|
|
44
|
+
// ── Tools ─────────────────────────────────────────────────────────────────
|
|
222
45
|
registerSubmitPlanTool(pi, {
|
|
223
46
|
onPlanSubmitted: (dir, submittedPlan) => {
|
|
224
|
-
planDir = dir;
|
|
225
|
-
plan = submittedPlan;
|
|
226
|
-
persist();
|
|
47
|
+
state.planDir = dir;
|
|
48
|
+
state.plan = submittedPlan;
|
|
49
|
+
state.persist(pi);
|
|
227
50
|
},
|
|
228
51
|
});
|
|
229
52
|
|
|
230
53
|
registerUpdateStepTool(pi, {
|
|
231
|
-
getPlan: () => plan,
|
|
54
|
+
getPlan: () => state.plan,
|
|
232
55
|
onStepUpdated: (step, status, notes) => {
|
|
233
|
-
if (!plan) return;
|
|
234
|
-
|
|
235
|
-
plan.steps[
|
|
236
|
-
|
|
237
|
-
persist();
|
|
56
|
+
if (!state.plan) return;
|
|
57
|
+
state.plan.steps[step - 1].status = status;
|
|
58
|
+
if (notes) state.plan.steps[step - 1].notes = notes;
|
|
59
|
+
state.persist(pi);
|
|
238
60
|
},
|
|
239
61
|
});
|
|
240
62
|
|
|
241
63
|
// ── Commands ──────────────────────────────────────────────────────────────
|
|
242
64
|
pi.registerCommand('plan', {
|
|
243
|
-
description: 'Enter plan mode, optionally with a starting prompt',
|
|
65
|
+
description: 'Enter plan mode, optionally with a starting prompt. Use "/plan resume" to pick up an existing plan.',
|
|
244
66
|
handler: async (args, ctx) => {
|
|
245
|
-
|
|
246
|
-
|
|
67
|
+
const trimmed = args?.trim();
|
|
68
|
+
if (trimmed === 'resume') {
|
|
69
|
+
await resumePlan(state, pi, ctx);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (state.planEnabled || state.executing) {
|
|
73
|
+
await exitPlanMode(state, pi, ctx);
|
|
247
74
|
return;
|
|
248
75
|
}
|
|
249
|
-
await enterPlanMode(ctx);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
76
|
+
await enterPlanMode(state, pi, ctx);
|
|
77
|
+
if (trimmed) pi.sendUserMessage(trimmed);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
pi.registerCommand('plan-exec', {
|
|
82
|
+
description: 'Execute the current plan in a clean session',
|
|
83
|
+
handler: async (_args, ctx) => {
|
|
84
|
+
if (!state.planDir || !state.plan) {
|
|
85
|
+
ctx.ui.notify('No plan to execute.', 'error');
|
|
86
|
+
return;
|
|
253
87
|
}
|
|
88
|
+
const stepList = state.plan.steps.map((s, i) => `${i + 1}. ${s.description}`).join('\n');
|
|
89
|
+
const kickoff = `Execute the following plan: "${state.plan.title}"\n\nSteps:\n${stepList}\n\nStart with step 1. Call update_step after completing each step.`;
|
|
90
|
+
await executeInNewSession(ctx, state.planDir, state.plan, kickoff);
|
|
254
91
|
},
|
|
255
92
|
});
|
|
256
93
|
|
|
257
94
|
pi.registerCommand('todos', {
|
|
258
95
|
description: 'Show current plan progress',
|
|
259
96
|
handler: async (_args, ctx) => {
|
|
260
|
-
if (!plan || plan.steps.length === 0) {
|
|
97
|
+
if (!state.plan || state.plan.steps.length === 0) {
|
|
261
98
|
ctx.ui.notify('No plan yet. Use /plan to start planning.', 'info');
|
|
262
99
|
return;
|
|
263
100
|
}
|
|
264
|
-
const statusIcon = { pending: '○', done: '✓', skipped: '⊘', blocked: '✗' };
|
|
265
|
-
const list = plan.steps
|
|
101
|
+
const statusIcon = { pending: '○', done: '✓', skipped: '⊘', blocked: '✗' } as const;
|
|
102
|
+
const list = state.plan.steps
|
|
266
103
|
.map((s, i) => `${i + 1}. ${statusIcon[s.status]} ${s.description}`)
|
|
267
104
|
.join('\n');
|
|
268
105
|
ctx.ui.notify(`Plan Progress:\n${list}`, 'info');
|
|
@@ -271,13 +108,18 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
271
108
|
|
|
272
109
|
pi.registerShortcut(Key.ctrlAlt('p'), {
|
|
273
110
|
description: 'Toggle plan mode',
|
|
274
|
-
handler: async (ctx) =>
|
|
111
|
+
handler: async (ctx) => {
|
|
112
|
+
if (state.planEnabled || state.executing) {
|
|
113
|
+
await exitPlanMode(state, pi, ctx);
|
|
114
|
+
} else {
|
|
115
|
+
await enterPlanMode(state, pi, ctx);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
275
118
|
});
|
|
276
119
|
|
|
277
|
-
// ──
|
|
120
|
+
// ── Event: block destructive bash in plan mode ────────────────────────────
|
|
278
121
|
pi.on('tool_call', async (event) => {
|
|
279
|
-
if (!planEnabled) return;
|
|
280
|
-
|
|
122
|
+
if (!state.planEnabled) return;
|
|
281
123
|
if (event.toolName === 'bash') {
|
|
282
124
|
const command = event.input.command as string;
|
|
283
125
|
if (!isSafeCommand(command)) {
|
|
@@ -289,231 +131,145 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
289
131
|
}
|
|
290
132
|
});
|
|
291
133
|
|
|
292
|
-
// ──
|
|
134
|
+
// ── Event: filter context ─────────────────────────────────────────────────
|
|
293
135
|
pi.on('context', async (event) => {
|
|
294
|
-
if (planEnabled) return;
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
};
|
|
136
|
+
if (state.planEnabled) return;
|
|
137
|
+
if (state.executing && state.executionStartIdx !== undefined) {
|
|
138
|
+
return { messages: filterExecutionMessages(event.messages, state.executionStartIdx) };
|
|
303
139
|
}
|
|
304
|
-
|
|
305
|
-
// Not executing — just strip stale plan-mode injected messages
|
|
306
|
-
return {
|
|
307
|
-
messages: event.messages.filter((m) => {
|
|
308
|
-
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
309
|
-
if (msg.customType === 'plan-mode-context') return false;
|
|
310
|
-
if (msg.role !== 'user') return true;
|
|
311
|
-
const content = msg.content;
|
|
312
|
-
if (typeof content === 'string') {
|
|
313
|
-
return !content.includes('[PLAN MODE ACTIVE]');
|
|
314
|
-
}
|
|
315
|
-
if (Array.isArray(content)) {
|
|
316
|
-
return !content.some(
|
|
317
|
-
(c: { type?: string; text?: string }) =>
|
|
318
|
-
c.type === 'text' && c.text?.includes('[PLAN MODE ACTIVE]'),
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
return true;
|
|
322
|
-
}),
|
|
323
|
-
};
|
|
140
|
+
return { messages: filterStalePlanMessages(event.messages) };
|
|
324
141
|
});
|
|
325
142
|
|
|
326
|
-
// ──
|
|
143
|
+
// ── Event: inject phase prompts ───────────────────────────────────────────
|
|
327
144
|
pi.on('before_agent_start', async () => {
|
|
328
|
-
if (planEnabled) {
|
|
145
|
+
if (state.planEnabled) {
|
|
329
146
|
return {
|
|
330
|
-
message: {
|
|
331
|
-
customType: 'plan-mode-context',
|
|
332
|
-
content: `[PLAN MODE ACTIVE]
|
|
333
|
-
You are in plan mode — a planning phase with strict bash restrictions.
|
|
334
|
-
|
|
335
|
-
Restrictions:
|
|
336
|
-
- Available tools: ${PLAN_TOOLS.join(', ')}
|
|
337
|
-
- Bash is restricted to read-only commands (ls, grep, git status, etc.)
|
|
338
|
-
|
|
339
|
-
Your task:
|
|
340
|
-
1. Analyze the codebase thoroughly using the available read-only tools
|
|
341
|
-
2. Ask clarifying questions if needed (use the questionnaire tool)
|
|
342
|
-
3. Produce a detailed, concrete plan
|
|
343
|
-
|
|
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
|
|
350
|
-
|
|
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.`,
|
|
353
|
-
display: false,
|
|
354
|
-
},
|
|
147
|
+
message: { customType: 'plan-mode-context', content: buildPlanModePrompt(), display: false },
|
|
355
148
|
};
|
|
356
149
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const stepList = remaining
|
|
366
|
-
.map((s) => `${s.num}. ${s.description}\n Details: ${s.details}`)
|
|
367
|
-
.join('\n\n');
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
message: {
|
|
371
|
-
customType: 'plan-execution-context',
|
|
372
|
-
content: `[EXECUTING PLAN — Full tool access enabled]
|
|
373
|
-
|
|
374
|
-
Context:
|
|
375
|
-
${plan.context}
|
|
376
|
-
|
|
377
|
-
Remaining steps:
|
|
378
|
-
${stepList}
|
|
379
|
-
|
|
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.`,
|
|
383
|
-
display: false,
|
|
384
|
-
},
|
|
385
|
-
};
|
|
150
|
+
if (state.executing && state.plan) {
|
|
151
|
+
const content = buildExecutionPrompt(state.plan);
|
|
152
|
+
if (content) {
|
|
153
|
+
return {
|
|
154
|
+
message: { customType: 'plan-execution-context', content, display: false },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
386
157
|
}
|
|
387
158
|
});
|
|
388
159
|
|
|
389
|
-
// ──
|
|
160
|
+
// ── Event: agent_end — blocked steps, completion, post-plan menu ──────────
|
|
390
161
|
pi.on('agent_end', async (_event, ctx) => {
|
|
391
|
-
//
|
|
392
|
-
if (executing && plan) {
|
|
393
|
-
const blocked = plan.steps
|
|
162
|
+
// ── During execution: handle blocked steps and completion ──
|
|
163
|
+
if (state.executing && state.plan) {
|
|
164
|
+
const blocked = state.plan.steps
|
|
394
165
|
.map((s, i) => ({ ...s, num: i + 1 }))
|
|
395
166
|
.filter((s) => s.status === 'blocked');
|
|
396
167
|
|
|
397
168
|
if (blocked.length > 0) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
? `Step ${
|
|
401
|
-
: `Step ${
|
|
169
|
+
const bs = blocked[0];
|
|
170
|
+
const info = bs.notes
|
|
171
|
+
? `Step ${bs.num}: ${bs.description}\nReason: ${bs.notes}`
|
|
172
|
+
: `Step ${bs.num}: ${bs.description}`;
|
|
402
173
|
|
|
403
|
-
const choice = await ctx.ui.select(`Step blocked — ${
|
|
404
|
-
'Skip this step',
|
|
405
|
-
'Provide instructions',
|
|
406
|
-
'Re-plan',
|
|
407
|
-
'Abort execution',
|
|
174
|
+
const choice = await ctx.ui.select(`Step blocked — ${info}\n\nWhat next?`, [
|
|
175
|
+
'Skip this step', 'Provide instructions', 'Re-plan', 'Abort execution',
|
|
408
176
|
]);
|
|
409
177
|
|
|
410
178
|
if (choice === 'Skip this step') {
|
|
411
|
-
plan.steps[
|
|
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) {
|
|
179
|
+
state.plan.steps[bs.num - 1].status = 'skipped';
|
|
180
|
+
await savePlanToDisk(state.planDir!, state.plan);
|
|
181
|
+
updateUI(state, ctx);
|
|
182
|
+
state.persist(pi);
|
|
183
|
+
if (state.plan.steps.some((s) => s.status === 'pending')) {
|
|
419
184
|
pi.sendUserMessage('The blocked step has been skipped. Continue with the next step.', { deliverAs: 'followUp' });
|
|
420
185
|
}
|
|
421
|
-
// If no pending, fall through to completion check below
|
|
422
186
|
} else if (choice === 'Provide instructions') {
|
|
423
187
|
const instructions = await ctx.ui.editor('Instructions for the blocked step:', '');
|
|
424
188
|
if (instructions?.trim()) {
|
|
425
|
-
plan.steps[
|
|
426
|
-
plan.steps[
|
|
427
|
-
await savePlanToDisk();
|
|
428
|
-
updateUI(ctx);
|
|
429
|
-
persist();
|
|
189
|
+
state.plan.steps[bs.num - 1].status = 'pending';
|
|
190
|
+
state.plan.steps[bs.num - 1].notes = undefined;
|
|
191
|
+
await savePlanToDisk(state.planDir!, state.plan);
|
|
192
|
+
updateUI(state, ctx);
|
|
193
|
+
state.persist(pi);
|
|
430
194
|
pi.sendUserMessage(
|
|
431
|
-
`Retry step ${
|
|
195
|
+
`Retry step ${bs.num} with these additional instructions: ${instructions.trim()}`,
|
|
432
196
|
{ deliverAs: 'followUp' },
|
|
433
197
|
);
|
|
434
198
|
}
|
|
435
199
|
return;
|
|
436
200
|
} else if (choice === 'Re-plan') {
|
|
437
|
-
await enterPlanMode(ctx);
|
|
201
|
+
await enterPlanMode(state, pi, ctx);
|
|
438
202
|
pi.sendUserMessage(
|
|
439
|
-
`Step ${
|
|
203
|
+
`Step ${bs.num} was blocked: ${bs.notes ?? 'no details'}. Re-analyze and create a revised plan.`,
|
|
440
204
|
{ deliverAs: 'followUp' },
|
|
441
205
|
);
|
|
442
206
|
return;
|
|
443
207
|
} else if (choice === 'Abort execution') {
|
|
444
|
-
await exitPlanMode(ctx);
|
|
208
|
+
await exitPlanMode(state, pi, ctx);
|
|
445
209
|
return;
|
|
446
210
|
}
|
|
447
211
|
}
|
|
448
212
|
|
|
449
|
-
// Check
|
|
450
|
-
const allResolved = plan.steps.every((s) => s.status === 'done' || s.status === 'skipped');
|
|
213
|
+
// Check completion
|
|
214
|
+
const allResolved = state.plan.steps.every((s) => s.status === 'done' || s.status === 'skipped');
|
|
451
215
|
if (allResolved) {
|
|
452
|
-
if (planDir) {
|
|
453
|
-
|
|
454
|
-
await
|
|
455
|
-
await savePlanToDisk();
|
|
216
|
+
if (state.planDir) {
|
|
217
|
+
await updatePlansManifest(state.planDir.replace(/^\.plans\//, ''), 'done', state.plan.title);
|
|
218
|
+
await savePlanToDisk(state.planDir, state.plan);
|
|
456
219
|
}
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
220
|
+
const done = state.plan.steps.filter((s) => s.status === 'done').length;
|
|
221
|
+
const skipped = state.plan.steps.filter((s) => s.status === 'skipped').length;
|
|
222
|
+
const total = state.plan.steps.length;
|
|
223
|
+
const stats = skipped > 0
|
|
224
|
+
? `${done}/${total} done, ${skipped} skipped`
|
|
225
|
+
: `${done}/${total} done`;
|
|
226
|
+
|
|
227
|
+
// Build a summary of what was actually done from step notes
|
|
228
|
+
const changeSummary = state.plan.steps
|
|
229
|
+
.map((s, i) => {
|
|
230
|
+
const icon = s.status === 'done' ? '✓' : '⊘';
|
|
231
|
+
const label = `${i + 1}. ${icon} ${s.description}`;
|
|
232
|
+
return s.notes ? `${label}\n ${s.notes}` : label;
|
|
463
233
|
})
|
|
464
234
|
.join('\n');
|
|
465
235
|
|
|
236
|
+
const summary = [
|
|
237
|
+
`**Plan Complete!** ✓ — ${state.plan.title}`,
|
|
238
|
+
'',
|
|
239
|
+
`> ${stats}`,
|
|
240
|
+
'',
|
|
241
|
+
'## Summary',
|
|
242
|
+
'',
|
|
243
|
+
changeSummary,
|
|
244
|
+
].join('\n');
|
|
245
|
+
|
|
466
246
|
pi.sendMessage(
|
|
467
|
-
{
|
|
468
|
-
customType: 'plan-complete',
|
|
469
|
-
content: `**Plan Complete!** ✓\n\n${list}`,
|
|
470
|
-
display: true,
|
|
471
|
-
},
|
|
247
|
+
{ customType: 'plan-complete', content: summary, display: true },
|
|
472
248
|
{ triggerTurn: false },
|
|
473
249
|
);
|
|
474
250
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
planDir = undefined;
|
|
478
|
-
executionStartIdx = undefined;
|
|
251
|
+
const { previousModel: pm, previousThinking: pt } = state;
|
|
252
|
+
state.reset();
|
|
479
253
|
pi.setActiveTools(EXEC_TOOLS);
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
pi.setThinkingLevel(previousThinking);
|
|
485
|
-
}
|
|
486
|
-
updateUI(ctx);
|
|
487
|
-
persist();
|
|
254
|
+
if (pm) await switchModel(pi, ctx, pm);
|
|
255
|
+
if (pt) pi.setThinkingLevel(pt);
|
|
256
|
+
updateUI(state, ctx);
|
|
257
|
+
state.persist(pi);
|
|
488
258
|
return;
|
|
489
259
|
}
|
|
490
260
|
return;
|
|
491
261
|
}
|
|
492
262
|
|
|
493
|
-
|
|
494
|
-
if (!
|
|
263
|
+
// ── After plan submission: show post-plan menu ──
|
|
264
|
+
if (!state.planEnabled || !ctx.hasUI) return;
|
|
265
|
+
if (!state.planDir || !state.plan) return;
|
|
495
266
|
|
|
496
|
-
// Show menu after plan submission
|
|
497
267
|
const choice = await ctx.ui.select('Plan ready — what next?', [
|
|
498
|
-
'Execute Plan',
|
|
499
|
-
'Refine Plan',
|
|
500
|
-
'Follow up',
|
|
501
|
-
'Exit plan mode',
|
|
268
|
+
'Execute Plan', 'Refine Plan', 'Follow up', 'Exit plan mode',
|
|
502
269
|
]);
|
|
503
270
|
|
|
504
271
|
if (choice === 'Execute Plan') {
|
|
505
|
-
|
|
506
|
-
updateUI(ctx);
|
|
507
|
-
|
|
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
|
-
);
|
|
272
|
+
pi.sendUserMessage('/plan-exec', { deliverAs: 'followUp' });
|
|
517
273
|
} else if (choice === 'Refine Plan') {
|
|
518
274
|
pi.sendUserMessage(
|
|
519
275
|
`Review the plan you just created with an adversarial lens. Challenge assumptions, find gaps, identify risks, and look for:
|
|
@@ -528,49 +284,48 @@ After your review, call submit_plan again with the improved plan.`,
|
|
|
528
284
|
{ deliverAs: 'followUp' },
|
|
529
285
|
);
|
|
530
286
|
} else if (choice === 'Follow up') {
|
|
531
|
-
|
|
532
|
-
if (followUp?.trim()) {
|
|
533
|
-
pi.sendUserMessage(followUp.trim(), { deliverAs: 'followUp' });
|
|
534
|
-
}
|
|
287
|
+
// No-op: dismiss menu, let user type naturally
|
|
535
288
|
} else if (choice === 'Exit plan mode') {
|
|
536
|
-
await exitPlanMode(ctx);
|
|
289
|
+
await exitPlanMode(state, pi, ctx);
|
|
537
290
|
}
|
|
538
291
|
});
|
|
539
292
|
|
|
540
|
-
// ──
|
|
293
|
+
// ── Event: session restore ────────────────────────────────────────────────
|
|
541
294
|
pi.on('session_start', async (_event, ctx) => {
|
|
542
|
-
if (pi.getFlag('plan') === true)
|
|
543
|
-
planEnabled = true;
|
|
544
|
-
}
|
|
295
|
+
if (pi.getFlag('plan') === true) state.planEnabled = true;
|
|
545
296
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
.filter(
|
|
550
|
-
(e: { type: string; customType?: string }) =>
|
|
551
|
-
e.type === 'custom' && e.customType === 'plan-mode',
|
|
552
|
-
)
|
|
553
|
-
.pop() as { data?: PersistedState } | undefined;
|
|
297
|
+
state.restore(
|
|
298
|
+
ctx.sessionManager.getEntries() as Array<{ type: string; customType?: string; data?: any }>,
|
|
299
|
+
);
|
|
554
300
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
planDir =
|
|
559
|
-
plan =
|
|
560
|
-
|
|
301
|
+
// Check for exec-pending handoff from planning session
|
|
302
|
+
const pending = await readAndClearExecPending();
|
|
303
|
+
if (pending) {
|
|
304
|
+
state.planDir = pending.planDir;
|
|
305
|
+
state.plan = await loadPlanFromDisk(pending.planDir);
|
|
306
|
+
if (state.plan) {
|
|
307
|
+
state.executing = true;
|
|
308
|
+
state.planEnabled = false;
|
|
309
|
+
pi.setActiveTools(EXEC_TOOLS);
|
|
310
|
+
await switchModel(pi, ctx, pending.config.model);
|
|
311
|
+
pi.setThinkingLevel(pending.config.thinking as ThinkingLevel);
|
|
312
|
+
updateUI(state, ctx);
|
|
313
|
+
state.persist(pi);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
561
316
|
}
|
|
562
317
|
|
|
563
318
|
// Apply tool restrictions, model, and thinking level
|
|
564
|
-
if (planEnabled) {
|
|
319
|
+
if (state.planEnabled) {
|
|
565
320
|
pi.setActiveTools(PLAN_TOOLS);
|
|
566
|
-
await switchModel(ctx, PLAN_MODEL);
|
|
321
|
+
await switchModel(pi, ctx, PLAN_MODEL);
|
|
567
322
|
pi.setThinkingLevel(PLAN_THINKING);
|
|
568
|
-
} else if (executing) {
|
|
323
|
+
} else if (state.executing) {
|
|
569
324
|
pi.setActiveTools(EXEC_TOOLS);
|
|
570
|
-
await switchModel(ctx, EXEC_MODEL);
|
|
325
|
+
await switchModel(pi, ctx, EXEC_MODEL);
|
|
571
326
|
pi.setThinkingLevel(EXEC_THINKING);
|
|
572
327
|
}
|
|
573
328
|
|
|
574
|
-
updateUI(ctx);
|
|
329
|
+
updateUI(state, ctx);
|
|
575
330
|
});
|
|
576
331
|
}
|