Files
kyleandClaude Opus 5.5 f6dd7f1741 M4b review, and follow-up tasks 16 to 18
The review of tasks 01 to 15 goes in the implementer log, with the
device check against the owner's Mattermost. Three follow-ups:

- 16: the refusal without a runner says why.
- 17: the texts that say who asked.
- 18: approvals from non-approvers in direct or group messages go to
  an approver.

Task 14's given serve_approvals.rs becomes serve_approvals.rs-task14,
since task 18 hands over a new version. Tip T31 is added.

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

7.6 KiB

M4b implementation plan: approvals over Mattermost

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

Goal: an ask call is posted in Mattermost, in the thread that asked for it (or, when no approver could answer it there or the session is not followed, in the first approver's direct messages, with who asked and what), and an approver answers it there with !approve <id> or !deny <id> [reason]. The answer is recorded exactly as bxctl's is, naming the approver and the post; the post then shows what came of it. Nobody else can answer, and no post is left looking answerable when it is not.

Architecture: proto gains the shared escaping (01) and the four gateway.sock messages (02); bxctl gains deny (03). brokerd records the answering post (04), reads [gateway] (05), and gets the courier, which carries each pending approval to gatewayd and its answer back (06). gatewayd reads [approvals] (07), strips a leading mention of itself and recognises approval commands (08), can edit a post and look up a name (09), renders an approval's posts (10), keeps the approvals it posted and judges commands (11), records their posts in the state file (12), listens on gateway.sock (13), posts and marks approvals from its serve loop (14), and wires the commands, the typing pause and the fallback notice (15). After the review: brokerd's refusal without a runner says why (16); the texts that say who asked (17); and approvals asked by non-approvers in direct or group messages go to an approver (18).

Spec: docs/specs/2026-09-24-m4b-approvals.md (approved; P16 applied to the brief). Runbook: task 06 adds gateway-unreachable, task 07 widens gatewayd-start-failed.

No task needs Mattermost, Podman or a network. Everything runs against fakes: a fake gatewayd for brokerd, and for gatewayd a fake Mattermost, a fake loopd and a fake brokerd client.

Global constraints

  • Everything in AGENTS.md, including "Lessons from earlier reviews".
  • No new dependency.
  • Branch m4b. One task, one fresh OpenCode session, one commit. Run cargo fmt --all before the gate. Review happens once, after task 15; tasks 16 to 18 are its follow-ups.
  • New files come as skeletons: types, constants and the glue written, todo!() bodies with the code to write in the comment above. Existing files are changed by the exact edits in the task, never replaced whole: a whole-file copy would overwrite an earlier task's work.
  • Some tasks hand over new versions of given test files, because the change is in what the test must see (a new field, a new text). Copy them over the old ones as the task says; they are still not to be edited.

Tasks

Every task had a reference implementation, built in this order from master with the gate at every step (tip T26), its given tests run against it, and mutations of it caught by them (tip T28); then the reference was moved off this machine (tip T18). Every skeleton was checked to compile at its task's end state and fail its tests, and every comment above a todo!() was read beside its signature (tip T29).

# File Delivers Tests Check
01 01-proto-escape.md proto::escape, moved from bxctl proto/tests/escape.rs reference; bxctl unchanged
02 02-proto-gateway-wire.md the four gateway.sock messages gateway_wire.rs, 8 fixtures reference
03 03-bxctl-deny.md bxctl deny cli_deny.rs reference
04 04-brokerd-answer-post.md answers record the post; Table::contains ledger_post.rs; 3 given tests updated reference
05 05-brokerd-config-gateway.md [gateway] config_gateway.rs; rig.rs updated reference
06 06-brokerd-courier.md the courier courier.rs, support/fake_gateway.rs reference; 10 mutations caught; no test hangs
07 07-gatewayd-config-approvals.md [approvals] config_approvals.rs reference
08 08-gatewayd-mention-commands.md the leading mention; Route::Approval sessions_mention.rs; sessions.rs, serve.rs updated reference; 7 mutations, 5 caught, 2 that cannot change behaviour
09 09-gatewayd-mm-edit.md patch_post, username mm_edit.rs reference; the bot's edit checked on the owner's server
10 10-gatewayd-approval-text.md the posts' text approvals_text.rs reference; 7 mutations caught
11 11-gatewayd-approvals-registry.md the registry, judge approvals_judge.rs reference; 6 mutations caught
12 12-gatewayd-state-approvals.md approval posts in state.json; turn_of state_approvals.rs reference
13 13-gatewayd-gateway-sock.md the listener and connection threads gateway_sock.rs reference; 6 mutations, 5 caught, 1 that cannot change behaviour
14 14-gatewayd-serve-approvals.md posting, marking, the restart, the listener in run serve_approvals.rs, support/brokerd_client.rs; fake_mm.rs updated reference; 5 mutations caught
15 15-gatewayd-answers.md commands judged, typing paused, the notice serve_answers.rs, deliver_notice.rs; deliver.rs, serve.rs, support/gateway.rs updated reference; 4 mutations caught
16 16-brokerd-refusing-message.md the refusal without a runner says why (review finding 2) runner.rs updated reference
17 17-gatewayd-asked-texts.md the texts that say who asked, and the note approvals_asked.rs reference; 5 mutations caught
18 18-gatewayd-redirect.md a non-approver's approval goes to an approver (review finding 1) serve_redirect.rs; serve_approvals.rs updated (task 14's copy is serve_approvals.rs-task14) reference; 9 mutations caught; 5 runs

At the end of task 15: about 845 tests (768 before task 01); at the end of task 18, about 854.

Changes during the run

  • 2026-09-24, task 15: tasks 01 to 14 committed, each on its first session. Task 15's session made every change the task asks for, and one given test still failed: a refusal's reason was missing from the post. It traced the cause to task 14's ended in serve/approvals.rs, a file task 15 did not list, and stopped instead of editing it, which was right. The fault was the plan's: in ended, the refusal's text and a DenyReason were both called reason, and the comment said Refused -> Ended::Refused { by, reason } without saying which; and task 14's own tests could not send a refusal, so nothing there could catch it. Task 14 took the DenyReason reading, then silenced the unused-variable warning on the other with reason: _ (and said so in its log). Task 15 now lists serve/approvals.rs with the exact fix. Checked in a scratch copy: the saved attempt (.state/runs/M4b/15-first-attempt*) plus that fix passes every suite three times and the gate, 845 tests. Resume from task 15.
  • 2026-09-24, review: accepted (docs/implementer-log.md, "M4b, tasks 01 to 15"). The device check found that an approval asked by a non-approver in a direct message could be answered only with bxctl; the spec was changed (d2e8141), and tasks 16 to 18 were added, each with a reference built from m4b, gated, and moved off this machine. Task 14's given serve_approvals.rs is now serve_approvals.rs-task14, since task 18 hands over a new version. Resume from task 16.

Running it

git switch m4b
BOXMAKER_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M4b
# the review's follow-ups:
BOXMAKER_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M4b 16

Keep the OpenCode TUI closed while it runs.