examples/acp-agent/cordis.snapshot.yml is a 26-line declarative overlay: one entry mounts @cordisjs/plugin-include on ./cordis.yml with patches that disable the llm-deepseek entry by id and insert llm-replay. Every other entry is the live tree loaded through the include, so the replay tier exercises exactly what ships and an app-shape change lands once — the silent-drift class the hand-mirrored 125-line twin invited is structurally gone. The bin is untouched; recording still boots cordis.yml; assertEntriesLoaded tolerates the disabled entry by design. All snapshot scenarios pass unchanged, byte-identical goldens included; the include applies patches at load time only, which a one-shot replay boot is exactly. Implements docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md (moved from proposed/ and amended); the acp-snapshot-tests and hook-snapshot-matrix RFCs' replay-config facts are amended in the same change.
3.1 KiB
RFC: Single-source the acp-agent replay config
Status: implemented
Problem
examples/acp-agent shipped two hand-maintained configs: cordis.yml (the live tree) and a cordis.snapshot.yml that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line llm-deepseek stanza versus the two-line llm-replay stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapshot tier would silently exercise a different app than the one that ships — the "green units, broken product" class of gap the snapshot tier exists to close, reintroduced one level up, with reviewer vigilance as the only defense.
Decision
cordis.snapshot.yml is a declarative overlay, not a copy: its single entry mounts @cordisjs/plugin-include on ./cordis.yml with patches that disable the llm-deepseek entry by id and insert the llm-replay entry (the vendored include plugin's patch mechanism: by-id overrides with a name-assertion guard, plus top-level inserts). Every other entry — the app, the bash executor, the fs/subagent/todo tools, both hook bridges, the system prompt — is the live tree itself, loaded through the include, so replay exercises exactly what ships and an app-shape change lands once. The dsh-acp-agent bin is untouched (it still just selects this file for DSH_SNAPSHOT=replay); recording still boots cordis.yml directly; the bin's assertEntriesLoaded guard tolerates the disabled entry by design (a disabled entry is the one legitimate fiber-less state).
One vendored-plugin fact the overlay depends on, deliberately: the include applies patches when it loads the file — its refresh()/internal/update paths re-read without re-patching — which is exactly enough for a one-shot replay boot (the replay app loads no hmr and nothing rewrites the config mid-run). The snapshot suite is the proof: all scenarios pass unchanged on the overlay, byte-identical goldens included.
Why not the alternatives?
Keeping the full twin with a symmetry verify-gate was the recorded fallback — it would have removed the silent-drift class but kept a 125-line near-copy whose only content was one entry's difference, growing with every plugin the app gains. A bin-side swap (parse the config, replace the entry, delete the file) would have put YAML surgery inside a published artifact and moved the replay delta out of sight; the overlay keeps the delta declarative, readable, and next to the base config — the teaching value the twin's defenders actually wanted.
Consequences
- A plugin added to
cordis.ymlis in the replay tree with no second edit; the drift class is structurally gone rather than gated. - The overlay depends on entries carrying stable
id:s — which the live config already does, and which the include's name-assertion patch guard makes checkable. - If a future replay tree needs a second divergence (another backend swapped), it is one more patch line, not a second fork of the file.