skill · phase: ideation
Roadmap
{{glossary:instance}}
.aidlc/roadmap/ is a first-class inbox, independent of every lifecycle phase. Items are captured cheaply now and converted into instances later. Capture is legal at any moment: mid-implementation on another instance, from a second session, or with no instance at all.
Projects have more than one person proposing work, with mixed technical depth, so the roadmap distinguishes submitted from agreed. A contributor with no checkout can author an item with the portable skill at .aidlc/resources/skills/roadmap-item/SKILL.md — loadable into any AI agent — and commit the result to inbox/.
Statuses Are Directories
.aidlc/roadmap/
inbox/ submitted, nobody has triaged it
backlog/ accepted, will do, not started
in-progress/ an instance exists
done/ shipped
hold/ parked or declined
The directory an item sits in is its status. There is no status field, and you must never add one — two representations of one fact drift apart, and a wrong status is indistinguishable from a right one. Changing status means moving the file.
hold/ holds both parked and declined items, and the history entry says which. One directory is deliberate — a sixth status would have to be threaded through every count, every move and every projection — but "not now" and "not ever" are different decisions, and until the reason was written down they were indistinguishable on disk. So every move into hold/ carries a reason, prefixed parked — or declined — :
history:
- { status: backlog, at: 2026-09-22 }
- { status: hold, at: 2026-09-23, reason: "parked — gated on the accounts table" }
The consequence worth stating: agreed work waiting on a trigger belongs in hold/, parked, not in backlog/. backlog/ means "could start now", and an item gated on something that does not exist yet sitting there makes every count of what is ready wrong. Parking it is not dropping it, and the reason is what proves that.
Item Format
One file per item: .aidlc/roadmap/<status>/YYYYMMDD-<slug>.md, beginning with exactly these keys in this order:
---
id: item-20260814-cost-tracking
title: "Cost tracking"
created_at: 2026-08-14
author: Rodrigo Asensio - rasensio@ # optional; omit rather than guess
source: human # human | agent | import
promoted_to: null # instance name once promoted, else null
depends_on: [] # other items, by id
history:
- { status: inbox, at: 2026-08-14 }
---
titleis always written inside double quotes, with no exceptions and no judgement call about whether this one needs them. An unquoted title is a YAML plain scalar, and a plain scalar may not begin with a reserved indicator character — so the single most natural way to name a code defect,title: `readItem` returns null, does not parse. Nothing reports it: the whole item then goes missing from browse, from promotion lookup, from theroadmap-doneaction and fromaidlc doctor, because an item that will not parse and an item that was never written look identical. Five of this project's first 49 items were lost this way for up to ten days. Quoting unconditionally is three keystrokes and removes the class; "quote it when it looks risky" does not, because backticks do not look risky.idis assigned once and never recomputed. Renaming the file or moving it between statuses leaves it unchanged. Theitem-prefix keeps it textually distinct from the filename so nothing derives one from the other.sourcerecords how the item was written:humandirectly,agentdrafted by an AI agent from someone's description,importmigrated from an older format.depends_onnames other items byid, so a dependency survives its target moving. Build order is derived from this graph when needed — there is no globalorderfield, because concurrent capture from independent sessions cannot coordinate a sequence.historyis append-only and advisory. Append{ status, at }when you move an item. An absent or incomplete history is never an error, and current status always comes from the directory — never from the last history entry. An entry may carry a third key,reason, and a move intohold/must; always write it inside double quotes, because the entry is a flow mapping and a reason containing a comma or a colon ends it early and takes the whole item's frontmatter with it.
Bodies are otherwise free-form. Items authored through the portable skill carry four further headings — Problem, Who it's for, What success looks like, Out of scope — which is a useful shape, not a validated one.
Project-Declared Extension Fields
A project that outgrows those eight keys declares its own in .aidlc/config.yaml, and everything that writes an item then emits them:
roadmap:
item_fields:
- name: requirements
required: true
default: []
name must be lowercase letters, digits and underscores, and may not shadow a stock key. status is refused by name — status is the directory, and a second representation of it is the one extension the layout can never allow. required defaults to true; default defaults to null.
The worked example is real. fieldmodel-apps keeps requirements traceability as data: docs/requirements-traceability.md holds the requirement IDs, every item declares which ones it delivers in requirements: [...], and scripts/check-roadmap-requirements.mjs computes coverage as a set difference. It requires the key even when empty, because a set difference over an optional key silently under-reports. Before this existed, the portable roadmap-item skill said "Do not add any other field" while that check demanded one — two individually-correct rules that no agent could satisfy at once, in a framework-owned file the project was not allowed to edit.
Declaring a field changes three things and nothing else:
- The portable
roadmap-itemskill's template carries the field, and its "add no other field" rule becomes bounded rather than absolute.aidlc updatere-emits it. aidlc roadmap importand theroadmap-layoutdoctor migration write the declared default into items they create. Triage moves androadmap-donealready preserve the field, because they edit frontmatter as text rather than re-serializing it.aidlc doctorreports items missing a required declared field and offers to backfill the declared default.
The framework checks presence and shape, never contents. Shape is taken from the declared default — a list default means a list value, a scalar means a scalar — and a default: null declares no shape, so only presence is checked. There is no config key that could express a rule about what goes in a field, and that absence is deliberate: requirements: [not-a-real-id] is the project's own script's business. A wrong-shaped value is reported and never rewritten, because only the person who wrote it knows what they meant.
Every Item Opens With an Executive Summary
The first section of the body, before anything else, is ## Executive Summary — six labelled lines, nothing more:
## Executive Summary
- **What it is** — the change itself, in one sentence, in words a non-engineer would use.
- **Who benefits** — the person who feels the pain today, named by role.
- **Customer value** — what they can do afterwards that they cannot do now.
- **Revenue** — Direct, Indirect, or None, then one sentence saying why.
- **Cost and risk** — rough size (hours, days, weeks) and the main way it goes wrong.
- **Recommendation** — Do now, Do later, or Don't do, plus the reason in one line.
This exists so a triage decision can be made from the top of the file. An item whose value cannot be stated in six lines is an item nobody can decide about, and it will sit in inbox/ until somebody re-derives the case from scratch.
The rule binds every path that writes into inbox/, not just this skill's capture flow. That includes an item written by an instance mid-phase, one drafted from a conversation that never touched capture, and one you write by hand. It was already the rule here and it was already followed here; what went wrong is that the other paths were never told. On 2026-09-23, 13 of the 18 items in one project's inbox had no summary — every one of them written by an instance, none of them triageable without re-deriving the case first. Not yet decided. is a valid value for any label; an absent section is not a valid item. aidlc doctor reports inbox/ items missing one, by name and by missing label; it never writes one, because an invented case reads exactly like a real one.
Four rules keep it honest:
Noneis a real answer for revenue, and often the correct one. Lifecycle plumbing earns nothing directly. WritingIndirectfor everything makes the field useless, which is worse than leaving it blank because it reads as answered. This is also the cheapest available check against the self-repair trap: a meta-feature that has to writeRevenue — NoneandCustomer value — none, it observes the lifecyclehas declared itself.- Write it last, from the rest of the body. A summary drafted before the detail is a guess wearing a summary's clothes.
Not yet decided.for any label you cannot fill. An admitted gap beats an invented figure; the next reader can tell the difference.- Six lines, no more. Longer reasoning belongs in the sections below it.
Older items may predate this section. Add it when you next touch such an item — derived from what the item already says, never from fresh speculation — rather than sweeping the whole directory.
Capture
Resolve the roadmap root. Always write to the primary checkout's
.aidlc/roadmap/, never a worktree's copy: resolve viagit rev-parse --git-common-dir(the directory containing that path is the primary checkout). Outside a git repo, use the current project root. Createinbox/if missing.Name the file.
YYYYMMDD-<slug>.md— today's date plus a short (2–4 word) slug.Handle collisions. If the filename already exists in any status directory, ask one question — extend the existing item or new item? On extend, append a dated
## Update (YYYY-MM-DD)section to it where it is; on new, suffix the slug (-2). Never silently overwrite.Merge first. Before writing a new file, check whether an open item — anything in
inbox/,backlog/orin-progress/— already owns the area this is about. If one does, append a dated## Received from <source>section to that item and stop; say which item it went to. This is about subject, where step 3 is about filename: four of those eighteen items were findings about areas an existing item already owned, one of them literally asking somebody to triage another item. Merging them took minutes and removed four files. A new file is the right answer when no open item owns the area — which is most of the time, and the check costs one read of a list you are already looking at.Ask the decisions the build will need. Name at most three choices requirements or design would otherwise stop on — a scope edge, a user-visible name or flag, a default — each with a recommendation, and ask them in one message, together with step 7's one question when it asks one. Any or all may be answered "skip". No follow-up round. A choice the braindump already settles is not asked. When nobody is there to answer — an item an instance writes, an unattended run — ask nothing.
This is the moment the person still has the context. Requirements and design read the item's
## Decisionssection before they ask, so a choice answered here is not asked again hours later, when nobody remembers why.Write the file into
inbox/, withsource: human,promoted_to: null, and onehistoryentry dated today. Recorddepends_ononly if the braindump itself names other items; never ask for dependencies. Write each answered decision as one line under## Decisions, after the item's other sections:- <decision> — <answer>. A skipped decision is not written; with none answered, there is no## Decisionssection. Beyond step 5's decisions, do not interrogate the user for structure — capture friction kills inboxes; one braindump in, one file out.Draft the executive summary from the braindump, do not extract it by interview. You write those six lines; the user does not. Ask at most one question, in step 5's message, and only when the braindump says nothing about who wants this or why — a good one is "would this make money, save money, or is it just something that needs doing?". Anything still unknown is
Not yet decided., which is the point of having the label.Confirm and return. Report the file path and the
Recommendationline, then return to whatever was happening. Showing the recommendation lets the user correct a wrong read immediately, while the context is still in front of them.
Hard rule: capture never reads or writes anything under .aidlc/state/ — no claims, no phase files, no transitions log. Two sessions capturing simultaneously just produce two files; no locking exists or is needed. Do not auto-commit; staging is the user's.
If the user includes credentials, tokens, or secrets in a braindump, flag it and omit them from the file.
Browse
List each status directory, reading only frontmatter and the containing directory — never item bodies. Show each item's date, title, status, and dependencies. Group by status in lifecycle order and give a count per status. Flag inbox/ items first: they are waiting on a decision only a human can make. Then flag backlog/ items whose dependencies are all done — those are the natural next candidates. Offer to open, triage, promote, or drop.
For a hold/ item, show whether it is parked or declined, with its reason, from the last history entry — that entry is in the frontmatter, so this costs nothing extra. An item with no reason recorded is reported as hold — reason not recorded, never guessed at: parked and declined are opposite answers, and a wrong one either revives work somebody killed or buries work somebody agreed to.
aidlc roadmap next is the deterministic answer to "what next?": the backlog/ item with the lowest priority among those whose depends_on are all done, unranked items last, ties by file name (--explain shows the order and what was skipped and why; --json for scripts). It is read-only and never promotes — promoting stays a person's decision. priority is an ordinary extension field a project declares in roadmap.item_fields; a rank, where 1 is first. Where it is declared, --explain also lists every unranked backlog item, so one sitting last is visible rather than silently last. --autopilot picks as autopilot does: it leaves out items marked run: attended, and items whose run is neither value, listing each as skipped. aidlc roadmap rank <item> <n> is the one way to change a rank: it puts the item at n and renumbers every ranked backlog item 1…k, so ranks stay unique and gap-free.
Seeing the roadmap. When someone asks to see the roadmap or its dependencies, run aidlc roadmap view rather than drawing a graph. It writes one self-contained HTML page — a kanban board of every item by status, and a zoomable map of the depends_on chains with blocked items and unknown ids marked — and opens it in the browser (--no-open only writes it; --out <path> writes it elsewhere). The page lives in the git common dir, outside every working tree, so it is never committed and nothing rebuilds it on a move; run the command again for a fresh one. aidlc roadmap graph is the same command.
The frontmatter-only rule holds for browsing, which is a list and must stay cheap enough to run over the whole roadmap. Triage below is the one flow allowed into the body, and only as far as the executive summary.
A filename appearing in two status directories is an error: report it rather than guessing which one is current.
Triage
Moving an inbox/ item to backlog/ means "we agree to do this". Moving it to hold/ means "not now", and carries a reason prefixed parked — or declined — . Move the file and append the matching history entry. Triage is the one flow that exists purely because more than one person can submit work — do not skip it by promoting straight from inbox/ unless the user says so.
Work you agree with but cannot start — gated on a trigger, a decision, or something that does not exist yet — goes to hold/ as parked, not to backlog/. Parking it with its trigger written down is the whole difference between a backlog you can read as "ready" and one where two of eleven entries are waiting on something.
Triage is the one flow that reads into the body, and it reads the executive summary and nothing else. Six labelled lines per item is what makes triaging a full inbox in one sitting possible; opening whole items is what makes triage something nobody ever gets round to. Show the summary, say which way you would go and why, and let the human decide. If an item has no executive summary, write one from what the item says before asking for a decision — a decision made without one is a decision made twice.
Read .aidlc/context/constitution.md when it exists and apply the constitution rule in .aidlc/resources/rules/constitution.md: an item that crosses a prohibition is the clearest hold/ there is, and naming the prohibition turns "not for us" into a reason the submitter can read.
Ranking on the way in
When the project declares priority in roadmap.item_fields, moving an item into backlog/ includes giving it a rank — otherwise it sorts last, and the order aidlc roadmap next and autopilot follow becomes the order nobody chose.
- Read the current order with
aidlc roadmap next --explain. - Propose a rank with a one-line reason drawn from the executive summaries — this item against its neighbours, not a score.
- Show where it lands:
aidlc roadmap rank <item> <n> --dry-runprints the whole order with the item marked. - Ask once: accept rank
n, or give another number. One answer settles it. - Run
aidlc roadmap rank <item> <answer>without--dry-run. Never editpriority:by hand — the command is what keeps ranks unique and gap-free.
The proposed rank is never written without the person's answer: a rank nobody looked at reads exactly like a decided one. There is no scoring formula; the person decides and the flow only proposes. Projects that do not declare priority triage exactly as before, with no rank asked for.
Run mode on the way in
When the project declares run in roadmap.item_fields, the same moment also settles whether autopilot may build the item alone: run: autopilot or run: attended. Autopilot picks with aidlc roadmap next --autopilot, which leaves attended items for a person; a person's aidlc roadmap next still offers them.
- Propose
autopilotorattendedwith a one-line reason, alongside the proposed rank.attendedfits an item that changes what a gate trusts, or needs a decision before any code;autopilotfits hours of work with a clear reproduction and a clear fix. - Ask it in the same question as the rank — one answer gives both, for example "rank 3, attended". Never a second question.
- Write the answer to the item's
run:line as text, adding the line afterpriority:when it is missing. Edit only that line; nothing else in the frontmatter changes.
Never write run: without the person's answer, for the reason the rank is never written without one. An item with no run: is one autopilot may build, so leaving it unset is a choice, not a neutral default. Projects that do not declare run triage exactly as before.
Milestones
A milestone is a goal a set of items serves — "Stage 1", "first-customer readiness". A roadmap has them when .aidlc/roadmap/milestones.yaml exists: an ordered milestones: list, each entry with an id (lowercase letters, digits and hyphens), a title and a one-line exit test. Without that file nothing in this section applies, and the roadmap reads and ranks exactly as it would otherwise.
milestones:
- id: stage-1
title: Stage 1
exit: a customer runs one region end to end
- Every new item names one. When the file exists, set
milestone:to a milestone id ornonein every item you write — a captured idea, a follow-up from a retrospective, an item an instance writes.nonemeans Unassigned on purpose. A missing field, or an id the file does not list, is whataidlc roadmap statusandaidlc doctorreport. - Status is derived, never written. A milestone with no items is not started; one whose items are all done or declined is done, and any parked item keeps it open; the first one in order that is not done is current, even when it has no items yet.
- What next prefers it.
aidlc roadmap next,--autopilotand autopilot put the current milestone's ready items first, then fall back to the usual order across the rest.--explainnames the current milestone. - "What is still missing?" Run
aidlc roadmap statusand quote it: each milestone and its status, then per milestone what is done, in flight, next, parked, open and dropped, then Unassigned.--milestone <id>prints one;--milestone noneprints Unassigned.aidlc roadmap viewshows the same as its first tab. - Parking. A parked item keeps its
milestone:— that is the goal it waits for. Write the trigger in the hold reason, not the goal.
Assigning milestones to an existing roadmap.
- Run
aidlc roadmap assign-milestones. It prints the milestones with their exit tests and every open item that is Unassigned, and writes nothing. - For each item, draft one milestone from its executive summary and the exit tests: the earliest milestone whose exit test the item is needed for. When no milestone fits, use
noneand say why in your reply. - Write them with
aidlc roadmap assign-milestones --set <item>=<id> --set <item>=<id> …. It checks every pair before writing any, and edits only each item'smilestone:line. - Do not commit. Tell the person to review
git diff, change what they disagree with, and commit it themselves. An item left out stays Unassigned and keeps being reported.
Promote
Promotion is where the roadmap meets the lifecycle:
{{glossary:scope}}
- Promote from
backlog/normally; frominbox/only when the user is explicitly skipping triage. - If the item's
depends_onnames items that are not yetdone, say so and confirm before proceeding — promoting out of order is allowed but should be deliberate. - Choose a template and scope as usual (defaults from
.aidlc/config.yaml; see aidlc-getting-started). - Create the instance per Creating an Instance (
.aidlc/resources/rules/creating-an-instance.md) — including firingon-instance-startlifecycle actions. - Seed the instance with the item: Full scope → the item is raw material the Ideation phase refines into that phase's required artifact (
idea.mdfor the feature templates,research-question.mdforspike); Standard scope → the item is input context for Requirements. - Set
promoted_to: <instance-name>, move the file toin-progress/, and append thehistoryentry.
Promote into a new worktree
When the user asks for a worktree ("create a new worktree for roadmap <file>"), they want to work on the item in a separate session — for example one per pane of a session manager. Run the promote steps above with these changes, and do not stop to ask anything the item and config already answer:
Find the item by file name,
id, or title, inbacklog/orinbox/. Asking for a worktree counts as choosing to skip triage. When more than one item matches, list them and ask which one.Instance name: the item's file name without its date prefix and extension (the item filed as
20260929-say-what-to-do-nextbecomessay-what-to-do-next). Template and scope come from the config defaults, unless the item or the user names them.Create the instance with
aidlc start <template> --name <name> --scope <scope> --worktree --non-interactive --roadmap-item <item-file>. Take the worktree path from theWorktree:line it prints. That one command creates the branch and the worktree, fireson-instance-start, sets the item'spromoted_to, moves it toin-progress/in the primary checkout, and copies it into the instance asroadmap-item.md— which is the seeding step 5 above asks for. Do none of it by hand: a hand-made move is the step that got skipped. The move is left uncommitted in the primary checkout; committing it is yours.Hand off. Do not start the phase in this session — unless the user's own message asks you to continue here, in which case
cdinto the worktree and follow aidlc-continue there. Otherwise the work belongs to the new session.aidlc startprintedHerdr: opened workspace …— it ran inside herdr, opened the worktree as a herdr workspace and already started the agent there on/aidlc-continue <name>. The hand-off is done: say which workspace it opened and stop. Do not print a command to paste.aidlc startprintedHerdr: could not open a workspace (…) — run: …— give the user that command as printed, with the reason.- No
Herdr:line — end with the exact command that opens the new session:
For Codex, swapcd <worktree> && claude "/aidlc-continue <name>"claudeforcodex. If the user names a session manager, give them the worktree path to point it at.
A user who does not want herdr to open anything passes
--no-opentoaidlc start.
When the instance completes, the roadmap-done lifecycle action moves every item whose promoted_to names it to done/ on its own, and says how many it moved — do not do it by hand, and do not remove that action to do it manually.
Drop
Move the item to hold/ and append the history entry, with a reason prefixed declined — . Dropping is an explicit act — never delete or overwrite an item to get rid of it, and never leave the reason off: hold/ holds parked work too, and an unexplained entry there is a decision nobody can tell apart from the opposite one.
Projecting to GitHub Issues
Optional, opt-in, and inert until a roadmap.sync section exists in .aidlc/config.yaml. When a user asks how the roadmap can be visible to people who will not clone the repo, or how it can appear where their team already triages, the answer is aidlc roadmap sync — see docs/roadmap.md.
Five things to know before you offer it:
- It is a projection, not sync. Disk stays the source of truth; nothing is written back to disk, so no item gains a
remote:field and no flow above changes. The link lives on the issue as a marker carrying the itemid. inbox/andhold/are not projected by default, because an untriaged idea and a parked one are not decisions anybody has published. Do not widen that list on a user's behalf.- Every write needs consent. Suggest
aidlc roadmap sync --dry-runfirst, always. It prints the plan, names the target repository, and touches nothing. - Milestones follow the file. With a
milestones.yaml, sync creates, retitles, closes and reopens one GitHub milestone per entry and sets each issue's milestone from its item'smilestone:line. Change a milestone in the file, never on GitHub — a GitHub-side edit is overwritten on the next sync. - The issue body carries the whole item — its frontmatter facts, its prose, and links to its file and to the items it names — inside a region marked
<!-- aidlc-body:begin -->…<!-- aidlc-body:end -->, regenerated on every sync. Text outside that region is preserved, so a note added to an issue description survives. When somebody asks why a projected issue is out of date, the answer is that a sync has not run since the item changed, not that the body is frozen.
Editing an item's body is therefore a publishing act on a repo with roadmap.sync configured. That is not a reason to hesitate, but it is a reason not to paste anything into an item you would not put on the tracker. Item prose is sanitized on the way out — HTML comments removed, @name and #123 made inert — so no sync notifies anybody; secrets are not sanitized, and never were. If a user's braindump contains credentials, the existing hard rule applies: flag it and leave them out of the file.
When the user's Project board does not move, that is expected: the projection stops at issues and labels, because moving a board needs a project: write token that GITHUB_TOKEN cannot have. Offer aidlc roadmap board-workflow > .github/workflows/aidlc-project-status.yml, which prints a GitHub Actions workflow that sets each issue's Status column from its status label; its header lists the three setup steps (Status columns, the AIDLC_PROJECT_TOKEN secret, the AIDLC_PROJECT_NUMBER variable). Writing that file is the user's decision — show the command, do not run the redirect unasked. Do not offer to build board writes into sync instead.
aidlc roadmap import <issue-url> is the opposite direction, and it is one-shot: it writes one inbox/ item with the issue linked and its executive summary left as Not yet decided., which is then a normal triage. Nothing reconciles afterwards.