diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro
index 1bb0909..994706c 100644
--- a/apps/marketing/src/pages/index.astro
+++ b/apps/marketing/src/pages/index.astro
@@ -8,7 +8,7 @@ import Step from '../components/Step.astro';
- For Claude Code & OpenCode
+ For Claude Code, OpenCode & Pi
@@ -28,7 +28,7 @@ import Step from '../components/Step.astro';
Interactive Plan Review for coding agents. Mark up and refine plans visually,
- share for team collaboration. Works with Claude Code and OpenCode.
+ share for team collaboration. Works with Claude Code, OpenCode, and Pi.
@@ -67,6 +67,15 @@ import Step from '../components/Step.astro';
>
Also watch for OpenCode →
+
+
+ Also watch for Pi →
+
diff --git a/apps/pi-extension/README.md b/apps/pi-extension/README.md
index c6f17dc..b0ab777 100644
--- a/apps/pi-extension/README.md
+++ b/apps/pi-extension/README.md
@@ -45,9 +45,9 @@ Start Pi in plan mode:
pi --plan
```
-Or toggle it during a session with `/plannotator` or `Ctrl+Alt+P`.
+Or toggle it during a session with `/plannotator` or `Ctrl+Alt+P`. The command accepts an optional file path argument (`/plannotator plans/auth.md`) or prompts you to choose one interactively.
-In plan mode the agent is restricted to read-only tools. It explores your codebase, then writes a plan to `PLAN.md` using markdown checklists:
+In plan mode the agent is restricted — destructive commands are blocked, writes are limited to the plan file. It explores your codebase, then writes a plan using markdown checklists:
```markdown
- [ ] Add validation to the login form
@@ -79,7 +79,8 @@ During execution, the agent marks completed steps with `[DONE:n]` markers. Progr
| Command | Description |
|---------|-------------|
-| `/plannotator` | Toggle plan mode on/off |
+| `/plannotator [path]` | Toggle plan mode. Accepts optional file path or prompts interactively |
+| `/plannotator-set-file
` | Change the plan file path mid-session |
| `/plannotator-status` | Show current phase, plan file, and progress |
| `/plannotator-review` | Open code review UI for current changes |
| `/plannotator-annotate ` | Open markdown file in annotation UI |
@@ -102,8 +103,9 @@ During execution, the agent marks completed steps with `[DONE:n]` markers. Progr
The extension manages a state machine: **idle** → **planning** → **executing** → **idle**.
During **planning**:
-- Tools restricted to: `read`, `bash` (read-only commands only), `grep`, `find`, `ls`, `write` (plan file only), `exit_plan_mode`
-- `edit` is disabled, bash is gated to a read-only allowlist, writes only allowed to the plan file
+- All tools from other extensions remain available
+- Bash is unrestricted — the agent is guided by the system prompt not to run destructive commands
+- Writes and edits restricted to the plan file only
During **executing**:
- Full tool access: `read`, `bash`, `edit`, `write`
diff --git a/apps/pi-extension/index.ts b/apps/pi-extension/index.ts
index c14892c..1c8e724 100644
--- a/apps/pi-extension/index.ts
+++ b/apps/pi-extension/index.ts
@@ -10,7 +10,7 @@
* - /plannotator command or Ctrl+Alt+P to toggle
* - --plan flag to start in planning mode
* - --plan-file flag to customize the plan file path
- * - Bash restricted to read-only commands during planning
+ * - Bash unrestricted during planning (prompt-guided)
* - Write restricted to plan file only during planning
* - exit_plan_mode tool with browser-based visual approval
* - [DONE:n] markers for execution progress tracking
@@ -26,7 +26,7 @@ import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
import { Type } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { Key } from "@mariozechner/pi-tui";
-import { isSafeCommand, markCompletedSteps, parseChecklist, type ChecklistItem } from "./utils.js";
+import { markCompletedSteps, parseChecklist, type ChecklistItem } from "./utils.js";
import {
startPlanReviewServer,
startReviewServer,
@@ -51,10 +51,8 @@ try {
// HTML not built yet — review feature will be unavailable
}
-// Tool sets by phase
-const PLANNING_TOOLS = ["read", "bash", "grep", "find", "ls", "write", "edit", "exit_plan_mode"];
-const EXECUTION_TOOLS = ["read", "bash", "edit", "write"];
-const NORMAL_TOOLS = ["read", "bash", "edit", "write"];
+/** Extra tools to ensure are available during planning (on top of whatever is already active). */
+const PLANNING_EXTRA_TOOLS = ["grep", "find", "ls", "exit_plan_mode"];
type Phase = "idle" | "planning" | "executing";
@@ -73,11 +71,12 @@ export default function plannotator(pi: ExtensionAPI): void {
let phase: Phase = "idle";
let planFilePath = "PLAN.md";
let checklistItems: ChecklistItem[] = [];
+ let preplanTools: string[] | null = null;
// ── Flags ────────────────────────────────────────────────────────────
pi.registerFlag("plan", {
- description: "Start in plan mode (read-only exploration)",
+ description: "Start in plan mode (restricted exploration and planning)",
type: "boolean",
default: false,
});
@@ -126,10 +125,26 @@ export default function plannotator(pi: ExtensionAPI): void {
pi.appendEntry("plannotator", { phase, planFilePath });
}
+ /** Apply tool visibility for the current phase, preserving tools from other extensions. */
+ function applyToolsForPhase(): void {
+ if (phase === "planning") {
+ const base = preplanTools ?? pi.getActiveTools();
+ const toolSet = new Set(base);
+ for (const t of PLANNING_EXTRA_TOOLS) toolSet.add(t);
+ pi.setActiveTools([...toolSet]);
+ } else if (preplanTools) {
+ // Restore pre-plan tool set (removes exit_plan_mode, etc.)
+ pi.setActiveTools(preplanTools);
+ preplanTools = null;
+ }
+ // If no preplanTools (e.g. session restore to executing/idle), leave tools as-is
+ }
+
function enterPlanning(ctx: ExtensionContext): void {
phase = "planning";
checklistItems = [];
- pi.setActiveTools(PLANNING_TOOLS);
+ preplanTools = pi.getActiveTools();
+ applyToolsForPhase();
updateStatus(ctx);
updateWidget(ctx);
persistState();
@@ -139,7 +154,7 @@ export default function plannotator(pi: ExtensionAPI): void {
function exitToIdle(ctx: ExtensionContext): void {
phase = "idle";
checklistItems = [];
- pi.setActiveTools(NORMAL_TOOLS);
+ applyToolsForPhase();
updateStatus(ctx);
updateWidget(ctx);
persistState();
@@ -158,7 +173,24 @@ export default function plannotator(pi: ExtensionAPI): void {
pi.registerCommand("plannotator", {
description: "Toggle plannotator (file-based plan mode)",
- handler: async (_args, ctx) => togglePlanMode(ctx),
+ handler: async (args, ctx) => {
+ if (phase !== "idle") {
+ exitToIdle(ctx);
+ return;
+ }
+
+ // Accept path as argument: /plannotator plans/auth.md
+ let targetPath = args?.trim() || undefined;
+
+ // No arg — prompt for file path interactively
+ if (!targetPath && ctx.hasUI) {
+ targetPath = await ctx.ui.input("Plan file path", planFilePath);
+ if (targetPath === undefined) return; // cancelled
+ }
+
+ if (targetPath) planFilePath = targetPath;
+ enterPlanning(ctx);
+ },
});
pi.registerCommand("plannotator-status", {
@@ -173,6 +205,27 @@ export default function plannotator(pi: ExtensionAPI): void {
},
});
+ pi.registerCommand("plannotator-set-file", {
+ description: "Change the plan file path",
+ handler: async (args, ctx) => {
+ let targetPath = args?.trim() || undefined;
+
+ if (!targetPath && ctx.hasUI) {
+ targetPath = await ctx.ui.input("Plan file path", planFilePath);
+ if (targetPath === undefined) return; // cancelled
+ }
+
+ if (!targetPath) {
+ ctx.ui.notify(`Current plan file: ${planFilePath}`, "info");
+ return;
+ }
+
+ planFilePath = targetPath;
+ persistState();
+ ctx.ui.notify(`Plan file changed to: ${planFilePath}`);
+ },
+ });
+
pi.registerCommand("plannotator-review", {
description: "Open interactive code review for current changes",
handler: async (_args, ctx) => {
@@ -266,7 +319,7 @@ export default function plannotator(pi: ExtensionAPI): void {
label: "Exit Plan Mode",
description:
"Submit your plan for user review. " +
- "Call this after drafting or revising your plan in PLAN.md. " +
+ "Call this after drafting or revising your plan file. " +
"The user will review the plan in a visual browser UI and can approve, deny with feedback, or annotate it. " +
"If denied, use the edit tool to make targeted revisions (not write), then call this again.",
parameters: Type.Object({
@@ -319,7 +372,7 @@ export default function plannotator(pi: ExtensionAPI): void {
// Non-interactive or no HTML: auto-approve
if (!ctx.hasUI || !planHtmlContent) {
phase = "executing";
- pi.setActiveTools(EXECUTION_TOOLS);
+ applyToolsForPhase();
persistState();
return {
content: [
@@ -348,7 +401,7 @@ export default function plannotator(pi: ExtensionAPI): void {
if (result.approved) {
phase = "executing";
- pi.setActiveTools(EXECUTION_TOOLS);
+ applyToolsForPhase();
updateStatus(ctx);
updateWidget(ctx);
persistState();
@@ -398,20 +451,10 @@ export default function plannotator(pi: ExtensionAPI): void {
// ── Event Handlers ───────────────────────────────────────────────────
- // Gate writes and bash during planning
+ // Gate writes during planning
pi.on("tool_call", async (event, ctx) => {
if (phase !== "planning") return;
- if (event.toolName === "bash") {
- const command = event.input.command as string;
- if (!isSafeCommand(command)) {
- return {
- block: true,
- reason: `Plannotator: command blocked (not in read-only allowlist).\nCommand: ${command}`,
- };
- }
- }
-
if (event.toolName === "write") {
const targetPath = resolve(ctx.cwd, event.input.path as string);
const allowedPath = resolvePlanPath(ctx.cwd);
@@ -444,7 +487,9 @@ export default function plannotator(pi: ExtensionAPI): void {
content: `[PLANNOTATOR - PLANNING PHASE]
You are in plan mode. You MUST NOT make any changes to the codebase — no edits, no commits, no installs, no destructive commands. The ONLY file you may write to or edit is the plan file: ${planFilePath}.
-Available tools: read, bash (read-only commands only), grep, find, ls, write (${planFilePath} only), edit (${planFilePath} only), exit_plan_mode
+Available tools: read, bash, grep, find, ls, write (${planFilePath} only), edit (${planFilePath} only), exit_plan_mode
+
+Do not run destructive bash commands (rm, git push, npm install, etc.) — focus on reading and exploring the codebase. Web fetching (curl, wget) is fine.
## Iterative Planning Workflow
@@ -590,7 +635,7 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
);
phase = "idle";
checklistItems = [];
- pi.setActiveTools(NORMAL_TOOLS);
+ applyToolsForPhase();
updateStatus(ctx);
updateWidget(ctx);
persistState();
@@ -657,9 +702,7 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
// Apply tool restrictions for current phase
if (phase === "planning") {
- pi.setActiveTools(PLANNING_TOOLS);
- } else if (phase === "executing") {
- pi.setActiveTools(EXECUTION_TOOLS);
+ applyToolsForPhase();
}
updateStatus(ctx);
diff --git a/apps/pi-extension/utils.ts b/apps/pi-extension/utils.ts
index eb570b4..f3f6f41 100644
--- a/apps/pi-extension/utils.ts
+++ b/apps/pi-extension/utils.ts
@@ -1,56 +1,10 @@
/**
* Plannotator Pi extension utilities.
*
- * Inlined versions of bash safety checks and checklist parsing.
+ * Checklist parsing and progress tracking helpers.
* (No access to pi-mono's plan-mode/utils at runtime.)
*/
-// ── Bash Safety ──────────────────────────────────────────────────────────
-
-const DESTRUCTIVE_PATTERNS = [
- /\brm\b/i, /\brmdir\b/i, /\bmv\b/i, /\bcp\b/i, /\bmkdir\b/i,
- /\btouch\b/i, /\bchmod\b/i, /\bchown\b/i, /\bchgrp\b/i, /\bln\b/i,
- /\btee\b/i, /\btruncate\b/i, /\bdd\b/i, /\bshred\b/i,
- /(^|[^<])>(?!>)/, />>/,
- /\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
- /\byarn\s+(add|remove|install|publish)/i,
- /\bpnpm\s+(add|remove|install|publish)/i,
- /\bpip\s+(install|uninstall)/i,
- /\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
- /\bbrew\s+(install|uninstall|upgrade)/i,
- /\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
- /\bsudo\b/i, /\bsu\b/i, /\bkill\b/i, /\bpkill\b/i, /\bkillall\b/i,
- /\breboot\b/i, /\bshutdown\b/i,
- /\bsystemctl\s+(start|stop|restart|enable|disable)/i,
- /\bservice\s+\S+\s+(start|stop|restart)/i,
- /\b(vim?|nano|emacs|code|subl)\b/i,
-];
-
-const SAFE_PATTERNS = [
- /^\s*cat\b/, /^\s*head\b/, /^\s*tail\b/, /^\s*less\b/, /^\s*more\b/,
- /^\s*grep\b/, /^\s*find\b/, /^\s*ls\b/, /^\s*pwd\b/, /^\s*echo\b/,
- /^\s*printf\b/, /^\s*wc\b/, /^\s*sort\b/, /^\s*uniq\b/, /^\s*diff\b/,
- /^\s*file\b/, /^\s*stat\b/, /^\s*du\b/, /^\s*df\b/, /^\s*tree\b/,
- /^\s*which\b/, /^\s*whereis\b/, /^\s*type\b/, /^\s*env\b/,
- /^\s*printenv\b/, /^\s*uname\b/, /^\s*whoami\b/, /^\s*id\b/,
- /^\s*date\b/, /^\s*cal\b/, /^\s*uptime\b/, /^\s*ps\b/,
- /^\s*top\b/, /^\s*htop\b/, /^\s*free\b/,
- /^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
- /^\s*git\s+ls-/i,
- /^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
- /^\s*yarn\s+(list|info|why|audit)/i,
- /^\s*node\s+--version/i, /^\s*python\s+--version/i,
- /^\s*curl\s/i, /^\s*wget\s+-O\s*-/i,
- /^\s*jq\b/, /^\s*sed\s+-n/i, /^\s*awk\b/,
- /^\s*rg\b/, /^\s*fd\b/, /^\s*bat\b/, /^\s*exa\b/,
-];
-
-export function isSafeCommand(command: string): boolean {
- const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
- const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
- return !isDestructive && isSafe;
-}
-
// ── Checklist Parsing ────────────────────────────────────────────────────
export interface ChecklistItem {