# 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/.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 `/personas//.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 `` 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 ``. 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 `, and pi passes `--system-prompt `. Both make the path absolute. The prompt text no longer appears in `/proc//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 `, 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 `.`. 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//` 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///` 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.