# Row 139 (#1633): decisions that cite the spec go to an agent; only hard waits reach Jason Builder: Dewey. Reviewer: Filbert. Gate owner: Sage. Brief: docs/plans/2026-10-15_q167-rows-r3.md at b71268b8e (blob 6805904555dc, rev 1565), section "Decisions that cite the spec go to an agent; only hard waits reach Jason". Round 1 built against 04c572003 (blob c22ce67cd). Requirements: PRD 1.5 REQ-DEC-2 and REQ-WAIT-1 to REQ-WAIT-3, and PRD 1.6 (this row) for Jason's ruling Q169. Calls from sage-0502, 0504, 0512, 0514, 0516, 0528, 0533 and 0539. sage-0523 (Mos's ruling) is superseded by Q169. Round 2 follows Jason's ruling Q169 (sage-0528), which replaces Mos's sage-0523 ruling that the first round 2 build followed. See "Round 2" below. Round 1's gate and mutant outputs stay in gate/ and mutants/. Round 2's are in r2/. Round 1's candidate is build.patch and candidate-manifest.sha256 at commit 62f354746. The ones at the packet root are round 2's, with Mos's REQ-ROLE-2 sentences (sage-0545): build.patch sha256 a5f21ead8195382fbf21da0ee8f66b794329c4b7f8737fa942ccd6c219524e3f, candidate-manifest.sha256 sha256 a4c5657ff69ef0665285e4c2095858346f2d325d454c6fa99edd999633dcd0f0. The round 2 candidate before those sentences (patch 7a29d911…, manifest 7a1fcae1…) is at commit c740f56ee and differs only in docs/prd/mosaic-stack.md. PRD-1.6.md holds the 1.6 text. Mos accepted it with the added sentences, and readings 1 to 4 (sage-0545). See "PRD sentence (sage-0545)" below. Base: 5661bd233bbe3e1f03a64a76ad7edcd9f6e41e89 (base.txt, origin/refactor after row 135). The candidate is build.patch on that base. candidate-manifest.sha256 lists every changed path with its sha256. repro.sh applies the patch to a fresh detached worktree and checks the digests. ## What ships 1. **The hard waits.** The broker's GATED and the business's GATED_ONLY are now exactly these: spend, data.delete, network.expose, account.create, credential.mint, message.external, git.push.protected and deploy. data.delete, network.expose and account.create are new actions in both ACTIONS lists, placed after deploy. 2. **The actions that left.** git.merge.protected, policy.change, prd.approve and role.revoke are no longer on either list. A role file may grant them, and a raise routes them under REQ-DEC-2. 3. **REQ-WAIT-3 by construction.** Neither list is written out by hand. Each side holds an explicit ROUTED list of 21 actions: `ROUTED` exported from vocabulary.mjs, and a Set in broker.mjs. The hard waits are ACTIONS minus ROUTED, so an action added to ACTIONS is a hard wait until a reviewed commit adds it to ROUTED. The bus imports no business code, so the two lists stay separate. packages/bus/tests/hard-waits.test.mjs pins them equal: HARD_WAITS equals GATED_ONLY, and the broker's ACTIONS equal the vocabulary's. 4. **Routing (broker.mjs:1206-1211).** `arbitrated = cls === 'cross-role' || (cls === 'gated' && spec !== null && !GATED.has(action))`. An arbitrated raise goes to `arbiters[domain]`. Otherwise a gated raise goes to the human and a within-role raise to the raiser. If the arbiter would be the raiser, the decision goes to the human, as cross-role routing already did. `spec` is non-null only when the raise cited a spec and readSpec accepted it. readSpec refuses a stale citation (`stale-spec`) and a spec whose last commit isn't the human's (`spec-unconfirmed`) before any decision is stored. 5. **REQ-WAIT-2.** The validator's refusal (role.mjs:41, "which is always gated") and the broker's `invalid-authority` already applied to every GATED_ONLY/GATED entry at both classes. They now cover the new list. No code change was needed there; the tests cover each hard wait at each class. 6. **roles/\*.json.** No change. No shipped role grants any of the four actions that left, and PRD 1.5 requires no new grant from this row. merge-gate's cross-role git.merge.protected grant is row 149's. ## Round 2: Jason's ruling Q169 Q169, verbatim: "The only agent that should be able to remove the assistant role is the chief of staff. The COS and the user should have the ability to (re)configure the assistant role and seat name. The user via WebUI or CLI, the COS via the proper tooling." - **The hard wait (kept from the first round 2).** `#hardWait(s, action, target)` (broker.mjs) is true for a GATED action, or for a role.revoke whose target's definition is in END_PROTECTED. `#classification` returns `gated` when it holds, and decision.raise's `arbitrated` check uses it in place of round 1's `GATED.has`. A raise to revoke the assistant goes to the human at every class, cited or not, so no arbiter resolves one. A role granted role.revoke cross-role or within-role can't route it to the arbiter or approve its own. - **The protection, new.** `#protectedEnd(s, target, r)` refuses `role-end-refused` for an END_PROTECTED target unless one of two things holds: - the caller's definition is `chief-of-staff` (STAFF_TO); - the cited resolution was recorded `via 'cli'`. decision.resolve records `cli` only for a human session that passed `#human(s)`. It runs in two places: - on the role.end reason path, after the existing refusal of the caller's own run, which stays; - in #checkAuthority for role.revoke, after the decision-not-approved check. So a revoke citing a decision an arbiter resolved is refused. One raised before the hard wait existed, or by a build that routed it to the arbiter, is refused the same way. - **Execution is row 155's.** The assistant's runs hold thread claims (row 121). The chief's end finds no role claim and refuses `not-held`. A cited revoke refuses `decision-mismatch`, because decision.raise records `holderRun` null and #checkAuthority reads undefined. Per sage-0528, 139 doesn't fix that. The tests assert only that the protection lets these through. - **Every other cited revoke** routes by REQ-DEC-2 and executes, unchanged from round 1. - **Configuration** (the second half of Q169) is row 155's. PRD 1.6 records what it covers. - **PRD 1.6.** It adds: - the status line; - a 1.6 source block quoting Q169; - text in REQ-ROLE-2, REQ-ROLE-4 and REQ-WAIT-1; - a Notes item. The Mos note from the first round 2 is gone. The text is recorded as Jason's ruling Q169. PRD-1.6.md has the text and four readings for Mos to confirm. - **Filbert's note 1** (a within-role role.revoke grant lets a role approve its own revoke of a non-assistant role) stays a PRD question (sage-0523). - business `classify` (resolve.mjs) isn't changed. It takes no target, and its only caller is the WebUI's per-action authority view (packages/webui/src/reads.mjs:154), which routes nothing. ### Files outside the brief's list (accepted, sage-0539) Each follows from Q169 and PRD 1.6: 1. **packages/tasks/requirements.json and packages/tasks/tests/requirements.test.mjs.** The test checks the PRD status line's version against requirements.json, so both go to 1.6. 2. **packages/harness/tests/runner.test.mjs and packages/harness/README.md.** The chief's end_run of the assistant now gets past the protection and refuses `not-held`. Before, it refused `role-end-refused`. 3. **packages/bus/tests/chief-of-staff.test.mjs.** It changes for the same reason: the chief ending itself stays `role-end-refused`, and ending the assistant is `not-held`. The end_run description in packages/harness/src/tools.mjs still says the assistant can't be ended. It's stale, and row 155 owns the fix (sage-0539). ## Tests - packages/bus/tests/hard-waits.test.mjs (new, 12 tests: 7 from round 1, 5 for Q169): - the lists match each other and the vocabulary; - the Broker refuses each hard wait at either class; - each hard wait goes to the human uncited, cited, and cited with a domain, and the arbiter's resolve gets `human-required`; - each action that left goes uncited to the human, cited to the technical or delivery arbiter; - an arbiter-approved cited role.revoke authorizes the raiser, and the human's resolve gets `agent-required`; - merge-gate's cited merge goes to the technical arbiter (cross-role, resolved via 'broker'); a coder's cited merge goes to the arbiter as `gated`, and uncited to the human; - a stale or unconfirmed citation is refused with no decision stored; an arbiter raising a cited policy.change of its own goes to the human. - Q169 (sage-0528's four, then one kept from the first round 2). The first and third assert the protection only, because execution is row 155's: 1. The chief's role.end of the assistant refuses `not-held`, not `role-end-refused`. The chief ending itself, and `lead` (granted role.end) ending the assistant, refuse `role-end-refused`. No action.allowed event is written. 2. A cited revoke of the assistant goes `gated → human`. For a decision an arbiter resolved, the test uses a cited revoke of pm, which went to reviewer, who resolved it yes. A second Broker on the same store then changes pm's definition to `assistant`. The coder's revoke citing that decision refuses `role-end-refused`. The decisions table is append-only, so the test changes the business, not the row. 3. The human resolves a cited revoke of the assistant; the arbiter's resolve first gets `human-required`. The coder's revoke citing it gets past the protection and refuses `decision-mismatch` (row 155). 4. A cited revoke of pm goes `gated → reviewer`, the reviewer resolves it, and the revoke returns `{ revoked: true }`. 5. A role granted role.revoke cross-role, and one granted it within-role, still send a revoke of the assistant to the human. The within-role one doesn't resolve itself, and its revoke gets `decision-not-approved`, not `decision-mismatch`. Their revokes of pm go cross-role to the arbiter and within-role to themselves. - packages/bus/tests/chief-of-staff.test.mjs: REQ-COS-1 now expects `not-held` for the chief's end of the assistant. Its own run stays `role-end-refused`. - packages/harness/tests/runner.test.mjs: end_run's second refusal is `not-held`. - packages/tasks/tests/requirements.test.mjs: the version literal is 1.6. - packages/business/tests/hard-waits.test.mjs (new, 3 tests): GATED_ONLY is the list; every action is routed or a hard wait, never both; the validator refuses each hard wait at either class and accepts the actions that left. - packages/bus/tests/spec-fixtures.mjs: `fixture()` takes `arbiters` and extra `roles`, and returns `agent`. Existing callers are unchanged. - scripts/test-land-work.sh: two cases, `not-human:arbiter-cross` (merge-gate raised cross-role, cto resolved via broker) and `not-human:arbiter-cited` (coder raised gated with a citation, cto resolved). Both exit 2 `not-human`. scripts/land-work/read.mjs is unchanged (sage-0516). ## Left out, as the brief and sage-0512/0516 say 1. **REQ-LAND-1's refusal of a `gated` merge decision, and its coder test** (coder raises a cited merge, the arbiter approves, the land refuses). This ships with whichever of 137 and 139 lands second. On 2026-10-16 at 18:31Z, `git log 5661bd233..origin/refactor` is empty: row 137 hasn't landed, so 139 doesn't carry it. If 137 lands first, 139 needs a round 2 that adds it. 2. **The host land op accepting an arbiter-resolved merge decision**, in 137's shared module. This also ships with whichever of 137 and 139 lands second. The module isn't on this base. 3. **The chief-of-staff grant to set instance names** (row 141, queued). Whichever of 139 and 141 lands second adds it. Until then, names are set only by the human through the CLI. ## For the reviewer - **role.revoke can now be granted within-role.** A within-role raise resolves itself, so a role file granting it would let that role approve its own revoke. REQ-DEC-2 and REQ-WAIT-1 allow this, and REQ-ROLE-2's "resolved decision for a revoke" still holds. No shipped role grants it. A file that adds it is a reviewed commit. - **role.launch where launch.by isn't the caller.** It still classifies `gated`, but it isn't a hard wait, so a cited raise now goes to the arbiter. REQ-DEC-2 reads that way. I didn't add a carve-out. - **"A change to the hard-waits list, or a grant of a hard-wait action, is itself a hard wait" (REQ-WAIT-2).** The list lives in code and changes only by a reviewed commit to vocabulary.mjs and broker.mjs. The validator and the broker refuse any role file that grants a hard wait. No decision can change either, so the broker has no decision action to classify for them. policy.change is routed. A policy.change decision can't edit the list, because the list isn't policy data. - **git.push.protected and force pushes.** REQ-WAIT-1 says git.push.protected covers any force push or history rewrite on any branch. I grepped packages/cli/src/git-socket.mjs and broker.mjs's push path for a non-fast-forward check and found none. I didn't trace the whole push path. It isn't in row 139's brief or files, so it's a follow-up question for Sage, not a change here. The PRD's notes answer part of it: the host refuses a non-fast-forward push to the working repository (packages/seat/src/working.mjs). ## Follow-ups (not row 139's files) - **A role.revoke or chief's end of the assistant can't execute.** Row 155 (#1649, Darkwing, after 139 and 141) fixes it; sage-0528 keeps it out of 139. Since row 121 the assistant's run holds a thread claim, not a role claim. decision.raise records `holderRun` as null, and #checkAuthority compares that with `#holder(...)?.holder_run`, which is undefined, so a cited revoke refuses `decision-mismatch`, then `not-held`. The chief's end refuses `not-held`. - **The end_run tool text is stale.** packages/harness/src/tools.mjs:156 (comment) and :158 (description) say the assistant can't be ended. Since Q169 the chief can, and the broker refuses `not-held` until row 155. Row 155 owns the text (sage-0539). - **The harness raise_decision tool has no `domain` parameter** (packages/harness/src/tools.mjs:195-208), and decision.raise defaults to `delivery`. No live agent reaches the technical arbiter, which REQ-ROLE-5's merge-gate raise needs. Row 149 (Dewey) takes it; Sage adds it to row 149's brief at its next amendment (sage-0528). - **role.revoke with no `target`.** The holder lookup throws a non-BusError, which surfaces as `storage-refused` instead of a validation refusal. This predates 139; the bus test passes a target. - **land-work's fixture** (scripts/land-work/fixture.mjs) can't set spec_path/spec_commit, so the `arbiter-cited` case has no citation columns. land-work doesn't read them, so the case still tests what it should. - **Contract text.** roles/pm.md:42-43 and roles/chief-of-staff.md:41-46 still say anything ungranted "is gated" and goes to the human, and the chief text lists policy and PRD changes among them. Under REQ-DEC-2 a cited raise goes to the arbiter. That text is row 149's (proactive contracts). - **The harness raise_decision description** (packages/harness/src/tools.mjs:195) doesn't mention that a spec citation changes the route. Harness text isn't in row 139's files. ## PRD sentence (sage-0545) Mos approved the 1.6 text with two sentences added to REQ-ROLE-2 after "the chief of staff is the only agent that removes it.": "The chief of staff removes it by ending its runs, and that end needs no decision. A revoke of the assistant, the chief's included, is a hard wait under REQ-WAIT-1." They went in verbatim, because the candidate's broker.mjs already does both: - **The chief's end needs no decision.** A `role.revoke` with `reason` (line 1168) consumes `role.end` (line 1171). `role.end` is in ROUTED (line 55), so it isn't in HARD_WAITS or GATED (lines 62-63), and `#hardWait` only adds `role.revoke` (line 494). For a chief granted `role.end` within-role (REQ-COS-1), #checkAuthority returns with no decision (line 659). `#protectedEnd` (line 1173) passes for the chief's definition (line 502). Past that, the end refuses `not-held` until row 155. - **The chief's revoke is a hard wait.** `#hardWait` (line 494) reads the target's definition, not the raiser, so `#classification` returns `gated` for anyone's raise (line 508). Line 1232 keeps a hard wait from the arbiter, cited or not, and line 1233 routes it to the human. - **A nuance, not a mismatch.** In #checkAuthority (line 681), `#protectedEnd` passes the chief whatever recorded the resolution. A revoke decision the chief raised while the target's definition wasn't protected, and an arbiter resolved, would pass the protection for the chief after the definition changed. Another role's would be refused (Q169 test 2). Changing a definition is configuration, which is row 155's. - **Not tested directly.** No test raises a revoke of the assistant as the chief. Q169 tests 2 and 5 raise it as coder, lead and boss, and the path has no raiser check to differ on. Sage scoped this round to the PRD (sage-0545), so no test was added. Reruns on the edited candidate, each file singly, 2026-10-16T19:15:11Z, outputs in r2/prd/: - packages/bus/tests/requirements.test.mjs: 3 pass, 0 fail. - packages/tasks/tests/requirements.test.mjs: 3 pass, 0 fail. - The candidate's new tests (both hard-waits files) don't read docs/prd/mosaic-stack.md, so there was nothing else to rerun. A grep of packages/*/tests and scripts/test-*.sh found only those two. repro.sh on the new patch, in a fresh worktree at base.txt: DIGESTS_OK. The worktree was removed. ## Gate, round 2 (Q169) Run under Sage's slot (sage-0533, sage-0539; the slot was Dewey's from 19:06Z), from the worktree at the round 2 candidate, with COMPOSE_PROJECT_NAME=dewey-r139. The script is r2/gate.sh, and the outputs are in r2/gate/. r2/gate/summary.txt has each run's exit code and times. - 2026-10-16T19:06:10Z to 19:08:58Z: all 14 runs rc=0, with no flakes. - scripts/test-land-work.sh: 86 passed, 0 failed, including `OK arbiter-cross: exit 2, not-human` and `OK arbiter-cited: exit 2, not-human`. - bus 306/306, business 112/112, harness 105/105 and tasks 97/97, 0 skipped. - test-task selftest 98 passed, 0 failed. - auth, conductor, config, discord, extension-package, foundation, queue and release: rc=0. - dewey-r139_default was removed with 0 containers on it. ## Checks, round 2 - **Before the gate, as built** (19:02:52Z to 19:03:20Z), each file run singly with 0 skipped: - bus: agents, broker, chief-of-staff (13), approval-binding, decisions, hard-waits (12), project-runtime, requirements, single-use, task-page, tester, replies, message-citation, threads, transport, spec-socket, decision-route, decision-close, messaging and spec. - **Two runs outside the slot**, before sage-0533 gave it: the tasks suite (97 pass) and packages/harness/tests/runner.test.mjs (24 pass). Sage was told in dewey-0534. Neither used Docker. Nothing collided: Darkwing had cleared, and Researcher was idle (sage-0539). - **The busy check**, by /proc cwd links only: - At 19:04:08Z, no node, mutant or gate processes from another seat. There were three bash shells, with no node, in ~/filbert-scratch/r137-r1. - In a watch from 19:04:25Z to 19:05:42Z (30 scans), none. - No dewey-r139 containers or networks. - **An overlap in the slot (sage-0542).** From about 19:06:14Z to 19:06:50Z, Filbert ran single test files in ~/filbert-scratch/r137-r1: seat working.test and cli git-socket.test, with probes. It wasn't a suite, a gate or Docker. Sage's single-file allowance in sage-0537 and its late correction caused it, and Filbert has stopped. My watch had ended at 19:05:42Z, so it didn't see the overlap. These gate steps ran in that window: - land-work, to 19:06:22Z; - bus, 19:06:22Z to 19:06:36Z; - business, 19:06:36Z to 19:06:38Z; - harness, 19:06:38Z to 19:06:51Z. All of them passed, and the gate runs no seat or cli tests, so there was nothing to rerun. ## Mutants, round 2 (Q169) r2/mutants/summary.txt; the script is r2/mutants.py. They ran from 2026-10-16T19:09:11Z to 19:10:46Z, under the same rules as round 1: - each mutant ran in its own process group, killed as a group on a 600-second timeout; - the file was restored and its hash checked after each one. m1 to m5 are round 1's, with m1 and m2 retargeted at `#hardWait`. All nine were killed: | Mutant | Change | Suite | Tests that failed | | --- | --- | --- | --- | | m1 | drop `spec !== null` | bus (13 fail) | both REQ-DEC-2 citation tests, and older tests that expect an uncited gated raise at the human | | m2 | drop `!this.#hardWait(...)`: a cited hard wait goes to the arbiter | bus (5 fail) | REQ-DEC-2 hard waits; three Q169 tests; spec.test's current-citation test | | m3 | add `account.create` to the broker's ROUTED | bus (3 fail) | REQ-WAIT-1, REQ-WAIT-2, REQ-DEC-2 hard waits | | m4 | add `spend` to the vocabulary's ROUTED | business (2 fail) | REQ-WAIT-1, REQ-WAIT-2 | | m5 | disable role.mjs:41's refusal of a hard-wait grant | business (3 fail) | REQ-WAIT-2, plus two older role tests | | m6 | `#hardWait` drops the assistant check | bus (3 fail) | Q169 tests 2, 3 and 5 | | m7 | `#protectedEnd` drops the chief exemption | bus (2 fail) | Q169 test 1; REQ-COS-1 | | m8 | `#protectedEnd` drops the human (`via 'cli'`) exemption | bus (2 fail) | Q169 test 3; REQ-ROLE-2 REQ-THREAD-1's approved-revoke test | | m9 | #checkAuthority no longer calls `#protectedEnd` for role.revoke | bus (1 fail) | Q169 test 2 | ## Gate, round 1 Run under Sage's slot (sage-0518), from the worktree at the round 1 candidate, with COMPOSE_PROJECT_NAME=dewey-r139. gate-summary.txt has each run's exit code and times. - 2026-10-16T18:28:41Z to 18:31:11Z: all 12 runs rc=0, with no flakes. - scripts/test-land-work.sh: the two new cases print `OK arbiter-cross: exit 2, not-human` and `OK arbiter-cited: exit 2, not-human`. - bus 301/301 and business 112/112, 0 skipped. - test-task selftest 98 passed, 0 failed. - auth, conductor, config, discord, extension-package, foundation, queue and release: rc=0. - The 19 affected test files also passed run singly, 0 skipped (18:25:46Z to 18:26:03Z): - bus 10; - business 7; - harness bundle and tools, since both read ACTIONS. - dewey-r139_default was removed with 0 containers on it. ## Mutants, round 1 mutants-summary.txt. Each mutant changed one file and reran its owning suite in its own process group. The file was restored and its hash checked before the next mutant. All five were killed: | Mutant | Change | Suite | New tests that failed | | --- | --- | --- | --- | | m1 | drop `spec !== null`: an uncited gated raise goes to the arbiter | bus (14 fail) | both REQ-DEC-2 citation tests, and 12 older tests that expect an uncited gated raise at the human | | m2 | drop `!GATED.has(...)`: a cited hard wait goes to the arbiter | bus (2 fail) | REQ-DEC-2 hard waits; spec.test's current-citation test | | m3 | add `account.create` to the broker's ROUTED | bus (3 fail) | REQ-WAIT-1, REQ-WAIT-2, REQ-DEC-2 hard waits | | m4 | add `spend` to the vocabulary's ROUTED | business (2 fail) | REQ-WAIT-1, REQ-WAIT-2 | | m5 | disable role.mjs:41's refusal of a hard-wait grant | business (3 fail) | REQ-WAIT-2, plus two older role tests |