Adapter conformance canary¶
Upstream agent CLIs release on their own schedule, and a release can break an adapter contract (rename a flag, drop a subcommand, change output shape) without any change on our side. Version-floor advisories (bernstein doctor, see src/bernstein/adapters/advisories.py) warn about known-old versions, but they cannot catch a fresh upstream release breaking a contract. The canary closes that gap: adapter breakage becomes our finding, not a user's broken unattended run.
What runs¶
A nightly workflow (.github/workflows/adapter-conformance-canary.yml) drives scripts/adapter_canary.py, which runs the canary matrix defined in src/bernstein/adapters/canary.py:
- every primary adapter (
agy,aider,claude,codex,copilot,gemini,opencode,qwen) against whatever upstream version the runner installed that night; - one pinned tiny goal on the cheapest usable model per adapter, so daily spend stays bounded and run-to-run diffs isolate upstream drift;
- the same in-process conformance check operators get from
bernstein adapters check: the installed binary's--helpsurface is matched against the adapter's declared contract (tests/contract/contracts/<adapter>.yaml).
Every probe is a receipt¶
Each probe seals a content-addressed receipt: canonical JSON whose SHA-256 is its identity. Two runs that observed the same upstream surface at the same timestamp produce byte-identical receipts; a mutated receipt fails verification (bernstein.adapters.canary.verify_canary_receipt) exactly like a tampered chain entry. Receipt hashes are mirrored into the HMAC audit chain (adapter.canary_receipt events), so a canary finding is reconstructable offline rather than living only in a CI log.
Regression handling¶
- A regression must repeat: two consecutive failures with the same failure fingerprint are required before the canary proposes an issue, so one upstream flake never pages anyone.
- A
--helpthat advertises none of a contract's required tokens (or prints nothing) is classified as an inconclusiveskip, not afail-- an installed CLI cannot legitimately drop its entire required surface in one release, so this signals a broken, paginated, or wholesale-redesigned--help(or a shim binary onPATH) that an operator must investigate, rather than genuine per-flag drift. A partial miss (at least one required token still present) remains a real driftfail. Theskipis independent of the process exit code. - Issues are deduped on the failure fingerprint (adapter + version + failure lines): the same regression never opens two issues, while a new upstream version failing fresh reports again.
- The opened issue carries the failing conformance transcript and the receipt hash, so the finding is reproducible from the issue alone.
Last-green table¶
The table below is regenerated by the canary from passing receipts; it is a projection, never a hand-maintained list. Primary adapters without a passing receipt (currently aider) have no last-green row, so the primary-adapter list above and this table diverge until the next green canary run for that adapter. Each row names the receipt hash prefix that attested it. bernstein doctor reads the same projection (shipped as src/bernstein/adapters/last_green.json) and warns when a locally installed agent version is ahead of last-green: the canary has not verified that release yet, so unattended runs are one upstream regression away from failing without warning.
| Adapter | Binary | Last-green version | Verified | Receipt |
|---|---|---|---|---|
| agy | agy | 1.0.0 | 2026-07-11T05:57:23Z | 006fb946868d |
| claude | claude | 2.1.212 | 2026-07-17T07:10:19Z | 34bdc4899fa4 |
| codex | codex | 0.144.5 | 2026-07-17T07:10:19Z | 3e470313f9ec |
| copilot | copilot | 1.0.71 | 2026-07-17T07:10:19Z | a5ded4092349 |
| gemini | gemini | 0.51.0 | 2026-07-17T07:10:19Z | 5799d19eba74 |
| opencode | opencode | 1.18.3 | 2026-07-17T07:10:19Z | cb48ced6635e |
| qwen | qwen | 0.19.11 | 2026-07-17T07:10:19Z | 13a2edf1b3cd |
Operator knobs¶
- Pin an adapter for unattended runs to its last-green version when doctor warns.
- Run one probe locally:
uv run python scripts/adapter_canary.py --adapter agy. - Regenerate this table from a local run:
uv run python scripts/adapter_canary.py --update-docs.