← All posts
Changelog context-bridge · 7 min read

A release that could not verify itself

0.12.3 added a command whose whole job is to check that the setup works instead of assuming it — then shipped with release notes nobody had checked. 0.12.4 is the correction, and the two guards that looked right before one of them worked.

0.12.3 added bridge verify — a command whose entire job is to stop assuming the setup works and go and check. Then it shipped with release notes nobody had checked.

0.12.4 exists only to correct them. Both are in this post, because the second one is a consequence of the first.

What 0.12.3 actually added

bridge verify. bridge doctor has always reported what it could see: binaries present, routes configured, sessions linked. Seeing is not the same as working. verify puts a real headless smoke question to every installed agent, checks its session and discovery readers, and then walks every directed route between installed agents — five agents means twenty routes, because a handoff from Claude to Codex is not the same path as Codex to Claude. It fails if fewer than two agents are installed, if an agent does not answer, if a session is missing or mismatched, if discovery is blind, or if any single route is unconfigured. --json for automation, non-zero exit on failure.

bridge status --json. The same status a human reads, in a shape a script can consume: active lane, linked agents, pending work kind, recent switch directions. Deliberately not included: vendor watermarks, session identifiers, absolute paths. Status output has a habit of ending up in issue reports.

bridge handoff <agent> --dry-run. Reports the delivery road it would take, the estimated payload and the artifacts it would write — and touches nothing. No link, no import, no prune, no vendor session.

OpenCode discovery stopped leaving processes behind. This is the fix I would have written the post about on its own. Discovery now prefers opencode session list --format json; the HTTP server path survives only as a fallback, and every probe inside it is bounded. The old path had a specific and nasty failure: the server binds its port before it can answer on it, so an unbounded curl would sit there waiting, and because timeout had SIGTERM'd bash, the trap that was supposed to kill the server never ran and execFileSync's own timeout never landed either. The caller hung too.

bash spawns the server curl waits on a port that is bound but not answering timeout SIGTERMs bash the EXIT trap never runs Ten bash/curl/server triples found alive on one machine

A schema canary for the OpenCode store. The bridge writes into OpenCode's internal SQLite database because there is no supported API. That is a boundary worth being honest about, so before relying on the store it now reads pragma_table_info for the four tables it depends on — project, session, message, part — and checks every column it needs is there. An incompatible schema fails closed with a diagnostic naming the missing columns, instead of writing into a shape it does not understand. A store that does not exist yet reports as such rather than as broken.

BRIDGE_DEBUG=1. Opt-in stderr diagnostics, redacted by construction rather than by care: any field whose name matches prompt|token|secret|password|content|message|transcript is replaced with [redacted] before it is serialised, the home directory collapses to ~, the working directory to ., and anything shaped like sk-, ghp- or xox*- is stripped.

Errors that say what to do. Handoff and state failures now carry the operation, the affected path and the next command to run.

Supply chain. A lockfile, npm ci --ignore-scripts in every CI job, and checkout@v5 / setup-node@v5. The package has no runtime dependencies, which is exactly why it was never installing anything in CI — and therefore never exercising its own metadata. Matrix: Node 18/20/22 across Ubuntu and macOS.

What the release notes said instead

Announced as new in 0.12.3

  • Agent-written handoff summaries
  • Whole-message context
  • Lanes and recovery hardening
  • Codex hook delivery at 8KB

Where they actually shipped

  • v0.10.0 — 23 July
  • v0.10.0 — 23 July
  • v0.10.0 — 22 July
  • v0.10.0 — 23 July

Nothing was invented. Every one of those features is real and works. They had simply been in people's hands for eight weeks, and lanes had already been announced here, in August.

The tell was mechanical. The notes claimed the Codex hook road had been made reachable and given an 8KB budget — but:

git diff --stat v0.12.2..HEAD -- src/hooks.mjs src/delivery.mjs
(empty)

Neither file had been touched. A release cannot have changed something it did not edit.

Why it happened

The notes were written from the session summary — a tidy forty-two step narrative of everything the work had involved. That summary was accurate. It just covered about two months, and nothing in it distinguished what had already shipped from what had not. Read as a list of accomplishments it is correct; read as a changelog it describes the wrong release.

A changelog is not a summary of effort. It is a claim about the difference between two tags.

0.12.4, which changes no code at all

npm is immutable, and it should be. The 0.12.3 tarball keeps its wrong notes forever. The only honest correction is a version above it, so 0.12.4 is a release whose entire content is an admission:

Corrected the 0.12.3 release notes. The previous entry incorrectly repeated features that shipped in 0.10.0.

Then the interesting part: trying to stop it happening again. It took three attempts, and the first two are more instructive than the third.

Assert the sentence "Corrected the 0.12.3 release notes" is present Assert no release title appears in two versions Assert every published manifest carries package.json's version

The first pinned a one-off historical sentence to whatever package.json currently says. It passed that day and would have failed the next release, for a reason having nothing to do with the bug.

The second looked much better — a genuine invariant, no duplicate release titles across versions. So I checked it the only way worth checking a guard: I restored the exact broken changelog from the bad commit and ran the new test against it.

✔ the current package version is the first changelog version
✔ a release title appears in only one changelog version

Green. On the very bug it was named after. Because 0.10.0 had said "A handoff carries whole messages now" and 0.12.3 said "Whole-message context and durable evidence" — nobody re-announces a feature in the same words. They rewrite it. The duplication the test guarded against had never happened and never would.

A test that passes on the failure it was written for is worse than no test, because the next person reads the green and stops looking.

The third stopped chasing the original mistake and went after something that actually breaks: every published manifest must carry package.json's version. A different bug — and one that had already happened twice in this project, including a commit from July titled "the version was written in five places and bumped in one".

It found one immediately. plugin/.claude-plugin/plugin.json and .claude-plugin/marketplace.json were both sitting at 0.12.2, which means 0.12.3 went out introducing itself as 0.12.2 to anyone installing it as a Claude Code plugin. The README said 0.12.2 as well.

This time the test was checked the same way, in both directions: put each manifest back to 0.12.2, watch it fail, name the right file.

The checklist entry that matters more than the test

Identify the previous release tag: git describe --tags --abbrev=0
Write the changelog only from the actual implementation diff.
Do not turn a session summary, roadmap or cumulative feature
list into the current release notes. Every entry must be
attributable to a changed file.

No test enforces that. It is the kind of thing only a habit catches.

What I take from it

bridge verify exists because "it looks configured" and "it works" are different claims, and only one of them survives contact with a real agent. The release notes needed exactly the same treatment and did not get it — in the release that added the command.

The guard that survived is not the one aimed at the mistake. It is the one aimed at something that genuinely breaks, and it was only trusted after being watched to fail.

Six version strings now agree, bridge --version reports 0.12.4 from a clean install, and the published tarball carries the corrected history.