abc993aa49
Addresses three concerns + one nit from the Phase 0 review pass.
executor.lua:
- M.exec guards empty / whitespace-only cmd up front, returns
"(empty command)" / -1 instead of running the wrapper on nothing.
- On sentinel-parse failure with empty output (typical of shell
parse errors — the syntax error itself escapes to the popen
parent's stderr because 2>&1 is inside the unparsable subshell),
surface "(no output — possible shell parse error)" rather than
a silent empty frame.
- extract_cmd_lines now skips whitespace-only / empty bodies; a
bare `CMD: ` line in assistant output no longer turns into an
"execute ''? [y/N]" prompt.
- "what" comments cleaned in maybe_chdir.
router.lua:
- path_like now matches `~` and `~/foo` so `~/scripts/build.sh`
classifies as shell (was: ai). Restores symmetry with executor's
maybe_chdir, which already expands `~` on `cd`.
repl.lua:
- :exec and :ask trim args and renderer.status a usage line on
empty rather than running an empty cmd / sending an empty turn
to broker.
Regression: full prior smoke suite still passes — known_commands
shell paths, all maybe_chdir branches, CMD: extraction with non-empty
bodies, exec exit-code recovery, all router branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
1.8 KiB
Lua
67 lines
1.8 KiB
Lua
-- router.lua — task classifier: meta / shell / AI.
|
|
-- See docs/PHASE0.md §5.
|
|
--
|
|
-- Pure function. Takes (line, config). Returns (kind, payload).
|
|
-- kind : "meta" | "shell" | "ai"
|
|
-- payload: the (possibly stripped) line that the dispatcher should act on.
|
|
--
|
|
-- Empty / whitespace-only lines are returned as ("ai", ""); the repl loop
|
|
-- skips them before dispatching.
|
|
|
|
local M = {}
|
|
|
|
local function trim(s)
|
|
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
|
|
end
|
|
|
|
local function first_word(s)
|
|
return s:match("^(%S+)") or ""
|
|
end
|
|
|
|
local function known_commands_set(config)
|
|
local set = {}
|
|
local list = config and config.shell and config.shell.known_commands or {}
|
|
for _, c in ipairs(list) do set[c] = true end
|
|
return set
|
|
end
|
|
|
|
-- §5.1 path-like: ./foo, ../foo, /usr/bin/foo, ~/foo, bare ~. Quoted /
|
|
-- escaped paths are intentionally out of scope in Phase 0. ~ is included
|
|
-- for symmetry with executor.maybe_chdir, which expands ~ on `cd ~/foo`.
|
|
local function path_like(token)
|
|
return token == "~"
|
|
or token:sub(1, 1) == "/"
|
|
or token:sub(1, 2) == "./"
|
|
or token:sub(1, 2) == "~/"
|
|
or token:sub(1, 3) == "../"
|
|
end
|
|
|
|
function M.classify(line, config)
|
|
line = trim(line or "")
|
|
if line == "" then return "ai", "" end
|
|
|
|
-- meta: ":" prefix
|
|
if line:sub(1, 1) == ":" then
|
|
return "meta", line:sub(2)
|
|
end
|
|
|
|
-- shell explicit override: "$" prefix
|
|
if line:sub(1, 1) == "$" then
|
|
return "shell", trim(line:sub(2))
|
|
end
|
|
|
|
local first = first_word(line)
|
|
local known = known_commands_set(config)
|
|
|
|
-- known-command allowlist
|
|
if known[first] then return "shell", line end
|
|
|
|
-- path-like first token
|
|
if path_like(first) then return "shell", line end
|
|
|
|
-- everything else -> AI
|
|
return "ai", line
|
|
end
|
|
|
|
return M
|