Candidate on 0bdd573ff + row 49 r1 (manifest 87ef1321...), 21 mutants killed,
targeted gate green. Nothing landed or pushed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
13 KiB
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
role.end: a new action. A role whose file grants it (only the chief of staff) ends another role's run withrole.revoke {role, reason}: a non-emptyreasonof at most 500 characters and nodecision. The reason is written torole_claims.reasonand to theaction.allowedevent body (class: within-role,reason).role.revokeitself stays gated for every role (decision 46), so any other role still needs a resolved decision.organization.change: a new action. A role whose file grants it calls the new broker verborganization.set, which takes exactly the keys of row 122's write path and goes through the samelaunchSettings.setwriter the human'slimits.setuses (same validation, same history receipt).- Schema 3c → 3d: the
role_revoke_needs_decisiontrigger accepts a revoke with a reason and no decision. - 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 isEND_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/<id>/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}getdecision-required; sending{role}getsinvalid-request; thehumangetsagent-required;readerandtesterare refused; therole_claimsrow is unchanged and norole.endevent 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 areasontogether with adecisionis 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-refusedfor 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 <new-copy>/packages/bus/src/migrate-schema.mjs <absolute dataRoot> (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}ororganization.setthrough 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:92still tells every role "Only the human stops a run, withmosaic stop" and to raise arole.revokedecision. 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
cd <packet dir>
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.