The plugin speaks the claude binary's stream-json/control protocol directly. That protocol moves: the
binary auto-updates and the @anthropic-ai/claude-agent-sdk reference (our protocol source-of-truth) is
published independently. Drift = the latest SDK/binary exposes a protocol kind the plugin doesn't model.
./gradlew checkDrift (on-demand, not wired into check):
- Updates both tools to latest first —
npm update @anthropic-ai/claude-agent-sdk(vendored SDK) andclaude --update(the binary). The whole point is to test against current reality. - Measures the surface: extracts
subtypeliterals + message-union members from the latestsdk.d.ts, and probes the updated binary (one canned turn) to capture the top-leveltypes /subtypes it emits. - Diffs against what the plugin models — the
KNOWN_EVENT_TYPES/KNOWN_SUBTYPESsets insrc/test/kotlin/dev/lain/claudejb/drift/ProtocolSurface.kt(mirrored fromprotocol/ProtocolParser.kt, which is where the decoder registry lives) and the recorded versions inscripts/drift-baseline.properties. - Prints an agent-consumable report and fails when the latest surface exposes a kind the parser doesn't handle (a bare version bump with a fully-covered surface passes).
Implementation: src/test/kotlin/dev/lain/claudejb/drift/ — pure ProtocolSurface + DriftDetector
(offline unit-tested in DriftDetectorTest) and the @Tag("driftLive") DriftLiveCheck (the live
download + probe, run only by the checkDrift task, excluded from the normal test task).
KNOWN_SUBTYPES is the full triaged surface — every subtype the plugin is aware of, whether it
parses it (system subtypes, can_use_tool, hook_callback), sends it (host→binary control:
initialize, set_model, get_session_cost, mcp_status, …), or deliberately rejects it
(request_user_dialog, mcp_call, … → UnsupportedControlRequest). A subtype in none of these is genuinely
new and worth a human look.
- Update — run
./gradlew checkDrift(updates SDK + binary, reports). It defaults to~/.local/bin/claude; pass-PclaudeBinary=<path>(orCLAUDE_BINARY) for a system-wide install. - Plugin code update — for each genuinely-new kind in the report. A system subtype is three
places: its payload as a
@Serializableclass in theprotocol/*Models.ktfile for that subject, its case in theClaudeEventunion (protocol/ClaudeEvent.kt), and onetyped(…)line in theSYSTEM_DECODERSregistry ofprotocol/ProtocolParser.kt— a registry rather than awhen, so adding a subtype is one entry and not a branch. A control kind is a builder inprotocol/ControlProtocol.ktwhen the host sends it, or a case inProtocolParser's control-request dispatch when the binary does. No-op if the surface is unchanged. - Tests —
./gradlew test(full non-UI pyramid green) plusnpm testif anything reached the UI. - Update the drift detector — extend
KNOWN_EVENT_TYPES/KNOWN_SUBTYPESto cover the triaged kinds, and bumpscripts/drift-baseline.properties(sdk,binary) to the updated versions. Re-run./gradlew checkDrift→ green. - Bump release —
versioninbuild.gradle.kts. - Code review + security review —
/code-reviewand/security-reviewover the diff. - Update
.mdfiles —CHANGELOG.md,RELEASE_NOTES.md,README.md,CLAUDE.md,PROJECTMAP.md, and the matrix inBINARY_COMPAT.md. - Commit — Conventional Commits (
build(protocol): re-baseline to claude X / SDK Y), GPG-signed, and noCo-Authored-Bytrailer. - Publish release — GitFlow PRs
feature → develop → main. The rulesets in.github/rulesets/decide how each door merges: intomain, a merge commit and nothing else, because rewriting the commit is what strips the author's signature off the thing being published; intodevelop, squash or merge. Rebase is allowed on neither. Do not tag — the merge intomaintriggersrelease.yml, which cuts and signsvX.Y.Zitself and publishes from it. SeeRELEASE_PROCEDURE.md.