- TypeScript 99.6%
- Nix 0.4%
|
All checks were successful
CI / Check, test, and validate package (push) Successful in 1m3s
- Skip plan_mode_complete registration and state restore when PI_SUBAGENT_* env is set so forked children complete tasks directly instead of answering with a plan - Strip inherited plan artifacts from subagent model context and stub /plan with an unavailable notice |
||
|---|---|---|
| .forgejo/workflows | ||
| src | ||
| test | ||
| .gitignore | ||
| devenv.lock | ||
| devenv.nix | ||
| devenv.yaml | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
🧭 pi-plan — Plan Before Pi Edits Code
Use a Codex-like /plan mode to explore a codebase, resolve ambiguity from project context, and approve an implementation-ready plan before Pi edits files.
✨ Features
- Toggles Plan mode with bare
/plan, or starts planning immediately with/plan <prompt>. - Enforces read-only planning through the plan prompt alone, with no runtime tool policy blocking your tools.
- Uses project context and explicit assumptions to resolve ambiguity, then explicitly completes a decision-ready plan.
- Reviews the complete plan before implementation, further planning, or discard.
- Implements in the planning session or a fresh linked session with the approved plan.
- Restores Plan state across resume and compaction.
- Configures plan reinjection, the toggle shortcut, and thinking level.
- Publishes statusline state and cooperates anonymously with Workflow Mutex Protocol v1 participants.
- Stays out of subagent sessions: no Plan mode, no completion tool, inherited Plan artifacts stripped from context.
📦 Install
This release requires Pi 0.80.6 or newer.
pi install npm:@hayshin/pi-plan
Install directly from the SSH git repository:
pi install ssh://forgejo@ssh.hayshin.dev/agents/pi-plan.git
Try without installing permanently:
pi -e npm:@hayshin/pi-plan
Try a local checkout directly from the repository root:
pnpm install
pi -e .
Pi loads src/index.ts and its TypeScript imports directly through Jiti. Package installs fetch runtime dependencies but require no build or generated dist/ files.
Install only from sources you trust because Pi extensions run with Pi's permissions.
🚀 Quick start
Run /plan to enter Plan mode, then ask the agent to inspect and design the change.
Run /plan <prompt> when the first planning request is already known.
Run /plan again to exit Plan mode.
💬 Commands
/plan
/plan <prompt>
Bare /plan is a pure toggle.
When Plan mode is off, it enters Plan mode without sending a model message; when Plan mode is on, it exits and discards a ready plan if one is stored.
The toggle works in TUI, RPC, print, and JSON modes with no menu and no UI requirement.
Use /plan <prompt> to enter Plan mode (or stay active) and immediately submit <prompt> as the first Plan-mode user message.
There are no reserved argument tokens: any non-empty argument is an inline planning prompt, and the command registers no argument completions.
Pi executes extension commands immediately during streaming, but changing Plan state or handing off implementation during an active run would mix two mode contracts inside one run. Plan mode therefore rejects busy start, exit, implementation, state-changing menu action, and configured-shortcut transitions without changing state; wait for the run to settle and retry. Prompts submitted while Plan mode is already active remain ordinary Plan follow-ups. TUI and RPC show a warning for a rejected busy transition, while print and JSON routes throw an observable error.
🛡️ Prompt-only enforcement
Plan mode is enforced by the plan prompt alone; the extension applies no runtime tool policy.
While Plan mode is active, every tool the model can call remains callable, and the prompt instructs the agent to explore read-only with inspection tools and non-mutating shell commands until it completes a decision-ready plan.
That makes Plan mode a collaboration contract rather than a sandbox: an agent that ignores the prompt can still write files or run mutating commands.
The only runtime gate is the plan_mode_complete helper itself, whose tool_call handler rejects calls while Plan mode is inactive or does not own the active workflow, so a stale or legacy completion call cannot accept a plan outside an active Plan workflow.
🚫 Subagent sessions
Plan mode never activates inside subagent sessions (detected via PI_SUBAGENT_SESSION, PI_SUBAGENT_ID, or PI_SUBAGENT_NAME, as set by @hayshin/pi-subagents).
There plan_mode_complete is not registered, /plan reports that Plan mode is unavailable, and inherited Plan artifacts (transition markers, proposed plans, completion calls/results) are stripped from the subagent's model context.
A forked child therefore treats the parent's planning history as background and completes its delegated task directly instead of answering with a plan.
🧭 Planning and implementation
A complete Plan mode answer should appear only after the agent has resolved discoverable facts and recorded explicit assumptions for remaining ambiguity.
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.
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 other tool in the batch 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 ordinary research or clarification turns never trigger automatic completion.
If a planning turn ends without plan_mode_complete, Plan mode simply stays active and you can ask the agent to complete the plan again.
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.
When a plan completes, the ready-plan menu is shown automatically in TUI and RPC modes after the agent settles.
It shows Implement here and Start fresh and implement, alongside Stay in Plan mode and Discard plan and exit, explains which conversation context each choice uses, and previews the selected Plan reinjection policy.
Dismissing the menu with Escape or Ctrl+C keeps the plan ready; the agent can resubmit it at any time, and asking the agent to re-present the plan calls plan_mode_complete again, which reopens the menu.
While Plan mode is active, bare /plan exits and discards the ready plan.
Implement here appends the Normal contract, captures the reinjection setting, and starts implementation in the current session with its complete planning conversation and tool calls.
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.
A resumed session keeps the plan ready; ask the agent to re-present the plan to reopen the menu before choosing fresh.
A successful fresh handoff does not delete or consume the source planning session.
Resume it later to inspect or hand off the ready 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.
Under Off — conversation history only, the destination receives the complete plan in its initial user prompt and does not persist active-plan state.
If that kickoff fails, the complete request remains in the destination editor and the source remains resumable.
Under either guaranteed-plan policy, the destination persists active-plan state before kickoff.
If guaranteed-plan persistence fails, the complete request is placed in the destination editor and the source remains resumable.
If a guaranteed-plan kickoff fails, the destination retains the active plan; send a message to continue or resume the parent planning session.
Fresh implementation checks idle state, the selected model, and authentication before session replacement; Implement here keeps its established preflight behavior.
Text print and JSON modes support the same /plan toggle and /plan <prompt> flow; there is simply no ready-plan menu in those modes.
Both implementation paths apply the current Plan reinjection policy in their destination.
The default Off — conversation history only policy does not create active-plan state or inject a hidden plan context.
Implement here sends Implement the plan. and leaves the accepted plan in ordinary planning history.
Start fresh and implement puts the complete plan in one ordinary initial user prompt because no reliable planning conversation is present.
Later model calls then rely on Pi's normal conversation history and compaction behavior.
Through first implementation run guarantees the exact plan throughout that run, including retries, compaction retries, and queued continuation, then clears active-plan state at agent_settled.
Until superseded guarantees the exact plan across later turns, resume, and manual or automatic compaction until a new plan supersedes it.
The guaranteed policies avoid a duplicate context block while the original implementation handoff remains available and inject one hidden canonical copy after that handoff is compacted away.
Reinjection can consume up to the existing 50,000-character plan limit in model context.
Cleanup is bound to the matching implementation, so an older run settling cannot clear a newer handoff.
While a guaranteed plan is active, the TUI reports the active implementation; there is no manual clear route. Settings-file changes never alter the policy already captured by an active guaranteed-plan implementation. Automatic first-run cleanup removes the active status and future injected context after the triggering implementation run has received the complete plan. Starting a new Plan-mode workflow or implementing a replacement plan supersedes an active guaranteed plan. The extension deliberately does not infer completion from assistant prose or agent settlement under Until superseded, so start a new Plan-mode workflow to supersede it when it no longer applies. Under Off — conversation history only, implementation messages remain ordinary conversation history and there is no active plan to remove. 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 implementation, exiting discards the ready plan and removes its completion result from later non-Plan model context.
While Plan mode is enabled, the extension also publishes a compact status for the status bar/footer.
With @pi-theme this renders as a colored token in the footer; otherwise Pi's built-in footer shows it in the extension status area:
Plan mode: Plan mode is enabled and still gathering context or drafting a plan.Plan mode ✓: A completed plan is stored until you implement it, continue planning, or exit Plan mode.Plan implementing: The exact accepted plan is guaranteed under Through first implementation run or Until superseded.
You can also exit directly.
While Plan mode is active, bare /plan exits, and before implementation that discards the latest proposed plan.
During a guaranteed-plan implementation, Plan mode is not active, so bare /plan starts a new Plan-mode workflow; that supersedes the active plan and 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:
/plan
🧱 Cache-stable mode transitions
Plan and Normal requests share one append-only conversation.
Every transition appends one hidden, model-visible, versioned contract marker (a few tokens) to the session, so repeatedly toggling /plan never duplicates the multi-kilobyte plan prompt in the session.
The context hook expands only the latest marker to the full versioned Plan or Normal contract for the model and drops older contract artifacts from the model context; they remain persisted in the session.
Ordinary linear turns do not rewrite or duplicate these contracts.
Implement here retains the Plan dialogue, tool calls, completion evidence, and Implement the plan. kickoff in order.
Start fresh and implement is the isolation path and transfers only the approved plan plus the Normal contract to a linked session.
The context hook filters repeated legacy plan-mode-context artifacts but preserves current transition messages.
If compaction removes the effective transition, the hook inserts one canonical fallback at a deterministic retained-history boundary.
Repeated transforms leave that fallback in place instead of moving it to the newest turn.
An inactive legacy state entry does not inject a Normal contract, so sessions that never entered Plan mode keep their ordinary context after resume or reload.
Manual /tree navigation restores branch-owned Plan state and chooses the matching contract without navigating or adding a branch summary.
Pi lists hidden transition messages in /tree; Plan mode rejects those internal targets, so select an adjacent conversation entry.
Plan mode registers plan_mode_complete once and keeps its name and definition stable across Normal, Plan, ready, implementation, and restored workflows.
Visible helpers do not mean /plan is active, and their descriptions exclude ordinary planning, the writing-plans skill, roadmaps, checklists, and plan-file work.
Only the latest active Plan contract authorizes the helper; inactive or stale calls fail without accepting a plan.
Plan mode does not widen a restrictive active-tool policy; start or restore fails when a required helper is unavailable.
Stable schemas preserve a cache-eligible prefix but cannot guarantee a hit because provider serialization, cache lifetime, minimum prefix size, implementation details, and session affinity remain external.
The default thinkingLevel: "inherit" avoids a Plan-specific reasoning-parameter change.
A fixed Plan thinking level remains supported, but changing reasoning parameters can prevent provider-side state reuse even when prompts and tool schemas stay stable.
🤝 Workflow coexistence
Plan mode is independently installable and keeps its standalone behavior when no other protocol participant is present.
On the characterized Pi 0.84.2 runtime, it participates in the anonymous workflow:mutex:v1 agent-workflow group.
It holds the group while Planning is active, while a completed plan awaits review, and while revision is underway.
Ordinary implementation after Plan handoff does not hold the group.
Every inactive start performs one final synchronous admission after asynchronous preflight and before changing Plan state, persistence, prompts, thinking level, queues, or status. If another participant is active, TUI and RPC show an anonymous warning that another workflow is active. Print and JSON direct routes throw the same anonymous error before mutation.
Command, shortcut, and restored activation use the same admission boundary.
Restored active Plan state acquires before restoring thinking, status, or model hooks. If restoration is busy, Plan mode stays non-running, leaves persisted history and active tools untouched, and requires a later reload or explicit new start after the other workflow ends. Planning-session cancellation during a fresh implementation preflight keeps the source Plan and its ownership. Successful session replacement relies on source-session shutdown to clean up and release; the destination's ordinary active implementation does not acquire the Plan mutex.
The coexistence guarantee is cooperative and applies only when every contender implements v1 on the characterized Pi runtime and shares its event bus and session-manager identity.
A pre-v1, mixed-version, non-participating, forked, or otherwise uncharacterized counterpart remains unsupported for mutual exclusion.
Plan mode does not identify, inspect, configure, start, stop, or depend on another extension.
Guaranteed coexistence with Goal requires @narumitw/pi-goal 0.53.0 or newer and this package at 0.52.0 or newer on the characterized Pi 0.84.2 runtime.
| Installation | Support |
|---|---|
| Plan mode without another workflow participant | Supported standalone behavior |
Plan mode >=0.52.0 with Goal >=0.53.0 on Pi 0.84.2 |
Workflow Mutex v1 coexistence guarantee |
| Either package below its floor, or another Pi runtime | Standalone behavior only; mutual exclusion unsupported |
🛠️ Tools
plan_mode_completerecords the complete approved Markdown plan and terminates the planning turn when called alone.
⚙️ Settings
Edit $PI_CODING_AGENT_DIR/pi-plan-mode.json (normally ~/.pi/agent/pi-plan-mode.json) to configure Plan thinking, Plan reinjection, and Plan mode shortcut.
The optional file is read at session start, watched for changes, and created only by a manual edit; the extension never writes it.
The shortcut is disabled when toggleShortcut is omitted.
{
"thinkingLevel": "inherit",
"implementationPlanRetention": "clear-on-start",
"toggleShortcut": "<your_key>"
}
Plan helper tools
Plan helper schemas are stable from extension registration onward, and Plan mode does not call setActiveTools().
Tool visibility alone is not Plan activation.
Only the latest effective active Plan contract authorizes plan_mode_complete; ordinary planning and the writing-plans skill use their own workflow instead.
The retired toolVisibility, defaultPlanTools, and safeSubcommands keys are ignored and preserved as unknown data when the settings file is read.
Plan reinjection
The stable JSON field implementationPlanRetention controls whether and how long the context hook restores the exact approved plan when ordinary model context no longer contains it.
Omit it or use clear-on-start for Off — conversation history only, the default Codex-like behavior with no active-plan state or hidden context injection.
In the planning session, this policy sends Implement the plan. and relies on the accepted plan already present in ordinary conversation history.
A fresh session instead places the complete plan in one ordinary kickoff prompt because its planning history is unavailable or intentionally excluded.
Use clear-after-first-run for Through first implementation run to guarantee the exact plan until that implementation's first fully settled run ends.
Use keep for Until superseded to guarantee and reinject the exact plan until a new plan supersedes it.
A resumed guaranteed-plan cleanup policy re-arms against the first context in the replacement session.
Failed handoff delivery restores the ready plan and does not run automatic cleanup.
Changing this setting applies to the next Implement action only. Each guaranteed-plan implementation stores its effective policy, so a later settings-file change cannot shorten or extend an implementation already in progress. Conversation-history-only implementation has no active Plan-mode state to show or clear after kickoff.
Toggle shortcut
toggleShortcut controls the global Plan-mode keybinding used by the TUI shortcut.
Omit this setting to keep the shortcut disabled.
Set toggleShortcut to the key string you want.
Avoid values that conflict with editor shortcuts.
Thinking level
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.
Editing the file does not change Pi's current or default thinking level and takes effect only when the next Plan workflow starts.
The extension only reads the file and leaves its bytes untouched.
Invalid JSON, invalid values, oversized content (over 64 KiB), non-regular files, and read failures produce a warning and fall back to inherited thinking, clear-on-start, and no toggle shortcut.
Each Pi process reads the file independently, so edits across concurrent processes are not coordinated.
Compatibility: a valid legacy plan-mode.json remains readable with a warning and is never modified automatically.
If both files exist, the canonical filename takes precedence.
🧠 Codex-like behavior
This extension maps Codex's ModeKind::Plan behavior onto Pi's extension API:
- Plan mode is conversational collaboration, not TODO or progress tracking.
- Bare
/plantoggles Plan mode;/plan <prompt>enters Plan mode before submitting the prompt. - The agent resolves material ambiguity with project context and explicit assumptions, then completes with a standalone
plan_mode_completecall instead of prose detection. update_plan-style TODO tracking is not part of the workflow; the plan prompt directs the agent away from it.- The default
clear-on-startpolicy uses conversation history;clear-after-first-runandkeepadd exact-plan guarantees. - Enforcement is prompt-only, so Plan mode is a collaboration contract rather than Codex's runtime tool policy.
🗂️ Package layout
pi-plan/
├── src/
│ ├── index.ts # Thin Pi package entrypoint loaded directly by Jiti
│ ├── plan-mode.ts # Extension registration, mode state, and UI loading boundary
│ ├── interactive-ui.ts # Exports the ready-plan menu surface
│ ├── plan-action-controller.ts # Ready-plan menu lifecycle and action routing
│ ├── plan-action-menus.ts # Ready-plan menu definition
│ ├── fresh-implementation.ts # Fresh linked-session implementation handoff
│ ├── implementation-retention.ts # Plan reinjection policies and first-run cleanup
│ ├── mode-contract.ts # Versioned Plan/Normal mode contracts
│ ├── completion-tool.ts # plan_mode_complete helper tool
│ ├── presentation.ts # TUI widget, statusline state, and completion rendering
│ ├── prompt.ts # Plan-mode prompt
│ ├── settings.ts # Settings-file reading and normalization
│ ├── state.ts # Persisted Plan state
│ ├── workflow-mutex.ts # Workflow Mutex Protocol v1 participation
│ ├── required-tools.ts # Required helper-tool checks
│ ├── message-transform.ts # Plan context artifacts and history transforms
│ └── extension-runtime.ts # Stale-extension-context helpers
├── test/
├── .forgejo/workflows/ci.yml
├── README.md
├── LICENSE
├── devenv.nix
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.json
└── vite.config.ts
src/index.ts is the Pi entrypoint and forwards to plan-mode.ts; the other source modules are internal.
Relative source imports use explicit .ts suffixes so Pi's Jiti loader can resolve the full graph, while UI-heavy modules remain behind dynamic imports.
The package exposes its Pi extension through package.json:
{
"pi": {
"extensions": ["./src/index.ts"]
}
}
Development requires no generated runtime; the authoritative TypeScript source is tested and published directly.
🧪 Development
Use Nix with flakes and devenv for the pinned development environment, or provide a compatible Node.js and pnpm locally:
devenv shell
pnpm install
Pi is needed only for interactive testing. Launch the extension from source with:
pnpm run dev
Vite+ provides formatting, linting, type checking, and the specialized Vitest harness:
pnpm run check
pnpm run lint
pnpm run fmt
pnpm run test
pnpm run test:watch
pnpm run test:coverage
Coverage reports are written to coverage/. The Forgejo workflow runs checks, coverage, and package validation on pushes and pull requests.
🔐 Security
Pi extensions execute with the user's full permissions. Review the source and dependencies before installing, and report security issues privately to the repository owner rather than opening a public issue with exploit details.
📤 Publishing
The npm package intentionally contains only src/, README.md, LICENSE, and package metadata. Validate the exact package contents before publishing:
pnpm run check
pnpm run test:coverage
pnpm run pack:check
pnpm publish
The package is public under the existing @hayshin/pi-plan identity. Keep Pi-provided APIs in peerDependencies, keep @narumitw/pi-tui-kit as a runtime dependency, and do not replace the source entry point with generated output.
🔎 Keywords
Pi extension, Pi coding agent, plan mode, Codex-like plan mode, AI coding workflow, read-only planning, implementation plan.
📄 License
MIT.
See LICENSE.