Single Rust binary that manages roadmap/tasks.toml in any project (Elixir, Rust, Python, Go, anything). Renders portable views: ROADMAP.md (agent-readable, dense), data.json (dashboard-consumable), optionally HTML (human-readable).
This file is the design contract — schema, CLI surface, invariants, deferred designs, out-of-scope. Live phase tracking moved to ROADMAP.md (rendered from roadmap/tasks.toml) on 2026-05-13; historical shipped phases live in CHANGELOG.md.
- Pre-commit boot ~10ms vs ~1.5s for an equivalent
mixtask. Compounds across N repos × commits/day. - Single static binary, install once via
cargo installor homebrew tap. No per-project language dependency. - Language-neutral by construction: works in Elixir, Rust, Python, Go projects identically.
serde+tomlmake the schema layer trivial;clapmakes the CLI ergonomic;minijinjahandles templating.
schema_version = 1
project = "ccxt_extract"
default_branch = "development"
[focus]
phase = 12 # active phase — `rmap next` and dashboards key off this
[linear]
team_key = "INE" # validates linear_id format (e.g. "INE-247")
workspace_url = "https://linear.app/efries" # used to construct issue URLs in rendered views
[phases.12]
name = "Per-Exchange Normalization"
order = 12
status = "in_progress"
[bundles.ticker_normalization]
phase = 12
order = 1
description = "Unified ticker fields across all exchanges"
[milestones.v0_1] # release lines cross phases by design
name = "v0.1 — first usable cut"
order = 1
status = "active" # pending | active | done — distinct from task status
target_version = "0.1.0" # optional, free-text
[[task]]
id = 74
phase = 12
bundle = "ticker_normalization"
milestone = "v0_1" # optional — pins to a release line; absent = unpinned
status = "done" # pending | in_progress | blocked | done | superseded
title = "parseTicker field map + coercion + enums"
scores = { d = 5, b = 8, u = 8 } # eff = (b+u)/(2d) computed by rmap, never stored
scored_at = "2026-04-15" # last D/B/U revision; >30d renders with `?` suffix
markers = ["parallel"] # subset of: parallel | cx | csr
assignee = "claude" # human | claude | codex | cursor (optional)
model = "claude-opus-4-7" # LLM model to use (optional, free-text); rmap delegate surfaces it
linear_id = "INE-247" # optional — when a Linear issue tracks this task
created_at = "2026-04-01" # ISO-8601 date (optional)
started_at = "2026-04-12" # set when status → in_progress
done_at = "2026-04-30" # set when status → done
shipped_in = "PR #21" # PR title/number or commit SHA
[[task]]
id = 75
phase = 12
bundle = "order_normalization"
status = "pending"
title = "parseOrder field map"
scores = { d = 6, b = 8, u = 8 }
scored_at = "2026-05-01"
depends_on = [74] # in-repo task IDs this is blocked by
linear_id = "INE-300"
assignee = "codex" # `rmap delegate 75 --to codex` reads this
acceptance_criteria = [ # rendered as bullet list; agents verify against this
"parseOrder accepts spot + futures payloads",
'Empty venues field maps to null, not ""',
]
# Cross-repo dependencies. relation: "blocks" | "blocked_by" | "related"
cross_repo = [
{ repo = "ccxt_client", task_id = 42, linear_id = "INE-310", relation = "blocks" },
]
body = """
Multi-line prompt-style body when needed. Optional.
"""
# When status = "blocked", blocked_reason is required:
# status = "blocked"
# blocked_reason = "Waiting on legal review of session token storage"# read / render
rmap render # write ROADMAP.md + data.json from tasks.toml; renders into TASKS / FOCUS / MERMAID marker pairs when present (zero-config)
rmap render --dry # print would-write diff, no file changes
rmap render --stdout # render ROADMAP.md to stdout (for diffing in hooks)
rmap render --html # planned — single-project view → roadmap/dist/index.html
rmap render --html --multi P1 P2 # planned — portfolio HTML across N repos/data.json paths
rmap export json # data.json to stdout (for piping)
# validation + health
rmap validate # schema check + integrity (orphan deps, marker validity, cycles)
rmap validate --check-render # also verify ROADMAP.md is in sync; exit 2 on drift, 1 on schema error
rmap doctor [--json] # health summary: validate findings, stale, score-decay, drift — always exits 0
# query / introspection — agent read-API
rmap next [--marker parallel] [--json] # next highest-Eff unblocked pending
rmap show <id> [--json] # full task detail; --json for piping
rmap list [--status S --marker M --phase N --bundle B --milestone V --json] # generalized query
rmap milestones [--has-next] [--status STATE] [--json] # release-line discovery + next-task glyphs
rmap schema # emit JSON Schema for editor completion + agent self-description
rmap diff [--against <ref>] [--json] [--verbose] # what changed in tasks.toml vs base ref (default: default_branch)
rmap stale --over <duration> [--json] # in_progress tasks idle > duration
# mutation — all routed through toml_edit
rmap status <id[,id,id]> <new> # flip status (bulk form atomic), re-render
rmap mark <id> +cx -parallel # add/remove markers without TOML editing
rmap milestone <id> <name|none> # pin a task to a release line (or unpin via "none")
rmap depend <id> on <id> [--cross-repo <repo>:<task_id>[:<relation>]] # add deps via mutation
rmap new # interactive task creation (dialoguer)
rmap new --from-stdin # non-interactive — agent piping
# delegation — cloud-agent workflow
rmap delegate <id> --to claude|codex|cursor # emit paste-ready Markdown prompt: title + body + deps + AC + per-agent environment-notes footer
# live dev — planned
rmap watch # FS watch on tasks.toml, render on change
rmap watch --json # planned — event stream for agent consumers
Cross-cutting invariant. The --json outputs of show, list, next, schema, diff, and doctor are the agent contract. Treat them like a public API: add fields freely, but never rename or remove without a schema_version bump.
Same source flow: tasks.toml → data.json → HTML. The HTML is a derived view, not a replacement for ROADMAP.md. Markdown stays canonical for git diffs, terminal scanning, and skill consumption. HTML is for humans skimming progress and for shareable snapshots that survive outside a checkout.
Audience: both agents and humans. Humans read the visual layout. Agents read the rendered HTML by extracting the embedded data island (see invariant 2 below). The same artifact serves both.
rmap render --html— single-project view atroadmap/dist/index.html(gitignored).rmap render --html --multi P1 P2 …— portfolio view across N repos. EachPnis either a project root (rmap discovers itsroadmap/data.json) or a path to adata.jsondirectly. Output:roadmap/dist/portfolio.htmlin the current working repo, or--out <path>to redirect.
┌──────────────────────────────────────────────────────────────────┐
│ rmap Phase 1: 8/10 ▓▓▓▓▓▓▓▓░░ Phase 2: 3/7 ▓▓▓░░░░ │
│ markers: [all][chain][api][docs] status: [✓][→][⏸][⛔] │
├──────────────────┬──────────────────┬────────────────────────────┤
│ PENDING │ IN PROGRESS │ DONE │
│ ┌────────────┐ │ ┌────────────┐ │ ┌──────────────────┐ │
│ │ #14 eff:6 │ │ │ #12 eff:5 │ │ │ #11 render-html │ │
│ │ html-out │ │ │ dep-graph │ │ │ #10 export-json │ │
│ │ ⊃ #12 #11 │ │ │ ●chain │ │ └──────────────────┘ │
│ │ ●chain ●ui │ │ └────────────┘ │ │
│ └────────────┘ │ │ │
├──────────────────┴──────────────────┴────────────────────────────┤
│ DEPENDENCY GRAPH (SVG, layered DAG) [+] │
└──────────────────────────────────────────────────────────────────┘
Vertical phases stacked; each phase header carries a progress bar so a top-down skim conveys the whole project in 5 seconds. Within a phase, three horizontal status columns (pending / in_progress / done) read left→right as progress. A sticky filter bar exposes marker chips and status toggles. The dependency graph sits below, collapsed by default.
Same shell, but the top level is repos-as-rows: each row is a mini progress strip (phase-bar summary) + the top 3 open tasks by eff. A top-of-page panel renders cross-repo blockers driven by the cross_repo field — arrows between repo cards visualize "X blocks Y across the fleet." Click a repo row → expands inline to the single-project layout.
- Self-contained single file. Inline CSS + vanilla JS. No CDN, no framework, no external assets, no build step. <50KB target per page. Opens offline; uploads to S3 as one blob; attaches to email; survives indefinitely without a server.
- Embedded data island. A
<script id="rmap-data" type="application/json">…</script>carries the fulldata.jsonverbatim inside the HTML. Agents extract structured data without DOM scraping; client-side filters read from this island. This is the single most important choice for "agents read it too" — the visual layer never becomes the bottleneck for an agent reading the file. - Semantic data attributes on every task element:
data-id,data-status,data-eff,data-markers,data-depends-on,data-phase. Stable selectors, greppable, future-proof. - Color = status, chips = markers. Status carries the primary visual signal (done=green, in_progress=blue, pending=slate, blocked=amber). Markers render as small muted text chips, not loud colors. Status colors must remain WCAG AA at body text size; print stylesheet falls back to status symbols (
✓ → ⏸ ⛔) for monochrome output. - Dep graph as SVG, layered DAG. Topological layers, downward arrows. Collapsed by default. SVG (not Canvas) because nodes are selectable, text is searchable, and each node carries
data-idfor agent introspection. In portfolio mode, cross-repo edges render distinctly (dashed + repo label) from in-repo edges. - Print stylesheet included. Leadership prints, hands around, marks up. Status symbols + grayscale layout, no progress bars (they don't print well — replaced with
N/M donenumerals).
- No dark mode toggle, no user preferences, no animations, no tooltips that hide content.
- No JS framework dependency. No npm. No bundler. Vanilla JS, hand-written, single file.
- No per-user customization. The HTML is a report artifact, not an app.
- No live updates — that's the Phoenix dashboard's job (see
dashboard_roadmap.md).rmap render --htmlproduces a static snapshot taken at render time. - No template authoring surface. The HTML template ships inside the rmap binary alongside the markdown template. Both evolve in lockstep with the schema.
rmap render --html is the portable static view: one file, offline-readable, share-friendly, zero infrastructure. The Phoenix LiveView dashboard (dashboard_roadmap.md) is the always-on live view: real-time file watchers, filter persistence, cross-repo aggregation across an entire fleet. Different surfaces for different rhythms — both consume the same data.json schema, both pin to schema_version.
When to use which:
- Static HTML — share a snapshot in email, attach to a PR, hand to leadership, archive a milestone, work offline.
- Live dashboard — daily driving on the desktop while authoring + flipping statuses across repos.
serde+serde_derive— schema deserializationtoml— source format parsing (preserves comments on round-trip viatoml_edit)toml_edit— forrmap status/rmap new(mutations preserve formatting + comments)clap(derive feature) — CLI argument parsingminijinja— template rendering for ROADMAP.mdserde_json—data.jsonemissionanyhow+thiserror— error handlingnotify—rmap watchfile eventsdialoguer— interactivermap newowo-colors— terminal output
Estimated ~600-1000 LOC.
~/_DATA/code/rmap/ — standalone Rust crate, separate repo from any project that uses it.
- Local dev:
cargo install --path ~/_DATA/code/rmap - Future: homebrew tap (
brew install efries/tap/rmap) once stable - CI: each project documents
rmapin its README setup section, alongside language toolchain - Per-project pin: optional
roadmap/.rmap-versionfile the binary checks against
schema_version = 2 at the top of tasks.toml is load-bearing. rmap refuses to render tasks files whose schema version it doesn't understand. Bump on breaking changes; per-project upgrades are LLM-driven (the agent edits tasks.toml directly, then runs rmap validate) — no rmap migrate subcommand exists or is planned (out-of-scope per the "Out of scope" list).
Read this section before changing anything. The schema example above IS the contract — don't deviate even when something looks more idiomatic.
Schema invariants — fail validation if violated:
schema_version = 2required at file top. Refuse to parse unknown versions with a clear error message; upgrades are LLM-driven, normap migratesubcommand.- Tasks are
[[task]](TOML array of tables), NOT[task.74](table-per-id). Order is file order — preserves user authoring order, keeps diffs small. eff = (b+u)/(2d)is computed at render time, never stored. Don't add anefffield to the schema.markersmust be a subset of{"parallel", "cx", "csr"}.statusmust be one of{"pending", "in_progress", "blocked", "done", "superseded"}. Whenstatus = "blocked",blocked_reasonis required.[milestones.<name>].statusmust be one of{"pending", "active", "done"}— distinct vocabulary from task status.task.milestone, when present, must reference a declared[milestones.<name>]key. Milestones are flat-namespace (no nesting) and a task pins to at most one.linear_id(when present) must match<team_key>-<integer>per[linear].team_key. Skip the format check entirely if the[linear]table is absent — Linear is opt-in.assignee(when present) must be one of{"human", "claude", "codex", "cursor"}.- Timestamps (
created_at,started_at,done_at,scored_at) are ISO-8601 dates (YYYY-MM-DD). All optional — presence is what unlocks decay / stale / recently-shipped features. <!-- TASKS:BEGIN phase=N -->/<!-- TASKS:END -->are preservation boundaries. Render replaces ONLY contents between matching markers; everything else inROADMAP.mdis hand-edited prose and must be byte-preserved. Same rule for<!-- FOCUS:BEGIN -->/<!-- FOCUS:END -->and<!-- MERMAID:BEGIN -->/<!-- MERMAID:END -->.
Mutation crate boundary. All write paths (status, mark, depend, new) round-trip through toml_edit::DocumentMut to preserve comments and formatting. Never use toml::from_str → to_string on a file that will be written back — it strips comments and reflows. New mutators inherit this contract.
Self-description boundary. rmap schema --json emits a JSON Schema that must match the live schema::Tasks deserializer. CI: a unit test parses the emitted schema and validates the example tasks.toml against it. Keeps the agent-consumable contract honest as fields evolve.
Delegation boundary. rmap delegate is a pure read — it never mutates state and never calls external APIs. It emits text to stdout for the user (or a wrapper script) to paste. Any Linear / GitHub / Slack posting stays in skills, not in rmap.
- Unit tests for parse: valid
tasks.tomldeserializes to the expectedTasksstruct; invalidschema_version/ marker / status /linear_id/assignee/ timestamp formats produce specific errors with file path + line location. - Golden tests for render:
tests/golden/<name>/tasks.toml+tests/golden/<name>/ROADMAP.md(expected output). Runner reads input, renders, asserts byte-equal against expected. Adding a test case = drop in a fixture pair. Render-template changes that break expected output fail the suite loudly. - Round-trip test: parse → serialize via
toml_edit→ assert no spurious diff. Catches comment-preservation regressions across mutator additions. - Marker preservation test: a
ROADMAP.mdfixture with hand-edited prose surrounding<!-- TASKS:BEGIN -->/<!-- TASKS:END -->blocks. After render, the prose is byte-equal; only marked block contents change. - Schema self-consistency test:
rmap schema --jsonoutput validates the exampletasks.toml. Fails if the emitted JSON Schema drifts from the live deserializer. - Cycle detection test: a
tasks.tomlfixture with adepends_oncycle failsvalidatewith a clear error pointing at the cycle members. - SKILLS.md exit-code gate:
tests/skills_smoke.rsparses every fencedbashblock inSKILLS.md, runs eachrmap …invocation againsttests/skills_fixture/, and asserts the declared exit code. The agent-contract gate for documented commands.
- No web server in
rmapitself. Dashboard is a separate Phoenix app — seedashboard_roadmap.md. - No remote sync / multi-user collaboration. Single-developer tool.
- No git integration beyond reading the working tree (no commit, no push).
rmap diffreadsgit show <ref>:tasks.tomlbut never writes. - No Linear / GitHub / Jira API integration in
rmapcore. The schema carries identifiers (linear_id,shipped_inPR refs) as data; external skills handle the API side and feed updates back viarmap status.rmap delegateemits a paste-ready prompt — it does not post anything. - No per-consumer migration scripts. Per-repo migration plans live in their own repos (e.g.
~/_DATA/code/ccxt_extract/migration_roadmap.md, mirrored in this repo as a design reference only). - No CI workflow, no GitHub Actions, no
rustfmt.tomlfor rmap itself. Cargo defaults are fine; this is a personal-productivity tool, not an OSS release. - No
--helppolish beyondclapdefaults, no man pages, no shell completions. - No render-template authoring surface. Templates ship inside the binary; users don't fork
minijinjafiles. If a render shape is wrong, fix it upstream and ship a new binary. - No process-side roadmap conventions. Coverage gates (
mix test --covertiers), Ceremony Floor (review-time triage of small findings), auto-CHANGELOG / auto-CLAUDE.md / auto-README updates on task completion: these are review and audit skill behaviors applied to roadmaps, not roadmap data. The schema deliberately stops at the data surface —rmapdoesn't enforce or automate them.