Files
jason.woltje 3007664504 row 221: C2-20 Bean and Chet persona import, stage and install script with a count-only dry run, personaTextFault exported from bundle.mjs, yaml 2.9.0 exact, fixture Hermes home and read trace (#1722)
Lands Gosalyn's round 1 candidate, 8f403d0d4 on gosalyn/c2-20-r2 (48f044ccc plus its BUILD.md, on
62ddc96cc), approved by Webby (webby-0073, REVIEW-r1 a0a00630e, rev 2808). 22 paths, manifest
20f05e3a6ad5. No 221 path changed between 62ddc96cc and this commit's parent. Patch-id dfbb754c6fb4, the
same as 62ddc96cc..8f403d0d4. The two state.db fixtures and auth.json are short text stand-ins.

Webby's N1, N2 and N4 go to the C2-20b follow-up row before Jason's import run. N3 (mutants.py:48
anchor) is in DEFERRED.md. The script itself runs only under Jason's go (design 10 step 6, 9.1).

Evidence: Gosalyn's singles on my slot, 419/0 on 48f044ccc (gosalyn-0104).
Gate on my slot, 11:07:50-11:19Z on b1fece6f0 plus the 22 paths: case2-persona-import.test.mjs alone
11/0, harness 140/0 (bundle.mjs changed), cli 677/0, and every scripts/test-*.sh green (auth 15,
conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 and 28,
release 14, task 98, all 0 failed). Lands before row 197 r7: no shared paths, and r7's proof candidates
both carry it (sage-1954).
2026-10-21 06:19:41 -05:00

8.9 KiB

Row 221 build notes, C2-20 Bean and Chet persona import (#1722, Vikunja #416)

Brief: docs/plans/2026-10-20_case2-rows.md "C2-20", blob 16847e94ae (sage-1834). Claimed rev 2736. Rulings: sage-1836 (fence, pairing) and sage-1840 (YAML parser, handoff).

Base

Built on Morgana's row 213 r4 tip 2b3e67e36 (origin/morgana/row213-r4), since the brief says "After: C2-08" and 221 exports 213's fence check. Before review it goes onto 213's landing under a new branch name, and its tests run again there.

What changed

  • scripts/case2-persona-import.mjs, new. Verbs stage and install, usage in its header.
  • packages/cli/tests/case2-persona-import.test.mjs, new, and packages/cli/tests/fixtures/case2-persona-import/, a fixture Hermes home plus trace.mjs, the read trace.
  • packages/harness/src/bundle.mjs: the three text rules in readPersona move into an exported personaTextFault(text), which returns null or the reason, and readPersona calls it. Nothing it checks changes and the messages are byte for byte the same, so 213's creative tests hold. There is one copy of the rules and the import calls it (sage-1836).
  • package.json and package-lock.json: yaml 2.9.0 becomes an exact direct dependency (sage-1840 Q3).

Rulings recorded

Profile to name (sage-1836, answers DESIGN.md:1322 as an implementation ruling, not a PRD change). Hermes at df5700ebe has no field holding a profile's own name. profile.yaml has only description and description_auto (hermes_cli/profiles.py:798-850), and agent_name at cli.py:3494 is skin branding with the default "Hermes Agent". So the human passes the pairing as --pair default=Bean --pair chet=Chet. The script checks both profile directories exist, each name is exactly Bean or Chet and the two differ, then prints the pairing with each SOUL.md's byte count and sha256 before it writes. The brief's "confirm profile to name" is met by the human supplying and confirming the pairing.

The persona is <profile dir>/SOUL.md, where Hermes loads it (agent/prompt_builder.py:1829-1855). The default profile's directory is the Hermes root, and others are profiles/<name>, with names checked against hermes_cli/profiles.py:37. A missing or blank SOUL.md is "no persona", as in Hermes. The file is staged byte for byte. Hermes strips it and runs its own scan before use, but this import copies her text unchanged.

Handoff between the steps (sage-1840 Q4). stage writes 0600 files owned by whoever ran it. install runs as <user> and refuses a staging file that isn't its own or has a group or other bit. The script changes no owner. It prints the root step, chown -R <user>: <staging>, as an instruction. Item for Jason's go: who runs stage on <host>, and the root chown between the two steps.

Word list

69 tool names: every literal name="..." in a registry.register( call under hermes-agent's tools/*.py at df5700ebe, plus Hermes and Discord. Two more registrations, in tools/mcp_tool.py:4997 and :5034, and plugin tools (hermes_cli/plugins.py:430) get their names at run time, so a fixed list can't hold them. Matching is whole word, any case. memory, terminal, todo, process, patch and clarify are ordinary words, so a persona line using them in prose counts, and the count leans high. It is a hint for Jason and changes nothing in the import.

Channel prompts

Hermes takes a platform's block from <platform> at the top level of config.yaml, else from platforms.<platform> or gateway.platforms.<platform> (gateway/config.py:1159-1178), and its prompts from channel_prompts (:1233). The script lists all three by platform, channel id and byte count, and says "not text" for a value that isn't a string. The dry run doesn't read config.yaml.

yaml parses with intAsBigInt, so an unquoted 18 or 19 digit Discord id prints exactly instead of rounding as a double, and with uniqueKeys: false, so a repeated key keeps the last value as Python's safe_load does. logLevel: "silent" stops yaml's warnings, which quote config text. yaml reads YAML 1.2 and Hermes's PyYAML reads 1.1. They differ on scalars such as yes and 0123, but neither changes a channel prompt's key or its byte count for a quoted or decimal id.

yaml dependency (sage-1840 Q3)

Made with npm install --save-exact [email protected] --package-lock-only --ignore-scripts at the worktree root. git config core.hooksPath was unset before and after. npm moved nothing else:

 package-lock.json | 3 ++-
 package.json      | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)
@@ -10,7 +10,8 @@
       "license": "UNLICENSED",
       "dependencies": {
         "@earendil-works/pi-coding-agent": "0.85.1",
-        "oauth4webapi": "3.8.8"
+        "oauth4webapi": "3.8.8",
+        "yaml": "2.9.0"
       }
     },

node_modules/yaml is 2.9.0, from the pinned pi packages (package-lock.json:502 and :556).

Where it runs. scripts/case2-persona-import.mjs isn't in stack-copy's COPY_PATHS, and a copy carries no node_modules of its own beyond its npm ci. So the script runs from a checkout at the reviewed commit after npm ci, where import "yaml" resolves through the root package.json and ../packages/harness/src/bundle.mjs is beside it. Whoever runs stage needs read access to that checkout and to her Hermes home, which is part of the item for Jason's go above.

Tests

Round 1, on 213 r4 2b3e67e36, slot grants sage-1851 and sage-1863. One node --test per file.

Run 1 started at 08:19:44Z. A 600 s tool timeout on my wrapper killed it at about 08:29:45Z, inside stack-release-external, after stack-release took 494 s. One test in this row's file failed in run 1. Its filter includes("per-channel prompt") also matched the "profile chet: no per-channel prompts" line that the next assert checks on its own. That was a test defect, and the script's output was right. The filter is now includes(": per-channel prompt "). The kill orphaned one fixture holder.cjs, which I killed, and I removed its tmp dirs. Sage counts this as one failure under mos-0823.

The rerun, 08:31:11Z to 08:33:31Z, covered this row's file, stack-release-external, stack-upgrade and the fixture dry run. The 15 files that run 1 finished green stand on code the fix didn't touch. Their counts come from run 1.

File pass fail
packages/business/tests/launch-settings.test.mjs 16 0
packages/cli/tests/stack-copy.test.mjs 16 0
packages/cli/tests/stack-release.test.mjs 195 0
packages/conversation/tests/cohort.test.mjs 37 0
packages/harness/tests/bundle.test.mjs 22 0
packages/harness/tests/claude-gate.test.mjs 3 0
packages/harness/tests/claude-session.test.mjs 8 0
packages/harness/tests/creative.test.mjs 7 0
packages/harness/tests/mcp-server.test.mjs 5 0
packages/harness/tests/pi-session.test.mjs 8 0
packages/harness/tests/skills.test.mjs 10 0
packages/plugins/tests/harness.test.mjs 9 0
packages/seat/tests/sandbox.test.mjs 10 0
packages/webui/tests/editor.test.mjs 5 0
packages/webui/tests/pinned-copy.test.mjs 10 0
packages/cli/tests/case2-persona-import.test.mjs 11 0
packages/cli/tests/stack-release-external.test.mjs 25 0
packages/cli/tests/stack-upgrade.test.mjs 20 0

Total over the 18 files: 417 pass, 0 fail, 0 cancelled, summed by awk.

The dry run on the fixture, stage --hermes-root packages/cli/tests/fixtures/case2-persona-import/hermes --pair default=Bean --pair chet=Chet --dry-run, exited 0 and printed:

files 2
profile default: lines 3, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0
profile chet: lines 2, lines starting "# " or "## " 0, lines naming a Hermes tool or channel 0

Rebase onto 213's landing (sage-1925)

213 landed as 361ba2387, with the same git patch-id --stable as r4 2b3e67e36, f313e69b6f7e. The refactor tip was 62ddc96cc. I cherry-picked 2d7a99aee onto it under the new branch gosalyn/c2-20-r2 as 48f044ccc. The patch-id is eb094ae783e9 before and after. None of the 22 paths changed between 2b3e67e36 and 62ddc96cc. The fence export from sage-1836 Q1 was already in the commit: personaTextFault from packages/harness/src/bundle.mjs, called by the script. I checked the test list again on the new base by grepping tests and scripts/test-*.sh for each non-test path's basename. The new hits name other packages' package.json files or write lockfiles in temp dirs, so the 18 files stand. 231 landed after that as 29ce1e360. It touches only packages/library and the queue-231 packet, none of these 22 paths.

Rerun on 48f044ccc, slot grant sage-1932, 10:18:09Z to 10:30:14Z, after a busy check. All 18 files ran whole, one node --test each, every rc 0: 419 pass, 0 fail, 0 cancelled, summed by awk. That's two more than the 417 above, both from the base. stack-release went from 195 to 196 and stack-upgrade from 20 to 21, and rows 238 and 240 changed those test files. Every other file has the count in the table above. The dry run printed the same three lines, rc 0.