Files
boxmaker/docs/plans/M4a/README.md
T
kyleandClaude Opus 5.5 02d5e25046 M4a task 14: post's comment was now_ms's; fixed, and every comment read
A script that rewrote the skeleton's comments put `now_ms`'s text on `post`. Task 14's second
session saw the contradiction and deliberated until cut off. Both comments are fixed, each
todo!() comment in tasks 14 and 15 was read beside its signature, and both tasks now say to stop
and quote a comment that does not fit. Lesson T29.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 00:40:05 -07:00

8.2 KiB

M4a implementation plan: gatewayd, conversations over Mattermost

For the implementing model: do not work from this file. The owner gives you one task file at a time (01-… to 15-…). This file is the index for the owner and the reviewer.

Goal: a direct message to Boxmaker's bot account on Mattermost becomes a turn in loopd, and the answer is posted in its thread. In channels shared with other agents, Boxmaker answers only posts that name it, and replies in its own threads that name nobody else. Anyone not on the allowlist gets nothing at all. gatewayd opens no listening port.

Architecture: proto gains SHA-1 (01). gatewayd gets its dependencies and test certificates (02), its configuration (03), its secrets and the runbook entries (04), then a network layer built bottom-up: TCP or TLS (05), HTTP/1.1 (06), the WebSocket handshake (07), frames (08) and the connection (09). Mattermost's JSON and REST calls (10), the routing of posts to sessions (11), the state file (12) and delivery on loop.sock (13) are each one module. The serve loop ties them together (14), and main starts it (15).

Spec: docs/specs/2026-09-23-m4a-gateway.md. Brief: docs/design.md (P15 applied). Every fail-closed message ends with a pointer into docs/runbook.md; task 04 adds all seven entries this milestone needs.

No task needs Mattermost or a network. Everything runs against fakes: TLS test servers with a test-only CA, a scripted WebSocket server, a scripted HTTP server, a fake Mattermost and a fake loopd. The checks against the owner's server are done by the design model afterwards.

Global constraints

  • Everything in AGENTS.md, including "Lessons from earlier reviews".
  • New dependencies, all in task 02 and no others: rustls 0.23.45 (no default features; ring, std, tls12), rustls-native-certs 0.8.4, zeroize 1.9.0; gatewayd also uses serde, serde_json and toml, already vetted.
  • Branch m4a. One task, one fresh OpenCode session, one commit. Run cargo fmt --all before the gate. Review happens once, after task 15.
  • Most tasks hand over a skeleton: the file with its types, constants and signatures written, and todo!() bodies with the steps as comments above them. Fill one function at a time and run cargo check -p <crate> after each (tip T25). Keep the comments; they say why.
  • Mattermost's JSON is another program's format: unknown fields are ignored. Our formats (gatewayd.toml, state.json) reject them.

Tasks

The last column is how the given tests were checked before hand-over (tip T17). Every task had a reference implementation, and the given tests were run against it at that task's end state. Then each task's end state was rebuilt on its own from master in this order, with the gate at every step (tip T26), and the reference was deleted so it cannot be read (tip T18). Each skeleton was checked to compile against its task's tests and fail them.

# File Delivers Tests Check
01 01-proto-sha1.md proto::sha1 proto/tests/sha1.rs reference; vectors checked with sha1sum
02 02-gatewayd-deps.md dependencies, deny.toml, TLS test certificates none new reference
03 03-gatewayd-config.md gatewayd.toml config.rs, support/tmp.rs reference
04 04-gatewayd-secrets.md SecretStore: credential, env, file; seven runbook entries secrets.rs reference
05 05-gatewayd-net.md TCP or TLS, verified net.rs, support/tls_server.rs reference
06 06-gatewayd-http.md HTTP/1.1 requests, size caps, rate-limit waits http.rs reference
07 07-gatewayd-ws-handshake.md base64, the WebSocket handshake ws_handshake.rs reference
08 08-gatewayd-ws-frames.md the frame decoder and encoder ws_frame.rs (a naive decoder as oracle, 300 seeds) reference
09 09-gatewayd-ws-conn.md the connection: pings, close, a dead peer ws_conn.rs, support/ws_server.rs reference; 7 mutations, all caught once a test for the Host header was added
10 10-gatewayd-mm.md Mattermost's events and REST calls mm_json.rs, mm_rest.rs, support/http_server.rs reference; 11 mutations, 10 caught, 1 that cannot change behaviour
11 11-gatewayd-sessions.md routing, commands, the queue per session sessions.rs reference; 14 mutations, 13 caught, 1 that cannot change behaviour
12 12-gatewayd-state.md state.json state.rs reference; 12 mutations, all caught
13 13-gatewayd-deliver.md turns on loop.sock, answers in the thread deliver.rs, support/fake_loop.rs reference; 11 mutations, 10 caught; the one left accepts an event frame with another id, which loopd never sends
14 14-gatewayd-serve.md the serve loop: routing, typing, catch-up, reconnecting serve.rs, serve_restart.rs, support/fake_mm.rs, support/gateway.rs reference; 14 mutations, all caught once four tests were added; 8 runs clean
15 15-gatewayd-main.md gatewayd serve --config <path> main.rs reference; run against the owner's server, token never printed

At the end of task 15: about 762 tests (650 before task 01).

Changes during the run

  • 2026-09-23, task 08: tasks 01 to 07 committed; task 08's session ended with nothing written. It planned all four todo!()s in its head, most of it deliberating how to avoid indexing, and was cut off (tip T25, again: Decoder::header has a dozen branches and was one todo!()). The tree also had handshake.rs with its imports in 2021-edition order, which no logged command explains; it was reverted. The attempt is saved in .state/runs/M4a/08-first-attempt.diff. The design model split the skeleton: next_message and header are now written as glue, over seven helpers of a few lines each whose comments give the exact expressions. The same was done for task 09's poll (glue over four helpers) before it could stop the same way. Both new skeletons were checked in a scratch copy of the branch: they compile and fail their tests, and filled literally from their comments they pass (7 of 7, 10 of 10 five times), clippy clean, gate ok. Resume from task 08.
  • 2026-09-23, task 14: tasks 08 to 13 committed, each on its first session. Task 14's session wrote nothing, not even the copies: it read twenty source files to learn the APIs, then planned connect's wait for hello in prose ("let me stop designing", and it did not) until it was cut off. Twelve-plus todo!()s across two files and the whole crate's API were too much for one turn. The design model wrote connect, Gateway::new and catch_up as glue (leaving catch_up_channel), gave handle_post and start their borrow- and move-sensitive lines verbatim, added a table of every call the helpers make with its signature (checked against the branch), and made copying and seeing the tests fail the first two actions. Task 15's serve comment, whose numbered steps had run together, was rewritten one step per item. Both were checked in a scratch copy of the branch at task 14's start: the skeletons compile and fail (serve 6, serve_restart 5 with 2 passing, main 4 with 1 passing), and filled from their comments they pass (five runs), clippy clean, gate ok. Resume from task 14.
  • 2026-09-24, task 14 again: this session copied the files and saw the tests fail, as told, then deliberated until cut off, over a real contradiction: post's comment was now_ms's text. The design model's script that rewrote the comments placed each after the first eight-space comment following the function's name; now_ms is a free function with a four-space comment, so its text went to post, and post's was lost. The check filled the bodies from the design model's own code, not from the comments, so it could not see this. Both comments are fixed; every todo!() comment in tasks 14 and 15 was then read beside its signature. Tasks 14 and 15 now say to stop and quote a comment that does not fit. The attempt is saved in .state/runs/M4a/14-second-attempt*. Resume from task 14.

Running it

git switch m4a
BOXMAKER_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M4a

Keep the OpenCode TUI closed while it runs.