Methodology
AI Project-Management Playbook
A project-management system for GitHub repos, built for working with coding agents. Every piece of work is an issue, and two things organize them — the release it ships in, and its labels. Nothing else. I worked it out running ForgeDB, then generalized it so any repo can pick it up — and packaged it so your agents read the rules and a linter enforces them.
TL;DR
- Every piece of work is a GitHub issue. No TODO.md, no second backlog, no board that holds anything the issues don't.
- Two things organize those issues: the milestone it ships in, and its labels. There is no priority field, no size field, and no workstream field.
- The labels form a ladder from “just an idea” to “shipped,” and moving up a rung requires one specific thing each time.
- A short list of rules about which labels can coexist turns every question — what have we committed to? what's scheduled? can we ship? — into a one-line search.
- Nothing gets built until a design note exists and then an implementation plan, in that order. The tests get written before the code.
- The code is the only thing that's actually true. Every doc, card, and label is a claim about it, and a claim that disagrees with the code is wrong.
- Those rules are simple enough to check mechanically, so it ships as a command that fails your build when the backlog breaks one — not just as a document you hope people read.
The severity is the point, and it comes from working through coding agents. An agent is far more literal than a teammate: it will happily act on a stale roadmap doc or a status label nobody moved, and it has no instinct for “that card is obviously out of date.” So the system is built so that the state of a piece of work is something you look up, not something someone has to remember to update.
The code and its git history are the only record of what is actually true. Everything else — a board card, a label, a roadmap doc, a design note — is a claim about the code and has to point back at it. When a claim and the code disagree, the claim is wrong.
How it works
Every issue answers two questions, and each question has exactly one mechanism behind it.
When
The milestone — always a version number
Is this scheduled, and which release is it going out in?
What, and how committed
The labels
What kind of work is this, and have we actually decided to do it?
- Large items break down through GitHub's built-in sub-issues — the real parent/child link that draws a progress bar, not checkboxes in the description and not a custom field.
- The project board is a saved view of the issues. It never holds anything the issues don't.
The labels are a ladder, not a pile
They rank work by how far it is from being in a user's hands. Moving up a rung takes one specific thing each time — never a judgement call.
- idea
- plan-next
- milestone
- in flight
- closed
- released
Somebody thought of it. Nobody has committed to building it.
To move up → Write a design note and get it accepted.
We're going to build it. We haven't said which release.
To move up → Pick the version — then remove this label.
Scheduled for a specific version.
To move up → Write the implementation plan, then start building.
Being built, starting from failing tests.
To move up → Merge — which closes the issue into its milestone.
The code is done. Nobody can install it yet.
To move up → Cut the GitHub Release for that milestone.
Actually shipped. A user can have it.
The reason for the last two rungs: “done” is ambiguous and it costs you credibility. Splitting it into code-is-finished and users-can-install-it means the roadmap can never quietly over-promise.
The details
Labels, and the rules between them
The ten labels, and the four rules about which ones can appear together — the rules are what make every question a one-line search.
Labels, and the rules between them
The ten labels, and the four rules about which ones can appear together — the rules are what make every question a one-line search.
Each label's description on GitHub is the rule it enforces, written out — so the process is visible in the label picker rather than in a document nobody opens. The setup command writes these for you.
Speculative feature idea; needs a design note before implementation.
Committed but not yet scheduled to a version milestone (milestone = scheduled).
Request for comment: design captured as an issue (proposals no longer committed to the repo).
A spike to measure; deliverable is a decision, not a shippable artifact. Never milestoned.
Umbrella tracking issue; decomposes via native sub-issues.
Blocks the tag: this milestone cannot be released until it is closed.
Known gap or stub in shipped code.
Performance cost / triage item.
Configurable-runtime-behavior work.
Legacy audit: prune dead / product-misaligned code.
Plus GitHub's stock labels (bug, documentation, enhancement, …) and the surface:* labels, if the repo ships more than one thing.
Which labels can coexist
Four rules, enforced on every issue. They're what keep the two axes from blurring into each other, and they're why the roadmap can be computed instead of maintained.
plan-next and a milestone: never both
plan-next means committed but not scheduled. The moment you pick a version, it is scheduled — so the label comes off. An issue carrying both is telling you two different things, and one of them is wrong.
idea and plan-next: never both
One says nobody has committed to this; the other says somebody has. Pick.
An experiment never gets idea, plan-next, or a milestone
A spike you've decided to run isn't speculative anymore, isn't feature work waiting in a queue, and never goes into a release.
A release-gate always has a milestone, and never idea, plan-next, or experiment
It exists to block one specific release, so it means nothing without the version it blocks — and blocking a release is a commitment by definition. While one is open, that version cannot be tagged, even if every feature on it is closed.
What that buys you
Because plan-next can never carry a milestone, “everything we've committed to but haven't scheduled” is exactly the plan-next filter — no compound query, no interpretation. The same trick answers the release question: an open release-gate label is the entire “can we ship?” check.
And what the board deliberately doesn't have
Each one is a second way to slice the work, which makes it a second thing to keep current — and it drifts, because nothing wires it to the code. They also push you to rank work by a number you guessed instead of by when it ships and what it is. If you're migrating a board that has them, delete the fields and every view built on them.
Shipping, and what blocks it
What a milestone is, why closed isn't shipped, the failure mode where every test passes but nobody can install what you built, the ledger that proves a version is releasable, and how many long-lived branches you actually need.
Shipping, and what blocks it
What a milestone is, why closed isn't shipped, the failure mode where every test passes but nobody can install what you built, the ledger that proves a version is releasable, and how many long-lived branches you actually need.
- A milestone is a version — v0.3.0, v1.0.0. Never a theme, never a sprint.
- Putting an issue on one is the only thing that means “scheduled.” Nothing else signals it.
- Keep several open ahead of the current release, so scheduled work always has somewhere to go. 1.0 is a horizon, not a milestone, until its contents are real.
- Closing an issue is not shipping it. The code merges and the issue closes into its milestone; the roadmap keeps saying “pending release” until you cut the tag.
Paste this into every milestone description:
Issues close into this milestone until it is tagged; on the roadmap they read as “pending release” until the vX.Y.Z GitHub Release exists.
Release mechanics
- Conventional commits generate the changelog, and one changelog feeds both surfaces — the GitHub Release body and the website.
- The changelog and roadmap filter out everything that isn't the core product, so only core version work headlines a release.
- If you publish packages, dry-run the publish before you tag. Passing tests inside the repo don't prove an installed user can build.
- Refresh anything that reads “what's next” from the Releases API when the release finishes publishing, not when the tag is pushed — otherwise a just-tagged version shows up as still upcoming.
When green tests aren't enough
This applies to one specific kind of product: one that publishes packages its own output then depends on. A code generator whose emitted code imports your published runtime. A library whose examples install the library. A plugin host and its SDK package. If your project publishes nothing, skip this entirely.
The publish gap
Someone lands work that makes the built output need an API you haven't published yet. Inside the repo everything resolves by local path and every test passes. A user installing from the package registry can't build at all. CI is green and the branch is unshippable.
No test suite catches this, because what's broken isn't the code — it's the relationship between your repo and the package registry, and the repo can't see the registry. The only proof is a clean-room run: from an empty directory, using the published tool, do exactly what a user does — install, scaffold, generate, build — and confirm every dependency resolves from the registry. Green tests have never shown this and never will.
Two ways to prevent it — pick one, and write it down
Option 1
Publish as you go
The moment work needs a newly published API, publish it before the change that depends on it lands. The gap never opens on any branch.
Cost: A lot of intermediate versions, all of them permanent, most of which nobody will ever install.
Option 2
Keep the gap off your main branch
Batch the publishing into the release and let an integration branch carry the unpublished state, so the main branch only ever holds things a user could actually install.
Cost: A second long-lived branch, and a release order you have to actually follow.
Either one works. Choosing neither doesn't. “We'll remember to publish before we tag” is not a mechanism — it's the exact thing that fails. Whichever you pick, write it into CONTRIBUTING.md so it outlives the person who picked it.
If you keep the gap off your main branch
- main holds released state. After a release it matches the tag, and it is always installable from source.
- develop is where the work integrates. It is allowed to depend on things you haven't published yet — that is its entire job.
- The release order is the whole point: publish the packages, then merge develop into main, then tag. Publishing after the merge puts the window right back.
- Run the clean-room check on main, not on develop. On the integration branch it would be red for an entire cycle, and a check that's always red is a check nobody reads.
So which branch does a docs or website change go to?
Not a question about which part of the product it belongs to. One question:
Does this change describe, use, or demonstrate something that isn't released yet?
No→straight to main
Typo fixes, styling, SEO, analytics, dependency bumps, broken links, corrections to docs for things already shipped. These deploy continuously and shouldn't wait on a release they have nothing to do with.
Yes→develop, in the same change as the feature
Documentation for an unreleased feature, examples using an unreleased API, screenshots of UI nobody can see yet, a changelog entry for behavior nobody can run.
Get this backwards and you publish documentation for a feature that doesn't exist yet — which is worse than having no page at all. It generates support load, and it makes your docs a liar at the exact moment someone is trusting them.
release-gate — the rung between closed and released
The ladder ends closed → released. The work that lives in that gap isn't feature work: publishing the packages, reconciling a version number, running the clean-room check, rotating a credential before it expires. Filed as ordinary tech-debt it looks like something you could put off, which is precisely backwards. The release-gate label names it, so “are we ready to ship?” is a search instead of a memory and the tag workflow has something mechanical to check. File one the moment you knowingly defer a release obligation — that's exactly when it's most likely to be forgotten, because everything still works on your machine.
And it carries a ledger of every versioned thing you ship
A release-gate issue that lists only the obligations somebody happened to notice is a checklist of remembered work — and the ones you miss are, by construction, the ones nobody wrote down. So the gate issue carries a table of every independently versioned thing in the project: every published package, every crate, every extension, every separately released binary. Not the ones you touched — all of them, each starting at “no change.” The table gets written when the milestone opens, before any work lands.
| Asset | Released | Bump needed | Why |
|---|---|---|---|
| pkg-core | 1.4.2 | minor | #123 added an additive API |
| pkg-cli | 0.9.0 | no change | |
| vscode-ext | 0.1.0 | no change |
As work lands, its row gets updated in the same pass that lands it. That's the whole mechanism. Deciding “does this need a bump?” with the change in front of you is reliable; reconstructing it at tag time from a diff is not.
Why the “no change” row has to be written down
The “no change” row has to be written rather than implied. A missing row and a “no change” row look identical at tag time and mean opposite things — one is verified untouched, the other was never considered — and only the explicit table can tell them apart. That distinction is the entire reason the gate can answer “are we releasable?” mechanically.
Include the things you don't think of as products. An internal package no user ever names still resolves from a registry, and its failure is the quiet one: the version exists, so nothing errors, and the release ships stale source behind a correct-looking version number. A publish dry-run doesn't catch that. The ledger is the only thing that does.
Which makes the whole gate issue four headings:
What blocks the tag
One obligation per line. Publishing an artifact, reconciling a version line, rotating a credential — never features, which are ordinary milestone work.
Versioned-asset ledger
Every independently versioned thing, defaulting to “no change,” updated as work lands.
Verification
How “releasable” gets proven rather than assumed: the command or check somebody can re-run, and what passing looks like. Flag any evidence that goes stale — a badge from before a publish is not a live result.
Release order
Publish, merge to the main branch, tag, close the milestone. The order is load-bearing, so write down anything that has to happen before something else and why.
One integration branch, never one per version
Once you have an integration branch, the obvious next thought is a second one — v0.5-develop sitting alongside v0.4-develop, so next-cycle work has somewhere to go. Don't. There is exactly one integration branch, and its name never contains a version.
The registry has one version line, so only one cycle can be measured
The publish gap is defined against what's currently published, and npm, crates.io, PyPI, a container tag — all of them are a single global namespace. Two branches carrying unpublished changes can't both be checked: whichever publishes first quietly redefines the other one's gap.
A version in the branch name writes the schedule down twice
This one applies even if you publish nothing. The milestone already says when something ships. Putting the version in a branch name says it again somewhere harder to query and harder to correct, and the two copies will eventually disagree — the same second-source-of-truth problem, now with merge conflicts.
Keep the name version-free and the branch advances itself. develop just means “the cycle in flight,” so the moment you tag a release it becomes the next cycle — no rename, no new branch, no workflow to edit.
Keeping next-cycle work off that branch
The check reads the milestone rather than the branch name — the schedule already lives on the issue, so there's no reason to write it down again:
A pull request targeting the integration branch may not close an issue milestoned later than the cycle in flight.
Note the shape: it forbids future milestones rather than requiring the current one. That's what makes it usable without exceptions — untracked chores, CI fixes, and typo pull requests all pass, and they should, because work with no issue can't be next-cycle work. Next-cycle work is defined by carrying that milestone, so the milestone is the only thing the rule can fire on.
Never configure which cycle is in flight — derive it. It's the lowest open milestone by version order, so there's no constant to keep updated and nothing that can drift from the actual schedule. That has one prerequisite worth stating because it's easy to skip: closing the milestone has to be part of the release ritual, right next to publishing and tagging. Leave one open after its tag and the check freezes there and starts blocking legitimate next-cycle work — loudly, which is the right way for it to fail.
So where does next-cycle work live in the meantime? On its own branch off the integration branch, unmerged, carrying its real milestone. Rebase after the release merge and it lands normally. That's cheaper than a second integration branch, where you'd pay to forward-port every fix continuously instead of merging once at the end.
The two long-lived branches that are fine
Neither of these is a second release line, which is why they don't break the rule.
A maintenance line cut from a tag
release/v0.4.x, when a patch is needed after the cycle has moved on. It branches backward off released state, so it carries no publish gap at all. Cut it when a patch actually comes up, not in advance.
A track that can't merge into the current cycle
A format break, a major rewrite. Name it for the work — format-v2 — and never for a version, precisely so nobody mistakes it for a release line.
Experiments stay off the schedule
A spike produces a decision, not something a user installs — so it never goes into a release, and a release never depends on how one turns out.
Experiments stay off the schedule
A spike produces a decision, not something a user installs — so it never goes into a release, and a release never depends on how one turns out.
A release ships features, fixes, and performance work — things that turn into a binary somebody installs. An experiment is a spike to measure something, and what it delivers is a decision. So it runs alongside the release schedule, never inside it.
- An experiment never goes on a version milestone. Experiments run as an unscheduled research track, alongside the release work rather than inside it.
- What the experiment concludes may commit you to new feature work — and that feature, not the experiment, is what gets scheduled.
- Never build a release around what you hope an experiment will find. You can't schedule a feature the experiment hasn't yet decided should exist.
- The comparison has to be fair and like-for-like. A verdict from a rigged measurement is worse than no verdict, because you'll act on it.
How to tell which one you have
If the main thing an issue produces is a measurement or a verdict, it's an experiment and it stays off the release schedule. If it's code that ships regardless of what any measurement says, it's ordinary work and it gets a milestone.
When a repo ships more than one thing
A core library, an editor extension, and a website all release on different schedules. Labels keep them from contaminating each other's roadmaps.
When a repo ships more than one thing
A core library, an editor extension, and a website all release on different schedules. Labels keep them from contaminating each other's roadmaps.
A surfaceis one independently shippable face of the product — the core library, the editor extension, the marketing site — each with its own release cadence and its own tags. They're labels, and you only need them if the repo ships more than one thing. A single-artifact repo has one implicit surface and needs no labels at all.
The main product line — what the core version numbers refer to. Usually left implicit.
Editor extension and language server. Ships on its own tag line, like ext-v0.1.0.
Marketing and docs site. Usually deployed continuously, with no version tag at all.
Anything else a user installs separately.
The one rule that matters here
Never put one of these on a core version milestone. A website issue parked on v0.5.0 reads as “done, waiting for v0.5.0” when it actually shipped weeks ago on its own schedule — and it will never appear in the core changelog. Non-core work ships on its own tag line, is filtered out of the core roadmap and changelog, and gets its own milestones if it versions at all.
Why “surface” and not “channel”: a release channel already means a stability stream — stable, beta, nightly. You can ship a beta of the extension, so the two ideas have to stay separable. And CI isn't a surface, because it ships nothing to anyone. The test is whether a user touches the thing.
Big work, and the roadmap
Epics break down through real sub-issues and may span several releases. The roadmap is computed from that structure rather than written by hand.
Big work, and the roadmap
Epics break down through real sub-issues and may span several releases. The roadmap is computed from that structure rather than written by hand.
An epicis an umbrella issue, and it's allowed to span several releases — don't force it to be small. Its children ship one at a time, each carrying its own milestone, and they're linked as real GitHub sub-issues so the progress bar rolls up on its own. Not checkboxes in the description, which drift. Not a custom field, which would be a third way of organizing work.
Decisions locked, with a date
A quoted block at the top listing what's settled, each with a one-line reason. It overrides any stale discussion further down the thread.
Summary
What this delivers, and whether it blocks a release.
Current state
Where the code actually is right now — not where you intend it to go. If you can't point a claim here at code or a commit, it doesn't belong.
Children
Linked as real sub-issues so GitHub rolls up the progress bar. Each child carries its own milestone, which is how an epic can span several releases.
Upstream and downstream
What this depends on, and what depends on it.
The roadmap is computed
A /roadmap page is generated from the two axes plus the sub-issue structure, never maintained by hand. Because of the label rules, every bucket is a single filter:
| Bucket | Which issues land in it |
|---|---|
| Shipped | closed, and the release is tagged |
| Active | has a milestone, or is being worked on now |
| Planned | labeled plan-next — so by the rules above, no milestone |
| Labs | labeled experiment or rfc |
| Ideas | labeled idea |
Design, then plan, then tests
Nothing gets coded until a design note and an implementation plan exist, in that order — and the tests get written before the code that passes them. Plus what to do when a gate has to be redone.
Design, then plan, then tests
Nothing gets coded until a design note and an implementation plan exist, in that order — and the tests get written before the code that passes them. Plus what to do when a gate has to be redone.
Nothing gets coded until two things exist, in this order: a design note, then an implementation plan. Both live as issues, never as files committed to the repo.
Designing and planning are separate jobs. Doing them one after the other, before any code, is what surfaces the problems while they're still cheap to fix.
The design note
What and whyan rfc issueThe problem, what you want to happen instead, the shape of the solution, what else you considered, and what you're explicitly not doing. It describes the solution, not the code. Once it's accepted, drop idea and add plan-next.
Catches: Ideas that fall apart on contact with the problem
The implementation plan
Howwritten on the issueWritten after the design is accepted and the work is scheduled, but before any code: which files you'll touch, what order to build in, what blocks what, the interfaces, and the tests you're going to write.
Catches: Surprises halfway through building
Tests first
Is it donefailing tests, then passing onesWrite the scenarios as failing tests, build until they pass, then clean up while they stay passing. The tests are the definition of done, so nobody has to argue about whether it's finished.
Catches: Arguments about whether it's finished
Each step feeds the next, so nothing gets worked out twice. The design catches the conceptual problems, the plan catches the execution problems, and the failing tests pin down what you meant before any code exists to disagree with it.
You never label the status — you look
There are no has-design or needs-design labels, and no effort labels. You can already tell where something stands by looking: is there an accepted design note? is there a plan on the issue? do the tests pass in CI? A status label is a claim somebody has to remember to update. The artifact doesn't need remembering — either it exists or it doesn't.
Where the writing lives
The design note is the rfc issue, never a proposal file committed to the repo. The only design docs in the tree are durable architecture references for features that already shipped. When something ships, fold its lasting design into ARCHITECTURE.md and close the rfc.
VERSION_ROADMAP.mdThe honest state of the current release: where things stand, what's locked into scope, what's finished, what got deferred.
WHAT_IT_IS.mdAn is/isn't account — what each feature actually guarantees and where it falls short. Where the README over-promises, this document wins.
CONTRIBUTING.mdWhere a newcomer learns the system: the two axes, the ladder and its rules, and the design-then-plan-then-tests order.
Redoing a gate you already accepted
Gates get reopened. New information lands, a constraint turns out to be an artifact of an assumption, building the thing reveals the design was solving the wrong problem. Redoing a gate is healthy. What isn't healthy is what the issue body says while you redo it — so the moment you decide to redo an accepted gate, you purge the body, before any new thinking happens. What's left is a placeholder and nothing else.
> **Gate 1 is being redone (reopened YYYY-MM-DD).** The previously accepted > design has been withdrawn and this body intentionally holds no design content. > Do not plan against anything here. The live discussion is in the comments.
Why this is a hard rule and not a nicety
A superseded design in the body doesn't read as superseded — it reads as the accepted design, because that's what a body is. Everything downstream trusts it: the next planning pass, an agent picking the issue up cold, a reviewer checking whether the implementation matches. The correction is invariably in a comment, and people reading top-down never get there. The failure is silent, and it compounds: a plan written against a withdrawn design looks exactly like a plan written against the live one.
Stashing the old body to a scratch file while you work is fine, and often useful. Delete the stash once the new gate is accepted and the new body is written — a lingering copy of a withdrawn design is the same hazard, one directory over.
The body stays a placeholder for the whole redo. It gets repopulated only at acceptance, from the accepted outcome — never patched incrementally as the thinking evolves, which just recreates the half-superseded state the purge exists to prevent.
Reconcile the sources on both sides of every gate
Gates are exactly where a stale claim does the most damage, because each gate's output is the next gate's input — so a bad input doesn't get caught downstream, it gets built on. Which means a reconciliation pass on both sides of every gate, not just around work somebody judged non-trivial, and not just around implementation.
Before the gate — verify
List every source making a claim about this work — issue bodies and comments, design and architecture docs, agent memory, code comments, the release-gate ledger — and check each against ground truth: the code, its history, the actual runtime state. Fix or delete what's drifted first, so the gate is built on something verified.
After the gate — propagate
Push the accepted outcome back outward — the issue body, the docs, memory, the issues that cross-link to it — so the next gate and the next session start aligned.
This is deliberately expensive, and it's still worth it: a reconciliation pass costs a bounded amount, once. Planning against a stale claim costs an unbounded amount, and you find out late.
Day-to-day
The standing habits that keep the issues current — and what the project board is actually allowed to do.
Day-to-day
The standing habits that keep the issues current — and what the project board is actually allowed to do.
These are the rules that keep the issues worth trusting.
The backlog lives in issues
No TASKS.md, no TODO.md, no shadow list. You ask what's next by listing open issues, not by opening a file.
File the issue before doing the work
The moment you commit to a piece of work, open an issue for it — tech-debt for a real gap, idea for something speculative — then build. Don't wait to be asked.
Re-read the issue list every session
Things change while you weren't looking. List the issues at the start of the work so you're not acting on a stale picture.
Cross-link docs and issues in both directions
When a new issue gives a home to claims scattered across docs, add the pointers both ways without waiting for permission.
Reconcile the sources on both sides of every gate
Task boundaries are the floor. The gates are the ones you can never skip, because a gate's input is the previous gate's output — a stale claim there gets built on rather than caught.
Keep the release-gate ledger current as you go
When a change touches an independently versioned asset, set that asset's row in the same pass. Not at tag time, from a diff.
Prioritize on engineering merit, never on demand
Before launch there is no usage data, so any argument from “demand” is smuggling in numbers you don't have. Argue from scope, risk, what unblocks what, and whether it fits what the product is.
The board is just saved searches
Its entire job is to give you the views below. Because of the label rules, each one is a trivial filter rather than a query you have to think about.
| View | What it answers |
|---|---|
| Everything | The full backlog, unfiltered. |
| Release spine | Grouped by milestone: what's scheduled, by version. |
| Epics | The big containers, which is the top level of the roadmap. |
| Planned | Committed but not yet scheduled. |
| Labs | Experiments and design notes — the research track. |
| Ideas | The speculative pile. |
| Release gates | Open blockers on a tag. An empty list means you can ship. |
| Surface board | Work grouped by which shippable thing it belongs to. |
| Execution | A kanban of what's actually in progress. |
GitHub's Status field (Todo / In Progress / Done) stays as a light in-flight indicator. It is not a third way of organizing work.
Mistakes this prevents
Every rule above exists because one of these bit me first. If you only read one section, read this one.
Mistakes this prevents
Every rule above exists because one of these bit me first. If you only read one section, read this one.
A second way of slicing the work
A priority field, a size field, a labels convention doing a field's job. There is one model — milestone, labels, sub-issues — and a second one is just another thing to keep current, which means it drifts.
plan-next sitting next to a milestone
The issue now says two contradictory things about whether it's scheduled, and you can't tell which is current.
An experiment on a release milestone
Experiments produce decisions, not things to install. They feed the schedule; they never sit on it. And a release should never be built around what you hope one will find.
Estimates driving scope
You can't reliably know how long something takes, and a guess mis-steers what gets cut.
Justifying work by demand
Before launch that data doesn't exist. Argue from engineering merit instead.
Writing code before designing
Design note, then implementation plan, then failing tests, then code.
Status labels nobody updates
has-design, needs-design, in-review. You can already see the answer by looking at whether the artifact exists.
Design docs rotting in the repo
Designs live as issues. Only the architecture of things that already shipped belongs in the tree.
Calling it done when it isn't installable
Closed and released are separate rungs for a reason.
Treating the board as the backlog
The issues are the backlog. The board is a saved search over them.
Website or extension work on a core milestone
It reads as “done, waiting for v0.5.0” when it shipped weeks ago, and it never appears in the core changelog.
A branch with green tests that can't actually be released
The publish gap. Tests inside the repo can't see it; only a clean-room install can. Publish as you go, or keep the gap off your main branch — remembering to do it before tagging is not a plan.
Docs that ship ahead of the feature they document
Documentation for unreleased behavior belongs on the integration branch with the feature, not merged early because “it's only docs.”
An integration branch with a version in its name — or one per upcoming version
The publish gap is measured against a single registry, so only one cycle can be in flight; and a version in the branch name records the schedule a second time, competing with the milestone that already says it.
A release obligation filed as ordinary tech-debt
It reads as deferrable when it's the opposite. Label it release-gate so “can we ship?” is a search rather than a memory.
A release-gate issue that lists only what somebody remembered
The obligations you miss are the ones nobody wrote down. Give it a row for every versioned asset, defaulting to “no change” — a missing row and a “no change” row mean opposite things.
A withdrawn design left sitting in a reopened gate's body
A body reads as the accepted design no matter what a comment further down says. Purge it to a placeholder before you start rethinking, and rewrite it only at acceptance.
A roadmap that promises more than the code does
State the limits in writing and let the code have the final word.
Written down, then enforced
The rules ship as a package: your agents read them out of your repo, and a linter fails the build when the backlog breaks one.
Written down, then enforced
The rules ship as a package: your agents read them out of your repo, and a linter fails the build when the backlog breaks one.
Written down, this is prose in a context window — which is to say a suggestion. The rules above are already yes-or-no questions about labels and milestones, so they can be run instead of read. That turns them into a command that exits non-zero, and an agent corrects itself against a failing check far more reliably than against a paragraph it only half-loaded.
Why it isn't a normal dependency
Two payloads, going to two different places by two different routes.
The rules themselves→Your agent's context window
Copied into your repo under .pm-playbook/ and committed, stamped with a version.
The setup command and the linter→Your GitHub and your CI
An ordinary npx binary.
The rules get copied into your repo rather than read out of node_modules, and that's deliberate: cloud agents, CI containers, and review sandboxes routinely have no node_modules at all; a committed file shows up in a pull request diff, so a change to the rules gets reviewed like anything else; and every agent harness can read a file in the repo, while none of them reliably resolve a package path mentioned in prose.
Copying costs you drift, so it's paid for with a manifest — the package version plus a hash of every file. The linter compares them and tells you to re-run setup when they've diverged. It's the lockfile idea applied to prose.
What lands in AGENTS.md is about twenty lines: a pointer to the vendored copy plus the label rules, never the whole doctrine. Always-loaded context is the scarcest thing in a repo, and spending hundreds of lines of it on project management would make the agent worse at everything else. The short stanza buys you the rest on demand. Setup can also write CLAUDE.md, .cursorrules, GEMINI.md, and the rest for whatever harnesses you already use, and re-running is safe — the stanza sits between markers, so your own writing is never touched.
The rules it checks
Everything above the line is one of the label rules from earlier, executed instead of described. The rest checks that the setup itself hasn't rotted.
| Rule | Fires when | Severity |
|---|---|---|
| PM001 | plan-next sits next to a milestone | error |
| PM002 | idea sits next to plan-next | error |
| PM003 | An experiment carries idea, plan-next, or a milestone | error |
| PM004 | A release-gate has no milestone to block | error |
| PM005 | A release-gate carries idea, plan-next, or experiment | error |
| PM006 | Non-core surface work is sitting on a core version milestone | error |
| PM007 | An epic isn't broken down into real sub-issues | warning |
| PM008 | A pull request would land next-cycle work on the integration branch | error |
| PM009 | A pull request mentions next-cycle work it doesn't close | warning |
| PM100 | The copy of the rules in your repo has drifted from the package | warning |
| PM101 | An agent instruction file is missing its pointer | warning |
| PM102 | A markdown backlog file has reappeared | warning |
| PM103 | Label changes from a newer version haven't been applied yet | warning |
The part that's aimed at agents, not people
Every violation comes with the command that fixes it, and there's a JSON mode that emits the whole report — that's the part meant for agents rather than people. A harness can hand the violations straight back to the model, which is the difference between the rules being documentation and the rules being a constraint.
The commands
initCopy the rules into your repo, add the issue templates, and wire your agent instruction files. Touches nothing outside your working directory.
bootstrapCreate the labels with their descriptions, a starter milestone, and the filtered board views. Safe to re-run.
checkLint the backlog against every rule. Local-only, JSON, and fail-on-warnings modes.
release-checkAnswers “can we tag this version?” — exits non-zero if the milestone is gated or unfinished.
scope-checkRefuses a pull request that would land next-cycle work on the integration branch.
migrateApplies label renames and removals after a breaking version. Previews by default.
rulesPrints the rule index.
Setup deliberately doesn't touch GitHub unless you ask it to. Creating labels changes state your whole team sees, and that should be a decision somebody made, not a side effect of installing a dependency.
The Claude Code plugin
The vendored copy already works in any harness that reads repo files — Claude Code, Cursor, Codex, Copilot, Gemini, Windsurf. The Claude Code plugin is optional, and what it adds is enforcement earlier in the loop.
/plugin marketplace add hoodiecollin/ai-pm-playbook
/plugin install pm-playbook@pm-playbookThe skillThe always-true core, loaded when it's relevant. It defers to the copy in your repo when there is one, since that copy is pinned to the version the project actually adopted.
/pm-playbook:checkRuns the linter and fixes what it finds, rather than reporting it back to you.
/pm-playbook:promoteMoves an issue up the ladder as one edit, so a promotion can't half-apply.
/pm-playbook:rfcFiles a design note grounded in the code, after checking it isn't a duplicate.
/pm-playbook:releaseAnswers “can we tag?”, keeping “blocked” and “unfinished” separate.
The hookBlocks a gh issue command that would break a label rule — before the issue exists.
The hook reads the command text and nothing else. That's on purpose: it catches what's self-evident in the command instantly and offline, and leaves anything that depends on repo state to the linter. A fast partial check beats a complete one that makes every command wait on the network. It also fails open on anything it can't parse — a hook that breaks your session is worse than no hook — and it never blocks the fix, because it only reads the flags that add things.
Versioning, and what happens when a label changes
The rules are versioned like code, because a change to them can turn issues you already have into violations.
A rule changed, or a label was renamed or removed. Your backlog might start failing the check, and a migration note ships with the release.
A new label, rule, or section.
Wording.
Your labels live in your GitHub, not in the package — so a release that renames one can't fix itself. The setup command writes labels by name and would just add the new one next to the old, leaving every existing issue on the stale taxonomy. That's what the migration command is for, and it previews before it acts, because the three cases aren't equally reversible: if only the old label exists it renames in place and GitHub keeps every assignment; if both exist it has to relabel each issue and then delete the old one; and if only the new one exists it skips, so re-running is safe.
Set it up in your repo
Two commands get you the rules in the repo and the labels on GitHub. Then a checklist, most of which you can skip.
Set it up in your repo
Two commands get you the rules in the repo and the labels on GitHub. Then a checklist, most of which you can skip.
The first command is local — it copies the rules into your repo and wires up your agent instruction files. The second provisions GitHub: the labels with their descriptions, a starter milestone, and the filtered board views. Both are safe to re-run.
npx @hoodiecollin/pm-playbook init
npx @hoodiecollin/pm-playbook bootstrap \
--repo <owner>/<name> --project <N> \
--surfaces "core,ide-extension,website"- Run init. It copies the rules into your repo, adds the issue templates, and wires your agent instruction files. Commit what it writes — agents read it out of the repo, so it can't be gitignored.
- Run bootstrap to provision GitHub: the labels with their descriptions, a starter milestone, and the filtered board views. It's safe to re-run.
- Set the group-by on the release-spine, surface, and execution boards by hand — grouping is the one thing the API won't do.
- Migrating an existing board? Delete the priority, size, and workstream fields, and every view that filtered or grouped by them.
- Define your surface labels — but only if the repo ships more than one thing.
- Seed the two roadmap docs, and write the model into CONTRIBUTING.md.
- If your project publishes packages its own output depends on, decide now whether you publish as you go or keep the gap off your main branch. Write down the answer and which branch a pull request should target, and make the clean-room check required on the default branch. If you publish nothing, skip this.
- Backfill: put the existing backlog on the ladder, assign milestones, and enforce the rules. A plan-next sitting next to a milestone is the number one sign of drift — check --all-states finds every violation at once.
- Convert epic checklists into real sub-issues.
- Wire the checks into CI, so the rules outlive whoever set them up: lint on pull requests, the release check before a tag, and the scope check on anything targeting the integration branch.
Wiring it into CI
This is the step that makes the rules stick — everything above is a convention until something exits non-zero.
# on pull requests — lint the backlog
- run: npx @hoodiecollin/pm-playbook check --repo ${{ github.repository }}
# before a tag — "can we actually release this?"
- run: npx @hoodiecollin/pm-playbook release-check ${{ github.ref_name }}
# on pull requests into the integration branch — keep next-cycle work out
- run: npx @hoodiecollin/pm-playbook scope-check ${{ github.event.pull_request.number }}The issue templates
ideaThe speculative rung. Filing one commits you to nothing.
rfcThe design note, written as an issue instead of a file.
implementation-planThe ordered build, written before any code.
epicSub-issues plus the decisions-locked and current-state skeleton.
release-gateWhat blocks the tag, plus the versioned-asset ledger. Open one per release milestone, at the start of the cycle.