@janvitos/pi-plan-build 0.1.30 → 0.1.32

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 CHANGED
@@ -13,12 +13,13 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
13
13
  - Per-session plans at `~/.pi/agent/plans/<session-id>.md`.
14
14
  - In Plan mode, built-in `edit` and `write` are restricted to the exact plan file.
15
15
  - Interactive `question`, `plan_enter`, and `plan_exit` tools.
16
- - Informational questions are answered directly in Plan mode, with read-only inspection when needed and no plan or approval ceremony.
16
+ - Plan mode supports read-only conversation and research across multiple turns, then persists the final plan when it is ready for approval.
17
17
  - The complete saved plan is rendered in the transcript before approval—without the built-in write preview's truncation.
18
- - Three approval actions:
18
+ - Existing approval actions remain available:
19
19
  - **Switch to Build and implement here**
20
20
  - **Start fresh and implement**
21
21
  - **Stay in Plan mode**
22
+ - **Experimental:** In fullscreen TUI, valid checklist plans also offer **Implement step by step**: a passive, non-overlapping docked right panel keeps the plan visible while natural-language prompts gate steps and return completed work for review. This feature is still under active development.
22
23
  - Staying in Plan mode—or pressing Escape in the approval dialog—produces a durable acknowledgement and stops the run until the user responds.
23
24
  - Mode state survives reloads, resumes, and forks.
24
25
  - When Pi recreates the custom editor, the latest 100 user prompts from the active session branch are restored for Up/Down history navigation.
@@ -28,6 +29,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
28
29
  - Pi `0.84.2` or newer
29
30
  - Node.js `22.6` or newer for the test command
30
31
  - TUI or RPC UI support for interactive questions and approval dialogs
32
+ - Pi fullscreen TUI mode for the optional docked step-by-step plan panel; regular mode and all existing workflows remain supported
31
33
 
32
34
  ## Install
33
35
 
@@ -82,25 +84,53 @@ Selecting **Stay in Plan mode**, or pressing Escape while the approval dialog is
82
84
 
83
85
  Both actions leave Plan mode active, stop the agent, and wait for the next user message.
84
86
 
87
+ ### Step-by-step execution panel (Experimental)
88
+
89
+ > **Experimental feature:** Step-by-step execution is still being developed. Expect UI and workflow changes, and please report issues or unexpected behavior.
90
+
91
+ When Pi uses `"tuiMode": "fullscreen"` and the saved plan contains top-level `- [ ]` items under `## Implementation Steps`, `plan_exit` also offers **Implement step by step**. This is opt-in per plan; it does not replace either one-shot implementation option.
92
+
93
+ The passive 72-column right panel reserves terminal columns, so the transcript and editor reflow instead of being covered. Long step instructions wrap across aligned continuation rows rather than being clipped. It never accepts focus or keyboard input and collapses below 132 terminal columns. Control the workflow entirely through natural-language prompts, for example:
94
+
95
+ - “Implement the next step” or “Start step 2.”
96
+ - “Step 1 is complete,” “I verified that one,” or “I already handled this.”
97
+ - “Change step 3 to …” or “Skip this step.”
98
+ - “Accept this result” or “Correct it by …”
99
+ - “Pause the plan,” “hide the plan,” or “show the plan.”
100
+ - “Cancel this plan” at any point to end step-by-step execution immediately.
101
+
102
+ The extension exposes these actions to the agent through `plan_step_control`; project mutations remain blocked until the user clearly approves a ready step or explicitly indicates that it is already complete. The agent interprets intent contextually rather than requiring exact phrases, while the extension validates every resulting state transition. Cancelling removes the panel and execution guards immediately, restores the full-width layout, and preserves the saved plan file for reference. The agent implements only that step, calls `plan_step_complete`, and waits for the user's next prompt. Accepting a result makes the next step ready but never starts it automatically. Progress, revisions, summaries, and panel visibility survive reload/resume. If such a session is opened in regular mode, progress is retained but cannot advance until fullscreen mode is restored; no overlay fallback is used.
103
+
104
+ Enable fullscreen in `~/.pi/agent/settings.json` and restart Pi:
105
+
106
+ ```json
107
+ {
108
+ "tuiMode": "fullscreen"
109
+ }
110
+ ```
111
+
112
+ The integration uses Pi 0.84.2's public fullscreen layout primitives plus a guarded read of its runtime layout root because the current extension API exposes `setLayoutRoot()` but not a corresponding getter.
113
+
85
114
  ## Plan-mode permissions
86
115
 
87
116
  Normal tools remain visible so the model can inspect the project. While a Plan run is active:
88
117
 
89
118
  - `edit` and `write` are permitted only for the canonical session plan file;
119
+ - the Plan prompt reserves those mutations for finalizing or explicitly revising the plan, not ordinary conversation or research;
90
120
  - other `edit` and `write` calls are blocked by the extension;
91
121
  - bash is not restricted at the permission layer, but the Plan prompt explicitly permits read-only exploration only.
92
122
 
93
123
  This mirrors the intended permission-oriented workflow rather than hiding normal tool schemas.
94
124
 
95
- ### Informational questions
125
+ ### Conversational planning
96
126
 
97
- Plan mode distinguishes informational Q&A from implementation planning. If a request only asks for an explanation or information, the agent answers directly and ends normally. It may inspect the project with read-only tools when the answer depends on local context, but it does not create or update a plan, ask planning questions unnecessarily, or call `plan_exit`. Plan mode remains selected for the next request.
127
+ Plan mode follows OpenCode’s standard conversational lifecycle while retaining this extension’s persisted approval flow. The agent can answer informational questions, discuss requirements and tradeoffs, inspect the project with read-only tools, and ask follow-up questions across multiple turns. Ordinary conversation and research do not create or update the plan file and do not invoke `plan_exit`.
98
128
 
99
- Requests to change code or produce an implementation plan continue through the normal plan-file and approval workflow.
129
+ Once the request is sufficiently understood and the agent is ready to present the final implementation plan—or the user explicitly asks it to finalize—the agent writes the complete canonical plan and calls `plan_exit`. An existing plan file does not trigger automatic edits during unrelated discussion.
100
130
 
101
131
  ## Design and attribution
102
132
 
103
- Pi Plan & Build is an independent extension with its own workflow and UI behavior. Its original mode prompts and transition semantics were informed by OpenCode 1.18.16, while clean-session implementation ideas were informed by the former `pi-plan-mode` extension. Those behaviors have since been adapted and extended for Pi; this project is not affiliated with either project.
133
+ Pi Plan & Build is an independent extension with its own workflow and UI behavior. Its conversational read-only lifecycle follows OpenCode’s standard Plan agent, while persisted finalization and approval are adapted for Pi. Earlier prompt and transition semantics were informed by OpenCode 1.18.16, and clean-session implementation ideas were informed by the former `pi-plan-mode` extension. This project is not affiliated with either project.
104
134
 
105
135
  The Plan workflow uses Pi's native exploration tools directly and does not bundle or require subagents.
106
136
 
@@ -111,7 +141,7 @@ npm test
111
141
  npm pack --dry-run
112
142
  ```
113
143
 
114
- The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content, and question formatting.
144
+ The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, conversational Plan guidance, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content, question formatting and cancellation, structured checklist parsing, step state transitions, safe instruction revisions, and responsive panel rendering.
115
145
 
116
146
  ### Publishing
117
147
 
package/index.ts CHANGED
@@ -2,15 +2,34 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { CustomEditor, getAgentDir, getMarkdownTheme, type EntryRenderer, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import { Key, Markdown, matchesKey, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
+ import { HStack, Key, Markdown, matchesKey, Text, truncateToWidth, visibleWidth, isViewportTUI, type Component, type TUI, type ViewportTUI } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
7
  import { registerQuestionTool } from "./question-ui.ts";
8
8
  import {
9
9
  buildPlanReminder,
10
+ buildPlanStepReminder,
11
+ buildPlanStepWaitingReminder,
10
12
  PLAN_ENTER_DESCRIPTION,
11
13
  PLAN_EXIT_DESCRIPTION,
14
+ PLAN_STEP_COMPLETE_DESCRIPTION,
12
15
  PLAN_TO_BUILD_REMINDER,
13
16
  } from "./prompts.ts";
17
+ import {
18
+ acceptPlanStep,
19
+ activePlanStep,
20
+ completePlanStep,
21
+ createPlanExecution,
22
+ decodePlanExecution,
23
+ pausePlanExecution,
24
+ requestPlanStepCorrections,
25
+ revisePlanStep,
26
+ skipPlanStep,
27
+ startPlanStep,
28
+ submitPlanStepForReview,
29
+ updatePlanChecklistStep,
30
+ type PlanExecutionState,
31
+ } from "./plan-execution.ts";
32
+ import { PlanPanel } from "./plan-panel.ts";
14
33
  import {
15
34
  applyManualSelection,
16
35
  buildFreshImplementationHandoff,
@@ -48,7 +67,10 @@ const LEGACY_PLAN_REVIEW_ENTRY_TYPE = "opencode-plan-review";
48
67
  const MODE_NOTICE_ENTRY_TYPE = "pi-plan-build-notice";
49
68
  const LEGACY_MODE_NOTICE_ENTRY_TYPE = "opencode-mode-notice";
50
69
  const STATUS_KEY = "pi-plan-build-mode";
51
- const MANAGED_TOOLS = new Set(["question", "plan_enter", "plan_exit"]);
70
+ const PLAN_STEP_CHOICE = "Implement step by step";
71
+ const PANEL_WIDTH = 72;
72
+ const PANEL_MIN_TERMINAL_WIDTH = 132;
73
+ const MANAGED_TOOLS = new Set(["question", "plan_enter", "plan_exit", "plan_step_control", "plan_step_complete"]);
52
74
  const MODE_ADDED_TOOLS = new Set([...MANAGED_TOOLS, "edit", "write"]);
53
75
  const EMPTY_PARAMETERS = Type.Object({});
54
76
 
@@ -58,6 +80,7 @@ interface StoredState {
58
80
  selectedMode: Mode;
59
81
  pendingReminder?: "plan" | "build";
60
82
  toolsBeforeModes?: string[];
83
+ execution?: PlanExecutionState;
61
84
  }
62
85
 
63
86
  function shorten(filePath: string, cwd: string): string {
@@ -76,6 +99,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
76
99
  let currentContext: ExtensionContext | undefined;
77
100
  let requestEditorRender: (() => void) | undefined;
78
101
  let freshImplementationRequest: FreshImplementationRequest | undefined;
102
+ let execution: PlanExecutionState | undefined;
103
+ let panel: PlanPanel | undefined;
104
+ let panelTui: (TUI & Partial<ViewportTUI>) | undefined;
105
+ let originalLayoutRoot: Component | undefined;
106
+ let panelLayoutRoot: Component | undefined;
107
+ let fullscreenPanelCapable = false;
79
108
 
80
109
  pi.registerFlag("plan", {
81
110
  description: "Start in Plan mode",
@@ -98,13 +127,57 @@ export default function planBuildModes(pi: ExtensionAPI): void {
98
127
  pi.registerEntryRenderer<{ message: string }>(LEGACY_MODE_NOTICE_ENTRY_TYPE, renderModeNotice);
99
128
 
100
129
  function stateData(): StoredState {
101
- return { version: 1, selectedMode, pendingReminder, toolsBeforeModes };
130
+ return { version: 1, selectedMode, pendingReminder, toolsBeforeModes, ...(execution ? { execution } : {}) };
102
131
  }
103
132
 
104
133
  function persist(): void {
105
134
  pi.appendEntry(STATE_TYPE, stateData());
106
135
  }
107
136
 
137
+ function updateExecution(next: PlanExecutionState): void {
138
+ execution = next;
139
+ panel?.setState(next);
140
+ persist();
141
+ panelTui?.requestRender();
142
+ }
143
+
144
+ function removePanelLayout(): void {
145
+ if (panelLayoutRoot && panelTui && originalLayoutRoot) panelTui.setLayoutRoot?.(originalLayoutRoot);
146
+ panelLayoutRoot = undefined;
147
+ panel = undefined;
148
+ panelTui?.requestRender();
149
+ }
150
+
151
+ function cancelPlanExecution(): void {
152
+ execution = undefined;
153
+ removePanelLayout();
154
+ persist();
155
+ applyTools("build");
156
+ }
157
+
158
+ function ensurePanelLayout(): boolean {
159
+ if (!execution || !fullscreenPanelCapable || !panelTui || !originalLayoutRoot || !currentContext) return false;
160
+ if (!panel) panel = new PlanPanel(execution, currentContext.ui.theme);
161
+ else panel.setState(execution);
162
+ if (!panelLayoutRoot) {
163
+ panelLayoutRoot = new HStack([
164
+ { component: originalLayoutRoot, basis: 0, grow: 1, shrink: 1, minSize: 58 },
165
+ {
166
+ component: panel,
167
+ basis: PANEL_WIDTH,
168
+ grow: 0,
169
+ shrink: 0,
170
+ minSize: PANEL_WIDTH,
171
+ maxSize: PANEL_WIDTH,
172
+ visible: (viewport) => execution?.panelVisible !== false && viewport.width >= PANEL_MIN_TERMINAL_WIDTH,
173
+ },
174
+ ]);
175
+ panelTui.setLayoutRoot?.(panelLayoutRoot);
176
+ }
177
+ panelTui.requestRender();
178
+ return true;
179
+ }
180
+
108
181
  function updateModeIndicator(ctx: ExtensionContext): void {
109
182
  // Clear the legacy footer status; the mode is now rendered inside the editor.
110
183
  ctx.ui.setStatus(STATUS_KEY, undefined);
@@ -122,7 +195,13 @@ export default function planBuildModes(pi: ExtensionAPI): void {
122
195
  if (mode === "plan") {
123
196
  pi.setActiveTools(unique([...base, "edit", "write", "question", "plan_exit"]));
124
197
  } else {
125
- pi.setActiveTools(unique([...base, "question", "plan_enter"]));
198
+ pi.setActiveTools(unique([
199
+ ...base,
200
+ "question",
201
+ "plan_enter",
202
+ ...(execution ? ["plan_step_control"] : []),
203
+ ...(activePlanStep(execution) ? ["plan_step_complete"] : []),
204
+ ]));
126
205
  }
127
206
  }
128
207
 
@@ -130,6 +209,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
130
209
  await fs.promises.mkdir(path.dirname(planPath), { recursive: true });
131
210
  }
132
211
 
212
+ function describePlanFile(): string {
213
+ return fs.existsSync(planPath)
214
+ ? `A plan file already exists at ${planPath}. Read it when relevant, but leave it unchanged while discussing or researching. Use the edit tool only when finalizing or explicitly revising the plan.`
215
+ : `No plan file exists yet. When ready to finalize, create your plan at ${planPath} using the write tool.`;
216
+ }
217
+
133
218
  async function selectMode(mode: Mode, ctx: ExtensionContext, source: "manual" | "tool"): Promise<void> {
134
219
  if (mode === selectedMode && (source === "manual" || mode === runMode)) return;
135
220
  const previous = selectedMode;
@@ -278,12 +363,8 @@ export default function planBuildModes(pi: ExtensionAPI): void {
278
363
  executionMode: "sequential",
279
364
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
280
365
  await selectMode("plan", ctx, "tool");
281
- const exists = fs.existsSync(planPath);
282
- const info = exists
283
- ? `A plan file already exists at ${planPath}. You can read it and make incremental edits using the edit tool.`
284
- : `No plan file exists yet. You should create your plan at ${planPath} using the write tool.`;
285
366
  return {
286
- content: [{ type: "text", text: buildPlanReminder(info) }],
367
+ content: [{ type: "text", text: buildPlanReminder(describePlanFile()) }],
287
368
  details: { mode: "plan", planPath },
288
369
  };
289
370
  },
@@ -295,6 +376,128 @@ export default function planBuildModes(pi: ExtensionAPI): void {
295
376
  },
296
377
  });
297
378
 
379
+ pi.registerTool({
380
+ name: "plan_step_control",
381
+ label: "Control Plan Execution",
382
+ description: `Use this tool to translate the user's natural-language instructions into one step-by-step plan action. Available actions: start a ready step, complete a clearly finished ready or reviewed step, accept a reviewed step, request corrections, skip a ready step, revise an unimplemented instruction, pause/resume or cancel execution, or hide/show the visual plan panel. Interpret clear user intent semantically, including direct completion statements, but do not advance based on hypothetical, uncertain, or unrelated conversation.`,
383
+ parameters: Type.Object({
384
+ action: Type.Union([
385
+ Type.Literal("start"),
386
+ Type.Literal("complete"),
387
+ Type.Literal("accept"),
388
+ Type.Literal("correct"),
389
+ Type.Literal("skip"),
390
+ Type.Literal("revise"),
391
+ Type.Literal("pause"),
392
+ Type.Literal("resume"),
393
+ Type.Literal("cancel"),
394
+ Type.Literal("hide"),
395
+ Type.Literal("show"),
396
+ ]),
397
+ step: Type.Optional(Type.Number({ description: "One-based step number; defaults to the current ready/review step", minimum: 1 })),
398
+ instruction: Type.Optional(Type.String({ description: "Replacement instruction required for revise" })),
399
+ }),
400
+ executionMode: "sequential",
401
+ async execute(_toolCallId, params) {
402
+ if (!execution) throw new Error("No step-by-step plan is active");
403
+ const target = params.step === undefined
404
+ ? execution.steps.find((step) => step.status === "ready" || step.status === "review")
405
+ : execution.steps[Math.floor(params.step) - 1];
406
+ const finish = (message: string) => ({
407
+ content: [{ type: "text" as const, text: message }],
408
+ details: { action: params.action, stepId: target?.id },
409
+ terminate: true,
410
+ });
411
+
412
+ if (params.action === "cancel") {
413
+ cancelPlanExecution();
414
+ return finish("Step-by-step execution was cancelled. The panel and execution guards were removed; the saved plan file remains available.");
415
+ }
416
+ if (params.action === "hide" || params.action === "show") {
417
+ updateExecution({ ...execution, panelVisible: params.action === "show" });
418
+ if (params.action === "show") ensurePanelLayout();
419
+ return finish(`The visual plan panel is now ${params.action === "show" ? "visible" : "hidden"}. Progress is unchanged.`);
420
+ }
421
+ if (execution.status === "completed") throw new Error("The plan is complete; only hide or show actions remain available");
422
+ if (params.action === "pause" || params.action === "resume") {
423
+ if ((params.action === "pause") === (execution.status === "paused")) return finish(`Plan execution is already ${params.action === "pause" ? "paused" : "running"}.`);
424
+ updateExecution(pausePlanExecution(execution));
425
+ return finish(`Plan execution is now ${params.action === "pause" ? "paused" : "running"}.`);
426
+ }
427
+ if (!target) throw new Error("No matching plan step is available for that action");
428
+ if (params.action === "start") {
429
+ if (execution.status === "paused") throw new Error("Resume plan execution before starting a step");
430
+ updateExecution(startPlanStep(execution, target.id));
431
+ applyTools("build");
432
+ pi.sendUserMessage(`Implement plan step ${execution.steps.findIndex((step) => step.id === target.id) + 1}: ${target.text}`, { deliverAs: "followUp" });
433
+ return finish("The requested step is approved. Its implementation is starting in a follow-up turn.");
434
+ }
435
+ if (params.action === "complete") {
436
+ updateExecution(completePlanStep(execution, target.id));
437
+ return finish(execution.status === "completed" ? "The step was marked complete and the plan is complete." : "The step was marked complete. The next step is ready and awaits user instruction.");
438
+ }
439
+ if (params.action === "accept") {
440
+ updateExecution(acceptPlanStep(execution, target.id));
441
+ return finish(execution.status === "completed" ? "The reviewed step was accepted and the plan is complete." : "The reviewed step was accepted. The next step is ready and awaits user instruction.");
442
+ }
443
+ if (params.action === "correct") {
444
+ updateExecution(requestPlanStepCorrections(execution, target.id));
445
+ applyTools("build");
446
+ pi.sendUserMessage(`Apply the corrections requested in the user's preceding message to plan step ${execution.steps.findIndex((step) => step.id === target.id) + 1}.`, { deliverAs: "followUp" });
447
+ return finish("The reviewed step was reopened. The requested corrections are starting in a follow-up turn.");
448
+ }
449
+ if (params.action === "skip") {
450
+ updateExecution(skipPlanStep(execution, target.id));
451
+ return finish(execution.status === "completed" ? "The step was skipped and the plan is complete." : "The step was skipped. The next step awaits user instruction.");
452
+ }
453
+ if (!params.instruction?.trim()) throw new Error("Revising a step requires a replacement instruction");
454
+ const plan = await fs.promises.readFile(planPath, "utf8");
455
+ const updatedPlan = updatePlanChecklistStep(plan, target.sourceLine, params.instruction);
456
+ await fs.promises.writeFile(planPath, updatedPlan, "utf8");
457
+ updateExecution(revisePlanStep(execution, target.id, params.instruction, updatedPlan));
458
+ return finish("The plan step instruction was revised and is awaiting user approval.");
459
+ },
460
+ renderCall(args, theme) {
461
+ const requestedStep = typeof args.step === "number" && Number.isFinite(args.step) ? Math.max(1, Math.floor(args.step)) : undefined;
462
+ const inferredStep = requestedStep ?? (execution
463
+ ? execution.steps.findIndex((step) => step.status === "ready" || step.status === "review") + 1
464
+ : 0);
465
+ const label = inferredStep > 0 ? `Step ${inferredStep}: ${args.action}` : `Plan: ${args.action}`;
466
+ return new Text(theme.fg("toolTitle", theme.bold(label)), 0, 0);
467
+ },
468
+ renderResult(result, _options, theme, context) {
469
+ const text = result.content.find((item) => item.type === "text")?.text ?? "Plan state updated";
470
+ return new Text(theme.fg(context.isError ? "error" : "success", text), 0, 0);
471
+ },
472
+ });
473
+
474
+ pi.registerTool({
475
+ name: "plan_step_complete",
476
+ label: "Complete Plan Step",
477
+ description: PLAN_STEP_COMPLETE_DESCRIPTION,
478
+ parameters: Type.Object({
479
+ summary: Type.String({ description: "Concise summary of what was implemented and verified" }),
480
+ }),
481
+ executionMode: "sequential",
482
+ async execute(_toolCallId, params) {
483
+ const step = activePlanStep(execution);
484
+ if (!execution || !step) throw new Error("No plan step is currently active");
485
+ updateExecution(submitPlanStepForReview(execution, step.id, params.summary));
486
+ applyTools("build");
487
+ return {
488
+ content: [{ type: "text", text: "The active plan step is awaiting user review. Stop now and do not begin another step." }],
489
+ details: { stepId: step.id, review: true },
490
+ terminate: true,
491
+ };
492
+ },
493
+ renderCall(_args, theme) {
494
+ return new Text(theme.fg("toolTitle", theme.bold("Submit plan step for review")), 0, 0);
495
+ },
496
+ renderResult(_result, _options, theme, context) {
497
+ return new Text(theme.fg(context.isError ? "error" : "success", context.isError ? "Plan step submission failed" : "Plan step ready for review"), 0, 0);
498
+ },
499
+ });
500
+
298
501
  pi.registerTool({
299
502
  name: "plan_exit",
300
503
  label: "Exit Plan Mode",
@@ -313,10 +516,43 @@ export default function planBuildModes(pi: ExtensionAPI): void {
313
516
  if (!plan.trim()) throw new Error("Cannot request plan approval because the plan file is empty");
314
517
  pi.appendEntry(PLAN_REVIEW_ENTRY_TYPE, { plan, planPath });
315
518
  const displayPath = shorten(planPath, ctx.cwd);
519
+ let stepExecution: PlanExecutionState | undefined;
520
+ let checklistError: string | undefined;
521
+ const panelAvailable = fullscreenPanelCapable && (panelTui?.terminal.columns ?? 0) >= PANEL_MIN_TERMINAL_WIDTH;
522
+ if (panelAvailable) {
523
+ try {
524
+ stepExecution = createPlanExecution(plan);
525
+ } catch (error: unknown) {
526
+ checklistError = error instanceof Error ? error.message : String(error);
527
+ }
528
+ }
529
+ const choices = [
530
+ PLAN_EXIT_APPROVE_CHOICE,
531
+ ...(stepExecution ? [PLAN_STEP_CHOICE] : []),
532
+ PLAN_EXIT_FRESH_CHOICE,
533
+ PLAN_EXIT_STAY_CHOICE,
534
+ ];
316
535
  const selection = normalizePlanExitChoice(await ctx.ui.select(
317
536
  `Build Agent: Plan at ${displayPath} is complete. What would you like to do?`,
318
- [PLAN_EXIT_APPROVE_CHOICE, PLAN_EXIT_FRESH_CHOICE, PLAN_EXIT_STAY_CHOICE],
537
+ choices,
319
538
  ));
539
+ if (selection.choice === PLAN_STEP_CHOICE && stepExecution) {
540
+ freshImplementationRequest = undefined;
541
+ execution = stepExecution;
542
+ await selectMode("build", ctx, "tool");
543
+ updateExecution(stepExecution);
544
+ ensurePanelLayout();
545
+ return {
546
+ content: [{ type: "text", text: "Step-by-step execution is ready. Stop now and wait for the user's natural-language instruction in the composer; the plan panel is visual-only." }],
547
+ details: { approved: true, action: "step-by-step", mode: "build", planPath },
548
+ terminate: true,
549
+ };
550
+ }
551
+ if (panelAvailable && !stepExecution && checklistError) {
552
+ ctx.ui.notify(`Step-by-step execution is unavailable: ${checklistError}.`, "warning");
553
+ } else if (fullscreenPanelCapable && !panelAvailable) {
554
+ ctx.ui.notify(`Step-by-step execution requires a terminal at least ${PANEL_MIN_TERMINAL_WIDTH} columns wide.`, "warning");
555
+ }
320
556
  const decision = classifyPlanExitChoice(selection.choice);
321
557
  if (decision === "stay") {
322
558
  freshImplementationRequest = undefined;
@@ -352,6 +588,9 @@ export default function planBuildModes(pi: ExtensionAPI): void {
352
588
  },
353
589
  renderResult(result, _options, theme, context) {
354
590
  const details = result.details as { approved?: boolean; action?: string } | undefined;
591
+ if (details?.action === "step-by-step" && !context.isError) {
592
+ return new Text(theme.fg("success", "Step-by-step execution ready — waiting for your instruction."), 0, 0);
593
+ }
355
594
  if (details?.action === "implement-fresh" && !context.isError) {
356
595
  return new Text(
357
596
  theme.fg("success", "Clean-session implementation selected — starting automatically."),
@@ -367,6 +606,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
367
606
  });
368
607
 
369
608
  pi.on("tool_call", async (event, ctx) => {
609
+ if ((runMode ?? selectedMode) === "build" && execution && execution.status !== "completed" && !activePlanStep(execution) && (event.toolName === "edit" || event.toolName === "write" || event.toolName === "bash")) {
610
+ return {
611
+ block: true,
612
+ reason: "Step-by-step execution is waiting for an explicit natural-language instruction from the user; no step is approved for project mutations.",
613
+ };
614
+ }
370
615
  if (runMode !== "plan" || (event.toolName !== "edit" && event.toolName !== "write")) return;
371
616
  const inputPath = (event.input as { path?: unknown }).path;
372
617
  if (isAllowedPlanMutation(ctx.cwd, inputPath, planPath)) return;
@@ -382,12 +627,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
382
627
  let content: string | undefined;
383
628
  if (runMode === "plan") {
384
629
  await ensurePlanDirectory();
385
- const exists = fs.existsSync(planPath);
386
- content = buildPlanReminder(
387
- exists
388
- ? `A plan file already exists at ${planPath}. You can read it and make incremental edits using the edit tool.`
389
- : `No plan file exists yet. You should create your plan at ${planPath} using the write tool.`,
390
- );
630
+ content = buildPlanReminder(describePlanFile());
631
+ } else if (activePlanStep(execution)) {
632
+ const step = activePlanStep(execution)!;
633
+ content = buildPlanStepReminder(planPath, execution!.steps.findIndex((item) => item.id === step.id) + 1, execution!.steps.length, step.text);
634
+ } else if (execution && execution.status !== "completed") {
635
+ content = buildPlanStepWaitingReminder(execution.steps.map((step, index) => `${index + 1}. [${step.status}] ${step.text}`).join("\n"));
391
636
  } else if (pendingReminder === "build") {
392
637
  content = PLAN_TO_BUILD_REMINDER;
393
638
  if (fs.existsSync(planPath)) content += `\n\nA plan file exists at ${planPath}. You should execute the plan defined within it.`;
@@ -402,6 +647,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
402
647
  runMode = undefined;
403
648
  applyTools(selectedMode);
404
649
  updateModeIndicator(ctx);
650
+ if (execution) ensurePanelLayout();
405
651
  });
406
652
 
407
653
  pi.on("session_start", async (event, ctx) => {
@@ -416,13 +662,15 @@ export default function planBuildModes(pi: ExtensionAPI): void {
416
662
  .pop() as { data?: unknown } | undefined;
417
663
  const decoded = decodeModeState(latest?.data);
418
664
  const raw = latest?.data as StoredState | undefined;
665
+ execution = decodePlanExecution(raw?.execution);
419
666
  selectedMode = decoded?.selectedMode ?? (pi.getFlag("plan") === true ? "plan" : "build");
420
667
  pendingReminder = raw?.pendingReminder ?? (decoded ? undefined : pi.getFlag("plan") === true ? "plan" : undefined);
421
668
  toolsBeforeModes = Array.isArray(raw?.toolsBeforeModes)
422
669
  ? raw.toolsBeforeModes.filter((name): name is string => typeof name === "string" && !MANAGED_TOOLS.has(name))
423
670
  : pi.getActiveTools().filter((name) => !MANAGED_TOOLS.has(name));
424
671
  planPath = makePlanPath(path.join(getAgentDir(), "plans"), ctx.sessionManager.getSessionId());
425
- if (selectedMode === "plan") await ensurePlanDirectory();
672
+ if (selectedMode === "plan" || execution) await ensurePlanDirectory();
673
+ if (execution && !fs.existsSync(planPath)) await fs.promises.writeFile(planPath, execution.planMarkdown, "utf8");
426
674
  applyTools(selectedMode);
427
675
  updateModeIndicator(ctx);
428
676
 
@@ -572,6 +820,13 @@ export default function planBuildModes(pi: ExtensionAPI): void {
572
820
  const editor = new ModeEditor(tui, theme, keybindings);
573
821
  for (const prompt of promptHistory) editor.addToHistory(prompt);
574
822
  requestEditorRender = () => editor.requestModeRender();
823
+ panelTui = tui;
824
+ fullscreenPanelCapable = isViewportTUI(tui) && typeof (tui as ViewportTUI).setLayoutRoot === "function";
825
+ if (fullscreenPanelCapable && !originalLayoutRoot) {
826
+ originalLayoutRoot = (tui as TUI & { layoutRoot?: Component }).layoutRoot;
827
+ fullscreenPanelCapable = originalLayoutRoot !== undefined;
828
+ }
829
+ if (execution) ensurePanelLayout();
575
830
  editor.onCycle = () => {
576
831
  if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");
577
832
  };
@@ -586,14 +841,23 @@ export default function planBuildModes(pi: ExtensionAPI): void {
586
841
  };
587
842
  return editor;
588
843
  });
844
+ if (execution && !fullscreenPanelCapable) {
845
+ ctx.ui.notify("Step-by-step progress was restored, but its plan panel requires fullscreen TUI mode. Progress is preserved.", "warning");
846
+ }
589
847
  }
590
848
  });
591
849
 
592
850
  pi.on("session_shutdown", async (_event, ctx) => {
851
+ removePanelLayout();
593
852
  ctx.ui.setStatus(STATUS_KEY, undefined);
594
853
  ctx.ui.setFooter(undefined);
595
854
  ctx.ui.setEditorComponent(undefined);
596
855
  requestEditorRender = undefined;
856
+ panel = undefined;
857
+ panelTui = undefined;
858
+ panelLayoutRoot = undefined;
859
+ originalLayoutRoot = undefined;
860
+ fullscreenPanelCapable = false;
597
861
  currentContext = undefined;
598
862
  });
599
863
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Plan safely, approve explicitly, then implement here or in a clean session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,13 +23,15 @@
23
23
  "index.ts",
24
24
  "prompts.ts",
25
25
  "question-ui.ts",
26
+ "plan-execution.ts",
27
+ "plan-panel.ts",
26
28
  "utils.ts"
27
29
  ],
28
30
  "publishConfig": {
29
31
  "access": "public"
30
32
  },
31
33
  "scripts": {
32
- "test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts",
34
+ "test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts plan-execution.test.ts plan-panel.test.ts",
33
35
  "prepublishOnly": "npm test"
34
36
  },
35
37
  "peerDependencies": {
@@ -0,0 +1,185 @@
1
+ export type PlanStepStatus = "pending" | "ready" | "active" | "review" | "completed" | "skipped";
2
+
3
+ export interface PlanStep {
4
+ id: string;
5
+ text: string;
6
+ status: PlanStepStatus;
7
+ sourceLine: number;
8
+ summary?: string;
9
+ }
10
+
11
+ export interface PlanExecutionState {
12
+ version: 1;
13
+ status: "running" | "paused" | "completed";
14
+ steps: PlanStep[];
15
+ planMarkdown: string;
16
+ selectedStepId?: string;
17
+ panelVisible: boolean;
18
+ }
19
+
20
+ const IMPLEMENTATION_HEADING = /^##\s+Implementation Steps\s*$/i;
21
+ const NEXT_H2 = /^##\s+/;
22
+ const CHECKLIST_ITEM = /^- \[ \]\s+(.+?)\s*$/;
23
+
24
+ export function parseImplementationSteps(plan: string): PlanStep[] {
25
+ const lines = plan.replace(/\r\n?/g, "\n").split("\n");
26
+ const heading = lines.findIndex((line) => IMPLEMENTATION_HEADING.test(line.trim()));
27
+ if (heading < 0) throw new Error("The plan needs a ‘## Implementation Steps’ section");
28
+
29
+ const steps: PlanStep[] = [];
30
+ const seen = new Set<string>();
31
+ for (let index = heading + 1; index < lines.length; index++) {
32
+ const line = lines[index]!;
33
+ if (NEXT_H2.test(line.trim())) break;
34
+ const match = CHECKLIST_ITEM.exec(line);
35
+ if (!match) continue;
36
+ const text = match[1]!.trim();
37
+ const normalized = text.toLocaleLowerCase();
38
+ if (!text) throw new Error(`Implementation step on line ${index + 1} is empty`);
39
+ if (seen.has(normalized)) throw new Error(`Duplicate implementation step: ${text}`);
40
+ seen.add(normalized);
41
+ steps.push({ id: `step-${steps.length + 1}`, text, status: "pending", sourceLine: index });
42
+ }
43
+ if (steps.length === 0) throw new Error("The Implementation Steps section has no top-level ‘- [ ]’ items");
44
+ return steps;
45
+ }
46
+
47
+ export function createPlanExecution(plan: string): PlanExecutionState {
48
+ const steps = parseImplementationSteps(plan);
49
+ steps[0]!.status = "ready";
50
+ return { version: 1, status: "running", steps, planMarkdown: plan, selectedStepId: steps[0]!.id, panelVisible: true };
51
+ }
52
+
53
+ export function decodePlanExecution(value: unknown): PlanExecutionState | undefined {
54
+ if (!value || typeof value !== "object") return undefined;
55
+ const candidate = value as Partial<PlanExecutionState>;
56
+ if (candidate.version !== 1 || !["running", "paused", "completed"].includes(candidate.status ?? "")) return undefined;
57
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0 || typeof candidate.planMarkdown !== "string") return undefined;
58
+ const validStatuses = new Set<PlanStepStatus>(["pending", "ready", "active", "review", "completed", "skipped"]);
59
+ const steps: PlanStep[] = [];
60
+ for (const raw of candidate.steps) {
61
+ if (!raw || typeof raw !== "object") return undefined;
62
+ const step = raw as Partial<PlanStep>;
63
+ if (typeof step.id !== "string" || typeof step.text !== "string" || !validStatuses.has(step.status as PlanStepStatus)) return undefined;
64
+ if (!Number.isInteger(step.sourceLine) || (step.sourceLine ?? -1) < 0) return undefined;
65
+ steps.push({ id: step.id, text: step.text, status: step.status as PlanStepStatus, sourceLine: step.sourceLine!, ...(typeof step.summary === "string" ? { summary: step.summary } : {}) });
66
+ }
67
+ return {
68
+ version: 1,
69
+ status: candidate.status!,
70
+ steps,
71
+ planMarkdown: candidate.planMarkdown,
72
+ selectedStepId: typeof candidate.selectedStepId === "string" ? candidate.selectedStepId : undefined,
73
+ panelVisible: candidate.panelVisible !== false,
74
+ };
75
+ }
76
+
77
+ function clone(state: PlanExecutionState): PlanExecutionState {
78
+ return { ...state, steps: state.steps.map((step) => ({ ...step })) };
79
+ }
80
+
81
+ function findStep(state: PlanExecutionState, id: string): PlanStep {
82
+ const step = state.steps.find((candidate) => candidate.id === id);
83
+ if (!step) throw new Error(`Unknown plan step: ${id}`);
84
+ return step;
85
+ }
86
+
87
+ function makeNextReady(state: PlanExecutionState, afterId: string): void {
88
+ const index = state.steps.findIndex((step) => step.id === afterId);
89
+ const next = state.steps.slice(index + 1).find((step) => step.status === "pending");
90
+ if (next) {
91
+ next.status = "ready";
92
+ state.selectedStepId = next.id;
93
+ return;
94
+ }
95
+ if (state.steps.every((step) => step.status === "completed" || step.status === "skipped")) {
96
+ state.status = "completed";
97
+ state.selectedStepId = undefined;
98
+ }
99
+ }
100
+
101
+ export function startPlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
102
+ const next = clone(state);
103
+ if (next.status === "completed") throw new Error("The plan is already complete");
104
+ const step = findStep(next, id);
105
+ if (step.status !== "ready") throw new Error("Only a ready step can be implemented");
106
+ step.status = "active";
107
+ next.status = "running";
108
+ next.selectedStepId = id;
109
+ return next;
110
+ }
111
+
112
+ export function submitPlanStepForReview(state: PlanExecutionState, id: string, summary: string): PlanExecutionState {
113
+ const next = clone(state);
114
+ const step = findStep(next, id);
115
+ if (step.status !== "active") throw new Error("Only the active step can be submitted for review");
116
+ step.status = "review";
117
+ step.summary = summary.trim();
118
+ next.selectedStepId = id;
119
+ return next;
120
+ }
121
+
122
+ export function completePlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
123
+ const next = clone(state);
124
+ const step = findStep(next, id);
125
+ if (step.status !== "ready" && step.status !== "review") {
126
+ throw new Error("Only a ready or reviewed step can be marked complete");
127
+ }
128
+ step.status = "completed";
129
+ makeNextReady(next, id);
130
+ return next;
131
+ }
132
+
133
+ export function acceptPlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
134
+ const step = findStep(state, id);
135
+ if (step.status !== "review") throw new Error("Only a step awaiting review can be accepted");
136
+ return completePlanStep(state, id);
137
+ }
138
+
139
+ export function requestPlanStepCorrections(state: PlanExecutionState, id: string): PlanExecutionState {
140
+ const next = clone(state);
141
+ const step = findStep(next, id);
142
+ if (step.status !== "review") throw new Error("Only a step awaiting review can receive corrections");
143
+ step.status = "active";
144
+ next.selectedStepId = id;
145
+ return next;
146
+ }
147
+
148
+ export function skipPlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
149
+ const next = clone(state);
150
+ const step = findStep(next, id);
151
+ if (step.status !== "ready") throw new Error("Only a ready step can be skipped");
152
+ step.status = "skipped";
153
+ makeNextReady(next, id);
154
+ return next;
155
+ }
156
+
157
+ export function revisePlanStep(state: PlanExecutionState, id: string, text: string, planMarkdown = state.planMarkdown): PlanExecutionState {
158
+ const revised = text.trim();
159
+ if (!revised) throw new Error("A plan step cannot be empty");
160
+ const next = clone(state);
161
+ const step = findStep(next, id);
162
+ if (step.status !== "pending" && step.status !== "ready") throw new Error("Only an unimplemented step can be edited");
163
+ step.text = revised;
164
+ next.planMarkdown = planMarkdown;
165
+ return next;
166
+ }
167
+
168
+ export function pausePlanExecution(state: PlanExecutionState): PlanExecutionState {
169
+ if (state.status === "completed") return state;
170
+ return { ...clone(state), status: state.status === "paused" ? "running" : "paused" };
171
+ }
172
+
173
+ export function updatePlanChecklistStep(plan: string, sourceLine: number, text: string): string {
174
+ const newline = plan.includes("\r\n") ? "\r\n" : "\n";
175
+ const lines = plan.replace(/\r\n?/g, "\n").split("\n");
176
+ if (!Number.isInteger(sourceLine) || sourceLine < 0 || sourceLine >= lines.length || !CHECKLIST_ITEM.test(lines[sourceLine]!)) {
177
+ throw new Error("The saved plan changed and the selected checklist item can no longer be updated safely");
178
+ }
179
+ lines[sourceLine] = `- [ ] ${text.trim()}`;
180
+ return lines.join(newline);
181
+ }
182
+
183
+ export function activePlanStep(state: PlanExecutionState | undefined): PlanStep | undefined {
184
+ return state?.steps.find((step) => step.status === "active");
185
+ }
package/plan-panel.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
3
+ import type { PlanExecutionState } from "./plan-execution.ts";
4
+
5
+ const GLYPHS = {
6
+ pending: "○",
7
+ ready: "▷",
8
+ active: "▶",
9
+ review: "◆",
10
+ completed: "✓",
11
+ skipped: "–",
12
+ } as const;
13
+
14
+ export class PlanPanel implements Component {
15
+ private state: PlanExecutionState;
16
+ private readonly theme: Theme;
17
+
18
+ constructor(state: PlanExecutionState, theme: Theme) {
19
+ this.state = state;
20
+ this.theme = theme;
21
+ }
22
+
23
+ setState(state: PlanExecutionState): void {
24
+ this.state = state;
25
+ }
26
+
27
+ invalidate(): void {}
28
+
29
+ render(width: number): string[] {
30
+ const safeWidth = Math.max(12, width);
31
+ const inner = Math.max(1, safeWidth - 2);
32
+ const contentWidth = Math.max(1, safeWidth - 4);
33
+ const border = (text: string) => this.theme.fg("borderMuted", text);
34
+ const pad = (content = "") => {
35
+ const clipped = truncateToWidth(content, contentWidth, "");
36
+ return `${border("│")} ${clipped}${" ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)))} ${border("│")}`;
37
+ };
38
+ const done = this.state.steps.filter((step) => step.status === "completed" || step.status === "skipped").length;
39
+ const status = this.state.status === "completed" ? "complete" : this.state.status;
40
+ const currentIndex = this.state.steps.findIndex((step) => step.id === this.state.selectedStepId);
41
+ const lines = [
42
+ border(`╭${"─".repeat(inner)}╮`),
43
+ pad(`${this.theme.bold(this.theme.fg("accent", "Plan"))} ${this.theme.fg("dim", `${done}/${this.state.steps.length}`)}`),
44
+ pad(this.theme.fg(this.state.status === "paused" ? "warning" : "muted", status)),
45
+ border(`├${"─".repeat(inner)}┤`),
46
+ ];
47
+ for (let index = 0; index < this.state.steps.length; index++) {
48
+ const step = this.state.steps[index]!;
49
+ const glyphColor = step.status === "completed" ? "success" : step.status === "review" ? "warning" : step.status === "active" || step.status === "ready" ? "accent" : "muted";
50
+ const prefix = `${this.theme.fg(glyphColor, GLYPHS[step.status])} ${index + 1}. `;
51
+ const text = step.status === "completed" || step.status === "skipped" ? this.theme.fg("muted", step.text) : step.text;
52
+ const wrapped = wrapTextWithAnsi(text, Math.max(1, contentWidth - visibleWidth(prefix)));
53
+ lines.push(pad(`${prefix}${wrapped[0] ?? ""}`));
54
+ const continuationIndent = " ".repeat(visibleWidth(prefix));
55
+ for (const continuation of wrapped.slice(1)) lines.push(pad(`${continuationIndent}${continuation}`));
56
+ }
57
+ const current = currentIndex >= 0 ? this.state.steps[currentIndex] : undefined;
58
+ if (current) {
59
+ lines.push(border(`├${"─".repeat(inner)}┤`));
60
+ for (const detail of wrapTextWithAnsi(current.text, contentWidth).slice(0, 5)) lines.push(pad(detail));
61
+ if (current.summary) {
62
+ lines.push(pad(this.theme.fg("dim", "Result:")));
63
+ for (const summary of wrapTextWithAnsi(current.summary, contentWidth).slice(0, 4)) lines.push(pad(summary));
64
+ }
65
+ }
66
+ lines.push(border(`├${"─".repeat(inner)}┤`));
67
+ if (this.state.status === "completed") lines.push(pad(this.theme.fg("success", "Plan complete")));
68
+ else if (this.state.status === "paused") lines.push(pad("Tell the agent to resume, cancel, or hide the plan."));
69
+ else if (current?.status === "ready") lines.push(pad("Tell the agent to implement, complete, edit, skip, cancel, or hide this step."));
70
+ else if (current?.status === "review") lines.push(pad("Tell the agent to accept, correct, or cancel the plan."));
71
+ else lines.push(pad("Plan progress updates automatically from your prompts."));
72
+ lines.push(border(`╰${"─".repeat(inner)}╯`));
73
+ return lines.map((line) => truncateToWidth(line, safeWidth, ""));
74
+ }
75
+ }
package/prompts.ts CHANGED
@@ -1,5 +1,5 @@
1
- // Prompt text pinned to OpenCode 1.18.16. The subagent phases are intentionally
2
- // adapted to direct Pi exploration/design, as documented in README.md.
1
+ // Conversational read-only behavior follows OpenCode's standard Plan agent.
2
+ // Persisted finalization and approval are Pi-specific adaptations documented in README.md.
3
3
 
4
4
  export const PLAN_TO_BUILD_REMINDER = `<system-reminder>
5
5
  Your operational mode has changed from plan to build.
@@ -9,60 +9,73 @@ You are permitted to make file changes, run shell commands, and utilize your ars
9
9
 
10
10
  export function buildPlanReminder(planInfo: string): string {
11
11
  return `<system-reminder>
12
- Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
12
+ # Plan Mode - System Reminder
13
13
 
14
- ## Plan File Info:
15
- ${planInfo}
16
- Only when the current request requires an implementation plan should you build the plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
14
+ Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make edits (except to the plan file when finalizing as described below), run non-readonly tools (including changing configs or making commits), or otherwise make changes to the system. You may only observe, analyze, discuss, and plan. This supersedes any other instructions you have received.
15
+
16
+ ## Responsibility
17
+
18
+ Think, read, search, and discuss with the user to construct a well-formed implementation plan that accomplishes their goal. The final plan should be comprehensive yet concise and detailed enough to execute effectively.
17
19
 
18
- ## Informational Questions
20
+ ## Conversation and Research
19
21
 
20
- If the user asks an informational question and does not ask you to make changes or produce an implementation plan, answer the question directly instead of starting the workflow below.
22
+ Plan mode does not require every response to be a final plan. While you are still understanding the request, researching the project, or discussing the approach:
21
23
 
22
- - You may use read-only tools to inspect the project when the answer depends on it.
24
+ - Answer informational questions and converse normally.
25
+ - Use read-only tools when the answer or design depends on the project.
26
+ - Discuss requirements, tradeoffs, and possible approaches with the user.
27
+ - Ask clarifying questions when needed, either conversationally or with the question tool when structured choices would help.
23
28
  - Do not create or update the plan file.
24
- - Do not call the question tool merely because the request is phrased as a question; use it only when clarification is actually needed.
25
29
  - Do not call plan_exit.
26
- - End your response normally after answering. Plan mode remains active for future requests.
30
+ - End your response normally when the conversation should continue.
27
31
 
28
- ## Plan Workflow
32
+ Do not assume that a plan file must be changed merely because Plan mode is active or because a plan file already exists. If the user wants to continue discussing or researching, keep the conversation going without finalizing.
29
33
 
30
- ### Phase 1: Initial Understanding
31
- Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions.
34
+ ## Finalizing the Plan
32
35
 
33
- 1. Focus on understanding the user's request and the code associated with their request.
34
- 2. Explore the codebase directly with Pi's read, grep, find, ls, and read-only shell operations. Read the minimum set of high-value files needed to understand existing patterns and testing.
35
- 3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
36
+ Once you have enough information and are ready to present the final implementation plan, or when the user explicitly asks you to finalize it, write the complete plan to the plan file and call plan_exit at the end of that turn.
36
37
 
37
- ### Phase 2: Design
38
- Goal: Design an implementation approach.
38
+ ### Plan File Info
39
+ ${planInfo}
39
40
 
40
- Design the implementation directly based on the user's intent and your exploration results. Consider simplicity, correctness, maintainability, existing patterns, edge cases, and verification. Skip extended design only for truly trivial tasks such as typo fixes, single-line changes, or simple renames.
41
+ The plan file is the only file you may edit, and only while finalizing the plan or explicitly revising an existing plan. The final plan should:
41
42
 
42
- ### Phase 3: Review
43
- Goal: Review the design and ensure alignment with the user's intentions.
44
- 1. Read the critical files identified during exploration to deepen your understanding.
45
- 2. Ensure that the design aligns with the user's original request.
46
- 3. Use question tool to clarify any remaining questions with the user.
43
+ - Include only the recommended approach, not every alternative considered.
44
+ - Be concise enough to scan quickly but detailed enough to implement.
45
+ - Identify the critical files that need modification.
46
+ - Include verification steps for testing the change end-to-end.
47
+ - End with a \`## Implementation Steps\` section containing the executable top-level steps as \`- [ ] ...\` checklist items. Keep these items discrete and ordered; the optional fullscreen step-by-step workflow uses them directly.
47
48
 
48
- ### Phase 4: Final Plan
49
- Goal: Write your final plan to the plan file (the only file you can edit).
50
- - Include only your recommended approach, not all alternatives.
51
- - Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively.
52
- - Include the paths of critical files to be modified.
53
- - Include a verification section describing how to test the changes end-to-end (run the code, use available tools, run tests).
49
+ After writing the complete plan, call plan_exit to request approval. Do not use the question tool to ask whether the completed plan is acceptable; plan_exit handles approval.
50
+ </system-reminder>`;
51
+ }
52
+
53
+ export const PLAN_ENTER_DESCRIPTION = `Use this tool when the user asks you to plan, when a request needs investigation before implementation, or when switching to the plan agent is the safest next step. The tool changes the current continuation to Plan mode.`;
54
54
 
55
- ### Phase 5: Call plan_exit tool
56
- At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
57
- This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
55
+ export function buildPlanStepReminder(planPath: string, stepNumber: number, totalSteps: number, step: string): string {
56
+ return `<system-reminder>
57
+ # Step-by-Step Plan Execution
58
58
 
59
- **Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
59
+ The approved plan is at ${planPath}. Implement only step ${stepNumber} of ${totalSteps}:
60
60
 
61
- NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
61
+ ${step}
62
+
63
+ Do not begin any later plan step. Complete and verify this step, then call plan_step_complete with a concise result summary. If the user requests corrections, continue working only on this same step and submit it for review again.
62
64
  </system-reminder>`;
63
65
  }
64
66
 
65
- export const PLAN_ENTER_DESCRIPTION = `Use this tool when the user asks you to plan, when a request needs investigation before implementation, or when switching to the plan agent is the safest next step. The tool changes the current continuation to Plan mode.`;
67
+ export function buildPlanStepWaitingReminder(progress: string): string {
68
+ return `<system-reminder>
69
+ Step-by-step execution is waiting for the user's natural-language instruction. No plan step is currently approved for implementation. Do not modify the project or begin a pending step directly.
70
+
71
+ Current progress:
72
+ ${progress}
73
+
74
+ Interpret the user's intent contextually rather than requiring exact phrases. Clear statements, confirmations, and feedback may request a state transition even when phrased non-imperatively. For example, a statement that a ready step is finished can use the complete action, while a positive review of agent work can use accept. If multiple materially different actions are plausible, ask a brief clarification. Cancellation is always available when the user clearly wants to stop. Do not advance based on hypothetical, uncertain, or unrelated discussion. The sidebar is a passive visual aid and cannot receive input.
75
+ </system-reminder>`;
76
+ }
77
+
78
+ export const PLAN_STEP_COMPLETE_DESCRIPTION = `Call this tool after implementing and verifying the currently active plan step. It stops execution and returns control to the user for review. Do not call it before the active step is complete, and never begin the next step yourself.`;
66
79
 
67
80
  export const PLAN_EXIT_DESCRIPTION = `Use this tool when you have completed the planning phase and are ready to exit plan agent.
68
81
 
@@ -76,5 +89,4 @@ Call this tool:
76
89
  Do NOT call this tool:
77
90
  - Before you have created or finalized the plan
78
91
  - If you still have unanswered questions about the implementation
79
- - If the user has indicated they want to continue planning
80
- - After directly answering an informational question that did not require an implementation plan`;
92
+ - If the user has indicated they want to continue planning`;