# Row 123 (#1617): the chief of staff ends runs and changes the organization file Seat: Researcher. Reviewer: Filbert (broker authority). Gate owner: Sage. Requirements: REQ-ROLE-2, REQ-ROLE-3, REQ-LIMIT-1, REQ-COS-1. Brief: `docs/plans/2026-10-14_prd-1.2-build.md` build step 6. **Base: 0bdd573ff + row 49 r1, candidate manifest 87ef1321a1a08be64a20499889fc24b5549b47d6d33abfd86db95f62fb3cb6d0** (Rocko's packet `agents/rocko/work/queue-49/r1/`, applied uncommitted). `row123.patch` is a diff of my changes only, against that combined tree (13 files, +685 −34). It applies with `git apply --cached --check` on base + row 49 r1, and the result round-trips to the candidate tree. Nothing is committed in the worktree, nothing is pushed, and no live store, unit or `~/.mosaic` file was touched. ## What the row does 1. **`role.end`**: a new action. A role whose file grants it (only the chief of staff) ends another role's run with `role.revoke {role, reason}`: a non-empty `reason` of at most 500 characters and **no `decision`**. The reason is written to `role_claims.reason` and to the `action.allowed` event body (`class: within-role`, `reason`). `role.revoke` itself stays gated for every role (decision 46), so any other role still needs a resolved decision. 2. **`organization.change`**: a new action. A role whose file grants it calls the new broker verb `organization.set`, which takes exactly the keys of row 122's write path and goes through the same `launchSettings.set` writer the human's `limits.set` uses (same validation, same history receipt). 3. Schema 3c → 3d: the `role_revoke_needs_decision` trigger accepts a revoke with a reason and no decision. 4. The broker has no role-name checks for the grants. The grant lives in `roles/chief-of-staff.json`. The only name the broker knows is `END_PROTECTED = ['assistant']` (a definition name) in the refusal below. ## Files | File | Change | |---|---| | `packages/business/src/vocabulary.mjs` | `role.end`, `organization.change` added to the action vocabulary | | `roles/chief-of-staff.json` | `withinRole`: `message.send`, `role.launch`, `role.end`, `organization.change`. `role.revoke` is not granted | | `packages/bus/src/broker.mjs` | `ACTIONS` gets both. `ORGANIZATION_KEYS` (one constant). `organization.set` beside `limits`/`limits.set`. The `role.revoke` reason form. `#claimEnd` stores the reason | | `packages/bus/schema.sql` | trigger `WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL`; sha256 `e4f1de9e…39b5` | | `packages/bus/src/migrations/3c-3d.sql` (new) | `DROP TRIGGER` + `CREATE TRIGGER` with the new text, nothing else | | `packages/bus/src/store.mjs` | `SCHEMA_SHA256`, `SCHEMA_VERSION = '3d'`, one more entry in Rocko's `migrations` array | | `packages/bus/tests/fixtures/schema-3c.sql` (new) | Rocko's schema.sql as it stands in row 49 r1 (sha 7587ac1d…eebdb) | | `packages/bus/tests/comment-migration.test.mjs` | **Eight lines in a row 49 file, with the reason: see below** | | `packages/bus/tests/chief-of-staff.test.mjs` (new) | 12 tests | | `packages/business/tests/chief-of-staff.test.mjs` | approved list gains the two actions; `role.revoke` asserted gated | | `packages/harness/tests/tools.test.mjs` | the chief's `untypedActions` is now `["role.end", "organization.change"]` (see Follow-ups) | | `packages/bus/README.md`, `packages/business/README.md` | verb rows, chief-of-staff paragraph, 3c→3d sentence | ### Eight lines in a row 49 file, and why `packages/bus/tests/comment-migration.test.mjs` is Rocko's. Three of its tests pin the current version as `'3c'` (the "ready" string, the pinned version, the future-version fixture). Adding the 3c→3d step makes `3d` current, so those three fail with no change to Rocko's step. I changed 8 lines (8 deleted, 8 added): `'3c'` → `'3d'`, the future fixture `'3e'`, the copy list gains `3c-3d.sql`, the expected line is `bus schema 3d ready`, titles updated. Sage accepted this for row 123 and posts a note on #1535. If row 49 goes to round 2, Rocko makes that test independent of the latest version, and I rebase and drop my edit. If row 49 lands as is, the edit rides with row 123. No other row 49 file is changed. ## Every field the chief of staff can change through `organization.set` Condition 1. This is the whole list, and it is the same list `limits.set` takes from the human (`ORGANIZATION_KEYS`, `broker.mjs:99`, used by both verbs): | Key | Rule | |---|---| | `revision` | **required** concurrency token; a stale one is refused by the writer | | `limits` | counts such as `limits.launch` (row 122's write path); the writer validates | | `models` | the model table | | `instances` | per-instance model selections | Nothing else. Fields outside the write path are refused with `invalid-request` before the writer is called, for the chief exactly as for the human: `launch` (so `launch.by`), `human`, `arbiters`, `roles`, `actor`, and the literal key `'launch.by'`. Credential references and plugins are not in the write path either. Arbiters are #1627 and not started. Test: **`REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set takes only the human's keys`** (`chief-of-staff.test.mjs:206`). It runs each outside key as `chief organization.set` and `human limits.set`, expects `invalid-request` in every case, and checks that the writer received no call. Mutants m10 and m11 fail it. The writer is told the actor `chief-of-staff.chief` (definition + instance, matching `ID_PATTERN`), so the receipt in `businesses/.history//changes.jsonl` names the chief, not a human. ## Storage now accepts a revoke with a reason and no decision Stated plainly, as required: **after 3d, SQLite accepts an `op='revoke'` row with a reason and no decision.** The trigger aborts only a revoke with neither. Storage no longer enforces "a decision is required" for `role_claims`. **Only the broker decides who may revoke that way.** The broker refuses the reason form unless the caller's role file grants `role.end`, and it checks before any insert. Tests at the broker, for Condition 2 and 3: - `REQ-ROLE-2: a role without role.end still needs a resolved decision to revoke` (line 98): pm, coder and assistant sending `{role, reason}` get `decision-required`; sending `{role}` gets `invalid-request`; the `human` gets `agent-required`; `reader` and `tester` are refused; the `role_claims` row is unchanged and no `role.end` event is logged. This is the non-chief reason-without-decision case refused before any insert. Mutants m1 and m16 fail it. - `REQ-ROLE-2: a reason is required, bounded, and never sent with a decision` (line 152): empty, non-string and over-500 reasons and an extra key are refused (the 500-character bound is inclusive), and a `reason` together with a `decision` is refused for the chief too (`invalid-request`), with the claim row unchanged. Mutants m2, m3. - `REQ-COS-1: the chief of staff cannot end its own run or the assistant's through role.end` (line 167): `role-end-refused` for both; nothing inserted. Mutants m4, m5. - `REQ-ROLE-2: storage accepts a revoke with a reason or a decision and aborts one with neither` (line 112): the schema rule alone. Mutants m19, m20. ## Acceptance checks against the brief | Brief check | Test (file `packages/bus/tests/chief-of-staff.test.mjs` unless stated) | |---|---| | chief ends a run without a decision, logged with its reason | line 80 (`role_claims.reason` and the `action.allowed` body) | | another role still needs a decision | line 98 (refused), line 132 (a resolved decision still ends the run, reason null) | | chief change validated, written, logged like a human's | line 234, runtime test with the real writer: same receipt shape as `limits.set` | | invalid change refused | line 234 (negative, non-numeric and unknown limits, unknown model, unknown instance, empty model table, stale `revision` → `revision-conflict`; file bytes and receipts unchanged) and line 206 (outside keys) | | `role.revoke` gated for every role, the grant is the chief's file | `packages/business/tests/chief-of-staff.test.mjs:86` | | upgrade of a real 3c store | line 343, below | ## Migration 3c → 3d Built on Rocko's engine and version scheme, no second scheme: a `{from:'3c', to:'3d', sql}` entry in `migrations`. The engine validates digest, version, WAL, timestamps and integrity per step, runs the step in a transaction, and verifies the target digest. The step is two statements (drop and recreate one trigger) and touches no row. Test `REQ-ROLE-2, REQ-COS-1: the 3c→3d migration lets the chief of staff end a held role with a reason; neither is still refused` (line 343): the 3c database is not hand-built DDL. It starts from Rocko's pinned 3b fixture (`schema-3b.sql`, with an event and a claim row) and goes through Rocko's real `3b.sql` and `3b-3c.sql` steps, run by a copy of `store.mjs` with only the 3c→3d entry removed and `schema-3c.sql` as its schema (so the 3c digest is pinned by `schema-3c.sql`). The test then shows the old trigger refuses a reason-only revoke, a 3d `Store` refuses the 3c database (`schema-mismatch`) until the explicit step runs, and after the step the version is `3d`, the event and claim rows are preserved, the chief ends the held role with a reason (stored, decision null), and a revoke with neither is still aborted. Mutants m6, m16, m19, m20 also fail it, and m21 fails at import. Cutover: no seat touches a live store. Mos's single re-pin window takes the live store 3b → 3c → 3d with `node /packages/bus/src/migrate-schema.mjs ` (Rocko's `MIGRATION.md` steps, unchanged). The bus schema changed, so this row needs the re-pin; it should ride in the same window as row 49. ## Mutants (one per check, each fails a test) 21 mutants, run one at a time in their own process group with a whole-group kill on timeout. Controls: bus 12/12, business 10/10. Every mutant exits 1. Log `mutants-run.txt`; per-mutant `.patch`, `.out`, `.rc` in `mutants/` (the `.patch` header paths name the runner's temp backup directory, which is gone). | # | Mutant | Fails | |---|---|---| | m1 | reason form skips the `role.end` check | 3 bus tests | | m2 | reason and decision together accepted | reason/decision test | | m3 | reason unbounded | reason test | | m4 | chief may end its own run | self/assistant test | | m5 | chief may end the assistant's run | self/assistant test | | m6 | reason not stored | 4 tests incl. migration | | m7 | reason not logged | chief-ends-run test | | m8 | decision path checks `role.end` instead of `role.revoke` | decision-path test (line 132) | | m9 | `organization.set` skips the check | 2 tests | | m10 | key list widened with `launch` | outside-keys test | | m11 | `revision` optional | outside-keys test | | m12 | wrong actor passed to the writer | actor test, real-writer test | | m13 | `limits` open to agents | non-grant refusal test | | m14 | writer failure no longer maps to `invalid-request` | real-writer test | | m15 | vocabulary drops `role.end` | 8 business tests | | m16 | chief file drops `role.end` | 4 bus tests | | m17 | chief file drops `organization.change` | 3 bus tests | | m18 | chief file grants `role.revoke` | 9 business tests | | m19 | trigger back to decision-only | 5 bus tests | | m20 | trigger never fires | storage and migration tests | | m21 | 3c→3d step file empty | file fails at import (`migration-target-mismatch`); a step that does not reach the pinned schema cannot ship | m8 survived my first run (11/11). The first draft had no test of the decision path with a resolved decision. I added the line-132 test and re-ran all 21 for this log. ## Targeted gate (decision 136), run in the q123 worktree 2026-10-15T16:27Z | Suite | Result | |---|---| | business | 88/88 | | bus | 190/190 | | tasks | 97/97 | | cli | 195/195 | | webui | 112/112 | | harness | 84/84 (first run 83/84, see below) | | seat | 132/132 | | `scripts/test-land-work.sh` | 84 passed, 0 failed | Logs in `suites/`, summary `suites-run.txt` (shows the first harness failure; `suites/harness.txt` is the rerun). First harness run failed `the chief of staff gets launch and send_message, and no task verbs` (`tools.test.mjs:41`): it asserts `untypedActions(chief, tools)` is empty, and the chief now holds two actions with no typed harness tool. I changed that assertion to `["role.end", "organization.change"]` with a row 123 comment. I did not add harness tools. The gate created no Docker networks (22 before, 22 after, same names). Sage's full gate at the landing target still applies. ## Follow-ups (not in this row) - **No harness tool yet.** The chief's session cannot call `role.revoke {role, reason}` or `organization.set` through a typed tool; the manifest lists both as untyped. The brief's row 123 does not name harness tools, and adding them widens the row. A harness row is needed before the chief can use either from a session. Sage decides. - `packages/harness/src/bundle.mjs:92` still tells every role "Only the human stops a run, with `mosaic stop`" and to raise a `role.revoke` decision. That is wrong for the chief once it has a tool. Change it with the tool row. - Row 49 round 2: rebase, drop the eight-line edit if Rocko makes his test version-independent. - #1627 (arbiters) not started. ## Verify ```sh cd sha256sum -c packet-manifest.sha256 git apply --check row123.patch # on base 0bdd573ff + row 49 r1 sha256sum -c candidate-manifest.sha256 # from the repo root of the candidate tree (13 files) ``` `mutants.sh` and `suites.sh` are the runners; both take a worktree and an output directory.