@bacnh85/pi-plan 0.1.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/README.md +74 -0
- package/index.ts +364 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# pi-plan
|
|
2
|
+
|
|
3
|
+
Pi extension that adds a lightweight plan mode inspired by Codex and Claude Code:
|
|
4
|
+
|
|
5
|
+
- Toggle plan mode with `/plan`, `/plan on`, `/plan off`, or `Ctrl+Alt+P`.
|
|
6
|
+
- Pick separate thinking/reasoning levels for planning and normal execution with `/plan levels`.
|
|
7
|
+
- Keeps planning read-only by disabling built-in `edit`/`write` and blocking destructive shell commands.
|
|
8
|
+
- Provides a `write_plan` tool so the agent writes reviewable Markdown plans into `.agents/plans/` in the current workspace.
|
|
9
|
+
- Prompts after a plan is written so you can approve execution, approve only, keep planning, or refine with feedback.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
From npm after the package is published:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:@bacnh85/pi-plan
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
From this repository checkout:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install ./extensions/pi-plan
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For local development without installing:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi -e ./extensions/pi-plan
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Start directly in planning mode:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pi --plan
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands and shortcuts
|
|
38
|
+
|
|
39
|
+
| Command / shortcut | Description |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `/plan` | Toggle plan mode. |
|
|
42
|
+
| `/plan on` / `/plan off` | Explicitly enter or exit plan mode. |
|
|
43
|
+
| `/plan levels` | Select planning and normal/execution thinking levels. |
|
|
44
|
+
| `/plan status` | Show current mode, thinking levels, and last plan file. |
|
|
45
|
+
| `/plan high` | Set the plan-mode thinking level directly (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). |
|
|
46
|
+
| `Ctrl+Alt+P` | Toggle plan mode. |
|
|
47
|
+
|
|
48
|
+
## Workflow
|
|
49
|
+
|
|
50
|
+
1. Enter plan mode.
|
|
51
|
+
2. Ask pi to research the task and propose an implementation.
|
|
52
|
+
3. The model explores read-only context and calls `write_plan`.
|
|
53
|
+
4. The plan is saved under `.agents/plans/<timestamp>-<title>.md`.
|
|
54
|
+
5. Choose one of the approval options:
|
|
55
|
+
- **Approve and execute**: exits plan mode, restores tools, applies normal thinking level, and sends a follow-up execution prompt.
|
|
56
|
+
- **Approve only**: exits plan mode without starting work.
|
|
57
|
+
- **Keep planning**: remains in plan mode.
|
|
58
|
+
- **Refine with feedback**: sends your feedback as a follow-up planning prompt.
|
|
59
|
+
|
|
60
|
+
## Design notes
|
|
61
|
+
|
|
62
|
+
Research findings used for this extension:
|
|
63
|
+
|
|
64
|
+
- Pi has no built-in plan mode by design, but extensions can implement it with commands, shortcuts, tool gating, status widgets, and prompt injection.
|
|
65
|
+
- Claude Code plan mode emphasizes read-only exploration, writing a Markdown plan, and an approval flow before edits.
|
|
66
|
+
- Codex guidance recommends plan mode for complex or ambiguous tasks and using higher reasoning levels for harder planning/debugging work.
|
|
67
|
+
|
|
68
|
+
This extension deliberately writes visible workspace plans instead of hidden internal state so you can review, edit, commit, or discard them like any other project artifact.
|
|
69
|
+
|
|
70
|
+
## Packaging and release
|
|
71
|
+
|
|
72
|
+
`package.json` declares this as a Pi package with the `pi-package` keyword and a `pi.extensions` entry for `./index.ts`. Runtime Pi imports are listed as peer dependencies per Pi package guidance.
|
|
73
|
+
|
|
74
|
+
The repository publish workflow includes `extensions/pi-plan`. Bump the package version in `package.json` when publishing a new npm release; the CI workflow only publishes packages whose versions changed, or the selected package from manual `workflow_dispatch`.
|
package/index.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isToolCallEventType, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const STATUS_KEY = "pi-plan";
|
|
8
|
+
const PLAN_DIR = path.join(".agents", "plans");
|
|
9
|
+
const PLAN_TOOL = "write_plan";
|
|
10
|
+
const PLAN_MODE_DISABLED_TOOLS = new Set(["edit", "write"]);
|
|
11
|
+
const DEFAULT_PLAN_TOOLS = ["read", "bash", "grep", "find", "ls", PLAN_TOOL];
|
|
12
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
13
|
+
|
|
14
|
+
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
15
|
+
type PlanStatus = "draft" | "approved" | "executing";
|
|
16
|
+
|
|
17
|
+
interface PlanState {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
executing: boolean;
|
|
20
|
+
planThinking: ThinkingLevel;
|
|
21
|
+
normalThinking: ThinkingLevel;
|
|
22
|
+
toolsBeforePlan?: string[];
|
|
23
|
+
lastPlanPath?: string;
|
|
24
|
+
lastPlanTitle?: string;
|
|
25
|
+
lastPlanStatus?: PlanStatus;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WritePlanParams {
|
|
29
|
+
title: string;
|
|
30
|
+
content: string;
|
|
31
|
+
status?: PlanStatus;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DESTRUCTIVE_BASH_PATTERNS = [
|
|
35
|
+
/\brm\b/i,
|
|
36
|
+
/\brmdir\b/i,
|
|
37
|
+
/\bmv\b/i,
|
|
38
|
+
/\bcp\b/i,
|
|
39
|
+
/\bmkdir\b/i,
|
|
40
|
+
/\btouch\b/i,
|
|
41
|
+
/\bchmod\b/i,
|
|
42
|
+
/\bchown\b/i,
|
|
43
|
+
/\btee\b/i,
|
|
44
|
+
/(^|[^<])>(?!>)/,
|
|
45
|
+
/>>/,
|
|
46
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
47
|
+
/\b(yarn|pnpm)\s+(add|remove|install|publish)/i,
|
|
48
|
+
/\bpip\s+(install|uninstall)/i,
|
|
49
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
50
|
+
/\bsudo\b/i,
|
|
51
|
+
/\bkill(all)?\b/i,
|
|
52
|
+
/\b(pk|re)?kill\b/i,
|
|
53
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
57
|
+
return (THINKING_LEVELS as readonly string[]).includes(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isPlanStatus(value: string | undefined): value is PlanStatus {
|
|
61
|
+
return value === "draft" || value === "approved" || value === "executing";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function unique(values: string[]): string[] {
|
|
65
|
+
return [...new Set(values)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function slugify(value: string): string {
|
|
69
|
+
const slug = value
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
72
|
+
.replace(/^-+|-+$/g, "")
|
|
73
|
+
.slice(0, 60);
|
|
74
|
+
return slug || "plan";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizePlanContent(params: WritePlanParams): string {
|
|
78
|
+
const title = params.title.trim() || "Plan";
|
|
79
|
+
const body = params.content.trim();
|
|
80
|
+
if (/^#\s+/m.test(body)) return `${body}\n`;
|
|
81
|
+
return `# ${title}\n\n${body}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function planPath(cwd: string, title: string): string {
|
|
85
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
86
|
+
return path.join(cwd, PLAN_DIR, `${stamp}-${slugify(title)}.md`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function relativeToCwd(cwd: string, absolutePath: string): string {
|
|
90
|
+
return path.relative(cwd, absolutePath).split(path.sep).join("/");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isDestructiveBash(command: string): boolean {
|
|
94
|
+
return DESTRUCTIVE_BASH_PATTERNS.some((pattern) => pattern.test(command));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
98
|
+
let planModeEnabled = false;
|
|
99
|
+
let executionMode = false;
|
|
100
|
+
let toolsBeforePlan: string[] | undefined;
|
|
101
|
+
let planThinking: ThinkingLevel = "high";
|
|
102
|
+
let normalThinking: ThinkingLevel = "medium";
|
|
103
|
+
let lastPlanPath: string | undefined;
|
|
104
|
+
let lastPlanTitle: string | undefined;
|
|
105
|
+
let lastPlanStatus: PlanStatus | undefined;
|
|
106
|
+
|
|
107
|
+
function setStatus(ctx: ExtensionContext): void {
|
|
108
|
+
if (planModeEnabled) {
|
|
109
|
+
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("warning", `plan:${planThinking}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (executionMode) {
|
|
113
|
+
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `exec:${normalThinking}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setPlanWidget(ctx: ExtensionContext): void {
|
|
120
|
+
if (!planModeEnabled) {
|
|
121
|
+
ctx.ui.setWidget(STATUS_KEY, undefined);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
ctx.ui.setWidget(STATUS_KEY, [
|
|
125
|
+
ctx.ui.theme.fg("warning", "Plan mode: read-only exploration"),
|
|
126
|
+
`Plan file target: ${PLAN_DIR}/`,
|
|
127
|
+
`Plan thinking: ${planThinking}; normal thinking: ${normalThinking}`,
|
|
128
|
+
"Use write_plan when the plan is ready for approval.",
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function persistState(): void {
|
|
133
|
+
pi.appendEntry("pi-plan", {
|
|
134
|
+
enabled: planModeEnabled,
|
|
135
|
+
executing: executionMode,
|
|
136
|
+
planThinking,
|
|
137
|
+
normalThinking,
|
|
138
|
+
toolsBeforePlan,
|
|
139
|
+
lastPlanPath,
|
|
140
|
+
lastPlanTitle,
|
|
141
|
+
lastPlanStatus,
|
|
142
|
+
} satisfies PlanState);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function enablePlanTools(): void {
|
|
146
|
+
const baseline = toolsBeforePlan ?? pi.getActiveTools();
|
|
147
|
+
toolsBeforePlan = baseline;
|
|
148
|
+
pi.setActiveTools(unique([...baseline.filter((tool) => !PLAN_MODE_DISABLED_TOOLS.has(tool)), ...DEFAULT_PLAN_TOOLS]));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function restoreTools(): void {
|
|
152
|
+
if (toolsBeforePlan) pi.setActiveTools(toolsBeforePlan);
|
|
153
|
+
toolsBeforePlan = undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyThinking(level: ThinkingLevel): void {
|
|
157
|
+
pi.setThinkingLevel(level);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function enterPlanMode(ctx: ExtensionContext): void {
|
|
161
|
+
planModeEnabled = true;
|
|
162
|
+
executionMode = false;
|
|
163
|
+
enablePlanTools();
|
|
164
|
+
applyThinking(planThinking);
|
|
165
|
+
setStatus(ctx);
|
|
166
|
+
setPlanWidget(ctx);
|
|
167
|
+
persistState();
|
|
168
|
+
ctx.ui.notify(`Plan mode enabled. Thinking=${planThinking}. Plans will be written to ${PLAN_DIR}/`, "info");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function leavePlanMode(ctx: ExtensionContext, restoreThinking = true): void {
|
|
172
|
+
planModeEnabled = false;
|
|
173
|
+
executionMode = false;
|
|
174
|
+
restoreTools();
|
|
175
|
+
if (restoreThinking) applyThinking(normalThinking);
|
|
176
|
+
setStatus(ctx);
|
|
177
|
+
setPlanWidget(ctx);
|
|
178
|
+
persistState();
|
|
179
|
+
ctx.ui.notify(`Plan mode disabled. Thinking=${pi.getThinkingLevel()}.`, "info");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function chooseThinkingLevels(ctx: ExtensionContext): Promise<void> {
|
|
183
|
+
const plan = await ctx.ui.select("Plan mode reasoning level", [...THINKING_LEVELS]);
|
|
184
|
+
if (plan && isThinkingLevel(plan)) planThinking = plan;
|
|
185
|
+
const normal = await ctx.ui.select("Normal/execution reasoning level", [...THINKING_LEVELS]);
|
|
186
|
+
if (normal && isThinkingLevel(normal)) normalThinking = normal;
|
|
187
|
+
if (planModeEnabled) applyThinking(planThinking);
|
|
188
|
+
else applyThinking(normalThinking);
|
|
189
|
+
setStatus(ctx);
|
|
190
|
+
setPlanWidget(ctx);
|
|
191
|
+
persistState();
|
|
192
|
+
ctx.ui.notify(`pi-plan levels: plan=${planThinking}, normal=${normalThinking}`, "info");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function handlePlanCommand(args: string, ctx: ExtensionContext): Promise<void> {
|
|
196
|
+
const subcommand = args.trim();
|
|
197
|
+
if (subcommand === "levels") return chooseThinkingLevels(ctx);
|
|
198
|
+
if (subcommand === "on" || subcommand === "start") return enterPlanMode(ctx);
|
|
199
|
+
if (subcommand === "off" || subcommand === "stop") return leavePlanMode(ctx);
|
|
200
|
+
if (subcommand === "status") {
|
|
201
|
+
ctx.ui.notify(
|
|
202
|
+
[`Mode: ${planModeEnabled ? "planning" : executionMode ? "executing" : "normal"}`, `Plan thinking: ${planThinking}`, `Normal thinking: ${normalThinking}`, `Last plan: ${lastPlanPath ? relativeToCwd(ctx.cwd, lastPlanPath) : "none"}`].join("\n"),
|
|
203
|
+
"info",
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (subcommand.length > 0 && isThinkingLevel(subcommand)) {
|
|
208
|
+
planThinking = subcommand;
|
|
209
|
+
if (planModeEnabled) applyThinking(planThinking);
|
|
210
|
+
setStatus(ctx);
|
|
211
|
+
setPlanWidget(ctx);
|
|
212
|
+
persistState();
|
|
213
|
+
ctx.ui.notify(`Plan thinking set to ${planThinking}`, "info");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (planModeEnabled) leavePlanMode(ctx);
|
|
217
|
+
else enterPlanMode(ctx);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pi.registerFlag("plan", {
|
|
221
|
+
description: "Start in pi-plan read-only planning mode",
|
|
222
|
+
type: "boolean",
|
|
223
|
+
default: false,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
pi.registerTool({
|
|
227
|
+
name: PLAN_TOOL,
|
|
228
|
+
label: "Write Plan",
|
|
229
|
+
description: `Write or replace the current implementation plan as Markdown under ${PLAN_DIR}/. Use in plan mode when the plan is ready for user review.`,
|
|
230
|
+
promptSnippet: `Write the implementation plan to ${PLAN_DIR}/ as a Markdown file for user review`,
|
|
231
|
+
promptGuidelines: [`Use ${PLAN_TOOL} in plan mode after repository exploration; do not use edit/write for implementation until the user approves the plan.`],
|
|
232
|
+
parameters: Type.Object({
|
|
233
|
+
title: Type.String({ description: "Short human-readable title for the plan" }),
|
|
234
|
+
content: Type.String({ description: "Complete Markdown plan content" }),
|
|
235
|
+
status: Type.Optional(Type.String({ description: "Plan status: draft, approved, or executing" })),
|
|
236
|
+
}),
|
|
237
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
238
|
+
const typedParams = params as WritePlanParams;
|
|
239
|
+
const destination = lastPlanPath ?? planPath(ctx.cwd, typedParams.title);
|
|
240
|
+
const content = normalizePlanContent(typedParams);
|
|
241
|
+
await withFileMutationQueue(destination, async () => {
|
|
242
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
243
|
+
await writeFile(destination, content, "utf8");
|
|
244
|
+
});
|
|
245
|
+
lastPlanPath = destination;
|
|
246
|
+
lastPlanTitle = typedParams.title.trim() || "Plan";
|
|
247
|
+
lastPlanStatus = isPlanStatus(typedParams.status) ? typedParams.status : "draft";
|
|
248
|
+
persistState();
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: "text", text: `Plan written to ${relativeToCwd(ctx.cwd, destination)}. Ask the user to approve, refine, or keep planning.` }],
|
|
251
|
+
details: { path: destination, title: lastPlanTitle, status: lastPlanStatus },
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
pi.registerCommand("plan", {
|
|
257
|
+
description: "Toggle pi-plan mode; subcommands: on, off, levels, status, or a thinking level",
|
|
258
|
+
getArgumentCompletions: (prefix) => {
|
|
259
|
+
const values = ["on", "off", "levels", "status", ...THINKING_LEVELS].filter((value) => value.startsWith(prefix));
|
|
260
|
+
return values.length ? values.map((value) => ({ label: value, value })) : null;
|
|
261
|
+
},
|
|
262
|
+
handler: async (args, ctx) => handlePlanCommand(args, ctx),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
pi.registerShortcut("ctrl+alt+p", {
|
|
266
|
+
description: "Toggle pi-plan mode",
|
|
267
|
+
handler: async (ctx) => {
|
|
268
|
+
if (planModeEnabled) leavePlanMode(ctx);
|
|
269
|
+
else enterPlanMode(ctx);
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
274
|
+
const entries = ctx.sessionManager.getEntries();
|
|
275
|
+
const saved = entries
|
|
276
|
+
.filter((entry: { type: string; customType?: string }) => entry.type === "custom" && entry.customType === "pi-plan")
|
|
277
|
+
.pop() as { data?: PlanState } | undefined;
|
|
278
|
+
if (saved?.data) {
|
|
279
|
+
planModeEnabled = saved.data.enabled ?? planModeEnabled;
|
|
280
|
+
executionMode = saved.data.executing ?? executionMode;
|
|
281
|
+
planThinking = saved.data.planThinking ?? planThinking;
|
|
282
|
+
normalThinking = saved.data.normalThinking ?? normalThinking;
|
|
283
|
+
toolsBeforePlan = saved.data.toolsBeforePlan ?? toolsBeforePlan;
|
|
284
|
+
lastPlanPath = saved.data.lastPlanPath ?? lastPlanPath;
|
|
285
|
+
lastPlanTitle = saved.data.lastPlanTitle ?? lastPlanTitle;
|
|
286
|
+
lastPlanStatus = saved.data.lastPlanStatus ?? lastPlanStatus;
|
|
287
|
+
}
|
|
288
|
+
if (pi.getFlag("plan") === true) planModeEnabled = true;
|
|
289
|
+
if (planModeEnabled) {
|
|
290
|
+
enablePlanTools();
|
|
291
|
+
applyThinking(planThinking);
|
|
292
|
+
}
|
|
293
|
+
setStatus(ctx);
|
|
294
|
+
setPlanWidget(ctx);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
pi.on("tool_call", async (event) => {
|
|
298
|
+
if (!planModeEnabled) return;
|
|
299
|
+
if (event.toolName === "edit" || event.toolName === "write") {
|
|
300
|
+
return { block: true, reason: `pi-plan: ${event.toolName} is disabled until the plan is approved. Use ${PLAN_TOOL} to write the plan file.` };
|
|
301
|
+
}
|
|
302
|
+
if (!isToolCallEventType("bash", event)) return;
|
|
303
|
+
if (isDestructiveBash(event.input.command)) {
|
|
304
|
+
return { block: true, reason: `pi-plan: bash command blocked in plan mode because it may modify state.\nCommand: ${event.input.command}` };
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
309
|
+
if (planModeEnabled) {
|
|
310
|
+
const relativePlan = lastPlanPath ? relativeToCwd(ctx.cwd, lastPlanPath) : `${PLAN_DIR}/<timestamp>-<title>.md`;
|
|
311
|
+
return {
|
|
312
|
+
message: {
|
|
313
|
+
customType: "pi-plan-context",
|
|
314
|
+
content: `[PI-PLAN MODE ACTIVE]\nYou are in read-only planning mode. Research the codebase and produce a reviewable implementation plan before making changes.\n\nRules:\n- Do not edit source files, configs, lockfiles, or git state.\n- You may read files, search, inspect git state, and run read-only shell commands.\n- Ask concise clarifying questions if requirements are ambiguous.\n- When the plan is ready, call ${PLAN_TOOL} with a complete Markdown plan.\n- The plan file must live in ${PLAN_DIR}/. Current/next plan path: ${relativePlan}\n\nPlan content should include:\n1. Goal and assumptions.\n2. Key findings with durable file/symbol paths.\n3. Proposed implementation steps.\n4. Verification plan.\n5. Risks, open questions, and rejected alternatives if relevant.`,
|
|
315
|
+
display: false,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (executionMode && lastPlanPath) {
|
|
321
|
+
let content = "";
|
|
322
|
+
try {
|
|
323
|
+
content = await readFile(lastPlanPath, "utf8");
|
|
324
|
+
} catch {
|
|
325
|
+
content = `Plan file unavailable at ${lastPlanPath}`;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
message: {
|
|
329
|
+
customType: "pi-plan-execution-context",
|
|
330
|
+
content: `[PI-PLAN EXECUTION]\nExecute the approved plan at ${relativeToCwd(ctx.cwd, lastPlanPath)}. Use normal mode thinking=${normalThinking}. Keep changes scoped to the plan and verify the result.\n\n${content}`,
|
|
331
|
+
display: false,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
338
|
+
if (!planModeEnabled || !lastPlanPath || !ctx.hasUI) return;
|
|
339
|
+
const relativePlan = relativeToCwd(ctx.cwd, lastPlanPath);
|
|
340
|
+
const choice = await ctx.ui.select(`Plan written: ${relativePlan}`, [
|
|
341
|
+
"Approve and execute",
|
|
342
|
+
"Approve only (exit plan mode)",
|
|
343
|
+
"Keep planning",
|
|
344
|
+
"Refine with feedback",
|
|
345
|
+
]);
|
|
346
|
+
if (choice === "Approve and execute") {
|
|
347
|
+
planModeEnabled = false;
|
|
348
|
+
executionMode = true;
|
|
349
|
+
lastPlanStatus = "approved";
|
|
350
|
+
restoreTools();
|
|
351
|
+
applyThinking(normalThinking);
|
|
352
|
+
setStatus(ctx);
|
|
353
|
+
setPlanWidget(ctx);
|
|
354
|
+
persistState();
|
|
355
|
+
pi.sendUserMessage(`Execute the approved plan in ${relativePlan}. Keep the implementation scoped to the plan, update the plan if reality differs materially, and run the verification described there.`, { deliverAs: "followUp" });
|
|
356
|
+
} else if (choice === "Approve only (exit plan mode)") {
|
|
357
|
+
lastPlanStatus = "approved";
|
|
358
|
+
leavePlanMode(ctx);
|
|
359
|
+
} else if (choice === "Refine with feedback") {
|
|
360
|
+
const feedback = await ctx.ui.editor("Plan feedback", "");
|
|
361
|
+
if (feedback?.trim()) pi.sendUserMessage(`Refine the plan in ${relativePlan} with this feedback:\n\n${feedback.trim()}`, { deliverAs: "followUp" });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bacnh85/pi-plan",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pi extension that adds a plan mode with workspace markdown plans and thinking-level presets.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/bacnh85/skills#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/bacnh85/skills.git",
|
|
13
|
+
"directory": "extensions/pi-plan"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/bacnh85/skills/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pi-package",
|
|
20
|
+
"pi-extension",
|
|
21
|
+
"plan-mode",
|
|
22
|
+
"planning",
|
|
23
|
+
"reasoning"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"README.md",
|
|
27
|
+
"index.ts"
|
|
28
|
+
],
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
36
|
+
"typebox": "*"
|
|
37
|
+
}
|
|
38
|
+
}
|