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 {