Files
stack/agents/dewey/work/queue-90/build.md
T

190 lines
12 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 90 (#1584): The approved requirement ids as data, round 1
Owner: Dewey. Reviewer: Filbert, then Sage's gate. Brief:
`docs/plans/2026-10-12_pm-task-flow.md`, section "The approved requirement
ids as data" (blob `e8a1150e`). Also PM fault 10 (#228 c91), covered by
the stop line.
Base `23338c7b` (queue rev 798). `build.patch` is
`git diff --cached --binary 23338c7b --` the twenty paths in
`candidate-manifest.sha256`. Apply it with `git apply --index` to a detached
worktree of the base; the manifest then checks 20/20 OK. No commit from the
base to `refactor` `1e4cfa96` touches the twenty paths (checked at
packaging).
| File | What changed |
|---|---|
| `packages/tasks/requirements.json` | new: the PRD's path and version, and its 24 ids, each with a one-line title |
| `packages/tasks/src/requirements.mjs` | new: `checkRequirements` (the file's schema), `loadRequirements` (checked, frozen) |
| `packages/tasks/src/verbs.mjs` | `task.create` refuses `unknown-requirement` for an id the list doesn't hold; the format check stays |
| `packages/bus/src/broker.mjs` | the constructor loads the list; a `requirements` read for role sessions |
| `packages/harness/src/tools.mjs` | `list_requirements` for task.create holders with a tracker; `task_create`'s `request` and `requirement`, `raise_decision`'s `blocking` |
| `packages/harness/src/runner.mjs` | the turn header names a message's `request` id (outside the file list; see Decisions) |
| `packages/harness/src/bundle.mjs` | one session-text line: only the human stops a run |
| `roles/pm.md` | two Duties bullets (the id from `list_requirements`, the request id; no id fits: a non-blocking `prd.approve`) and two Authority paragraphs (blocking holds a task; stopping a run) |
| `packages/tasks/README.md` | `requirements.json`, the `task.create` detail, `unknown-requirement` in Refusals |
| `packages/bus/README.md` | the `requirements` row; `decision.raise`'s `task-required` rule |
| `packages/tasks/tests/requirements.test.mjs` | new: the list against the PRD, frozen, each schema refusal, the load errors |
| `packages/tasks/tests/verbs.test.mjs` | two tests: only a listed id creates; a received message's `request` creates and its id refuses |
| `packages/tasks/tests/{adapter,fields,shapes,sync}.test.mjs` | `REQ-S-1`, not in the PRD, becomes `REQ-TASK-1` |
| `packages/bus/tests/requirements.test.mjs` | new: who can read `requirements`; the PM's non-blocking `prd.approve` and `role.revoke` with no task reach the human's Inbox; blocking still refuses `task-required` |
| `packages/harness/tests/tools.test.mjs` | the PM's tool list; `list_requirements`'s gating, verb and text; the three descriptions; the row 89 byte count (below) |
| `packages/harness/tests/runner.test.mjs` | the turn header with and without a `request` |
| `packages/harness/tests/bundle.test.mjs` | the stop line |
## What the PM sees now
- **Which ids exist.** `list_requirements` returns
`{prd, version, requirements:[{id,title}]}`, 24 ids for PRD 1.0. Its
text says `task_create` takes only these, and that a request no id fits
gets a `prd.approve` decision instead of a task. `task_create` refuses
any other id with `unknown-requirement`, so `REQ-QA-1` (P19 #229, #230)
now refuses.
- **The request id.** The turn header of a human message reads
`Message <id> from jason, class REQUEST, request <event id>:`.
`task_create`'s `request` says to take that `request <id>`, not the
message's own id. Before this change the header left `request` out, so
the PM had only the message id, which refuses `request-not-found`.
- **The Inbox path without a task.** `raise_decision`'s `blocking` says a
blocking decision holds a task and needs `task_ref`, and a request with
no task is false. `pm.md` says the same, and that a gated decision with
no task still reaches the human's Inbox. The bus test raises both
`prd.approve` and `role.revoke` from the PM that way and reads them in
the human's (and reader's) inbox.
- **Stopping a run.** The session text and `pm.md` say only the human stops
a run, with `mosaic stop`; a session that thinks a run should stop raises
`role.revoke`, `blocking` false, the role instance as target, naming the
run and saying why. The bundle line goes to every role session, not only
the PM, since every session holds `raise_decision`.
## Decisions
1. **`runner.mjs` is outside the brief's file list.** The brief says each
message `message.receive` returns carries `request`, and the tool should
say to use it. The broker does attach it (`broker.mjs` message.receive),
but `turnRequest` built the session's turn text from `id`, `from_role`,
`class`, `in_reply_to` and `decision` only. Without one line there, the
new `request` description would point the PM at a field it never sees.
I told Sage before packaging; Sage's ruling: keep it, name it here and in
the review request. Row 93 bases its `runner.mjs` on this one if row 90
lands first (Sage's note on row 93, rev 800).
2. **The broker loads the list in its constructor**, so a bad
`requirements.json` refuses at bus start, not at the first read. The
tasks adapter loads it again in `verbs(ctx)`, at adapter start. Both load
the same committed file.
3. **`requirements` is an agent read (`#agent`)**, the same guard as
`task.create`: a human session refuses `agent-required`, a reader
`read-only`, a role session that doesn't hold its role `not-holder`. The
bus doesn't also check that the role holds `task.create`. The list is the
approved PRD's ids, nothing secret, and the harness gives the tool only to
task.create holders. The reader allowlist in `request()` is unchanged.
4. **`requirements.mjs` imports nothing from the bus**, so the broker can
import it statically without a cycle (tasks already imports the broker).
Its errors are `RequirementsError`, as `fields.mjs`'s are
`FieldTableError`.
5. **Tests use `REQ-TASK-1`, a real id, in place of `REQ-S-1`**, with no
seam to inject a test list. Sage agreed.
6. **The titles are mine**, one line of 120 characters or fewer, written
from each PRD bullet. The test checks ids, order and version against the
PRD, not the titles. A reviewer may want different wording; changing a
title changes no behaviour.
## Limits
- A bad file at bus start shows as `startup-refused` over IPC.
`process.mjs` maps any error that isn't a `BusError` to that code, so the
`RequirementsError` message (`requirements[3].id: not a requirement id`)
doesn't reach the host. The start still refuses. Making it a `BusError`
would change the boot protocol, outside this row.
- `packages/harness/README.md` isn't in the file list, so it doesn't yet
name `list_requirements` or the `request` in the turn header. A one-line
follow-up if the reviewer wants it.
- `packages/tasks/src/index.mjs` exports `loadFields` but not
`loadRequirements`. Nothing imports the list through the index; adding the
export is outside the file list.
- Nothing here reads the PRD at run time. A later PRD version needs a new
`requirements.json` in a reviewed commit, and the test fails until it
matches.
- #229 and #230 keep their `REQ-QA-1`; correcting them is the PM's and
Jason's (out of scope in the brief).
- Live at the next bus restart and PM launch (decision 109): Mos is notified
before landing.
## Correction to row 89
Row 89's `packages/harness/tests/tools.test.mjs:115` comment and its
`build.md` (line 183) say 19999 emoji in a JSON string are "80002 bytes".
They are 79998: 19999 × 4 + 2 quotes (checked with `Buffer.byteLength`).
This row fixes the test comment. Row 89's packet stays as written; this is
the correction entry.
## Gate (my run on the candidate)
`gate.sh` on a fresh detached worktree of `23338c7b` with `build.patch`
applied by `git apply --index`; manifest 20/20 OK; suites sequential, each
node suite from its package directory. 2026-10-12T22:43:29Z to 22:46:50Z.
Summary in `gate-summary.txt`.
| Suite | Result |
|---|---|
| tasks node | 79 pass, 0 fail |
| bus node | 111 pass, 0 fail |
| harness node | 66 pass, 0 fail |
| webui node | 86 pass, 0 fail |
| cli node | 115 pass, 0 fail |
| business node | 60 pass, 0 fail |
| seat node | 27 pass, 0 fail |
| control-board node | 124 pass, 0 fail |
| test-auth | 15 passed |
| test-conductor | 17 passed |
| test-config | 24 passed |
| test-discord | 66 passed |
| test-extension-package | 18 passed |
| test-foundation | 44 passed |
| test-queue | 27 passed |
| test-release | 14 passed |
| test-task | 26 passed; run with `DOCKER_HOST=unix:///nonexistent`, so it skips the adapter seam, workspace/capability, live task and live recall cases (docker unavailable) |
Every rc is 0. No commit from the base to `refactor` `1e4cfa96` touches the
twenty paths.
## Mutants
`mutants.py` swaps one exact string in a scratch copy of the candidate (a
plain directory, its 20 files byte-identical to the candidate when run 2
ended), runs the tasks, bus or harness node tests in their own process
group (killed after 300 s, which counts as a kill), and restores the file.
44 mutants.
- **Run 1** (`mutants-run1.txt`): 43 killed, 1 survived, `empty list
allowed`. The cause was the test, not the code: the schema table's loop
used an edit's return value when it was an array, so
`(l) => (l.requirements = [])` handed `checkRequirements` the `[]` itself
and tested "not an object" instead of the empty list. The fix: a separate
not-an-object loop for `[]`, `null` and a string, and the table now edits
in place and ignores the return. I also changed "a numeric version" from
`1` to `1.5`, so it can't pass the version regex if the type check goes.
- **Run 2** (`mutants-run2.txt`): after the fix, `list version 1.1` and
the 20 mutants on `requirements.mjs`, which the changed schema and load
tests cover. 21 killed. The other 23 were killed in run 1 by tests the
fix didn't touch.
| Mutant | Site | What it undoes |
|---|---|---|
| list order swapped, two ids swapped, drops the last id, adds REQ-QA-1, version 1.1 | `requirements.json` | the list equals the PRD's ids, order and version |
| top keys open, entry keys open | `requirements.mjs` | unknown keys refuse |
| prd pattern dropped, prd takes a parent step | `requirements.mjs` | `prd` is a `docs/prd/*.md` path with no `..` |
| version any type, version 0 major, version minor optional | `requirements.mjs` | `version` is a `N.M` string, N from 1 |
| empty list allowed | `requirements.mjs` | at least one id |
| id numbered 0, id suffix allowed, id twice allowed | `requirements.mjs` | the id format, anchored; no id twice |
| title 121, title 119, title newline allowed, blank title allowed | `requirements.mjs` | a title is 1 to 120 characters, one line |
| not frozen, frozen shallow, unchecked load | `requirements.mjs` | the loaded list is checked and deep-frozen |
| not JSON / ENOENT reported as unreadable | `requirements.mjs` | the load error names the cause |
| unknown-requirement dropped, as invalid-request | `verbs.mjs` | an unlisted id refuses `unknown-requirement` |
| only the first id, all but the last id | `verbs.mjs` | every listed id creates |
| requirements agent guard dropped, keys dropped, returns the ids only, constructor loads nothing | `broker.mjs` | the read's guard, its no-argument rule, its shape, the load at start |
| list_requirements ungated, gated on task.assign, without tracker, wrong verb, no prd.approve | `tools.mjs` | who gets the tool, its verb, its text |
| request, requirement, blocking description old | `tools.mjs` | the three new descriptions |
| turn request always, turn request dropped | `runner.mjs` | the header names `request` only when there is one |
| stop line dropped | `bundle.mjs` | the session text's stop line |