190 lines
12 KiB
Markdown
190 lines
12 KiB
Markdown
# 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 |
|