# Row 140 (#1634): the pm asks the chief of staff for staff, and the chief hires and launches Seat: Researcher. Reviewer: Filbert. Gate owner: Sage. Brief: `docs/plans/2026-10-15_q167-north-star-rows.md`, the row 140 section (REQ-COS-1). **Base: dda7e31b0** (detached worktree `/mnt/storage/scratch/researcher/q140b`). Canonical HEAD was 276965d6d at packet time; the one commit after dda7e31b0 changes only docs and agents files (`git diff --name-only dda7e31b0 276965d6d` has nothing under `packages/`, `roles/` or `scripts/`). I first built on df5bbfb82 (row 142 round 1). HEAD moved: rows 144 and 145 landed and changed `packages/harness/src/bundle.mjs` and `tools.mjs`, so I rebased my diff onto dda7e31b0 before any mutant or gate run. It applied with one conflict, in `bundle.mjs`: both sides added lines after the reviews lines (row 145's web lines, my staff line). I kept both, web lines first. `row140.patch` is a diff of my changes only: 11 files, +354 −5. It passes `git apply --cached --check` on dda7e31b0 and round-trips to the candidate tree `e1dd2690111e9839038ffc6b0fbed1aac8a17736` (checked with a temp `GIT_INDEX_FILE`). `candidate-manifest.sha256` lists the 11 files (sha256 of the manifest: `df9e3fea692c6a5edf20b9fa9be3b8435ce851bba4bcffef1921688d8f34aaaf`; verify with `sha256sum -c` run in a tree holding the candidate). Secrets scan of the patch (token shapes, PEM headers, `password=`): no match. No binary content. Nothing is pushed, and no live store, unit, credential or `~/.mosaic` file was touched. I hold no Gitea credential; the in-review move and the review request are Sage's. ## What the row does The pm sends the chief of staff a staff request: a task and the roles it needs, with counts. The chief launches through `role.launch` within the organization's limits and answers by message with the instances it launched, or with the refusal and its reason. ### Broker: `staff.request` (`packages/bus/src/broker.mjs`) Args: `task_ref` (a task ref), `roles` (1 to 10 entries, each exactly `{role, count}`, `role` an identifier, `count` an integer 1 to 20, no role twice), optional `note` (up to 16384 characters), optional `decision`. Order of checks, in the code's order: 1. Shape: unknown or missing keys, a bad task ref, an empty or over-long role list, a bad role entry, a count outside 1..20 or not an integer, a repeated role: `invalid-request`. 2. Authority: it spends the caller's **`message.send`** grant (within the role, or a decision for cross-role), the same grant `message.send` spends. The `action.allowed` event names `operation: staff.request`, `target: `, `staff_for: ` and the roles. The field is `staff_for`, not `task_ref`, because `#event` refuses an `action.*` event carrying `task_ref` unless the event's subject is that task (`task-subject-required`); this event is filed under the chief it was sent to. 3. Sender: the caller's role definition must be `pm`, else `staff-request-refused`. 4. Recipient: the business's launcher (`launch.by`) must exist and be a `chief-of-staff` instance, else `no-chief-of-staff`. 5. `#checkMessaging`: the definitions' `messaging` lists apply, sender and recipient (`message-recipient-refused`, `message-sender-refused`). 6. It writes one message of class `STAFF-REQUEST` to the launcher through `#reviewMessage` (so it routes and queues like any message) and returns `{message, to}`. A refused request writes nothing: the broker request runs in one transaction, so the `action.allowed` event of step 2 rolls back with a later refusal. I checked this directly (a coder's `staff.request` was refused with `staff-request-refused` and its `action.allowed` count stayed 0). `review.request` has the same order. **Only the pm can send one, and nothing else can forge one.** `message.send` and `thread.send` keep their class lists (REQUEST, ASSIGNMENT, REVIEW-REQUEST, REVIEW-RESULT, RESULT, INFO, DECISION, REACTION and REQUEST, INFO, REACTION). `STAFF-REQUEST` is in neither, so `invalid-class` stops every other writer, the pm and the human included. No schema change: `messages.class` is free text and no event kind was added. **It grants nothing.** The pm still holds no `role.launch` (`roles/pm.json` is unchanged and the test reads the shipped file). The chief of staff launches with the `role.launch` it already holds, so every limit that already applies still applies: the organization's per-model caps, the instance list in `launch.instances`, the host's capacity refusals, the launcher's own checks. The request adds an input to the chief's decision; it adds no launch path. ### Harness - `packages/harness/src/tools.mjs`: a pm-only typed tool `request_staff` (verb `staff.request`, action `message.send`) with the same bounds in its schema (`roles` minItems 1, maxItems 10, items `{role, count integer 1..20}`). - `packages/harness/src/bundle.mjs`: one prompt line for a session that has the tool: it doesn't launch sessions; `request_staff` names the task and each role with how many; the chief of staff answers by message. - The chief of staff reads the request as an ordinary message turn and replies with the existing `message.send` (class RESULT, `in_reply_to`); no new chief tool. ### Role contracts (text only) - `roles/pm.md`: the last duty bullet says the pm doesn't launch sessions and asks the chief of staff with `request_staff`. - `roles/chief-of-staff.md`: one new bullet, "Answer a staff request": a STAFF-REQUEST from the pm names a task and roles with counts; no other role can send one; launch with `launch` within the same limits, one session per instance; reply with the instances launched and, for the rest, the exact refusal; a request never widens the limits. I kept my chief text to the staff request, as the brief says, because row 146 (Dewey) also edits this file. ## Files | File | Change | |---|---| | `packages/bus/src/broker.mjs` | `staff.request` case, four constants | | `packages/bus/tests/staff-request.test.mjs` | new, 7 tests | | `packages/harness/src/tools.mjs`, `bundle.mjs` | `request_staff`, the prompt line | | `packages/harness/tests/tools.test.mjs`, `bundle.test.mjs` | the pm tool list, a new tool test, a new prompt test | | `packages/cli/tests/launcher.test.mjs` | one end-to-end test (below) | | `packages/bus/README.md`, `packages/harness/README.md` | the verb row and the class sentence; the tool paragraph | | `roles/pm.md`, `roles/chief-of-staff.md` | contract text only | ### Tests - `staff-request.test.mjs` (7): pm to chief delivery and the chief's reply; **only pm** (coder and chief get `staff-request-refused`; `message.send` and `thread.send` with the class get `invalid-class` from coder, chief, pm and human; the human's `staff.request` gets `agent-required`; the reader `read-only`; the tester `tester-refused`; the chief receives nothing); a pm without `message.send` is refused like any gated action; the request goes to the launcher and only when that is a chief of staff (`launch` null, or `by` a coder, gives `no-chief-of-staff`); the messaging lists hold both ways; the shape bounds (19 bad inputs, 3 good ones); the request grants nothing (shipped `pm.json` lacks `role.launch`, shipped `chief-of-staff.json` has it, and the authorization result for both is unchanged by a request). - `launcher.test.mjs`: "a launched pm's request_staff reaches the chief of staff; no other session has the tool and the launch limits still hold". It uses the real bwrap runner (the sandbox ran; the test is skipped without one). A launched pm calls `request_staff` through the tool socket and the chief of staff receives the STAFF-REQUEST; a coder's tool list has no `request_staff`, and its attempt reads `refused: unknown-tool`; after the request nothing has launched, the next coder launch works, the reviewer launch is refused `capacity-full`, and no launch is attributed to the pm. That last part is the "chief's launch stays inside the organization's limits" evidence: the request neither launches anything nor changes what the chief's `launch` allows. - `tools.test.mjs` 18/18 and `bundle.test.mjs` 21/21 cover the tool's schema, its gate to the pm, and the prompt line only for a session that has the tool. ## Mutants `mutants.sh `, 2026-10-15T22:58:49Z, results in `mutants-run.txt`, one `.out`, `.rc` and `.patch` per mutant in `mutants/`. A dry run (`MUTANT_DRY=1`) first showed every edit matches its source exactly once on this base. Three controls (the staff-request, tools and bundle test files on the unmutated candidate) pass 7/7, 18/18 and 21/21. **All 21 mutants fail their test by a named assertion** (no timeout kills). The worktree was restored (`git diff` against the index is empty). | Mutant | What it removes or breaks | Killed by | |---|---|---| | m1 | the sender-must-be-pm check | staff-request: only-pm test | | m2 | the recipient must be a chief of staff | staff-request: launcher test | | m3 | a missing launcher no longer refuses | staff-request: launcher test | | m4 | the messaging-list check | staff-request: messaging-lists test | | m5 | `message.send` accepts STAFF-REQUEST | staff-request: only-pm test | | m6 | `thread.send` accepts STAFF-REQUEST | staff-request: only-pm test | | m7 to m9 | count maximum, count minimum, integer check | staff-request: shape test | | m10, m11 | role list maximum and minimum | staff-request: shape test | | m12 | a role named twice | staff-request: shape test | | m13 | the task ref check | staff-request: shape test | | m14 | extra keys in a role entry | staff-request: shape test | | m15 | the message goes to the sender, not the launcher | staff-request: delivery test | | m16 | the log no longer names the task (`staff_for`) | staff-request: delivery test | | m17 | the request spends a different action than `message.send` | staff-request: the no-message.send, only-pm and delivery tests | | m18 | `request_staff` offered to every role | tools.test | | m19 | the tool schema's count maximum | tools.test | | m20 | the tool calls the wrong broker verb | tools.test | | m21 | the prompt line goes to every role that can send a message | bundle.test | The launcher end-to-end test has no mutant of its own: the mutants above kill each check at the unit that holds it, and the launcher test is the integration check that those pieces meet. ## Gate `suites.sh ` (the row 142 script, unchanged), 2026-10-15T22:59:28Z to 23:03:08Z (`gate-started.txt`, `gate-finished.txt`, `suites-run.txt`, output per suite in `suites/`). All rc=0, in the worktree at dda7e31b0 plus the candidate. It runs every `scripts/test-*.sh` (10 of them) and the node suites: | Suite | Result | |---|---| | cli | 208/208 | | business | 106/106 | | bus | 259/259 | | plugins | 28/28 | | tasks | 97/97 | | webui | 113/113 | | harness | 102/102 | | seat | 135/135 | | scripts/test-auth.sh | 15 passed, 0 failed | | scripts/test-conductor.sh | 17 passed, 0 failed | | scripts/test-config.sh | 24 passed, 0 failed | | scripts/test-discord.sh | 78 passed, 0 failed | | scripts/test-extension-package.sh | 18 passed, 0 failed | | scripts/test-foundation.sh | 44 passed, 0 failed | | scripts/test-land-work.sh | 84 passed, 0 failed | | scripts/test-queue.sh | 28 passed, 0 failed | | scripts/test-release.sh | 14 passed, 0 failed | | scripts/test-task.sh | 98 passed, 0 failed | No flake (#258, #261, #262, #263) recurred (no match in any suite output). The slot was Sage's "slot clear" (sage-0406); a busy check by exit status, `/proc` cwd links only, read CLEAR before the mutants and again before the gate. Docker: the gate left one network, `q140b_default`, which is mine; I removed it with `docker network rm q140b_default` and the network list now equals the before-list (`suites/networks-before.txt`, `networks-after.txt`). ## Notes for the reviewer and for follow-ups - **Row 146 overlap.** Row 146 (Dewey) also edits `roles/chief-of-staff.md`. My change there is one new bullet; expect a rebase on whichever lands second. The sentence "until the broker gives you a verb" in that file is stale for `policy.change`; Sage asked Dewey to fix it in row 146, so I left it. - **The STAFF-REQUEST body wording is guidance, not enforcement.** The broker composes the body ("Launch what the organization's limits allow with launch, then answer this message with the instances you launched, or with the refusal and its reason."). The chief of staff is a model session; nothing makes it answer. What the row enforces is who may ask, the request's shape, and that the request grants no launch. - **The bounds 10 roles and 20 per role are request-shape bounds**, not capacity limits (out of scope per the brief). They stop a malformed or runaway request; the organization file and the host's launch enforce actual capacity. - **Carried from row 132, unchanged by this row:** the `change_organization` description says "replaces" while the broker merges per instance; a revision-only call is refused by the writer, not by the schema. Earlier S9/N6 follow-ups also still stand. ## Not covered - A live pm and chief of staff session exchanging a real request. The evidence is the in-process broker tests and the launcher test with the fake adapter; the row goes live in a re-pin. - Role grants beyond PRD 1.5, and capacity limits (out of scope per the brief). - Whether a real chief of staff model answers well; that is a prompt question, not a gate.