10 KiB
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 optionalskillslist. Names match/^[A-Za-z0-9._-]+$/, with "." and ".." refused, no repeats, none on version 1 role files (role.mjs).resolve.mjscarries 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.mjsreads each skill from<dataRoot>/skills-enabled/<name>, copies it to<bundle>/skills/<name>and renders every file, in sorted path order, between<skill-file skill="..." path="...">and</skill-file>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.lstatSyncthroughout, 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.mdas one--system-promptargument 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 bywc -cover the repo copies: coder 16853, reviewer 16252, cto 35888, pm 29080. Each fits (skills.test.mjs, the SKILL_BYTES test). launcher.mjs:254checksreadSkills(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.mdandpackages/harness/README.mddescribe the section and the flags. The other test edits give the fixtures askills-enableddirectory.
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 toprompt.mdless trailing newlines - every marked file equal to its bundle copy
- no skill path or marker anywhere else in argv
--no-skillsand no--skillon pi,--disable-slash-commandsand--restrictedon 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)
- The rendered
prompt.mdmatches the bundle copies byte for byte: skills.test.mjs:45 for the build and :170 for what each adapter actually passes. --no-skills,--disable-slash-commandsand--restrictedare still on:adapters/pi/adapter.sh,adapters/claude/adapter.sh, and skills.test.mjs:170 with M11-M13.candidate-manifest.sha256againstgit show c6d7d538f.- The 64 KiB cap against the one-argument limit, and that every repo role fits.
- 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-58says a symlinked skill, a bad file name, non-UTF-8 bytes, the end marker and an over-cap list refuseskill-not-enabled. The code gives thembundle-refused, which the launcher maps toskill-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: truefixes 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
9f0039711of c6d7d538f on1a01f63ea, range-diff=, patch-id 7e1948e2f2e9 equal. F1 doc commit6b6567b8b.landing-manifest.sha256is 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.