sage: row 64 packet, build notes, both gates, mutants and the landing manifest; BUILD.md item 5 corrected for Darkwing N1 (mos-0863 D5, #1557)

This commit is contained in:
2026-10-19 16:18:46 -05:00
parent 6b6567b8b4
commit ebfc23c431
8 changed files with 1374 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
# 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 `<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. `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.
@@ -0,0 +1,28 @@
f4a4bc8bebf42c9cd897c4d7b3fea9635fff7c177fd2b881a4ab35d0492e4a3c adapters/claude/adapter.sh
19f73115a5768cd67eef24d904706d5df1ec5c5d8108c92cba6968fa4f56bc88 adapters/pi/adapter.sh
cb05dbe506967957c152d417046b0902fd407a58936af87f0100199c0d389235 adapters/README.md
2318a58523c9a0f93ea2c4e8439b761f8190480ee5ccb6f93466c878750b5ad4 packages/business/src/resolve.mjs
f24f27b05c3e02fa909c543e331b38ec6385892629ac0b731b52bf3a56d6a2cd packages/business/src/role.mjs
f5cf086b161112f88ab930f5b557ff7f832ebbd143b4e5291a295d4e60db31de packages/business/tests/resolve.test.mjs
609efd32e5e325d518c09f48765f587efb5d2ff794c656c09e3002bd1a70f543 packages/business/tests/role.test.mjs
0573985439a072378b49541136ec4d2091db1f14a9fa7137bbf7229872d8614b packages/cli/src/launcher.mjs
0e5f58ec412bf484714afd016e821deaad6228012c03bb0a709bd728a503f841 packages/cli/tests/channel-fixture.mjs
ec4bbbc98adab6561205e5ec8e2f2495f1f55224da53e567c7a72e36b5410709 packages/cli/tests/git-socket.test.mjs
e5c0aa0242ee92c474a7795074354123efafa8de497fcfeacfc87c8ca31efa32 packages/cli/tests/helpers.mjs
4ac87c566046746e1d91957b6451a562a2b07187eae3ab6958edf26ed73ad33f packages/cli/tests/instances.test.mjs
aad8187d8464b6fe88d8993948158fff7ac9ab7d07b4e670f32866d8ff41e85f packages/cli/tests/launcher.test.mjs
776c180a09ed9791b09d06f5f28f18d2e500c5e2b5ad19f93042a19787b5853f packages/cli/tests/thread-launch.test.mjs
fce6c078205462f63a60cfe80c68047b93bf201c99f77814085c920add08eb6c packages/harness/README.md
46d8b7159077683e4028fdc389bab1c5d4634a0bdb4e2889c58524838346cd05 packages/harness/src/bundle.mjs
d492c9a8059cc0cbc4e48d7ab5e053c0693d8a8bb99f900d4650db1b23de9838 packages/harness/tests/bundle.test.mjs
e81b3e0b1deb22d15680bc6dade4edea070f2d0abbfa972883dfe9fa2e5c62a0 packages/harness/tests/claude-session.test.mjs
8d1ae8daec5f89452c78fd64756b5c51210187b2ab17a76caffec2e0101da697 packages/harness/tests/helpers.mjs
0aad8e9bbd910d848b5e60a8b0009c50a803b908b533332fc440e64ccdfeebb4 packages/harness/tests/pi-session.test.mjs
d98f9e4283afb1c2dbc655710bde594ba8cbf65e5a15f2d2d6e170144afcab3c packages/harness/tests/skills.test.mjs
8583a53a1c16e8a570fe3f1e015fe23556bd89ba5d2baa88e3d1392dcf8e8234 packages/webui/tests/instances.test.mjs
0b4f0fba3f928f018f8b2cc7319e88ba3e46dfa9a40bc18b66f8c8d0fb13fbcd packages/webui/tests/routes-browser.test.mjs
8c3558e1bf640fc86a737100a0840b44cf003cb49057c6c2e5d5a6b90466aec1 packages/webui/tests/routes.test.mjs
13713c4e64ade3b1b1eedabed75f5f76fe65e73f6df336b771d1e34881d111f3 roles/coder.json
60b6482a29a578234b4e5bf0d4c9a123bd3a65a65dc6e488f474546cf4456bb2 roles/cto.json
1af1cb70512163eb5c39ae942496a4cc309b0b1d47b9cb48253ac788862f0e9f roles/pm.json
4d2b2ab1208aced3a82d7de1ace879574fae2930ac1bc684f76e1d16e58cec8f roles/reviewer.json
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
manifest OK
base 6b6567b8b dirty 0 DOCKER_HOST=unset
pkg-bus rc=0 ℹ pass 428 ℹ fail 0 | ℹ duration_ms 2910.720699
pkg-business rc=0 ℹ pass 149 ℹ fail 0 | ℹ duration_ms 1911.055343
pkg-cli rc=0 ℹ pass 349 ℹ fail 0 | ℹ duration_ms 124237.078818
pkg-control-board rc=0 ℹ pass 124 ℹ fail 0 | ℹ duration_ms 592.19684
pkg-conversation rc=0 ℹ pass 182 ℹ fail 0 | ℹ duration_ms 33517.979983
pkg-discord rc=0 ℹ pass 183 ℹ fail 0 | ℹ duration_ms 2609.094649
pkg-harness rc=0 ℹ pass 133 ℹ fail 0 | ℹ duration_ms 11385.31588
pkg-ledger rc=0 ℹ pass 89 ℹ fail 0 | ℹ duration_ms 12841.758249
pkg-mosaic rc=0 ℹ pass 69 ℹ fail 0 | ℹ duration_ms 2087.375348
pkg-plugins rc=0 ℹ pass 111 ℹ fail 0 | ℹ duration_ms 250.708711
pkg-project rc=0 ℹ pass 37 ℹ fail 0 | ℹ duration_ms 1039.534021
pkg-runs rc=0 ℹ pass 43 ℹ fail 0 | ℹ duration_ms 725.337132
pkg-seat rc=0 ℹ pass 151 ℹ fail 0 | ℹ duration_ms 5814.69479
pkg-tasks rc=0 ℹ pass 97 ℹ fail 0 | ℹ duration_ms 1149.422432
pkg-webui rc=0 ℹ pass 239 ℹ fail 0 | ℹ duration_ms 57192.798712
script-auth rc=0 | selftest: 15 passed, 0 failed
script-conductor rc=0 | selftest: 17 passed, 0 failed
script-config rc=0 | selftest: 24 passed, 0 failed
script-discord rc=0 | discord suite: 78 passed, 0 failed
script-extension-package rc=0 | extension package selftest: 18 passed, 0 failed
script-foundation rc=0 | selftest: 44 passed, 0 failed
script-land-work rc=0 | land-work selftests: 86 passed, 0 failed
script-queue rc=0 ℹ pass 156 ℹ fail 0 | queue suite: 28 passed, 0 failed
script-release rc=0 | selftest: 14 passed, 0 failed
script-task rc=0 | selftest: 98 passed, 0 failed
GATE_PASSED
removed network sage-r64_default
+29
View File
@@ -0,0 +1,29 @@
manifest OK
base c6d7d538f dirty 0 DOCKER_HOST=unset
pkg-bus rc=0 ℹ pass 428 ℹ fail 0 | ℹ duration_ms 2962.041151
pkg-business rc=0 ℹ pass 149 ℹ fail 0 | ℹ duration_ms 1896.955166
pkg-cli rc=0 ℹ pass 349 ℹ fail 0 | ℹ duration_ms 118551.279573
pkg-control-board rc=0 ℹ pass 124 ℹ fail 0 | ℹ duration_ms 578.067759
pkg-conversation rc=0 ℹ pass 182 ℹ fail 0 | ℹ duration_ms 32999.614386
pkg-discord rc=0 ℹ pass 183 ℹ fail 0 | ℹ duration_ms 2725.161912
pkg-harness rc=0 ℹ pass 133 ℹ fail 0 | ℹ duration_ms 11394.510251
pkg-ledger rc=0 ℹ pass 89 ℹ fail 0 | ℹ duration_ms 12782.512329
pkg-mosaic rc=0 ℹ pass 69 ℹ fail 0 | ℹ duration_ms 2033.740426
pkg-plugins rc=0 ℹ pass 111 ℹ fail 0 | ℹ duration_ms 244.365573
pkg-project rc=0 ℹ pass 37 ℹ fail 0 | ℹ duration_ms 1031.796824
pkg-runs rc=0 ℹ pass 43 ℹ fail 0 | ℹ duration_ms 722.039369
pkg-seat rc=0 ℹ pass 151 ℹ fail 0 | ℹ duration_ms 5809.458583
pkg-tasks rc=0 ℹ pass 97 ℹ fail 0 | ℹ duration_ms 1123.624373
pkg-webui rc=0 ℹ pass 239 ℹ fail 0 | ℹ duration_ms 56273.900823
script-auth rc=0 | selftest: 15 passed, 0 failed
script-conductor rc=0 | selftest: 17 passed, 0 failed
script-config rc=0 | selftest: 24 passed, 0 failed
script-discord rc=0 | discord suite: 78 passed, 0 failed
script-extension-package rc=0 | extension package selftest: 18 passed, 0 failed
script-foundation rc=0 | selftest: 44 passed, 0 failed
script-land-work rc=0 | land-work selftests: 86 passed, 0 failed
script-queue rc=0 ℹ pass 156 ℹ fail 0 | queue suite: 28 passed, 0 failed
script-release rc=0 | selftest: 14 passed, 0 failed
script-task rc=0 | selftest: 98 passed, 0 failed
GATE_PASSED
removed network sage-r64_default
@@ -0,0 +1,28 @@
f4a4bc8bebf42c9cd897c4d7b3fea9635fff7c177fd2b881a4ab35d0492e4a3c adapters/claude/adapter.sh
19f73115a5768cd67eef24d904706d5df1ec5c5d8108c92cba6968fa4f56bc88 adapters/pi/adapter.sh
cb05dbe506967957c152d417046b0902fd407a58936af87f0100199c0d389235 adapters/README.md
2318a58523c9a0f93ea2c4e8439b761f8190480ee5ccb6f93466c878750b5ad4 packages/business/src/resolve.mjs
f24f27b05c3e02fa909c543e331b38ec6385892629ac0b731b52bf3a56d6a2cd packages/business/src/role.mjs
f5cf086b161112f88ab930f5b557ff7f832ebbd143b4e5291a295d4e60db31de packages/business/tests/resolve.test.mjs
609efd32e5e325d518c09f48765f587efb5d2ff794c656c09e3002bd1a70f543 packages/business/tests/role.test.mjs
0573985439a072378b49541136ec4d2091db1f14a9fa7137bbf7229872d8614b packages/cli/src/launcher.mjs
0e5f58ec412bf484714afd016e821deaad6228012c03bb0a709bd728a503f841 packages/cli/tests/channel-fixture.mjs
ec4bbbc98adab6561205e5ec8e2f2495f1f55224da53e567c7a72e36b5410709 packages/cli/tests/git-socket.test.mjs
e5c0aa0242ee92c474a7795074354123efafa8de497fcfeacfc87c8ca31efa32 packages/cli/tests/helpers.mjs
4ac87c566046746e1d91957b6451a562a2b07187eae3ab6958edf26ed73ad33f packages/cli/tests/instances.test.mjs
aad8187d8464b6fe88d8993948158fff7ac9ab7d07b4e670f32866d8ff41e85f packages/cli/tests/launcher.test.mjs
776c180a09ed9791b09d06f5f28f18d2e500c5e2b5ad19f93042a19787b5853f packages/cli/tests/thread-launch.test.mjs
a52268a146a121a9c479571672537912325001d0389a4a22a2d3c682ce283046 packages/harness/README.md
46d8b7159077683e4028fdc389bab1c5d4634a0bdb4e2889c58524838346cd05 packages/harness/src/bundle.mjs
d492c9a8059cc0cbc4e48d7ab5e053c0693d8a8bb99f900d4650db1b23de9838 packages/harness/tests/bundle.test.mjs
e81b3e0b1deb22d15680bc6dade4edea070f2d0abbfa972883dfe9fa2e5c62a0 packages/harness/tests/claude-session.test.mjs
8d1ae8daec5f89452c78fd64756b5c51210187b2ab17a76caffec2e0101da697 packages/harness/tests/helpers.mjs
0aad8e9bbd910d848b5e60a8b0009c50a803b908b533332fc440e64ccdfeebb4 packages/harness/tests/pi-session.test.mjs
d98f9e4283afb1c2dbc655710bde594ba8cbf65e5a15f2d2d6e170144afcab3c packages/harness/tests/skills.test.mjs
8583a53a1c16e8a570fe3f1e015fe23556bd89ba5d2baa88e3d1392dcf8e8234 packages/webui/tests/instances.test.mjs
0b4f0fba3f928f018f8b2cc7319e88ba3e46dfa9a40bc18b66f8c8d0fb13fbcd packages/webui/tests/routes-browser.test.mjs
8c3558e1bf640fc86a737100a0840b44cf003cb49057c6c2e5d5a6b90466aec1 packages/webui/tests/routes.test.mjs
13713c4e64ade3b1b1eedabed75f5f76fe65e73f6df336b771d1e34881d111f3 roles/coder.json
60b6482a29a578234b4e5bf0d4c9a123bd3a65a65dc6e488f474546cf4456bb2 roles/cto.json
1af1cb70512163eb5c39ae942496a4cc309b0b1d47b9cb48253ac788862f0e9f roles/pm.json
4d2b2ab1208aced3a82d7de1ace879574fae2930ac1bc684f76e1d16e58cec8f roles/reviewer.json
@@ -0,0 +1,14 @@
M1-no-launcher-precheck KILLED | launcher.test.mjs rc=1 ✖ row 64 a role skill that isn't enabled refuses skill-not-enabled, and one that isn't a plain direc
M2-not-enabled-mapped-refused KILLED | launcher.test.mjs rc=1 ✖ row 64 a role skill that isn't enabled refuses skill-not-enabled, and one that isn't a plain direc
M3-no-path-check KILLED | skills.test.mjs rc=1 ✖ a skill file that can't go into the prompt refuses: a bad name, non-UTF-8 bytes, or the end marker
M4-no-size-check KILLED | skills.test.mjs rc=1 ✖ a role's skills over SKILL_BYTES together refuse, naming the skill that crossed it (0.949446ms)
M5-no-end-marker-check KILLED | skills.test.mjs rc=1 ✖ a skill file that can't go into the prompt refuses: a bad name, non-UTF-8 bytes, or the end marker
M6-walk-follows-symlinks KILLED | skills.test.mjs rc=1 ✖ a skill directory that isn't plain refuses: a symlinked skill, SKILL.md or sub-file, or a non-file
M7-found-follows-symlinks KILLED | skills.test.mjs rc=1 ✖ a skill directory that isn't plain refuses: a symlinked skill, SKILL.md or sub-file, or a non-file
M8-no-newline-before-end KILLED | skills.test.mjs rc=1 ✖ each repo role's prompt carries exactly its listed skills, each file as the bundle copy and the so
M9-role-allows-dot KILLED | role.test.mjs rc=1 ✖ row 64 skills: optional, distinct skill directory names, none on version 1 (0.778394ms)
M10-resolve-drops-skills KILLED | resolve.test.mjs rc=0 ; skills.test.mjs rc=1 ✖ each repo role's prompt carries exactly its listed skills, each file as the bundle copy and the so
M11-claude-slash-on KILLED | skills.test.mjs rc=1 ✖ each adapter passes the skills only as --system-prompt text, with skill discovery off (14.743943ms
M12-claude-unrestricted KILLED | skills.test.mjs rc=1 ✖ each adapter passes the skills only as --system-prompt text, with skill discovery off (14.675833ms
M13-pi-skills-on KILLED | skills.test.mjs rc=1 ✖ each adapter passes the skills only as --system-prompt text, with skill discovery off (8.408382ms)
restored True
+59
View File
@@ -0,0 +1,59 @@
import os, signal, subprocess, sys, shutil
W='/mnt/storage/scratch/sage-r64/wt'
OUT='/mnt/storage/scratch/sage-r64/mutants'
os.makedirs(OUT, exist_ok=True)
BUNDLE='packages/harness/src/bundle.mjs'; LAUNCH='packages/cli/src/launcher.mjs'
ROLE='packages/business/src/role.mjs'; RESOLVE='packages/business/src/resolve.mjs'
CLAUDE='adapters/claude/adapter.sh'; PI='adapters/pi/adapter.sh'
SKT=('packages/harness','tests/skills.test.mjs')
LT=('packages/cli','tests/launcher.test.mjs')
RT=('packages/business','tests/role.test.mjs')
RST=('packages/business','tests/resolve.test.mjs')
M=[
('M1-no-launcher-precheck', LAUNCH, ' readSkills(resolved.skills, skillsDir);\n', '', [LT]),
('M2-not-enabled-mapped-refused', LAUNCH, 'e.code === "skill-not-enabled" ? "skill-not-enabled" : "skill-refused"', '"skill-refused"', [LT]),
('M3-no-path-check', BUNDLE, ' if (!SKILL_PATH.test(entry)) throw', ' if (false) throw', [SKT]),
('M4-no-size-check', BUNDLE, ' if (size.bytes > SKILL_BYTES) throw', ' if (false) throw', [SKT]),
('M5-no-end-marker-check', BUNDLE, ' if (text.includes(SKILL_END)) throw', ' if (false) throw', [SKT]),
('M6-walk-follows-symlinks', BUNDLE, ' const stat = lstatSync(path);', ' const stat = statSync(path);', [SKT]),
('M7-found-follows-symlinks', BUNDLE, ' try { return lstatSync(path); }', ' try { return statSync(path); }', [SKT]),
('M8-no-newline-before-end', BUNDLE, '\\n${f.text}\\n${SKILL_END}', '\\n${f.text}${SKILL_END}', [SKT]),
('M9-role-allows-dot', ROLE, ' || name === "." || name === ".."', '', [RT]),
('M10-resolve-drops-skills', RESOLVE, ' skills: [...definition.skills],', ' skills: [],', [RST, SKT]),
('M11-claude-slash-on', CLAUDE, ' --disable-slash-commands \\\n', '', [SKT]),
('M12-claude-unrestricted', CLAUDE, ' --restricted \\\n', '', [SKT]),
('M13-pi-skills-on', PI, 'SKILLS_FLAG="--no-skills"', 'SKILLS_FLAG=""', [SKT]),
]
files=sorted({m[1] for m in M})
BK={}
for f in files:
b=os.path.join(OUT, f.replace('/','__')+'.orig'); shutil.copyfile(os.path.join(W,f), b)
if open(b,'rb').read()!=open(os.path.join(W,f),'rb').read(): sys.exit('backup failed '+f)
BK[f]=b
try:
for name,f,o,n,tests in M:
orig=open(BK[f]).read()
assert orig.count(o)==1, name
mut=orig.replace(o,n)
if 'statSync(path)' in n and 'lstatSync' not in n:
imp='import { lstatSync, mkdirSync,'
assert mut.count(imp)==1, name+' import'
mut=mut.replace(imp,'import { lstatSync, statSync, mkdirSync,')
open(os.path.join(W,f),'w').write(mut)
results=[]
for pkg,test in tests:
lp=os.path.join(OUT,f'{name}--{os.path.basename(test)}.log'); log=open(lp,'w')
p=subprocess.Popen(['node','--test',test],cwd=os.path.join(W,pkg),stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try: rc=p.wait(timeout=300)
except subprocess.TimeoutExpired:
os.killpg(p.pid,signal.SIGKILL); p.wait(); rc='timeout'
log.close()
txt=open(lp).read()
fails=[l for l in txt.splitlines() if l.startswith('✖ ') and 'failing tests' not in l]
results.append((os.path.basename(test),rc,fails[0][:100] if fails else ''))
killed=any(r[1]!=0 for r in results)
print(name,'KILLED' if killed else 'SURVIVED','|',' ; '.join(f'{t} rc={rc} {fl}' for t,rc,fl in results),flush=True)
shutil.copyfile(BK[f],os.path.join(W,f))
finally:
for f in files: shutil.copyfile(BK[f],os.path.join(W,f))
print('restored', all(open(BK[f],'rb').read()==open(os.path.join(W,f),'rb').read() for f in files), flush=True)