12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Current state
Boxmaker is a sovereign personal agent harness written in Rust. Work proceeds one milestone at a
time (M0 to M7, table in docs/milestones.md). M0 (measurements), M1 (workspace, proto, gate,
Decision), M2a (the inference path: inferproxy, loopd's HTTP, SSE and llama client, the
startup self-test) and M2b (sessions, the turn loop, loopd serve, bxctl chat) are done. M3 is split:
M3a (brokerd's decision path, spec docs/specs/2026-09-18-m3a-decision-path.md) is done and
merged, reviewed in docs/implementer-log.md; its open findings (17, 18, 20) are listed there; 14 was
closed by M3b task 02. M3b (the container runner and the four tools, docs/specs/2026-09-22-m3b-runner.md) is done and
merged: tool containers run from the image deploy/tools-image.nix builds, checked on straylight.
M4 is split: M4a (gatewayd, conversations over Mattermost, spec
docs/specs/2026-09-23-m4a-gateway.md) is done and merged, reviewed in docs/implementer-log.md
and checked end to end against the owner's Mattermost; no finding is open. M4b (approvals over
Mattermost, spec docs/specs/2026-09-24-m4b-approvals.md, 18 tasks) is done and merged, reviewed
in docs/implementer-log.md and checked end to end against the owner's Mattermost, including an
approval asked by a non-approver; no finding is open. M5 (memory, then the scheduler) is split into
M5a, M5b and M5c by P17 to P21 (applied 2026-09-24). M5a (local memory, memoryd, spec
docs/specs/2026-09-24-m5a-local-memory.md, 22 tasks) is done and merged, reviewed in
docs/implementer-log.md and checked on straylight against the owner's Mattermost; the device
check found that a recalled tainted note left a session trusted, fixed by task 21. P24 split the
rest of M5. M5b (hardening: the sanitizer, tag neutralisation in loopd, images and untrusted
links as code in gatewayd, the invalid-arguments reason, bxcore::secrets, one instance per
daemon; spec docs/specs/2026-09-26-m5b-hardening.md, 14 tasks) is done and merged, reviewed in
docs/implementer-log.md and checked on straylight against the owner's Mattermost; the device
check found that the self-test overran inferproxy's burst (fixed in review). M5c is Honcho,
through the owner's roncho (v1.0.0 on crates.io; docs/specs/2026-09-25-roncho-requirements.md
and docs/specs/2026-09-27-m5c-honcho.md). M5c-1 (the write path) is done and merged, reviewed in
docs/implementer-log.md. M5c-2 (the read path: recall over the fleet and ask_memory, spec
sections 3, 8, 9, 10 and 13) is done, reviewed in docs/implementer-log.md and checked on
straylight against rift; no finding is open. M5d (scheduler and compaction, spec
docs/specs/2026-10-03-m5d-scheduler.md) is split: M5d-1 (epochs and compaction) is done,
reviewed in docs/implementer-log.md and checked on straylight (spec 7); no finding is open.
M5d-2 (heartbeat, cron, ToolSummary, live-slot isolation) is done and on master, reviewed in
docs/implementer-log.md; finding 1 closed by task 11; spec 7 device-checked on straylight
(heartbeat on slot 2 left a following slot-0 turn's cache_n at 1398 of 1399; jobs/tier.toml
promoted only a trusted note). M6 (subagents and cloud consult) is approved
(docs/specs/2026-10-04-m6-subagents-consult.md). M6a (subagent tool, isolated child on
slots.background, scheduler skip, brokerd taint inherit; spec
docs/specs/2026-10-04-m6-subagents-consult.md sections 3, 5, 6, 7 M6a and 8; plan
docs/plans/M6a/) is done, reviewed in docs/implementer-log.md; no finding is open; spec 7
device-checked on straylight (child sub-m6a-live-2 on slot 2; following slot-0 turn cache_n
1787 of 1788). M6b (consult behind brokerd, allowlisted loopback HTTP, Decision body; spec
docs/specs/2026-10-04-m6-subagents-consult.md sections 4, 5, 6, 7 M6b and 8; plan
docs/plans/M6b/) is done, reviewed in docs/implementer-log.md; no finding is open; spec 7
device-checked on straylight (clean consult audit body matched the captured loopback POST;
a private untrusted recall still consulted under max_taint = "private", matching spec 4.1's
class gate). M6 is on origin/master (3063c3e). M7 (split deployment) has a draft spec
docs/specs/2026-10-04-m7-split-deployment.md awaiting the owner's answers; no plan yet.
P22 (proposed, not applied) makes Boxmaker a fleet of
independent agents, one per host, deployed with Nix in a last milestone, M8; flake.nix builds
every binary, the tool image and a dev shell. Work happens on titan (macOS, no Nix); build and run
on straylight, where a clone is built with nix build. The owner's Honcho
client roncho is at ~/src/roncho. Branches land on master by rebase and
fast-forward, never a merge commit. The owner's Mattermost settings and the
bot's token are in mattermost.env at the repository root (on straylight,
~/.config/boxmaker/mattermost.env): ignored by git, never committed, and the token is never
printed.
Straylight now serves Ornith as four slots over one 262,144-token pool; see
docs/inference-contract.md, "Deployment change, 2026-09-20", before relying on cache behaviour.
docs/runbook.md has an entry for every
fail-closed state; any new one needs an entry and a pointer in its message. Check docs/implementer-log.md for
what is in flight.
docs/design.mdis the binding design brief. If it looks wrong or conflicts with a measurement, stop and say so. Changes to it land as their own commit and are recorded indocs/decisions.md, which also lists proposed changes that are not yet applied.docs/inference-contract.mdholds the M0 measurements from straylight. Where it and the brief disagree, the measurements are newer.docs/specs/holds design specs;docs/plans/M<n>/holds the task files Ornith works from, andfiles/under it holds the tests and fixtures the tasks copy in.AGENTS.mdholds the implementer's standing rules; its code rules apply to any code written here too.docs/implementer-log.mdis the implementer's own record, one row per task. Reviews go at the bottom.docs/implementer-lessons.mdturns review findings into tips, for Ornith and for writing its tasks. Every milestone review updates it, and short general rules are promoted intoAGENTS.md. Do not edit the repo while Ornith has a task in flight: its tasks require a clean working tree.spike/is throwaway measurement code, not harness code.
Roles: implementation is done by Ornith-1.5-35B-A3B (served by straylight) through OpenCode on this
machine, which reads AGENTS.md. Builds are deployed to straylight afterwards. Design, specs,
measurement and review are done here. Write plans for Ornith as small
closed tasks: exact paths, given type signatures, verified crate APIs, tests specified first, and
"stop and report" instead of judgement calls. The model the harness serves is Ornith-1.5-35B-A3B.
The inference server is shared with other sessions. Before using Ornith slot 1 or sending unpinned
requests, check GET /slots?model=ornith-1.5-35b-a3b so you do not evict someone's cache.
Commands
make gateruns offline:cargo fmt --check, clippy with warnings denied,cargo test,cargo-deny, and a check that fails on any source file over 500 lines. Run it before calling any work done, and report the exit status and last lines.- Single test:
cargo test -p <crate> <test_name>. make auditrunscargo deny check advisories; it needs the network.nix build(on a host with Nix, such as straylight) builds every binary;nix build .#tools-imagethe tool image;nix developgives the toolchain. Seedocs/dependencies.md.make verify-device(from M2a task 13) runs the ignored tests incrates/loopd/tests/device.rsagainst straylight through a privateinferproxy. It uses Ornith slot 0.tools/check-m3a-device.shrunsbrokerd,loopdand an approval end to end against it.bxctlis the owner CLI:bxctl chattalks to a runningloopd serve --config <path>;bxctl approvals,approve,refuse,grants checkandaudit verifytalk tobrokerd serve(or read the audit log directly);bxctl reindexcomes in M5a.gatewayd serve --config <path>is the Mattermost channel (M4a).
Architecture in brief
Separate binaries in one Cargo workspace. Each role holds as little authority as possible:
loopdowns sessions and prompt assembly. It has no credentials and no network. Its only I/O is Unix sockets togatewayd,brokerd,inferproxyand (from M5a)memoryd.memoryd(M5) owns memory:memory/soul.md,memory.md,user.md(identity),notes.md(experience), the FTS5 index, and from M5c the sync to the fleet's Honcho. It runs memory tool callsbrokerdhas decided, and reaches Honcho and nothing else.brokerdis the only place authority lives. It reads owner-written grants (it cannot write them), runs each approved tool call in a fresh rootless container, and writes a hash-chained JSONL audit log.gatewaydis the Mattermost channel. Outbound only, no listening port; approvals arrive as!approve/!denyreplies over the WebSocket.inferproxyis a ~100-line byte forwarder from a Unix socket to the sharedllama-serverrouter on the host. It is required, becauseloopdhas no network.bxcore(formerlyproto) holds shared types, and from M5b the secret store and the sanitizer.toolkitholds tool container entrypoints.
Structural rules that span crates:
- No crate depends on another role's crate. Crates depend only on
bxcore. - Authority is encoded in types. A tool can't run without a
Decision. It is defined inbrokerdwith a private field and noDeserialize, so only the policy module can construct one (covered by a compile-fail test).bxcorecarries a plainDecisionRecord. - No source file over 500 lines.
- Files are the source of truth (
sessions/,memory/,grants/,audit/). SQLite is only for rebuildable indexes and queues.
Inference contract (the constraint most likely to be broken by accident)
Prompt processing on straylight is slow, and the hybrid-attention model can't partially rewind its KV cache, so any change to an earlier byte of the prompt forces an expensive full re-read.
- Each turn's request must be a strict extension of the previous one. Volatile content (time, heartbeat notes, memory refreshes, recalled memory) goes only in the newest message, never in the system prompt or earlier history.
- Assistant messages are logged and replayed exactly as the server returned them, including
reasoning_contentandtool_calls. - The
toolsarray is fixed per epoch; changing it re-reads the whole prompt. Other tools are found withfind_tooland called through thecall_toolmeta-tool. Never prompt the model to call an undeclared tool: the server's grammar forces the call into a declared one. - Tool results are size-capped when first appended and never trimmed later.
- The baseline (system prompt, tool schemas, the three memory files) stays at 3,000 tokens or less, measured with the server's tokenizer.
- Compaction runs after a turn at or above 85% of
n_ctx(configurable), and starts a new epoch (sessions/<id>/<epoch>.jsonl). Old logs are kept. A quiet session stays on its current epoch. - Every request carries
id_slot, and a session never changes slot. The server is shared, so slots are not reserved: a cold cache is a normal event thatloopdlogs, never an error. - Streaming always, with
return_progress: true. Timeouts are "no bytes for N seconds", never total deadlines, plus a longer limit for waiting on a busy slot or a model load.
Working rules from the brief
- Verify every external crate API on docs.rs, and every llama-server or Mattermost request field against primary docs, before use. Crate names and server parameters in the brief come from memory and aren't verified. If you can't fetch the docs, say so; don't guess.
- Keep dependencies few. Justify each in
docs/dependencies.md. Any outbound call must be listed indocs/egress.md. No telemetry and no update checks. - If a feature isn't in the brief, propose it; don't build it. Stay inside the current milestone's scope.
- Write tests first. Make one logical change per commit. Never commit runtime data, secrets, or spike output that contains conversation content.