emit
Agent configs generated from AGENT_SURFACES, and drift detected against it.
The four attestation-<surface> entries in ~/.hermes/config.yaml were typed
by hand. install.py writes only the single full-surface attestation entry
and its doctor matched on the substring "attestation", so a config with the
full server and zero surfaces reported ok -- the same false-clean the scheduler
check had before it learned to look for a second entry.
Two copies of one fact with no check between them is the whole problem, so
there is exactly one generator here and every consumer calls it, install.py
included. Emitting from two places to keep two files in sync would be an
embarrassing bug to reintroduce inside its own fix.
Nothing here overwrites. A difference between generated and on-disk is
reported with a diff and left alone; writing happens only on an explicit
attest emit --write. A checksum would turn a user's deliberate edit into a
warning they cannot act on, and silently rewriting it is worse. This is the
contract _write_refresh_script already uses ("missing or stale"), so it is
this repo's existing convention rather than a new one.
Finding
dataclass
One disagreement between the generated config and the one on disk.
Surface
dataclass
One agent's remit: what it may see, and why it is its own session.
check_hermes(servers, root)
Compare an on-disk mcp_servers mapping against what this table says.
Compares against the generator, not against a substring. orphaned is the
one a substring check can never find and the reason this exists: an entry
naming a surface that no longer exists still launches, and
register_all raises on it -- loudly, but at the user's next tool call
rather than at install time.
claude_agents(root)
.claude/agents/<name>.md bodies, one per surface.
Same table, different runtime. The tool list is the surface's prefixes rather than an enumeration: a new tool in a namespace joins its agent without anyone remembering to add it here.
hermes_servers(root)
The mcp_servers fragment: one entry per surface.
enabled: false is correct and stays. A surface is chosen at launch by a
person -- that is the finding the split rests on -- and five servers all
enabled would put every tool back into one session.
parse_config_dump(text)
hermes config get mcp_servers output, as a mapping.
Not a YAML parser and not trying to be -- the same call _config_shape in
ledger_adapters/generic.py makes, and for the same reason: this input is
machine-generated, two levels deep, and unquoted, and taking on a parser
dependency for it is how this codebase got burned before.
hermes mcp list is the obvious alternative and is the wrong one: it
truncates args to a fixed column width, so every entry would look stale.