This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
rmap is a single-binary Rust CLI that manages portable roadmap data (roadmap/tasks.toml) for any project, regardless of language. It renders ROADMAP.md and roadmap/data.json from the TOML source. Since 2026-05-13 rmap drives its own roadmap from roadmap/tasks.toml → ROADMAP.md; see DESIGN.md for the design contract and AGENTS.md for additional contributor guidelines.
Universal includes (per ~/.claude/setup-guide.md). No Rust-specific template exists; rmap takes the universal baseline only — the Elixir/Phoenix includes don't apply. Delegation includes (Linear/cloud-agent) are intentionally omitted — rmap has no git remote and is not in the Linear/cloud-agent queue. The harness MCP IS wired (.mcp.json — see § "Driving harness from this repo"). web-command.md is intentionally NOT imported: per the setup-guide's "Skills vs Includes" rule, situational tool references auto-load as skills — rmap does no browser work, so the web-command skill covers the rare case without paying the token cost every session.
@/.claude/includes/across-instances.md
@/.claude/includes/critical-rules.md
@/.claude/includes/worktree-workflow.md
@/.claude/includes/task-prioritization.md
@/.claude/includes/task-writing.md
@/.claude/includes/rmap.md
@~/.claude/includes/workflow-philosophy.md
cargo build # compile
cargo test # unit + integration + golden tests
cargo test --test cli # run a single test file
cargo test render_command_updates # run tests matching substring
cargo fmt --check # check formatting (rustfmt defaults)
cargo clippy --all-targets -- -D warnings
cargo run -- validate # exercise CLI during dev
cargo run -- render --dry
Edition is 2024 (Cargo.toml). MSRV: whatever ships with Rust 1.85+.
Reinstall after any source change. rmap is dogfooded on itself, so a stale ~/.cargo/bin/rmap will silently render with the old schema or reject TOML using a newly-added field. After any change to src/, run cargo install --path . before invoking rmap again. The skills_smoke test compiles fresh under cargo and catches SKILLS.md regressions regardless of the installed binary; interactive rmap calls do not.
Prefer rmap CLI over direct tasks.toml edits when a mutator exists. Today the mutator surface is rmap status (single + bulk), rmap mark, rmap depend, and rmap new — all go through toml_edit and validate-then-write. Direct edits are the only path for everything else (bundle/phase CRUD, focus, scores, titles, ACs, top-level metadata); after any direct edit run cargo run -- validate --check-render before committing.
Keep ~/.claude/includes/rmap.md in sync. It's the consumer-facing decision-layer doc (which command, when) imported by every rmap-using project's CLAUDE.md, including this one. Update it in the same commit when the command surface or a user-visible schema affordance changes. rmap.md deliberately does NOT enumerate fields — rmap schema / rmap --help is authoritative.
Evaluate roadmap & task design from the consuming-agent POV. rmap IS a tool for Claude. When picking up rmap tasks or reviewing the roadmap, evaluate the design from the perspective of the agent (Claude) that will actually consume the tool day-to-day — not just the perspective of the AC author. If the AC overspecifies in ways that hurt daily ergonomics, push back and propose the consumer-first alternative in the plan. Examples: a transition that requires manual TOML edit between two commands is friction Claude will route around; a field that's not surfaced in rmap show is invisible to the consumer; a section ordering optimized for one rare-path command (delegate) at the cost of the daily-path command (show) is the wrong tradeoff. (feedback_decide_as_consumer.md captures the same posture as personal memory; this rule lifts it to the project so it's authoritative for all rmap work.)
Pipeline: tasks.toml → schema::Tasks → render_roadmap_str (markdown), export_json_str (data.json), or render_html_str (static HTML, opt-in via --html) → write back. Mutations route through toml_edit to preserve user formatting and comments.
Modules — each file's doc comment is the authoritative reference for its internals:
schema.rs—serdestructs,#[serde(deny_unknown_fields)]everywhere.TaskIdisNumber(u32) | Text(String)untagged.validate.rs— semantic checks on a parsedTasks(versions, statuses, markers, score range, deps, cycles, references, timestamps).render.rs— three-pass marker walker overROADMAP.md: focus → mermaid → tasks. Only bytes between matched marker pairs are rewritten.export.rs—ExportedTaskJSON shape; adds computedeff. Pretty by default;_compact_variant feeds the HTML data island.render_html.rs—rmap render --htmlsingle-project view (templates/roadmap.html.j2) and--html --multiportfolio view (templates/portfolio.html.j2), sharing component macros (templates/_components.html.j2) and one stylesheet (templates/_styles.css); all minijinja, allinclude_str!'d. Builds DAGs via the longest-path layering intopo.rs. Portfolio inputs areProjectInputenvelopes (project root,tasks.toml, ordata.jsonpath — seemain.rs::load_project_input).topo.rs— pure longest-path layeringcompute_layers(tasks) -> {id → depth}over the in-repodepends_ongraph (extracted fromrender_html), pluscompute_layers_from_edgesover pre-extracted(id, deps)lists (the portfolio's JSON-loaded tasks). Shared byrender_html.rs(DAG vertical slotting) andexport.rs(the computeddep_layerfield).mutate.rs—status/mark/depend/newpaths. All usetoml_edit::DocumentMutand end withvalidate_tasks_strbefore returning.next.rs— pure selectors:next_tasks(tasks, filter, count)(highest-Eff pending unblocked) andready_tasks(tasks, filter, count, dispatchable)(the whole parallel-safe set), both ranked via sharednext::rank_tasks— a 4-tier lexicographic key (focus phase × active milestone, focus dominant) then Eff descending.is_unblockedis the dep-satisfied predicate; theactivemilestone set is computed once per call fromtasks.milestones.query.rs—show/listread paths.TaskFilter+find_task+list_taskspure; humans getformat_task*, JSON delegates toexport.rs.bundles.rs—rmap bundlesread path. Per-bundlenext_taskreusesnext::next_task.next_bundle.rs—rmap next-bundlepure selector. Broad actionability (in-bundle pending deps satisfy if themselves actionable). Topological emit via Kahn's.delegate.rs—rmap delegateprompt formatter. Read-only Markdown; never calls Linear/GitHub/Slack.import.rs—rmap importprompt formatter. Interpolates project name + live JSON Schema intotemplates/import_prompt.md.diff.rs—rmap diffengine.diff_toml(base, current, verbose) -> TomlDiff. Per-task field walk hand-maintained viadiff_fields!macro.schema_json.rs— JSON Schema forTasksviaschemarsderives.scoring.rs— sharedefficiency,format_efficiency,tier_glyph, date helpers. Pure-integer Howard Hinnant date math; no chrono.stale.rs— pureparse_duration+find_stale. No I/O.doctor.rs— soft-signal aggregator. Always exits 0; strict gates remain onvalidate.paths.rs— ancestor walk to findroadmap/tasks.toml. CLI flag overrides;html_pathis fixed-derived.watch.rs— pure helpers forrmap watch.write_if_changed(idempotency primitive),is_tasks_toml_event(filter), JSON event-line builders.main.rs—clapderive CLI.run() -> Result<ExitCode>. Wires every command to its module.
Easy to violate without breaking tests immediately. The "why" lives in source doc comments and tests; this list is the index.
Render & markers
- Marker boundaries are byte-preserved (TASKS / FOCUS / MERMAID / VISION / MILESTONES). Don't normalize input bytes outside matched pairs.
- FOCUS / MERMAID / VISION line shape and empty-state strings are agent-grep contract. Changing wording is a
schema_versionbump. - Archive collapse triggers on
phases.N.status = "done"and emits the one-lineSee [CHANGELOG.md#…]body. Line shape is locked byvalidate --check-render.
Schema & validation
schema_version = 2is required. Bump on any breaking schema change.effis never persisted. Computed at render/export time;schema::Taskwould reject it viadeny_unknown_fields.- D/B/U range is
1..=10. The error message stringmust be in 1..=10is part of the agent-grep contract. linear_idvalidation is conditional on[linear]table presence (Linear is opt-in).blocked_reasonis required iffstatus = "blocked". Mutator re-validates, so the transition can't write without one. Settable viarmap status <id> blocked --reason "<text>"(free-text, overwrites, blocked-only — ignored with a one-line stderr note on other transitions, mirroring the outcome flags). Auto-cleared when a blocked task leaves the blocked state (the reason described a state that no longer holds); re-blocking keeps/overwrites. No interactive prompt — if neither--reasonnor an existing value is present, the validation error fires and the file stays byte-equal.implementedis required and non-empty iffstatus = "done". Mirrors theblocked_reasonpattern. Error stringis done but missing implementedis agent-grep contract.delivered_byandverifiedare optional outcome-layer fields, both transition-time, both settable only onstatus = "done".delivered_byis free-text (which agent shipped the task; mirrorsmodel— unvalidated, no agent registry).verifiedis a bool with two-state semantics:Some(true)= an independent evaluator confirmed the task; absent = not graded.Some(false)is permitted by the schema but the mutator never writes it (presence flag only). Doctor emits a softClaimedNotGradedadvisory whenstatus = "done"&&verified.is_none()— always exit 0; hand-built/bootstrap tasks legitimately land ungraded.attemptsis an append-only transition-time list, settable only onstatus = "pending". Each entry is an inline-table{ at, by?, report }(stored likecross_repo):atis auto-filled fromtoday_iso(),byis free-text agent attribution (optional),reportis the failure evidence (a reviewer's rejection report). Appended — never overwritten — byrmap status <id> pending --report "<text>" [--attempt-by <agent>]; each call adds exactly one entry (no dedup).--reporton a non-pendingtransition is ignored with a one-line stderr note (mirrors the outcome/--reasonflags);--attempt-bywithout--reportis likewise a no-op note. Renders inrmap showandrmap delegate's## Prior attemptssection; surfaces in--json/data.json(skipped when empty, so attemptless tasks round-trip byte-identically). Transition-time field → mirror surfacescanonical_task_key_index(index 30, trailing) +diff_fields!+ExportedTask/EXPORTED_TASK_FIELDS; deliberately NOT inTASK_VERBOSE_WHITELIST(a list, likecross_repo).rmap doctormilestone drift advisories are soft (exit 0, no auto-mutation).MilestoneFullyDoneButOpenwhen every task pinned to a milestone isdonebut milestone status ispendingoractive;MultipleActiveMilestoneswhen more than one milestone isactive(rmap.md: keep exactly one). Human lines cite milestone slug and pinned-task count; JSON kindsmilestone_fully_done_but_open/multiple_active_milestones.rmap doctorphase / focus drift advisories are soft (exit 0, no auto-mutation; phase/focus state is user-curated).PhaseFullyDoneButOpenwhen every task in a phase isdonebut phase status is stillpending/active;PhaseHasInProgressButPendingwhen apendingphase has ≥1in_progresstask;FocusPhaseClosedwhenfocus.phasepoints at a phase that appears closed (done status or all tasks done). Human lines cite the phase number (and task ids for the in-progress case); JSON kindsphase_fully_done_but_open/phase_has_in_progress_but_pending/focus_phase_closed.touchesis an optional creation-time free-text list, advisory and unvalidated (posture ofmodel/assignee). Semantically distinct fromfiles_to_modify:files_to_modifyis the implementer's write target;touchesis the broader involvement hint (files that may be read or written) — typically a superset. Consumer collision rule (documented, NOT enforced in rmap): two tasks conflict iff(touches(A) ∪ files_to_modify(A)) ∩ (touches(B) ∪ files_to_modify(B)) ≠ ∅. Creation-time field → all six mirror surfaces (see theTaskdoc comment inschema.rs); ondiff_fields!but deliberately NOT inTASK_VERBOSE_WHITELIST.domainsis an optional creation-time free-text list, advisory and unvalidated. It tags the capability/domain evidence an orchestrator may group by (for examplerust,otp,ecto); rmap owns no vocabulary and only carries/export the strings. Creation-time field → all six mirror surfaces; ondiff_fields!but deliberately NOT inTASK_VERBOSE_WHITELIST(a list, liketouches/cross_repo).- Timestamps validate by shape (
YYYY-MM-DD), not semantics.9999-99-99passes on purpose; values live next to user-edited TOML. - Status / marker / cross-repo-relation enums live in
validate.rsconstants; render-time match arms inrender.rsdon't share a source — keep both in sync. - Milestone status enum (
pending | active | done) lives invalidate.rs::VALID_MILESTONE_STATUSES— distinct vocabulary from task status.rmap milestonessort order is(status_rank: active=0/pending=1/done=2 asc, milestone.order asc); "active first" is load-bearing for the daily release-cut query. task.milestonereferences must resolve intasks.milestones.validate_milestone_referencesenforces this; mutator pre-validates before writing.TaskIdEq/Hashare normalizing acrossNumber(n)↔Text("n"). A task id is a primary key; the disk form (TOML integer vs TOML string of the same digits) does not change which task it names.validate_unique_idsrelies on this to catch cross-form collisions;validate_dependencies/validate_dependency_cycles/doctor.rsdegenerate-bundle check /next_bundle.rsactionability memo are correct on mixed-form files because of it. Text ids that don't parse asu32(e.g."INE-5","alpha") keep their own canonical key. Agent-grep substring for the duplicate-id error isduplicate task id.
Mutations
- All mutators use
toml_edit::DocumentMut(nevertoml::from_str) and end withvalidate_tasks_str(...)before returning. Invalid mutations leave the file byte-equal. - Bulk
rmap status 1,2,3 doneis atomic — all-resolve-or-no-write. Don't add a "best effort" flag without explicit user request. rmap statusis the only mutator that auto-fills lifecycle timestamps (done_at,started_at). Never overwrites existing values. Changing this is aschema_versionbump.rmap markandrmap statusauto-sort task keys when inserting a new field; idempotent calls do not.add_dependency_strdeliberately does NOT auto-sort.rmap newauto-allocates numeric IDs only.TaskId::Textis never auto-generated. Duplicate explicit IDs error before any write.- Lifecycle timestamps are NOT settable on creation.
started_at,done_at,blocked_reason,shipped_inare absent fromNewTaskFields— those are transition fields owned byrmap status.
Agent contract (renaming/removing breaks consumers)
--jsonoutputs ofshow/list/next/next-bundle/ready/bundles/schema/diff/doctorare additive-only. Add fields freely; rename/remove →schema_versionbump.rmap next --countJSON shape is split by N: default (--count 1) emits a bare object/null;--count >1emits an array. Flipping default-to-array is a bump.rmap nextranking is 4-tier lexicographic(in_focus_phase × in_active_milestone) ⇒ Eff desc, focus dominant: tier 0 (both) > tier 1 (focus-only) > tier 2 (active-milestone-only) > tier 3 (neither). Tasks pinned to ANY milestone withstatus = "active"qualify. Without[focus], every task counts as "in focus" → tiers collapse to 0/1. The focus-dominance bit (tier 1 > tier 2) is the load-bearing decision.next::rank_tasksis the single source of this sort, shared withready.rmap readyis the parallel-safe dispatch set: allpendingtasks whose everydepends_onisdone, ranked by the same 4-tier key asnext(vianext::rank_tasks). The set is mutually independent by construction — a pending task whose deps are alldonecannot depend on another pending task — so there is NO--independentflag (it would be a no-op). Unlikenext,--countis optional (default = the whole set) and--phasefilters the pool.--bundle B= the dispatchable layer-0 of B.--jsonis alist-shaped envelope.dep_layeris never persisted. Computed at export time (src/topo.rslongest-path depth over the in-repodepends_ongraph); likeeff,schema::Taskrejects it viadeny_unknown_fields. Always built from the FULLtasks.taskgraph, never a filtered slice —export's slice-taking fns (export_task_json_str,export_tasks_array_json_str) take&Tasksfor exactly this. Surfaces on every--jsonpayload (additive).--dispatchable(onready/list) excludeshandbuild-marked tasks.handbuild∈VALID_MARKERSflags human-driven-browser work (LiveView/UI/DOM) — the minority exception, so everything else is headless-dispatchable by default.query::is_dispatchableis the predicate; onreadyit filters beforerank+countso the cap counts only dispatchable tasks.--fields a,b,c(onready/list) projects--jsonto a bare array of objects carrying only the named keys (envelope dropped — token-cheap). Implies--json; unknown name → exit 1 naming the offender, validated againstexport::EXPORTED_TASK_FIELDS. Absent optional keys simply don't appear per task.rmap next-bundleranking is(in_focus_phase desc, sum_eff desc, bundle.order asc)and the three empty-state stderr spellings are load-bearing.rmap bundlesrow separator and five-branch glyph ladder (✅/🚧/all-blocked ⛔/pending:<n> (deps unmet) ⏸/next:<id> [Eff:x.y] <tier_glyph>) are agent-grep contract.rmap milestonesmirrorsrmap bundles's five-branch glyph ladder and adds a trailing[target=<version>]segment whenmilestone.target_versionis set. Sort key and row shape are agent-grep contract.- MILESTONES marker section is opt-in and grouped:
<!-- MILESTONES:BEGIN -->/<!-- MILESTONES:END -->renders one block per milestone sorted likermap milestones, including name, target_version, status glyph, hypothesis description, and done/total pinned-task counts. Roadmaps without the marker pair render byte-identically. - Render-row 🚀 segment is conditional + positional:
🎁 **bundle** · 🚀 **milestone** · {module} · {category} {title}. Inserted between bundle and module_segment; emitted only whentask.milestone.is_some(). Rows without a milestone render byte-identically to pre-Task-24 — regression-guarded by golden fixtures. - Render-row
⛔ {blocked_reason}segment is conditional + trailing: appended after the tier glyph, emitted only whentask.status == "blocked"andblocked_reasonis non-empty. Non-blocked rows (and blocked rows are the only ones affected) render byte-identically otherwise — additive, golden-guarded bytests/golden/mermaid_block. - Eff tier glyph is centralized in
scoring::tier_glyph(>=2.0 🎯 / >=1.5 🚀 / >=1.0 📋 / else ⚠️). NEVER fold intoformat_efficiency— JSON payloads must stay numeric. rmap delegate's seven canonical##sections (Context→Task→Acceptance criteria→Out of scope→Files to modify→Scoring→Environment notes) and the[D:_/B:_/U:_ → Eff:_] <glyph>Scoring shape are locked byemits_canonical_section_order_with_distinguishing_line.rmap delegate --tois optional;assigneeis the routing default.delegate::resolve_targetresolves the target: explicit--toalways wins (and renders theStored assignee: ... (overridden)bullet when it differs); without it the task'sassigneeIS the target. No assignee → exit 1has no assignee; pass --to <agent>;assignee = "human"→ exit 1is assigned to human; pass --to <agent> to delegate anyway. Both error strings are agent-grep contract. Routing metadata is split:assignee= which agent executes,model= free-text LLM id/pin,domains= free-text capability tags for downstream scoring,delegate --to= render-time override.rmap delegate's per-agent footer mirrors~/.claude/includes/cloud-agent-environments.md— sync manually when the skill changes.rmap diff --againstdefaults tocurrent.default_branch— never hardcode"main".rmap diff --verboseis additive. Non-verbose output stays byte-identical to pre-11b. Whitelist members inTASK_VERBOSE_WHITELIST/METADATA_VERBOSE_WHITELISTare part of the contract.- HTML data island id is
rmap-data, script typeapplication/json. Agents parse the element's text; they do NOT scrape the DOM. - HTML task cards carry six
data-*attributes (data-id,-status,-eff,-markers,-depends-on,-phase); DAG nodes carrydata-id; phase sections carrydata-phase-status. Stable selector contract. - Portfolio HTML (
--html --multi) adds two islands:rmap-data(aggregate{"projects":[…]}of every input's envelope) andrmap-relations(resolved cross-repo edge array{source, target, relation}); repo rows carrydata-slug/data-name/data-has-rel. Same parse-the-island-not-the-DOM rule.
Mirror-surface edit rules
When adding a field to schema::Task, decide whether it is a creation-time field (set at rmap new time) or a transition-time field (set later by rmap status / rmap mark / rmap depend / etc.), then update the appropriate surfaces in the same commit. The full invariant lives on the Task doc comment in src/schema.rs; this is the working summary.
- Creation-time field → SIX surfaces:
main.rs::StdinTask(stdin parse shape)mutate.rs::NewTaskFields(mutator argument struct)mutate.rs::add_task_str(TOML writer)mutate.rs::canonical_task_key_index(key ordering for serialization)diff.rs::diff_fields!(drift surface forrmap diff)export.rs::ExportedTask(--json/data.jsonshape) — ANDexport.rs::EXPORTED_TASK_FIELDS(the--fieldsprojection's validation set;exported_task_fields_cover_serialized_keysguards drift). Any newExportedTaskfield (creation-time, transition-time, or computed likeeff/dep_layer) must be added to this const.- Then decide whether to add to
diff::TASK_VERBOSE_WHITELIST. Interactiveprompt_task_fields(main.rs) is optional — power-user fields (branch,files_to_modify,touches,cross_repo) intentionally require--from-stdinrather than dialoguer.
- Transition-time field (lifecycle timestamps,
implemented, outcome-layer, etc.) → update the owning mutator (set_status_strfor status transitions, etc.) plusdiff::diff_fields!andexport::ExportedTask. Stays absent fromStdinTask/NewTaskFieldson purpose — today:started_at,done_at,blocked_reason,shipped_in,implemented,delivered_by,verified,attempts. - New top-level field on
schema::Tasks→ also editdiff::diff_metadataANDexport::ExportedTasks(Task-level macro doesn't cover them; hand-walked).
Time & determinism
today_iso()is the only source of "now". ReadsRMAP_TODAYenv var first, falls back toSystemTime::now(). Date-sensitive tests MUST setRMAP_TODAYon theCommandenv (ortoday.txtfor golden fixtures).
Watch
rmap watchwatches theroadmap/directory (not the file) withRecursiveMode::NonRecursive, and filters viais_tasks_toml_event. The filter is the infinite-loop guard against our owndata.jsonwrite.rmap watchevent shape is the agent contract —schema_version+eventdiscriminator (rendered/error) +outputs/messageare additive-only. BumpWATCH_SCHEMA_VERSIONfor renames.
Exit codes
rmap doctoralways exits 0 on success (informational only). Strict gates:validate(exit 1 on schema error),validate --check-render(exit 2 on render drift).
The "consumers" the agent contract above protects are not hypothetical — the primary one is harness, a sibling Elixir/OTP project at ../harness/ (/Users/efries/_DATA/code/harness/). Harness is an AI-orchestrator-driven task-execution engine: it pulls tasks from rmap roadmaps, dispatches each to a headless coding agent (Claude Code, Codex, Cursor, Grok, Antigravity, Pi) in an isolated git worktree, grades the result with the target project's own check stack, and writes the verified outcome back via rmap status. Harness's CLAUDE.md § "rmap is ours" sends roadmap-CLI gaps here to be fixed, never worked around in harness — this section is the reciprocal pointer.
The shell-out surface (../harness/lib/harness/roadmap.ex, Harness.Roadmap). Harness never parses tasks.toml itself; it shells out to the installed rmap binary and treats stdout as API. Every call passes an explicit --tasks-path; success is gated on JSON-decode (or non-empty delegate output), not exit 0. Commands consumed:
rmap next --json·rmap show <id> --json·rmap list --json [--status S]·rmap next-bundle --json— browse/ingestrmap ready --dispatchable --fields id,assignee,markers— the cron poller's autonomous selection surface (MCP toolroadmap__ready); the poller routes each task on itsassigneermap delegate <id> --to <agent>— the verbatim output IS the prompt dispatched to the agent- Write-backs:
rmap status <id> in_progress(on dispatch),rmap status <id> done --verified --shipped-in <sha>(lander, after a green verdict + push),rmap status <id> blocked --reason "..."(terminal sink)
Changing any of these — JSON shapes, --fields projection, delegate section format, status flags, the handbuild semantics of --dispatchable — means checking Harness.Roadmap (and Harness.Dispatch / Harness.Lander / Harness.Cron.RoadmapPoller) in the same change, plus the consumer-side docs below.
Renderable ≠ executable (the two-sided executor contract). rmap delegate --to renders for seven agents (claude / codex / cursor / grok / antigravity / pi / droid); harness has AgentAdapters for only six — droid is renderable but rejected at harness's dispatch boundary ({:unknown_adapter, "droid"}). Adding a new --to target in rmap is half the job: the agent only becomes dispatchable once harness grows a matching AgentAdapter + @valid_agents entry. When widening rmap's delegate/assignee set, note the harness-side gap explicitly (a task in harness's roadmap, or a line in the commit) rather than implying end-to-end support.
Consumer-side contract docs (update when rmap's surface changes underneath them):
../harness/skills/harness-driver/SKILL.md— the AI-orchestrator contract for driving harness (dispatch patterns, MCP tool surfacedispatch__*/roadmap__*, result shapes). It documents rmap-derived behavior (theready --dispatchableset, delegate-rendered prompts, the renderable-vs-executable split) and carries an explicit anti-staleness contract.../harness/CLAUDE.md— § "Agent Headless Entry Points" and § "Dogfooding" reference rmap's delegate targets and selection commands.../harness/docs/dogfooding-workflow.md— the operator runbook; verdict table references rmap status write-backs.
Harness registers projects (including itself) with a roadmap_path and drives them through this surface unattended (Oban.Plugins.Cron) — a silent break in rmap's JSON or prompt output surfaces as failed autonomous dispatches there, not as an rmap test failure here. The skills_smoke test and the additive-only invariants above are the local proxies for that contract; treat them as guarding harness specifically.
The relationship also runs the other way: rmap's own roadmap tasks can be dispatched through harness (Context A of the harness-driver skill — consuming repo drives the harness BEAM). The wiring:
.mcp.jsonregisters two HTTP servers against the harness BEAM (user-startediex -S mixin../harness/; never boot it yourself):harness→mcp__harness__*(the native flat driver tools —dispatch__task,dispatch__await,dispatch__status,dispatch__verdict_detail,roadmap__*; primary surface) andharness_eval→mcp__harness_eval__project_eval(arbitrary-Elixir escape hatch into harness's BEAM, for struct-level ops the flat tools omit).- rmap is registered as a harness project in harness's gitignored
config/dev.local.exs(:rustpreset,roadmap_path= this repo). Registration changes need a harness BEAM restart (the user does that). Per-project cron autonomy defaults OFF — registration alone does not start autonomous dispatch. - Load on demand when driving (not eager-imported, per the selective-load philosophy): the
dev-lifecycle:harness-workflowskill (the delegate → verify → repair → land loop) and../harness/skills/harness-driver/SKILL.md(MCP tool shapes, dispatch patterns, sharp edges).
tests/cli.rs— black-box CLI tests viaCommand::new(env!("CARGO_BIN_EXE_rmap")). Each test gets a unique temp dir from a per-test atomic counter. Date-sensitive tests setRMAP_TODAYon theCommandenv.tests/skills_smoke.rs+tests/skills_fixture/— parses every fencedbashblock inSKILLS.md, extracts the optional# exit: <N>annotation (default 0), and runs eachrmapinvocation in a fresh fixture copy withRMAP_TODAY=2026-05-12pinned. Agent-contract gate forSKILLS.md— renaming or removing a documented command requires updating bothSKILLS.mdand the fixture in the same commit.tests/golden/<case>/— fixture triples:tasks.toml,ROADMAP.input.md,ROADMAP.md(expected output). Optionaltoday.txtpins the render date. Addtoday.txtto any fixture usingscored_atto avoid drift into score-decay.tests/roundtrip.rs— parse →toml_editround-trip → assert no spurious diff. Catches comment-preservation regressions.
ROADMAP.md (rendered from roadmap/tasks.toml) tracks open phases; DESIGN.md carries the design contract and out-of-scope list; CHANGELOG.md is the shipped-phase record. Implemented today: validate, render (incl. --html static single-project view and --html --multi portfolio view), watch, export json, status (single + bulk), mark, depend, new (interactive + --from-stdin), next, ready, show, list, bundles, schema, diff, delegate, import, stale, doctor. Score-decay rendering is automatic on tasks with scored_at >30d or missing. Deliberately out of scope (per DESIGN.md): Linear API calls, web server, git integration beyond git show <ref>:<path> for rmap diff, shell completions, CI workflow, multi-user sync. Don't add these without checking the roadmap first.