From f8cde24d879ce8c7a1f469c23302a9bae34ee9eb Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Thu, 5 Feb 2026 18:51:49 -0500 Subject: [PATCH] feat: implement plan approval gate before execution Adds plan approval workflow per GitHub issue #111: - Register plan_approval tool in tools index - Add plan approval handling in message handler - Check for pending plans on each message - Detect approval/rejection messages - Handle plan approval flow - Create plan approval modal TUI component - Update system prompts to instruct agents to use plan_approval tool - Balanced tier: Updated to require plan_approval for complex tasks - Thorough tier: Updated with plan_approval workflow Workflow: 1. Agent analyzes task complexity with plan_approval 2. Agent creates plan with steps, context, risks 3. Agent submits plan for user approval 4. User approves/rejects/provides feedback 5. Only after approval, agent proceeds with execution Closes #111 --- src/prompts/system/tiers/balanced.ts | 56 ++-- src/prompts/system/tiers/thorough.ts | 91 +++--- src/services/chat-tui/message-handler.ts | 59 ++++ src/tools/index.ts | 2 + .../components/modals/plan-approval-modal.tsx | 268 ++++++++++++++++++ 5 files changed, 405 insertions(+), 71 deletions(-) create mode 100644 src/tui-solid/components/modals/plan-approval-modal.tsx diff --git a/src/prompts/system/tiers/balanced.ts b/src/prompts/system/tiers/balanced.ts index 79f14c6..8c42377 100644 --- a/src/prompts/system/tiers/balanced.ts +++ b/src/prompts/system/tiers/balanced.ts @@ -91,47 +91,55 @@ For multi-step tasks (3+ steps), use todowrite to track progress: export const BALANCED_TIER_PLAN_GATE = `## Plan Mode (Balanced Tier) -For COMPLEX tasks that meet these criteria, enter plan mode: -- Multi-file refactoring +For COMPLEX tasks that meet these criteria, use the plan_approval tool: +- Multi-file refactoring (3+ files) - New feature implementation - Architectural changes +- Security-related changes +- Database modifications - Tasks that could take 10+ tool calls -### Plan Mode Workflow +### CRITICAL: Plan Approval Workflow -1. **Explore Phase** - Gather context with glob, grep, read -2. **Plan Phase** - Create structured plan with steps -3. **Approval Gate** - Present plan, wait for user signal to proceed -4. **Execute Phase** - Implement the plan -5. **Verify Phase** - Test and confirm +You MUST use the plan_approval tool for complex tasks. DO NOT execute file modifications until the user approves the plan. -### Plan Format +1. **Analyze Task** - Use plan_approval with action="analyze_task" to check if plan approval is needed +2. **Create Plan** - Use plan_approval with action="create" to start a plan +3. **Add Context** - Use plan_approval with action="add_context" to document files analyzed +4. **Add Steps** - Use plan_approval with action="add_step" for each implementation step +5. **Add Risks** - Use plan_approval with action="add_risk" for identified risks +6. **Submit Plan** - Use plan_approval with action="submit" to present plan to user +7. **Wait for Approval** - DO NOT PROCEED until user says "yes", "proceed", "approve", or similar +8. **Execute** - Only after approval, implement the plan step by step + +### Example Plan Approval Flow \`\`\` -## Plan: [Task Name] +// Step 1: Analyze the task +plan_approval action="analyze_task" task_description="Implement user authentication system" -### Context -- [Key file 1]: [purpose] -- [Key file 2]: [purpose] +// Step 2: Create plan +plan_approval action="create" title="User Authentication System" summary="Add JWT-based auth with login, register, and protected routes" -### Steps -1. [First change] - [file affected] -2. [Second change] - [file affected] -3. [Verification] - [command] +// Step 3: Add context +plan_approval action="add_context" plan_id="" files_analyzed=["src/routes/", "src/middleware/"] -### Risks -- [Potential issue and mitigation] +// Step 4: Add steps +plan_approval action="add_step" plan_id="" step_title="Create auth middleware" step_description="Add JWT verification middleware" files_affected=["src/middleware/auth.ts"] risk_level="medium" -Ready to proceed? (Continue with implementation or provide feedback) +// Step 5: Submit for approval +plan_approval action="submit" plan_id="" testing_strategy="Run auth tests" rollback_plan="Revert commits" + +// STOP HERE - Wait for user to approve \`\`\` ### When to Skip Plan Mode -Skip plan mode for: +Skip plan approval for: - Single file changes -- Simple bug fixes -- Adding a function or component -- Configuration changes +- Simple bug fixes with obvious solutions +- Adding comments or documentation +- Formatting changes - Tasks with clear, limited scope`; export const BALANCED_TIER_AGENTS = `## Agent Delegation diff --git a/src/prompts/system/tiers/thorough.ts b/src/prompts/system/tiers/thorough.ts index 15cf93a..02c2ee3 100644 --- a/src/prompts/system/tiers/thorough.ts +++ b/src/prompts/system/tiers/thorough.ts @@ -110,67 +110,64 @@ Only consult when: - Security/compliance decisions - Changes that affect external systems or users`; -export const THOROUGH_TIER_PLAN_MODE = `## Advanced Plan Mode +export const THOROUGH_TIER_PLAN_MODE = `## Advanced Plan Mode with Approval Gate -For complex tasks, use structured planning with approval: +For complex tasks, you MUST use the plan_approval tool before executing file modifications. -### Plan Document Format +### CRITICAL: Use plan_approval Tool -\`\`\`markdown -# Implementation Plan: [Feature Name] +Even with full autonomy, you MUST: +1. Use plan_approval action="analyze_task" to check if approval is needed +2. If needed, create and submit a plan using plan_approval +3. WAIT for user approval before executing any file modifications +4. Only proceed after user says "yes", "proceed", "approve", or similar -## Executive Summary -[1-2 sentence overview] +### Plan Approval Tool Workflow -## Context Analysis -### Files Analyzed -- \`path/to/file.ts\`: [purpose and relevance] +\`\`\` +// 1. Analyze if plan approval is needed +plan_approval action="analyze_task" task_description="..." -### Current Architecture -[Brief description of existing patterns] +// 2. Create the plan +plan_approval action="create" title="Feature Name" summary="What we'll do" -### Dependencies -- [External dependency 1] -- [Internal module 1] +// 3. Add context (files analyzed, architecture) +plan_approval action="add_context" plan_id="" files_analyzed=[...] current_architecture="..." -## Implementation Strategy +// 4. Add each step +plan_approval action="add_step" plan_id="" step_title="Phase 1" step_description="..." files_affected=[...] risk_level="medium" -### Phase 1: [Name] -**Objective**: [Goal] -**Files affected**: [list] -**Changes**: -1. [Specific change] -2. [Specific change] +// 5. Add risks +plan_approval action="add_risk" plan_id="" risk_description="..." risk_impact="high" risk_mitigation="..." -### Phase 2: [Name] -... +// 6. Submit for approval +plan_approval action="submit" plan_id="" testing_strategy="..." rollback_plan="..." -## Risk Assessment -| Risk | Impact | Mitigation | -|------|--------|------------| -| [Risk 1] | [High/Med/Low] | [Strategy] | - -## Testing Strategy -- Unit tests: [approach] -- Integration tests: [approach] -- Manual verification: [steps] - -## Rollback Plan -[How to undo if needed] - ---- -**Awaiting approval to proceed with implementation.** +// STOP - Wait for user approval before any file modifications! \`\`\` -### Plan Approval Workflow +### After Approval -1. Generate comprehensive plan -2. Present to user with "Awaiting approval" -3. User can: - - Approve: "proceed", "go ahead", "looks good" - - Modify: "change X to Y" - - Reject: "stop", "don't do this" -4. On approval, execute with full autonomy`; +Once user approves: +- Execute with full autonomy +- Use agent orchestration for parallel implementation +- Complete the entire feature end-to-end +- Handle errors and edge cases + +### When Plan Approval is Required + +ALWAYS use plan_approval for: +- Multi-file refactoring +- New feature implementation +- Architectural changes +- Security-related changes +- Database modifications +- Any task affecting 3+ files + +You MAY skip for: +- Single file fixes with obvious solutions +- Adding comments or documentation +- Simple configuration changes`; export const THOROUGH_TIER_AGENTS = `## Agent Orchestration System diff --git a/src/services/chat-tui/message-handler.ts b/src/services/chat-tui/message-handler.ts index ca94e80..b632f43 100644 --- a/src/services/chat-tui/message-handler.ts +++ b/src/services/chat-tui/message-handler.ts @@ -69,6 +69,15 @@ import { executeDetectedCommand, } from "@services/command-detection"; import { detectSkillCommand, executeSkill } from "@services/skill-service"; +import { + getActivePlans, + isApprovalMessage, + isRejectionMessage, + approvePlan, + rejectPlan, + startPlanExecution, + formatPlanForDisplay, +} from "@services/plan-mode/plan-service"; // Track last response for feedback learning let lastResponseContext: { @@ -408,6 +417,56 @@ export const handleMessage = async ( // Check for feedback on previous response await checkUserFeedback(message, callbacks); + // Check for pending plan approvals + const pendingPlans = getActivePlans().filter((p) => p.status === "pending"); + if (pendingPlans.length > 0) { + const plan = pendingPlans[0]; + + // Check if this is an approval message + if (isApprovalMessage(message)) { + approvePlan(plan.id, message); + startPlanExecution(plan.id); + callbacks.onLog("system", `Plan "${plan.title}" approved. Proceeding with implementation.`); + addDebugLog("state", `Plan ${plan.id} approved by user`); + + // Continue with agent execution - the agent will see the approved status + // and proceed with implementation + state.messages.push({ + role: "user", + content: `The user approved the plan. Proceed with the implementation of plan "${plan.title}".`, + }); + // Fall through to normal agent processing + } else if (isRejectionMessage(message)) { + rejectPlan(plan.id, message); + callbacks.onLog("system", `Plan "${plan.title}" rejected. Please provide feedback or a new approach.`); + addDebugLog("state", `Plan ${plan.id} rejected by user`); + + // Add rejection to messages so agent can respond + state.messages.push({ + role: "user", + content: `The user rejected the plan with feedback: "${message}". Please revise the approach.`, + }); + // Fall through to normal agent processing to get revised plan + } else { + // Neither approval nor rejection - treat as feedback/modification request + callbacks.onLog("system", `Plan "${plan.title}" awaiting approval. Reply 'yes' to approve or 'no' to reject.`); + + // Show the plan again with the feedback + const planDisplay = formatPlanForDisplay(plan); + appStore.addLog({ + type: "system", + content: planDisplay, + }); + + // Add user's feedback to messages + state.messages.push({ + role: "user", + content: `The user provided feedback on the pending plan: "${message}". Please address this feedback and update the plan.`, + }); + // Fall through to normal agent processing + } + } + // Check for skill commands (e.g., /review, /commit) const skillMatch = await detectSkillCommand(message); if (skillMatch) { diff --git a/src/tools/index.ts b/src/tools/index.ts index e5d46a2..bbd4afb 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -14,6 +14,7 @@ import { multiEditTool } from "@tools/multi-edit/execute"; import { lspTool } from "@tools/lsp"; import { applyPatchTool } from "@tools/apply-patch"; import { taskAgentTool } from "@tools/task-agent/execute"; +import { planApprovalTool } from "@tools/plan-approval/execute"; import { isMCPTool, executeMCPTool, @@ -42,6 +43,7 @@ export const tools: ToolDefinition[] = [ lspTool, applyPatchTool, taskAgentTool, + planApprovalTool, ]; // Tools that are read-only (allowed in chat mode) diff --git a/src/tui-solid/components/modals/plan-approval-modal.tsx b/src/tui-solid/components/modals/plan-approval-modal.tsx new file mode 100644 index 0000000..2c49dcc --- /dev/null +++ b/src/tui-solid/components/modals/plan-approval-modal.tsx @@ -0,0 +1,268 @@ +import { createSignal, createMemo, For, Show } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import type { ImplementationPlan, PlanStep } from "@/types/plan-mode"; + +/** + * Plan approval request for the TUI + */ +export interface PlanApprovalRequest { + plan: ImplementationPlan; + resolve?: (response: { approved: boolean; message?: string }) => void; +} + +interface PlanApprovalModalProps { + request: PlanApprovalRequest; + onRespond?: (approved: boolean, message?: string) => void; + isActive?: boolean; +} + +interface ApprovalOption { + key: string; + label: string; + approved: boolean; + message?: string; +} + +const APPROVAL_OPTIONS: ApprovalOption[] = [ + { key: "y", label: "Approve - proceed with implementation", approved: true }, + { key: "e", label: "Edit - modify plan before execution", approved: false, message: "edit" }, + { key: "n", label: "Reject - cancel this plan", approved: false }, +]; + +export function PlanApprovalModal(props: PlanApprovalModalProps) { + const theme = useTheme(); + const [selectedIndex, setSelectedIndex] = createSignal(0); + const [scrollOffset, setScrollOffset] = createSignal(0); + const isActive = () => props.isActive ?? true; + + const MAX_VISIBLE_STEPS = 5; + + const plan = () => props.request.plan; + + const visibleSteps = createMemo(() => { + const steps = plan().steps; + const offset = scrollOffset(); + return steps.slice(offset, offset + MAX_VISIBLE_STEPS); + }); + + const canScrollUp = () => scrollOffset() > 0; + const canScrollDown = () => scrollOffset() + MAX_VISIBLE_STEPS < plan().steps.length; + + const handleResponse = (approved: boolean, message?: string): void => { + if (props.request.resolve) { + props.request.resolve({ approved, message }); + } + props.onRespond?.(approved, message); + }; + + const getRiskIcon = (risk: PlanStep["riskLevel"]): string => { + const icons: Record = { + high: "\u26A0\uFE0F", + medium: "\u26A1", + low: "\u2713", + }; + return icons[risk]; + }; + + const getRiskColor = (risk: PlanStep["riskLevel"]): string => { + const colors: Record = { + high: theme.colors.error, + medium: theme.colors.warning, + low: theme.colors.success, + }; + return colors[risk]; + }; + + useKeyboard((evt) => { + if (!isActive()) return; + + evt.stopPropagation(); + + if (evt.name === "up") { + if (evt.shift) { + // Scroll plan view + if (canScrollUp()) { + setScrollOffset((prev) => prev - 1); + } + } else { + // Navigate options + setSelectedIndex((prev) => + prev > 0 ? prev - 1 : APPROVAL_OPTIONS.length - 1, + ); + } + evt.preventDefault(); + return; + } + + if (evt.name === "down") { + if (evt.shift) { + // Scroll plan view + if (canScrollDown()) { + setScrollOffset((prev) => prev + 1); + } + } else { + // Navigate options + setSelectedIndex((prev) => + prev < APPROVAL_OPTIONS.length - 1 ? prev + 1 : 0, + ); + } + evt.preventDefault(); + return; + } + + if (evt.name === "return") { + const option = APPROVAL_OPTIONS[selectedIndex()]; + handleResponse(option.approved, option.message); + evt.preventDefault(); + return; + } + + if (evt.name === "escape") { + handleResponse(false, "cancelled"); + evt.preventDefault(); + return; + } + + // Handle shortcut keys + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { + const charLower = evt.name.toLowerCase(); + const option = APPROVAL_OPTIONS.find((o) => o.key === charLower); + if (option) { + handleResponse(option.approved, option.message); + evt.preventDefault(); + } + } + }); + + return ( + + {/* Header */} + + + Plan Approval Required + + + + {/* Title and Summary */} + + + {plan().title} + + {plan().summary} + + + {/* Estimated Changes */} + + + +{plan().estimatedChanges.filesCreated} create{" "} + + + ~{plan().estimatedChanges.filesModified} modify{" "} + + + -{plan().estimatedChanges.filesDeleted} delete + + + + {/* Steps */} + + + Implementation Steps ({plan().steps.length} total): + + + {"\u25B2"} more above... + + + {(step, index) => ( + + + {getRiskIcon(step.riskLevel)}{" "} + + + {scrollOffset() + index() + 1}.{" "} + + + {step.title.substring(0, 50)} + {step.title.length > 50 ? "..." : ""} + + + )} + + + {"\u25BC"} more below... + + + + {/* Risks */} + 0}> + + + Risks ({plan().risks.length}): + + + {(risk) => ( + + - + + {risk.description.substring(0, 60)} + + + )} + + 2}> + + {" "} + ... and {plan().risks.length - 2} more + + + + + + {/* Options */} + + + {(option, index) => { + const isSelected = () => index() === selectedIndex(); + const keyColor = () => + option.approved ? theme.colors.success : theme.colors.error; + return ( + + + {isSelected() ? "> " : " "} + + [{option.key}] + + {option.label} + + + ); + }} + + + + {/* Footer */} + + + {"\u2191\u2193"} options | Shift+{"\u2191\u2193"} scroll steps | Enter select | y/e/n shortcut + + + + ); +}