Candidate on 96656dd21: broker split refuses invalid-recommendation after the
option checks, raise_decision text asks for one key and names the yes default,
bus README codes table, broker and tools tests. Mutants m1 (old shared line)
and m2 (order swapped) killed; full gate passed 15:42:03-15:47:36Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
5.5 KiB
Row 112 (#1606) round 1: decision.raise takes one recommendation key
Brief: docs/plans/2026-10-14_decision-raise-recommendation.md. Five owned files, no schema change.
BASE: 96656dd210. origin/refactor is 28c08328e, which changes only docs/SESSIONS.md after BASE. No manifest path moved.
Candidate: build.patch against BASE. candidate-manifest.sha256 lists exactly the five owned files. I applied the patch to a fresh git archive 96656dd21 export and every digest matched.
What changed
packages/bus/src/broker.mjs, decision.raise. The shared check splits in two, options first:
- if (new Set(choices).size !== choices.length || !choices.includes(a.recommendation))
- fail('invalid-options');
+ if (new Set(choices).size !== choices.length) fail('invalid-options');
+ if (!choices.includes(a.recommendation)) fail('invalid-recommendation');
The option count and shape checks above it still refuse invalid-options. They run before both new lines, so a bad option list never reports as a bad recommendation. invalid-approval-choice is unchanged. decisions.recommendation stays one key.
packages/harness/src/tools.mjs, raise_decision:
| Field | Before | After |
|---|---|---|
question |
The question, with enough context to answer it | ...with enough context to answer it and the reason for your recommendation |
recommendation |
Your recommended option key, with a reason | Exactly one option key, as written in options, with nothing added. The reason goes in question |
approvalChoice |
The option key that approves the action | ...approves the action. It defaults to yes, so pass it whenever no option key is yes |
packages/bus/README.md. A table after the routing paragraph lists invalid-options, invalid-recommendation and invalid-approval-choice, the field each names, when each refuses, and the check order.
Tests
packages/bus/tests/broker.test.mjs gains one test, "a recommendation that is not exactly one option key refuses invalid-recommendation after the options". It raises prd.approve, the action the PM was trying to raise:
"yes: it matches the direction","maybe"and a missing recommendation each refuseinvalid-recommendation.- Duplicate
yeskeys refuseinvalid-optionswith recommendationyes, and also with"yes: a reason", where the recommendation misses as well. - A single option refuses
invalid-optionswith a recommendation that isn't a key. - The
decisionsandmessagesrow counts are the same after all of those as before, and the inbox is empty. - A plain
yesraises, and the stored recommendation isyes.
The existing duplicate-key case at broker.test.mjs:144-154 still passes unchanged, so I left it alone. Its recommendation a is a key, so it only tests duplicates.
packages/harness/tests/tools.test.mjs adds five assertions to the existing PM tool test. The recommendation text starts with "Exactly one option key, as written in options, with nothing added", no longer says "with a reason", and sends the reason to question. The question text asks for that reason, and approvalChoice names the yes default.
Mutants
mutants.py in this packet. Each mutant swaps the two new broker lines, runs the whole bus suite (node --test tests/*.test.mjs in packages/bus), and puts broker.mjs back from its .orig copy. Both report restored=True. The run was 15:41:47-15:41:55Z, with the slot from Sage. mutants-run.log has the summary.
| Mutant | Change | Bus suite | Result |
|---|---|---|---|
| m1 | the old shared line, so a recommendation miss refuses invalid-options (the brief's mutant) |
147 pass, 1 fail | killed |
| m2 | the recommendation checked before duplicate keys | 147 pass, 1 fail | killed |
The new test is the only failure each time. m1 fails at broker.test.mjs:168, where "yes: it matches the direction" gets invalid-options (mutant-m1.log). m2 fails at :174, where duplicate keys with "yes: a reason" get invalid-recommendation (mutant-m2.log). On BASE, m1 is the shipped code and every bus test passes on it, which is why the row needed a test.
Gate
gate.sh in this packet. It ran on the candidate with the slot from Sage, 15:42:03-15:47:36Z, on BASE 96656dd21 with DOCKER_HOST unset and docker 29.7.2. It runs every package's node suite and every scripts/test-*.sh. GATE_PASSED, exit 0. gate-run.log has the summary, and gate/ has one log per step. The worktree still matched the manifest after the run.
| Step | Passed | Failed |
|---|---|---|
| bus | 148 | 0 |
| business | 67 | 0 |
| cli | 184 | 0 |
| control-board | 124 | 0 |
| conversation | 182 | 0 |
| discord | 178 | 0 |
| harness | 80 | 0 |
| ledger | 78 | 0 |
| mosaic | 69 | 0 |
| queue | 156 | 0 |
| runs | 43 | 0 |
| seat | 120 (0 skipped) | 0 |
| tasks | 92 | 0 |
| webui | 108 | 0 |
| scripts | auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 28 (node 156), release 14, task 98 | 0 |
Bus goes from 147 to 148 with the new test. Harness stays at 80 because the new assertions sit in an existing test. The other counts match the row 110 gate.
The real bwrap tester case ran and passed. It did not skip. From gate/pkg-seat.log:77:
✔ a confined session that runs tester-cli.mjs is refused as tester, and the same shim outside the sandbox binds (266.505117ms)
None of #258, #261, #262 or #263 came back. The release step made wt_default at 15:46:49Z with no containers, and I removed it at 15:47:45Z.
Going live
Not part of this row. The brief puts the copy, re-pin and bus restart under Mos's go, shared with 179697a8a (row 110).