Commit Graph

4 Commits

Author SHA1 Message Date
marfrit ee4d7f86d6 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>
2026-05-10 19:08:27 +00:00
marfrit abc993aa49 review followup: empty-input guards, ~/ symmetry, CMD: filter
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>
2026-05-10 17:41:35 +00:00
marfrit 5fb4023c55 executor: io.popen wrapper, cd interception, CMD: extraction
Phase 0 implementation per PHASE0.md §6, §7.

  M.exec(cmd)              -> (output, exit_code)
  M.maybe_chdir(cmd)       -> nil | true | false, errmsg
  M.extract_cmd_lines(text)-> { "ls -la", "echo hi", ... }

Two non-obvious bits:

1. LuaJIT 2.1's io.popen():close() follows the Lua 5.1 ABI and returns
   only `true` — no child exit status. The §7 manifest sketch assumes
   Lua 5.2's three-return form, which doesn't apply here. Recover the
   exit code by appending `; echo __AISH_EXIT_<tag>__$?` after the
   command and parsing the sentinel-prefixed integer back out. Phase 1
   replaces this with waitpid via libc FFI when PTY support lands.

2. `cd` interception is a §3 invariant: must not delegate to popen
   (popen forks; a child cd evaporates). maybe_chdir parses the line,
   ~ expands, calls libc.chdir, returns success/failure separate from
   "not a cd" (nil) so the caller can distinguish.

CMD: extraction is anchored at start-of-line per the §3 "exact prefix,
single space" invariant — leading whitespace before CMD: does not match.

Smoke covers: echo capture (code=0), failed ls (code!=0), `false`
(code=1), multi-line output preserved, all maybe_chdir branches
(non-cd / bare / explicit / ~ expansion / failure), CMD extraction
including the leading-whitespace-rejection case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:03:19 +00:00
claude-noether 4310207738 Phase 0: scaffold tree + manifest
- README, .gitignore, CLAUDE.md (project conventions)
- docs/PHASE0.md — full Phase 0 manifest (locked substrate)
- 10 root .lua modules + 4 ffi/ bindings, all stubs raising NotImplemented
  with module-scoped responsibilities matching the manifest
- config.lua wired to current dirac/hossenfelder endpoints (qwen-coder-7b
  snappy/32k + cloud via OpenRouter through hossenfelder)

File names match docs/PHASE0.md §4 exactly. Module bodies fill in across
later phases; the tree shape is locked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:16:07 +00:00