3. Checkout
The client-facing heading. Its effort is the sum of everything beneath it rather than a figure anybody typed, and it carries no month at all, which is why an epic can never quietly disagree with its own contents.
Those are the first three things a client asks, and a correct stack of work items still leaves all three open. The roadmap builder takes the reviewed package and answers them: epics and stories over tasks, effort summed from the individual estimates, and every task placed in a month against a real capacity figure.
The requirements pass upstream of this produces something genuinely good: every requirement typed and traced to the line it came from, split into items small enough to hand to one person, each sized individually, each labelled must, should, could or won’t, and the whole package reviewed by two agents on different model families.
What that produces is a correct, reviewed, well-sized set. A plan is a sequence, and a sequence is what gets signed: months attached and a total at the bottom. Getting from one to the other takes more than formatting: it needs a rule for rolling effort up, a rule for ordering work that depends on other work, and an answer for the items nobody managed to size.
The last of those is where most estimating tools quietly fail. Summing a column is trivial. Deciding what to do about the row that has no number in it is the part that determines whether the total at the bottom is honest.
One document, validated against delivery-roadmap.schema.json, with a spreadsheet rendered from the same object for the people who will only ever open a spreadsheet.
The client-facing heading. Its effort is the sum of everything beneath it rather than a figure anybody typed, and it carries no month at all, which is why an epic can never quietly disagree with its own contents.
A coherent slice a client can accept or defer as one decision. It carries a scope label of its own, and the month range shown against it is the span its tasks occupy: 2026-09 ~ 2026-10.
One unit of work, small enough for one person or one agent. Tasks are the only tier with an estimate of their own and the only tier that gets scheduled. Everything above them is arithmetic.
There is no model in this stage. Rollups, ordering and month assignment are arithmetic over the package, so the output is generated rather than composed, and the golden file it produces from the fixture is byte-compared in CI. That matters for a duller reason than it sounds: it makes the roadmap diffable. Re-run it after a scope conversation and the change set is the scope conversation. A tool that returns a freshly worded opinion each run cannot be reviewed, only re-read.
What follows is the roadmap.json the builder emits from its bundled fixture package, rendered field by field. It is transcribed from the golden file rather than written to look plausible, so if the generator changes shape this table stops matching the product.
build-roadmap@v1ctxpkg-fixture-enrichedv1| ID | Item | Scope | Month | Effort |
|---|---|---|---|---|
| E1 | epic3. Checkout | no scope classification | not scheduled directly | 13.0 hours |
| E1-S1 | story3.1 Card payments | must | 2026-09 ~ 2026-10 | 7.5 hours |
| E1-S1-T1 | taskTokenise card details at the edge | must | 2026-09 | 4.0 hours |
| E1-S1-T2 | taskRetry tokenisation on provider timeout | must | 2026-10 | 1.5 hours |
| E1-S1-T3 | taskValidate CVV before authorisationafter E1-S1-T1 | must | 2026-10 | 2.0 hours |
| E1-S2 | story3.3 Refunds | should | 2026-11 | 5.5 hours |
| E1-S2-T1 | taskIssue partial refunds | should | 2026-11 | 5.0 hours |
| E1-S2-T2 | taskRecord an audit row per refundafter E1-S2-T1 | could | 2026-11 | 0.5 hours |
| E2 | epic5. Infrastructure | no scope classification | not scheduled directly | at least 5.0 hours, a partial total |
| E2-S1 | storyHarden the payment event queue | must | 2026-09 ~ 2026-10 | 5.0 hours |
| E2-S1-T1 | taskMake event consumption idempotent | must | 2026-09 | 3.0 hours |
| E2-S1-T2 | taskAlert when the queue backs upafter E2-S1-T1 | should | 2026-10 | 2.0 hours |
| E2-S2 | story5.2 Dead letters | no scope classification | 2026-09 | effort unknown, this work was never sized |
| E2-S2-T1 | taskRetain undeliverable events | no scope classification | 2026-09 | effort unknown, this work was never sized |
3. Checkout
3.1 Card payments
Tokenise card details at the edge
Retry tokenisation on provider timeout
Validate CVV before authorisation
after E1-S1-T1
3.3 Refunds
Issue partial refunds
Record an audit row per refund
after E1-S2-T1
5. Infrastructure
Harden the payment event queue
Make event consumption idempotent
Alert when the queue backs up
after E2-S1-T1
5.2 Dead letters
Retain undeliverable events
Total effort, a floor
18.0+ h
Floor, not an estimate. 1 of 8 tasks was never sized, so the real figure is this or higher. The unsized work is reported by identifier rather than counted as zero.
Hours by month
Hours by scope
A bucket holding unsized work is reported as a floor, or as unknown where that is all it holds.
E2-S2-T1 was never sized. The work-breakdown agent had not run on this package, so there is no estimated_minutes and no bite_size to fall back on. The generator reports the effort unknown, names the task in the report, and marks every total above it a floor: the epic reads 5.0+ h and the grand total reads 18.0+ h.
Rendering that cell as 0.0 h would have been one line of code and it would have made the plan look complete. It would also have quietly promised the work for free, because a zero in an effort column is not read as “unknown”, it is read as “nothing”. Every downstream total would have inherited the lie, and the first person to notice would have been whoever had to build the thing.
This is the part worth buying. Any tool can total a column. A plan that tells you precisely where it does not know is worth more than a plan that looks finished, and it is the only kind you can safely sign.
Warnings
1Unestimated tasks
1E2-S2-T1Dropped dependencies
1act-queue-001b -> act-queue-001 (target not in roadmap)The dropped edge is worth reading too. That dependency pointed at a parent work item rather than at a task, and parents are not schedulable, so the edge could not be honoured. It was dropped and recorded. A tool that tells you what it discarded is easier to trust than one that silently does the same thing.
Effort is never invented at this stage. For every task the generator asks the same two questions in the same order, and if both come back empty it says so.
execution_brief.estimated_minutes5 of 8 tasks hereAn explicit estimate written against the work item, converted to hours. This is the only figure anybody actually intended as an estimate, so it wins outright wherever it exists.
bite_size2 of 8 tasks hereThe size band the work-breakdown agent assigned, mapped to a fixed number of hours: xs to 0.5, s to 2, m to 4. Coarse, but it is a deliberate statement about size rather than a by-product of something else.
unknown1 of 8 tasks hereNeither field present, so the effort is reported unknown and the task is named in the report. Nothing is guessed, interpolated from siblings, or averaged, because a guess here is indistinguishable from an estimate once it reaches a spreadsheet.
effort_hint is ignored on purposeThere is a third field sitting right there, and using it would take the unknown count in this sample to zero. The roadmap builder will not touch it, because the assembler derives effort_hint from the character count of the source text. A requirement somebody wrote three paragraphs about reads as a large task. A one-line requirement with a fortnight of work behind it reads as a small one. It measures how much was written, not how much there is to do.
Reading it would improve exactly one number, the proportion of tasks with an estimate, and degrade the only one that matters. Leaving the effort unknown is less flattering and more useful.
Every depends_on edge between tasks becomes an edge in a graph, and the graph is topologically sorted, which orders every task after the work it depends on. Each one therefore lands in the same month as the work it needs, or a later one. In this sample, validating the CVV depends on tokenising the card, so tokenisation takes September and validation waits for October.
Note what a dependency does not do. Both refund tasks land in 2026-11 even though one depends on the other, because an edge forbids running earlier, it does not demand a fresh month. The pair costs 5.5 h together and fits inside one.
Tasks are then packed into months in that order against a capacity figure. A cycle cannot be sorted, so where one exists it is broken at a deterministic point and reported rather than resolved quietly. Same input, same break, same file.
This run, at 8 h per month
September stops an hour short of the cap, and that hour is not slack anybody missed. The cheapest unblocked task left costs 1.5 hours, and the one 0.5-hour task in the plan sits behind a 5-hour predecessor that does not fit either. Nothing legal fits, so October opens.
The reason to generate a roadmap rather than draw one is that a generated plan keeps its wiring. Every row can still answer where it came from, and re-running the pass produces a diff instead of a fresh opinion, which is the difference between a document you maintain and a document you replace.
ctxpkg-fixture-enrichedact-card-001E1-S1-T12026-09Each task carries the identifiers of the action items it came from, and each of those carries a link to the source it was extracted from. Every row keeps that chain, so nobody has to defend a line of the plan from memory in a steering meeting.
The same inputs produce a byte-identical file, and the sample on this page is compared byte for byte in our own CI. So after the next meeting you get a delta you can read line by line, rather than a new plan whose differences you have to go looking for.
Where a dependency could not be honoured the run says which one and why. This sample carries one such entry: a dependency pointing at a parent work item rather than a task, which is not schedulable.
act-queue-001b -> act-queue-001 (target not in roadmap)A generated plan is easy to oversell. These are the limits, stated here rather than discovered later.
The output is effort in hours and a month per task. Turning that into a figure on a contract involves rates, risk appetite, and margin, none of which live in this document. The roadmap is an input to a price rather than a price.
See how a proposal is builtCapacity is a flat number of hours per month. It knows nothing about leave, onboarding, the integration that always goes wrong, or which of these tasks the team has done before. A delivery manager still owns the plan.
There is no model in the loop inventing a number for unsized work. If the breakdown never reached a task, the roadmap reports it unknown and marks the totals a floor. That is the limit working correctly.
The order respects the depends_on graph it was handed. If nobody recorded that the migration has to precede the cutover, the plan will schedule them in whichever order capacity suggests. What was left unsaid stays unknown to it.
The table on this page is a real roadmap.json, generated by build-roadmap@v1 from package ctxpkg-fixture-enriched. The demo runs the same path end to end: meeting notes in, requirements assembled and reviewed, then this roadmap derived from them, streaming each stage as it lands. You can download the workbook at the end.
Bring us a problem, a half-formed idea, or a system that has outlived its stack. We will tell you honestly whether we are the right team for it.