executor: swap popen+sentinel for pty.spawn (Phase 1)

Replaces the Phase 0 io.popen + sentinel-echo exit-code recovery with
forkpty + waitpid via ffi/pty. The §7 amendment paragraph on PHASE0.md
is rewritten to point at PHASE1.md §5 — the workaround is gone, not
just renamed.

User-visible behavioral changes:
  - Interactive commands (vim, less, htop, top) now work via $cmd /
    :exec / known-command shell paths because the child has a real
    PTY for line discipline.
  - Exit codes are accurate: `false` -> 1, `exit 7` -> 7, signal kill
    -> 128+N (bash convention), shell parse error -> sh's 2.
  - Broken-shell-syntax cmd now shows the actual sh diagnostic
    (e.g. "Syntax error: end of file unexpected") instead of Phase 0's
    "(no output — possible shell parse error)" guess.
  - Output normalization: PTY emits CR LF; executor collapses \r\n
    -> \n to keep the Phase 0 contract ("output uses \n separators").

Code path:
  pty.spawn(cmd) -> drain master_fd until EOF
                 -> wait() returns ("exit", N) | ("signal", N) | ...
                 -> exit_code mapped: exit -> N, signal -> 128+N, else -1

Phase 0 invariants intact: `cd` interception unchanged (still libc.chdir
per §3 + §7), `CMD: ` extraction unchanged.

PHASE0.md §7: the "LuaJIT 2.1 popen-close caveat" paragraph is rewritten
to "Superseded by Phase 1" — points at PHASE1.md §5 for the live model.
The illustrative sketch is left in place as historical context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 19:08:27 +00:00
parent 10d2fc5ac1
commit ee4d7f86d6
2 changed files with 41 additions and 28 deletions
+8 -7
View File
@@ -171,13 +171,14 @@ local function exec(cmd)
end
```
**LuaJIT 2.1 popen-close caveat.** The sketch above assumes Lua 5.2's
three-return `io.popen():close()` shape. LuaJIT 2.1 follows the Lua 5.1
ABI and returns just `true` — no exit status. The Phase 0 implementation
recovers the exit code by appending a sentinel echo to the wrapped
command (`(cmd) 2>&1; echo __AISH_EXIT_<tag>__$?`) and parsing it back
out. Phase 1's PTY work replaces this with `waitpid` via libc FFI; the
sketch becomes accurate at that point.
**Superseded by Phase 1.** The §7 sketch was never quite accurate on
LuaJIT 2.1 (which follows the Lua 5.1 ABI for `io.popen():close()` and
returns only `true` — no exit status). The Phase 0 implementation worked
around this with a sentinel-echo wrapper (`(cmd) 2>&1; echo
__AISH_EXIT_<tag>__$?`) and parsed the status back out of stdout. Phase 1
retired the workaround entirely: `executor.lua` now spawns the child via
`forkpty` and recovers exit status via `waitpid(WEXITSTATUS)`. See
docs/PHASE1.md §5 for the current PTY model.
Output is captured and:
1. Printed to the terminal