README is now self-contained for a human reader landing on the repo cold: project value-prop, status, quick-orientation reading order, directory layout, build/runtime deps, run + config invocation, and a pointer to CLAUDE.md for contribution norms. CLAUDE.md is rewritten as the substrate a fresh Claude session needs to pick up Phase 0→1 implementation without prior conversation context: - Reading order (PHASE0.md → README → config.lua) - Phase-loop discipline (8+1 with loopbacks) - Eight invariants from PHASE0.md called out as non-negotiable without manifest amendment - Bottom-up implementation order for Phase 0 (libc → readline → curl → context → executor → router → broker → renderer → repl → main) - Testing approach without a test framework - Open question on JSON library (dkjson recommended; needs §3 amendment) - Ambiguity handling pattern (ask vs log-in-§13 vs stop-and-ask) - Commit style + Co-Authored-By trailer template - Model-class caveat: small Q4 coder models have output variance, validate before exec, confirm_cmd defaults exist for this reason - Push credential note for sessions without ssh-keys-on-Gitea Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.8 KiB
CLAUDE.md — aish project handoff
You are continuing work on aish, an AI-augmented conversational shell implemented in LuaJIT. This file is auto-loaded when a Claude session opens a clone of this repo. Read it once at session start.
1. Read these first, in order
docs/PHASE0.md— locked substrate. Every architectural decision is here. Do not contradict it without amending it.README.md— human-facing project summary and orientation.config.lua— the runtime model registry. Endpoints reflect a specific LAN; the user may have already adapted yours.
If a question seems open after reading those three, ask the user a single focused question rather than guessing. Cost of asking is one turn; cost of building on a wrong assumption is half a phase.
2. Where you are in the phase loop
aish follows an 8(+1) phase loop:
0 substrate → 1 formulate → 2 analyze → 3 baseline → 4 plan
→ 5 review → 6 implement → 7 verify → 8 memory-update
(+1 reflect)
Loopbacks:
3 → 1if baseline data invalidates the formulation7 → 4if verification fails — fix the plan, not the substrateany → 0if a substrate fact turns out to be wrong
Don't skip phases. Phase 0 is done (the manifest is locked); your starting position depends on what's already in the tree.
Check the state by reading docs/ and git log --oneline. If only
PHASE0.md exists in docs/ and every module raises NotImplemented, you
are at Phase 0 → Phase 1 transition: the substrate is locked, no module
is implemented, your job is to write the Phase 0 implementation per the
manifest.
If you find phase docs docs/PHASE1.md, docs/PHASE2.md etc., follow
their state.
3. Source-of-truth invariants — do not violate without amending PHASE0.md
These are not stylistic preferences; they are decisions that downstream phases depend on:
| Invariant | Where it's locked |
|---|---|
| LuaJIT 2.x only — no PUC-Rio Lua-only constructs | §3 |
FFI only — no compiled C extensions, no luarocks packages |
§3 |
| Module file names in §4 are stable across phases | §4 |
/v1/chat/completions is the broker contract |
§6 |
CMD: (exact prefix, single space) is the command-extraction marker |
§6 |
Config is plain Lua loaded with dofile, not JSON/YAML |
§3, §10 |
cd is intercepted via libc chdir, not delegated to popen |
§7 |
| Phase 0 has no disk I/O for history (memory only) | §8 |
If a Phase N implementation needs to break one of these, amend PHASE0.md in the same commit and call out the change in the commit message. Don't silently diverge.
4. Implementation order for Phase 0
Bottom-up beats top-down for this codebase. Suggested ordering:
ffi/libc.lua—chdir,strerrorwork; verifiable in isolation.ffi/readline.lua—readline(),add_history(),free()work; test with a tiny REPL stub before wiringrepl.lua.ffi/curl.lua— easy interface, blocking POST with response capture into a Lua string. Test against any local llama-server with a curl one-liner side-by-side.context.lua— pure data structure, trivial to unit-test.executor.lua—popenwrapper,cdinterception (uses libc.chdir),CMD:line extraction.router.lua— pure function; classify(line, config) → (kind, payload).broker.lua— usesffi/curl.lua+ JSON encode/decode. You will need a JSON library — see §6 below.renderer.lua— output formatting; trivial.repl.lua— wires everything via the dispatch table.main.lua— already mostly there; finalize oncerepl.runexists.
Don't write all ten in one commit. One commit per module, build the
chain by passing through; each commit should leave the tree in a state
where luajit main.lua either runs further than the previous commit or
fails with the next NotImplemented.
5. Testing approach
There is no test framework dependency by design. Testing is per-module
ad-hoc with luajit -e 'local m = require("module"); ...' from the repo
root, or a smoke luajit main.lua after each module lands.
For broker testing without burning model time: any of the local
llama-servers in config.lua will respond to a hand-crafted POST. Use
curl -sS http://dirac.fritz.box:8081/v1/chat/completions -d '{...}' to
generate a known-good reference, then compare your FFI output.
6. JSON encode/decode — undecided
The manifest doesn't pick a JSON library. LuaJIT 2.x has no built-in JSON. Options:
dkjson— pure Lua, single file, vendor it undervendor/dkjson.lua. Slow but no dependency.cjson— fast, but it's a C extension. Violates §3 (no compiled extensions). Skip unless you amend the manifest.- Hand-rolled minimal encoder — viable since the broker payload shape is small and well-defined; ~50 LOC.
Recommend dkjson vendored. Decide and add a note to PHASE0.md §3 in the same commit.
7. When you hit ambiguity
The user (mfritsche) prefers being asked over being assumed-about. The phrase "Ask readily — prefer quick question over 3+ guessing attempts" applies. Concrete pattern:
- If the question affects only the current commit: ask.
- If it affects a future phase: log it in
docs/PHASE0.md§13 (Open Questions) with target phase, keep working. - If it affects multiple existing modules: stop, ask before refactoring.
Don't suggest pausing the session. Don't suggest "let's pick this up tomorrow". Don't pre-emptively defer work the user hasn't asked you to defer. The user controls pace.
8. Commit style
Imperative subject, file-scoped where possible. Examples:
executor: implement io.popen wrapper with stderr mergeffi/curl: blocking POST with header list and response bufferrepl: wire router → executor → broker dispatchphase0 amendment: vendor dkjson under vendor/
Body explains the why if non-obvious. Reference PHASE0.md sections by number when relevant.
Co-Authored-By trailer on Claude-authored commits:
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(or whichever model you actually are; check before substituting.)
9. What aish does NOT do — out of scope, all phases
These are listed in PHASE0.md §12. Briefly:
- Does not manage llama.cpp lifecycle (assumed externally running)
- Does not implement model inference
- Is not a multiplexer (no tmux semantics)
- Is not a sandbox (no namespaces, no seccomp)
If you find yourself implementing any of those, stop — that's a different project.
10. The model serving aish (typically)
aish targets local llama.cpp endpoints. The committed config.lua
references the user's home network (dirac.fritz.box,
hossenfelder.fritz.box). The user's other Claude sessions have
established that small Q4_K_M models (Qwen2.5-Coder-7B and similar) have
variance issues on code generation tasks — the same prompt can yield
both correct and broken code across rolls. Practical implications for
aish:
- Default
temperatureto0.2or lower for code tasks. - Don't assume the model output is correct — validate before exec.
- The
confirm_cmd = truedefault inconfig.shellis there for this reason; don't disable it without a deliberate UX change.
This isn't paranoia, it's a measured property of the local model class the user runs.
11. Identity
If this is a session running on a fleet host other than the user's
primary Claude window, your Gitea identity is claude-<hostname>. For
aish (a non-PR-flow repo), commits as that identity are fine without a PR.
If you need to push and lack credentials, use a Gitea Personal Access
Token in the URL: git push https://<user>:<token>@git.reauktion.de/marfrit/aish.git main.
The user has marfrit-level credentials available via a separate channel if needed for repo-admin operations.