@hank-warren/pi-plan-mode 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +4 -0
- package/README.md +60 -246
- package/package.json +3 -4
- package/src/active-implementation-menu.ts +4 -3
- package/src/command.ts +4 -10
- package/src/completion-tool.ts +3 -1
- package/src/fresh-implementation.ts +19 -26
- package/src/interactive-ui.ts +0 -1
- package/src/plan-action-controller.ts +5 -29
- package/src/plan-action-menus.ts +16 -23
- package/src/plan-export.ts +9 -7
- package/src/plan-file.ts +85 -0
- package/src/plan-launch-menu.ts +18 -78
- package/src/plan-mode.ts +126 -505
- package/src/presentation.ts +21 -40
- package/src/prompt.ts +15 -12
- package/src/settings-menu.ts +4 -181
- package/src/settings.ts +6 -105
- package/src/state.ts +22 -118
- package/src/auto-permissions-delegation.ts +0 -122
- package/src/implementation-retention.ts +0 -122
- package/src/message-transform.ts +0 -232
- package/src/required-tools.ts +0 -22
- package/src/saved-plan-menu.ts +0 -93
- package/src/saved-plan-preflight.ts +0 -39
- package/src/tool-policy.ts +0 -563
- package/src/tool-selection.ts +0 -98
package/NOTICE.md
CHANGED
|
@@ -5,3 +5,7 @@ This package is a fork of [`@narumitw/pi-plan-mode`](https://github.com/narumiru
|
|
|
5
5
|
The original work is Copyright (c) 2026 narumiruna and is used under the MIT License included in [`LICENSE`](LICENSE).
|
|
6
6
|
|
|
7
7
|
Fork-specific changes are Copyright (c) 2026 Hank Warren and are released under the same MIT License.
|
|
8
|
+
|
|
9
|
+
## Divergence from upstream
|
|
10
|
+
|
|
11
|
+
As of 1.0 this package has diverged substantially from upstream and no longer tracks it. Plan mode was rewritten around a durable plan file and no longer manages tool permissions: the tool selector, `defaultPlanTools`, the Bash inspection allowlist, `safeSubcommands`, the Auto Permissions Bash policy, the saved-plan state, and the plan-retention/context-reinjection machinery were all removed. Upstream changes are no longer merged.
|
package/README.md
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
# 🧭 pi-plan-mode —
|
|
1
|
+
# 🧭 pi-plan-mode — Plan mode for Pi
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@hank-warren/pi-plan-mode) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
`@hank-warren/pi-plan-mode` adds a
|
|
5
|
+
`@hank-warren/pi-plan-mode` adds a `/plan` mode to Pi for research and design. You gather information, ask questions, and land on a plan — then implement it, either in the same conversation or in a fresh one.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Plan mode is a mode of intent, not a permission system.** It blocks `edit`, `write`, and `update_plan` while planning and leaves every other tool exactly as you configured it. Command safety stays with your permission extension (for example [`@hank-warren/pi-auto-permissions`](../pi-auto-permissions)), which already reviews Bash. Plan mode never touches the session's active tool set, so it cannot break other extensions.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The plan is written to a **durable file** that survives compaction, survives resume, and can be hand-edited.
|
|
10
10
|
|
|
11
11
|
## ✨ Features
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Adds required `plan_mode_question` and `plan_mode_complete` tools for structured questions and completion.
|
|
21
|
-
- Presents the complete plan and lets you implement with the planning conversation or start a fresh linked session carrying only the approved plan, as well as export, save, stay, or discard.
|
|
22
|
-
- Exports ready, saved, or active implementation plans with `/plan export [path]` without overwriting existing paths; the omitted-path destination is configurable, and exporting a ready plan completes and exits Plan mode.
|
|
23
|
-
- Lets each accepted plan remain active, serve only as the first implementation handoff, or clear after the first implementation run; the current retained-plan behavior remains the default.
|
|
24
|
-
- Keeps legacy `<proposed_plan>` responses compatible without advertising XML as the primary workflow.
|
|
25
|
-
- Shows Plan mode state in Pi's statusline as `plan active`, `plan ready`, `plan saved`, or `plan implementing`; `@narumitw/pi-statusline` adds the default `📝` icon unless configured otherwise (this repo's `@hank-warren/pi-statusline` does not render extension statuses).
|
|
26
|
-
- Persists Plan mode, one session-local saved plan, and active implementation state so resume and compaction retain the exact accepted plan.
|
|
13
|
+
- `/plan` mode with a planning system prompt: explore first, ask decision questions, do not implement.
|
|
14
|
+
- `plan_mode_question` for structured 1-3 question decision points with options and a free-form path.
|
|
15
|
+
- `plan_mode_complete({ plan })` writes the plan to `<agent dir>/plans/<session-id>.md`.
|
|
16
|
+
- **Pointer, not payload.** An active plan adds one line to the system prompt naming the file. The plan body is never injected into context, so a 50-page plan costs the same as a one-liner and survives compaction for free.
|
|
17
|
+
- Two ways to implement: continue in this conversation, or open a fresh session that reads the same file.
|
|
18
|
+
- `/plan export [path]` copies the plan anywhere, never overwriting an existing target.
|
|
19
|
+
- Hand-edit the plan file at any time; every command and both implementation paths read from disk.
|
|
27
20
|
|
|
28
21
|
## 📦 Install
|
|
29
22
|
|
|
30
|
-
This release requires Pi 0.80.6 or newer.
|
|
31
|
-
|
|
32
23
|
```bash
|
|
33
24
|
pi install npm:@hank-warren/pi-plan-mode
|
|
34
25
|
```
|
|
@@ -39,244 +30,80 @@ Try without installing permanently:
|
|
|
39
30
|
pi -e npm:@hank-warren/pi-plan-mode
|
|
40
31
|
```
|
|
41
32
|
|
|
42
|
-
Try this package locally from the repository root:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
pi -e ./packages/pi-plan-mode
|
|
46
|
-
```
|
|
47
|
-
|
|
48
33
|
## 🚀 Usage
|
|
49
34
|
|
|
50
35
|
```text
|
|
51
|
-
/plan
|
|
52
|
-
/plan start
|
|
53
|
-
/plan <prompt>
|
|
54
|
-
/plan
|
|
55
|
-
/plan
|
|
56
|
-
/plan
|
|
57
|
-
/plan
|
|
58
|
-
/plan
|
|
59
|
-
/plan export [path]
|
|
60
|
-
/plan exit
|
|
36
|
+
/plan open the menu for the current state
|
|
37
|
+
/plan start enter Plan mode without sending a prompt
|
|
38
|
+
/plan <prompt> enter Plan mode and start planning <prompt>
|
|
39
|
+
/plan show display the stored plan
|
|
40
|
+
/plan finalize ask the agent to complete the plan now
|
|
41
|
+
/plan implement implement the completed plan here
|
|
42
|
+
/plan export [path] copy the plan to a Markdown file
|
|
43
|
+
/plan exit leave Plan mode and delete the plan file
|
|
61
44
|
```
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
and no plan is stored, the launch menu shows the effective next-start tools and offers **Start Plan
|
|
65
|
-
mode**, **Choose tools, then start…**, **Settings**, and **How Plan mode works**. Settings edits the
|
|
66
|
-
persistent defaults for later workflows. Launch-menu tool changes remain a draft until **Done — start
|
|
67
|
-
Plan mode** is selected; Back, Escape, Ctrl+C, disposal, session replacement, and shutdown discard
|
|
68
|
-
the draft without changing Plan state, active tools, thinking, or the stored selection.
|
|
69
|
-
|
|
70
|
-
Use `/plan start` when you want to enter Plan mode directly without sending a model message. Use
|
|
71
|
-
`/plan <prompt>` to enter Plan mode and immediately submit `<prompt>` as the first Plan-mode user
|
|
72
|
-
message. The exact argument `start` is reserved for direct activation; longer text such as `/plan
|
|
73
|
-
start a migration` remains an inline planning prompt. `--plan` also remains a direct activation path.
|
|
74
|
-
|
|
75
|
-
Use **Choose tools, then start…** or the `/plan tools` compatibility shortcut to choose a
|
|
76
|
-
session-specific override before Planning starts. Both routes use the same draft selector: **Done —
|
|
77
|
-
start Plan mode** stores the selection and starts the workflow, while cancellation leaves Plan mode
|
|
78
|
-
off and changes nothing. The bounded multi-select shows 10 rows at a time, supports viewport paging,
|
|
79
|
-
descriptions, and explicit unavailable rows for blocked tools. In TUI mode, type to fuzzy-search tool
|
|
80
|
-
names, descriptions, policy, and source metadata; RPC keeps the complete unfiltered list. Once Plan
|
|
81
|
-
mode is active, tools are locked: `/plan` no longer offers tool or Settings actions, and `/plan tools`
|
|
82
|
-
rejects the request. Exit and start a new workflow if a different tool set is required. The
|
|
83
|
-
`plan_mode_question` tool keeps its one-off question/choice dialog because it is a model-requested
|
|
84
|
-
planning interaction, not command-menu navigation. `/plan show` displays the stored
|
|
85
|
-
plan without starting a model turn, including the accepted plan while implementation is active.
|
|
86
|
-
`/plan finalize` explicitly asks the agent to complete the plan or ask one remaining material
|
|
87
|
-
question, `/plan save` stores a completed ready plan for later and leaves Plan mode, and `/plan
|
|
88
|
-
export [path]` writes a ready, saved, or active implementation plan to Markdown. Completed and saved
|
|
89
|
-
plan menus offer **Implement here**, which continues with the planning conversation, and **Start
|
|
90
|
-
fresh and implement**, which opens a new session and transfers only the approved plan. The direct
|
|
91
|
-
`/plan implement` compatibility route remains equivalent to **Implement here** and never opens a
|
|
92
|
-
selector. A successful ready-plan export also leaves Plan mode; saved and active implementation
|
|
93
|
-
exports retain their existing state. `show`, `save`, `export`, and `implement` fail closed when no
|
|
94
|
-
applicable plan is stored; `finalize` requires active Plan mode.
|
|
95
|
-
|
|
96
|
-
`/plan export` uses the configured **Export destination**, which defaults to `PLAN.md`. Supply a path
|
|
97
|
-
to override that default for one export. Relative paths resolve from the command's current `ctx.cwd`
|
|
98
|
-
at export time, absolute paths remain absolute, a leading `@` is accepted for Pi path compatibility,
|
|
99
|
-
and missing parent directories are created. Explicit `/plan export <path>` input always wins over the
|
|
100
|
-
setting. Export never overwrites an existing file, directory, or symbolic
|
|
101
|
-
link: choose another path or remove the existing target first. A successful export adds one trailing
|
|
102
|
-
newline but otherwise preserves the accepted Markdown exactly. After a ready plan is written, Plan
|
|
103
|
-
mode ends, its tools and thinking level are restored, and the ready state is cleared without starting
|
|
104
|
-
a model turn. Exporting a saved or active implementation plan leaves that state unchanged. Failed or
|
|
105
|
-
cancelled exports leave every Plan-mode state unchanged. The resulting file is available to the agent
|
|
106
|
-
through its normal file-reading tools. Export is an explicit user-requested file mutation;
|
|
107
|
-
model-initiated Plan-mode writes remain blocked.
|
|
108
|
-
|
|
109
|
-
In TUI and RPC, **Export plan…** opens a single-line path input from every ready, saved, or active
|
|
110
|
-
plan menu. The input shows both the configured value and its currently resolved path. Submit an empty
|
|
111
|
-
value to use the configured destination, or enter a relative or absolute one-off path. A failed TUI
|
|
112
|
-
export retains the draft for correction; RPC reopens its input dialog. Escape returns to
|
|
113
|
-
the owning menu without writing a file. A successful ready-plan export closes the menu and ends Plan
|
|
114
|
-
mode; saved and active implementation menus close without changing their stored state.
|
|
115
|
-
|
|
116
|
-
When Plan mode is active, ask the agent to design the change. The agent may inspect files and run read-only commands, but it should not edit files or execute the implementation. It should explore first, then use structured questions when your preference or a tradeoff materially changes the plan. Configure persistent defaults or a one-workflow tool override before activation; Planning and ready menus deliberately keep those controls locked.
|
|
117
|
-
|
|
118
|
-
By default, Plan mode manages only Pi's built-in tools: `read`, limited `bash`, available read-only built-ins such as `grep`, `find`, and `ls`, plus the required `plan_mode_question` and `plan_mode_complete` tools. Built-in `edit` and `write` are blocked. `update_plan` is also blocked because it tracks execution progress rather than conversational planning. Extension and custom tools are disabled by default because Pi tools do not expose standardized mutability metadata; enable them before starting from Settings or the staged workflow selector only when you accept the risk. For example, you can opt into `firecrawl_scrape`, `firecrawl_search`, or `lsp_diagnostics` if those extensions are loaded and you want to use them during planning.
|
|
119
|
-
|
|
120
|
-
With the default `limited` Bash policy, Plan mode uses a fail-closed inspection allowlist, including when an extension overrides the canonical `bash` tool name. It accepts common inspection commands, read-only Git and npm queries, pipelines and command lists composed entirely of accepted commands, plus selected checks such as `npm test`, `npm run typecheck`, and `cargo test`. It rejects output/input redirects, shell expansion, substitutions, subshells, background jobs, mutating flags, dependency changes, editors, and unknown commands. A rejected parsed command list or pipeline identifies its first blocked command segment; malformed or unsupported shell syntax reports the complete submitted input instead. Tests and builds may still write ignored caches or build artifacts and may execute project-defined hooks; enable or invoke them only when the repository is trusted. This is extension-level risk reduction, not an OS sandbox.
|
|
121
|
-
|
|
122
|
-
Set `bashPolicy` to `auto-permissions` to integrate with `@ogulcancelik/pi-auto-permissions` 0.1.x. Plan mode first applies its normal inspection allowlist. For a command that allowlist would block, it stands down only when the Auto Permissions extension is loaded and its current configuration contains a matching, non-trusted `guarded` rule. Missing, disabled, malformed, unmatched, convention-only, or trusted-group-bypassed configurations keep the Plan-mode block. Auto Permissions then performs its normal guardian review or user approval flow. Plan mode still blocks built-in `edit`, `write`, and `update_plan`, and its model instructions still prohibit implementation and mutation.
|
|
123
|
-
|
|
124
|
-
`plan_mode_question` follows Codex's `request_user_input` pattern: the agent can ask 1-3 concise questions, each with meaningful options and a free-form Other path. If you cancel or no interactive UI is available, the agent should ask a concise plain-text question or proceed only with a clearly stated low-risk assumption instead of prematurely producing a final plan.
|
|
125
|
-
|
|
126
|
-
Pi activates tools by tool name. The pre-start selector stores accepted session selections by name
|
|
127
|
-
and shows each effective tool's source from Pi metadata, such as `built-in`, a user extension path,
|
|
128
|
-
or a project extension path. If an extension overrides a built-in tool with the same name, Pi exposes the effective tool for that name and the selector shows that source.
|
|
129
|
-
|
|
130
|
-
A complete Plan mode answer should appear only after the agent has resolved discoverable facts and high-impact user decisions. The agent must call `plan_mode_complete({ plan })` alone as its final action, passing the complete Markdown plan. The tool rejects empty or whitespace-only plans and plans longer than 50,000 JavaScript characters; it does not truncate. Its visible result contains the full plan, and versioned result details let the extension restore it safely from the active session branch.
|
|
131
|
-
|
|
132
|
-
`plan_mode_complete` uses Pi's `terminate: true` hint. Termination is best effort: if a model puts it in a parallel tool batch, Pi terminates the batch early only when every finalized sibling tool also terminates. The prompt therefore requires the completion call to be standalone and last. The extension deliberately does not infer completion from phrases such as “I will present the plan,” and it does not automatically retry a turn with no plan because research and clarification turns may legitimately remain unfinished. If a turn ends without a plan, Plan mode stays active; use `/plan finalize` for explicit recovery.
|
|
133
|
-
|
|
134
|
-
Legacy sessions and models may still submit one non-empty `<proposed_plan>` block with tags on their own lines. That compatibility path remains accepted, but it is not the primary workflow. Empty, malformed, unclosed, or multiple legacy blocks keep Plan mode active and produce a warning.
|
|
135
|
-
|
|
136
|
-
After completion, `/plan` opens the ready actions when interactive UI is available. The same flat menu shows **Implement here** and **Start fresh and implement**, explains which conversation context each choice uses, and previews how long the approved plan will remain active. **Implement here**—and the compatibility route `/plan implement`—disables Plan mode, restores full tool access, captures the current **After Implement** policy, and starts implementation in the current session with its planning conversation. **Start fresh and implement** waits for the source session to become idle, verifies the selected model and authentication, creates a new session linked to the persisted source as its parent, and transfers the exact approved plan without copying planning messages, tool results, or compaction/branch summaries. The destination still loads its normal `AGENTS.md`, skills, project resources, and extensions. Choosing **Export plan…** asks for a destination, writes the plan, restores normal tools and thinking, and leaves Plan mode without starting a model turn. Choosing **Save for later**—or running `/plan save`—instead stores one plan in the current Pi session before leaving Plan mode.
|
|
137
|
-
|
|
138
|
-
When a workflow was started only with `--plan` and no `/plan` command has run in that session, the automatic menu cannot obtain Pi's command-only session replacement capability; choosing fresh asks you to reopen `/plan`, where the same action is available. A successful fresh handoff does not delete or consume the source planning session. Resume it later to inspect or hand off the ready/saved plan again; this deliberate duplication is the recovery path if the destination work is abandoned. In-memory sessions create an unlinked fresh session because no parent file exists. Escape, Ctrl+C, menu disposal, source replacement/shutdown, model/auth failure, or cancellation by another extension before replacement leaves the source plan unchanged. Once replacement succeeds, the destination persists active-plan state before kickoff. If persistence fails, the complete request is placed in the destination editor and the source remains resumable. If kickoff fails, the destination retains the active plan; send a message to continue, use `/plan exit` to clear it, or resume the parent planning session.
|
|
139
|
-
|
|
140
|
-
A saved plan appears as `plan saved` and remains available after reload, resume, branch-local fork, and compaction in that session. It does not expire automatically, cross into a new session, or participate in ordinary model context. Open `/plan` to Show, Implement here, Start fresh and implement, Export, open Settings, or Clear it; `/plan show`, `/plan implement`, `/plan export [path]`, and `/plan exit`/`off` retain their direct routes in TUI and RPC. Fresh implementation checks idle state, the selected model, and authentication before session replacement; Implement here keeps its established preflight behavior. Starting another workflow with `/plan start`, `/plan <prompt>`, or `/plan tools` is blocked until the saved plan is implemented or cleared, so the single saved slot is never silently overwritten. Resuming that session with `--plan` moves the saved plan back to ready Plan mode. Cancellation or failed implementation preflight leaves it unchanged.
|
|
141
|
-
|
|
142
|
-
Text print and JSON modes cannot display the bare `/plan` menu and reject that route before changing
|
|
143
|
-
state; use `/plan start` for direct no-prompt activation or `/plan <prompt>` to start planning with a
|
|
144
|
-
prompt. `/plan tools` also rejects before changing state because its staged selector requires TUI or
|
|
145
|
-
RPC. These modes can export any stored plan with `/plan export [path]`, save a ready plan with
|
|
146
|
-
`/plan save`, and clear it with `/plan exit` or `/plan off`. Successful export is observable through
|
|
147
|
-
the created file; exporting a ready plan also leaves Plan mode, while saved and active implementation
|
|
148
|
-
state remains unchanged. An existing target or missing plan fails the command without changing state.
|
|
149
|
-
These modes reject saved-plan display and implementation before changing state because Pi provides
|
|
150
|
-
neither printable custom-message output nor acknowledged extension-triggered turns; resume the
|
|
151
|
-
session in TUI or RPC to show or implement it.
|
|
152
|
-
|
|
153
|
-
Both implementation paths apply the current **After Implement** policy in their destination; the fresh-session choice does not change retention semantics. With the default **Keep plan active** policy, the exact accepted plan remains active across later turns, session resume, and manual or automatic compaction without depending on the compaction summary. Plan mode avoids a duplicate context block while the original implementation handoff remains available and injects one hidden canonical copy after that handoff is compacted away. This can consume up to the existing 50,000-character plan limit in model context when reinjection is needed. **Use plan for handoff only** clears retained state immediately after the first implementation request receives the complete plan. **Clear after first implementation run** keeps the plan through that run, including retries, compaction retries, and queued continuation, then clears it at `agent_settled`. Cleanup is bound to the matching implementation, so an older run settling cannot clear a newer handoff.
|
|
154
|
-
|
|
155
|
-
While implementation is active, `/plan show` displays the accepted plan. Interactive `/plan` offers Show, Export plan…, Settings, Start a new plan, and Clear; `/plan exit` and `/plan off` are the direct clear routes. Settings changes never alter the policy already captured by the active implementation. Automatic cleanup has the same observable result as clearing: its active status and future injected plan context disappear, while the implementation request that triggered cleanup still receives the complete plan. Starting a new Plan-mode workflow or implementing a replacement plan supersedes the previous active plan. The extension deliberately does not infer completion from assistant prose or agent settlement, so clear the active plan when the implementation no longer applies. Choosing Stay before implementation keeps the plan ready. Revision feedback starts another Plan-mode turn and clears the previous implementable plan until an updated completion arrives. For clarification-only follow-ups, the agent answers and resubmits the complete unchanged plan so it remains implementable. Before saving or implementation, exit/off discards the ready plan and removes its completion result from later non-Plan model context.
|
|
156
|
-
|
|
157
|
-
While Plan mode is enabled, the extension also publishes a compact status for Pi statuslines. With a statusline that renders extension statuses (for example `@narumitw/pi-statusline`; this repo's `@hank-warren/pi-statusline` does not), this appears as:
|
|
158
|
-
|
|
159
|
-
- `plan active`: Plan mode is enabled and still gathering context or drafting a plan.
|
|
160
|
-
- `plan ready`: A completed plan is stored until you implement it, export it, save it, continue planning, or exit Plan mode.
|
|
161
|
-
- `plan saved`: One completed plan is stored outside model context in the current session until you implement or clear it.
|
|
162
|
-
- `plan implementing`: The exact accepted plan is currently retained under its captured **After Implement** policy.
|
|
163
|
-
|
|
164
|
-
You can also exit directly. Before implementation, direct exit discards the latest proposed plan; while a plan is saved, it clears that saved plan. During implementation, it removes both the original implementation handoff and the extension's canonical active-plan block from later model calls; an earlier Pi-generated compaction summary may still describe prior work:
|
|
165
|
-
|
|
166
|
-
```text
|
|
167
|
-
/plan exit
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## ⚙️ Settings
|
|
171
|
-
|
|
172
|
-
Open **Settings** from an inactive `/plan` menu to edit one flat group of five workflow choices: **Plan thinking**, **Plan tools**, **Bash policy**, **After Implement**, and **Export destination**. You can also edit `$PI_CODING_AGENT_DIR/pi-plan-mode.json` (normally `~/.pi/agent/pi-plan-mode.json`) manually; `safeSubcommands` remains JSON-only. The file is optional, is read at session start, and is created only after an explicit Settings save or manual edit.
|
|
46
|
+
`--plan` starts a session directly in Plan mode.
|
|
173
47
|
|
|
174
|
-
|
|
175
|
-
{
|
|
176
|
-
"thinkingLevel": "inherit",
|
|
177
|
-
"defaultPlanTools": ["read", "bash", "grep", "find", "ls", "mcp"],
|
|
178
|
-
"bashPolicy": "auto-permissions",
|
|
179
|
-
"implementationPlanRetention": "keep",
|
|
180
|
-
"defaultPlanExportPath": "PLAN.md",
|
|
181
|
-
"safeSubcommands": {
|
|
182
|
-
"git": ["status", "log", "rev-parse", "blame"],
|
|
183
|
-
"gh": ["pr view", "pr list", "issue view", "issue list"]
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
```
|
|
48
|
+
While Plan mode is active, ask the agent to design the change. It can read, search, and run commands, but `edit`, `write`, and `update_plan` are blocked. When the plan is decision-complete, the agent calls `plan_mode_complete` and the plan is written to disk.
|
|
187
49
|
|
|
188
|
-
|
|
50
|
+
From a completed plan you can:
|
|
189
51
|
|
|
190
|
-
|
|
52
|
+
- **Implement here** — Plan mode turns off and implementation continues in this conversation.
|
|
53
|
+
- **Start fresh and implement** — a new linked session opens, pointed at the same plan file, without carrying the planning conversation.
|
|
54
|
+
- **Export plan…** — write the plan to a path of your choice.
|
|
55
|
+
- **Stay in Plan mode** — keep refining. The next planning turn supersedes the previous plan.
|
|
191
56
|
|
|
192
|
-
|
|
57
|
+
Print and JSON modes cannot show the interactive menu; use `/plan start`, `/plan <prompt>`, `/plan show`, `/plan export`, and `/plan exit` there.
|
|
193
58
|
|
|
194
|
-
|
|
59
|
+
## 📄 The plan file
|
|
195
60
|
|
|
196
|
-
|
|
61
|
+
The plan lives at `<agent dir>/plans/<session-id>.md` — normally `~/.pi/agent/plans/<session-id>.md`.
|
|
197
62
|
|
|
198
|
-
|
|
63
|
+
- **It is the plan.** Session state stores only the path.
|
|
64
|
+
- **Hand-edit it freely.** Everything reads from disk, so your edits are what the agent implements.
|
|
65
|
+
- **It survives compaction** because the model only ever sees a one-line pointer to it, and re-reads the file when needed.
|
|
66
|
+
- **A fresh implementation session points at the same file.** The plan is never copied, so both sessions see the same content.
|
|
67
|
+
- `/plan exit` deletes it. Export first if you want to keep a copy.
|
|
199
68
|
|
|
200
|
-
|
|
201
|
-
- Use `auto-permissions` to retain the Plan allowlist while selectively deferring blocked commands that match an active Auto Permissions guarded rule.
|
|
69
|
+
Writes are atomic (temp file plus rename), so a reader never sees a partial plan.
|
|
202
70
|
|
|
203
|
-
|
|
71
|
+
## ⚙️ Settings
|
|
204
72
|
|
|
205
|
-
|
|
73
|
+
Open **Settings** from the `/plan` menu, or edit `$PI_CODING_AGENT_DIR/pi-plan-mode.json` (normally `~/.pi/agent/pi-plan-mode.json`). The file is optional.
|
|
206
74
|
|
|
207
|
-
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"thinkingLevel": "inherit",
|
|
78
|
+
"defaultPlanExportPath": "PLAN.md"
|
|
79
|
+
}
|
|
80
|
+
```
|
|
208
81
|
|
|
209
|
-
|
|
82
|
+
### Plan thinking
|
|
210
83
|
|
|
211
|
-
|
|
84
|
+
`thinkingLevel` requests a fixed thinking level while Plan mode is active. Supported values are `inherit`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. The extension restores your previous level on exit unless you changed it manually during Plan mode. Saving applies to the next Plan workflow, never one already running.
|
|
212
85
|
|
|
213
86
|
### Export destination
|
|
214
87
|
|
|
215
|
-
`defaultPlanExportPath` controls only exports that omit a path
|
|
88
|
+
`defaultPlanExportPath` controls only exports that omit a path, and defaults to `PLAN.md`. Relative values resolve against the current working directory at export time. An explicit `/plan export <path>` always wins. Export never overwrites an existing file, directory, or symbolic link.
|
|
216
89
|
|
|
217
|
-
|
|
90
|
+
Unknown keys are preserved. Settings removed in 1.0 (`defaultPlanTools`, `bashPolicy`, `safeSubcommands`, `implementationPlanRetention`) are ignored rather than treated as errors, so an existing settings file keeps working.
|
|
218
91
|
|
|
219
|
-
|
|
92
|
+
## 🔐 What Plan mode does and does not enforce
|
|
220
93
|
|
|
221
|
-
|
|
94
|
+
Plan mode blocks exactly three tools while planning: `edit`, `write`, and `update_plan`. That is the whole enforcement surface.
|
|
222
95
|
|
|
223
|
-
|
|
224
|
-
- `gh`: `pr view`, `pr list`, `issue view`, and `issue list`.
|
|
96
|
+
It deliberately does **not** police Bash, subagents, MCP tools, or any other extension tool. Those decisions belong to your permission layer, which can see the whole session and judge each call. Pair Plan mode with a permission extension such as `@hank-warren/pi-auto-permissions` if you want command review during planning.
|
|
225
97
|
|
|
226
|
-
|
|
98
|
+
Plan mode also never calls `setActiveTools`. Extensions that register tools lazily (MCP connections, subagent supervision channels) keep working normally, and nothing needs to be restored when Plan mode exits.
|
|
227
99
|
|
|
228
|
-
|
|
100
|
+
## 📊 Statusline
|
|
229
101
|
|
|
230
|
-
|
|
231
|
-
git rev-parse --show-toplevel
|
|
232
|
-
git blame -- src/plan-mode.ts
|
|
233
|
-
git diff --cached
|
|
234
|
-
git show --stat --oneline HEAD
|
|
235
|
-
git log -p -1 HEAD -- src/plan-mode.ts
|
|
236
|
-
gh pr view 218 --json number,title,state
|
|
237
|
-
gh issue list --state open --json number,title,state
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
The command-specific validators still reject unsafe forms, including:
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
git blame --textconv -- src/plan-mode.ts
|
|
244
|
-
git cat-file --filters HEAD
|
|
245
|
-
git diff --ext-diff
|
|
246
|
-
git log --show-signature -1
|
|
247
|
-
git remote show origin
|
|
248
|
-
git show --textconv HEAD
|
|
249
|
-
gh pr merge 218
|
|
250
|
-
gh pr view 218
|
|
251
|
-
gh pr view 218 --web
|
|
252
|
-
gh pr view 218 > pr.txt
|
|
253
|
-
gh pr list --json number,title && gh pr merge 218
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
Redirects, shell expansion and substitution, explicit pager or browser requests, explicit external diff/textconv/filter/signature helpers, output flags, malformed command layouts, and any chain containing an unsafe segment fail closed. Read-dominant Git validators accept ordinary inspection flags without requiring `--no-textconv` or `--no-ext-diff`; Git may therefore invoke a helper configured by the user or trusted repository even when the command does not request one explicitly. Use the negative flags when you want to suppress those configured helpers. Mixed read/write surfaces remain narrower: use `git remote show -n` to avoid invoking a transport helper, while mutating `branch` and `remote` forms remain blocked. GitHub CLI read paths require `--json <fields>` output so Plan mode does not rely on `GH_PAGER`, `PAGER`, or gh pager configuration. Unknown `safeSubcommands` keys or values, non-array values, and non-string entries invalidate the entire settings file and trigger the normal warning/default fallback on session start.
|
|
257
|
-
|
|
258
|
-
Read-only does not mean private: Git inspection can expose repository history and tracked secrets, while `gh` queries can expose remote repository, pull request, and issue data available to your authenticated account. The policy reduces accidental mutation and explicit helper execution; it is not a sandbox or a confidentiality boundary.
|
|
259
|
-
|
|
260
|
-
### Thinking level
|
|
261
|
-
|
|
262
|
-
Plan mode inherits Pi's current thinking level by default. Set `thinkingLevel` to request a fixed level only while Plan mode is active. Supported values are `inherit`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. The extension snapshots the prior level and restores it on exit only if the level still matches the value it applied; a manual change made during Plan mode is preserved. A Settings save does not change Pi's current or default thinking level and takes effect only when the next Plan workflow starts.
|
|
263
|
-
|
|
264
|
-
Settings saves are serialized in invocation order inside one Pi process. Each save re-reads the latest valid document, preserves unknown top-level fields and unedited `safeSubcommands`, then publishes through a same-directory temporary file and rename. A missing file stays absent until an explicit save. Invalid JSON, invalid values, oversized content, non-regular files, and read failures make Settings read-only; the existing bytes and previous effective settings remain. This in-process queue is not a cross-process lock, so concurrent separate Pi processes can still race.
|
|
102
|
+
Plan mode publishes a compact status for statuslines that render extension statuses:
|
|
265
103
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
This extension maps Codex's `ModeKind::Plan` behavior onto Pi's extension API:
|
|
271
|
-
|
|
272
|
-
- Plan mode is a conversational collaboration mode, not TODO/progress tracking.
|
|
273
|
-
- `/plan <prompt>` follows Codex behavior by switching to Plan mode before submitting the inline prompt.
|
|
274
|
-
- The agent should use `plan_mode_question` for important non-discoverable preferences or tradeoffs before finalizing.
|
|
275
|
-
- The agent completes with a standalone `plan_mode_complete` tool call instead of relying on semantic prose detection.
|
|
276
|
-
- `update_plan` checklist use is blocked while Plan mode is active.
|
|
277
|
-
- The implementation boundary is explicit: Plan mode restores tools before saving or starting implementation, saving keeps the plan outside ordinary model context, choosing implementation immediately triggers a normal agent turn with full tool access, and the accepted plan follows the captured `keep`, `clear-on-start`, or `clear-after-first-run` lifecycle.
|
|
278
|
-
- Pi extension safety is approximated with tool classification and fail-closed filtering for every effective tool named `bash`; the optional Auto Permissions integration delegates only exact commands covered by active guarded rules. Other non-built-in tools remain user-selected at user risk because Pi does not expose standardized tool mutability metadata.
|
|
279
|
-
- Unlike native Codex, this extension uses a terminating Pi tool plus an `agent_settled` ready flow; Pi cannot provide sandbox-level enforcement.
|
|
104
|
+
- `plan active` — planning is under way.
|
|
105
|
+
- `plan ready` — a completed plan is waiting for your choice.
|
|
106
|
+
- `plan implementing` — a plan file is active and guiding implementation.
|
|
280
107
|
|
|
281
108
|
## 🗂️ Package layout
|
|
282
109
|
|
|
@@ -284,9 +111,10 @@ This extension maps Codex's `ModeKind::Plan` behavior onto Pi's extension API:
|
|
|
284
111
|
packages/pi-plan-mode/
|
|
285
112
|
├── index.ts # Pi package entrypoint
|
|
286
113
|
├── src/
|
|
287
|
-
│ ├── plan-mode.ts # Extension registration, mode state,
|
|
288
|
-
│ ├──
|
|
289
|
-
│
|
|
114
|
+
│ ├── plan-mode.ts # Extension registration, mode state, hooks
|
|
115
|
+
│ ├── plan-file.ts # Durable plan file read/write/delete
|
|
116
|
+
│ ├── interactive-ui.ts # Lazily loaded interactive menus
|
|
117
|
+
│ └── *.ts # Prompt, question, export, settings modules
|
|
290
118
|
├── test/
|
|
291
119
|
├── README.md
|
|
292
120
|
├── NOTICE.md
|
|
@@ -294,20 +122,6 @@ packages/pi-plan-mode/
|
|
|
294
122
|
└── package.json
|
|
295
123
|
```
|
|
296
124
|
|
|
297
|
-
The root `index.ts` forwards to the internal source entrypoint. The package exposes its Pi extension through `package.json`:
|
|
298
|
-
|
|
299
|
-
```json
|
|
300
|
-
{
|
|
301
|
-
"pi": {
|
|
302
|
-
"extensions": ["./index.ts"]
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
## 🔎 Keywords
|
|
308
|
-
|
|
309
|
-
Pi extension, Pi coding agent, plan mode, Codex-like plan mode, AI coding workflow, read-only planning, implementation plan.
|
|
310
|
-
|
|
311
125
|
## 📄 License
|
|
312
126
|
|
|
313
127
|
MIT. See [`LICENSE`](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-plan-mode",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Plan mode for Pi: research and design with a durable plan file that survives compaction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
9
|
"pi",
|
|
10
10
|
"plan-mode",
|
|
11
|
-
"planning"
|
|
12
|
-
"permissions"
|
|
11
|
+
"planning"
|
|
13
12
|
],
|
|
14
13
|
"author": "Hank Warren",
|
|
15
14
|
"license": "MIT",
|
|
@@ -4,10 +4,11 @@ import { type PlanExportDestinationProvider, planExportInputScreen } from "./pla
|
|
|
4
4
|
|
|
5
5
|
interface ActiveImplementationMenuOptions {
|
|
6
6
|
statusText: string;
|
|
7
|
+
planPathLine?: string;
|
|
7
8
|
getExportDestination: PlanExportDestinationProvider;
|
|
8
9
|
signal: AbortSignal;
|
|
9
10
|
isCurrent(): boolean;
|
|
10
|
-
show(): void
|
|
11
|
+
show(): void | Promise<void>;
|
|
11
12
|
exportPlan(path: string, signal: AbortSignal): Promise<boolean>;
|
|
12
13
|
settings(signal: AbortSignal): Promise<boolean>;
|
|
13
14
|
startNew(): void;
|
|
@@ -26,7 +27,7 @@ export async function showActiveImplementationMenu(
|
|
|
26
27
|
active: () => ({
|
|
27
28
|
kind: "actions",
|
|
28
29
|
title: "Active implementation plan",
|
|
29
|
-
lines: [options.statusText],
|
|
30
|
+
lines: [options.statusText, ...(options.planPathLine ? [options.planPathLine] : [])],
|
|
30
31
|
items: [
|
|
31
32
|
{ id: "show", label: "Show active implementation plan", action: "show" },
|
|
32
33
|
{ id: "export", label: "Export plan…", to: "export" },
|
|
@@ -40,7 +41,7 @@ export async function showActiveImplementationMenu(
|
|
|
40
41
|
},
|
|
41
42
|
actions: {
|
|
42
43
|
show: async () => {
|
|
43
|
-
options.show();
|
|
44
|
+
await options.show();
|
|
44
45
|
return { kind: "close" };
|
|
45
46
|
},
|
|
46
47
|
export: async ({ value, signal }) =>
|
package/src/command.ts
CHANGED
|
@@ -6,18 +6,12 @@ export interface CommandArgumentCompletion {
|
|
|
6
6
|
|
|
7
7
|
const PLAN_COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
8
8
|
{ value: "start", label: "start", description: "Start Plan mode without sending a prompt" },
|
|
9
|
-
{ value: "show", label: "show", description: "Show the
|
|
9
|
+
{ value: "show", label: "show", description: "Show the stored plan" },
|
|
10
10
|
{ value: "finalize", label: "finalize", description: "Request a completed plan" },
|
|
11
|
-
{ value: "implement", label: "implement", description: "Implement the completed
|
|
12
|
-
{ value: "save", label: "save", description: "Save the completed plan for later" },
|
|
11
|
+
{ value: "implement", label: "implement", description: "Implement the completed plan" },
|
|
13
12
|
{ value: "export", label: "export", description: "Export the stored plan to a Markdown file" },
|
|
14
|
-
{ value: "exit", label: "exit", description: "Leave Plan mode
|
|
15
|
-
{ value: "off", label: "off", description: "Leave Plan mode
|
|
16
|
-
{
|
|
17
|
-
value: "tools",
|
|
18
|
-
label: "tools",
|
|
19
|
-
description: "Choose tools before starting this Plan workflow",
|
|
20
|
-
},
|
|
13
|
+
{ value: "exit", label: "exit", description: "Leave Plan mode and clear the plan" },
|
|
14
|
+
{ value: "off", label: "off", description: "Leave Plan mode and clear the plan" },
|
|
21
15
|
];
|
|
22
16
|
|
|
23
17
|
export function completePlanArguments(argumentPrefix: string): CommandArgumentCompletion[] | null {
|
package/src/completion-tool.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type PlanModeCompletionDetails = {
|
|
|
9
9
|
version: typeof PLAN_MODE_COMPLETE_VERSION;
|
|
10
10
|
source: typeof PLAN_MODE_COMPLETE_TOOL_NAME;
|
|
11
11
|
plan: string;
|
|
12
|
+
planPath?: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export const PLAN_MODE_COMPLETE_PARAMS = {
|
|
@@ -54,13 +55,14 @@ export function planFromCompletionDetails(value: unknown) {
|
|
|
54
55
|
return normalized.ok ? normalized.plan : undefined;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
export function planModeCompleted(plan: string) {
|
|
58
|
+
export function planModeCompleted(plan: string, planPath?: string) {
|
|
58
59
|
return {
|
|
59
60
|
content: [{ type: "text" as const, text: `**Proposed Plan**\n\n${plan}` }],
|
|
60
61
|
details: {
|
|
61
62
|
version: PLAN_MODE_COMPLETE_VERSION,
|
|
62
63
|
source: PLAN_MODE_COMPLETE_TOOL_NAME,
|
|
63
64
|
plan,
|
|
65
|
+
...(planPath ? { planPath } : {}),
|
|
64
66
|
} satisfies PlanModeCompletionDetails,
|
|
65
67
|
terminate: true,
|
|
66
68
|
};
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import
|
|
4
|
-
import type {
|
|
2
|
+
import { readPlanFile } from "./plan-file.js";
|
|
3
|
+
import type { PlanModeState } from "./state.js";
|
|
5
4
|
|
|
6
5
|
type NewSessionOptions = Exclude<Parameters<ExtensionCommandContext["newSession"]>[0], undefined>;
|
|
7
6
|
type ReplacementContext = Parameters<NonNullable<NewSessionOptions["withSession"]>>[0];
|
|
8
7
|
|
|
9
8
|
export interface FreshImplementationRequest {
|
|
10
9
|
plan: string;
|
|
11
|
-
|
|
12
|
-
retention: ImplementationPlanRetention;
|
|
10
|
+
planPath: string;
|
|
13
11
|
stateEntryType: string;
|
|
14
12
|
isCurrent(): boolean;
|
|
15
13
|
}
|
|
@@ -17,7 +15,6 @@ export interface FreshImplementationRequest {
|
|
|
17
15
|
interface FreshImplementationFromStateOptions {
|
|
18
16
|
getState(): PlanModeState;
|
|
19
17
|
menuIsCurrent(): boolean;
|
|
20
|
-
retention: ImplementationPlanRetention;
|
|
21
18
|
stateEntryType: string;
|
|
22
19
|
}
|
|
23
20
|
|
|
@@ -28,8 +25,13 @@ export type FreshImplementationResult =
|
|
|
28
25
|
| { kind: "rejected" }
|
|
29
26
|
| { kind: "stale" };
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The handoff names the plan file rather than inlining the plan, so the request
|
|
30
|
+
* stays small and the agent re-reads the authoritative file if context is
|
|
31
|
+
* compacted mid-implementation.
|
|
32
|
+
*/
|
|
33
|
+
export function formatImplementationHandoff(planPath: string) {
|
|
34
|
+
return `Plan mode is now disabled. Implement the approved plan stored at ${planPath}. Read that file first; it is the source of truth and may have been edited.`;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export async function startFreshImplementationFromState(
|
|
@@ -44,10 +46,9 @@ export async function startFreshImplementationFromState(
|
|
|
44
46
|
return { kind: "rejected" } as const;
|
|
45
47
|
}
|
|
46
48
|
const initialState = options.getState();
|
|
47
|
-
const
|
|
48
|
-
const plan =
|
|
49
|
-
|
|
50
|
-
if (!plan || !source) {
|
|
49
|
+
const planPath = initialState.planPath;
|
|
50
|
+
const plan = planPath ? await readPlanFile(planPath) : undefined;
|
|
51
|
+
if (!planPath || !plan) {
|
|
51
52
|
ctx.ui.notify("No completed plan is available to implement.", "warning");
|
|
52
53
|
return { kind: "rejected" } as const;
|
|
53
54
|
}
|
|
@@ -57,15 +58,12 @@ export async function startFreshImplementationFromState(
|
|
|
57
58
|
return (
|
|
58
59
|
options.menuIsCurrent() &&
|
|
59
60
|
current.enabled === wasEnabled &&
|
|
60
|
-
|
|
61
|
-
? current.latestPlan === plan && current.latestPlanSource === source
|
|
62
|
-
: current.savedPlan === savedPlan)
|
|
61
|
+
current.planPath === planPath
|
|
63
62
|
);
|
|
64
63
|
};
|
|
65
64
|
return startFreshImplementationSession(ctx, {
|
|
66
65
|
plan,
|
|
67
|
-
|
|
68
|
-
retention: options.retention,
|
|
66
|
+
planPath,
|
|
69
67
|
stateEntryType: options.stateEntryType,
|
|
70
68
|
isCurrent,
|
|
71
69
|
});
|
|
@@ -84,19 +82,14 @@ export async function startFreshImplementationSession(
|
|
|
84
82
|
if (!(await preflightModel(ctx, request.isCurrent))) return { kind: "rejected" };
|
|
85
83
|
if (!request.isCurrent()) return { kind: "stale" };
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
plan: request.plan,
|
|
90
|
-
source: request.source,
|
|
91
|
-
startedAt: Date.now(),
|
|
92
|
-
retention: request.retention,
|
|
93
|
-
};
|
|
85
|
+
// The destination points at the same durable plan file: the plan itself is
|
|
86
|
+
// never copied, so both sessions observe later hand-edits identically.
|
|
94
87
|
const destinationState: PlanModeState = {
|
|
95
88
|
enabled: false,
|
|
96
89
|
awaitingAction: false,
|
|
97
|
-
|
|
90
|
+
planPath: request.planPath,
|
|
98
91
|
};
|
|
99
|
-
const handoff = formatImplementationHandoff(request.
|
|
92
|
+
const handoff = formatImplementationHandoff(request.planPath);
|
|
100
93
|
const parentSession = ctx.sessionManager.getSessionFile();
|
|
101
94
|
let setupError: string | undefined;
|
|
102
95
|
let kickoffError: string | undefined;
|
package/src/interactive-ui.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { showActiveImplementationMenu } from "./active-implementation-menu.js";
|
|
2
2
|
export { showPlanModeMenu, showReadyPlanMenu } from "./plan-action-menus.js";
|
|
3
3
|
export { showPlanLaunchMenu } from "./plan-launch-menu.js";
|
|
4
|
-
export { showSavedPlanMenu } from "./saved-plan-menu.js";
|
|
5
4
|
export { showPlanModeSettings } from "./settings-menu.js";
|