Structured issue tracking for agent-managed repositories: AI agents write small JSON records, humans read a generated, interactive, self-contained
ledger.html.
A ledger is a directory of small JSON files, committed next to the code, holding what is wrong
with the repository and what to do next. There is no service to run. A single static Go binary,
cs-ledger, validates the records and renders the page, and you commit both.
┌──────────────┐ ┌───────────────────┐ ┌──────────────┐
│ agents │ write │ ledger/ │ render │ ledger.html │
│ ├─────────►│ issues/*.json ├────────►│ │
│ file, note, │ │ queue.json │ check │ one file, │
│ close │ │ ledger.json │◄────────┤ no server │
└──────────────┘ └───────────────────┘ └──────────────┘
all committed
No build and no server. Open either of these in a browser:
- This repository's own ledger, tracking its own work (source).
- A frozen copy of another project's ledger, kept as a test corpus (source).
An agent that finds a bug halfway through a task has nowhere good to put it. A comment gets lost, a
TODO never gets read, and an issue tracker needs credentials the agent does not have. The finding
gets mentioned once in a summary nobody keeps, and then it is gone. A record in the ledger outlives
the session that wrote it, and the next agent starts from it.
Build the tool from a clone and put a ledger in a repository that has none. INSTALL.md has the other ways to get it:
git clone https://github.com/codesweep-ai/ledger && cd ledger
make install # ~/.local/bin/cs-ledger
cd ~/my-service
cs-ledger init ledger --project my-service --prefix MYSThat writes the config, an empty issues/, a copy of the guide, a router beside it, and a first
render. An agent that opens a record finds the router and follows it to the guide. To point one
there from the start, a line in AGENTS.md, CLAUDE.md or a role brief carries the same weight:
This repo keeps a ledger. Read
ledger/GUIDE.mdand follow it: file records before building, close only with verified evidence, keep the queue current, and runcs-ledger checkgreen before every commit.
From then on, agents write records and run two commands before each commit. Here they are against this repository's own ledger:
$ cs-ledger render && cs-ledger check
wrote ledger/ledger.html (… bytes, 10 issues, 0 drafts)
check OK: 10 issues, 0 drafts, 0 warning(s), ledger.html freshIf the page no longer matches the records, check says so and exits non-zero, so a stale page
cannot reach a commit:
$ cs-ledger check
validation errors (1):
- ledger.html: STALE — records changed without re-render. Run: cs-ledger render
check FAILED: 1 error(s), 0 warning(s)One file per issue, named after its ID:
{
"id": "MYS-004",
"title": "Token refresh races on concurrent requests",
"type": "defect",
"severity": "high",
"status": "closed",
"foundBy": "session agent, while reading auth/refresh.go",
"opened": "2026-08-11",
"resolved": "2026-08-13",
"stint": "rev-5",
"evidence": {
"commits": ["9f2c1ab"],
"integrated": [],
"verified": "100 concurrent refreshes, 1 token minted (was 7)"
},
"resolution": null,
"details": "Two goroutines that both find an expired token each mint a new one...",
"notes": [{ "date": "2026-08-12", "text": "Reproduced with a 100-way race." }],
"links": []
}The rules are few and they are the point. A closed record has to say how the fix was proved and
which commit did it. check resolves that sha against the repository, so an invented one fails the
gate. Notes append and never rewrite history. IDs are never renumbered or reused.
SPEC.md states all of them.
ledger.html inlines its own data, CSS and JavaScript, so it works from file:// with no network
request. Four views share one masthead:
- brief: the landing view. What changed lately, what the queue says to fix next and why, and what needs a human right now.
- board: records stacked by status.
- list: the full inventory, grouped and sorted on demand.
- activity: every dated event, newest first.
The page is a pure function of the records, with no timestamp of its own. That is what lets check
prove it is current, by rendering again and comparing bytes. It also makes a merge conflict in
ledger.html a non-event: you never hand-merge that file, you render it again.
make build # bin/cs-ledger, Go alone
make check # gofmt, vet, Go suite, coverage, linters
make ci # every gate CI runs; needs npm
bin/cs-ledger check ledger # this repo's own ledger
bin/cs-ledger check fixtures/sandbox/ledger
make viewer-build # rebuild viewer/index.html; needs npmviewer/app/ holds the Vite and React source. It builds against @codesweep-ai/ui, an npm
dependency pinned to one exact version, and writes a self-contained viewer/index.html that the
Go binary embeds. That file is committed, so building the tool needs Go alone. Node 22.13 or
newer with npm is needed only to change the viewer, and by make ci, which rebuilds it to prove
the committed copy is current.
- INSTALL.md · getting the binary, and scaffolding a ledger with it
- GUIDE.md · how to keep a ledger: the moves, the evidence rules, the queue
- MANUAL.md · every verb, flag, file and diagnostic
- SPEC.md · the record schema, the rules, and why they are the rules
- CONTRIBUTING.md · working on the tool