repl: background CMD&: with handle/poll (closes #8)
Builds, long-running network calls, and file watches no longer block
the turn. A new "CMD&: <cmd>" marker (analogue of CMD:) tells the REPL
to spawn the command in the background, return immediately, and poll
for completion between user inputs.
Process model: shell-wrapped to avoid needing fork()/execv() FFI.
nohup sh -c '(<cmd>) > <log> 2>&1; echo $? > <status>' </dev/null
>/dev/null 2>&1 & echo $!
The child is reparented to init; we hold only the PID and the path to
the .status sidecar. Completion is detected by the .status file
existing (the wrapper writes it as its last act). No waitpid needed —
the child isn't ours after the popen subshell exits.
Storage: <history.dir>/bg/<id>.log + <id>.status. The directory is
created lazily at startup (mkdir -p). Requires history.dir to be
configured; without it CMD&: emits an error status and the model
sees an "[bg failed to start]" exec-output note.
check_bg_done() runs at the top of each main-loop iteration alongside
check_every_due(). When a job is detected as exited, the REPL:
- emits a status line "[bg:<id> exited <code>, <bytes>, <secs>s wall] <cmd>"
- appends the same string to ctx as exec output, so the model sees
the completion on its next turn (natural follow-up: "ok the build
finished; let me check the log")
Meta surface:
:bg-spawn <cmd> start a bg job directly (no AI needed; also
useful for testing without depending on the
model emitting CMD&:)
:bg-list show running/done jobs (id, pid, state, runtime, cmd)
:bg-output <id> dump the log file to stdout
:bg-kill <id> SIGTERM (note: only delivers if the PID is
still the actual command — long-lived shells
may need pkill by name)
Scope (deliberately limited for v1):
- No callback-mode readline: bg completion detection is pre-prompt,
not mid-readline. If a build finishes while the user is typing,
notification comes when they hit Enter.
- Permission policy DSL (#9) does NOT apply to CMD&: — the
asynchronous gating model wasn't designed for the y/N flow.
Filed as follow-up if needed.
- Norris not extended: helpers.exec_cmd is still synchronous; the
planner doesn't dispatch bg jobs.
- Plan mode interaction: CMD&: in plan mode emits "PLAN: & <cmd>"
and a "[plan] would bg-run: <cmd>" exec-output note, no spawn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -127,11 +127,21 @@ end
|
||||
-- Extract `CMD: ` lines from an assistant response per the §6 broker contract.
|
||||
-- The "CMD: " prefix is a §3 substrate invariant: exact prefix, single space,
|
||||
-- start-of-line only. Leading whitespace before CMD: does NOT match.
|
||||
-- "CMD&: " lines are issue #8 background variants — extracted separately so
|
||||
-- repl.lua can route them to the bg spawner instead of the synchronous gate.
|
||||
function M.extract_cmd_lines(text)
|
||||
local cmds = {}
|
||||
for line in (text or ""):gmatch("[^\n]+") do
|
||||
local cmd = line:match("^CMD: (.*)$")
|
||||
-- Skip whitespace-only / empty bodies; "CMD: " alone is degenerate.
|
||||
if cmd and cmd:match("%S") then cmds[#cmds + 1] = cmd end
|
||||
end
|
||||
return cmds
|
||||
end
|
||||
|
||||
function M.extract_cmd_bg_lines(text)
|
||||
local cmds = {}
|
||||
for line in (text or ""):gmatch("[^\n]+") do
|
||||
local cmd = line:match("^CMD&: (.*)$")
|
||||
if cmd and cmd:match("%S") then cmds[#cmds + 1] = cmd end
|
||||
end
|
||||
return cmds
|
||||
|
||||
Reference in New Issue
Block a user