# Row 109 (#1603) build notes Researcher, 2026-10-13. Brief: `docs/plans/2026-10-13_pm-tester-and-fresh-state.md`, "A new PM run reads current launch state and task status before it plans". BASE: `02fe2d9d5` (origin/refactor after row 106 landed as `8b58cb7e4`). Reviewer: Filbert. Gate owner: Sage. Candidate change: `row109.patch` (8 files: 7 modified, 1 new). Built and run in `/mnt/storage/scratch/researcher/q109`, detached at `02fe2d9d5`, `node_modules` linked to the canonical one. The patch applies to BASE; the packet is not in it. ## What ships - `packages/bus/src/broker.mjs`: read verb `state`, no arguments, open to role, human and reader sessions (reader allowlist gains `state`). It returns `{launch:{state,at,event}, tasks:{open:[{task_ref,title}], total, cap}}` for the capability's business. - `launch` is the latest `launch.revoked` / `launch.restored` event of the business: `state` `revoked`, else `allowed`; `at` and `event` are that event's time and id, null when no event exists (`allowed`). - `tasks.open` is `#tasks(business,{done:false})`, so it is the tracker snapshot's open tasks, tombstones left out, in `task_ref` order, refs and titles only. The cap is the `tasks` page size (`TASK_PAGE`, 50), stated as `cap`; `total` counts every open task. - Nothing is stored. No schema change. - `packages/harness/src/runner.mjs`: before each turn the runner calls `state` and prefixes the turn request with `stateBlock(state, readAt)`: `Current state, read at :`, `Launches: ...`, `Open tasks: N[, the first K of cap C].`, one line per task. The read time is the runner's clock at the call. A failed read (anything but a dead session) fails that turn without running the adapter: the sender gets the usual RESULT `The turn failed: could not read the current state: `, the failure is logged, and `message.read` still runs. A dead session (`SESSION_GONE`) exits as it does on `message.receive`. - `turnRequest()`: the header gains `sent ` from the message row's `at`, after the sender and before the class. - `roles/pm.md`: one rule in Protocol: the block is the truth for launch state and task status; a message or a note that disagrees is out of date; before planning to resume or launch work on a task, check it with `get_task`. - READMEs: one table row in `packages/bus/README.md`, one paragraph in `packages/harness/README.md`. - `packages/harness/src/tools.mjs` is untouched: `state` is not exposed as a tool; only the runner calls it. ## Fixture edit (accepted by Sage, rows 109 scope) `packages/harness/tests/fixtures/fake-adapter.mjs` took the message body as everything after the first blank line of the request. The state block adds a blank line above the `Message ...:` header, which broke every TOOL/SLEEP/FAIL/NOMARKER test. It now takes the body after the `Message ...:` header line (regex), with the old split as fallback. This is the only reason for the edit; no other harness test needed a change. Every other test using the fixture passes in the full harness suite (79/79). ## Tests - `packages/bus/tests/state.test.mjs` (new, 6): never switched reads `allowed` with null time and event; revoked then restored reads `allowed` with the restore event's time and id (and `revoked` with the revoke id in between); a task closed on the tracker leaves the open list and `total` drops; a tombstone is not open; the list stops at the cap and says so while `total` counts all; role and reader sessions read the same state, an argument refuses `invalid-request`, an unbound cap refuses `unauthenticated`. - `packages/harness/tests/runner.test.mjs` (6 new tests, 20 in the file; harness suite 73 -> 79). By name: 1. `turnRequest shows the message's own sent time, a day old as well as new` 2. `stateBlock names the launch state with its time and event, and the open tasks with the cap` 3. `launches revoked and then restored: the turn's block reads allowed and gives the restore time` 4. `a task closed on the tracker is missing from the next turn's open list` 5. `a message read a day after it was sent shows the time it was sent` (the broker's clock jumps a day after the send) 6. `a failed state read fails the turn: no adapter runs, the sender hears why, the message is read` (no adapter turn file, RESULT with the reason, message read, log line) My message to Sage before the gate run said five new runner tests. That was a miscount: it left out test 2, the `stateBlock` unit test, which I wrote as a unit test beside test 1. The suite count (+6) is the evidence. The existing first end-to-end test, `a message becomes a turn, the answer goes back as a RESULT, ...`, is changed, not new: it now checks the exact block and the header with `sent `. - The state block and the time are checked by the runner tests through the fake adapter, which echoes the request. ## Mutants (the kills) `mutants.sh ` runs one test file for the control and one per mutant, each mutant one exact-once text edit, and restores every source on exit. Outputs in `mutants/`; run summary in `mutants-run.txt`. `mutants/.patch` is `git diff` of that file against BASE, so it also shows the candidate's own edits; the mutation is the one changed expression. The candidate was byte-identical to `row109.patch` after the run. | mutant | edit | result | failing tests | |---|---|---|---| | control | none | runner 20/20, bus 6/6 | none | | m1 | request is `turnRequest(m)`, no block | runner 17/20 | the first end-to-end test; "launches revoked and then restored"; "a task closed on the tracker is missing" | | m2 | header drops `sent ` | runner 17/20 | the first end-to-end test; "a message read a day after it was sent"; "turnRequest shows the message's own sent time" | | m3 | a failed read sets `block = ""` instead of failing the turn | runner 19/20 | "a failed state read fails the turn" | | m4 | launch event read oldest-first | bus 5/6 | "launches revoked and then restored read allowed, with the restore time and event id" | | m5 | open list built without the `done:false` filter | bus 5/6 | "a task closed on the tracker leaves the open list" | | m6 | `cap` dropped from the output | bus 4/6 | the closed-task test and the cap test | The `roles/pm.md` rule has no test; it is prose. Out of scope as the brief says: expiring old messages, tracker poller or `list_tasks` defaults, and who may switch launches. ## Suites (all in q109, output teed to `suites/`) BASE `02fe2d9d5`; slot held 23:04:51Z to 23:08:36Z, "clear" sent; no process of mine left in q109. | suite | result | Sage's gate count | |---|---|---| | `node --test packages/bus/tests/*.test.mjs` | 134 pass, 0 fail | 128 (+6 new) | | `node --test packages/harness/tests/*.test.mjs` | 79 pass, 0 fail | 73 (+6 new) | | `node --test packages/queue/tests/*.test.mjs` | 156 pass, 0 fail | 156 | | `scripts/test-release.sh` | 14 passed, 0 failed | 14 | | `test-auth` 15, `test-conductor` 17, `test-config` 24, `test-discord` 66, `test-extension-package` 18, `test-foundation` 44, `test-task` 98 | 0 failed each | n/a | | `scripts/test-queue.sh` | 28 passed, 0 failed | n/a | The `queue verify and render --check` case of `test-queue.sh` is skipped in a worktree (q109 is not the queue's canonical root `/mnt/storage/src/mosaic-stack`; Filbert's N3 from row 101). Sage's canonical-root gate rerun is the evidence of record. I did not apply the patch to or run suites in the canonical checkout. ## PM workspace (read-only) `pm-workspace-names.txt`: the PM's persistent workspace holds one planning-notes file, `findings-2026-10-12.md` (7737 bytes, 2026-10-12T14:53Z), and `stack/`, a git checkout of the repository. Names only; no contents read. The note is a likely second source of the stale picture alongside old inbox messages; the pm.md rule covers it. The file is left alone. ## Scope and live Owned files only, plus the accepted fixture. Live use needs a new copy and re-pin under Mos's go, with decision 109 notices before and after (runner.mjs reaches new role sessions at their next start; the broker verb needs a bus restart).