Files
stack/agents/filbert/work/queue-57/BUILD.md
T

9.2 KiB
Raw Blame History

Row 57 (#1546): the bus reads decisions routed to roles and recent decisions, candidate packet

Author: Filbert. Reviewers: Darkwing and Dewey. Brief: "Bus read: decisions routed to roles and recent decisions" in docs/plans/2026-10-10_design-implementation.md. Base: 958668a8476250edab8025a48a5c553dfd75f99b (base.txt). The feature is uncommitted; only this packet is committed. I pushed nothing.

Files

files.txt lists the 8 repository paths. candidate-manifest.sha256 has their sha256. build.patch is git diff --cached --binary 958668a8 over them: 8 files, +297/−17.

File Change
packages/bus/src/broker.mjs new verb decisions, {list: 'roles' | 'recent'}; RECENT = 50; the reader allowlist adds it
packages/bus/src/views.mjs decisions(list) in the Q1 client module
packages/bus/tests/decisions.test.mjs new: 6 tests, listed below
packages/bus/README.md "Human and reader paths" says what the verb returns and who may call it; the "Shared reads" example
packages/webui/src/serve.mjs GET /api/bus/decisions?list=roles|recent; any other list is 400 and never reaches the bus
packages/webui/src/bus.mjs the header comment only; the reader already passes any views() verb through
packages/webui/tests/bus.test.mjs one new test for the route; the 405 test and the busReader test cover it too
packages/webui/README.md the bus read list and the port exposure note

Scope

The brief owns packages/bus/, packages/webui/src/bus.mjs and its tests. Two files outside that list changed:

  • packages/webui/src/serve.mjs. The brief's "its webui endpoint" is a route, and the routes live there, not in bus.mjs. The change adds the verb to BUS_VERBS and checks ?list= before the bus is called.
  • packages/webui/README.md. It lists every /api/bus/ route and what the port exposes, so a new route without it would leave the README wrong.

No Inbox tab uses the endpoint yet. The brief ships the read and the endpoint; the tabs are the design rows' work.

How it works

decisions is a broker verb like inbox, in the same read transaction.

  • Who: human and reader sessions. A role session gets human-required, claimed or not, and the refusal is recorded as action.refused like every refusal. A reader still gets read-only for every write.
  • Arguments: {list} and nothing else (keys(a, ['list'], ['list'])). Any other list is invalid-request.
  • roles: the business's open decisions with route_to <> 'human', in seq order. Open means no resolved, withdrawn or expired event, the same rule as inbox and the schema's inbox view. The human's own open decisions stay in inbox. Unlike inbox, gated decisions are not put first: the list spans roles, and route_to is on each row.
  • recent: the business's closed decisions of every route, newest close first (decision_events.seq DESC), at most RECENT (50). Each row has closed, the closing decision_events row, from the same #closed() that the authority check and decision.resolve use. The schema trigger decisions_resolve_once allows one close per decision, so the join can't repeat a row.
  • Each row is #decisionView(d), the shape inbox returns: options parsed, blocking a boolean, and the authorization context.

What a reader may read

The brief says the reader's authority doesn't widen. Per row, it doesn't: trail(<decision id>) already gives a reader or human session the same decision row and its closing event, and test 4 checks every listed row against it. What is new is the listing. Before this row a reader needed the id. Decision ids are random UUIDs. A decision with a task_ref could already be found through tasks and then the task's trail, but one without (a within-role decision, or a scope change with only a target) could not.

The WebUI port is where that matters. F1 on #1522 recorded that anything that can connect to the Console's port reads what a reader capability reads, including a T3 seat with curl and a managed S6 session, and slice 1 accepted that. With this row that includes both lists, so a role session that can reach a running Console's port can list other roles' open decisions there, though not through its own bus capability. The README's exposure note now says so. The brief's test is on the bus capability (test 5); closing the port is outside this row, as it was for S5.

Tests

packages/bus/tests/decisions.test.mjs, each for both a human and a reader session where it applies:

  1. roles lists the open decisions routed to cto and pm, never the human's, with the inbox shape; inbox still lists only the human's.
  2. recent lists closed decisions of every route, newest close first, with how each closed: resolved by the human (via cli, with its note), resolved by a role, withdrawn, expired, and within-role. roles is then empty.
  3. recent stops at RECENT rows, newest first.
  4. Every row of both lists equals the decision row, and its closed the closing event, that trail gives the same session for that id.
  5. A role session (cto, pm, coder, and an unclaimed launch) gets human-required for roles, recent and an unknown list, and each refusal is recorded. Each role's inbox stays its own. A role in another business is refused too.
  6. One business only (a reader of other sees only other's rows), and strict arguments ({}, all, limit, an array, Recent). A successful read writes no row in events, decisions, decision_events, messages, deliveries or role_claims. A reader still can't send decision.seen.

packages/webui/tests/bus.test.mjs:

  • New: the route serves both lists from a real broker through the reader view, with no-store, {rows, at}, and a hostile question unescaped in the JSON. A missing, empty, unknown, wrong-case or NUL-suffixed list, or ?subject= instead, is 400 and never calls the bus.
  • The 405 test sends every non-GET method to the new route too.
  • The busReader test checks decisions('recent') sends {business, verb: 'decisions', args: {list: 'recent'}} to the transport.

Mutants

out/mutants/run.mjs edits one source file in a second worktree, runs the bus and webui suites, and restores the file (out/mutants/result.txt):

Mutant Change Killed by
role-allowed a role session may call decisions bus test 5
roles-with-human roles includes the human's decisions bus test 1; webui route test
roles-with-closed roles includes closed decisions bus tests 1, 2, 6; webui route test
roles-other-business roles ignores the business bus test 6
recent-other-business recent ignores the business bus test 6
recent-oldest-first recent oldest close first bus tests 2, 3
recent-by-raise recent ordered by raise, not close bus test 2
recent-no-limit no LIMIT bus test 3
recent-limit-plus-one LIMIT RECENT + 1 bus test 3
recent-resolved-only recent drops withdrawn and expired bus test 2
recent-no-closed rows lose closed bus test 2; webui route test
loose-keys limit accepted bus test 6
unknown-list-empty an unknown list returns [] bus test 6
reader-refused the reader allowlist drops decisions bus tests 1-4 and 6; webui route test
web-no-list-check the route passes any ?list= to the bus webui route test
web-no-verb decisions not in BUS_VERBS webui route test and 405 test
views-fixed-list views().decisions ignores its argument webui route test and busReader test

All 17 are killed. roles-other-business first matched the inbox query too; I narrowed the pattern and reran it with roles-with-human (result-rerun.txt). In the first run, roles-with-human's webui run also hit the 600-second timeout in unrelated conversation-view tests while the machine was loaded; the rerun shows the route test failing on its own.

Gate

Run on 958668a8 with the candidate applied, sequentially, TMPDIR in my scratch and DOCKER_HOST pointed at a missing socket (out/gate/):

Suite Result
node --test packages/bus/tests/*.test.mjs rc 0, 80 pass, 0 fail
node --test tests/ in packages/webui rc 0, 40 pass, 0 fail
test-auth.sh rc 0, 15 passed
test-conductor.sh rc 0, 17 passed
test-config.sh rc 0, 24 passed
test-discord.sh rc 0, 66 passed
test-extension-package.sh rc 0, 18 passed
test-foundation.sh rc 0, 44 passed
test-queue.sh rc 0, 27 passed
test-release.sh rc 0, 4 passed; the Docker cases skip without a daemon
test-task.sh rc 0, 26 passed

The manifest checks OK after the gate. Sage's rerun with Docker is the gate of record.

Things a reviewer should know

  • RECENT is fixed at 50 and the verb takes no limit. The design's Recent tab shows one page; a limit argument would be a new input to validate for no current caller.
  • The recent query reads decision_events once per row through #closed(), as inbox does. At 50 rows that is 51 indexed reads in one transaction.
  • A decision raised and closed in the same call (within-role with a choice) appears only in recent, never in roles.