/** * MPC (Mental Preview & Correction) Extension * * A multi-phase mode where the LLM explores the codebase, dry-runs the task, * detects issues, backtracks if needed, and presents a verified plan before * any code is modified. * * Phases: explore → dryrun → issues → (backtrack →) verified → execute * * Commands: * /mpc - Toggle MPC mode on/off * /mpc-next - Manually advance to next phase (fallback if LLM misses marker) * /mpc-resume - Resume the most recent in-progress plan from disk * /mpc-status - Show current phase and plan file path * * Shortcut: Ctrl+Alt+M */ import * as path from "path"; import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { Key } from "@mariozechner/pi-tui"; import { buildPhasePrompt, phaseLabel } from "./phases.js"; import { MpcPlanPersister } from "./persist.js"; import { detectPhaseMarker, getAssistantText, hasIssues, type MpcPhase } from "./utils.js"; const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"]; const FULL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"]; const MAX_BACKTRACKS = 2; // Destructive command patterns — blocked during read-only phases 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*eza\b/, ]; 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; } export default function mpcExtension(pi: ExtensionAPI): void { let phase: MpcPhase = "idle"; let backtrackCount = 0; let persister: MpcPlanPersister | null = null; // Track last turn's full text per phase for recording let lastTurnText = ""; // The task description (captured from first user prompt after /mpc enable) let currentTask = ""; // Whether this session was resumed from a persisted plan let resumedFromFile = false; // ─── Persist helpers ──────────────────────────────────────────────────── function getPersister(ctx: ExtensionContext): MpcPlanPersister { if (!persister) { persister = new MpcPlanPersister(ctx.cwd); } return persister; } // ─── Status ───────────────────────────────────────────────────────────── function updateStatus(ctx: ExtensionContext): void { if (phase === "idle") { ctx.ui.setStatus("mpc", undefined); return; } const color = phase === "verified" ? "success" : phase === "execute" ? "accent" : "warning"; ctx.ui.setStatus("mpc", ctx.ui.theme.fg(color, `MPC: ${phaseLabel(phase)}`)); } // ─── Lifecycle ────────────────────────────────────────────────────────── function enable(ctx: ExtensionContext, task = ""): void { phase = "explore"; backtrackCount = 0; currentTask = task || "(task pending)"; resumedFromFile = false; lastTurnText = ""; // Start a new persisted plan const p = getPersister(ctx); const filePath = p.start(currentTask); const relPath = path.relative(ctx.cwd, filePath); pi.setActiveTools(READ_ONLY_TOOLS); ctx.ui.notify(`MPC enabled → plan: ${relPath}`); updateStatus(ctx); } function disable(ctx: ExtensionContext): void { if (persister) { persister.complete("idle"); } phase = "idle"; backtrackCount = 0; currentTask = ""; resumedFromFile = false; persister = null; pi.setActiveTools(FULL_TOOLS); ctx.ui.notify("MPC disabled. Full tool access restored."); updateStatus(ctx); } function toggle(ctx: ExtensionContext): void { if (phase === "idle") { enable(ctx); } else { disable(ctx); } } function advance(ctx: ExtensionContext, next: MpcPhase): void { // Record the current phase's output before advancing if (persister && lastTurnText && phase !== "idle") { persister.recordPhase(phase, lastTurnText, next, backtrackCount); } else if (persister) { persister.updateState(next, backtrackCount); } phase = next; if (next === "execute") { pi.setActiveTools(FULL_TOOLS); } updateStatus(ctx); pi.sendMessage( { customType: "mpc-phase", content: buildPhasePrompt(next), display: false }, { triggerTurn: true, deliverAs: "steer" }, ); } // ─── Commands ─────────────────────────────────────────────────────────── pi.registerCommand("mpc", { description: "Toggle MPC mode (mental preview & correction before execution)", handler: async (_args, ctx) => toggle(ctx), }); pi.registerCommand("mpc-next", { description: "Manually advance MPC to the next phase (use if LLM missed the phase marker)", handler: async (_args, ctx) => { if (phase === "idle") { ctx.ui.notify("MPC is not active. Use /mpc to enable.", "warning"); return; } const transitions: Partial> = { explore: "dryrun", dryrun: "issues", issues: "verified", backtrack: "verified", verified: "execute", }; const next = transitions[phase]; if (next) { ctx.ui.notify(`MPC: manually advancing from ${phaseLabel(phase)} → ${phaseLabel(next)}`); advance(ctx, next); } else { ctx.ui.notify(`MPC: no transition available from phase "${phaseLabel(phase)}"`, "warning"); } }, }); pi.registerCommand("mpc-resume", { description: "Resume the most recent in-progress MPC plan from disk", handler: async (_args, ctx) => { if (phase !== "idle") { ctx.ui.notify("MPC is already active. Use /mpc to disable first.", "warning"); return; } const resumable = MpcPlanPersister.findResumable(ctx.cwd); if (!resumable) { ctx.ui.notify("No in-progress MPC plan found in this directory.", "warning"); return; } const relPath = path.relative(ctx.cwd, resumable.path); const plan = resumable.plan; const confirmed = await ctx.ui.confirm( "Resume MPC Plan?", `Found plan: ${relPath}\nTask: ${plan.task}\nPhase: ${phaseLabel(plan.currentPhase)}\nResume from here?`, ); if (!confirmed) return; // Restore state const p = getPersister(ctx); p.load(resumable.path); phase = plan.currentPhase; backtrackCount = plan.backtrackCount; currentTask = plan.task; resumedFromFile = true; if (phase === "execute" || phase === "verified") { pi.setActiveTools(FULL_TOOLS); } else { pi.setActiveTools(READ_ONLY_TOOLS); } updateStatus(ctx); ctx.ui.notify(`MPC resumed: ${phaseLabel(phase)} — ${relPath}`); // Inject resume context so LLM knows where we are pi.sendMessage( { customType: "mpc-resume", content: buildResumePrompt(plan), display: false, }, { triggerTurn: false }, ); }, }); pi.registerCommand("mpc-status", { description: "Show current MPC phase and plan file path", handler: async (_args, ctx) => { if (phase === "idle") { ctx.ui.notify("MPC is not active."); return; } const p = getPersister(ctx); const filePath = p.filePath; const relPath = filePath ? path.relative(ctx.cwd, filePath) : "(no file)"; ctx.ui.notify( `MPC: phase=${phaseLabel(phase)}, backtracks=${backtrackCount}, plan=${relPath}`, ); }, }); // ─── Shortcut ─────────────────────────────────────────────────────────── pi.registerShortcut(Key.ctrlAlt("m"), { description: "Toggle MPC mode", handler: async (ctx) => toggle(ctx), }); // ─── Capture task from first user prompt after enable ─────────────────── pi.on("input", async (event, ctx) => { if (phase !== "explore" || resumedFromFile) return; // First real user message after /mpc enable — use it as task description const text = event.text.trim(); if (text && text.length > 0 && currentTask === "(task pending)") { currentTask = text.length > 80 ? text.slice(0, 77) + "..." : text; // Rename the plan file stub to include the actual task slug const p = getPersister(ctx); const newPath = p.renameWithTask(currentTask); if (newPath) { const relPath = path.relative(ctx.cwd, newPath); ctx.ui.notify(`MPC plan: ${relPath}`); } } }); // ─── Tool call guard (block destructive ops in read-only phases) ───────── pi.on("tool_call", async (event) => { if (phase === "idle" || phase === "execute" || phase === "verified") return; if (event.toolName !== "bash") return; const command = event.input.command as string; if (!isSafeCommand(command)) { return { block: true, reason: `MPC (${phaseLabel(phase)} phase): write command blocked. Use /mpc-next to advance or /mpc to abort.`, }; } }); // ─── Inject phase prompt before each turn ──────────────────────────────── pi.on("before_agent_start", async () => { if (phase === "idle") return; return { message: { customType: "mpc-context", content: buildPhasePrompt(phase), display: false, }, }; }); // ─── Track last turn text for phase recording ──────────────────────────── pi.on("turn_end", async (event, ctx) => { if (phase === "idle") return; const text = getAssistantText(event.message); if (text) lastTurnText = text; if (phase === "execute" || phase === "verified") return; if (phase === "explore" && detectPhaseMarker(text, "explore")) { advance(ctx, "dryrun"); } else if (phase === "dryrun" && detectPhaseMarker(text, "dryrun")) { advance(ctx, "issues"); } else if (phase === "issues") { if (detectPhaseMarker(text, "issues")) { // "NO ISSUES FOUND" — skip backtrack advance(ctx, "verified"); } else if (hasIssues(text)) { if (backtrackCount >= MAX_BACKTRACKS) { ctx.ui.notify("MPC: max backtracks reached — advancing to verified with unresolved issues noted.", "warning"); advance(ctx, "verified"); } else { backtrackCount++; advance(ctx, "backtrack"); } } } else if (phase === "backtrack" && detectPhaseMarker(text, "backtrack")) { advance(ctx, "verified"); } }); // ─── Agent end: confirmed execution or plan review ─────────────────────── pi.on("agent_end", async (_event, ctx) => { if (phase === "execute") { if (persister) persister.complete("execute"); phase = "idle"; backtrackCount = 0; currentTask = ""; persister = null; pi.setActiveTools(FULL_TOOLS); updateStatus(ctx); return; } if (phase !== "verified") return; if (!ctx.hasUI) { // Non-interactive mode: stay at verified; user can /mpc-next to execute return; } const p = getPersister(ctx); const relPath = p.filePath ? path.relative(ctx.cwd, p.filePath) : null; const planLabel = relPath ? ` (saved: ${relPath})` : ""; const choice = await ctx.ui.select(`MPC: Plan verified${planLabel}. What next?`, [ "Execute the plan", "Refine further", "Abort", ]); if (choice === "Execute the plan") { phase = "execute"; pi.setActiveTools(FULL_TOOLS); updateStatus(ctx); if (persister) persister.updateState("execute", backtrackCount); pi.sendMessage( { customType: "mpc-execute", content: "Execute the verified plan. Follow the steps exactly as described.", display: true, }, { triggerTurn: true }, ); } else if (choice === "Refine further") { const refinement = await ctx.ui.editor("Refinement instructions:", ""); if (refinement?.trim()) { pi.sendUserMessage(refinement.trim()); } } else { // Abort — mark plan as idle on disk if (persister) persister.complete("idle"); disable(ctx); } }); } // ─── Resume context prompt ────────────────────────────────────────────────── import type { MpcPlanFile } from "./persist.js"; function buildResumePrompt(plan: MpcPlanFile): string { const phaseLabels: Record = { idle: "Idle", explore: "Phase 1: Exploration", dryrun: "Phase 2: Dry Run", issues: "Phase 3: Issue Detection", backtrack: "Phase 4: Backtrack", verified: "Phase 5: Verified Plan", execute: "Executing", }; const lines: string[] = [ `[MPC RESUMED — continuing from a previous session]`, `Task: ${plan.task}`, `Current phase: ${phaseLabels[plan.currentPhase]}`, `Backtracks so far: ${plan.backtrackCount}`, "", "Previously completed phases:", ]; const order: MpcPhase[] = ["explore", "dryrun", "issues", "backtrack", "verified"]; for (const ph of order) { const record = plan.phases[ph]; if (!record) continue; lines.push(`\n### ${phaseLabels[ph]} (completed ${record.completedAt})`); lines.push(record.content.trim()); } lines.push(""); lines.push( `Continue from the current phase: ${phaseLabels[plan.currentPhase]}. ` + `Do not repeat completed phases. Pick up where you left off.`, ); return lines.join("\n"); }