Workspace.run executes a single argv with NO shell (shlex.split + subprocess.run),
so shell operators are never operators — a && chain runs its FIRST program with the
rest of the line as literal arguments, and a heredoc blocks forever on stdin that no
shell will ever feed. One dogfood journal (tetris_glm/events/be46ea…jsonl) showed both
ends of that failure: a declared grep chain that verification passed vacuously (later
patterns became unopenable filenames; grep -q exit-0’d on the first match anyway)
and a declared heredoc that hung to timeout and fed a finalization spiral.
This module is the shared gate every model-authored command boundary applies BEFORE a
string reaches Workspace.run: && normalizes to a conjunction of single-argv segments
(matching the planner’s per-segment classification, §12/ADR-0044); every other operator
is a legible, model-correctable rejection (§10) — never a silent mangle.
Functions
argv_segments(command: str) -> tuple[list[str], str]
Split command on && into single-argv segments, rejecting other shell operators.
Args:
command: The model-supplied, shell-style command string.
Returns:
(segments, "") — each segment re-joined via shlex.join, argv-equivalent to
the original — or ([], reason) naming the unrunnable construct.