Files
jason.woltjeandClaude Sonnet 5.5 2209ac4c17 docs: add Researcher row 123 packet (chief of staff ends runs and changes the organization file)
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]>
2026-10-15 11:39:36 -05:00

13 KiB
Raw Permalink Blame History

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/<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} 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 <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} 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

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.