Plot release
Verify release readiness and guide the release process. Part of the Plot workflow. Use on /plot-release.From its SKILL.md
npx -y skills add plot-pm/plot --skill plot-releaseAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
22.2 KB, as published. Nobody here has run it
Plot: Cut a Release
Create a versioned release from delivered plans. This workflow can be run manually (using git and the git-host CLI), by an AI agent interpreting this skill, or via a workflow script (once available).
Input: $ARGUMENTS is optional. Can be:
rc— cut a release candidate tag and generate a verification checklist- A version number (e.g.,
1.2.0) or bump type (major,minor,patch) — cut the final release
--ignore-sprint may accompany any of these: it clears the sprint gate in step 0 and nothing else.
Examples: /plot-release rc, /plot-release minor, /plot-release 1.2.0, /plot-release 2.5.2 --ignore-sprint
Setup
Add a ## Plot Config section to the adopting project's CLAUDE.md:
## Plot Config
<!-- Optional: uncomment if using a GitHub Projects board -->
<!-- - **Project board:** owner/number (e.g. eins78/5) -->
- **Branch prefixes:** idea/, feature/, bug/, docs/, infra/
- **Plan directory:** docs/plans/
- **Active index:** docs/plans/active/
- **Delivered index:** docs/plans/delivered/
Model Guidance
| Steps | Min. Tier | Notes |
|---|---|---|
| 0. Sprint Gate | Small | The states come from plot-sprint-release.sh; applying the rule is mechanical. The Should-Have question needs a person, not a bigger model |
| 1. Determine Version | Mid | Heuristic: plan types → bump suggestion |
| 2A. RC Path | Small | Git tag, template generation |
| 2B. Release Notes | Mid | Discovery logic, changelog collection |
| 3. Cross-check Notes | Frontier (orchestrator) + Small (subagents) | Orchestrator compares; small subagents can gather commit messages and plan changelogs in parallel |
| 4-5. Hand-off, RC cleanup | Small | Template list, no-ops |
| 5b. Record the Release in the Plans | Small | Mechanical per plan; the version comes from git tag --contains, not judgment. Gate on the sweep's real footer |
| 5c. Sprint Override Record | Small | One line into ## Notes, from facts step 0 already collected |
| 6. Summary | Small | Formatting |
User interaction: Use
AskUserQuestion(Claude Code) /ask_question(Cursor) for all questions, proposals, and confirmations.No user present? If
PLOT_UNATTENDED=1is set, do not call the question tool — each question below declares what to do instead, and every skipped question is named in the output. See Running unattended.
0. The Sprint Gate
Run this before anything else. A release that has already been tagged cannot be un-cut, so the sprint's claim has to be checked while refusing is still cheap.
The gate applies to the final cut, not to an RC. /plot-release rc proceeds
past open Must Haves — report them, do not refuse. A release candidate is how a
sprint's remaining work gets verified, so gating it would take away the tool
operators use to finish the very items being gated on. The gate fires when the
version becomes real.
Neither the plan nor the brief settled the RC case; this is the reading that keeps the gate from blocking its own remedy. Worth confirming in review.
../plot/scripts/plot-sprint-release.sh 2>/dev/null
The script reports facts and decides nothing: the sprint's declared Release:
target, and every Must/Should/Could item as done, open or disputed. Read
its JSON and apply the rule below. It never exits non-zero for an unfinished
item — a script that refused would be making this call itself.
No active sprint, or an active sprint with no Release: field → the gate
does not apply. Say so in one line and go to step 1. This is the majority
case and must stay silent-ish: a sprint that groups work without shipping a
version is still a sprint, and a repo that never declares one sees no change
at all.
Otherwise, the two tiers get two different treatments, because they are two different promises. A Must Have is the commitment; a Should Have is what the sprint hoped to reach.
Must Haves refuse
If any Must Have is open or disputed, refuse and stop. Name every one,
and say what clears it:
plot-release: sprint the-board-tells-the-truth targets 2.5.2 and has
1 unfinished Must Have:
[one-place-for-what-a-row-can-do] — Approved, not delivered
Deliver it, move it to Deferred, or pass --ignore-sprint.
A disputed item is a checked box whose plan is not delivered — the same
false-positive completion /plot-sprint close refuses on. Report it as what it
is, not as merely unfinished:
[alpha] — checked in the sprint, but the plan is not delivered
--ignore-sprint is the named escape, in the tradition of --allow-local
and --during-release. A gate with no exit is one people route around by never
declaring a release at all, which would cost the field its adoption.
It clears the sprint gate and nothing else. Not the phase guardrails, not the delivered-plan checks, not step 5b's sweep. If you find yourself reaching for it to get past something else, that is a different problem.
Should Haves prompt
If the Must Haves are clear and any Should Have is open or disputed, ask
— naming them — and take yes or no in the moment:
Sprint the-board-tells-the-truth targets 2.5.2. All Must Haves are done.
3 Should Haves are open:
[the-board-answers-agents] — wave 1 delivered, wave 2 open
[plot-board-setup] — not delivered
Set the 32 delivered-but-unreleased plans to Released
Cut 2.5.2 anyway?
Answering no cuts nothing — stop, changing no files.
There is no flag for this tier, deliberately. A hard gate on stretch goals is one operators learn to force past, and a flag typed reflexively has stopped being a gate. But silence is the failure this whole step exists to fix: a release cut with three Should Haves open is a decision, and a decision made without being asked is one nobody made. The confirmation is the record that a person looked.
Could Haves neither block nor prompt
Report them in the summary if any are open. Nothing more.
When nobody is there to answer
Under PLOT_UNATTENDED=1 — a release cut from CI rather than a terminal — the
Should-Have prompt becomes a warning: name the open items, state that
nobody was asked, and proceed. The Must-Have gate still refuses, in both
modes.
PLOT_UNATTENDED answers may I ask?, never may I proceed?. A variable set
in the least-supervised environment must have strictly less power than the
operator, so it never converts a refusal into a pass.
This clause is the local half of a wider fix.
PLOT_UNATTENDEDis defined once, for the fifteen skills that tell an agent to ask, bydocs/plans/2026-08-18-a-question-nobody-can-answer-is-a-hang.md. That branch had not landed when this gate was written, so the degradation is implemented here and stated here; when the shared reference lands, this section should point at it rather than restate it.
The override writes itself into the sprint
When --ignore-sprint is used, record it in the sprint file's ## Notes —
the version, the date, and the Must Haves that were open:
## Notes
- 2.5.2 cut 2026-08-18 with `--ignore-sprint`; 1 Must Have open:
[one-place-for-what-a-row-can-do]
Write it directly under ## Notes, not inside ### Scope Changes. That
subsection logs what the sprint decided about its own contents; a release cut
over its objection is something that happened to the sprint. Filing it there
would bury the record under a scope log nobody reads for release facts.
Write it after the tag exists, in step 5c — not here. Until then no version has been released, and a note claiming one is the same defect step 5b's "only run this once the tag exists" guards against. The gate's job in step 0 is to decide and to remember the open items; the writing happens once the thing being recorded is true.
If the release is abandoned between step 0 and the tag, nothing was written and nothing needs undoing — which is the point of deferring it.
Commit it with the release's other sprint-side changes.
This couples a release command to a sprint file, and the coupling is accepted
deliberately: the retrospective asks what the timebox changed, and this is
exactly the fact it cannot reconstruct from a shell history nobody rereads. It
is the same reason Approved: and Delivered: are records in the plan rather
than lines in a log.
Two active sprints may target one release — two teams, one train. That is legitimate and is not refused; the gate answers to both, and the message names which sprint each unfinished item came from.
1. Determine Version
Check for the latest git tag:
git tag --sort=-v:refname | head -1
If $ARGUMENTS is rc:
- Determine the target version (same rules as below — check delivered plans, suggest bump type)
- Check for existing RC tags for this version:
git tag --list "v<version>-rc.*" - Next RC number: if no existing RCs, use
rc.1; otherwise increment - Proceed to step 2A (RC path)
If $ARGUMENTS specifies a version (e.g., 1.2.0):
- Use it directly (validate it's valid semver)
- Proceed to step 2B (final release path)
If $ARGUMENTS specifies a bump type (major, minor, patch):
- Calculate the new version from the latest tag
- Proceed to step 2B (final release path)
If $ARGUMENTS is empty:
- Check if there's an open RC checklist (
docs/releases/v*-checklist.md) with all items checked - If yes: propose cutting the final release for that version
- If no: look at delivered plans since the last release to suggest a bump type:
- Any features → suggest
minor - Only bug fixes → suggest
patch - Breaking changes noted in changelogs → suggest
major
- Any features → suggest
- If unable to determine bump type from plan metadata, ask the user to specify the version directly
- Propose the version and confirm with the user
Unattended (
PLOT_UNATTENDED=1):
- Bump type derivable from the delivered plans' types — proceed with it. That is a reading of recorded facts, not a choice.
PLOT-UNASKED: Release v<version> (<bump> from <n> delivered plans)? — default — proceeding- Bump type not derivable — stop. There is no safe default for a version number, and guessing one tags a repo permanently.
PLOT-UNASKED: What version should this release be? — stopped — plan types were inconclusive; nothing tagged
Smaller models: Skip the automatic bump type suggestion. Instead, list the delivered plans with their types and ask the user: "What version should this release be? (major/minor/patch or exact version)" Let the human decide.
2A. RC Path — Cut Release Candidate
Tag the RC:
git tag -a v<version>-rc.<n> -m "Release candidate v<version>-rc.<n>"
git push origin v<version>-rc.<n>
Generate verification checklist:
Collect all delivered plans since the last release (via docs/plans/delivered/ — check the Delivered date in each plan's Status section against the last release tag date). For each delivered feature or bug plan, extract the ## Changelog section and create a checklist item. If a plan has a Sprint: <name> field, include the sprint name alongside the checklist item for context. Whether the sprint blocks the release is decided in step 0, from its Release: field — not here.
mkdir -p docs/releases
Write docs/releases/v<version>-checklist.md:
# Release Checklist — v<version>
RC: v<version>-rc.<n> (YYYY-MM-DD)
## Verification
- [ ] <feature/bug slug> — <changelog summary>
- [ ] <feature/bug slug> — <changelog summary>
## Automated Tests
- [ ] CI passes on RC tag
## Sign-off
- [ ] All items verified by: ___
- [ ] Final release approved by: ___
Unattended (
PLOT_UNATTENDED=1): the RC tag and checklist are mechanical — cut them. But never fill a sign-off line, and never treat an unsigned checklist as passed. Those two blanks are the record of a person taking responsibility; an agent writing into them forges it.PLOT-UNASKED: Who verified / approved this release? — refused — sign-off is a person's to give; the lines are left blank
git add docs/releases/v<version>-checklist.md
git commit -m "release: v<version>-rc.<n> checklist"
git push
Summary (RC):
- RC tag:
v<version>-rc.<n> - Checklist:
docs/releases/v<version>-checklist.md - Plans included: list of slugs
- Progress:
[ ] Draft > [ ] Approved > [x] Delivered > [*] Released (RC) - Suggested next actions:
- Test against the checklist items
- If bugs found: fix via
bug/branches, merge, then/plot-release rcfor next RC - When all items pass:
/plot-releaseto cut the final release
2B. Final Release Path — Generate Release Notes
Check for project-specific release note tooling, then either run it or fall back to manual collection.
Discover tooling — check in this order:
- Changesets: Does
.changeset/config.jsonexist? If so, the project uses@changesets/cli. - Project rules: Read
CLAUDE.mdandAGENTS.mdfor release note instructions (e.g., custom scripts, specific commands). - Custom scripts: Check
package.jsonfor release-related scripts (e.g.,release,version,changelog).
If tooling is found: remind the user to run it (e.g., pnpm exec changeset version for changesets). Do not run release tooling automatically — the user controls when and how versions are bumped. Then proceed to step 3 (cross-check).
If no tooling is found: collect changelog entries from delivered plans and present them to the user:
# Get the date of the last release tag (exclude RC tags)
LAST_TAG=$(git tag --sort=-v:refname | grep -v '\-rc\.' | head -1)
if [ -n "$LAST_TAG" ]; then
LAST_RELEASE_DATE=$(git log -1 --format=%ai "$LAST_TAG" | cut -d' ' -f1)
else
LAST_RELEASE_DATE="1970-01-01"
fi
# Find delivered plans newer than the last release
ls docs/plans/delivered/ 2>/dev/null
For each delivered plan since the last release:
- Read the
## Changelogsection - Read the
## Statussection for the Type (feature/bug/docs/infra) - Collect the changelog entries
Only include feature and bug plans in the release notes (docs/infra are live when merged — they don't need release).
Present the collected entries to the user and suggest they add them to CHANGELOG.md. Do not write to CHANGELOG.md directly.
3. Cross-check Release Notes
Model tiers for this step:
- Frontier (e.g., Opus): Full cross-check — compare changelog entries against delivered plans and commit messages. Can delegate data gathering (reading plans, collecting commit messages) to small subagents. Flag significant gaps (missing features, phantom entries). Don't nitpick wording.
- Mid (e.g., Sonnet): Compare changelog entry count against delivered plan count. Can delegate plan reading to small subagents. Flag obvious mismatches (plan with no corresponding entry, entry with no corresponding plan). Skip semantic content comparison.
- Small (e.g., Haiku): Skip gap detection. Present the generated release notes and ask: "Do these release notes look complete?" Human review is the final gate.
Whether generated by tooling or manually constructed, compare the changelog against the actual work:
- Collect the list of delivered plans and commit messages since the last tag
- Compare against the generated changelog entries
- Only flag significant gaps or errors — e.g., a delivered feature completely missing from the changelog, or a changelog entry that doesn't match any actual work
- Don't nitpick wording or minor omissions — offer improvements only if there are clear, meaningful gaps
- If gaps are found, show them to the user and ask whether to fix before proceeding
Unattended (
PLOT_UNATTENDED=1): proceed with the notes as generated, and list every gap found in the output — the gaps are the finding, and a person reads them after the fact.PLOT-UNASKED: Fix <n> changelog gaps before proceeding? — default — proceeded; the <n> gaps are listed above
This cross-check is the primary value of /plot-release — verifying that release notes accurately reflect delivered work.
4. Hand-off to Project Release Process
The remaining mechanics — updating CHANGELOG.md, bumping the version, tagging, pushing — belong to the project's own release process, not to plot-release. Plot's job ended with the cross-check in step 3.
Different projects release differently: some use changesets (pnpm exec changeset version + a "Version Packages" PR), some run a CI release workflow triggered by a tag, some do it manually. plot-release is a participant in that flow, not the driver.
For reference — if the project has no release tooling and the user asks for the manual sequence, it typically looks like:
- Update
CHANGELOG.mdwith the entries collected in step 2B - Bump version (e.g.
pnpm version <version> --no-git-tag-version) - Commit:
git commit -am "release: v<version>" - Tag:
git tag -a v<version> -m "Release v<version>" - Push:
git push origin main && git push origin v<version>
Do not execute these on the user's behalf. Point them at their release tooling (or the list above if there is none) and stop.
5. Clean Up RC Artifacts
If RC tags exist for this version, they remain in git history (don't delete them — they're part of the release record). The checklist file at docs/releases/v<version>-checklist.md stays committed as documentation of what was verified.
5b. Record the Release in the Plans
The release exists; the plans it shipped do not know it. Until this step, no
plan in this repo's history had ever reached Phase: Released — not once across
sixteen versioned releases — because step 4 hands off to the project's release
process and nothing came back afterwards.
Only run this once the tag exists. A plan marked before the tag is cut
claims a version nobody released. Verify with git tag --list v<version> before
writing anything.
For each plan currently at Phase: Delivered:
-
Skip docs/infra plans.
/plot-deliveralready told their authors they are live on merge; marking them Released contradicts a message Plot itself sends. -
Resolve the version from git, never from dates. Take the plan's last
→ #Nannotation, get its merge commit, and find the release tag containing it:SHA=$(../plot/scripts/plot-host.sh pr-state <N> | jq -r '.mergeCommit') TAG=$(git tag --contains "$SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | head -1)The delivery date is when the plan was booked, not when its code merged — those can be months apart, and two tags may share a date. Dates get this wrong;
--containscannot. -
A plan with no annotation, or no merge commit, is left alone and reported as unresolvable. An invented version in a transition record is a claim nobody re-checks.
-
Write, in the plan's
## Status:- **Phase:** Released - **Released:** <tag date>, <version>The symlink does not move.
delivered/means "no longer active", not "phase is exactly Delivered" — unlike/plot-deliver, this step moves nothing.
Idempotent: a plan already at Released with a record for this version is left untouched. Re-running after a partial failure converges.
Commit on the default branch using the disposable-branch mechanic from
/plot-approve step 4 (including its branch-protection fallback).
Then the gate. This is a multi-file write followed by a push, the shape that half-lands — and worse than delivery's, because it touches N plans, so a partial write leaves some released and some not with nothing to say which. Run the sweep and show its real output:
../plot/scripts/plot-reconcile-scan.sh 2>/dev/null | tail -1
unreleased_delivered=0 clears the gate. Any other number is a hard stop: show
section 6's findings and fix them before proceeding.
Report what you did NOT mark, with the reason. A silently skipped plan looks identical to a plan with nothing to do — precisely the confusion that hid this for sixteen releases:
Released as v2.3.0:
fleet-agent-view docs/plans/2026-08-15-fleet-agent-view.md
Not marked:
some-docs-plan docs plan — live when merged
older-plan unresolvable: no PR annotation
summary: … unreleased_delivered=0 …
5c. Record a Sprint Override
If step 0 was cleared with --ignore-sprint, now write the line it held
into the sprint file's ## Notes — the tag exists, so the version is finally a
fact rather than an intention. Format and placement are in step 0.
Nothing to write if --ignore-sprint was not used, or if no sprint declared a
Release:. Idempotent: a note already present for this version is left alone.
6. Summary
Orient, don't enumerate (Manifesto Principle 11): open the summary with where the work now stands, what falls out next, and why — the mechanical details follow.
Print:
- Version:
v<version> - Plans included:
<slug>— <type><slug>— <type>
- Cross-check result: complete / gaps found
- Sprint gate: passed / not applicable / cleared with
--ignore-sprint, and for the last, the Must Haves that were open and the sprint note written - Plans marked Released:
<slug>→<version>for each, and every plan not marked with its reason (docs/infra, or unresolvable) - Release-recorded gate: paste the sweep's actual
summary:footer from step 5b — the objective artifact, not the word "verified" - RC iterations: <count> (if any)
- Progress:
[ ] Draft > [ ] Approved > [ ] Delivered > [x] Released - Plot verification complete — hand off to the project's release process (changesets, CI, or manual) for version bump, tag, and push.
- Suggested next actions:
- Run the project's release tooling (or the manual sequence from step 4 if none exists)
- Run
/plotto verify clean state - Start next cycle:
/plot-ideaor/plot-sprint