Files
jason.woltjeandClaude Sonnet 5.5 d10a1a4677 researcher: row 140 packet, the pm asks the chief of staff for staff and the chief launches within limits (#1634)
Candidate on dda7e31b0: staff.request verb (pm only, STAFF-REQUEST class no other writer can produce),
request_staff pm tool and prompt line, pm and chief-of-staff contract text, 7 bus tests, tools and bundle
tests, one launcher end-to-end test. 21/21 mutants killed; node suites and all ten scripts/test-*.sh green.
Packet only; the candidate itself is row140.patch and is not applied to the tree. Nothing pushed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-15 18:05:45 -05:00

13 KiB
Raw Permalink Blame History

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: <chief>, staff_for: <task ref> 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 <worktree> <outdir>, 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 <worktree> <outdir> (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.