Skip to main content

roadmap-core

The roadmap graph: status derivation, dependency and relation edges, arc state, validation, and the markdown renderers behind roadmap/ROADMAP.md and ARCS.md.

Stdlib-only and dependency-free, which is the point rather than a nicety. Three properties depend on it:

  1. scripts/roadmap.py loads roadmap_core/graph.py by path, so a coding agent in a checkout can read the backlog with no install, no DB, no admin token and no network.
  2. The Lucille backend imports the same module, so a status derived by the API and a status rendered into the committed markdown cannot disagree — one implementation, two callers. That divergence is a failure this repo has been bitten by before.
  3. It is the extraction seam. The roadmap is being prepared to run as its own product whose default store is a single SQLite file with nothing to provision; a package that pulled in a web framework or an ORM could not be adopted by another repo without adopting Lucille with it.

Storage

store.py is the schema — one SQLite file, CREATE TABLE IF NOT EXISTS on first open, no migration step. stores.py is what you talk to:

LocalStore ApiStore
needs a writable path a call(method, path, payload)
provisioning none a running host
claim/release one BEGIN IMMEDIATE transaction one HTTP request
impact raises Unsupported the host's tickets

Both satisfy the same Store protocol, so a caller never branches on which it holds. ApiStore is constructed with the host's own caller and holds no token, no URL and no urllib import — auth stays entirely a host concern, and test_stores.py asserts that rather than trusting it.

from roadmap_core.stores import LocalStore

with LocalStore("roadmap/roadmap.db") as store:   # created if absent
    store.upsert_item({"key": "a-thing", "title": "A thing"})
    store.claim("a-thing", by="claude/some-branch")

From the CLI, --source local on push, claim, release and status, or ROADMAP_SOURCE=local once. ROADMAP_STORE sets the path.

Adopting it in another project

Measured end to end by tests/test_adoption.py, which runs the CLI as a subprocess against a scratch project with nothing on the path but this package — no backend, no FastAPI, no SQLAlchemy, no Postgres, no server, no token.

pip install "roadmap-core[files]"   # [files] adds PyYAML, which authoring needs
mkdir -p roadmap/items
export ROADMAP_SOURCE=local

That is the whole install. roadmap is a console script that comes with the package — there is nothing to copy. (It used to say curl -o scripts/roadmap.py <this repo>/scripts/roadmap.py, and this repo is private, so the package was installable, importable and useless to anyone outside it.)

Then the ordinary loop, which needs nothing else:

cat > roadmap/items/first-thing.yaml <<'YAML'
id: first-thing
title: The first thing to do
status: ready
evidence: |
  Why this is worth doing, and how you will know it worked.
YAML

roadmap push            # files -> store
roadmap ready           # what is startable
roadmap claim first-thing
roadmap release first-thing

The store is one SQLite file at roadmap/roadmap.db. There is nothing to provision and no migration to run: it is created on first open.

When something is off, ask

roadmap doctor          # is this project's setup actually working?
roadmap --version       # which roadmap-core is this?

doctor exits non-zero when something is genuinely broken and names the remedy. Run it first, because the way this setup fails is by looking fine: the read commands answer from the store, and a store nobody has pushed to is empty, so validate says "ok — 0 item(s), no problems" and ready says the backlog is finished. Both are green, confident and wrong. Same for a command run from outside the project: every path still resolves, and push reports "no item files to push", which reads as an empty backlog rather than as a wrong directory.

Those are the two failures tests/test_adoption.py was written after, and both are one line of doctor output:

FAIL  store       roadmap/roadmap.db holds 0 items while roadmap/items/ holds 7.
                  Every read command will report an empty backlog and call it ok.
                  Seed it: `roadmap push`

Who owns a claim depends on which store you have. On the SQLite floor the file is authoritative: CI rebuilds the store from roadmap/items/*.yaml every run, so push takes a file's claim when it CREATES the item — otherwise a held item renders as ready and sync --check fails for as long as anybody is working. Against a served store the file is a projection of a store that outlives the checkout, so a claim in a file is never pushed: a stale clone would recreate one the store had already released. Either way push ignores it on UPDATE, because the store is the live record of who holds what.

Not guessable from the field's name, which is why it is written down here.

Two things that are conventions rather than choices, both found by doing this rather than by reading the code:

  • Your project root is the nearest ancestor holding roadmap/items or .git, so the commands work from anywhere inside it. Set ROADMAP_REPO_ROOT to pin it. Deliberately not bare roadmap/: that is a directory the tool creates, so keying on it let one command run in the wrong place mint the marker that made that place look like a project forever after.
  • Authoring is writing a YAML file, not calling an API. push is what moves it into the store; there is no roadmap new. That is deliberate: filing an item belongs in a diff somebody reviews.

ROADMAP_SOURCE=local selects the SQLite store. Without it the CLI expects the API store, which is how Lucille runs it — see roadmap_core.stores.

Agents (MCP)

roadmap-mcp serves the same graph to a coding agent as MCP tools over stdio — ready, list, show, validate, claim, release, set_status. Point a client at it:

{
  "mcpServers": {
    "roadmap": {
      "command": "roadmap-mcp",
      "env": { "ROADMAP_SOURCE": "local" }
    }
  }
}

There is no extra to install and no SDK underneath. mcp_server.py speaks the JSON-RPC protocol in stdlib, so it is present wherever the package is — which is the same reason the rest of this package is dependency-free, applied to one more caller. An extra would be one an adopter can forget, for a server that needs nothing.

Reads default to the files, so an agent in a fresh clone can ask what to work on with no store, no server and no token. Writes need a store to arbitrate them: set ROADMAP_SOURCE=local as above, or the write tools expect the API store. There is deliberately no files write target — a claim nothing adjudicated is not a claim, and two agents could each hold the same item.

A write tells the agent to commit something, and it means it. claim, release and set_status project into roadmap/items/<key>.yaml, and on the floor that projection is the durable record — an unmerged one is a claim no other checkout can see. The tool result carries the CLI's own words for this under notes.

CI

Copy templates/roadmap.yml to .github/workflows/roadmap.yml. That is the whole CI story for the floor:

push      # files -> store, rebuilt fresh each run
validate  # schema, dangling dependencies, cycles
sync --check   # is the committed ROADMAP.md still what the graph renders?

No schedule, no credentials, no bot identity, no commit back to the default branch, no self-hosted runner. tests/test_adoption.py reads the commands out of that file and runs them, so a template that has drifted from the CLI fails rather than reading as tested.

The third line is the one that earns the workflow. ROADMAP.md is generated but committed — that is what lets an agent read the backlog with no install and no network — and a generated file nobody regenerates is a file that lies.

Do not commit roadmap/roadmap.db. It is derived: push rebuilds it from the YAML on first open, and a binary file in git conflicts on every claim. The files are the record; the store is the transaction that decides who gets one.

Upgrading to a served store

The floor's simplicity comes from one property: the store is inside the checkout, so there is no second copy to drift from. Move the store to a server — so that claims are visible across machines the moment they are taken, rather than when a branch merges — and four things come back, none of which the template can supply for you:

What returns Why
A credential step the store is now behind auth, and the CLI needs a token per run
A wait-for-reachable step a concurrent deploy can hold the store down, and being early is not being wrong
pull and a bot commit the store now knows things no checkout does, and they have to land in the files an agent reads
A schedule finishing an item is usually a code change somewhere else entirely, so no path filter can catch it — only re-asking on a clock can

ApiStore is constructed with your own caller, so the auth stays yours (see the table under Storage). Lucille's .github/workflows/roadmap-sync.yml is the worked example of all four, and the reason it is not shipped as a template: almost every line of it is a consequence of Lucille's own deployment, and handing an adopter that machinery for a problem they do not have reads as required rather than as one option.

Running a served store yourself: roadmap serve

The four rows above assume somebody already runs a server. roadmap serve is one you can run: stdlib only, one process for many projects ("tenants"), each tenant's roadmap its own SQLite file behind the same /admin/roadmap API the db source speaks. A client changes nothing but two variables.

# on the server's host: tenants and tokens are managed here, never over HTTP
roadmap serve --data /srv/roadmap tenant add my-org
roadmap serve --data /srv/roadmap token create --tenant my-org --label "ci"
#   prints the token once; the server keeps only its SHA-256
roadmap serve --data /srv/roadmap run --host 0.0.0.0 --port 8443 \
    --tls-cert /etc/roadmap/fullchain.pem --tls-key /etc/roadmap/privkey.pem

# wherever the project's sessions run
export ROADMAP_API_URL=https://roadmap.example.com:8443
export ROADMAP_API_TOKEN=rmk_...          # LUCILLE_* names are read as fallbacks
roadmap push --source db && roadmap claim <key> --source db
Property How
Tenants cannot see each other one SQLite file per tenant; the tenant comes from the token, never the URL
A copied registry holds no credential tokens are 256 random bits, stored as SHA-256 only, shown once
Least privilege read, write, admin scopes; admin for prune, refile, arc deletion and a forced claim
No token crosses a network in the clear TLS 1.2+, or loopback behind your own TLS proxy; anything else refuses to start without --allow-plaintext
A probe learns nothing unknown, revoked, expired and disabled-tenant tokens get one identical 401
Bounded body cap, no chunked bodies, socket timeout, concurrency cap, per-token and per-address budgets (429 with Retry-After)
Accountable an audit log of every request under the token's id, and a per-tenant transition log: GET /admin/roadmap/{key}/history, GET /admin/roadmap/transitions?after=<id>

To run it on a VM behind a Caddy that already serves :443 there, see deploy/vm/README.md: the files, the one-time DNS and VM steps, the deploy script, and the admin, upgrade and backup commands, each run end to end against a stand-in for that Caddy before it was written.

Operating it is yours: back up --data (it is SQLite files: registry.db and tenants/*.db), renew the certificate, and rotate a token by creating the new one before revoking the old (roadmap serve --data … token list | revoke). impact answers 501 here: it needs a host's feedback tickets.

What is NOT here

HTTP and auth anywhere but roadmap_core/server.py. The graph is pure functions over plain dicts keyed by key, so the same code serves DB rows, API payloads and parsed YAML with no adapter; the server is a thin layer over LocalStore and the graph, and nothing else imports it but the CLI's serve command. Lucille's own persistence stays in backend/app/crud/roadmap.py over SQLAlchemy; the two definitions of the same three tables are held together by backend/tests/test_roadmap_store_parity.py, which asserts both the columns and the row dicts the two readers produce.

The package's tests live in tests/ here and import nothing outside the stdlib. .github/workflows/roadmap-core-tests.yml runs them in a job that fails if app, fastapi, sqlalchemy or yaml can be imported at all — the isolation is asserted, not assumed.

Metadata

Release files for roadmap-core 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for roadmap-core 0.4.0
File Size Uploaded Provenance Actions
roadmap_core-0.4.0.tar.gz 153.1 kB With provenance Details for roadmap_core-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for roadmap-core 0.4.0
File Interpreter ABI Platform Actions
roadmap_core-0.4.0-py3-none-any.whl Python 3 none any Details for roadmap_core-0.4.0-py3-none-any.whl

Total release size: 261.4 kB

Release files / roadmap_core-0.4.0.tar.gz

Download URL roadmap_core-0.4.0.tar.gz
Size 153.1 kB
Tags Source
SHA-256 checksum
How to use checksums
bb07dd80be3e73543522ef7d39750145312f9fd7ea6ad95d79e9a6d169f7d3b7
BLAKE2b-256 checksum
How to use checksums
82046a079ea19cefb992789eb532b3a2b6bb61b8d4bf1e645727cbe62a4a9776
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / roadmap_core-0.4.0-py3-none-any.whl

Download URL roadmap_core-0.4.0-py3-none-any.whl
Size 108.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8290b36e491992cf61a49b242084d97b2b418b85237b4595e5b53b06595a8d54
BLAKE2b-256 checksum
How to use checksums
45d1cf3fc64d9301ae7595ed8cd744408807aa9e8280c22cc7c64a07561cb509
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Security for roadmap-core 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for roadmap_core-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page