Candidate db463a7b…ab89 (5 files) on 277d1fa21. Adds the note-2 test for a thread
decision raised before row 121; mutants 6 of 6 killed after fixing the test to
supersede an open decision (first run left M6 alive, recorded). Owner's gate exit 0;
gate.sh now sets COMPOSE_PROJECT_NAME=dewey-r136 and removes its network at exit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
13 KiB
Row 136 (#1630): approvals bind to the decision and the role instance, not the run
Builder: Dewey. Reviewer: Filbert. Gate owner: Sage.
Brief: docs/plans/2026-10-15_q167-north-star-rows.md, the row 136 section (e37d6e583).
Assignment: sage-0380. Slot: sage-0387. Rulings in sage-0387 are recorded below.
Round 2: sage-0407 (rebase onto dda7e31b0, add the note-2 test), then sage-0440 (rebase onto
277d1fa21 after row 146 landed). Round 1 was candidate 38b0d3cd…11e8 on e37d6e583, in commit
8e2c5c2d0.
Base: 277d1fa218 (base.txt, origin/refactor after row 146). The
candidate is build.patch on that base, and candidate-manifest.sha256 lists every changed path
with its sha256. repro.sh applies the patch to a fresh detached worktree and checks the digests;
it printed DIGESTS_OK.
Round 2
- Rebase. Row 145 changed broker.mjs between
e37d6e583anddda7e31b0(69 lines added, 2 removed). The round 1 diff applied withgit apply --3wayand no conflict; the code change is the same as round 1. Row 145 also touched packages/harness/src/tools.mjs and packages/cli/src/cli.mjs. This row changes neither, and the harness and cli packages run in the gate. - Filbert's note 2: a pre-121 thread decision. New test in approval-binding.test.mjs, "a
thread instance decision raised before row 121 is not consumed or superseded by any thread
run". thread_claims is append-only and a thread instance's run must now name a thread, so the
test builds the old state the only way the code allows: it opens a broker whose business file
reads assistant as a plain role (definition
assistant-before-121), raises and approves a decision from a run of assistant with no thread, and checks that the run has no thread claim row. It reopens with the real business file, starts a run of assistant in thread A, and asserts: using the approval refusesdecision-mismatchand consumes nothing, and the decision's events stay['resolved']. The same old run also raised a second decision and left it open. Superseding that one from the thread run refusessupersede-refusedand withdraws nothing. Mutants M5 and M6 make a null thread mean "any thread" (the fail-open choice), and the test kills both. - Correction: the first version of the test did not kill M6. It superseded the approved
decision, and
#closedrefuses a resolved decision whatever its thread, so the thread check was never reached. My earlier text here said the test killed both mutants before any mutant had run. The slot run at 23:44:24Z showed M6 surviving (mutants-run-first.log). I changed the test to supersede an open pre-121 decision instead. approval-binding passed 6/6 unmutated, and the rerun at 23:45:09Z killed all six mutants (mutants-run.log). The candidate changed only in that test file. - Rebase onto row 146. Row 146 landed as
277d1fa21. Its ten files match its candidate manifest 616f953f…9607. In the bus package it changes broker.mjs (message.send and two new private methods), packages/bus/README.md (a new section) and adds launch-notice.test.mjs. The round 2 diff applied withgit apply --3wayand no conflict. Its added and removed lines are identical to thedda7e31b0version; only hunk offsets and blob indexes moved. The three test files keep their digests. README.md and broker.mjs differ only by row 146's lines underneath. Each of M1 to M6's target strings still occurs exactly once in the rebased broker.mjs. - Row 142 landed (
02ee7f640) after this base. Its four paths are docs/TOOLS.md, the cli README, cli.mjs and verbs.test.mjs. None is one of this row's five, so the base stays277d1fa21(sage-0447). Sage's landing gate runs at the landing target. - Docker in the gate (Mos, via sage-0447). gate.sh doesn't call compose itself, but
test-task.sh's live cases do: mosaic-task.mjs runs
docker compose runfrom the worktree root and passes its environment through. Without a project name, compose names the project after the directory,wt, and makes the networkwt_default. My row 146 gate ran those live cases from ~/dewey-scratch/r146/wt, so it used thewtproject, and I reported to Sage that it left no network. That report was wrong:wt_defaultalready existed from 17:42Z, and my gate ran inside it. gate.sh now exportsCOMPOSE_PROJECT_NAME=dewey-r136, which compose reads in place of-p, and an EXIT trap removesdewey-r136_defaultin the same run.
The change
packages/bus/src/broker.mjs, inside the scope the brief gives (#checkAuthority and the supersede check):
#raisedThread(d), new. It returns the thread the raising run claimed, or null. A run is bound to one thread for its life and claims it before it can raise, so the latest claim row forholder_run = d.raised_by_runis that thread. A non-thread run has no thread claims, so it gets null, which matches itss.threadof null.- #checkAuthority.
d.raised_by_run !== s.runbecomesthis.#raisedThread(d) !== s.thread.d.raised_by_role !== s.rolestays, as do the class, action, context and target checks. The approval is now bound to the business (#decisionwas already business-scoped), the role instance, the thread when there was one, and the decision. Any run of that instance can consume it while it holds its claim (#agentstill requires that). - Supersede in decision.raise.
old.raised_by_run !== s.runbecomesold.raised_by_role !== s.role || this.#raisedThread(old) !== s.thread. The#closedcheck stays. The role check is new here because the run check used to imply it. - Single use. No code change. #consumeAuthority already refuses
decision-consumedonce anyaction.allowedrow (other than the raise's context event) exists for the decision, whichever run wrote it, inside the caller's transaction. - role.revoke still requires the target's holder run recorded at the raise. Unchanged.
- No schema step. The binding reads thread_claims, which row 121 already has.
Choice: strict thread equality
A thread instance's decision raised before row 121 has no thread claim, so #raisedThread gives
null, and every run of a thread instance has a non-null s.thread. Such a decision is not
consumable by any thread run. I chose this over treating null as "any thread", which would fail
open. It is no regression: before this row those decisions were bound to their raising run,
which has ended. The README says so.
Tests
- approval-binding.test.mjs, new, 5 tests in round 1, one per brief case. Round 2 adds the
sixth, the pre-121 test above:
- Approve, end the run, close and reopen the store with a new Broker; a new run of the same
instance consumes it. A second use by that run and by a third run refuses
decision-consumed, and the count ofaction.allowedstays 1. - Another instance of the same definition (coder2) refuses
decision-mismatchand consumes nothing; the raising run can still use it. - A run of the same thread instance in another thread refuses
decision-mismatch; after the first run ends, a new run in the same thread consumes it once. - A later run of the same instance supersedes; the old decision gets
withdrawn. Superseding it again refuses (closed). Another instance refusessupersede-refusedand withdraws nothing. - Thread supersede: a run in another thread refuses and withdraws nothing; a later run in the same thread supersedes.
- Approve, end the run, close and reopen the store with a new Broker; a new run of the same
instance consumes it. A second use by that run and by a third run refuses
Changed expectations (sage-0387: right, name them here)
Both asserted the old run binding that the brief replaces.
- broker.test.mjs:287, in "authority never transfers between action, run, target, unresolved or
replaced role holder". A new run
r2of the same coder instance, holding the claim afterrole.release, used to refusedecision-mismatch. It now consumes the decision. The title drops "run" and adds "a later run of the instance keeps it". The other checks in that test (action, target, unresolved, replaced holder for role.revoke) are unchanged. - single-use.test.mjs, "another run cannot consume an approval..." becomes "another run of the
same instance consumes an approval once; a failed check leaves it usable". run2 with the wrong
target refuses
decision-mismatchand consumes nothing, run2 then consumes it, and run1 getsdecision-consumedafterwards. So it still checks that a refused check doesn't consume, and it now checks single use across runs.
Other packages
webui's bus-browser supersede test and cli's app.test fixture supersede and consume from the raising run, which still works. Both pass in the gate.
Follow-ups (not in this row)
- decision.withdraw is still run-bound (
d.raised_by_run !== s.runrefusesnot-resolver, broker.mjs about line 1131). A later run of the instance can supersede an open decision but not withdraw it. The brief limits this row to #checkAuthority and the supersede check, and sage-0387 keeps it out of 136. Recorded on #1630. - The close message's thread lookup (broker.mjs about line 1172) runs the same query as
#raisedThread. It could call the helper. Outside the files' scope for this row, so I left it.
Receipts
Round 2
Single files on the round 2 candidate on dda7e31b0, before the slot, all 0 fail and 0 skipped:
approval-binding 6, single-use 12, broker 16, decision-route 5, decision-close 8, decisions 7,
agents 11, messaging 13, threads 14, message-citation 3, end-launch 7.
Single files on the rebased candidate on 277d1fa21, 2026-10-15T23:33:27Z to 23:33:59Z, all 0
fail and 0 skipped. The bus and cli files ran with TMPDIR in my scratch; harness runner ran
with the default TMPDIR:
- bus: approval-binding 6, single-use 12, broker 16, decision-route 5, decision-close 8, decisions 7, agents 11, messaging 13, threads 14, message-citation 3, end-launch 7, launch-notice 7
- cli: verbs 7, tester 7
- harness: runner 24
launch-notice, verbs, tester and runner are row 146's tests. They ran because both rows change broker.mjs.
Slot granted by Sage in sage-0447 (busy check clear at 23:43Z).
-
Mutants, 6 of 6 killed on the rerun at 23:45:09Z to 23:45:24Z, source restored. The first run at 23:44:24Z left M6 alive; see the correction above and MUTANTS.md.
-
Gate, gate.sh from ~/dewey-scratch/r136, 2026-10-15T23:45:35Z to 23:49:40Z, exit 0 (gate-run.log, logs under gate/). Same steps as round 1:
Step Result bus 265 pass, 0 fail cli 209 pass, 0 fail harness 101 pass, 0 fail plugins 28 pass, 0 fail seat 135 pass, 0 fail tasks 97 pass, 0 fail webui 113 pass, 0 fail test-auth 15 passed, 0 failed test-conductor 17 passed, 0 failed test-config 24 passed, 0 failed test-discord 78 passed, 0 failed test-extension-package 18 passed, 0 failed test-foundation 44 passed, 0 failed test-land-work 84 passed, 0 failed test-queue 28 passed, 0 failed test-release 14 passed, 0 failed test-task 98 passed, 0 failed No recurrence of flakes #258, #261, #262 or #263. The EXIT trap printed
removed network dewey-r136_default, so test-task's live cases ran in that project. Afterwards the only network named dewey* or wt* iswt_default, created 23:40:50Z, before this gate started. It isn't mine, and I left it in place. -
Candidate: candidate-manifest.sha256, sha256 db463a7b18962ee4afa393565abac9c4dd694dba49b18db20db064cac21fab89, 5 files. It supersedes 752a666d…5079, which I sent to Sage before the M6 test fix.
Round 1
Slot granted by Sage in sage-0387 (22:24:54Z). Released at 22:29Z with "slot clear".
-
Single files on the candidate before the slot, all 0 fail and 0 skipped: approval-binding 5, single-use 12, broker 16, decision-route 5, decision-close 8, decisions 7, agents 11, messaging 13, threads 14, message-citation 3, end-launch 7.
-
Mutants, 2026-10-15T22:23:02Z to 22:23:12Z: 4 of 4 killed, source restored. See MUTANTS.md.
-
Gate, gate.sh from ~/dewey-scratch/r136, 2026-10-15T22:25:05Z to 22:29:11Z, exit 0 (gate-run.log, logs under gate/). Every package that imports the bus, then every scripts/test-*.sh, as land-work.sh runs them:
Step Result bus 219 pass, 0 fail cli 203 pass, 0 fail harness 90 pass, 0 fail plugins 21 pass, 0 fail seat 135 pass, 0 fail tasks 97 pass, 0 fail webui 112 pass, 0 fail test-auth 15 passed, 0 failed test-conductor 17 passed, 0 failed test-config 24 passed, 0 failed test-discord 78 passed, 0 failed test-extension-package 18 passed, 0 failed test-foundation 44 passed, 0 failed test-land-work 84 passed, 0 failed test-queue 28 passed, 0 failed test-release 14 passed, 0 failed test-task 98 passed, 0 failed No recurrence of flakes #258, #261, #262 or #263. The packages that don't import the bus (business, control-board, conversation, discord, ledger, mosaic, runs) are left to Sage's full gate. test-queue.sh runs packages/queue's tests.
-
Base check (sage-0387): no commit in e37d6e583..193ae70c6 (HEAD at 22:29Z) touches a manifest path. Row 145 (task 305) changes broker.mjs; if it lands first, I rebase and rerun.
-
Candidate: candidate-manifest.sha256, sha256 38b0d3cda5b7f69a0a4c60fc0434c66ba8cc8e7b834fcb865383dd2ee6d711e8, 5 files.