Files
boxmaker/docs/plans/M4b/README.md
T
kyleandClaude Opus 5.5 af7137dafe M4b task 15: fix task 14's dropped refusal reason, as a listed edit
Task 15's session found the defect in serve/approvals.rs, a file it did not list, and stopped.
The comment in task 14 used one name for two values; task 14's tests could not reach it. Lesson T30.

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

90 lines
6.2 KiB
Markdown

# 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 `15-…`). 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, for a session
not in Mattermost, in the first approver's direct messages), 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).
**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.
- **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 |
At the end of task 15: about 845 tests (768 before task 01).
## 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.
## Running it
```sh
git switch m4b
BOXMAKER_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M4b
```
Keep the OpenCode TUI closed while it runs.