marfrit 76a8f97009 repl: cloud preplanner + local executor split for Norris (closes #89)
Phase 10 C4 — the orchestration commit. Splits Norris autonomous
mode into a one-shot cloud preplan + per-step local executor flow,
with graceful fall-back to single-model Norris when preplan is
disabled or fails.

run_norris additions (in order):

  1. R4 fix: clear ctx.norris_active/_goal/_tasks at the TOP so a
     prior crashed Norris can't leak stale state into the new launch.

  2. Preplan block (gated on cfg.norris.preplanner):
     - Look up the preplanner preset in cfg.models; warn + skip if
       absent.
     - Build a system prompt asking for TASK: <imperative> lines
       (R1: %d via string.format — gsub("N", ...) would corrupt
       "No prose / commentary / numbering" to "16o prose").
     - Scrub messages per the preplan model's redact policy; run
       broker.chat (non-streaming, per Q-PP2) with category
       "norris-preplan"; R7: respect pre_cfg.timeout_ms.
     - On success: rehydrate; record usage via _record_usage;
       extract_task_lines; cap to tasks_max; populate
       ctx.norris_tasks = { current = 1, list = parsed }.
     - On ANY failure (transport err / empty list / bogus preset):
       status log + leave ctx.norris_tasks nil → single-model
       fall-back. R3 design: NOT routed via call_broker; a fallback
       retry would silently swap planning models which is worse
       than a clean hard-fail.

  3. Executor cfg resolution (independent of preplan per Q-PP1):
     cfg.norris.executor names a preset → executor_cfg = that cfg.
     Unset / missing preset → executor_cfg = active_cfg (existing
     :model-selection behavior).

  4. Loop body: pass executor_cfg (not active_cfg) to
     safety.norris_step. After each "continue" result, advance
     ctx.norris_tasks.current. When current > #list, exit with
     synthesized status "tasks_complete" + reason "all N preplanned
     tasks executed".

  5. Exit cleanup: clear ctx.norris_tasks alongside the existing
     norris_active/_goal clears so a re-launch starts fresh.

renderer.norris_end gains "tasks_complete" as a non-error status
(cyan, same as "done"). Distinct from "done" (executor said
GOAL: complete) — executor exhausted the plan but didn't confirm
goal, which is a clean exit, not an error.

E2E verified (preplanner=fast, executor=fast on hossenfelder:8082):

  :norris print the date and the current uptime
  → preplanned 2 tasks via fast
  → ─ step 1/3 ─ Print the current date.
  → CMD: date → Sun May 17 ...
  → ─ step 2/3 ─ Print the current uptime.
  → CMD: uptime → ... up 1 day ...
  → NORRIS TASKS COMPLETE: all 2 preplanned tasks executed

  :cost detail correctly shows two rows for the same model:
    norris-preplan  1 calls,  95 /  12 tokens
    norris          1 calls, 364 /   9 tokens

Fall-back verified:
  cfg.norris.preplanner = "doesnotexist" →
    "[aish] preplanner 'doesnotexist' is not in cfg.models;
     running single-model" → Norris runs as Phase 6.

No-preplan path verified (no cfg.norris block):
  Norris runs exactly as Phase 6, no behavior change.

Regression: 87/87 safety, 31/31 router_model, repl loads.

Closes #89.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 08:21:25 +00:00

aish

aish — AI-augmented conversational shell.

A single REPL that interleaves shell command execution and language-model conversation, backed by a llama.cpp HTTP broker. Implementation is LuaJIT 2.x with FFI bindings to libcurl, GNU readline, and libc — no C extensions, no build step, one source tree.

Why

Three flows that currently live in three windows fold into one:

  1. "Run this command and show me the output" — fast feedback loop, no copy-paste between terminal and chat.
  2. "Explain or write code based on the output we just looked at" — exec output is automatically injected into the model's context.
  3. "Plan and execute a multi-step task with confirmation gates" — landing in Phase 3 as Chuck Norris autonomous mode.

aish is not a wrapper around bash. It's a first-class interactive environment where the shell is one of several execution channels.

Status

Component State
Repository skeleton in this commit
Phase 0 manifest docs/PHASE0.md — locked
Phase 0 implementation 🔜 next session
Phase 1+ 📋 enumerated in PHASE0.md §11

Every module file currently raises not implemented (Phase 0 pending) when called. luajit main.lua fails loudly at the first un-implemented function, never silently.

Quick orientation

Read this If you want to know
docs/PHASE0.md §12 What aish is and what Phase 0 ships
docs/PHASE0.md §3 Technology decisions (LuaJIT, FFI, readline, libcurl, llama.cpp)
docs/PHASE0.md §4 Directory layout — these file names are stable across all phases
docs/PHASE0.md §5 How input is dispatched (meta / shell / AI)
docs/PHASE0.md §6 Broker contract: /v1/chat/completions, CMD: extraction
docs/PHASE0.md §10 Config schema and resolution order
docs/PHASE0.md §11 Phase sequence (what lands when)
docs/PHASE0.md §13 Open questions, tracked per phase
CLAUDE.md Project conventions for AI-assisted contributors

Directory layout

aish/
├── main.lua              # entry point
├── repl.lua              # readline loop, dispatch, prompt
├── broker.lua            # llama.cpp HTTP client
├── router.lua            # input classifier (meta/shell/AI)
├── executor.lua          # command exec + CMD: extraction
├── context.lua           # in-memory turn history
├── history.lua           # disk persistence (Phase 1+)
├── safety.lua            # destructive-op gate (Phase 3+)
├── renderer.lua          # output formatting
├── config.lua            # default model registry + preferences
├── ffi/
│   ├── curl.lua          # libcurl easy interface
│   ├── readline.lua      # GNU readline
│   ├── pty.lua           # forkpty (Phase 1+)
│   └── libc.lua          # chdir, errno, strerror
└── docs/
    └── PHASE0.md         # locked substrate

Build / runtime dependencies

System packages (Debian / ALARM / Arch names):

  • luajit (>= 2.0)
  • libcurl4 / libcurl-openssl-3 runtime
  • libreadline8 runtime
  • libc6 runtime (always present)

No compilation, no luarocks, no make. Just luajit main.lua.

Running

Once Phase 0 ships:

luajit main.lua                          # uses ~/.config/aish/config.lua
luajit main.lua --config ./config.lua    # explicit config path
AISH_CONFIG=/path/to/config.lua luajit main.lua

Config resolution order is documented in docs/PHASE0.md §10.

Configuration

config.lua is a Lua file returning a single table. The committed config.lua in this repo is both the canonical example and the development-fallback config (lowest precedence). Copy it to ~/.config/aish/config.lua and edit endpoints to your local llama.cpp servers, or point AISH_CONFIG at your own.

The default endpoints assume mfritsche's home network:

  • fastdirac.fritz.box:8081 (Qwen2.5-Coder-7B q4 8k ctx)
  • deepdirac.fritz.box:8080 (Qwen2.5-Coder-7B q4 32k ctx)
  • cloudhossenfelder.fritz.box:8082 (forwards to OpenRouter)

Replace these with your own llama.cpp endpoints if you're not on that LAN.

License

Not yet selected. Default-private until decided.

Project conventions

See CLAUDE.md for contribution conventions, commit style, and the phase-loop discipline this project follows.

S
Description
AI-augmented conversational shell — LuaJIT REPL with llama.cpp broker, shell executor, and routed AI inference.
Readme MIT 2.2 MiB
Languages
Lua 99.8%
Shell 0.2%