Files
jason.woltje 361ba2387f row 213: Case 2 C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file (#1714)
Lands Morgana's round 4 candidate, 2b3e67e36 (base c771a36f4, branch morgana/row213-r4), approved by Webby
(webby-0071, REVIEW-r4 02bb0afb3, rev 2775). Manifest digest 26d4d0ecff56, 35 paths, plus Morgana's
four packet files under agents/morgana/work/queue-213/ (base.txt, BUILD.md, candidate-manifest.sha256,
mutants.py): 39 paths, blobs and modes from the candidate. No 213 path changed between c771a36f4 and
this commit's parent. Patch-id f313e69b6f7e, the same as c771a36f4..2b3e67e36.

Evidence: Morgana's slot run on 2b3e67e36 (sage-1844): singles 191/0 and 31/31 mutants killed.
Gate on my slot (sage-r195land, base 38b83ea5c plus 195's 5 and 213's 39 paths, 09:45:52-09:57:52Z): business 200/0, bus 542/0, harness 140/0, cli 663/0; scripts auth 15/0, conductor 17/0, config 31/0, discord 78/0, extension-package 18/0, foundation 44/0, land-work 86/0, queue 156/0 plus 28/0, release 14/0, task 98/0; the models mutant L1 killed (launch-settings 15/1), restore matched.
Between the gate base 38b83ea5c and 4364dddb0 only docs/plans/2026-10-20_host-credentials.md changed (01e1fe512, 4364dddb0), which no test reads. Row 195 landed as this commit's parent; it shares no path with 213.
Webby's N2 (a host grant for a creative role) is a row 235 acceptance item (mos-1163), not a deferral.
2026-10-21 04:58:30 -05:00

18 KiB

Row 213 (C2-08) build: creative roles, persona file and block, M1, M2, M6, M10, prompt by file

Morgana (Opus 5.5), 2026-10-20. Queue row 213, Gitea #1714. Brief: docs/plans/2026-10-20_case2-rows.md § C2-08 (on refactor from 32b54ac3b). Design: Gosalyn's Case 2 design r10, sections 4 and 5. Reviewer Webby, gate owner Sage. Base: base.txt (088087ac9). Manifest: candidate-manifest.sha256. D1 and D2 approved as deviations in sage-1492.

What it is

  1. Six creative roles (roles/<name>.json and .md, roleVersion 2): companion and research get read and web, with network api-only. ideas, proofreader, fact-checker and continuity get read only, with network none. All six get withinRole ["message.send"], no credentials, and messaging that sends to and receives from human only. vocabulary.mjs names them in CREATIVE_DEFINITIONS, plus CREATIVE_KINDS, CREATIVE_TOOLS and THREAD_DEFINITIONS. END_PROTECTED becomes THREAD_DEFINITIONS.
  2. M1 (role.mjs checkCreative): a creative role file is refused if it names a tool outside CREATIVE_TOOLS, holds any authority but withinRole message.send, lacks messaging, names anyone but human in messaging, or is version 1.
  3. M2: since the role files name no write, edit or bash tool, the existing gate refuses write, edit and bash on pi and Write, Edit and Bash on claude-code for each creative role. No gate code changed. The test pins it.
  4. M6: tools.mjs leaves raise_decision out for a creative definition. broker.mjs refuses decision.raise from a creative session with decision-not-allowed, after #secretCheck and before the spec-cited path, so a cited raise is refused too.
  5. M10: a creative instance belongs to the human.
    • launch-settings.mjs creativeHumanOnly gives organization-creative-human-only to any other actor. It covers: a models key that a creative instance's model or fallback.model var names (changed or removed), setRoutes for the instance, and the instances key. The instances key is refused for every actor with organization-creative-instance, because the model changes through instances.set or routes.set.
    • instance-settings.mjs: add, change and redefine to or from a creative definition are the human's. A creative definition is END_PROTECTED, so the new closed() lets this writer still add and change one. accepted and protected use closed() too, so the human's Console form offers the creative definitions and marks those instances as editable.
  6. Persona (design 4): vars persona.name (one line, max 40) and persona.sha256 (64 lowercase hex), agent layer. The file lives at <configDir>/personas/<business>/<instance>.md.
    • bundle.mjs readPersona opens it with O_NOFOLLOW and refuses with persona-invalid when the file is missing, a symlink, not a regular file, owned by another uid, carries group or other mode bits, is over 8000 characters, is not UTF-8, or holds </persona-file> or a line starting # or ## .
    • It refuses with persona-mismatch when the bytes don't match the digest.
    • A file present with no persona.sha256 var is ignored. A digest with no personaDir refuses.
    • The block sits between the contract and ## This session, wrapped in <persona-file instance=... sha256=...>.
  7. Web: webPlan refuses web-source-not-allowed when a creative definition is on harness-builtin, so every creative fetch goes through the broker.
  8. Prompt by file: claude passes --system-prompt-file <path>, and pi passes --system-prompt <path>. Both make the path absolute. The prompt text no longer appears in /proc/<pid>/cmdline. adapters/README.md says so.

Refusal codes

persona-invalid, persona-mismatch, web-source-not-allowed, organization-creative-human-only and organization-creative-instance are new. decision-not-allowed is named in the brief but is also new in code. My morgana-0021 message called it an existing code, which was wrong.

Deviations (sage-1492)

  • D1 packages/business/src/business.mjs: checkPersonaVars, plus one call line at the end of checkRoles's per-instance block. The persona vars are accepted only on an instance of a creative definition, and only both together. Without D1, any instance's agent layer could carry a persona digest, and the bundle would read a persona file for a pm or chief. Test: business.test.mjs "persona vars: agent layer, creative definition only, both or neither". It covers the business layer, pm and chief, one key alone and malformed values. instance-settings.test.mjs also covers it: changing a persona-carrying companion to security-reviewer is refused. Mutants: D1a, D1b, D1c.

  • D2 packages/cli/src/launcher.mjs: one line, personaDir: join(configDir(env), "personas"), in the buildBundle call. Without it a creative instance with a persona can never launch. Test: launcher.test.mjs "row 213 a creative instance's persona is read from /personas: off its digest the launch fails with the mismatch as its reason". Mutant: D2.

  • Adapter tests' file (webby-0040 N1). Brief line 68 puts the adapter tests in claude-session.test.mjs and pi-session.test.mjs. They are in harness/tests/creative.test.mjs instead, next to the other row 213 harness tests, so the row's adapter cases sit in one file and the two session test files stay unchanged. Same cases: argv holds the absolute prompt path and no marker, and pi refuses a missing, unreadable or directory prompt.

The checkRoles change sits in its own block and the launcher change is one line, so the rebase over rows 194 and 199 (both touch business.mjs, and 199 touches launcher.mjs) stays small.

Pinned-test edits and the pi tightening (sage-1492, named here)

  1. packages/bus/tests/project-runtime.test.mjs:26: the pinned broker import line gains CREATIVE_DEFINITIONS (M6 reads it).
  2. packages/harness/tests/skills.test.mjs:170: the test checked the prompt text in argv after --system-prompt. Each adapter now passes a path, so the test looks up the flag for each harness, checks that the next argument is the bundle's prompt file, and reads the MARKED and chief checks from that file.
  3. adapters/pi/adapter.sh: [ -f "$PROMPT_FILE" ] && [ -r "$PROMPT_FILE" ], exit 2 otherwise. Pi 0.85.1 uses the path string as the prompt when it can't read the file. Test: harness/tests/creative.test.mjs "pi refuses a missing, unreadable or directory prompt" (pi never runs). Mutant: A1.

One more pinned assertion changed: business/tests/assistant.test.mjs REQ-HARN-2 expected only the assistant to hold web. Companion and research now hold it too.

Files

Source: packages/business/src/{vocabulary,role,vars,business,launch-settings,instance-settings}.mjs, packages/harness/src/{bundle,tools}.mjs, packages/bus/src/broker.mjs, packages/cli/src/launcher.mjs, adapters/{pi,claude}/adapter.sh, adapters/README.md, roles/{companion,research,ideas,proofreader, fact-checker,continuity}.{json,md}.

Tests changed: business/tests/{role,business,launch-settings,instance-settings,assistant}.test.mjs, harness/tests/skills.test.mjs, bus/tests/project-runtime.test.mjs, cli/tests/launcher.test.mjs. New: harness/tests/creative.test.mjs (7), bus/tests/creative.test.mjs (3).

I grepped packages/*/tests and scripts/ for each changed source path next to a 64-hex string. The hits are thread-migration.test.mjs (a DDL digest, not a pin on broker.mjs; 3/3 pass) and discord's own tools.mjs. No test pins a changed file.

Evidence

Each file alone from its package with node --test --test-reporter=tap <file>, 2026-10-20, all fail 0:

  • Touched: business role 17, business 17, launch-settings 16, instance-settings 12, assistant 11; harness creative 7, skills 10, bundle 22; bus creative 3, project-runtime 3; cli launcher 30. Round 2 reran bus creative, instance-settings, business, launch-settings, role, harness creative and bundle, bus tester-writes 8 and chief-of-staff 18, and the vars readers business vars 7, discord context 7 and git 12.
  • Other files that read a changed path: business hard-waits, record-grants, token-limit-settings, workflow-roles, chief-of-staff, project-repo, plugin-settings; harness bundle 22, tools 22, claude-session 8, pi-session 8, raise-domain, spec-tools, claude-gate, mcp-server, gate, runner 34, git-tools; bus hard-waits, launch-notice, tester-writes, business, broker 16, workflow-roles, web 19, task-page, thread-migration 3; discord git, web, setspark, engine, tools; plugins harness, loader; seat working, sandbox, session; cli launch-limit-checks, limits, git-socket, launch-account, thread-launch, launch-limits, launch-routes, instances, stack-copy, stack-release 174; webui limits-browser, editor, settings-form, tester-browser.

Suites, scripts/test-*.sh and the mutants are not run yet. They need Sage's slot.

Drift: git diff --stat 088087ac9 305857c8f over the 35 manifest paths is empty (2026-10-20, sage-1501). Row 192's async scripts/mosaic-config.mjs and webui-service.sh are outside the manifest. The broker's new import comes from vocabulary.mjs, which it already imported, so RUNNER_FILES doesn't change.

Mutants: mutants.py in this directory, 31 mutants over 11 files. It backs up each file to scratch first, aborts on a bad backup or any anchor count other than one (all 31 checked as unique without editing), runs the named test files per mutant, restores each file after its mutant and again in finally. Log: mutants.log, written on the run. The brief's models mutant is L1.

Mutant File Tests
D1a/b/c persona vars check (D1) business.mjs business, instance-settings
D2 no personaDir (D2) launcher.mjs launcher
L1 models key by anyone (the models mutant) launch-settings.mjs launch-settings
L2 fallback.model not checked launch-settings.mjs launch-settings
L3 instances key allowed launch-settings.mjs launch-settings
L4 setRoutes by anyone launch-settings.mjs launch-settings
L5 creativeHumanOnly never refuses launch-settings.mjs launch-settings, instance-settings
I1/I2/I3 add, change, redefine by anyone instance-settings.mjs instance-settings
R1/R2/R3 checkCreative off, wider tools, non-human messaging role.mjs role
B1-B8 mode bits, digest, headings, symlink, char cap, block, no dir, builtin web bundle.mjs harness creative
T1 raise_decision offered tools.mjs harness creative
K1 broker raise check off broker.mjs bus creative
K2/K3 chief or tester session reaches the writer as the human (round 2, B1) broker.mjs bus creative
V1 persona.name line separators accepted (round 2, N3) vars.mjs business
A1 pi without -f; A2/A3 relative path kept adapters harness creative, skills

Residuals

  • ID_PATTERN allows dots, so a human id could read like <definition>.<instance>. That is only cosmetic: creativeHumanOnly compares the actor with document.human.
  • TOCTOU between pi's [ -f ] && [ -r ] and pi's own read of the bundle's prompt file.
  • The other-uid owner check in readPersona can't be tested without root.
  • O_NOFOLLOW covers only the last path component, so a symlinked personas/ or personas/<business>/ is followed (webby-0040 N2). The digest still refuses any bytes but the pinned ones. A comment in bundle.mjs says so.
  • The claude adapter checks the prompt file with [ -r ] only, and pi checks -f and -r. The brief asks for pi's check (webby-0040 N4).
  • Launch-failed mapping. A BundleError thrown from buildBundle inside start() becomes the Refusal launch-failed, with the code's message in the log's why. That holds for the persona codes and for webPlan's web-source-not-allowed. By then the run dir launches/<business>/<run>/ exists with only session/, and no bundle is written. Only check()'s readSkills preflight surfaces bundle codes. The D2 test pins both facts. I kept the launcher change to one line, as sage-1492 asked.

Follow-ups (not in this row)

  • A persona preflight in launcher check(), so a bad persona refuses with its own code before a run dir exists. Sage agreed it as a follow-up (sage-1501) and files it after this row lands.
  • The webui bus.mjs code-to-HTTP map lacks the new codes.
  • webby-0053 N1: checkCreative doesn't refuse credentials, so a later role-file commit could give a creative instance a gitea credential. All six creative role files have credentials: [] today. A one-line refusal and a role.test.mjs case close it.
  • The cli instances.mjs message wording for the creative refusals.

Round 2 (webby-0040, round 1 CHANGES on bb5f4600)

  • B1: bus/tests/creative.test.mjs adds "M10 through the broker". It runs on a real organization file and writer behind startBroker, with a claimed chief session, a bound tester session and the human.

    • The chief's organization.set of bean's launch.models key is refused with organization-creative-human-only. Its instances key is refused with organization-creative-instance.
    • The tester's organization.set and limits.set are refused tester-refused (N5: the broker refuses first). Its instances.set (change bean, add an ideas instance) and routes.set for bean reach the writer as tester and are refused with organization-creative-human-only.
    • The file bytes are unchanged and no committed log entry exists after all of these.
    • The human's limits.set of the same change lands, and the one committed entry names jason.
    • Mutants K2 and K3 change the broker's actor naming for a role or tester session.
  • B2: instance-settings.test.mjs asserts the mode of writer.logFile("acme") (0600) and that it sits in the history dir. It lives at .history/acme/changes.jsonl, so the round 1 loop over that dir already covered it, but no line named it.

  • N1: listed under Deviations. N2: comment and residual. N3: persona.name now refuses U+2028 and U+2029, with two values added to the business test and mutant V1. N4: noted under residuals.

  • Drift: git diff --stat 088087ac9 f41729178 over the 35 manifest paths is empty. Round 2 manifest digest b5ff0ac0e754cb1e5891417f4ab1da9597f5cf49fb2fe2d5f0fbf066f821c43a.

Round 3 (sage-1715): rebase onto 9b5362018

Rows 199 (cb5fdb064), 237 (d7d943199) and 210 (f5753c0e8) landed on six manifest paths after the round 2 approval. Round 3 rebases the three commits onto origin/refactor 9b5362018 on a new branch, morgana/row213-r3. The round 2 branch stays as it was. Nothing else changed.

  • Five files conflicted. Each resolution keeps both sides:
    • bus/src/broker.mjs and bus/tests/project-runtime.test.mjs: the vocabulary import keeps rows 199 and 210's names (forkProblem, forkUrl, credentialKind, validArgv, commandAllowed) and adds CREATIVE_DEFINITIONS last. The pinned import line in the test carries the same list. RUNNER_FILES is unchanged, since the import is still the vocabulary file only.
    • business/src/business.mjs: one vocabulary import with row 199's names plus CREATIVE_DEFINITIONS. The checkRoles(roles, file, rolesDir, business, hosts) signature is row 199's. checkPersonaVars runs after row 199's identity and publish checks.
    • business/src/role.mjs: the import keeps row 210's HOST_SERVICE and adds CREATIVE_DEFINITIONS and CREATIVE_TOOLS.
    • business/src/vocabulary.mjs: the creative constants and END_PROTECTED = THREAD_DEFINITIONS come before row 199's Gitea URL parse, which is kept whole.
  • cli/src/launcher.mjs merged without a conflict.
  • git range-diff 088087ac9..aee32d185 9b5362018..morgana/row213-r3: commits 2 and 3 are =. Commit 1 differs only in the conflict hunks above.
  • None of the new base's added lines name END_PROTECTED, THREAD_DEFINITION, CREATIVE_* or the creative refusal codes, apart from docs. All 31 mutant anchors still count 1. The hash-pin grep finds no 64-hex pin on a manifest path.
  • The manifest is rebuilt from the round 3 tree. It has the same 35 paths, and six digests changed: the six drifted files. base.txt is 9b5362018.
  • Singles of every touched test file, plus the seat sandbox and bus project-runtime tests (RUNNER_FILES rule), wait on Sage's slot. They are recorded here when run.

Round 4 (sage-1778, mos-1160): one commit on row 234's landing c771a36f4

Row 234 landed as c771a36f4 after the round 3 approval (webby-0053). Round 4 is one commit on c771a36f4, on the new branch morgana/row213-r4. It is the round 3 change applied onto the landed blobs. The round 3 branch stays as it was.

  • Overlap, git diff --name-only 9b5362018 c771a36f4 against the round 3 paths: business/src/business.mjs, bus/src/broker.mjs and bus/tests/project-runtime.test.mjs. Each starts from the landed blob, and the row 213 edits are merged by hand. No landed hunk is reverted (sage-1760).
    • broker.mjs and the pinned project-runtime import line: row 234's list with HOST_ID, then CREATIVE_DEFINITIONS. The test keeps both rows' comments. The import still names only vocabulary.mjs, so RUNNER_FILES is unchanged.
    • business.mjs: row 234's import (no HOST_PREFIX, no hosts.mjs) plus CREATIVE_DEFINITIONS. checkPersonaVars sits before row 234's HOSTS_MOVED and its four-argument checkRoles, and still runs after the identity and publish checks.
  • Check: the removed and added non-packet lines of diff c771a36f4..r4 and diff 9b5362018..2d2ad2760 differ only in those three import lines. All 31 mutant anchors count 1.
  • Row 234's hosts verbs: #hostsCaller admits the human, the tester or the chief definition (grant-chief-only), so a creative role can't reach them and M1 stays as narrow.
  • The manifest has the same 35 paths. The three overlap files changed digest. base.txt is c771a36f4.
  • Singles and the 31 mutants wait on Sage's slot. They are recorded here when run.