ADR 0024: The plugin binary is packages by concern, and a harness is a value
Status: accepted (2026-09-22). Canonical copy in git at docs/adr/0024-plugin-binary-packages-by-concern-and-host-values.md. Part of the architecture quality plan, track 3e; follows the split pattern of ADR 0023; the findings are T6 to T12, T18, T21 and T22 in the tooling ledger.
Context
tools/demarkus-plugin is the one binary every demarkus plugin adapter calls. Its lifecycle package (provision, 2,142 lines) held eight concerns and its state package (registry, 1,065 lines) seven, with a nine argument join, a mutable package global selecting the harness, three --format switch ladders in main, three semver comparators and no test over the lifecycle functions that had produced the last two regressions (findings T3 and T4). The plan asked for the split, one host package and the seams the tests need, behaviour preserved: plugin output, hook output, state files and log text byte identical.
Decision
internal/provisionis the orchestration and health reporting root (Provision,Init,Status,HealthWarning,VerifyAuth,DetectServers) over five packages:releasepins and installs the binaries,procscandiscovers processes and probes ports and binaries with every probe bounded and an unrunnable probe reported as unknown,tokensmints, migrates and verifies the plugin token,serverspawns, adopts and restarts the managed server,seedpublishes the first documents.progressis the one[demarkus-memory]stderr log. The dependency shape isreleaseonprocscan;tokensonprocscanand the registry'sstatefile;serveronprocscan,tokensandrelease;seedon nothing butprogress, taking its client and token as inputs; only the root on all of them.configowns the~/.demarkuslayout (BinPath,TokenPath, the memory roots, the managed log and tokens paths) and both halves ofplugin-memory.conf(LoadConfig,SaveConfig).internal/registryis a directory of packages:statefileis the transactional engine (lock, atomic write, prepare then apply with rollback, record field and slug validation);catalogreads and binds the memory catalog and owns the local memory's alias and the names a join may not take;knowledge,promoteandbrokereach keep one registry or validation;joincommits a memory join as onejoinPlan;mcpconfigedits a harness's MCP config file. Shared test fixtures live inregistrytestandprovisiontest, which import only packages whose tests do not import them back.- The catalog record has one codec,
config.MemoryRow, and one records reader,config.Records;MemoryRegisteris gone, a join is the only writer. internal/hostholds what differs per harness at the output edge: the hook payload shapes, the project directory variable, the MCP config file and its remote server entry, the once per session nudge, the pi proxy unwrap.Claude,Cursor,PiandOpenCodeare values;ForHook,ForGateandForMcpresolve the--formatand--harnessvocabularies, which do not change.mcpconfigtakes the host as a parameter. The tool name grammar stays withconfig.internal/semveris the one release version parser; each caller keeps its own leniency (updatereads an unparseable version as 0.0.0, the drift probe says nothing).- Test seams are package values:
release.BaseURL,tokens.DialProbe,statefile.Writer, the managed ports inprovision. The server stub the lifecycle tests run is a compiled program, becausepsreports a script's interpreter, and it answers--version, binds its-portand ignores SIGHUP as the real server does. scripts/check-identical-copies.shguards every hand copied adapter file: the eight bootstraps byte for byte, the branded hooks modulo the plugin label and surface flag, the TSrunBinhelpers per family.
Consequences
- Plugin output, hook output, state files and log text are unchanged; the hook shapes are pinned by
host's tests. Two edge cases moved with the dedup and are recorded in /debt.md: the memory-default listing normalizes a hand corrupted catalog row, and the update check reads a prerelease suffixed version as unparseable. - The lifecycle has tests for
EnsureBinaries,server.Ensure,Initin all three modes,provisionLocked,VerifyAuthandHealthWarning; the T3 probe regression is pinned inprocscanandserver, T4 inmain. - Writing those tests showed that a managed server which dies at startup is reported as spawned: the plugin process never reaps its detached child, so the exited process reads as alive to
kill 0. Recorded as debt, not fixed here, because the fix changes the failure path's output. - The pin bump workflow reads
release.go(ServerVersion,ClientVersion). - The lint ratchet shrinks by the join's argument and result limit entries and the paths move.