# Row 64: a managed session gets its role's skills as prompt text on both harnesses Author and gate owner: Sage (mos-0855). Reviewers Darkwing and Filbert. Sage, 2026-10-19. Brief `docs/plans/2026-10-11_managed-session-config.md`, anchor "Managed sessions get their role's skills on both harnesses", blob 21ae25a3. Issue #1557. Rulings mos-0857 (reviewer checks), mos-0862 (mutant coverage). Base 087d202bc. Candidate c6d7d538f, one commit, built in `/mnt/storage/scratch/sage-r64/wt` and gated in a clean detached worktree at `/mnt/storage/scratch/sage-r64/cand`. The commit is in the canonical object store, so `git show c6d7d538f` works from the canonical checkout. `candidate.diff` is that output. `candidate-manifest.sha256` lists the 28 changed paths. ## The choice (`probe.md`) The probe ran Claude Code 2.1.303 and read the pinned pi docs. On Claude Code a `--plugin-dir` skill works only with the `Skill` tool on, and with it on, Claude Code's own bundled skills come along under names that belong to the Claude Code version. On pi, `--skill` puts a description in the prompt and the model reads SKILL.md with a file tool, which the gate refuses because the bundle is outside the workspace. So the skill text goes into `prompt.md` on both harnesses, rendered from the bundle copies. Pi keeps `--no-skills`. Claude Code keeps `--disable-slash-commands` and `--restricted`. Neither harness discovers a skill, no new tool passes the gate, and the manifest's digests cover what the model reads. ## What changed - `roles/*.json`: an optional `skills` list. Names match `/^[A-Za-z0-9._-]+$/`, with "." and ".." refused, no repeats, none on version 1 role files (`role.mjs`). `resolve.mjs` carries the list on the record. - First lists: coder ms-unslop, ms-sdlc, ms-file-read, ms-file-write. Reviewer ms-unslop, ms-sdlc, ms-file-read. Cto ms-unslop, ms-sdlc, ms-archify, ms-grill-me. Pm ms-unslop, ms-goal, ms-communications. The chief of staff lists none. - `bundle.mjs` reads each skill from `/skills-enabled/`, copies it to `/skills/` and renders every file, in sorted path order, between `` and `` lines under a "## Skills" heading. A role with no list gets no heading, no skills directory and an empty manifest list. The manifest records each skill's path and per-file sha256. - Refusals: a listed skill that isn't enabled is skill-not-enabled and names `scripts/skill.sh activate`. A symlink anywhere in the skill (the skill directory, SKILL.md, a sub-file) or a non-file entry, a file name outside SKILL_PATH, non-UTF-8 bytes, or a file holding the end marker is skill-refused. `lstatSync` throughout, so a symlink is refused, not followed. - SKILL_BYTES is 64 KiB across all of a role's skills together. The claude adapter passes `prompt.md` as one `--system-prompt` argument and Linux caps one argument at 128 KiB (MAX_ARG_STRLEN), so the cap leaves room for the contract and the rest of the prompt. Bytes per role by `wc -c` over the repo copies: coder 16853, reviewer 16252, cto 35888, pm 29080. Each fits (skills.test.mjs, the SKILL_BYTES test). - `launcher.mjs:254` checks `readSkills(resolved.skills, skillsDir)` after the route and contract checks and before the harness-off and binary checks, so the refusal comes before any run exists and doesn't depend on the harness. skill-not-enabled stays skill-not-enabled, anything else becomes skill-refused. - `adapters/README.md` and `packages/harness/README.md` describe the section and the flags. The other test edits give the fixtures a `skills-enabled` directory. ## Tests the brief names, each on both harnesses | Brief test | Where | |---|---| | exactly the contracted skills | skills.test.mjs:45, four roles x pi and claude-code, prompt vs bundle copy vs source, byte for byte | | a contracted skill that isn't enabled refuses before any spawn | skills.test.mjs:109 (build, both harnesses), launcher.test.mjs row 64 pi test and the row 64 Claude Code test (launch, no run made) | | an enabled but unlisted skill is absent | skills.test.mjs:92, both harnesses | | no list: Pi runs with --no-skills, Claude Code gets none | skills.test.mjs:79 (build, both harnesses) and :170 (chief through each real adapter) | skills.test.mjs:170 runs each real `adapter.sh` against a stand-in CLI that records argv. For the reviewer and the chief on each harness it checks: - one `--system-prompt`, equal to `prompt.md` less trailing newlines - every marked file equal to its bundle copy - no skill path or marker anywhere else in argv - `--no-skills` and no `--skill` on pi, `--disable-slash-commands` and `--restricted` on Claude Code The four-way loop and the Claude Code launcher test were added after gate run 1 started, when I read the brief's "each on both harnesses" against the tests. I stopped run 1 (bus and business had passed). Run 2 is on the amended commit. ## Mutants (`mutants.py` and `mutants-run2.txt` here, run 2 on c6d7d538f, logs in `/mnt/storage/scratch/sage-r64/mutants/`) | Mutant | Change | Caught by | |---|---|---| | M1 | drop the launcher precheck | launcher.test.mjs, both row 64 tests | | M2 | map skill-not-enabled to skill-refused | launcher.test.mjs, both row 64 tests | | M3 | no SKILL_PATH check | skills.test.mjs | | M4 | no SKILL_BYTES check | skills.test.mjs | | M5 | no end-marker check | skills.test.mjs | | M6 | the walk follows symlinks (statSync) | skills.test.mjs | | M7 | the found check follows symlinks | skills.test.mjs | | M8 | no newline before the end marker | skills.test.mjs | | M9 | role names allow "." and ".." | role.test.mjs | | M10 | resolve drops the skills list | skills.test.mjs (resolve.test.mjs passes) | | M11 | Claude Code without --disable-slash-commands | skills.test.mjs | | M12 | Claude Code without --restricted | skills.test.mjs | | M13 | pi without --no-skills | skills.test.mjs | 13 of 13 killed. M10 is caught only by skills.test.mjs. Mos ruled that enough (mos-0862): a mutant counts as killed if any gate test catches it. M6 and M7 add `statSync` to the import so they fail on behavior, not on a ReferenceError. The files were restored byte for byte (`restored True`) and the orphan sweep found nothing. ## Base drift `packages/harness/README.md` and `packages/harness/tests/bundle.test.mjs` changed on refactor in row 134 (3a600cf5b), in different hunks from this row. The rebase onto 087d202bc was clean, and the gate runs on that base. ## Gate `/mnt/storage/scratch/sage-r64/gate.sh` from the template, on the clean worktree at c6d7d538f, dirty 0, with the manifest checked before any suite. Run 2: GATE_PASSED (`gate-run2.txt` here, logs in `/mnt/storage/scratch/sage-r64/out/`). 15 package suites, 2384 pass and 0 fail by an awk sum over the log (harness 133, cli 349, business 149, plugins 111). All 10 script suites passed. The compose network was removed, and no container from the gate is left. Run 1, on the earlier commit ece9fb5a3, was stopped by hand after bus and business passed (`gate-run1.txt`, `out-stopped-run1/`). It didn't fail. I stopped it to add the both-harness tests above. ## For the reviewers (mos-0857) 1. The rendered `prompt.md` matches the bundle copies byte for byte: skills.test.mjs:45 for the build and :170 for what each adapter actually passes. 2. `--no-skills`, `--disable-slash-commands` and `--restricted` are still on: `adapters/pi/adapter.sh`, `adapters/claude/adapter.sh`, and skills.test.mjs:170 with M11-M13. 3. `candidate-manifest.sha256` against `git show c6d7d538f`. 4. The 64 KiB cap against the one-argument limit, and that every repo role fits. 5. That the launcher precheck comes before any run. (Round 1 correction, Darkwing N1: the code also puts it before the harness-specific checks, but no test pins that order. The brief asks for "before any spawn", which the launcher tests check.) Landing waits for Mos's go (mos-0856, mos-0861 A2): activate the skills in the live data root, then start one managed coder session per harness and attach the manifest. ## Round 1 follow-ups - F1 (Filbert, #1557 comment 29218, doc-only): `packages/harness/README.md:55-58` says a symlinked skill, a bad file name, non-UTF-8 bytes, the end marker and an over-cap list refuse `skill-not-enabled`. The code gives them `bundle-refused`, which the launcher maps to `skill-refused`. No test reads that README (grep of packages/*/tests and scripts/test-*.sh), so under decision 137 it doesn't block. It gets its own doc commit at landing, after the reviewed candidate, so the approved manifest stays as reviewed. - Darkwing (#1557 comment 29219) N1: covered by the item 5 correction above. N2: a leading UTF-8 BOM is stripped by the decoder, so the copy and digest would be 3 bytes off (`ignoreBOM: true` fixes it). N3: EACCES or ENOTDIR inside a skill dir comes out as launch-failed or a raw throw, not skill-refused, though the launch still refuses. N2 and N3 are follow-up issue #1672. N4: the cli and webui fixture edits are mechanical. N5: past this row, coder, reviewer, cto and pm launches refuse until all eight listed skills are active in the data root. Live has only ms-communications. Any re-pin past row 64 activates them first (sage-1064 R4). - Landing prep (sage-1065): cherry-pick 9f0039711 of c6d7d538f on 1a01f63ea, range-diff `=`, patch-id 7e1948e2f2e9 equal. F1 doc commit 6b6567b8b. `landing-manifest.sha256` is the same 28 paths at 6b6567b8b; 27 match the candidate manifest and the README differs by F1 only. Nothing goes onto refactor before Mos's go. ## Landing (mos-0863) Mos gave the go for steps 1, 2, 3 and 5 in mos-0863. Step 4, the live activation, is now a window step (D3, `agents/sage/work/window-plan/carry-past-row64.md`). N2 and N3 are #1672, with its own row after 173 (D4). D5: the item 5 correction above is the N1 text fix, in this packet commit. The landing gate ran `gate-land.sh` on a clean worktree at 6b6567b8b, with `landing-manifest.sha256` checked before any suite (`gate-land-run1.txt` here, logs in `/mnt/storage/scratch/sage-r64/out-land/`). 9f0039711 and 6b6567b8b go onto refactor by fast-forward from 1a01f63ea, so the landed shas are the gated ones.