The speaker goes on every turn of the chat: interactive, --say and --say's retry. Without the
flag it is None. A name that is empty or holds whitespace or a control character (Unicode), or a
second --speaker, is a usage error: the usage text on stderr and exit status 2. The usage text
gains the flag and one line explaining it. Given: chat_speaker.rs (new) and new versions of
chat.rs, chat_approvals.rs and cli.rs. Task 01 now copies chat.rs-task01, the older chat.rs.
Checks: every before block found once at ref 03; replayed from the task text on a fresh ref-03
worktree with its own build directory, byte-identical to the reference, gate ok with 1158 tests
(1148 + 10); /tmp unchanged around the reference's and the replay's gate runs; 20 of 20
mutations caught; the new tests passed 40 of 40 runs under CPU load; given files pass fmt and
the 500-line check; no chat-template tag in the plan or the given files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A batch carries the Mattermost user id of each message; the turn's speaker
is the username of the one user who wrote all of it (Client::username,
kept in a map on the event loop), and None for two or more people or a
failed lookup. The turn's text is unchanged.
Checks: reference replayed from the task text on a fresh ref-02 worktree
with its own build dir, byte-identical; make gate ok with 1148 tests
(1138 + 10); /tmp unchanged around both gate runs; 21 mutations, all
caught; new tests stressed 40 times under load, no failure; every before
block found once at ref 02; no chat-template tags.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner's hosts (titan, talos) run 1.98.1; nixos-26.05's Rust is
1.95.0, so builds and lints differed by host. The flake gains a second
input, oxalica's rust-overlay (following our nixpkgs, so host configs
can still follow theirs), and both the package build and the dev shell
use its stable 1.98.1 with clippy and rustfmt. The workspace's
rust-version rises to 1.98 to match; AGENTS.md, docs/dependencies.md
and the M5c-1 plan follow.
Checked on straylight: nix build gives all seven binaries; the dev
shell has rustc, cargo and clippy 1.98.1, rustfmt 1.9.0, cargo-deny
0.19.6; make gate in it passes with 1122 tests, as on titan.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
loopd copies the Turn's speaker onto the User record it logs, as it
came (not checked, not neutralised), and never into a request; after a
finished turn it sends SessionUpdated with the turn's number, the count
of User records in the session's log (Session::turns), so the first
turn is 1, a failed turn that was logged counts, and the count goes on
after a reopen or a restart. Failed turns are still not told.
Given: speaker_turn.rs (8 tests) and new versions of loopd's memory.rs,
turn_broker.rs, end_to_end.rs and support/turn.rs (run_turn gains
speaker, MemorySource::updated gains turn). Task 01 now copies
memory.rs-task01, its own version.
Checks: the reference passes make gate with 1138 tests (1130 + 8); the
task replayed from its text on a fresh ref-01 worktree with its own
target is byte-identical to the reference and passes the gate; /tmp
unchanged around both gate runs; 15 one-line mutations (speaker not
logged or dropped by the channel or neutralised, speaker in the request
at log time or at replay, turn 0-based or +1, counting Assistant
records, all records or only speakers' turns, a global or per-process
counter, told before logging or on failure, MemoryPort sending 0) all
caught; speaker_turn run 60 times under load, no failure; no
chat-template tag in the plan or given files; task lines within 100
characters but tables.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task 01 changes bxcore's wire and log types for M5c-1: Turn.speaker and
LogRecord::User.speaker (optional, skipped when None, so old frames and logs
keep their bytes), SessionUpdated.turn (required, refused when missing), and
the new SessionState / SessionStateReply messages. The other crates only
construct the new fields (speaker: None, turn: 0) and brokerd's kind() names
the two new kinds; later tasks give them values. 20 given files.
Checks, against a reference at 7b40686 with its own build directory:
- every "before" block found exactly once at 7b40686 (and at master, which
changed no crate since);
- step 2's failures as the task names them: two strict tests, fifteen test
targets with E0559/E0560/E0432/E0599 only;
- make gate: gate: ok, 1130 tests (1122 + 8); /tmp unchanged around it;
- replay from the task text in a fresh worktree of 7b40686 with its own
target/: crates/ byte-identical to the reference, gate ok, 1130; removed;
- 23 mutations: 21 caught; the two that drop `default` from an Option
field cannot change behaviour (serde reads a missing Option as None);
- no chat-template tag whole in the task or the given files; lines over
100 characters only in tables, fixtures and verbatim strings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`.envrc` runs `use flake` when Nix is present, so on the NixOS hosts
(programs.direnv.enable brings direnv and nix-direnv) cd'ing into a
clone gives the pinned Rust toolchain, clippy, rustfmt, cargo-deny and
make; on titan, which has no Nix, it does nothing. nix-direnv's cache,
.direnv/, is ignored. Checked on straylight in a throwaway copy:
cargo 1.95.0, cargo-deny 0.19.6, rustc from the Nix store.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
29 request/response pairs, each request exactly as roncho 1.0.0's
blocking client sends it, recorded in a throwaway workspace
(boxmaker-plan-probe) created for it and deleted afterwards; no other
workspace was touched. For the fake Honcho server of task 10. Found:
creation is get-or-create (201, then 200); adding messages to a missing
session creates it; a 422 echoes the whole input; a deleted conclusion
answers 204 with an empty body, and 404 the second time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner's Honcho client, blocking, with TLS: default features off,
feature tls. It adds only itself to the lock file (checksum a5ccccc2…,
as checked against crates.io in review). cargo deny passes. The
dependency and the egress to rift are documented; the egress table also
lists the design model's probe of rift in a throwaway workspace.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Memory items 4 and 5: two workspaces per agent and one fleet workspace
for untrusted notes; the private ceiling, secret sessions never synced;
trusted memory writes mirrored as conclusions; the agent's peer not
observed; no rebuild, corrections by contra-fact or deleting our own
conclusion; the fleet workspace always untrusted. Recorded in
docs/decisions.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A turn without complete speaker information is not sent; every peer in
the fleet workspace is a member, so fleet notes are messages that one
search finds, with a retraction message for an undone note (Honcho
cannot delete a message); the fleet workspace derives nothing
(reasoning, summaries and dreaming off); API keys per workspace are
first-class, with a runbook entry for a refused key.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Designed with the owner on 2026-09-27: per agent a trusted and an
untrusted workspace plus one fleet workspace for untrusted notes;
private ceiling, secret sessions never synced; no rebuild (contra-facts,
or deleting our own conclusion); the owner and other people observed,
the agent not; every trusted memory write mirrored as a conclusion; a
decision ledger that doubles as the outbox; recall over the local index
and the fleet; ask_memory by workspace chat. Grounded in roncho 1.0.0
and Honcho 3.2.0 (source read at v3.2.0) and rift's live metrics.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
decisions: M5b settles the invalid-arguments reason, tag
neutralisation and the shared secret store (their open rows go), and
the verdict on the run's two driver trials. CLAUDE.md's current state:
M5b done, M5c (Honcho, roncho 1.0.0) next; straylight's clone is
boxmaker-m5b.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found in the M5b device check on straylight: the self-test asked about
each of the 33 tags and each neutralised form, 66 requests, one
connection each, and inferproxy's token bucket (burst 10) refused the
ninth, so loopd would not start. Now it asks about all the tags at once,
one per line, and then all their neutralised forms: the answer to the
first must hold each tag's recorded token in order, the second none of
them. Each tag is a special token, split out before anything else, so
the lines between cannot change it; and a neutralised form is only
dangerous as a tag's own token, so that is what the second answer must
not hold. The errors name a tag by its id, as before.
The fake tokenizer reads text with several tags in it; the tests cover
each tag as ordinary tokens, as another listed or unlisted token, a
duplicated token in the middle and at the end, a form holding each tag,
and both requests failing. Spec section 8 and the runbook entry follow.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The review of tasks 01 to 14 in the implementer log (task 14's row
renamed to its file's name), and four tips for writing tasks: what to
do with a skeleton's comments, golden files for scanners, naming the
log row, and running the implementer without plugins.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text from outside the model (the user's message, every tool result, and the
memory block) is neutralised the moment it is first logged, so every later
request replays the same bytes and a tag in that text cannot reach the model as
one of its own control tokens. The model's own output, replay, and the system
prompt are left byte-for-byte.
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
Open item from M5b task 09: loopd reads a session's trust from its
epoch-0 log, so compaction must carry it into the new epoch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The task table follows the files (03 tags, 04 refuse, 05 flag) and
gives each task's gate count and mutations; the interfaces are the ones
the tasks built (task 02's role argument and SecretSpec, Refusal.at,
Flag, TurnDone.untrusted required, disarm, LockError::line); the review
focus names the tasks that pin each item; which task adds each runbook
entry, and the -taskNN convention for replaced given copies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The last runbook entry, the rule-change procedure of spec 6.5; the
entries memory-write-refused and memory-write-flagged now send the
reader to it instead of to the spec. Tasks 06, 08 and 12 added the
other five, because the gate needs an entry for every pointer.
Checks: the edits, applied from the task's text to ref 13, give a
runbook byte-identical to the reference; each before block found once;
the gate passes on the reference with 1118 tests; /private/tmp unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each locks <home>/run/<role>.lock in main (gatewayd after its secret,
so nothing is made before the secret is read). brokerd's old
second-instance test in serve.rs expected the audit lock's message,
so serve.rs has a new version; task 01 now copies serve.rs-task01.
Checks: given tests fail at ref 12 as the task says (2 each in
brokerd, memoryd, gatewayd; serve.rs passes); the task's quoted edits
applied to ref 12 reproduce ref 13's tree exactly; replay gate ok with
1118 tests (1113 - 1 + 6), /tmp unchanged; 14 mutations, all caught;
the six instance test files stressed 25 runs under 16 busy loops and
20 under 32, no failure.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bxcore::instance::lock (File::try_lock, file 0600, missing directory
0700), LockError::line for the two lines the daemons print, and the
already-running runbook fragment; loopd locks <home>/run/loopd.lock
before its socket and self-test, inferproxy <socket>.lock.
Checks: given tests fail at ref 11 as the task says (bxcore E0432;
loopd 3 and inferproxy 3 fail); the task's quoted edits applied to
ref 11 reproduce ref 12 byte for byte; replay gate ok with 1113 tests
(1099 + 14), /tmp unchanged; 25 mutations, 24 caught (the uncaught
one maps TryLockError::Error to Held, not reachable from a test);
given files rustfmt-clean, under 500 lines; no chat-template tags.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The task file, the skeleton crates/bxctl/src/sanitize.rs (five todo!()
bodies: parse_sanitize, code_point, token, prefix, tag_count) and two
given tests: sanitize_check.rs (8, runs the binary) and
sanitize_report.rs (10, the library).
Decisions: the one argument after `check` is the text, whatever it
looks like; none means standard input. Output: refuse line (first rule
only; a character as U+XXXX, a tag as its token id, a secret as its
prefix and an ellipsis), flag line (rule only), neutralise line (every
occurrence of every tag). Exit 0 nothing fires, 1 something fires, 2
usage error or unreadable text. Nothing of the text is printed.
Checks: every given test fails before the task as the task says (the
report file does not compile: one E0432, two E0599; the binary tests
exit 2); the skeleton state gives 10 and 8 not-yet-implemented
failures; the reference passes make gate with 1099 tests (1081 + 18);
the task's edits applied to a fresh ref-10 copy with its own build
directory give a tree byte-identical to the reference; 21 mutations,
each caught; 40 runs under load, no failure; /private/tmp unchanged
around a gate run; rustfmt and the line limit on the given files; no
tag or key-shaped string in the plan or given files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task 10 (spec 9, the owner's option C): a new module gatewayd::links with
disarm(text, untrusted) -> Disarmed { text, links }, given as a skeleton
with 16 small todo!() helpers, and deliver posting each answer through
answer_posts: disarm, split_answer, disarm each part again, NOTICE once
at the end of the last post when a link was put in a code span.
Checks, against a reference kept off master:
- Replayed from ref 09 in a scratch worktree with its own target/: the
task's edits (applied from the task text) plus the given files plus the
reference bodies are byte-identical to the reference. make gate on
rustc/clippy 1.98.1: gate: ok, 1081 tests (1058 + 7 + 7 + 9). /tmp
holds nothing new after the gate.
- Step 2 (E0432 only) and step 3 (6/1, 7, 9 failing, all "not yet
implemented"; warnings as the task lists) seen in the replay.
- 71 one-line breaks of the reference, each caught by the given tests.
- Differential fuzz of generated Markdown through Mattermost's marked
fork (mattermost/marked c211e4c, the webapp's options) and CommonMark:
on the final code, 600k replies in each mode, half with inline LaTeX
on, render no image and (untrusted) no link.
- disarm(disarm(x)) == disarm(x) on 200k generated replies, both modes.
- Given files pass rustfmt; skeleton comments read beside their
signatures; no chat-template tag in the plan or given files;
deliver_links 40 runs under CPU load, no failure.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bxcore's TurnDone gains untrusted: bool, always written and required
when read (no serde default: a missing field would read as a clean
session, the wrong way to fail). loopd's new Session::untrusted is true
once any ToolResult in the log is labelled untrusted, and channel::handle
puts it in the TurnDone; the log is the one source, so it holds across
resumes and restarts. gatewayd, bxctl and brokerd sources are unchanged;
their tests that build a TurnDone get new versions. Task 01's broker.rs
is kept as broker.rs-task01, and task 01 now copies it.
Checks: the given tests fail at ref 08 as the task says (strict's
turn_done.json, and 13 test targets with E0560/E0609 only); the task's
edits applied from its text to a fresh ref 08 tree are byte-identical to
the reference; the gate passes with 1058 tests; 12 mutations caught;
/private/tmp and TMPDIR unchanged around the runs; no tag in plan or
given files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
loopd's self-test gains a check, run second (after "server matches
config", before any prompt): each tag in bxcore::sanitize::TAGS must be
one token from /tokenize, its recorded id, and its neutralised form
more than one. A failure names the tag by its id, never its text, and
points at a new runbook entry, loopd-tags-unexpected, given as a
fragment; SelfTestError::pointer picks the entry per failure. One
request per text (66): /tokenize has no batch form. The fake server
gains respond and a fake tokenizer; selftest.rs, serve.rs, pointers.rs
and support/mod.rs get M5b copies.
Checks, on a reference at ref 08 (titan, rustc 1.98.1, own target/):
- /tokenize fields from llama.cpp tools/server/README.md at b10964
(b29c606) and its handler; on straylight all 33 tags gave their ids
and every neutralised form 7 to 11 tokens, 66 requests in ~0.3 s
- given tests before the change: 4 fail as the task lists, selftest_tags
does not compile (7 E0599); pointers passes
- make gate: gate: ok, 1050 tests (1042 + 8); /tmp and $TMPDIR
unchanged around it; the four suites 25 runs under load, no failure
- 20 mutations, each caught: id not compared; neutralised check
dropped; > 1 as >= 1, as == 1 missing 0, as > 2; first or last tag
skipped; tag asked twice; pointer dropped or wrong (all or Tokenize);
check skipped in serve; check run last; parse_special false;
add_special left out; id missing from the message; the tag's text in
the message; tokenize error as Infer; wrong count; wrong other id
- the task's edits applied to a fresh ref-07 copy give files
byte-identical to the reference; every before block found once
- no chat-template tag whole in the task or given files; task lines
within 100 characters except tables and verbatim lines; given files
under 500 lines
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
loopd neutralises the user's message (the User record), every text
appended as a tool result (results, recall's, denials, failures and
loopd's own answers share the one append in run_turn), after the cap,
and the memory block in Baseline::assemble_with. The model's output,
system.md and replay are untouched: an old log with a raw tag is
replayed byte for byte.
Checks, on a reference at ref 07 (titan, rustc 1.98.1, own target/):
- given tests before the change: 7 of 9 fail as the task lists; the
two that guard what must not change pass before and after
- make gate: gate: ok, 1042 tests (1033 + 9); /tmp unchanged around it;
the two new suites 20 runs, no failure
- 14 mutations, each caught: user not neutralised; neutralised before
the cap; recall path missed; only untrusted results; truncated from
the final length; block not neutralised; model content, reasoning or
tool arguments neutralised; replay re-neutralises user, tool or
system; open rewrites records; system.md neutralised too
- the task's edits applied to a fresh ref-06 copy give files
byte-identical to the reference; every before block found once
- no chat-template tag whole in the task or given files; task lines
within 100 characters except tables; given files under 500 lines
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
memoryd's writes go through bxcore::sanitize::refuse, then flag; check.rs
and its test go. A refused write answers with the rule's sentence and is
logged with the rule's name; a flagged identity write becomes untrusted
notes with the rule in the answer, the journal (new optional field `flag`)
and the log, and is never staged for review; the block leaves out an
entry that breaks a refuse rule and logs the rule. The three runbook
entries the new pointers need are given here as fragments, because the
gate's runbook check fails on a pointer without its entry.
Checks, on titan (rustc 1.98.1, clippy 0.1.98):
- given tests fail before as the task says: 7 failing in edit, serve and
serve_sanitize; files and sanitize do not compile (E0609; E0432, E0609,
E0615)
- the task's edits, parsed from the task file and applied to a fresh
ref-05 worktree with its own target/, give a tree byte-identical to the
reference; its gate: ok, 1033 tests; /private/tmp unchanged
- 26 one-line mutations of the reference, each caught by the given tests
- new tests stressed under load: 30 runs, 0 failures
- no chat-template tag and no whole fake key in the task or given files
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Records what porting Hermes' patterns decided: seven rules kept,
"dump the conversation" narrowed, agent-configuration changes left out
(Boxmaker's agent loads no such files), about fifty look-alikes,
apostrophes folded, and a flag in bxctl names only its rule. A false
positive also marks a session that recalls the note untrusted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>