Files
gnome-jarvis/vendor/agent-harness/agent/plan-mode.js
T
2026-09-11 13:41:22 -04:00

177 lines
5.1 KiB
JavaScript

/**
* 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,
};