Version Compatibility Doctrine
How to set min_herdr_version and evolve a plugin without breaking installed users. The doctrine comes from qu8n/herdr-automatic-rename, whose manifest documents it, reinforced by collie’s ADR 0006 and herdr core’s own wire-compatibility mindset. Source mirror: _repos/qu8n-herdr-automatic-rename/.
The asymmetry that drives everything
Two kinds of forward compatibility exist in herdr, and they fail differently:
| Surface | Older herdr encounters it | Failure mode |
|---|---|---|
min_herdr_version above running herdr | Refuses to load | Hard: plugin_requires_newer_herdr, re-checked on every dispatch |
Unknown event name in [[events]] | Install-preview warning | Soft: section ignored at runtime |
| Unknown env var / unknown manifest field | Usually ignored | Soft |
From qu8n’s manifest, verbatim in spirit:
# min_herdr_version stays at 0.7.1 deliberately. A requirement above the running
# herdr is a HARD load failure (plugin_requires_newer_herdr), re-checked on every
# event dispatch, whereas an event name an older herdr does not know is only an
# install-preview warning. Newer-herdr features are therefore version-gated at
# runtime instead.
min_herdr_version = "0.7.1"
So raising the floor punishes every user on an older herdr, on every single event. Declaring a newer event costs nothing. The doctrine follows directly:
- Keep
min_herdr_versionat the oldest herdr that actually works. Not your dev version. - Gate newer features at runtime, not in the manifest.
- Declare newer events freely; old herdr warns at install and ignores them at runtime.
Runtime gating in practice
qu8n gates a newer-API feature behind a capability check (ar_agent_prefix_ok in its code). In Rust the same idea looks like:
fn supports_reorder_events() -> bool {
// Probe a field or method that only exists in newer herdr, or
// parse `herdr --version` once and cache it.
herdr_at_least(0, 8, 0)
}
fn main() -> anyhow::Result<()> {
let cmd = std::env::args().nth(1).unwrap_or_default();
match cmd.as_str() {
"run" if supports_reorder_events() => reconcile_full(),
"run" => reconcile_without_reorder(),
_ => reconcile_full(),
}
}
[[startup]] is a worked example: it appeared in herdr 0.7.5. qu8n declares it while keeping min_herdr_version = "0.7.1", and notes that older herdr ignores the whole section. A restored session on an old herdr just waits for an unrelated event before the first pass.
Frozen command strings: collie’s rule
Runtime gating covers your code, but the manifest commands themselves are cached by older herdr at install time. collie’s ADR 0006 records the incident: on herdr < 0.8.0, a managed install invokes the action set cached at install, so changing the bytes of bash scripts/collie-ctl.sh <verb> broke old installs. Rules:
- Manifest command strings are a public API with backward-compat obligations.
- Change behavior behind the entrypoint (script, binary), never the string that invokes it.
- If you must rename a binary, keep the old string working (a shim) for a transition window.
The general lesson
herdr core’s own AGENTS.md applies the same discipline to its wire protocol: treat anything another process may have cached or parsed as immutable, extend rather than mutate. For plugin authors the surfaces are:
| Surface | Who caches it | Compat obligation |
|---|---|---|
| Manifest command strings | herdr installs (< 0.8.0) | Freeze bytes |
| Event payload fields | your own handlers | New fields may appear; read defensively (#[serde(default)]) |
| State file format | your plugin | Version it (pane-topic-sync stores "version": 2 and reads v1) |
| Config file keys | your users’ files | Never rename; add new keys, keep old ones honored |
Defensive payload parsing in Rust:
#[derive(serde::Deserialize)]
struct AgentStatusEvent {
pane_id: String,
agent_status: String,
#[serde(default)] pub agent: Option<String>,
#[serde(default)] pub title: Option<String>,
// newer herdr may add fields; serde ignores unknown keys by default
}
Pre-release checklist
# Does it actually run on the declared floor? Install an old herdr and check.
herdr plugin install ./plugins/tab-topic # on the oldest supported herdr
# Do unknown-event warnings appear only for events you intend to be new?
# Does every manifest command string still match the previous release byte-for-byte?
git diff HEAD~1 -- plugins/*/herdr-plugin.toml