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:

SurfaceOlder herdr encounters itFailure mode
min_herdr_version above running herdrRefuses to loadHard: plugin_requires_newer_herdr, re-checked on every dispatch
Unknown event name in [[events]]Install-preview warningSoft: section ignored at runtime
Unknown env var / unknown manifest fieldUsually ignoredSoft

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:

  1. Keep min_herdr_version at the oldest herdr that actually works. Not your dev version.
  2. Gate newer features at runtime, not in the manifest.
  3. 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:

SurfaceWho caches itCompat obligation
Manifest command stringsherdr installs (< 0.8.0)Freeze bytes
Event payload fieldsyour own handlersNew fields may appear; read defensively (#[serde(default)])
State file formatyour pluginVersion it (pane-topic-sync stores "version": 2 and reads v1)
Config file keysyour users’ filesNever 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