@garaje/base 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/agents/doc-writer.md +28 -0
  4. package/agents/implementer.md +31 -0
  5. package/agents/planner.md +33 -0
  6. package/agents/reviewer.md +36 -0
  7. package/extensions/.gitkeep +0 -0
  8. package/extensions/README.md +96 -0
  9. package/extensions/agent-rules.ts +29 -0
  10. package/extensions/command-guard.ts +94 -0
  11. package/extensions/confirm-destructive.ts +59 -0
  12. package/extensions/custom-compaction.ts +127 -0
  13. package/extensions/dirty-repo-guard.ts +56 -0
  14. package/extensions/git-checkpoint.ts +53 -0
  15. package/extensions/model-status.ts +31 -0
  16. package/extensions/notify.ts +55 -0
  17. package/extensions/plan-mode/README.md +72 -0
  18. package/extensions/plan-mode/index.ts +396 -0
  19. package/extensions/plan-mode/utils.ts +168 -0
  20. package/extensions/preset.ts +412 -0
  21. package/extensions/protected-paths.ts +69 -0
  22. package/extensions/role-commands.ts +69 -0
  23. package/extensions/session-name.ts +27 -0
  24. package/extensions/standalone-commands.ts +101 -0
  25. package/extensions/status-line.ts +32 -0
  26. package/extensions/subagent/agents.ts +125 -0
  27. package/extensions/subagent/index.ts +1015 -0
  28. package/extensions/usage-status.ts +75 -0
  29. package/lib/agents.ts +5 -0
  30. package/lib/base-paths.ts +8 -0
  31. package/lib/presets.ts +23 -0
  32. package/lib/rules.ts +37 -0
  33. package/package.json +42 -0
  34. package/presets.json +58 -0
  35. package/prompts/roles/doc-writer.md +26 -0
  36. package/prompts/roles/implementer.md +29 -0
  37. package/prompts/roles/planner.md +31 -0
  38. package/prompts/roles/reviewer.md +34 -0
  39. package/rules/00-workspace.md +23 -0
  40. package/rules/10-safety.md +20 -0
  41. package/rules/20-self-modification.md +32 -0
  42. package/skills/park/SKILL.md +113 -0
  43. package/skills/upgrade-bay/SKILL.md +35 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Git Checkpoint Extension
3
+ *
4
+ * Creates git stash checkpoints at each turn so /fork can restore code state.
5
+ * When forking, offers to restore code to that point in history.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+
10
+ export default function (pi: ExtensionAPI) {
11
+ const checkpoints = new Map<string, string>();
12
+ let currentEntryId: string | undefined;
13
+
14
+ // Track the current entry ID when user messages are saved
15
+ pi.on("tool_result", async (_event, ctx) => {
16
+ const leaf = ctx.sessionManager.getLeafEntry();
17
+ if (leaf) currentEntryId = leaf.id;
18
+ });
19
+
20
+ pi.on("turn_start", async () => {
21
+ // Create a git stash entry before LLM makes changes
22
+ const { stdout } = await pi.exec("git", ["stash", "create"]);
23
+ const ref = stdout.trim();
24
+ if (ref && currentEntryId) {
25
+ checkpoints.set(currentEntryId, ref);
26
+ }
27
+ });
28
+
29
+ pi.on("session_before_fork", async (event, ctx) => {
30
+ const ref = checkpoints.get(event.entryId);
31
+ if (!ref) return;
32
+
33
+ if (!ctx.hasUI) {
34
+ // In non-interactive mode, don't restore automatically
35
+ return;
36
+ }
37
+
38
+ const choice = await ctx.ui.select("Restore code state?", [
39
+ "Yes, restore code to that point",
40
+ "No, keep current code",
41
+ ]);
42
+
43
+ if (choice?.startsWith("Yes")) {
44
+ await pi.exec("git", ["stash", "apply", ref]);
45
+ ctx.ui.notify("Code restored to checkpoint", "info");
46
+ }
47
+ });
48
+
49
+ pi.on("agent_end", async () => {
50
+ // Clear checkpoints after agent completes
51
+ checkpoints.clear();
52
+ });
53
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Model status extension - shows model changes in the status bar.
3
+ *
4
+ * Demonstrates the `model_select` hook which fires when the model changes
5
+ * via /model command, Ctrl+P cycling, or session restore.
6
+ *
7
+ * Usage: pi -e ./model-status.ts
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ pi.on("model_select", async (event, ctx) => {
14
+ const { model, previousModel, source } = event;
15
+
16
+ // Format model identifiers
17
+ const next = `${model.provider}/${model.id}`;
18
+ const prev = previousModel ? `${previousModel.provider}/${previousModel.id}` : "none";
19
+
20
+ // Show notification on change
21
+ if (source !== "restore") {
22
+ ctx.ui.notify(`Model: ${next}`, "info");
23
+ }
24
+
25
+ // Update status bar with current model
26
+ ctx.ui.setStatus("model", `🤖 ${model.id}`);
27
+
28
+ // Log change details (visible in debug output)
29
+ console.log(`[model_select] ${prev} → ${next} (${source})`);
30
+ });
31
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pi Notify Extension
3
+ *
4
+ * Sends a native terminal notification when Pi agent is done and waiting for input.
5
+ * Supports multiple terminal protocols:
6
+ * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
7
+ * - OSC 99: Kitty
8
+ * - Windows toast: Windows Terminal (WSL)
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+
13
+ function windowsToastScript(title: string, body: string): string {
14
+ const type = "Windows.UI.Notifications";
15
+ const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
16
+ const template = `[${type}.ToastTemplateType]::ToastText01`;
17
+ const toast = `[${type}.ToastNotification]::new($xml)`;
18
+ return [
19
+ `${mgr} > $null`,
20
+ `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
21
+ `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,
22
+ `[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,
23
+ ].join("; ");
24
+ }
25
+
26
+ function notifyOSC777(title: string, body: string): void {
27
+ process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
28
+ }
29
+
30
+ function notifyOSC99(title: string, body: string): void {
31
+ // Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part
32
+ process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
33
+ process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
34
+ }
35
+
36
+ function notifyWindows(title: string, body: string): void {
37
+ const { execFile } = require("child_process");
38
+ execFile("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
39
+ }
40
+
41
+ function notify(title: string, body: string): void {
42
+ if (process.env.WT_SESSION) {
43
+ notifyWindows(title, body);
44
+ } else if (process.env.KITTY_WINDOW_ID) {
45
+ notifyOSC99(title, body);
46
+ } else {
47
+ notifyOSC777(title, body);
48
+ }
49
+ }
50
+
51
+ export default function (pi: ExtensionAPI) {
52
+ pi.on("agent_end", async () => {
53
+ notify("Pi", "Ready for input");
54
+ });
55
+ }
@@ -0,0 +1,72 @@
1
+ # Plan Mode Extension
2
+
3
+ Read-only exploration mode for safe code analysis.
4
+
5
+ ## Features
6
+
7
+ - **Built-in write tools disabled**: Disables edit/write while preserving other active tools
8
+ - **Bash allowlist**: Only read-only bash commands are allowed
9
+ - **Plan extraction**: Extracts numbered steps from `Plan:` sections
10
+ - **Progress tracking**: Widget shows completion status during execution
11
+ - **[DONE:n] markers**: Explicit step completion tracking
12
+ - **Session persistence**: State survives session resume
13
+
14
+ > Forked from the upstream `plan-mode` example: the slash command was
15
+ > renamed from `/plan` to `/plan-mode` so the `role-commands` extension can
16
+ > own `/plan` as a wrapper around the `planner` preset. See
17
+ > [`docs/extensions.md`](../../../docs/extensions.md#plan-mode-vs-plan) for
18
+ > when to use which.
19
+
20
+ ## Commands
21
+
22
+ - `/plan-mode` - Toggle plan mode
23
+ - `/todos` - Show current plan progress
24
+ - `Ctrl+Alt+P` - Toggle plan mode (shortcut)
25
+
26
+ ## Usage
27
+
28
+ 1. Enable plan mode with `/plan-mode` or `--plan` flag
29
+ 2. Ask the agent to analyze code and create a plan
30
+ 3. The agent should output a numbered plan under a `Plan:` header:
31
+
32
+ ```
33
+ Plan:
34
+ 1. First step description
35
+ 2. Second step description
36
+ 3. Third step description
37
+ ```
38
+
39
+ 4. Choose "Execute the plan" when prompted
40
+ 5. During execution, the agent marks steps complete with `[DONE:n]` tags
41
+ 6. Progress widget shows completion status
42
+
43
+ ## How It Works
44
+
45
+ ### Plan Mode (Read-Only)
46
+ - Built-in edit/write tools disabled
47
+ - Other active tools remain available
48
+ - Bash commands filtered through allowlist
49
+ - Agent creates a plan without making changes
50
+
51
+ ### Execution Mode
52
+ - Full tool access restored
53
+ - Agent executes steps in order
54
+ - `[DONE:n]` markers track completion
55
+ - Widget shows progress
56
+
57
+ ### Command Allowlist
58
+
59
+ Safe commands (allowed):
60
+ - File inspection: `cat`, `head`, `tail`, `less`, `more`
61
+ - Search: `grep`, `find`, `rg`, `fd`
62
+ - Directory: `ls`, `pwd`, `tree`
63
+ - Git read: `git status`, `git log`, `git diff`, `git branch`
64
+ - Package info: `npm list`, `npm outdated`, `yarn info`
65
+ - System info: `uname`, `whoami`, `date`, `uptime`
66
+
67
+ Blocked commands:
68
+ - File modification: `rm`, `mv`, `cp`, `mkdir`, `touch`
69
+ - Git write: `git add`, `git commit`, `git push`
70
+ - Package install: `npm install`, `yarn add`, `pip install`
71
+ - System: `sudo`, `kill`, `reboot`
72
+ - Editors: `vim`, `nano`, `code`
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Plan Mode Extension
3
+ *
4
+ * Read-only exploration mode for safe code analysis.
5
+ * When enabled, built-in write tools are disabled.
6
+ *
7
+ * Forked from upstream: the slash command was renamed `/plan` → `/plan-mode`
8
+ * so the `role-commands` extension can own `/plan` as a wrapper around the
9
+ * `planner` preset. See docs/extensions.md § "Plan mode vs /plan".
10
+ *
11
+ * Features:
12
+ * - /plan-mode command or Ctrl+Alt+P to toggle
13
+ * - Bash restricted to allowlisted read-only commands
14
+ * - Extracts numbered plan steps from "Plan:" sections
15
+ * - [DONE:n] markers to complete steps during execution
16
+ * - Progress tracking widget during execution
17
+ */
18
+
19
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
20
+ import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
21
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
+ import { Key } from "@earendil-works/pi-tui";
23
+ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.ts";
24
+
25
+ // Tools
26
+ const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
27
+ const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
28
+ const PLAN_MODE_DISABLED_TOOLS = new Set<string>(["edit", "write"]);
29
+ const PLAN_MANAGED_TOOLS = new Set<string>([...PLAN_MODE_TOOLS, ...NORMAL_MODE_TOOLS]);
30
+
31
+ interface PlanModeState {
32
+ enabled: boolean;
33
+ todos?: TodoItem[];
34
+ executing?: boolean;
35
+ toolsBeforePlanMode?: string[];
36
+ }
37
+
38
+ // Type guard for assistant messages
39
+ function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
40
+ return m.role === "assistant" && Array.isArray(m.content);
41
+ }
42
+
43
+ // Extract text content from an assistant message
44
+ function getTextContent(message: AssistantMessage): string {
45
+ return message.content
46
+ .filter((block): block is TextContent => block.type === "text")
47
+ .map((block) => block.text)
48
+ .join("\n");
49
+ }
50
+
51
+ export default function planModeExtension(pi: ExtensionAPI): void {
52
+ let planModeEnabled = false;
53
+ let executionMode = false;
54
+ let todoItems: TodoItem[] = [];
55
+ let toolsBeforePlanMode: string[] | undefined;
56
+
57
+ pi.registerFlag("plan", {
58
+ description: "Start in plan mode (read-only exploration)",
59
+ type: "boolean",
60
+ default: false,
61
+ });
62
+
63
+ function updateStatus(ctx: ExtensionContext): void {
64
+ // Footer status
65
+ if (executionMode && todoItems.length > 0) {
66
+ const completed = todoItems.filter((t) => t.completed).length;
67
+ ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
68
+ } else if (planModeEnabled) {
69
+ ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
70
+ } else {
71
+ ctx.ui.setStatus("plan-mode", undefined);
72
+ }
73
+
74
+ // Widget showing todo list
75
+ if (executionMode && todoItems.length > 0) {
76
+ const lines = todoItems.map((item) => {
77
+ if (item.completed) {
78
+ return (
79
+ ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
80
+ );
81
+ }
82
+ return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
83
+ });
84
+ ctx.ui.setWidget("plan-todos", lines);
85
+ } else {
86
+ ctx.ui.setWidget("plan-todos", undefined);
87
+ }
88
+ }
89
+
90
+ function uniqueToolNames(toolNames: string[]): string[] {
91
+ return [...new Set(toolNames)];
92
+ }
93
+
94
+ function getPlanModeTools(activeToolNames: string[]): string[] {
95
+ return uniqueToolNames([
96
+ ...activeToolNames.filter((name) => !PLAN_MODE_DISABLED_TOOLS.has(name)),
97
+ ...PLAN_MODE_TOOLS,
98
+ ]);
99
+ }
100
+
101
+ function getNormalModeTools(activeToolNames: string[]): string[] {
102
+ return uniqueToolNames([
103
+ ...NORMAL_MODE_TOOLS,
104
+ ...activeToolNames.filter((name) => !PLAN_MANAGED_TOOLS.has(name)),
105
+ ]);
106
+ }
107
+
108
+ function enablePlanModeTools(): void {
109
+ if (toolsBeforePlanMode === undefined) {
110
+ toolsBeforePlanMode = pi.getActiveTools();
111
+ }
112
+ pi.setActiveTools(getPlanModeTools(toolsBeforePlanMode));
113
+ }
114
+
115
+ function restoreNormalModeTools(): void {
116
+ pi.setActiveTools(toolsBeforePlanMode ?? getNormalModeTools(pi.getActiveTools()));
117
+ toolsBeforePlanMode = undefined;
118
+ }
119
+
120
+ function persistState(): void {
121
+ pi.appendEntry("plan-mode", {
122
+ enabled: planModeEnabled,
123
+ todos: todoItems,
124
+ executing: executionMode,
125
+ toolsBeforePlanMode,
126
+ });
127
+ }
128
+
129
+ function togglePlanMode(ctx: ExtensionContext): void {
130
+ planModeEnabled = !planModeEnabled;
131
+ executionMode = false;
132
+ todoItems = [];
133
+
134
+ if (planModeEnabled) {
135
+ enablePlanModeTools();
136
+ ctx.ui.notify("Plan mode enabled. Built-in write tools disabled.");
137
+ } else {
138
+ restoreNormalModeTools();
139
+ ctx.ui.notify("Plan mode disabled. Full access restored.");
140
+ }
141
+ updateStatus(ctx);
142
+ persistState();
143
+ }
144
+
145
+ // NOTE: renamed from /plan → /plan-mode so the role-commands extension can
146
+ // own /plan as a wrapper around the `planner` preset.
147
+ pi.registerCommand("plan-mode", {
148
+ description: "Toggle plan mode (read-only exploration)",
149
+ handler: async (_args, ctx) => togglePlanMode(ctx),
150
+ });
151
+
152
+ pi.registerCommand("todos", {
153
+ description: "Show current plan todo list",
154
+ handler: async (_args, ctx) => {
155
+ if (todoItems.length === 0) {
156
+ ctx.ui.notify("No todos. Create a plan first with /plan-mode", "info");
157
+ return;
158
+ }
159
+ const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
160
+ ctx.ui.notify(`Plan Progress:\n${list}`, "info");
161
+ },
162
+ });
163
+
164
+ pi.registerShortcut(Key.ctrlAlt("p"), {
165
+ description: "Toggle plan mode",
166
+ handler: async (ctx) => togglePlanMode(ctx),
167
+ });
168
+
169
+ // Block destructive bash commands in plan mode
170
+ pi.on("tool_call", async (event) => {
171
+ if (!planModeEnabled || event.toolName !== "bash") return;
172
+
173
+ const command = event.input.command as string;
174
+ if (!isSafeCommand(command)) {
175
+ return {
176
+ block: true,
177
+ reason: `Plan mode: command blocked (not allowlisted). Use /plan-mode to disable plan mode first.\nCommand: ${command}`,
178
+ };
179
+ }
180
+ });
181
+
182
+ // Filter out stale plan mode context when not in plan mode
183
+ pi.on("context", async (event) => {
184
+ if (planModeEnabled) return;
185
+
186
+ return {
187
+ messages: event.messages.filter((m) => {
188
+ const msg = m as AgentMessage & { customType?: string };
189
+ if (msg.customType === "plan-mode-context") return false;
190
+ if (msg.role !== "user") return true;
191
+
192
+ const content = msg.content;
193
+ if (typeof content === "string") {
194
+ return !content.includes("[PLAN MODE ACTIVE]");
195
+ }
196
+ if (Array.isArray(content)) {
197
+ return !content.some(
198
+ (c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"),
199
+ );
200
+ }
201
+ return true;
202
+ }),
203
+ };
204
+ });
205
+
206
+ // Inject plan/execution context before agent starts
207
+ pi.on("before_agent_start", async () => {
208
+ if (planModeEnabled) {
209
+ return {
210
+ message: {
211
+ customType: "plan-mode-context",
212
+ content: `[PLAN MODE ACTIVE]
213
+ You are in plan mode - a read-only exploration mode for safe code analysis.
214
+
215
+ Restrictions:
216
+ - Built-in edit and write tools are disabled
217
+ - Other currently active tools remain available
218
+ - Bash is restricted to an allowlist of read-only commands
219
+
220
+ Ask clarifying questions using the questionnaire tool.
221
+ Use brave-search skill via bash for web research.
222
+
223
+ Create a detailed numbered plan under a "Plan:" header:
224
+
225
+ Plan:
226
+ 1. First step description
227
+ 2. Second step description
228
+ ...
229
+
230
+ Do NOT attempt to make changes - just describe what you would do.`,
231
+ display: false,
232
+ },
233
+ };
234
+ }
235
+
236
+ if (executionMode && todoItems.length > 0) {
237
+ const remaining = todoItems.filter((t) => !t.completed);
238
+ const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
239
+ return {
240
+ message: {
241
+ customType: "plan-execution-context",
242
+ content: `[EXECUTING PLAN - Full tool access enabled]
243
+
244
+ Remaining steps:
245
+ ${todoList}
246
+
247
+ Execute each step in order.
248
+ After completing a step, include a [DONE:n] tag in your response.`,
249
+ display: false,
250
+ },
251
+ };
252
+ }
253
+ });
254
+
255
+ // Track progress after each turn
256
+ pi.on("turn_end", async (event, ctx) => {
257
+ if (!executionMode || todoItems.length === 0) return;
258
+ if (!isAssistantMessage(event.message)) return;
259
+
260
+ const text = getTextContent(event.message);
261
+ if (markCompletedSteps(text, todoItems) > 0) {
262
+ updateStatus(ctx);
263
+ }
264
+ persistState();
265
+ });
266
+
267
+ // Handle plan completion and plan mode UI
268
+ pi.on("agent_end", async (event, ctx) => {
269
+ // Check if execution is complete
270
+ if (executionMode && todoItems.length > 0) {
271
+ if (todoItems.every((t) => t.completed)) {
272
+ const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
273
+ pi.sendMessage(
274
+ { customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
275
+ { triggerTurn: false },
276
+ );
277
+ executionMode = false;
278
+ todoItems = [];
279
+ updateStatus(ctx);
280
+ persistState(); // Save cleared state so resume doesn't restore old execution mode
281
+ }
282
+ return;
283
+ }
284
+
285
+ if (!planModeEnabled || !ctx.hasUI) return;
286
+
287
+ // Extract todos from last assistant message
288
+ const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
289
+ if (lastAssistant) {
290
+ const extracted = extractTodoItems(getTextContent(lastAssistant));
291
+ if (extracted.length > 0) {
292
+ todoItems = extracted;
293
+ }
294
+ }
295
+
296
+ if (todoItems.length === 0) return;
297
+ persistState();
298
+
299
+ // Show plan steps and prompt for next action
300
+ const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
301
+ const planTodoListMessage = {
302
+ customType: "plan-todo-list",
303
+ content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
304
+ display: true,
305
+ };
306
+
307
+ const choice = await ctx.ui.select("Plan mode - what next?", [
308
+ "Execute the plan (track progress)",
309
+ "Stay in plan mode",
310
+ "Refine the plan",
311
+ ]);
312
+
313
+ if (choice?.startsWith("Execute")) {
314
+ const firstTodoItem = todoItems[0];
315
+ if (!firstTodoItem) return;
316
+
317
+ planModeEnabled = false;
318
+ executionMode = true;
319
+ restoreNormalModeTools();
320
+ updateStatus(ctx);
321
+ persistState();
322
+
323
+ const remainingList = todoItems.map((t) => `${t.step}. ${t.text}`).join("\n");
324
+ const execMessage = `Execute the plan.
325
+
326
+ Remaining steps:
327
+ ${remainingList}
328
+
329
+ Start with: ${firstTodoItem.text}
330
+ After completing a step, include a [DONE:n] tag in your response.`;
331
+ pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" });
332
+ pi.sendMessage(
333
+ { customType: "plan-mode-execute", content: execMessage, display: true },
334
+ { triggerTurn: true, deliverAs: "followUp" },
335
+ );
336
+ } else if (choice === "Refine the plan") {
337
+ const refinement = await ctx.ui.editor("Refine the plan:", "");
338
+ if (refinement?.trim()) {
339
+ pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" });
340
+ pi.sendUserMessage(refinement.trim(), { deliverAs: "followUp" });
341
+ }
342
+ }
343
+ });
344
+
345
+ // Restore state on session start/resume
346
+ pi.on("session_start", async (_event, ctx) => {
347
+ if (pi.getFlag("plan") === true) {
348
+ planModeEnabled = true;
349
+ }
350
+
351
+ const entries = ctx.sessionManager.getEntries();
352
+
353
+ // Restore persisted state
354
+ const planModeEntry = entries
355
+ .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
356
+ .pop() as { data?: PlanModeState } | undefined;
357
+
358
+ if (planModeEntry?.data) {
359
+ planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
360
+ todoItems = planModeEntry.data.todos ?? todoItems;
361
+ executionMode = planModeEntry.data.executing ?? executionMode;
362
+ toolsBeforePlanMode = planModeEntry.data.toolsBeforePlanMode ?? toolsBeforePlanMode;
363
+ }
364
+
365
+ // On resume: re-scan messages to rebuild completion state
366
+ // Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
367
+ const isResume = planModeEntry !== undefined;
368
+ if (isResume && executionMode && todoItems.length > 0) {
369
+ // Find the index of the last plan-mode-execute entry (marks when current execution started)
370
+ let executeIndex = -1;
371
+ for (let i = entries.length - 1; i >= 0; i--) {
372
+ const entry = entries[i] as { type: string; customType?: string };
373
+ if (entry.customType === "plan-mode-execute") {
374
+ executeIndex = i;
375
+ break;
376
+ }
377
+ }
378
+
379
+ // Only scan messages after the execute marker
380
+ const messages: AssistantMessage[] = [];
381
+ for (let i = executeIndex + 1; i < entries.length; i++) {
382
+ const entry = entries[i];
383
+ if (entry.type === "message" && "message" in entry && isAssistantMessage(entry.message as AgentMessage)) {
384
+ messages.push(entry.message as AssistantMessage);
385
+ }
386
+ }
387
+ const allText = messages.map(getTextContent).join("\n");
388
+ markCompletedSteps(allText, todoItems);
389
+ }
390
+
391
+ if (planModeEnabled) {
392
+ enablePlanModeTools();
393
+ }
394
+ updateStatus(ctx);
395
+ });
396
+ }