wowbagger
v0.5.0
Published
Git-native work ledger for coding agents: deterministic ready queues, guarded CAS mutations, claims and fencing, and self-contained HTML reports.
Maintainers
Readme
wowbagger
The backlog may be infinite. The next item should not be ambiguous.
Don't Panic! The books that helped shape my childhood taught me to meet absurd systems with curiosity, humor, and a reliable way to find the next step. Douglas Adams's Hitchhiker's Guide creations are part of that inspiration; Wowbagger is an independent work, not an official or affiliated project.
Wowbagger is a work ledger for coding agents. Every backlog item is one Markdown file in your repository; every lifecycle change is a reviewable Git diff. There is no database, no hosted service, and no private agent memory to lose.
Who it is for. Maintainers running one or more coding agents across worktrees, sessions, and models, who need the backlog to survive a compaction, a restart, or a change of harness — and who want the agent's writes guarded rather than trusted.
What you get. A deterministic ready queue that answers "what is actionable today". Guarded single-item writes with exact-byte compare-and-swap, so an agent cannot silently clobber your edit. Honest capability discovery, so a tool can tell what this core will and will not promise. A shipped skill that teaches an agent to use those guarantees instead of hand-editing your Markdown.
Start here: install the core and set up a ledger.
Status: published and self-hosted.
0.5.0is published on npm and has a matching repository tag. It is the version this repository runs its own backlog on. The API is stable at core contract version 5.Channels. A stable release sets both
latestandnextto the stable version and publishes withnpm publish --tag latest. A later prerelease moves onlynextand publishes withnpm publish --tag next, solateststays on the stable release. While every published release is a prerelease,latestmirrorsnext: install the prerelease explicitly withwowbagger@next. After the first stable release, a bare install resolves to the stable release.What is proved. Contract version 5 validates the complete Markdown ledger, selects a deterministic ready queue, exposes bounded
listand losslessinspectprojections, and publishes guardedcreate,transition,patch,parent-migrate, andsnoozemutations. Mutations use exact-byte compare-and-swap, atomic no-clobber publication, and explicit reconciliation when a response is lost. Claims, fencing, adoption, prospective merge verification, and publication finalization coordinate cooperating writers without pretending to be an exclusive dispatch lock.
reportis a self-contained decision workspace: one scoped item browser with Work next and other quick views, an area/status matrix, scoped attention actions, dependency impact, interactive Flow charts, and a 3D dependency graph that all share one scope. Version 2 report configurations add named custom views whose statistics, readiness, attention, flow, graph, and impact all describe one filtered subset. Reports remain derived output, not mirrored ledger state.The core ships Claude Code, Codex, and OpenCode adapter packages on one shared engine. Read
capabilities --jsonbefore relying on a target-specific claim; platform support is evidence-based, not inferred from whether a CLI starts.What is not a lock. A work claim is not an exclusive dispatch lease. On a provisioned Git-backed ledger, claims coordinate cooperating agents through a durable journal in Git's shared common directory:
claim acquireuses observed-state compare-and-swap,publish-claimedfences one item against the active owner generation and expected revision, andclaim-verifyreconciles response-loss and post-merge outcomes. Capability discovery therefore reportsmode: "merge-coordinated"andsafe_exclusive_dispatch: false. Direct filesystem writes, hostile processes, other clones, and non-claim-aware tools still bypass the protocol.
TL;DR for agents
Wowbagger is the core authority for a Git-native work ledger. Use it instead of editing ledger Markdown by hand.
wowbagger --version # require 0.5.0
wowbagger capabilities --json # require contract_version: 5
wowbagger validate --ledger ledger --json
wowbagger ready --ledger ledger --as-of YYYY-MM-DD --json
wowbagger inspect --ledger ledger --number N --jsonFor a write, inspect immediately before dispatch, send the returned exact-byte
revision as the compare-and-swap witness, use the explicit core mutation, and
validate again. On a provisioned ledger, commit each create, transition,
parent-migrate, snooze, patch, or publish-claimed mutation before the
next mutating command. Never replay a lost write: reconnect, re-read current
state, and treat the outcome as unknown until the core or a human resolves it.
Numbers are the human-facing item identity; wb_... ULIDs are internal
identities.
The core owns validation, ready selection, projections, lifecycle, CAS, publication, claims, fencing, and reconciliation. The harness or host owns dispatch, process safety, routing, and human approval. Claims coordinate cooperating writers; they are not exclusive locks.
Start here
Install the core CLI, then verify it. The supported runtime is Node.js 24; Node 26 remains excluded because of the separate Vitest incompatibility:
npm install -g wowbagger@latest # stable release
npm install -g [email protected] # exact plugin-matched release
# or, from this release's Git tag:
# npm install -g github:lstutzman/wowbagger#v0.5.0
wowbagger --version # 0.5.0
wowbagger capabilities --json # must report contract_version: 5In Claude Code, install the managed plugin:
claude plugins install wowbaggerOr, from inside a Claude Code session:
/plugin install wowbaggerThis route is available after Wowbagger is listed in Claude Code's official marketplace. Until then, or when installing a fork or unreleased revision, use the direct repository marketplace:
/plugin marketplace add lstutzman/wowbagger
/plugin install wowbagger@wowbaggerFor Codex and other agents, install the editable skill with skills:
npx skills@latest add lstutzman/wowbagger --skill wowbaggerChoose one route. Do not install both the managed Claude plugin and the editable skill, or the skill will be loaded twice.
The plugin and skills installer drive the separately installed core rather
than bundling one, so a mismatch is detectable instead of silent. The skill
reads wowbagger --version and capabilities; it requires the same
distribution version as the plugin and core contract_version: 5. It refuses
an absent or incompatible core. It will not fall back to editing ledger files
by hand, because that would bypass validation and atomic publication.
Neither installer adds an MCP server, remote service, hook, or background process. The plugin and skill operate on the ledger through the installed core.
Set the ledger up before the first item
Decide where items live before your first create. A ledger publishes items
to <ledger>/<id>.md unless a committed <ledger>/.wowbagger/layout.json binds
a subdirectory:
mkdir -p path/to/ledger/.wowbagger path/to/ledger/items
echo '{"layout_version":1,"items_directory":"items"}' > path/to/ledger/.wowbagger/layout.jsonlayout_version must be 1 and items_directory names the committed item
directory. create then publishes atomically to <items_directory>/<id>.md,
and validation rejects parsed items outside it. Commit the directory the file
names — create publishes into an existing directory and does not make one.
Nothing is renamed after a create. This repository dogfoods that binding: its
own items live in ledger/items/.
If your items will mirror an external tracker and carry consumer-owned fields, declare those fields before the first item. The declaration makes each named extension member patchable:
echo '{"extensions_version":1,"members":{"external_id":"string"}}' \
> path/to/ledger/.wowbagger/extensions.jsonEach member declares one value type: string, integer, boolean, or
string-list. A ledger without the file has no patchable extension member,
and set.extensions refuses the missing declaration by name. The declaration
authorizes writes; it does not define item validity, so validate does not
read it. Commit the declaration with the other ledger setup.
If an existing ledger already carries extension values, do not create the declaration by hand. Select every member and type explicitly in a request, review a dry run, then publish the same proposal:
{"members":{"tags":"string-list"}}wowbagger extensions-provision --ledger path/to/ledger \
--input declaration.json --json --dry-run
wowbagger extensions-provision --ledger path/to/ledger \
--input declaration.json --json
git add path/to/ledger/.wowbagger/extensions.json
git commit -m "Declare patchable ledger extensions"The command first requires a valid complete ledger. It validates every
occurrence of each selected member, reports occurrence counts, changes no item
bytes, and publishes one canonical declaration without overwriting a different
one. Commit that file before the first corresponding patch, inspect the
target for its current revision, then use set.extensions.tags. An item that
writes the selected member with a YAML anchor or alias remains
extension-anchored and requires a reviewed hand-edit.
Cores at 0.1.0-alpha.4 and earlier ignore the layout file and publish every
item at the ledger root.
On such a core, relocating an item by hand has a trap. create writes an
untracked file, so git mv refuses the path it just published; an unchecked
batch then runs git add -A and commits the item at the ledger root, where it
silently stays. Use plain mv, then git add both paths, and check the exit
code of every command before the commit:
mv path/to/ledger/<id>.md path/to/ledger/items/<id>.md || exit 1
git add path/to/ledger || exit 1A committed item outside the configured items directory then fails validation and refuses every read and every guarded mutation on that ledger, including ones that never touch it. The refusal names the expected path and the relocation that repairs it.
File the first item
Every command below takes --json and writes exactly one JSON object to
standard output. mint-id prints a canonical ID so no caller writes base32 by
hand:
wowbagger mint-id --json # -> result.id
cat > request.json <<'JSON'
{
"id": "wb_...",
"item": {
"title": "Map the fictional route",
"kind": "task",
"priority": 2,
"provenance": { "source": "user-request", "recorded_at": "2030-01-10T12:34:56.789Z" },
"depends_on": [],
"related": []
},
"body": "\n# Problem\n\nWhat is wrong.\n\n# Acceptance criteria\n\n1. ...\n"
}
JSON
wowbagger create --ledger path/to/ledger --input request.json --jsonThe item lands in triage with a core-assigned number — the integer handle
you say out loud. create does not accept a number and does not accept a
status. Move it into the backlog with an explicit accepting transition, then
ask what is workable:
wowbagger inspect --ledger path/to/ledger --number 1 --json # -> revision
wowbagger transition --ledger path/to/ledger --input accept.json --json
wowbagger ready --ledger path/to/ledger --as-of YYYY-MM-DDtransition fences on the expected_revision that inspect just returned, so
inspect immediately before you transition. On a provisioned ledger, commit each
mutation before running the next one — see
Commit each mutation before the next one.
For an isolated consumer pilot, create or select the disposable worktree before the agent starts. Then launch a new session with that worktree as its project root. Follow the isolated dogfood pilot runbook; do not try to drive a sibling worktree from an already-running agent session.
To use the core directly from a clone instead, see Core commands.
Why the name?
Wowbagger the Infinitely Prolonged is a Douglas Adams character faced with an absurdly large, strictly ordered list and the prospect of working through it one entry at a time.
That is also a fair description of software maintenance.
The project is an independent literary nod and is not affiliated with or endorsed by Douglas Adams' estate.
Why adopt the skill instead of editing Markdown by hand
The ledger is plain Markdown, so an agent can edit it with a text editor. The skill exists because four things break when it does.
- Validation is whole-ledger and fail-closed. One malformed item refuses
every read and every guarded mutation on that ledger, including commands that
never touch it. A hand-edit finds that out later, and usually in someone
else's session.
create,transition,parent-migrate,snooze, andpatchvalidate the complete candidate ledger before publishing anything, and refuseunchanged. - A hand-edit has no lost-update guard. Every guarded write takes the exact
SHA-256 revision
inspectreturned and refuses if the bytes moved. An editor writes over whatever is there. - Publication is atomic and no-clobber. A guarded write either lands whole or does not land. A half-written item from an interrupted editor is a broken ledger.
- On a provisioned ledger, a hand-edit is a stale write. The claim journal
validates recorded mutations against Git
HEAD. An out-of-protocol edit makes the next mutation refuse exit 6unauthorized-revision, and every later mutation stays blocked until an operator rules on it. The protocol used to force that edit for a wrong title; it no longer does —patchcovers it.
What the skill adds on top of the CLI is the part an agent gets wrong unaided: it checks the core version before the first command, it says #N to people and ULIDs to the tool, it reads acceptance criteria out of the item body rather than the metadata, it derives an epic's progress from its children instead of its status, and it tells the user before a batch that filing ten items means ten commits, not one.
It also refuses to fall back. If the core is absent or the contract version is not 3, the skill stops and says so. It does not quietly start editing files.
Core, adapter, skill, plugin
Four separate things, deliberately:
| Piece | What it is | Where it lives |
|---|---|---|
| core | The wowbagger CLI. Harness-neutral; it knows no vendor. | src/, bin/, npm package wowbagger |
| adapter | Per-harness machinery that lets an agent drive the core safely: negotiation, capability checks, forwarding, honest outcome mapping. It owns no lifecycle logic. | adapters/ |
| skill | Harness-native instructions telling an agent when and how to use the core. | skills/wowbagger/SKILL.md |
| plugin | The Claude Code distribution wrapper: marketplace entry, skill, adapter. Self-hosted from this repository. | .claude-plugin/ |
The core and the plugin install independently and must carry matching distribution versions. The core contract version and the adapter contract version are separate domains: the core is at 5, the adapter is at 2, and the legacy work-claim, ledger-publication, and ledger-mutation envelopes stay at 1.
Installation, compatibility, and security
Installation routes
Wowbagger ships as an npm package with a single wowbagger binary. There are
two supported install routes:
- npm registry —
npm install -g wowbaggerinstalls the stable release once it exists;npm install -g wowbagger@nextinstalls the newest prerelease. While every published release is a prerelease,latestmirrorsnext, so a bare install resolves to the same bytes. After the first stable release,lateststays on stable and onlynextfollows later prereleases. - git tag —
npm install -g github:lstutzman/wowbagger#v0.5.0installs this release. Installing at a ref installs the core and every adapter that ref carries.
Either route installs the core and the wowbagger command. The Claude Code
plugin is a separate artifact (see Start here).
Compatibility
The contract version is top-level contract_version, reported by
wowbagger capabilities --json. Contracts change it; refactors do not. The
npm/Git distribution version names release bytes. General API consumers
negotiate the contract version. The shipped plugin skill additionally requires
the exact core distribution version that shipped with it, because its
instructions can depend on additive behavior from that release.
A widening inside one contract version does not move it, so the version field
cannot answer every question. set.body_append and set.extensions both
shipped after 0.1.0-alpha.5 published contract_version: 3. Probe for them by
sending the request and reading the refusal — an unknown-member issue at
/set/body_append means the core predates the append — or pin the distribution
version.
- Node.js: 24 and later. The release gate runs the full suite on Node 24.20.0 with the strict deprecation gate; Node 26 stays excluded because of the separate Vitest incompatibility.
- Platforms: the core runs wherever Node.js runs. The Claude Code adapter
declares Darwin, Linux, and Windows
supportedfrom native common-vector evidence. Every other shipped adapter target remainsunverified. - Other tooling:
wowbaggermanages a Git-tracked Markdown ledger. It needs an accessible Git checkout for work-claim and namespace operations. Beforeprovision, runwowbagger claim capabilities --ledger <dir> --jsonand requireresult.operations.work_claim.supported: true.
Security
- Read-only by default.
validate,ready,report,inspect,list,capabilities, andmint-idnever modify anything. Every item mutation (create,transition,parent-migrate,snooze,patch, andpublish-claimed) is an explicit, reviewable write. - Lock is not a claim. A short mutation lock serializes writers during one operation. It does not grant a work claim.
- Claims are merge-coordinated, not exclusive.
claim acquireuses compare-and-swap against the observed claim state.publish-claimedchecks the active owner generation and expected ledger revision before it writes one item.claim-verifyrecords the final Git outcome and detects later revision drift. Legacycreateandtransitionrefuse claim conflicts. - Adopting a revision is an operator ruling, not an escape hatch.
claim-adoptmoves the authorized revision to one named committed revision and stops. It writes no item byte, it is per item and per revision explicit, there is no adopt-all, and the next out-of-protocol edit refuses again. - Local authority only. The protocol protects cooperating worktrees in one
Git repository. It does not stop direct filesystem writes, hostile
processes, other clones, or alternate write paths. Capability discovery
therefore reports
mode: "merge-coordinated"andsafe_exclusive_dispatch: false. - Supply chain. Install only from the npm registry or this repository's
git tags, and verify the
contract_versionyour adapter or script requires.
This README is documentation, not a substitute for the contracts. The machinery behind these properties is specified in the documents listed under Where the contracts live.
Upgrading from an earlier wowbagger
This section is written for agents as much as humans: if you already drive a wowbagger core, this is how you move forward safely.
Upgrade the pieces you installed:
npm install -g wowbagger@latest # public npm registry
npm install -g github:lstutzman/wowbagger#v0.5.0 # immutable Git release
git pull && npm ci # or: a direct checkoutIn Claude Code, update the plugin the same way it was installed:
/plugin marketplace update wowbagger
/plugin update wowbagger@wowbaggerwowbagger --version
wowbagger capabilities --jsonThe plugin requires its exact core distribution version and top-level core
contract_version: 5. Direct API consumers must check the contract version
they support; installed plugin users must also keep the plugin and core
distribution versions equal.
The shipped adapter selects only adapter contract version 2 and requires core
contract version 5. The adapter contract and the core contract are separate
version domains: the adapter stays at 2 while the core is at 5. A v1-only
consumer receives unsupported-adapter-contract-version; it does not receive v2
behavior. The schema-2 transport is available. Ledger migration remains a
separate quiesced maintenance operation. The
schema-2 migration runbook documents the required
backup, dry run, explicit --apply, lock refusal, and recovery procedure. The
tool is dry-run-only by default:
TMPDIR=/tmp node scripts/migrate-schema-2.js --ledger path/to/ledgerBehaviour changes are recorded in CHANGELOG.md — read its Unreleased section on every upgrade. If you automated against an earlier core, these are the changes most likely to touch you:
Stop hand-editing frontmatter.
wowbagger patchnow coverstitle,priority,depends_on,related, the body, and every extension member the ledger declares, all under the same per-ID lock and revision compare-and-swap astransition. Hand-edits bypass validation and atomic publication, and on a provisioned ledger they block the next mutation.numberis not yours. On schema version 2 the core assigns the number atcreateand refuses a caller-supplied one;patchrefuses it because it is immutable identity. Keep a legacy identifier in a declared extension member or in the item body.Repair duplicate numbers through
ledger-repairversion 1. Generate a read-only proposal withnumber-repair-proposal, review everyexpected_revisionandreplacement_number, then apply the complete mapping withnumber-repair. The command preserves ULID identities and relations and does not change core contract version 5.Run
version-drift --jsonbefore mutation. It compares the installed skill pin, required core contract, and running core, and names the stale package, plugin cache, or linked checkout with remediation.Delete your local ULID generator.
wowbagger mint-id --jsonprints a canonical ID;--date YYYY-MM-DDselects the creation date the ID must encode.Read
core.numberandcore.priorityfrom results instead of decodingsource_base64. Every frontmatter field lives underitem.core;item.idis the one deliberate duplicate.readywithout--jsonis for you to read:#number pri=priority titleper line, in ready order. Machine consumers keepready --json, which is byte-stable.A claim request with an own
__proto__member is now refused asinvalid-requestinstead of silently losing the member.createtells you where the item landed: results reportcore.status: "triage", and the refusal for a caller-suppliedstatusnames the accepting transition (triage to backlog) that makes an item ready.Bind a subdirectory layout in the ledger, not in each runner. Commit
<ledger>/.wowbagger/layout.jsonwith{"layout_version":1,"items_directory":"items"}.createthen derives<ledger>/items/<id>.md; validation rejects parsed items outsideitems/. Without the file, the compatible layout remains<ledger>/<id>.md.A date refusal now carries the item's own dates.
date-before-createdanddate-before-updatedboth carryitem_createdanditem_updated, so correcting the request costs noinspectround-trip. Item dates derive from the ULID timestamp, which is UTC: an item minted just after midnight UTC carries tomorrow's date for anyone west of UTC.
The problem
Coding agents lose context. They are restarted, compacted, moved between worktrees, or replaced by a different model. A useful backlog therefore cannot live only in one conversation or one harness's private state.
Wowbagger makes the repository the durable coordination boundary:
- One inspectable Markdown file per backlog item.
- YAML metadata for lifecycle, priority, dependencies, and structured provenance.
- Git history as the audit log and recovery mechanism.
- Dependency-aware ready queues so an agent can ask what is actionable now.
- Guarded one-item creation, patching, and lifecycle transitions with exact-byte revisions, cooperative locks, and explicit refusal when a change needs a multi-item transaction.
- A documented adapter boundary for tool-capable agent harnesses, without coupling the core to one vendor.
- Mechanical validation and derived reports instead of duplicated status data.
Harness-neutral by design
Claude Code is an adapter, not the architecture. The core schema and command interface will not depend on Claude-specific hooks, slash commands, paths, or environment variables.
flowchart TD
Claude[Claude Code adapter] --> Core[Wowbagger core]
Codex[Codex adapter] --> Core
OpenCode[OpenCode adapter] --> Core
Other[Kimi and other tool-capable agents] --> Core
Core --> Markdown[Markdown and YAML backlog]
Core --> Git[Git audit and history]The documented compatibility targets are:
- Claude Code
- OpenAI Codex
- OpenCode
- Kimi and other OpenAI-compatible model APIs hosted in agent harnesses that provide repository filesystem and command-execution tools
An OpenAI-compatible API describes model transport; it does not by itself provide agent tools. The adapter contract records the required host capabilities and refusal rules, and the integration guide states what a Kimi or other OpenAI-compatible host can do today — driving the core CLI directly — versus what a verifiable compatibility claim requires. Neither claims that API compatibility alone makes a harness compatible.
This checkout ships three adapter packages on one shared entrypoint runtime:
adapters/claude-code/, adapters/codex/,
and adapters/opencode/. Each answers the bootstrap wire
with its own identity and honest host declaration. Invocation forwarding, path
and limit guards, approval, and context all enter through the shared shipped
engine. Run the conformance suite to see the evidence:
TMPDIR=/tmp node spec/run-adapter-implementation.js # claude-code
TMPDIR=/tmp node spec/run-adapter-implementation.js --target codex
TMPDIR=/tmp node spec/run-adapter-implementation.js --target opencodeThe native Darwin and Linux Claude Code reports each pass all 212 assertions across all 16
cases and report "status": "pass". Codex and OpenCode execute the same 212
assertions through the same engine, but both target reports remain "fail"
pending target-specific evidence, and every platform declaration on those two
manifests stays unverified. The Kimi and OpenAI-compatible harness adapters
are not written.
All three shipped packages are read-only as they stand, and say so. None
wires a consumer approval source, so each declares no trusted approval and
refuses create, transition, and patch with capability-unavailable
naming the missing capability, before any core process starts. Mutation
authority is a runtime dependency a host supplies in code: a process that
embeds runAdapterEntrypoint passes hostRuntime — the approval source, the
clock, the redeemed-nonce store, and the core executable identity it attests —
and its describe result then advertises trusted approval truthfully. The
approval never rides the bootstrap request, which the model controls;
docs/adapter-contract.md section 5.1 states the mechanism and its rules.
Core commands
The current core requires Node.js 24. Node 26 is not in the supported matrix.
From a Wowbagger checkout,
./bin/wowbagger.js --help prints the full command inventory,
./bin/wowbagger.js <command> --help prints that command's usage, and
./bin/wowbagger.js --version prints the installed package version. The
commands below are the current inventory:
npm ci
./bin/wowbagger.js validate --ledger path/to/ledger --json
./bin/wowbagger.js ready --ledger path/to/ledger --as-of 2030-01-15 --json
./bin/wowbagger.js ready --ledger path/to/ledger --as-of 2030-01-15
./bin/wowbagger.js report --ledger path/to/ledger --as-of 2030-01-15 --json
./bin/wowbagger.js capabilities --json
./bin/wowbagger.js inspect --ledger path/to/ledger --id wb_... --json
./bin/wowbagger.js inspect --ledger path/to/ledger --number 30 --json
./bin/wowbagger.js list --ledger path/to/ledger --input query.json --json
./bin/wowbagger.js create --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js transition --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js parent-migrate --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js snooze --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js patch --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js extensions-provision --ledger path/to/ledger --input declaration.json --json
./bin/wowbagger.js mint-id --json
./bin/wowbagger.js publish-claimed --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js claim-merge-verify --ledger path/to/ledger --base main --head feature --json
./bin/wowbagger.js claim-sync --ledger path/to/ledger --json
./bin/wowbagger.js claim-adopt --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js mutation-finalize --ledger path/to/ledger --recovery-token token --json
./bin/wowbagger.js provision --ledger path/to/ledger --json
./bin/wowbagger.js claim capabilities --ledger path/to/ledger --json
./bin/wowbagger.js claim acquire --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js claim read --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js claim renew --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js claim release --ledger path/to/ledger --input request.json --json
./bin/wowbagger.js claim-verify --ledger path/to/ledger [--id wb_...] --jsonvalidate writes exactly one JSON result to standard output. A valid ledger
returns:
{"valid":true,"errors":[]}ready validates first, then returns only the normative ready result:
{"as_of":"2030-01-15","valid":true,"ready":["wb_..."]}validate and ready require --ledger; ready also requires an ISO
calendar --as-of date. Without --json, ready prints a human queue —
#number pri=priority title per ready item — while ready --json stays
byte-stable for machine consumers. Invalid ledgers return the validation JSON and
exit nonzero. The core rejects invalid UTF-8, symbolic-link entries, unreadable
paths, and .md special files rather than returning a partial view. Real
directories ending in .md remain containers and are traversed. These checks
provide deterministic read hygiene; they are not a sandbox against a privileged
process racing filesystem changes.
inspect returns a lossless raw-byte snapshot and its SHA-256 revision, by
--id or by --number. create publishes only a caller-supplied canonical ID
through atomic no-clobber publication — mint-id prints one, so no consumer
writes base32 by hand. transition compares the inspected revision while
cooperative per-ID locks are held, then changes one lifecycle item or refuses
the request if dependent cleanup or child disposition would require changing
another item.
What patch may change
patch re-scopes an existing item in band, under the same per-ID lock,
exact-byte compare-and-swap, candidate whole-ledger validation, and atomic
publication as transition. The patchable set is exactly:
| set member | Rule |
|---|---|
| title | Non-empty string, replaced whole. |
| priority | Non-negative integer; null removes it. |
| depends_on | Whole relation list, replaced. [] clears it. |
| related | Whole relation list, replaced; null removes it. |
| body | Whole body replaced. "" empties it. Never merges. |
| body_append | Written after the current body. Mutually exclusive with body. |
| extensions | Container naming declared extension members; each value replaces that member whole, null removes it. |
A set member outside that list is an invalid-request issue at its /set
pointer — a typo becomes a refusal, never a new frontmatter member. number,
kind, and provenance are refused deliberately.
Two traps are worth carrying here rather than leaving to discovery. A body
patch replaces and never merges: a consumer mirroring an external source MUST
read-modify-write from the body inspect just returned and MUST never
regenerate from the source alone, because expected_revision is a byte-level
lost-update guard with no semantic safety at all. Use set.body_append when the
change is an addition. And extension members are patchable only where the
ledger declares them — see Set the ledger up before the first
item.
Which members you own at all is a four-way split — core-owned,
consumer-editable through patch, mutable through a dedicated command, and
create-once — stated member by member in the mutation contract's
frontmatter ownership table. Use parent-migrate to repoint an existing
item to or from an epic, and snooze to set or clear snoozed_until. Read the
table; do not send a patch and interpret the refusal.
An epic's progress is derived, never stored
An epic stores no progress and has no backlog -> in-progress edge. Its
progress is the terminal ratio: direct children whose status is done or
killed, over all direct children. That is one definition with three surfaces —
the mutation contract, the epic completion rollup, and the report's
epic-enablement factor all read the same set. A terminal date is not the test: an
archived or deferred child is work postponed, not work retired, and does not
count.
Diagnosing an invalid ledger
One invalid item refuses every read and every guarded mutation on that ledger, including commands that never touch it. That refusal is the diagnosis, and no command asks you to parse the Markdown by hand:
validate --jsonlists every error. An error whose repair the validator can derive also carriesexpected_pathand aremediationnaming the repair.inspectstill refuses with exit 3ledger-invalid— a revision from an unjudged ledger must never look like a mutation precondition — but the refusal carrieserror.details.item, the complete snapshot of the item you asked for, whenever no validation error names that item's path. A faulted item is withheld;validatealready names its repair.claim-verify --jsonreportsresult.ledger_validation. Bare verification is strict repository-wide mode;--id <item>keeps all findings visible but fails only for that item and global barriers. Exit 0 with an invalidledger_validationstill means claim state is clean but validation blocks mutation.
Work claims
provision binds one ledger namespace to the repository. claim manages
durable acquire, read, renew, and release decisions. publish-claimed accepts
the exact candidate item bytes and fences their publication against the active
owner generation and expected revision. claim-verify reconciles pending
publication outcomes against the working tree and Git HEAD; run it after a
claimed publication is committed or merged, and before the next fenced
operation. claim-adopt is the non-destructive remedy described below.
Core and work-claim versions use distinct negotiation fields. Read the
top-level contract_version from core capabilities. Read
result.operations.work_claim.api_version from
claim capabilities --ledger <dir> --json. A claim response's top-level
contract_version is the legacy claim-envelope marker; do not compare it with
the core version. A contract consumer that receives an unsupported version
refuses rather than guessing. The shipped plugin skill also requires its exact
core distribution version. Direct checkout use — ./bin/wowbagger.js from a
clone — remains supported and is what this repository's own ledger uses.
Commit each mutation before the next one
On a provisioned ledger — one where provision has bound a namespace and
claim capabilities reports mode: "merge-coordinated" — there is one
operating rule:
Commit each mutation to Git before running the next mutating command.
The durable claim store reconciles every recorded mutation with Git HEAD and
the working tree. The next mutation refuses when that reconciliation finds an
unauthorized-revision, requires Git finalization, or requires synchronization
for the item the command targets. A synchronization finding on an unrelated
item remains visible to claim-verify but does not block the command.
An existing item's latest authorized working-tree bytes and an earlier
authorized revision at HEAD form an authorized predecessor/successor window.
That window produces no finding, so another mutation can run before the first
one is committed. Acceptance of the later mutation does not make either change
durable. Commit each mutation anyway, then run claim-verify.
The loop that works:
./bin/wowbagger.js create --ledger path/to/ledger --input request.json --json
git add path/to/ledger && git commit -m "Record the mutation"
./bin/wowbagger.js claim-verify --ledger path/to/ledger --id wb_... --json
./bin/wowbagger.js transition --ledger path/to/ledger --input next.json --jsonFor example, an authorized new item that is still absent from HEAD makes the
next command return exit 6:
{"ok":false,"namespace":"ledger-mutation","command":"create-v1","contract_version":1,
"state":"unchanged","error":{"code":"claim-store-unavailable",
"message":"The durable claim store is unavailable.",
"details":{"reason":"publication-reconciliation-required","findings":[{
"code":"stale-write-detected","reason":"git-finalization-required",
"expected_path":"wb_....md",
"remediation":"Commit wb_....md in Git, then run claim-verify."}]}}}state: "unchanged" is exact — nothing was written. claim-verify is the
reconciliation procedure. Read details.findings, do what each
remediation string says, run claim-verify until it returns exit 0, then
repeat the refused command. A worktree-synchronization-required finding on an
unrelated item does not block the requested mutation. The same finding on the
target item, and every unauthorized-revision finding, remains blocking.
Batch work is where this bites. Filing ten items means ten serial
create --auto-commit calls and ten commits, not one batch commit. Wowbagger
permanently rejects batch create for the direct-Markdown architecture:
limits.multi_item_atomicity remains false, request order is the supported
bulk order, and each create must finish or recover before the next begins. See
the batch-create decision.
Or fold the commit into the mutation
--auto-commit performs that whole loop inside one invocation, on a provisioned
ledger only:
./bin/wowbagger.js transition --ledger path/to/ledger --input next.json --json --auto-commitIt is opt-in per invocation. There is no configuration setting or environment
default, because a hidden default would make existing automation create Git
commits unexpectedly. The flag is accepted on create, transition,
parent-migrate, snooze, patch, and publish-claimed.
What one flagged invocation does: refuse if anything is staged anywhere or any
foreign path under the ledger is dirty; reconcile; run the mutation unchanged;
commit exactly the changed item and at most one
.wowbagger/reconcile-<namespace>.md with a fixed subject such as
wowbagger: transition item #7; verify the commit; then run claim-verify
before it answers. A command that owns the claim journal may rebuild only its
derived reconciliation log during preflight. create remains strict, and
every other dirty ledger path still refuses. On success the result gains
git_commit, commit_paths, and claim_verified.
If claim verification refuses, auto-commit preserves its code and reason in
claim_verify_code and claim_verify_reason. Only
claim_verify_reason: "claim-store-locked" is retryable; unresolved
reconciliation is not.
Unstaged and untracked files outside the ledger are left alone. Hooks and
signing are honoured; --no-verify is never passed. Nothing is pushed.
A refused mutation never commits. When the item is published but the commit
fails, the answer is exit 6 git-commit-failed with a recovery token, and one
idempotent command finishes the job:
./bin/wowbagger.js mutation-finalize --ledger path/to/ledger --recovery-token <token> --jsonRepeating it creates no second commit, so a lost response and a failed commit recover the same way. docs/mutation-contract.md section 13 is the full contract.
When the response is lost
A mutation you dispatched can lose its response: the process is signalled or times out, a stream arrives truncated, or the transport to the machine that owns the ledger drops. None of that observes the ledger, so none of it says whether the write applied.
Only a complete observed result establishes an outcome. Exit 0 with state
committed is a success; a complete refusal with state unchanged is a proven
non-write; exit 6 post-commit-recovery-required says the item is published and
cleanup remains; exit 6 write-outcome-unknown says publication was attempted
and the visible bytes are indeterminate. Anything else — signal, timeout,
truncated output, no envelope, no response at all — is unresolved.
Unresolved is answered by one sequence, never a retry: Dispatch once, never
replay, invalidate the inspected revision, reconnect, then re-read the ledger.
Re-reading is validate plus inspect of the ID you already know, compared
against what you observed before you dispatched. What you read back is current
ledger state; it never proves that the lost dispatch caused it. A person reviews
that comparison before any new mutation, and the new mutation is built on the
current revision, not resent.
Exit 4 is not response loss. A revision-conflict proves the write did not run;
re-inspect and decide again.
There is no operation ID and no replay endpoint, because nothing replays. docs/mutation-contract.md section 10 is the full contract.
When the item was changed outside the protocol
An unauthorized-revision finding means someone edited the item without going
through a guarded verb. There are two remedies, and choosing between them is
an operator's decision, not the agent's:
- Restore the authorized revision, commit, and run
claim-verify. This discards the edit. - Adopt the committed revision with
claim-adopt, then runclaim-verify. This keeps the edit and moves the authorized revision to it.
./bin/wowbagger.js claim-adopt --ledger path/to/ledger --input adopt.json --jsonAdoption is per item and per revision explicit — the request names the item, the
revision it believes is authorized, the revision being adopted, and who is
ruling. There is no adopt-all. It writes no item byte, so updated and the body
survive exactly, and it appends one revision-adoption journal entry so the
audit trail records the ruling. It refuses a stale witness, an unexpired claim on
the item, a revision that is not at Git HEAD and in the caller's own working
tree, and a ledger that would not validate. Adoption is not a fence hole: the
next out-of-protocol edit refuses again, measured against the adopted revision.
Full rules, the other blocking finding codes, and why validating against working-tree bytes was rejected are in the mutation contract section 12 and the work-claim contract sections 3.2 and 3.3.
The HTML report
report validates the complete ledger, reads .wowbagger/report.json, and
atomically publishes one self-contained HTML file. The output must be outside
the ledger. Relative configured output paths resolve from .wowbagger/;
relative --out overrides resolve from the caller's working directory.
{
"report_version": 1,
"repository": { "name": "Example repository", "logo": "logo.svg" },
"title": "Ledger report",
"output": "../../ledger-report.html",
"fields": {
"area": "/priority_area",
"complexity": "/complexity",
"rank": "/priority_rank",
"class": "/class",
"due": "/due",
"tags": "/tags"
},
"swarm": { "eligible_complexities": ["small", "medium"] }
}repository.logo, fields, and swarm are optional. Field values resolve
from parsed frontmatter with RFC 6901 JSON Pointers. A swarm requires mapped
area and complexity fields. The report fetches nothing at view time.
tags is the one multi-value mapped field. It accepts a nonempty string or an
array holding only nonempty strings: a scalar reads as a one-tag set, exact
duplicates collapse, and values sort deterministically. The mapping never
splits commas, lowercases values, coerces objects, or partially accepts a
mixed-type array. An empty array counts as missing; any other rejected value
is omitted from the item and counted as invalid metadata. area stays scalar.
The model carries fieldCoverage, one entry per configured field plus area
and tags when unmapped, ordered by field name with present, missing, and
invalid counts over the retained report population. An unmapped field counts
every retained item as missing, and a visible missing-mapping notice tells an
unconfigured mapping apart from missing item values. Missing metadata never
matches a filter value: there is no Unclassified bucket, so a filter for a
literal Unclassified tag matches only items really carrying that tag.
The report is a decision-focused workspace, not a state snapshot. It has
three sections behind accessible view navigation: Items (the default),
Flow, and Dependencies. Only the selected section is visible when
scripting runs; without scripting, every section stays readable through its
anchor and native details elements, and the artifact states its fixed scope.
Items opens with the state counts, then a sticky control strip: search, five quick views (Work next, In progress, Blocked, Needs triage, and All open), and the display controls (grouping, sorting, Basic/Standard/ Detailed, Show history, Expand all, Collapse all). Below 1100px the display controls fold behind a Display toggle so search and quick views stay in reach. Search and the facet groups form the scope; the scope narrows the summaries, Flow, and Dependencies alike. Quick views and Show history change only the list. Work next keeps its recommended order and prints the reasons beside each row; any other sort presents itself as that sort.
There is one list and one canonical detail per retained item. Desktop widths use a list/detail split; narrower widths show the selected detail inline. Opening a detail never clears the search, the facets, or the list position, and a detail opened from Flow or Dependencies returns to Items with the scope intact. Expand all acts on visible rows only.
The filters are facet groups: Readiness, Status, Kind, Priority, and one
group for every configured mapped field, each a fieldset of checkbox chips
behind a collapsed Filters control. Values inside a group are alternatives
and groups narrow each other; the search box is one more condition on the same
answer. Every chip carries the count it would leave, measured against the
search and the other groups but never against its own. Missing metadata is its
own Missing chip, distinct from a literal Unclassified value. The result
count states how much of the retained set is showing, and Clear filters
gives every selection back.
Above the list sit scoped summaries that open exact contributing items through a labelled drilldown pill: attention actions (in progress, blocked, needs triage, oldest, and started work past this ledger's own 85th-percentile cycle time), an area/status matrix with count and blocked count per cell, and Scoped members of existing batches, which intersects the area-diverse batches with the scoped ready set and omits empty batches. Each item detail states its downstream reach (transitive dependents in the report) and, separately, the items that become ready if done; neither alters core readiness or the recommended order.
Flow
Flow recomputes from the scoped open and terminal population in the browser:
cumulative flow, weekly arrivals against closures with done counted
separately, throughput with a four-week mean, current aging by status,
acceptance-to-completion samples, and the closure forecast. Inclusive From
and To controls default to the twelve-week window; a start after the end,
or an end after the report date, is refused with a visible error while the last
valid charts stay. Weekly buckets, aging cells, completion samples, and
cumulative date/band selections each drill into the exact contributing items,
and the accessible tables offer the same actions. The forecast is computed only
when Flow first opens and cached by cohort and range. Missing acceptance history
is stated as reconstruction uncertainty; an item killed straight from triage is
complete history, not a gap. The fixed server-rendered charts remain for
readers without scripting, each with role="img" and an aria-label that states
its finding in words.
The ledger graph
Dependencies draws the scoped items as a force-directed 3D graph. Every item is
a node, labelled #N, coloured by readiness for open items and by terminal
status for closed ones, and sized by the same transitive unblocking leverage the
recommended order uses. Edges run from a prerequisite or a parent to the item
it releases: a depends_on edge is straight and arrowed, a parent edge is
curved and unarrowed. Hovering a node opens a card with its number, title,
status, age, leverage, and reasons; clicking it, or a roster row, opens the
canonical detail in Items. Downstream and ready-if-done actions on the roster
drill into the same sets the detail names.
The graph has no filter of its own: it follows the shared scope, so a scope change drops nodes, every link that touched one of them, and their labels together, then reheats the layout in place. A hidden blocker never turns a retained item ready, because readiness is projected over the complete ledger before any view narrows it. The graph starts only when Dependencies first opens, pauses while another section is shown, and an empty scope draws an empty graph that says so.
The renderer is 3d-force-graph
over Three.js, vendored into vendor/3d-force-graph/ at a pinned version
1.80.0 with its upstream SHA-256 recorded in
vendor/3d-force-graph/VERSIONS.json and
pinned by a test. It is inlined into the report at generation time. Nothing is
fetched from a CDN, at generation time or at view time, and the report's content
security policy forbids every remote load, connect-src included. The bundle
costs roughly 1.3 MB of the report's size; the report stays one self-contained
file you can attach, open offline, and share.
Without WebGL the graph section says so and expands its own roster instead: one row per node, carrying that node's number, title, status, age, leverage, and reasons. No decision-relevant content exists only in the 3D view.
The recommended order is a report-layer derivation. It is recomputed from
ledger bytes at render time, never persisted, never a mutation, and it does not
change ready: the core still selects and sorts by priority, created date,
then ID. Ordering runs as separate, visible steps rather than one opaque score
— expedite class, then due proximity, then transitive unblocking leverage over
depends_on, then epic enablement from parent, then priority, then age, then
the mapped complexity as a WSJF-style size denominator — and every step that
placed an entry is printed beside it.
Two mapped fields carry the value dimensions the schema deliberately does not:
class— a class of service, one ofexpedite,fixed-date,standard, orintangible.expeditelifts an item above every other ready item. An absent value meansstandard. An unrecognised value is ranked as standard and reported by number in the report, never silently dropped.due— an ISO calendar date. The nearest due date sorts first and an overdue one sorts first of all; an item with no due date sorts behind every dated one at that step.
Both ride the ordinary extension-member channel, so the core neither reads nor
validates them. complexity weighs xs/s/small as 1, m/medium as 2,
l/large as 3, and xl/extra-large as 5; any other value carries no
weight and is shown as written.
Named custom report views
A named custom view is a second self-contained report generated from the same complete ledger. Every section of it — statistics, Work next and the other quick views, the Attention summaries and area/status matrix, dependency impact, Flow, the graph, the drill-down pill, terminal history, and the swarm batches — describes one configured subset, so the file is honest to share as a scoped report. Excluded items are absent from the bytes rather than hidden by a stylesheet. The base report stays available and unchanged.
Report configuration report_version: 2 accepts every version 1 member and adds
one more, views. A version 1 configuration keeps generating its base report
unchanged, and a version 2 configuration with no view selected publishes the
same base report from its inherited base members:
{
"report_version": 2,
"repository": { "name": "Example repository" },
"title": "Ledger report",
"output": "../../ledger-report.html",
"fields": {
"area": "/priority_area",
"complexity": "/complexity",
"class": "/class",
"security": "/security"
},
"views": {
"security-blockers": {
"title": "Security blockers",
"output": "../../reports/security-blockers.html",
"filters": {
"readiness": ["blocked"],
"status": ["backlog", "in-progress"],
"kind": ["task"],
"fields": {
"class": ["bug"],
"security": ["high", "critical"]
}
}
}
}
}Generate one view by name:
wowbagger report --ledger <dir> --view <name> --as-of YYYY-MM-DD --jsonOne invocation generates one artifact; no flag generates them all. --out <file>
overrides the selected output for that invocation, view or base alike, and the
configured paths are still validated when it is present.
A view name is a portable identifier matching ^[a-z][a-z0-9-]{0,63}$. Names are
case-sensitive and views holds at most 64 views. Each view takes exactly
title, output, and filters. Unknown members fail closed.
filters takes exactly readiness, status, kind, and fields, and at least
one of readiness, status, kind, or fields must be present. The semantics
are the drill-down's: OR within one filter group; AND across groups, so
readiness: ["blocked"] with class: ["bug"] means blocked bugs. readiness
takes ready, blocked, or ineligible; status takes triage, backlog,
in-progress, done, killed, archived, or deferred; kind takes task
or epic.
A fields key must also be a configured report field. Each field filter is a
non-empty array of unique JSON strings, finite numbers, or booleans, and matching
preserves JSON scalar type and value: stringification is not equality, so a
mapped 2 does not answer a filter for "2". An item carrying no mapped value
for a field matches no value selected for that field. A tags filter uses
any-member matching, so one item carrying two tags answers either tag. No
title-text inference, regular expression, arbitrary JSON pointer, or body
search belongs in a view filter.
Wowbagger validates the complete ledger and computes readiness against the complete ledger before it filters, so excluding a blocker never makes blocked work read as ready. What a view derives from its retained set does change: statistics, ranking leverage, epic enablement, evidence, attention, graph membership, and the swarm batches are all view-scoped, so the same item can report smaller numbers here than in the base report. That is why the artifact names its view and its fixed criteria at the top. A retained item that names an excluded dependency, parent, blocker, or related item still prints that item's number instead of a raw ULID, and the excluded item gets no row, no graph node, no history entry, and no hidden payload.
Inside the file, the interactive facets and the graph status chips narrow the retained subset further and can never reveal an excluded item. Clear filters restores the complete custom-view subset, never the base ledger.
The base output and every view output must be pairwise distinct after path
resolution, and each must resolve outside the ledger under the same no-follow
containment rule the base output already obeys. A colliding or contained output
is report-config-invalid before anything is rendered, --out present or not.
A named success adds exactly one member to the existing report result,
result.view, and its item_count and ready_count describe the filtered
subset. A base report's result gains no view member:
{
"ok": true,
"command": "report",
"contract_version": 5,
"result": {
"report_version": 2,
"as_of": "2026-08-21",
"output": "/absolute/reports/security-blockers.html",
"item_count": 12,
"ready_count": 0,
"view": "security-blockers"
}
}--view requires report_version: 2. A missing or unknown name is
report-view-not-found at exit 2 and leaves every existing output untouched. An
invalid filter value, an unmapped fields key, and a colliding output are
report-config-invalid at exit 2. An empty matched subset is not a failure: it
publishes a valid report with zero items and explicit empty-state copy. Failed
publication preserves the prior artifact, so never read an existing output as
fresh. wowbagger capabilities --json advertises the whole surface at
result.operations.report, so no consumer has to probe by generating a file.
A custom view is scoped output and not a security boundary: it applies no redaction and no access control, and the artifact states plainly that it is a filtered subset of the named repository ledger. Automation reads the JSON result, never the generated HTML and never human output.
This repository keeps its report configuration in
ledger/.wowbagger/report.json. Generate the ignored local report with the
current UTC date:
npm run report -- --as-of YYYY-MM-DDIf you verify the report in a browser from a checkout, generate a deterministic synthetic report through the real pipeline:
node scripts/report-design-demo.js --out /private/tmp/wowbagger-report-demo.html --items 40That output is synthetic and checkout-only: it describes fixed demo data, never this repository's ledger.
Where the contracts live
The README is the map. These are the territory, and they are normative where they disagree with anything above:
| Document | What it rules | |---|---| | SPEC.md | The ledger schema, validation rules, and ready selection. | | docs/mutation-contract.md | Response domains and dispatch, capabilities, inspect, create, transition, patch, the extension declaration, the frontmatter ownership table, errors and recovery, and commit-per-mutation. | | docs/work-claim-contract.md | Provisioning, claim CAS rules, claimed publication, reconciliation, revision adoption, and the difference between strict fenced and merge-coordinated backends. | | docs/adapter-contract.md | The harness adapter boundary: negotiation, forwarding, guards, approval, and honest outcome mapping. | | docs/host-contract.md | The direct-core host boundary: package resolution, the shell-free process tuple, bounded transport, owning-host paths, response dispatch, advertised limits, and the packaged JSON Schemas. | | docs/openai-compatible-integration.md | What an OpenAI-compatible host can do today, and what a compatibility claim would require. | | docs/schema-2-migration.md | The quiesced schema version 1 to 2 migration runbook. | | skills/wowbagger/SKILL.md | The shipped agent instructions. |
Where contract prose and a fixture disagree, the fixture is normative.
Working on wowbagger
This section is the contributor's route from "I want to help" to a merged change.
The ledger is the backlog
There is no separate issue tracker. The repository's real backlog is
ledger/, and it is driven with the tool itself:
npm ci
./bin/wowbagger.js validate --ledger ledger --json
./bin/wowbagger.js ready --ledger ledger --as-of YYYY-MM-DD # human queue
./bin/wowbagger.js inspect --ledger ledger --number 30 --json # the item you pickedReplace YYYY-MM-DD with the current UTC date. ready prints
#number pri=priority title per line in ready order. The acceptance criteria
live in the item body, not in the metadata — inspect and read
result.item.body.
Items are created into triage and reach backlog only through an explicit
accepting transition with a recorded decision. This repository dogfoods
layout.json, so its items live in ledger/items/.
Which contributions are useful now
- Reproducible defects. A failing command, its exact JSON envelope, the exit status, and the smallest ledger that shows it.
- Dogfood friction. Anything the tool made harder than doing it by hand. That is a defect worth recording, not a personality flaw of the user.
- Documentation drift. A claim in this README, a contract, or the skill that HEAD does not support. Name the claim and the source that refutes it.
- Platform evidence. A native conformance run on Linux or Windows is what
moves an adapter platform declaration off
unverified. Attach the report. - Portability and coordination requirements. Concrete constraints from a real harness beat speculative API surface.
Prefer an adapter to a core change when a requirement is harness-specific. File the finding as a ledger item rather than leaving it in a transcript.
The verification gate
Four commands. All four must pass, and the test commands run on Node 24.20.0:
TMPDIR=/tmp /opt/homebrew/opt/node@24/bin/node --test test/*.test.js
TMPDIR=/tmp /opt/homebrew/opt/node@24/bin/node --pending-deprecation --throw-deprecation --test test/*.test.js
TMPDIR=/tmp /opt/homebrew/opt/node@24/bin/node spec/run-adapter-implementation.js
TMPDIR=/tmp /opt/homebrew/opt/node@24/bin/node bin/wowbagger.js validate --ledger ledger --jsonTMPDIR=/tmp is not optional: the default macOS temporary path makes the claim
lock socket path too long. Use an explicit Node 24.20.0 binary path.
The supported runtime matrix is Node 24.20.0. Node 26 remains excluded until the separate Vitest incompatibility reported by Lee is resolved.
npm test, npm audit --omit=dev, and git diff --check are useful alongside
it; they are not a substitute for the four commands above.
The rules that are not negotiable
- The oracles are independent.
spec/adapter-reference.jsandtest/work-claim-reference.jsare separate re-implementations that conformance tests compare against.src/adapter/deliberately re-implements the first rather than importing it, andsrc/claim-request.jshas the same arrangement with the second. Never import an oracle intosrc/, and never change an oracle to match an implementation. Collapsing either pair into a shared implementation would make its conformance tests p
