/** * Plan-mode state machine (Grok-shaped). No Bare imports — unit-testable on Node. * * States: inactive | pending | active | exitPending * While active, write/shell tools are blocked except the session plan file. */ const STATES = ['inactive', 'pending', 'active', 'exitPending']; const WRITE_TOOLS = ['write_file', 'search_replace', 'run_terminal_cmd']; const PLAN_FILE_NAMES = ['plan.md']; function create(snapshot) { const s = snapshot && typeof snapshot === 'object' ? snapshot : {}; let state = String(s.state || 'inactive'); if (STATES.indexOf(state) < 0) state = 'inactive'; if (state === 'pending') state = 'inactive'; if (state === 'exitPending') state = s.awaitingApproval ? 'active' : 'inactive'; return { state, reminderCount: Number(s.reminderCount) || 0, planPath: String(s.planPath || 'plan.md'), awaitingApproval: !!s.awaitingApproval, wasActive: !!s.wasActive, pendingExitReminder: !!s.pendingExitReminder, }; } function isActive(pm) { if (!pm) return false; if (pm === true) return true; return pm.state === 'active' || pm.state === 'exitPending'; } function enterPending(pm) { if (!pm) return false; if (pm.state === 'inactive') { pm.state = 'pending'; pm.pendingExitReminder = false; return true; } if (pm.state === 'exitPending') { pm.state = 'active'; pm.pendingExitReminder = false; return true; } return false; } function activate(pm) { if (!pm) return false; if (pm.state !== 'pending' && pm.state !== 'inactive') return false; pm.state = 'active'; pm.wasActive = true; pm.reminderCount = 0; pm.awaitingApproval = false; pm.pendingExitReminder = false; return true; } function requestExit(pm) { if (!pm || pm.state !== 'active') return false; pm.state = 'exitPending'; pm.awaitingApproval = true; return true; } function approveExit(pm) { if (!pm) return false; if (pm.state !== 'active' && pm.state !== 'exitPending') return false; pm.state = 'inactive'; pm.awaitingApproval = false; pm.reminderCount = 0; pm.pendingExitReminder = false; return true; } function rejectExit(pm) { if (!pm) return false; pm.state = 'active'; pm.awaitingApproval = false; return true; } function reminder(pm, opts) { opts = opts || {}; const path = (pm && pm.planPath) || 'plan.md'; const has = !!opts.hasContent; const full = has ? 'Plan mode is active. Do not make any edits or writes to the system.\n\n' + 'A plan file exists at ' + path + '. You can read it and make edits using write_file or search_replace.\n' + 'This is the only file you are allowed to edit.\n\n' + 'Your turn should only end with either ask_user_question to clarify requirements or exit_plan_mode to present your plan to the user.' : 'Plan mode is active. Do not make any edits or writes to the system.\n\n' + 'No plan written yet. Write your plan to ' + path + ' using write_file.\n' + 'This is the only file you are allowed to edit.\n\n' + 'Your turn should only end with either ask_user_question to clarify requirements or exit_plan_mode to present your plan to the user.'; const sparse = 'Plan mode is still active. Do not make any edits or writes to the system except for the plan file.'; const useFull = !pm || pm.reminderCount % 2 === 0; if (pm) pm.reminderCount += 1; return useFull ? full : sparse; } function exitReminder() { return 'You have exited plan mode. You can now make edits, run tools, and take actions. Implement the approved plan.'; } function isWriteTool(name) { return WRITE_TOOLS.indexOf(String(name || '')) >= 0; } function basename(p) { const s = String(p || '').replace(/\\/g, '/'); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(i + 1) : s; } function isPlanFilePath(filePath, planPath) { const want = basename(planPath || 'plan.md').toLowerCase(); const got = basename(filePath).toLowerCase(); if (!got) return false; if (got === want) return true; return PLAN_FILE_NAMES.indexOf(got) >= 0; } function gateWrite(pm, name, args) { if (!isActive(pm)) return null; if (!isWriteTool(name)) return null; if (name === 'run_terminal_cmd') { return ( 'Plan mode is active. Shell is blocked. Write only ' + ((pm && pm.planPath) || 'plan.md') + ', or call exit_plan_mode.' ); } const file = args && (args.path || args.file); if (isPlanFilePath(file, pm && pm.planPath)) return null; return ( 'Rejected: file edits are not allowed in plan mode - the only editable file is the plan file (' + ((pm && pm.planPath) || 'plan.md') + ').' ); } function snapshot(pm) { if (!pm) return { state: 'inactive', reminderCount: 0, planPath: 'plan.md', awaitingApproval: false }; return { state: pm.state, reminderCount: pm.reminderCount, planPath: pm.planPath || 'plan.md', awaitingApproval: !!pm.awaitingApproval, wasActive: !!pm.wasActive, pendingExitReminder: !!pm.pendingExitReminder, }; } module.exports = { STATES, WRITE_TOOLS, create, isActive, enterPending, activate, requestExit, approveExit, rejectExit, reminder, exitReminder, isWriteTool, isPlanFilePath, gateWrite, snapshot, };