# 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 from jason, class REQUEST, request :`. `task_create`'s `request` says to take that `request `, 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 |