control roomdocs/PLANNING.md

How we decide what to do next

How the next unit gets chosen

How we decide what to do next

Written on 2026-09-17, during the conversation that produced it, because the owner asked for the planning process to be recorded rather than re-derived. It describes a mechanism that is half built, and it names what is missing as plainly as what exists - the gaps are the useful half.

The problem, measured

Every figure below is from docs/state.json, which names its population. The first version of this document used hand-counted numbers over a pool that included work already built, and two of them were wrong - see the correction at the end.

tickets504
unbuilt and dispatchable242
blocked_by edges62
the unblocked frontier229
connected components29, largest 11
dispatchable at derived P016

Two failures, and they compound.

Nothing says what waits on what. 62 edges across 242 dispatchable tickets leaves a frontier of 229. So anything asking "what is next" reads 229 tickets, and prioritisation is a whole-backlog operation every time. The original figure here was 397, from a larger population - the conclusion is unchanged and its size is 42% smaller.

Nothing revises a score after it is written. value and urgency are self-reported once, at write time, and never revisited - so a priority is a claim about a ticket's author as much as about the ticket. This paragraph originally said the scale had saturated, on the strength of 67 P0s; that counted the declared field over a pool including built work, and over the 242 dispatchable it is 16. The saturation claim is withdrawn and the never-revised claim is what stands.

Why decomposing the problem does not fix it

The obvious answer is hierarchy: group the tickets, prioritise inside each group, then prioritise between the winners. The owner's objection to his own suggestion is correct and worth keeping:

I would argue that you could find cases where doing this won't be the optimal or even nearly the optimal way. You had to have to understand everything, to understand that there's one specific thing that is best done first.

That is not a tuning problem. A globally critical item that ranks second inside a strong group never meets the winner of a weak one, so decomposition structurally suppresses it, and no amount of better scoring recovers it.

This repository has the live instance. Deploying the one product a customer can touch was, on 2026-09-16, not a ticket at all - and had it been one it would have competed with every other P0 in the products lane. Any planner promoting each lane's best item would never have surfaced it. The count first written here was eleven, over the wrong population - the argument does not depend on the number, which is why it survived the correction and the saturation claim did not.

Two things help, and neither is an algorithm.

Group by outcome, not by area. The lanes - components, ops, docs, core, products, cli - are skill groupings. "The best components ticket" versus "the best docs ticket" is a comparison with no shared unit, which is exactly where the suppression happens. Epics shaped as outcomes - gets us a paying customer, stops a defect reaching a user, makes the factory faster - make the within-group comparison meaningful and turn the across-group question into a strategy call taken occasionally rather than a ranking re-solved constantly.

Prefer a signal that survives grouping. A ticket that unblocks eight others is important however you partition the backlog, because the fact is about the graph and not about the group. Centrality is the only prioritisation signal here that cannot be self-reported - it is counted rather than claimed, so optimism cannot inflate it. It is also currently unavailable: the largest connected component is 11 tickets, so maximum out-degree is about three.

That is the third independent argument for the dependency graph, and the strongest. The graph is not backlog hygiene. It is the only route to prioritisation that does not require somebody to hold 229 tickets in their head.

The three moves, in dependency order

1. Derive candidate edges. plan_batch.inferred_files returns every repository path a ticket's body claims - a function that crashed on its first ever invocation until 2026-09-16, so nobody had used it. Two tickets claiming the same path are a candidate edge.

The trap: naive co-occurrence produces thousands of false candidates, because CLAUDE.md and docs/LESSONS.md are named by dozens of tickets each. A shared rare path is evidence; a shared common one is noise. So candidates are weighted by path rarity.

And the hard constraint: nothing writes an edge automatically. A wrong edge is worse than a missing one - it makes a ready ticket look blocked and no gate would catch it. The output is a ranked list with its evidence, and judgement stays with a person. The number that decides whether the idea works is how far the frontier falls from 229.

2. Feed centrality into the score. Once edges exist, a ticket's out-degree becomes a scoring input that nobody can inflate. This depends on move 1 and does not start before it.

3. Outcome epics. Fifteen to twenty-five of them, replacing the lane as the grouping that prioritisation uses. This is judgement rather than derivation: proposed by the coordinator, ruled on by the owner.

What is missing from this plan

Named on 2026-09-17 in answer to a direct question, and ordered by how much each one undermines the rest.

~~There is no stated finish line.~~ Closed 2026-09-17 - the owner stated it and it is in docs/PURPOSE.md under "The finish line": ninety percent of a SaaS's infrastructure from one click including the migrations between scale stages, nine percent more from one product-level interview, and a standard enforced by design rather than by discipline. This paragraph stays struck rather than deleted only because the outcome epics below now have their input and somebody should see where it came from.

Nothing measures the cost of a product end to end. The stated purpose is to ship SaaS products serially, so the number that matters is tokens and days to ship product N. tokens per unit measures a step, not an outcome. Only one product exists and it predates measurement, so the figure is unknown.

Every move above is about the factory's queue, and the product is not in the loop. Edges, centrality and epics all make the backlog easier to plan. None of them makes the one live product better, and it is the only thing with a user. A plan whose every item improves the planning is a plan with a smell.

Nothing retires a ticket. 242 dispatchable, some certainly stale - written about code that has since changed. status: withdrawn exists and nothing proposes withdrawals, so the queue only grows and prioritisation gets harder every week by construction.

Nothing re-scores when the world changes. Every value and urgency was set once, at write time, and never revisited. When GitHub Actions stopped starting jobs on 2026-09-14 the correct response was to promote everything that depended on CI; nothing did, because nothing can.

The dashboard has no write path. The owner will read it and want to act - approve, reprioritise, answer an open question. Read-only means he returns to a chat session to do anything, which removes about half the point of having it.

Only move 1 has a success criterion. The frontier falling from 229 is measurable. What a successful epic set or a successful dashboard looks like is not stated, and unstated criteria are how a thing gets declared finished by whoever is tired.

How this document should be used

It is a record of reasoning, not a rule with an enforcer, and it will go stale the way docs/METHOD.md warns about. The parts worth keeping are the argument for centrality and the list of gaps. When a gap closes, delete its paragraph - a plan that still lists a solved problem is worse than no plan, because it is trusted.

Correction, 2026-09-17: which arguments survived the real numbers

OPS-143 built docs/state.json and found that "live" meant three different things here, none of them stated. Recorded rather than quietly edited, because the pattern is more useful than the fix:

argumentstatus
decomposition suppresses a globally critical itemholds. Structural, and independent of any count
the frontier is too large to readholds, smaller. 229 rather than 397
centrality is unavailableholds fully. 62 edges, largest component 11, max out-degree about three
priority has saturatedwithdrawn. 16 of 242 is an ordinary rate

Three of four stand. The one that fell is the one I reached for hardest, and it fell because I counted a convenient population rather than asking which population the question was about. Every count in this document now comes from state.json, which names its own.