Phase 3 commit #5 per docs/PHASE3.md §12. Wires safety.norris_step
(commit #4) into the REPL with the user-facing surface.
ffi/readline.lua extensions (A1 + R-C4):
- rl_insert_text + rl_redisplay added to ffi.cdef block; M.insert_text
and M.redisplay wrappers exposed.
- M.bind: removed `:free()` on previous callback. Now keeps every
bound callback pinned for process lifetime in `_pinned` list
(alongside `_bound[seq]` for current lookup). Avoids the
use-after-free window between unbind and rebind that R-C4 flagged.
Memory cost is bounded — one closure per key sequence binding.
context.lua Norris suffix (R-C3 / §8):
- to_messages() composes a dynamic NORRIS MODE block onto the
system prompt when ctx.norris_active is set. The block carries
ctx.norris_goal so eviction of the user's "[norris] goal:" turn
doesn't lose the anchor. Returns to plain system prompt when
Norris exits.
repl.lua Norris driver:
- prompt() now shows ⚡ marker when ctx.norris_active per PHASE0.md §9.
- \C-n bound to a real handler — inserts ":norris " at the cursor
(replaces Phase 1 status placeholder).
- run_norris(goal) function: sets norris_active + norris_goal,
appends a "[norris] <goal>" user turn, renders the banner, then
loops calling safety.norris_step with an injected helpers table
until a terminal status returns. Renders the closing banner.
- norris_halt(): the [N] proceed/skip/abort prompt called by
safety.norris_step via helpers.halt. Empty input → abort (safe).
- dispatch_tool(): factored from the Phase 2 ask_ai code so
safety.norris_step can call it.
- norris_exec(): factored exec path for autonomous mode (skips
the interactive run_shell cd-status renderer).
- :norris <goal> meta — launches autonomous mode
- :norris off meta — drops Norris flag (rare; usually 'abort')
- :safety patterns meta — lists active is_destructive rules
- :safety check <cmd> meta — probes a hypothetical command
End-to-end mock-driven test:
Submitted ":norris find files in /tmp" → banner → step 1 emits
tool_call (auto_approved per policy) → dispatched → frame rendered
→ step 2 emits "GOAL: complete" → sub-loop exits → DONE banner.
2 broker invocations, no stalls.
config.lua safety example block lands in commit #6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 readline binding wiring per PHASE1.md §7.
ffi/readline:
M.bind(seq, lua_fn) -> bool
Wraps lua_fn as a C callback (signature `int (int, int)` per
readline's rl_command_func_t) and registers it via
rl_bind_keyseq(seq, cb). Returns true on success (rl returns 0).
Trampolines are pinned in module-local state so they outlive the
bind call — readline retains the function pointer for the process
lifetime. Rebinding the same seq frees the previous trampoline.
Bound handlers are pcall-wrapped so a Lua error doesn't crash
readline's input loop.
repl:
Binds \C-n to a no-op that emits
"[aish] Norris mode not yet implemented (Phase 3)"
Verifies the mechanism end-to-end; Phase 3 (Norris autonomous mode)
replaces the body with the actual toggle.
Smoke covers bind / rebind-same-seq (exercises the :free path) /
bind-different-seq with no errors. Live keyboard verification waits
on user-test.
Phase 1's 8(+1) inner loop is now functionally through `implement`;
next inner phase is `verify` (review pass) followed by memory-update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 binding per PHASE0.md §9. M.readline(prompt) returns the line
as a Lua string (the C buffer is freed via libc free immediately after
ffi.string copies it) or nil on EOF. M.add_history skips empty lines.
Loader handles the case where libreadline-dev's unversioned
`libreadline.so` symlink isn't installed — falls through to
`readline.so.8` (current Debian/Arch ALARM) and `.so.7` (older)
before giving up. This trips on noether-the-LXD: only the runtime
package is present.
Smoke (stdin from heredoc, two lines + EOF):
p1> hello world -> "hello world"
p2> second line -> "second line"
p3> -> nil (EOF)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>