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

12 KiB
Raw Blame History

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