Skip to content

Audit: Plan retirement

What we check

A plan in docs/plans/ is written to get one piece of work done, and nothing removes it when that work is finished, so docs/plans/ only grows. docs/plan-retirement.md is the convention that ends that: a plan whose index row has carried a terminal status for more than 90 days has its reasoning moved into docs/, its remaining Future work filed as issues and its files deleted, leaving its index row pointing at a permalink. This criterion finds the plans that are due.

A repository with no docs/plans/index.md is N/A: the index is what records a status, and a repository that does not plan this way has nothing to measure. Where there is one, the criterion fails naming every plan whose row is Complete, Superseded or Abandoned and has been for longer than 90 days.

The clock

The clock starts at the commit where the row changed into a terminal status from one that was not, read from the history of docs/plans/index.md. It is never the last commit to touch the plan's files, and the plan's own files play no part in it. Finished plans are edited all the time by changes that are not about them -- a sweep making links absolute, a template fix, retirement itself rewriting the pointers in other plans -- so a clock that restarted on every touch would never run out, and the plans most worth retiring are exactly the ones those sweeps keep touching.

Only a change into a terminal status starts it. A plan reopened and finished again starts a new clock. A move from one terminal status to another, such as Complete to Superseded, does not, and neither does an edit that leaves the status alone.

A revision in which the row cannot be read as terminal, for any reason, reads as "not terminal" there, and the clock starts after it. That covers a live status, but equally a status outside the shared vocabulary and a table reshaped so that the row no longer parses. This is the conservative direction, because it delays a retirement rather than causing one, and it means an index-wide reformat resets every row's clock.

A row is identified by its plan file's name, so renaming the file and its index link together is a row the earlier revisions do not have, and its clock starts at the rename. That is the direction to err in: a rename delays a retirement rather than causing one, and the row becomes eligible again on its own once the rename is older than the threshold.

The history is walked newest first and the walk stops at the first revision older than the threshold, so it costs what the index churned inside the 90 days rather than what the repository has done since it was created. A row still terminal in a revision older than the threshold has been terminal at least that long, which is all eligibility turns on, so an age is reported as a lower bound -- "at least 97 days" -- and never as an unlabelled number.

The date read from a commit is the committer date, not the author date: a rebase or a cherry-pick keeps the author date of the original work, and what is being measured is how long the row has said terminal in this repository.

The checkout the audit reads must carry that history. A shallow one does not, so the criterion refuses to answer rather than report that nothing is eligible; the audit workflow clones with --filter=blob:none for this reason. A filtered clone fetches a historical blob at the moment the criterion reads it, and the clone leaves no credential behind, so that fetch is anonymous: the criterion can only read the history of a repository the audit can read anonymously.

There are three ways the criterion reports error rather than a verdict: a shallow checkout, a revision of the index larger than the 2 MB cap a file is read under, and a walk that exceeds its five-minute budget. Each is a statement about the audit rather than a finding about the repository, which is why it files no issue and closes none.

Rows that are reported rather than counted

Four kinds of row are named in the result and left out of the count, because each is a plan this criterion cannot speak for, and silence about them would read as approval:

  • A row already retired, whose link is a permalink into this repository pinning a commit under docs/plans/. Retirement leaves the row and its status in place -- retired is not a status -- so the permalink is what says the work is done.
  • A row naming a plan somewhere else, by any other absolute link. This repository's history cannot date it and this repository's pull request cannot retire it.
  • A row whose Plan cell cannot be read: it links nothing, or its status is not one of the seven terms in the shared vocabulary. What the vocabulary is, and whose finding a status outside it is, belongs to plan-index.
  • A terminal row whose status change the history does not show, which is what an index created or renamed into place inside the last 90 days looks like. Dating such a row by the commit that moved the file would retire a plan on the strength of a rename, so it is left undated and reported instead. It becomes eligible on its own once that commit is older than the threshold.

A row whose linked plan file is no longer in the tree is not one of these: it is counted like any other. The criterion reads the index and its history and never the plan's files, and what retirement leaves behind is the row -- a terminal row pointing at a plan that has already been deleted still has to become a permalink before a reader can tell which it is. The retire-plans helper reports such a row as unreadable instead, because its own job is to move the plan's reasoning and it has no text to move; the two tools disagree about the row on purpose, each answering for the part of the work it does.

A table with no Status column

A plan listed in a table that has no Status column records no status, so it can never become eligible. The criterion reports those plans and never fails for them, on a passing result as much as on a failing one. The findings list carries every such plan, while the details string carries a bounded summary of the same list, so a passing result has findings on purpose.

That is deliberate, and it is the rule most likely to be argued with, because it leaves a repository able to hold finished plans out of retirement indefinitely by not giving them a status. The reason to take it anyway is that the alternative is an audit demanding a status somebody will invent to clear an issue, and an invented status in the one page people read to decide what wants attention is worse than none. A criterion cannot tell a deliberate omission from an oversight. A repository keeping such a table should say in the index why, as private-ci does, and docs/plan-retirement.md carries the rule so that it is written down rather than implied by a check.

What the issue means

The issue this files is a backlog, not a deadline. Nothing about a plan goes wrong on the ninety-first day; the issue is the record of which plans have reasoning that is no longer being maintained, and of how much of docs/plans/ is describing software that has since been rewritten.

It also stays open while any eligible plan remains. A repository that retires three of its eleven has made its index better and is not finished, and the issue naming the other eight is the only record of what is left. That is intended rather than an impatience: retirement is deliberately one pull request per repository and one commit per plan, reviewed and merged by a person, so a partial pass through a long list is the expected shape of the work.

Template

No template of its own: a plan is not written differently because it will one day be retired, and the convention applies ninety days after its last work lands, when nobody is reading the template.

The fix is the retire-plans skill, in the private llm-setup repository. It lists the eligible plans, moves each one's reasoning into the documentation that will be maintained, files the deferred work still worth doing as issues labelled from-retired-plan, rewrites every pointer to the plan, turns its index row's link into a permalink and deletes its files -- one commit per plan, one pull request per repository. The parts with one right answer are its retire-plans helper; deciding where a plan's reasoning belongs is not one of them, which is why retirement is never automatic.

A plan whose reasoning has no sensible home in docs/ is not retired. It is left where it is and reported, so that a person can decide: deleting the only copy of a decision's justification to meet a 90-day rule is exactly the loss retirement exists to prevent. Such a plan keeps this criterion failing, which is the correct state -- something is outstanding -- and the reason it is outstanding belongs in the issue.

Projects

Per-project compliance for this criterion is regenerated on every run of the consistency audit: see the compliance page.

📝 Report an issue with this page