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

179 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.