# Row 84 (#1578): M1 chief-of-staff role file, contract and tests, candidate packet, round 1 Author: Rocko. Reviewers: Darkwing and Filbert. Gate: Sage. Brief: `docs/plans/2026-10-12_m1-chief-of-staff-role.md` (blob `da2a7b7e`), section "M1 chief-of-staff role file, contract and tests" (lead decision 104). Base: `4918d410` (`base.txt`). The packet is uncommitted. I made no commits, pushes or Gitea calls, and read no token. Every test uses a fixture business in a temporary directory; nothing touched Jason's business file or anything under `~/.config/mosaic-dev`. ## Files (`files.txt`, `candidate-manifest.sha256`) | File | Change | |---|---| | `roles/chief-of-staff.json` | new, roleVersion 2, decision 104's list | | `roles/chief-of-staff.md` | new, the contract, in the style of `roles/pm.md` | | `packages/business/tests/chief-of-staff.test.mjs` | new, 5 tests | | `packages/business/README.md` | new subsection "Chief of staff instance" after "Business file" | | `agents/rocko/work/queue-84/mutants.py` | the mutant rerun: `python3 mutants.py ` | No source file changes. Nothing in the repository lists `roles/` by directory, so the new file changes no other suite. ## The role file Exactly the brief's list: name `chief-of-staff`, title "Chief of staff", contract `chief-of-staff.md`; tools `read`, `grep`, `find`, `ls`; network `api-only`; `authority.withinRole` `["message.send"]`; `crossRole` `[]`; `credentials` `[]`. ## The contract It names the role, not the persona. It says the holder sends the human's requests to the PM as messages and asks the PM for work; relays rulings verbatim and asks the human when one is unclear; never resolves a decision for the human; never holds or imitates human authority; never routes around a refusal. One duty goes beyond the brief's four points and comes from the approved draft ("raises decisions to Jason"): bringing decisions that need the human to the human and carrying the answer back. The protocol section says the holder never holds a token and sends messages through the bus, as `pm.md` does. ## Tests `node --test packages/business/tests/chief-of-staff.test.mjs`: 5 pass. Each test writes the fixture business (`businessDoc` plus an instance `chief` with holder `mos`) mode 600 under a temp `config/businesses/` and loads it with `loadBusiness` and the repository's `roles/`. - The role file holds exactly the list (`loadRole`). - The instance loads and resolves; the resolved tools, network, authority and credentials equal the list; tracker and launch are null. - `classify`: `message.send` within; `task.create`, `role.launch` and every other action in `ACTIONS` gated. - A business can't grant more than the role file (below). - A `launch` block with `by: "chief"` refuses, naming the missing `role.launch`. ### One departure from the brief's wording The brief asks for "a business that grants the instance a gated action through its limits is refused". In the code, `limits.authority` is an allowlist that only narrows (`packages/business/src/resolve.mjs:66-69`, `vars.mjs:117-125`): naming `task.create` there is accepted and has no effect, so it can't refuse. The test covers both sides: - **No widening.** `limits.authority` at the business and agent layers naming `task.create`, `role.launch`, `deploy` and `message.external`, `limits.network` `open` and `limits.tools` with `write`, `edit` and `bash` resolve to exactly the list, and those actions stay gated. - **Refusals.** An instance with an `authority`, `tools`, `network` or `scopes` key refuses as unsupported; a Gitea credential reference refuses (the role needs none); a `tracker` refuses (no Vikunja); a `limits.authority` entry outside the vocabulary refuses. Making a gated action in limits refuse would be a change to `packages/business/src`, outside the files this row owns. If the reviewers want it, it's a follow-up. ## Mutants (`mutants.py`, `out/mutants.txt`) 13 mutants, each applied alone to the candidate tree, all killed: the role file with `write` added, `bash` added, `ls` dropped, network `open`, network `none`, `task.create` within, `role.launch` within, `withinRole` empty, a crossRole action, a Gitea credential; and in `business.mjs`, an instance allowed an `authority` key, the `launch.by` check removed, the credential map check removed. ## Gate (`out/gate/`, `out/gate.sh`) Sequential, in the scratch worktree at `4918d410` with the candidate and `node_modules` linked, full output kept per suite: | Suite | Result | |---|---| | business node (`packages/business/tests/*.test.mjs`) | 65 pass, 0 fail | | test-auth | 15 passed, 0 failed | | test-conductor | 17 passed, 0 failed | | test-config | 24 passed, 0 failed | | test-discord | 66 passed, 0 failed | | test-extension-package | 18 passed, 0 failed | | test-foundation | 44 passed, 0 failed | | test-queue | 27 passed, 0 failed (the canonical-root checks skip in a worktree, as always) | | test-release | 14 passed, 0 failed | | test-task | 98 passed, 0 failed | No `✖` or `not ok` in any output file. `rocko-r84_default` was removed after the Docker suites. The gate ran before the README entry changed from a placeholder to `mos` (below); no suite reads the README, and the business node suite was rerun after the change (`out/node-business-after-readme.txt`). ## For the reviewers - The README entry names the instance `mos`: Jason's Q120, lead decision 116, brief re-pinned at blob `da2a7b7e`. The tests use the fixture name `chief`. This row doesn't write the business file. - `examples/mosaic-stack.example.json` is not in this row's files, so it has no chief-of-staff instance.