Agent-config emitters
Date: 2026-08-22
Status: implemented 2026-08-23 in d29c604. One addition the spec did
not anticipate: the doctor found a state the installer could not reach --
it reported four missing surfaces that nothing created -- so
step_mcp_wiring now registers them rather than only reporting them. A
check for an unreachable state tells every user their install is broken.
Roadmap: rescopes spec 5 of 2026-08-21-architecture-roadmap.md
Depends on: nothing. See "What changed since the roadmap".
What changed since the roadmap
The roadmap made spec 5 depend on spec 4 and generate from the swarm.toml
that spec 4 would have produced. Spec 4 is closed
(2026-08-22-swarm-refutation.md): the swarm was measured at 7.3/15 against
8/15 for doing nothing, and there will be no swarm.toml.
The need it named did not go away — it moved. Agent definitions still exist,
they are just produced by AGENT_SURFACES instead of a swarm config, and they
are currently written by hand. So the emitter generates from
AGENT_SURFACES, which exists and is already the single source of truth for
what a surface contains.
Problem, demonstrated live
~/.hermes/config.yaml on this machine contains five attestation entries: the
full attestation server, plus attestation-feed, attestation-provenance,
attestation-knowledge, attestation-symbolic, each setting ATTEST_TOOLS
and each enabled: false.
install.py's step_mcp_wiring writes exactly one of those — the full
attestation entry — and returns ok the moment it sees the string
attestation in hermes mcp list. The four surface entries were typed by
hand during this session and nothing in the repo knows they exist.
The consequences are already latent:
- Nothing detects drift. Add a fifth surface to
AGENT_SURFACESand the config keeps offering four. Rename one and the config points at a name that now raises —register_allraises on an unknownATTEST_TOOLSrather than silently serving everything, so the failure is loud, but it happens at the user's next tool call rather than at install time. - The doctor cannot see it.
mcp_wiringmatches on a substring. A config with the full server and zero surfaces reportsok, which is the same false-clean the scheduler check had before it learned to look for a second entry. - A fresh install gets none of them. Anyone who runs
attest installtoday gets the flat surface, which measured 8/15 — not the four surfaces, which are the reason the surfaces exist.
This is a source-of-truth problem with two copies and no check between them.
Scope
One generator, two consumers, one check.
The generator
AGENT_SURFACES gains, alongside each surface's tool prefixes, the prose a
config needs: a one-line description and the model hint. Today that prose
exists only in a comment above the table and in the hand-typed YAML.
@dataclass(frozen=True)
class Surface:
prefixes: frozenset[str]
summary: str # one line, shown in the agent picker
rationale: str # why this is its own session — for the emitted comment
AGENT_SURFACES becomes dict[str, Surface]. _allowed reads
.prefixes; the change is mechanical and the existing architecture tests that
enforce namespacing keep passing unchanged.
The rationale strings already exist as the comment block above the table and as
the "why it is its own agent" column in 2026-08-22-agent-surfaces-design.md.
Moving them into the data is the point: prose that explains a config should
live where the config is generated from, or it rots separately from it.
Consumer 1 — Hermes MCP entries
emit_hermes_config() -> dict returns the mcp_servers fragment: one entry
per surface, command: uv, args run --project <root> attest-mcp, env
ATTEST_TOOLS: <name>, enabled: false.
enabled: false is the correct default and stays. A surface is chosen at
launch by a person, per the refutation's finding; five servers all enabled
would put every tool back in one session and undo the split.
Consumer 2 — Claude Code subagents
emit_claude_agents() -> dict[str, str] returns .claude/agents/<name>.md per
surface, with frontmatter carrying description (the summary) and the tool
allow-list. Same schema, different runtime — which is the roadmap's actual
point: "One schema, many consumers, so the definitions cannot drift."
The check
attest install --check gains surface verification, and this is the part that
earns the spec.
Following the scheduler fix from the same review: the doctor must compare
against the generator, not against a substring. For each surface in
AGENT_SURFACES, the config must have an entry whose ATTEST_TOOLS matches
and whose args point at this checkout. Report:
- missing — surface exists, config does not have it
- stale — config entry points at a different checkout path
- orphaned — config has an
attestation-*entry naming a surface that no longer exists
Orphaned is the one that matters most and the one a substring check can never find. It is the same shape as the duplicate crontab entry: a hand-added thing that used to be right, that the tool that owns the domain cannot see.
Hand-edited emitted files
The roadmap asked how to detect one. The answer is to not need to.
Do not checksum, and do not overwrite. A checksum turns a user's edit into a warning they cannot act on, and silently overwriting is worse. Instead:
- Emitted files carry a header naming their generator and the command to regenerate.
--checkreports a difference between generated and on-disk asBROKENwith a diff, and never rewrites.- Writing happens only on explicit
attest emit --write.
A user who deliberately edits an emitted file gets a persistent, accurate
report that it differs, and the choice of what to do about it. That is the same
contract _write_refresh_script already uses ("missing or stale"), so it is
this repo's existing convention rather than a new one.
Where emission lives
A CLI subcommand, attest emit, and a call from install.py. Not a build
step: there is no build, and adding one to keep two YAML fragments in sync is
disproportionate. Not install-only: a user who adds a surface should not have
to re-run an installer to get its config.
step_mcp_wiring calls the same generator so install and emit cannot diverge —
which is the whole failure being fixed, and it would be an embarrassing one to
reintroduce in the fix.
Success criteria
attest emit --checkreports BROKEN on this machine's current config only after a surface is added or renamed, andokfor the four entries as they stand — verified by adding a fifth surface, observing BROKEN, and removing it.- Adding a surface to
AGENT_SURFACESand runningattest emit --writeproduces a working Hermes entry with no hand editing. - Renaming a surface produces
orphanedfor the old entry, not silence. - A hand-edited emitted file is reported, never overwritten.
install.pyandattest emitproduce byte-identical output for the same surfaces — asserted by a test, because two callers of one generator is exactly where this class of bug returns.- Full pre-commit gate green.
Out of scope
Emitting for runtimes nobody here uses. Two consumers is enough to prove the schema is not shaped around one of them; a third is speculative until someone wants it.