Open source · Python ≥3.11 · Apache-2.0

Your format is a convention.
Make it a schema.

roadkeep owns the writes to your ROADMAP.md, CHANGELOG.md, IMPROVEMENTS.md and STRATEGY.md — so the format is enforced at the point of insertion instead of remembered by whoever is typing.

Shipped as a Claude Code plugin, because the author to constrain is usually an agent.

🗂 Your existing Markdown — no migration 🚫 Zero runtime dependencies 🔍 Greppable, diffable, no service
argparse + tomllib · nothing else Round-trip or refuse Every question is a command Per-project configuration
The problem, measured

This did not start as an idea

It started as three readings from a real production repository, where all three files declared a format and none of them enforced it.

docs/ROADMAP.md
142 words
average, across 92 task lines — worst line 1406 characters
Declared rule: one sentence per task.
agents.md
186 KB
≈ 46k tokens, loaded on every single turn
Declared rule: an index, nothing more.
docs/IMPROVEMENTS.md
539 KB
in a sibling project — rationale that was never dropped
Declared rule: rationale for unshipped work.

The finding that decided the design: six of the eight worst lines were written in the session that then diagnosed the problem.

This is not inattention. An author — human or model — who has the whole design in working memory will write it down where the reader is. An instruction to be terse does not survive the moment its author knows more than the line allows.

Why a linter was the wrong answer
“The saving is the analysis,
not the characters.”

A linter reports after the prose exists. By then the tokens are spent, and the author is being asked to delete work they just did. A field with maxLength: 200 refuses before a sentence is composed to fill it — the same rule, two orders of magnitude cheaper. It turns an analytical act (“is this too long, and what would I cut?”) into a procedural one (“call add).

What makes it different

The space around this is not empty

So the honest comparison is a narrow one.

ToolWhat it does wellWhy roadkeep is not it
markdownlint Structure and style of Markdown Explicitly not prose — it will never tell you a sentence is too long
Vale Prose rules and style guides A linter: it reports after the text exists, which is the cost being avoided
Backlog.md, taskmd,
the markdown-task family
Mature task management, kanban, MCP One .md file per task, with acceptance criteria and DoD — more room, and more room invites more prose
ADR / MADR Rationale that survives; superseded is never deleted An ADR set grows monotonically — that curve is the 539 KB above
1

Enforced at the write path

The refusal happens before the prose exists. Everything else in this space reports afterwards, at review time.

2

It governs the file you already have

One line in your existing ROADMAP.mdnot a new store you migrate into. A repository with a hand-written backlog is the target, not the obstacle, so it will measure yours and tell you what adopting would cost before you commit to it.

3

Round-trip or refuse

Parse → render → byte-identical, or the tool declines to write the file at all. A tool that owns writes to a hand-edited file has to prove it cannot corrupt one.

4

Query instead of read

Every question a maintainer asks the file is a command, so answering it costs no context. Reading a backlog end-to-end to find one ready task cost ~5k tokens in this very repository.

The six laws

A change that breaks one is wrong, even if requested

L4 is the one people try to relax first. A generator that writes the symptom for you would reintroduce exactly the drift this exists to stop.

L1

The format is a schema, enforced where the text is created; lint is only the backstop.

L2

The store is the repository — Markdown, greppable, diffable. No database, no service.

L3

Round-trip or don't write — parse → render → byte-identical.

L4

The tool never writes prose — it validates and renders.

L5

Query instead of read — every question is a command.

L6

Configuration, not convention — prefix, paths, markers and limits are per project.

Status

The model, the writes, the queries and the gate are done

What is left is the layer that makes an agent unable to route around any of it.

41 of 46 tasks shipped
11 shipped · 0 open A — The model (a task is data before it is a line)
8 shipped · 0 open B — Authoring (insert, never hand-edit)
9 shipped · 0 open C — Query (consult without reading the file)
9 shipped · 0 open D — The gate
3 shipped · 1 open E — Adoption
1 shipped · 4 open F — The Claude Code plugin (the guardrail at the agent boundary)

Next ready RK21 — A standard adopted by one project is a preference

A whole task is four calls. Not a manual to learn — a shape: ask what to work on, write the line, ship it, prove nothing drifted.

# the arc of one task roadkeep brief what to work on, and everything it costs to start it roadkeep add compose the line — the field is refused, never the sentence roadkeep ship <id> three edits across three files, all of them or none roadkeep lint exit 1 when anything drifted. That code is the contract.

Around those sit the rest — status, sections, the dependency walk, the git-derived history, the projection that keeps this page honest. Every one takes --json carrying which file and which line the answer came from, because an answer an agent cannot audit gets verified by reading the file, which is the cost the command existed to remove. The full surface lives in --help and the repository; a landing page is the wrong place for it.

Python ≥3.11, zero runtime dependencies — which is what makes the first line below possible: there is nothing to resolve.

uvx roadkeep lint

No install and no checkout. Or pip install roadkeep to keep it around.

Non-goals

These are binding, and half the point

Worth reading before proposing work.

No web UI and no server. Files and a CLI.

No issue-tracker sync. A backlog that lives in a service is one an agent cannot grep.

No model and no prompts inside the tool. It validates and renders; it never writes the symptom or the rationale.

No dates, quarters or estimates. A marker is maturity, not a schedule.

🧪

This repository is the conformance fixture

roadkeep lint must pass on its own docs/. The format is proven by the artefact rather than asserted in a README — including that one. A limit that cannot express these lines is the wrong limit, not a set of wrong lines, and the test suite asserts it against docs/ROADMAP.md under this repository's own roadkeep.toml.