diff --git a/repl.lua b/repl.lua index 0abb4ce..455f03e 100644 --- a/repl.lua +++ b/repl.lua @@ -35,6 +35,8 @@ Meta commands: :mcp disconnect drop an MCP session :norris launch Chuck Norris autonomous mode on :norris off exit Norris mode (rare — usually 'abort' at halt) + :plan toggle plan mode (CMD: lines printed, NOT executed) + :plan on / :plan off set plan mode explicitly :safety patterns list active destructive-op patterns :safety check probe is_destructive against without running :remember shortcut: :memory add fact @@ -60,6 +62,13 @@ function M.run(config) .. "' not found in config.models") end + -- Plan mode (issue #5): when true, CMD: lines are NOT executed; they + -- are echoed as "PLAN:" and fed back to the next-turn context as + -- would-have-run notes so the model can iterate without side effects. + -- Off by default; toggle with :plan / :plan on / :plan off. Orthogonal + -- to Norris mode (Norris has its own halt protocol). + local plan_mode = false + -- Phase 5: render the evicted turns into a compact transcript for -- the summarizer prompt. Same shape as :memory summarize uses. local function render_evicted(turns) @@ -358,6 +367,9 @@ function M.run(config) if ctx.norris_active then return ("[aish:%s \xE2\x9A\xA1]> "):format(active_name) end + if plan_mode then + return ("[aish:%s plan]> "):format(active_name) + end return ("[aish:%s]> "):format(active_name) end @@ -581,14 +593,22 @@ function M.run(config) -- CMD: extraction on the final pure-text response only. for _, cmd in ipairs(executor.extract_cmd_lines(final_resp)) do - local doit - if config.shell and config.shell.confirm_cmd then - local ans = rl.readline(("execute '%s'? [y/N] "):format(cmd)) or "" - doit = (ans:lower():sub(1, 1) == "y") + if plan_mode then + -- Issue #5: print PLAN: and feed back as a would-have-run + -- note. Same context flow as a real exec output so the + -- model can iterate on the plan turn by turn. + renderer.status(("PLAN: %s"):format(cmd)) + ctx:append_exec_output(("[plan] would run: %s"):format(cmd)) else - doit = true + local doit + if config.shell and config.shell.confirm_cmd then + local ans = rl.readline(("execute '%s'? [y/N] "):format(cmd)) or "" + doit = (ans:lower():sub(1, 1) == "y") + else + doit = true + end + if doit then run_shell(cmd) end end - if doit then run_shell(cmd) end end end @@ -725,6 +745,19 @@ function M.run(config) active_name, active_cfg = name, config.models[name] renderer.status("model -> " .. name) end, + plan = function(args) + local sub = (args:match("^%s*(%S*)") or ""):lower() + if sub == "" then + plan_mode = not plan_mode + elseif sub == "on" then + plan_mode = true + elseif sub == "off" then + plan_mode = false + else + renderer.status("usage: :plan [on|off]"); return + end + renderer.status("plan mode " .. (plan_mode and "on" or "off")) + end, models = function() renderer.status(("models (active: %s):"):format(active_name)) for name, cfg in pairs(config.models) do