| name | pm |
| description | Rank every live ticket into ../docs/PLAN.md and keep the shipped, canceled and on-hold folders true. Wrong today first, then dependencies, then cost; only the owner moves work on or off hold. Use when the user says "what should I build next", "rank the tickets", "make a plan", "priorities", or hands over the plan folder to be brought up to date. |
| argument-hint | [optional: a subject to rank within] |
| user-invocable | true |
Build the running order
../docs/PLAN.md is read to answer one question: what do I pick up now. Every live ticket, ranked, one line of why each row sits where it does. Rewrite it in place — git holds every older ranking, and the last line of the file stamps when this one was ranked, to the minute.
../docs/done/PLAN.md holds the retired rows. Moving one there is /done's job, not this skill's, so a row leaves the live file the day its last box is ticked and the list gets shorter as work ships.
../docs/canceled/PLAN.md holds the refused ones — the third of the three, and the one nothing else writes. Canceling is the owner's call and no skill's, so there is no /done for it: a plan is moved into ../docs/canceled/ and this pass is what gives it a row, by walking that folder rather than by being told (step 9).
../docs/on-hold/PLAN.md holds work the owner has paused. It is outside the running order without being refused: the ticket keeps its stage and track, and its row records the live folder it returns to. Only the owner's word moves a ticket in or out. This pass performs that move, fixes every link to the ticket and rebuilds both lists.
Never run git. Never edit a ticket's phases. A ticket that is wrong is /design's work; here it gets a tier 0 row and a corrected status. Outside the four rankings this pass edits a ticket only to add or remove its on-hold note on the owner's word; it also edits the ticket's README row, a glossary row, and a track's step order where this pass proves it wrong.
Ordered pass
1. Read every live ticket and index row
Open the README, the four plan files, the tracks, the glossary and every live and held ticket before ranking.
2. Re-derive every status
Read dated lines and boxes from each ticket and rebuild the status cells.
3. Rank wrong work, dependencies and cost
Apply the three tests in order and file anything that cannot honestly be ranked.
4. Put every row in its tier and sub-band
Use the tier definitions and the phase count; held work is not a tier.
5. Rewrite the live table
Write one numbered row per live ticket with its blocks, track and one-sentence reason.
6. Keep the glossary and tracks true
Add missing planning words and correct a proved-wrong subject order.
7. Recheck the release kind
Hold every ticket folder to the version class the release skill will read.
8. Read the shipped plan
Confirm every retired row remains inside the tier table it left.
9. Rebuild the canceled and on-hold plans
Walk both folders and write one row for every ticket there.
10. Rebuild derived cells and check all six files
Bundle status and Devs with, stamp the ranking and run the plan checks.
11. Hand back
The whole reply is the owner's message, word for word.
1. Read first
../docs/README.md — every ticket, one line, and the source of every ticket path.
../docs/GLOSSARY.md — the words the ranking is written in.
../docs/TRACKS.md — every subject order, and the source of every Track cell this pass writes. Read it before ranking, not after: a row is placed knowing which subject it belongs to, and a ticket whose subject has no track yet needs one written here in this same pass.
- Every ticket under
../docs/features/, ../docs/refactor/ and ../docs/fixes/, off the disk. Those hold subject folders, so walk them rather than one level. A ticket the README missed still gets a row.
- Every file under
../docs/canceled/, off the disk too. That is the only way a refused plan is found: nothing announces one, and a ticket moved there with no row is a decision that exists nowhere anybody reads.
- Every file under
../docs/on-hold/, off the disk too. A held ticket stays out of the live ranking until the owner restores it, and the row here keeps the live folder it returns to.
../docs/done/PLAN.md, ../docs/canceled/PLAN.md and ../docs/on-hold/PLAN.md — rows already closed, refused or parked are rows not to re-rank.
2. Re-derive every status
The cell is computed, so this pass runs just bundle-plan-status and writes no status by hand. The ticket is the authority for how far it has itself got, and it says so in its own dated lines: Ready for a plan with none, Designed for one carrying /design's line, Dev for one carrying the Building since line /dev writes when it opens a phase or the one legacy Built work confirmed line, Released for one shipped and not yet retired. scripts/check-plan-stage.mjs refuses a cell that is not what those lines give, and names the row. When the README disagrees with the ticket, fix the README.
There is no higher of two reports to keep any more, and the window that rule existed for is closed. A build used to look exactly like an untouched plan for the minutes between starting and ticking its first box, so a pass deriving the cell from the file alone wrote Dev back down to Designed and the owner read a build under way as one nobody had started. The Building since line is written before anything else is touched, so that window has a dated line in it and the derivation needs nothing retained.
A stage is read off the ticket and never written ahead of it. Designed, Dev and Released each rest on the ticket's own dated Designed line, so a row claiming one without it is the running order telling the owner a build is happening off a plan nobody has read against the code. A ticket with a box ticked and no design line is Ready with a tier 0 row saying so, never Dev: the cell is a report, and reporting a stage to make the row look further along is the one lie the whole tree is written to prevent.
This pass and /done are the running order's only writers, and neither runs beside a build. It is one ranked list — two rewrites of it are not something any merge can settle — and it is the one file the Devs with column cannot promise a pair is safe over, since the column is what says a pair is safe. Two sessions build in this checkout at once; ranking is not building, and it waits for both of them.
Then check in the code, not in the file:
- Does the status match the app? A ticket claiming a feature that ships, or the reverse, poisons every row resting on it.
- Does it preserve the owner’s prompts and supplied images inline where they are evidence? A supplied screenshot is its visual record for that state, not a missing wireframe.
- Is what it waits on real? Read the ticket doing the waiting — a dependency taken from one ticket's own account of another is how a plan goes circular.
- Do its citations still land?
- Does every phase say how it is proved?
What reading cannot settle is a tier 0 row, not a guess.
3. Rank on three things, in this order
- Is something wrong today — a bug, a panic, a vault opening incorrectly, or a rule the work runs under saying something untrue. Incorrect outranks incomplete.
- Is it built twice if it goes second — a piece two or more rows want, and smaller than they are, is built once here or several times below. A root bigger than the rows under it saves nothing by going first: that is a subject's order, which is
TRACKS.md's, not a tier.
- What it costs — the cheaper of two rows that tie goes first.
The tests pick the tier in that order, then run again inside it, which is what makes a row arguable rather than a matter of taste.
Cost never moves a row between tiers. It orders rows inside one. Joining it to test 1 is what once left every expensive fault sitting in the middle of the features.
Not counted: absent is not wrong, so missing capability never reaches tier 1; a missing test is a risk, not the app being wrong; a dependency counts only where the waiting ticket names it, and a shipped ticket is not a dependency at all.
Unrankable, because the cost is unknown. The ticket stays Ready with the reason in its row: it changes the window and has no drawn What it looks like section, or its phases carry no test box. Check both over every ticket while walking the folders.
A ticket carrying two jobs is split before it is ranked. This is the only pass that reads every live ticket in one sitting, so it is the one that can see a file has become two — its summary sentence needs an and to stay true, its phases answer more than one question, or its cost is two costs, which is what makes it unrankable rather than merely large. Split it at the seam with /ticket: each half keeps a name of its own, a README row and a row here, and each names the other. Size alone is not the test — a big swing is a tier, not a split.
Build upkeep is held, not ranked, and this pass is what enforces it. A plan whose written footprint is entirely app/scripts/, app/.agents/, app/Justfile, AGENTS.md or the plan tree changes how the work gets done rather than what the app does, and the owner holds every one of them on a standing word — the ranking is for the app, and a list of the machinery's own faults crowds out the work somebody using Leaftext would notice. So a live one found at Ready or Designed is moved to ../docs/on-hold/ in this pass by the rule below, and one already at Dev or Released is left alone, because holding a build somebody is in the middle of loses it. The only thing filed live off that footprint is a bug in the app itself, where the machinery is merely how it was found.
Anything this pass turns up that is not a row gets a ticket — a gap nothing covers, a fault nobody has filed, a rule the tree is running under that is untrue. Write it with /ticket, give it its README row, rank it here in the same pass. Never a sentence in the hand-back: reading eighty tickets against the code is the pass most likely to find something, and a finding with no file is one nobody sees again.
Anything the work in front of this pass hints could be faster is a performance finding: file it as a ticket in the same turn, without stopping for a benchmark or fixing it in passing, and never name it in the reply. Reading every ticket together can expose repeated planning work, a broader read than the decision needs or a wait in the workflow; the marked finding is written first, then this pass ranks it by the rule below.
Every ticket carrying > **Performance finding.** belongs to the Performance track and tier 0. nothing-files-a-performance-finding is the one unmarked step in that track because it builds the filing route; keep the bootstrap row in the position the owner chose rather than lifting it into tier 0.
4. The tiers
| Tier | What is in it |
|---|
| 0 | Somebody reading the code to settle a claim the rest of the list rests on, or a ticket carrying > **Performance finding.**. Cost orders marked findings inside tier 0; they are never compared against features below it |
| 1 | Wrong today. Whatever the app, or a rule the work runs under, does incorrectly — at whatever it costs |
| 2 | The shared piece. Two or more rows wait on it and it is smaller than they are, so it is built once here or several times below |
| 3 | The features people would name, cheapest first — and the work behind them nobody would name: how the repo is built, and the published pages, where neither is wrong today, a shared piece, nor a big swing |
| 4 | Big swings, each absorbing the time all of tiers 1 to 3 take together. On its own size — a small row behind one is put here by the blocker rule below, not by this definition |
Tier 0 comes first, because the list is only as good as the statuses under it and performance findings accumulate before planned features. Readings keep the claim they must settle; marked performance findings are ordered cheapest first inside tier 0 and never against a lower-tier feature. Held work has no tier or position: move it to ../docs/on-hold/<subject>/, move its README row under ## On hold, keep its track step linked to the new path, and record its stage, return folder and the owner's reason in ../docs/on-hold/PLAN.md. On restoration, reverse those moves and rank it from the rules rather than from its old position. A tier with no rows is deleted, heading and all, and comes back when it has one. No estimate anywhere — no minutes, no hours, no days.
No row sits in a tier above its own blocker. It is the one rule that outranks the three tests: a shared piece behind a big swing goes with the swing, and a one-line row behind one stays behind it, because a row somebody cannot start is worse than a row somebody has to scroll to.
A long band is cut into sub-bands on cost, because the band is already ordered cheapest first and the reader's question is how big the run under a heading is. A row's cost is the number of ### Phase headings in its ticket — the slices it ships in — or, where the Ticket cell ranks a named run (**phases 1–4**, **phase 1**), the length of that run.
| Sub-band | Phases |
|---|
### One or two phases | 1–2 |
### Three or four phases | 3–4 |
### Five phases or more | 5 and up |
- A blocked row sits in its blocker's sub-band where that is the dearer of the two. The blocker rule, one level down: a one-phase row behind an eight-phase one is not something anybody picks up cheaply.
- A
### heading, never a numbered band. The number on a band says which of the three tests placed the row, and cost is the test that never moves a row between bands. Each sub-band gets its own table; positions run straight through, because a sub-band holds none of its own.
- A sub-band with no rows is not written, the way a tier with no rows is deleted heading and all.
- Inside a sub-band, cheapest first as everywhere else, except that a row never precedes what it waits on.
- A band over half the file, holding rows of more than one size, must be cut.
scripts/check-plan.mjs counts the phases itself: it refuses a band that should be cut and is not, a row left above the first heading, and a row under a heading its count does not name.
Inside tier 1, what stops somebody using the app comes before what looks wrong. A machine that cannot install it outranks a shadow drawn the wrong way however the two compare on cost.
Size is not a test. A tier holding most of the list is what a tree of mostly-features looks like, and no count makes a definition wrong. What makes one wrong is asking for two unrelated things at once, or asking for something no row can satisfy — three tiers emptied that way once, and the file went on calling itself ranked on three tests while sorting on one. Read the words of a definition, never the count under it.
5. The live file is a table
It opens with its title, # Leaftext Plan Log, and the first work table is the first thing under it. Counts and anything off the list go after every table.
# Leaftext Plan Log
## Tier 1 — wrong today
| # | Ticket | Status | Blocks | Blocked by | Track | Devs with | Why |
## Tier 3 — the features people would name, cheapest first
### One or two phases
| # | Ticket | Status | Blocks | Blocked by | Track | Devs with | Why |
### Three or four phases
| # | Ticket | Status | Blocks | Blocked by | Track | Devs with | Why |
- The number is the position, not the ticket's identity: moving a row renumbers everything under it, and the number is never copied anywhere else. The name is what every line of prose cites.
Why is the problem and what answers it — what the app does wrong, or cannot do yet, in the words of somebody using it, and where it helps, the thing that puts it right. It is the ticket's own ## Why in one sentence. Never why the row sits where it does: the heading above it has already said which of the three tests placed it, and a placement is worth no words at all. One sentence a cell, 200 characters at the outside; scripts/check-plan.mjs counts every one and refuses a longer one.
- Never a neighbor. No
behind the row above, no ahead of everything under it, no top of the band, no last of the tier, no position, and no argument for the tier the row landed in. A cell written about where a row sits is made untrue by the next reorder and nobody comes back to rewrite a hundred of them — which is how this column grew to two thirds of the file, 153 cells averaging 352 characters with the longest at 956. The check refuses those words.
- Never a date. When it was found, asked for or designed is the ticket's own record, and a second copy here goes stale the moment the ticket moves on. The check refuses one.
- Nothing that belongs to the ticket — no citation, no phase count, no box count, no cost breakdown, no account of what it will build, no restating what the README already says the ticket is.
- No preamble under a heading, no method, no record, no picture. How rows are ranked is this skill; what an earlier ranking got wrong is
done/PLAN.md's.
- Every ticket name is a link, in every cell and every line of prose, using the path from the README. A bare name is a ranking error.
Blocks is Blocked by read the other way — every live row whose Blocked by names this one, linked, or —. It carries no claim of its own: the waiting ticket's cell is the source, and this one exists so a row being weighed says what sits behind it without reading the whole file. scripts/check-plan.mjs holds the two columns to each other.
Blocked by holds live blockers only, linked, or —. A ticket that has shipped does not block anything, so naming one there reads as a wait that is over.
If a cell needs more words, the ticket is what needs them.
6. Every word this file spends has a glossary row
../docs/GLOSSARY.md is what makes the ranking readable by somebody who did not write it, so a planning word spent here and missing there gets a row in this pass. That covers the ones easiest to miss: each column heading whose meaning is not its own name, each status value, tier, row, position, track, step, and anything off the list. Why needs none: the heading is the definition.
A row is one or two sentences saying what the word means today, with a link to whatever owns it. No history, no dates, no counts of how many files carry it — that is a log, and it belongs in a ticket. A word nothing uses any more loses its row.
7. Which folder a ticket sits in is the version it ships under
features/ takes the middle number up and the last back to zero; refactor/ and fixes/ take the last number up one. /git-release reads the folder and does what it says, so a ticket filed wrong ships under the wrong number — and this is the only pass that walks all three folders. features/ is the app not doing something yet, refactor/ is doing it differently, fixes/ is doing it wrong today. Move it, fix its README row, and write the move into its row. No version number goes in PLAN.md.
8. The shipped file
../docs/done/PLAN.md is retired rows and nothing else: one table per tier, each row struck through with the date and time it closed and what the build found. That is the half a later reader cannot get anywhere else. It ends with what the retired rows add up to, and what earlier rankings got wrong. A row is cited by its ticket's name in both files.
The columns are its own, not the live file's. Tiers 1 to 3 carry Ticket, Status, what was wrong and what landed, and Cost; tier 0 carries Work, Status and Why first, and what it found. The live file's seven columns do not travel: a position is dropped on the way across, a track stays readable in TRACKS.md, and Blocks and Blocked by are questions about work that is left.
Every row sits inside one of those tables, under the heading for the tier it was retired from. Nothing goes above the file's title — a row there belongs to no tier and sits under no header row, so the file opens as a headerless table and nothing retired can be found by where it was ranked. /done is what places a row; scripts/check-plan.mjs refuses one left outside a table or short of its header's cells.
9. The canceled file
../docs/canceled/PLAN.md is every plan decided against, grouped the way that folder's own subject folders are, each row struck through with the date and time it was dropped and the reason. It exists because a refused answer that is only deleted comes back — somebody reads the same fault six months later, reaches the same idea, and rebuilds the thing that was already thrown out. So a row says what it was, what killed it, and what came out of it that is still worth having.
- Walk the folder, do not wait to be told. A file there with no row gets one in this pass. A row here whose file is gone loses its row.
- A canceled ticket is not a canceled fault. Where the thing it was aimed at is still wrong, the row names the live ticket that holds it, so nobody reads the cancellation as the fault being closed.
- The ticket keeps every word it had. It gains a note at the top saying who canceled it and when — a canceled plan is not trimmed, and this file never becomes the only copy of the reasoning.
- A date it never recorded is a
—, not a guess.
The on-hold file
../docs/on-hold/PLAN.md is every plan the owner paused, grouped by subject. Its row keeps the stage already reached, the live folder it returns to, when the owner paused it and why. A held ticket is neither live nor refused, so it has no position and no tier.
- Only the owner moves one. A ranking pass never parks work because it looks costly or restores it because it looks urgent. Build upkeep is the one standing instruction: the owner has already said every such plan is held, so this pass performs that move without asking again — and no pass ever restores one.
- Move the file and every link. The ticket goes to the matching subject folder under
on-hold/; its track step stays in place and points at the new path.
- Keep the return kind. The row records
features, refactor or fixes, so restoring it does not guess which release class it had.
- A date the earlier Hold band never recorded is
—. The migration date is not the date the owner made the decision.
10. The six files know each other
The tree is read from whichever file somebody opens first, so each one names the rest: the README says what every plan is, PLAN.md what is left, on-hold/PLAN.md what is paused, done/PLAN.md what shipped, canceled/PLAN.md what was refused, and TRACKS.md the subject orders cutting across them — with GLOSSARY.md holding the words all six are written in. Check the links each way in this pass.
11. Hand back
The whole reply is the owner's message, word for word. Which rows moved and why, what tier 0 turned up and what is at the top now are all written into the list itself, which is where the owner reads them. Nothing in the app moved; the tree stays dirty.
Reference
../docs/PLAN.md — the live list. Read it for how short a row is allowed to be.
../docs/done/PLAN.md — the retired rows, with what each build found.
../docs/canceled/PLAN.md — the refused ones, with what killed each and what survived it.
../docs/on-hold/PLAN.md — the parked ones, with their stage, reason and return folder.
../docs/README.md — every ticket, one line each. Read first.
../docs/GLOSSARY.md — the words the ranking is written in.
/ticket writes them, /design fixes one this finds wrong, /dev builds the top row, /git-release ships it, /done retires its row.