Files

20 KiB

Row 285 build notes: C2-08b creative and chief role changes

Seat: Scrooge. Reviewer: Webby. Gate owner: Sage. Issue #1790 (mos-1309 item 5). Brief: docs/plans/2026-10-20_case2-rows.md at 94c33a43c, row 285 section, gate under "### Gate". Base: 94c33a43c on branch scrooge/row285.

Status

  • The code, tests, fixtures and docs are written locally. No test has run. Sage's order puts row 252 first at the next CLEAR, and nothing runs during another seat's slot.
  • The Results section is empty until that run. Nothing below claims a pass.
  • Mos ruled the mos-1272 points in mos-1332 (18:55Z). mos-1333 (19:04Z) withdrew item 3, the business.mjs publish fence. See "Old tests" below.

What changes

  • The six creative roles (companion, ideas, research, proofreader, fact-checker, continuity):
    • withinRole is ["message.send", "library.write"].
    • credentials is [{"service": "gitea", "scopes": ["read:user", "write:repository"]}].
    • Each contract's Authority and Protocol lines describe the two library write tools, which are limited to .md files in supporting folders the user marked. The agent never holds a token.
  • The chief of staff:
    • credentials is [{"service": "gitea", "scopes": ["read:repository", "read:user"]}], with no library.write.
    • The contract says the broker holds that token and it can't publish or push.
  • vocabulary.mjs drops CREATIVE_KINDS and adds CREATIVE_ACTIONS (message.send, library.write) and CREATIVE_SERVICE (gitea).
  • role.mjs checkCreative refuses a creative role that holds any withinRole action outside CREATIVE_ACTIONS, any crossRole action, or any credential service other than gitea.
  • broker.mjs #giteaRole (:2138) also refuses the chief of staff with gitea-not-declared (sage-2921).
  • business.mjs does not change (mos-1333 item 1). The fence added in 5f81d0da0 is removed in a later commit, so the branch's business.mjs equals the base.

Rulings

  • webby-0164: PRD 1.15 rewrites REQ-LIB-6 so creative agents write .md files in supporting folders. The REQ-WAIT-3 routing line is the missing piece.
  • mos-1328:
    • library.write routing goes in PRD 1.16, which needs Jason's explicit yes (REQ-WAIT-2).
    • library.write stays out of ROUTED until 1.16 lands.
    • If Jason changes the bounds, row 285 gets a new brief.
  • sage-2915:
    • The scopes a role declares must equal what any token minted for that role holds, at the commit where 285 lands.
    • The creative scopes ["read:user","write:repository"] are approved.
    • The ROUTED edit gets its own commit, citing the paragraph.
  • sage-2921:
    • Refuse the chief in #giteaRole, with a gitea-grant.test case next to X-G1. broker.mjs (#giteaRole only) and gitea-grant.test join the manifest.
    • Grep the tests for the gitea-not-declared text and for pins on broker.mjs before in-review.
    • The ROUTED commit cites 1.16 (row #1796).
  • mos-1330:
    • Item 1: the fixture change gets its own commit. Only fixture data changes, and named tests cover a missing gitea ref.
    • Item 2: option A is approved. The live chief gets one gitea ref. Every repo business writer gets the ref.
  • sage-2937: if Mos rules (b), business.mjs joins the files owned, scoped to the refusal near :166 and clear of :283-291 (row 287, Megavolt).
  • webby-0174: each changed expected value carries Mos's ruling key. The load fence needs a design line, its own test, a mutant and a grep of the tests for the N10 text.
  • mos-1332: the mos-1272 rulings, items 1-5, cited under "Old tests". The ROUTED commit and the landing still wait on Jason's yes on PRD 1.16 (Q32, Vikunja 546). The build, the gate and Webby's review go ahead.
  • mos-1333: withdraws mos-1332 item 3, since the fence contradicted the brief (:1825, :1829, :1839, and row 287's chief publish ["books"] at :1998). Items 1, 2, 4 and 5 stand.
    • The fence leaves business.mjs in a new commit, no force-push. business.mjs leaves the manifest and its fence mutants are dropped.
    • business.test "row 285: a creative instance with identity and publish loads" is back in its 8c529fb4d form.
    • N10 takes option (a). See "Old tests" item 3.
    • A new named test: a chief with its gitea ref, identity and publish ["stack"] loads.
    • Row 287 has nothing to reshape. Its binding note below still applies.
  • sage-2946: the bus suite runs whole (broker.mjs is in the manifest and bus tests read roles/chief-of-staff.json), plus the cli example readers. Item 2 is answered below under "Scopes field required".

Scopes rule evidence (sage-2915 q2)

At 94c33a43c, exactly one path mints a seat token. That is the Gitea grant, broker.mjs:2420, where api.tokenCreate(user, tokenName) takes the default scopes. The default is SEAT_SCOPES ['read:user', 'write:repository'] (gitea-grant.mjs:29, :263).

The only other tokenCreate caller is the admin rotate (gitea-grant.mjs:331), which uses ADMIN_SCOPES.

The creative declaration therefore equals SEAT_SCOPES. The chief's read declaration would be narrower than a minted token. #giteaRole refuses the chief, so no grant path mints the chief a token at the landing commit.

Scopes field required (sage-2946 item 2, Megavolt's question)

role.mjs refuses a gitea credential entry with no scopes. {"service": "gitea"} goes through checkGiteaScopes (role.mjs:100-111), which calls requireDistinctList(scopes, "gitea scopes", ..., {nonEmpty: true}). util.mjs:42 refuses a non-array with "gitea scopes must be an array". Only a host entry may omit scopes (role.mjs:134). So every gitea entry names its scopes, and for a minted token they must equal SEAT_SCOPES (sage-2915).

Binding note for row 287 (C2-21)

  • The library read grant row must mint the chief's declared scopes (read:repository, read:user) and not SEAT_SCOPES.
  • It needs a test showing the chief's token lacks write:repository.
  • If it reuses #giteaRole, it has to lift the chief refusal and pass the declared scopes in the same change.

Live step (mos-1330 item 2, option A)

The live dragon-lin chief holds no credentials, and there are no creative instances. After row 285 lands, the chief entry in the live business file needs one gitea reference, or the file refuses at load with "must reference exactly the services its role definition needs (gitea; got none)".

Exact JSON, added to the chief instance (store form, the form the human's instances.set add writes):

"credentials": { "gitea": { "store": true, "rotateBy": "YYYY-MM-DD" } }

The file form also works: { "gitea": { "file": "<configDir>/secrets/<business>/chief-of-staff-gitea.token", "rotateBy": "YYYY-MM-DD" } }. That file must exist, mode 0600.

  • The reference creates no account, mints no token and grants nothing by itself.
  • The window owner adds it after a backup of the business file, and before P3.
  • Precondition for the store form: the store root <configDir>/secrets/agents must exist and be private, since bus/src/credentials.mjs:212 calls privateDir on it when the reference is read.

Repo business writers (mos-1330 item 2)

A git grep of scripts, templates, src, tools and packages outside tests found two repo files that write a business file with a chief. Both get the gitea file reference:

  • packages/business/examples/mosaic-stack.example.json:114, the chief entry. Two test files read it (corrected after webby-0178 B2):
    • packages/business/tests/business.test.mjs:256, "the shipped example refuses as written and validates once filled in". It replaces /home/you with its tmp root and checks the role list and the sync token path. The chief's new file ref goes through the same replacement.
    • packages/cli/tests/stack-release-fixture.mjs:351 (EXAMPLE, through biz()). It fills botId and the dates but keeps the /home/you token paths, as it already does for the other roles' refs. Its readers are stack-release.test, stack-release-external.test and stack-upgrade.test.
    • All four test files run whole at the gate.
  • packages/business/README.md:174, the chief example line, plus the prose at :163 and :183 that said the chief holds no credentials.

No script, template or case installer writes a chief or creative instance. scripts/case1-accept.sh only talks to chief.

Fixture changes (mos-1330 item 1, own commit)

Data only, with one named exception listed under "Old tests".

  • packages/business/tests/helpers.mjs:71-74
    • Old: // The chief of staff launches (REQ-LAUNCH-1, decision 139); it holds no / // credential and no tracker bot. / chief: { definition: "chief-of-staff" },
    • New: // The chief of staff launches (REQ-LAUNCH-1, decision 139); it has no / // tracker bot. Its one credential reference is gitea, for the library / // read grant (row 285, mos-1330). / chief: { definition: "chief-of-staff", credentials: { gitea: ref("chief-of-staff", "gitea") } },
  • Creative and chief instances loaded through the loader each gain credentials: { gitea: { store: true, rotateBy: FAR_DATE } }, and the import adds FAR_DATE where needed:
    • packages/business/tests/instance-settings.test.mjs:334, :335, :380 (import :12)
    • packages/business/tests/lifecycle-settings.test.mjs:237 (import :11)
    • packages/business/tests/launch-settings.test.mjs:184, bean (FAR_DATE already imported)
    • packages/business/tests/resolve.test.mjs:117, chief2 (import :7)
    • packages/bus/tests/creative.test.mjs:98, bean in the written business file (import :10). The startBroker businesses object at :111 doesn't go through the loader and is unchanged.
    • packages/cli/tests/launcher.test.mjs:769, bean (import :1)
  • packages/business/tests/business.test.mjs: the persona test's bean fixtures get credentials: CREATIVE_GITEA (4 places).

New named tests (mos-1330 item 1):

  • business.test "row 285: a chief or creative instance without credentials.gitea refuses at load". Each case refuses with the checkCredentialMap "(gitea; got none)" message, and a positive control loads.
  • business.test "row 285: a creative instance with identity and publish loads" (brief :1825, :1839, restored by mos-1333 item 2). The grant still refuses it at raise (#giteaRole).
  • agent-identity.test "row 285: a chief with its gitea reference, identity and publish loads" (mos-1333 item 4, brief :1829). It also checks the chief's credentials are exactly gitea.

Old tests (mos-1272, ruled in mos-1332 and mos-1333)

Webby (webby-0174) passes each item only with Mos's ruling key next to it.

  1. packages/business/tests/chief-of-staff.test.mjs (mos-1332 item 1, accepted as pushed). Expected values follow the role file:
    • APPROVED.credentials (:24). Old: []. New: [{service: "gitea", scopes: ["read:repository", "read:user"]}].
    • The resolved-record test (:90). Old: {}. New: {gitea: {service: "gitea", file: <root>/secrets/chief-of-staff-gitea.token, rotateBy: FAR_DATE}}.
    • :155. Old name: "the instance takes no credential references and no tracker", expecting "(none; got gitea)". New name: "the instance takes only the gitea credential reference and no tracker", adding a vikunja ref and expecting "(gitea; got gitea, vikunja)". The tracker refusal is unchanged.
  2. packages/business/tests/role.test.mjs (mos-1332 item 2, accepted). checkCreative checks the service and not the scopes (webby-0178 N1). The scopes are pinned by the load test, which deepEquals each of the six creative role files' credentials to [{service: "gitea", scopes: ["read:user", "write:repository"]}], so a widened scope in any one file is red.
    • The six-creative-roles load test now pins withinRole [message.send, library.write] and credentials [{gitea, [read:user, write:repository]}] exactly. Old: [message.send] and [].
    • The M1 block checked record.write and one crossRole case. It now loops over every routed action outside CREATIVE_ACTIONS, keeps the crossRole refusal, and adds the vikunja and host credential refusals. The refusal text changes from "may hold only withinRole message.send" to "may hold only withinRole message.send and library.write".
  3. packages/business/tests/agent-identity.test.mjs:49-50 (N10) (mos-1333 item 3, option (a); replaces mos-1332 item 3). After 285 the chief's definition declares gitea, so the old second case no longer refuses. Only the fixture instance and the instance name in the regex change. The text after the name is unchanged.
    • Old: refuses(() => check((d) => { d.roles.chief.identity = structuredClone(IDENTITY); d.roles.chief.publish = ["stack"]; }), / /roles\.chief\.publish needs identity and a gitea definition/);
    • New: refuses(() => check((d) => { d.roles.helper = { definition: "assistant", holder: "mos", identity: structuredClone(IDENTITY), publish: ["stack"] }; }), / /roles\.helper\.publish needs identity and a gitea definition/);
    • The standalone helper test added in 5f81d0da0 is removed. The fence and its business.test change are reverted.
    • Grep of packages/*/tests/* for "needs identity and a gitea definition": only agent-identity.test :47, :48 and :52. No other reader.
    • Mutant M9 below.
  4. packages/business/tests/instance-settings.test.mjs:289 (mos-1332 item 4, accepted). Old: { services: [], accepted: true }. New: { services: ["gitea"], accepted: true }. If the :298 listing changes at the run, the exact new value goes here and Webby hears about it. Anything that removes or weakens a check goes back to Mos.
  5. Other fixture edits (mos-1332 item 5): data only, no ruling needed.

Commit plan

  1. Role files, contracts, vocabulary, role.mjs, broker #giteaRole, gitea-grant.test and role.test, plus this file. 2a. The mos-1332 commit (5f81d0da0): instance-settings.test:289 and this file. Its business.mjs fence and two fence tests are reverted by the mos-1333 commit. 2b. The webby-0178 commit (d79cfe674): B1, B2 and notes. 2c. The mos-1333 commit: the fence removed, business.test restored, N10 option (a), the named chief test and this file.
  2. The fixture commit (mos-1330): helpers.mjs, chief-of-staff.test, business.test (fixture refs and the two new named tests), the loader fixtures above, the example json and README. Mos ruled the old-test points in mos-1332.
  3. The 1.16 commit: library.write in ACTIONS and ROUTED in both business/src/vocabulary.mjs and bus/src/broker.mjs, citing the PRD 1.16 paragraph (row #1796), with a hard-waits test that it is routed. It's held until Jason's yes and 1.16 lands.

Until commit 3, every creative role file refuses at load (REQ-WAIT-2: a GATED_ONLY action in a role file). The branch is green only with all three commits.

Mutant plan

Kill rules are written before the runs. Each mutant is one edit, then a restore, a sha256 compare and a clean git status.

M Mutant Source Expected kill
M1 drop role.definition === STAFF_TO from #giteaRole broker.mjs:2138 gitea-grant.test "X-G1: the chief of staff refuses at raise"
M2 CREATIVE_ACTIONS gains role.end vocabulary.mjs role.test M1 routed-action loop
M3 drop the creative services check role.mjs checkCreative role.test M1 vikunja and host refusals
M4 drop the crossRole check role.mjs checkCreative role.test M1 crossRole refusals
M5 creative role file without library.write roles/companion.json role.test creative-load test
M6 chief scopes widened with write:repository roles/chief-of-staff.json chief-of-staff.test "the shipped file is exactly the approved list"
M9 drop || !services.includes("gitea") from the N10 check business.mjs:166 agent-identity.test N10's helper case :51-52 (mos-1333 item 5)

M7 and M8 targeted the withdrawn fence and are dropped (mos-1333 item 1). M9 mutates a file outside the manifest only to show the N10 test still guards it.

Manifest

Source and roles:

  • packages/business/src/vocabulary.mjs
  • packages/business/src/role.mjs
  • packages/bus/src/broker.mjs (#giteaRole only, sage-2921)
  • roles/{companion,ideas,research,proofreader,fact-checker,continuity}.{json,md}
  • roles/chief-of-staff.{json,md}

Tests:

  • packages/bus/tests/gitea-grant.test.mjs (sage-2921)
  • packages/business/tests/role.test.mjs
  • packages/business/tests/business.test.mjs
  • packages/business/tests/helpers.mjs (fixture, mos-1330)
  • packages/business/tests/chief-of-staff.test.mjs (old test, mos-1272)
  • packages/business/tests/instance-settings.test.mjs
  • packages/business/tests/lifecycle-settings.test.mjs
  • packages/business/tests/launch-settings.test.mjs
  • packages/business/tests/resolve.test.mjs
  • packages/bus/tests/creative.test.mjs
  • packages/cli/tests/launcher.test.mjs
  • packages/business/tests/agent-identity.test.mjs (N10 :51-52 and one named test, mos-1333 items 3 and 4)

Docs:

  • packages/business/examples/mosaic-stack.example.json
  • packages/business/README.md
  • agents/scrooge/work/queue-285/BUILD.md (this file)

Gate checks before in-review

  • git grep CREATIVE_KINDS -- packages roles scripts is empty. Checked locally.
  • Grep the tests for gitea-not-declared, and for 64-hex pins on broker.mjs and each changed role file.
  • Run business.test, stack-release.test, stack-release-external.test and stack-upgrade.test whole, since they read the example json (webby-0178 B2).
  • Run the bus suite whole (sage-2946 item 1): broker.mjs is in the manifest, and these bus tests read roles/chief-of-staff.json by name: chief-of-staff, fire-revokes-grants, host-run, instances-verb, names, reload, routes-verb. Sage listed five. A git grep finds the seven.
  • Run each fixture-importing suite whole: the business suite, the bus suite, the cli suite, scripts/test-config.sh and every scripts/test-*.sh, with TMPDIR under /mnt/storage/scratch/tmp.
  • Sage confirms the result against mos-1309 item 5.

r2 rebase (sage-3578, 10-23)

  • Rebased 34ee4df7d onto origin/refactor 45dca69f6 as scrooge/row285-r2; the old branch is untouched and nothing was force-pushed.
  • Commit map: 0e369895b to 9a77fff7a, 8c529fb4d to 115889e4a, 5f81d0da0 to aabfdfc60, d79cfe674 to 66e3c3d61, 34ee4df7d to cf8ba6ef4.
  • git range-diff 94c33a43c..34ee4df7d 45dca69f6..cf8ba6ef4 gives = on all five. git patch-id --stable is unchanged for the whole range (72cc8ef5) and for broker.mjs (caa0e756). merge-tree was clean.
  • Drift 94c33a43c..45dca69f6 (262 commits). On this row's paths only broker.mjs (row 306), bus creative.test and cli launcher.test moved, and they merged clean. roles/, packages/business/ and contracts/ have no upstream change. Row 291 (4874d1e83) and row 308 (30e6bb680) touch the PRD, the requirements json and the tasks and bus requirements tests, which this row does not change. Row 306 touches broker.mjs, access-revoke.test, revoke-window.test and harness runner.mjs.
  • New and changed upstream tests that read roles/<name>.json: bus access-revoke, launcher-released, revoke-window, session-clear, session-compactions, sessions, thread-shares, chief-of-staff and creative. Bus chief-of-staff, creative and tester-writes import business tests/helpers.mjs, as do many cli, webui, harness and plugins tests. No 64-hex pin names a role file or broker.mjs. CREATIVE_KINDS has no reader at 45dca69f6. project-runtime.test pins vocabulary.mjs import-free; this row adds no import.
  • Gate scope therefore grows: run the bus, business, cli, webui, harness and plugins suites whole, plus the scripts above.

Commit 3: library.write routed

PRD 1.16 landed with row 291 (4874d1e83) and Jason's yes is mos-1354 Q32, so the hold on this commit is gone. library.write goes last in ACTIONS in both business/src/vocabulary.mjs and bus/src/broker.mjs, keeping bus hard-waits.test's order-sensitive ACTIONS equal VOCABULARY, and into ROUTED in both, so HARD_WAITS is unchanged. A new business hard-waits test (REQ-WAIT-3, REQ-LIB-6) checks it is in ACTIONS, routed and not GATED_ONLY.

Open for review, not in scope: the PRD says library.write is granted to the six creative roles only. role.mjs checkCreative bounds what a creative role holds, but nothing refuses library.write in a non-creative role's withinRole. The brief asks only that the chief refuses it under an allowlist validator, and the chief has none. Flagged for Webby and Sage.

N1 (sage-3593): role validator should refuse library.write on a non-creative role (PRD 1.16 creative-only). Not part of 285. Role files change only through reviewed commits (invariant 7), so a review would see such a grant today, but a refusal at load is the fail-closed form. Webby confirms it in the round, and Sage files it in DEFERRED as a follow-up row at landing.

Review scope (sage-3593): commits 1-5 are a rebase, so check the range-diff. Commit 3 (8f8b8cf15) is new code, so review it in full.

Results

Not run.