Lands Gosalyn's round 1 candidate,8f403d0d4on gosalyn/c2-20-r2 (48f044cccplus its BUILD.md, on62ddc96cc), approved by Webby (webby-0073, REVIEW-r1a0a00630e, rev 2808). 22 paths, manifest 20f05e3a6ad5. No 221 path changed between62ddc96ccand 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 on48f044ccc(gosalyn-0104). Gate on my slot, 11:07:50-11:19Z onb1fece6f0plus 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).
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. Verbsstageandinstall, usage in its header.packages/cli/tests/case2-persona-import.test.mjs, new, andpackages/cli/tests/fixtures/case2-persona-import/, a fixture Hermes home plustrace.mjs, the read trace.packages/harness/src/bundle.mjs: the three text rules inreadPersonamove into an exportedpersonaTextFault(text), which returns null or the reason, andreadPersonacalls 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.jsonandpackage-lock.json:yaml2.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.