# Row 247 build notes, C2-20b persona import follow-up (#1749) Brief: docs/plans/2026-10-20_case2-rows.md "C2-20b persona import follow-up: printed keys, the dry run fence check and the mkdir race", landed in 335bb3e30. These are Webby's row 221 notes N1, N2 and N4 (webby-0073, REVIEW-r1 a0a00630e on webby/reviews). N3 is in DEFERRED (sage-1941). ## Base Branch gosalyn/c2-20b from 335bb3e30, which is row 221's landing 300766450 plus the brief. ## What changed - N1, `scripts/case2-persona-import.mjs`: the per-channel prompt line prints the platform key, with its fixed `platforms.` or `gateway.platforms.` prefix, and the channel key through `JSON.stringify`. A key holding a newline prints as `\n` inside one quoted line. The profile name on that line is not config text: `pairing` already holds it to `PROFILE_ID`. - N2: the dry run calls `personaTextFault` on each persona after its count line. A fault prints as `profile P: persona , so C2-08's fence would refuse it`, where the reason is the fixed string `personaTextFault` returns, never persona text or a line number. With any fault the run refuses, exit 1, and writes nothing, as stage would. The header comment no longer says a fault is counted, not refused, and now says a SOUL.md over four times the fence in bytes refuses before any count, which is what `soul()` already did (Webby's second point under N2). - N4: `privateDir` catches an EEXIST from its mkdir and refuses with ` was made by something else between the check and mkdir`. Any other mkdir error is thrown as before, since the brief names only EEXIST. - Tests, `packages/cli/tests/case2-persona-import.test.mjs`: - The per-channel prompt test expects the quoted keys. - New: a chet config.yaml with a newline in the platform key and in the channel key prints one quoted line, and no output line starts with the forged text. - The dry run heading test now expects the fence fault line, exit 1 and the refusal, still with nothing written. Its persona has two heading lines, which the fence refuses. - New: a persona of 8001 two-byte characters, 16002 bytes and under the 32000 byte cap, makes the dry run print the over 8000 characters fault, write nothing and exit 1. At exactly 8000 it exits 0. - New: the mkdir race refuses by name at stage, for the staging directory, and at install, for `personas/`. Each refused directory is left empty. - New fixture `packages/cli/tests/fixtures/case2-persona-import/mkdir-race.mjs`, an `--import` preload like `trace.mjs`. It makes the directory named by `$PERSONA_MKDIR_RACE` just before the script's own `mkdirSync` of that path, so the script meets a real EEXIST from the kernel, not a thrown stub. ## Known limit of N1 `JSON.stringify` escapes C0 controls, `"` and `\`, but leaves DEL, C1, U+2028, U+2029 and the bidi controls raw. None of those ends a line on a terminal, so the forged line N1 names can't happen. A bidi control could still reorder how the rest of that one line displays. The brief asks for `JSON.stringify`, and Sage kept it in sage-1964 because the keys come from Jason's own Hermes config. Note for Webby: row 231's `quote()` in packages/library covers the wider set, and this row doesn't import it. If you want it, ask for it in review. ## Mutants One per fix, plus a second for N1 so each of its two keys has its own. Each replaces one exact string in the script, runs the test file, then restores the script from a copy and checks its sha256. | Mutant | Fix | Change | |---|---|---| | M1 | N1 | the channel key prints raw | | M2 | N1 | the platform key prints raw | | M3 | N2 | the dry run's refusal after a fault is skipped | | M4 | N4 | privateDir's EEXIST branch never matches, so EEXIST is thrown raw | ## Outside readers `git grep` over packages/*/tests/* and scripts/test-*.sh finds `case2-persona-import` only in its own test file and in two files of its fixture tree, README.md and trace.mjs, which name the test file in a comment. No test pins a hash of the script. ## Tests Two slot runs, `busy.sh && run.sh` in /mnt/storage/scratch/gosalyn-r247, on the test file alone. Run 1, grant sage-1969, 11:47:45Z to 11:48:00Z, was red: 14 tests, 12 pass, 2 fail. The fault was in the script. Adding `JSON.stringify` to the per-channel line dropped the space after the comma, so it printed `"-1001",29 bytes`. The two per-channel tests caught it. run.sh also went on to the mutants with a red base, so their counts meant nothing. Sage approved both fixes in sage-1971: the space goes back, and run.sh now exits 3 before the mutants unless the base is rc=0 with fail 0. Run 2, grant sage-1971, 11:49:04Z to 11:49:22Z, was green: 14 tests, 14 pass, 0 fail, rc=0. | Mutant | Line | rc | Failed | Killed by | |---|---|---|---|---| | M1 | 237 | 1 | 2 | the per-channel prompt test and the newline key test | | M2 | 237 | 1 | 2 | the same two | | M3 | 238 | 1 | 2 | the dry run fence test and the dry run heading test | | M4 | 192 | 1 | 1 | the mkdir race test | After the run the script's sha256 was 7031d2c0dc9bd22bc748b364a35b461fc01cb98b795032a2e0ec436f3188d4e4, the same as the copy taken before it. The /proc scan found no process in the worktree or the scratch dir.