close-out-epic ยท diff
git:20261027.47c5175 to git:20261027.599bb58
2 added, 2 removed. Audit A to A.
---
name: close-out-epic
description: Close out an epic before it's released, so that its open-ticket count means something. Lists every open ticket under the epic and proposes, for each, fix before release, move to the epic's follow-up epic, or make standalone, then waits for the user's decisions. Moves the deferred tickets out of the epic with a link back, rewrites each one's description so it still makes sense outside the epic, posts one closing comment on the epic saying what was deferred and where it went, and checks the epic has no open tickets left. Run it again after the release ships, to confirm production runs it, check each release workflow and its tracker sync, close any ticket left waiting to ship with the version it shipped in, rewrite the epic's Release order and Close-out comments as shipped, and offer to draft the epic's release notes. Use when the user says "close out PROJ-X", "get PROJ-X ready to release", "move the leftovers out of the epic", "what's still open in this epic", "wrap up the epic" or "the epic shipped", and from build-epic right before release QA.
---
# Close Out Epic
An epic released with tickets still open under it can mean two very different things: "we shipped and left work behind" or "we decided this was ready". Closing it out makes it the second one, on the record. Every ticket still under the epic at release is either done or being done before release; everything else has been moved somewhere it will be found, with a trail back.
**First, load the team config with `roost-config`.**
Do this at release time, not earlier. Until then, "later" items stay in the epic (`slice-handoff` tracks them in its Carry-over comment), because decisions can change.
## Inputs
- **EPIC:** the epic's ticket key.
## 1. List what's open
Find every open ticket under EPIC, at any depth: its children of every type, and their sub-tasks. "Open" means it isn't closed (done, won't do, or any other closed state) and it isn't at `tracker.statuses.ready_for_release` (the tracker adapter's common searches show the query). Read the epic's **Carry-over** comment too, if it has one. Leave out the epic's deploy ticket and its sub-tasks: they're the release itself. Leave out the tests' shared setup, where it's a ticket (the tests adapter's "Shared setup"): it's part of the epic's tests, and closes with the last test that uses it; close any whose tests are all done. Leave out person-owned tasks (labelled `tracker.person_task_label`) and manual tests whose description says `When: after release` ("Manual tests" in the tests adapter) too: they stay in the epic with their owner. List the ones not done yet in the Close-out comment, with their owner and "When".
For each ticket, note what it's about (from its summary, description and links), where it came from (story, review finding, exploratory testing), and its Components.
## 2. Propose a place for each
| Place | When |
|---|---|
| **Before release** | It harms a customer at launch: security, data loss, a broken main flow, a 5xx on a public endpoint, or an accessibility failure in a main flow. It stays in the epic and gets fixed. |
| **Follow-up epic** | It's still about this epic's feature, and it's the kind of work you'd plan together with the rest of the feature's leftovers (hardening, edge cases, polish of the same screens and endpoints). |
| **Standalone** | It came up during the work but isn't about the feature: shared infrastructure, logging, tooling, lint, local development. It belongs with general engineering work. |
Judge each the `triage-ticket` way: is it real, who does it hurt, how badly. Show one table, with a recommendation and a one-line reason each:
| # | Ticket | Recommendation | Why |
|---|---|---|---|
| 1 | PROJ-2814 Onboarding loses focus on a 400 | Before release | Accessibility failure in sign-up, the epic's main flow |
| 2 | PROJ-2813 Page through more than 100 keys | Follow-up epic | API keys feature; no customer near 100 keys at launch |
| 3 | PROJ-2828 Fix the 44 lint errors | Standalone | Not about the feature |
**This is the only stop.** Wait for the user's decisions, then carry them all out without asking again. If any ticket is "before release", say the close-out finishes when those are done, and come back to step 5 then.
## 3. Make each moved ticket stand on its own
A ticket written inside an epic leans on things that only make sense there: review finding codes ("U9", "A5"), "the checkpoint", plan contracts ("C3"), slice names, and branch names that disappear once merged. Before moving a ticket, read its description and add a short **Background** paragraph at the top:
- which epic and feature it came from (with the epic's key), and what that epic shipped that this builds on;
- the code it's about now: file paths on `main` (or the merged stack), not branch names or PR numbers that will be closed;
- any shorthand replaced with plain words.
Keep the rest of the description as it is. If the description already stands on its own, add only the first line ("Came from PROJ-X, โฆ").
## 4. Move them
- **Follow-up epic:** if any ticket goes there, create the follow-up epic once (reuse it if EPIC already has one): summary "<EPIC's summary>: follow-ups", the same Components as the tickets, a description that says it holds work deferred from EPIC at release, and a "relates to" link to EPIC. Set each ticket's parent to it.
- **Standalone:** remove the parent.
- **Every moved ticket** gets a "relates to" link to EPIC, in addition to the links it already has (the story it came from).
Set and clear parents as the tracker adapter says ("Set or clear a parent"). Read every ticket back afterwards: parent, links and description.
## 5. Close the epic out
- Keep one **Close-out** comment on EPIC, starting with the heading "Close-out": what shipped (the stories), and what was deferred, as a table of ticket, where it went (follow-up epic key, or standalone), and why. Link the follow-up epic. The first run creates it; a later run (for example after release QA defers more) finds it by that heading and rewrites it in place by its ID, as the Carry-over comment is kept. Never post a second one.
- Update the Carry-over comment, if the epic has one, so it says nothing is left over.
- Check that no ticket under EPIC is still open, at any depth, other than the "before release" ones being worked on, the deploy ticket with its sub-tasks, person-owned tasks, and after-release manual tests. Say the count.
## 6. After the release ships
- Run this part again once production runs the epic's code, and confirm that first: the rollout's deploy step passed, or, for an epic with no rollout, the production deploy that carries its release has run (merging deploys nothing on its own, unless the repo's `production` is `merge`). For each repo with work in the epic, find its last change's landing commit, the commit that brought it onto the default branch, as `release-notes` step 1 says: a PR merged into another branch (on a stack) lands only when that branch does, so its own merge doesn't count. Check where it merged (`gh pr view <n> -R <repo> --json baseRefName`). When you can't look, and nothing you were given says it merged into another branch, take it as merged into the default branch for confirming production and offering the note, put that assumption in the ๐ **Your turn** block, and until the user confirms where it merged, make no epic-wide tracker writes (the Release order and Close-out comment rewrites, closing the epic) and no writes to tickets with work in that repo, a hand-run tracker sync included; writes to tickets that touch only confirmed repos go ahead. When the user answers: if it merged into the default branch, make the held writes; if into another branch, that repo isn't shipped, so say so, check its production again once that branch lands, and say the release note offered would be held. Then its `production` key says where to look: for a `workflow`, that workflow's successful run whose commit contains the landing commit (`gh run list --workflow <file> --status success`); for `release`, the published release that contains it; for `merge`, the merge of the PR that landed it. A repo without `production`: confirm it as you would otherwise, and if you can't tell, ask the user. Until production runs it, the epic isn't shipped. Closing out before release leaves tickets at `tracker.statuses.ready_for_release`; shipping should move them to `tracker.statuses.done`, and it doesn't always. If the config has no ready-for-release status, check only that every ticket is done.
+ Run this part again once production runs the epic's code, and confirm that first: the rollout's deploy step passed, or, for an epic with no rollout, the production deploy that carries its release has run (merging deploys nothing on its own, unless the repo's `production` is `merge`). For each repo with work in the epic, confirm production with the `release-notes` skill's script, `../release-notes/scripts/production_date.py` from this skill's folder (`skills/release-notes/scripts/production_date.py` in the plugin), run as `release-notes` step 3 says, with the repo's `production` and the epic's merged PRs in that repo. The script decides where each PR landed (a PR merged into another branch, on a stack, lands only when a PR brings that branch onto the default branch, so its own merge doesn't count), takes the last to land, and applies the same rules as for a release note: for a `workflow`, that workflow's first successful run on the default branch or a tag whose commit contains the landing commit; for `release`, the first published release that contains it; for `merge`, the merge of the PR that landed it. `status: dated` means production runs it, with `deploy` saying what carried it. `held` means it doesn't yet, with `reason` saying why: when a PR hasn't reached the default branch, that repo isn't shipped, so say so, check again once it lands, and say the release note offered would be held. `not_found` means GitHub can't tell you: say the `reason`, and ask the user. When the user has told you what deployed, use that, and don't run the script for that repo: a deploy or release they say carries the epic's work in a repo (for example "its fix is in 3.21.0", and 3.21.0 deployed) confirms that repo; PRs they say merged, without naming another branch, merged into the default branch. When you can neither run the script nor tell from what you were given what deployed a repo's work, say so in the ๐ **Your turn** block and ask; until the user answers, make no epic-wide tracker writes (the Release order and Close-out comment rewrites, closing the epic) and no writes to tickets with work in that repo, a hand-run tracker sync included; writes to tickets that touch only confirmed repos go ahead. A repo without `production`: confirm it as you would otherwise, and if you can't tell, ask the user. Until production runs it, the epic isn't shipped. Closing out before release leaves tickets at `tracker.statuses.ready_for_release`; shipping should move them to `tracker.statuses.done`, and it doesn't always. If the config has no ready-for-release status, check only that every ticket is done.
- **Check each release's workflow run.** If the repos release with `deploy.release_tool` (for example Release Please), check in each repo that released that the run that published the release succeeded, including any tracker sync job it has (`gh run list --workflow <its workflow file>`). If a run failed or its sync job was skipped, run the repo's tracker sync by hand for that tag (a dry run first), and file a Bug for the workflow, linked to EPIC.
- **Person-owned tasks due after release:** remind each owner in the ๐ **Your turn** block. The epic closes when they're done; until then, say which are open.
- **Manual tests due after release** (`When: after release`): ask the session of the repo that owns the work each checks to run it and record a `manual run:` comment, with the steps it can't take as ๐ **Your turn** actions ("Manual tests" in the tests adapter). Close each only on a passing run; until then it stays open, and so does the epic.
- **Every other ticket under EPIC is Done,** at any depth (manual tests close only as above). A fix that shipped in the other repo's commit, or one that no commit names, can still sit at `tracker.statuses.ready_for_release`: close it by hand, add the version it shipped in as its fix version, and say in a comment where it shipped and how it was verified.
- **Rewrite the epic's comments as shipped:** the Release order comment says every PR is merged and in which release, with the deploy dates, and the Close-out comment says what's in production. Add the tickets found during the rollout to the Close-out comment, under their own heading.
- - **Offer the release note.** Once production runs the epic, ask whether to draft its release notes with the `release-notes` skill (one per feature it shipped; usually one). Only offer: don't write the note unless the user asks. Offer it even while a repo's merge is waiting for the user to confirm (above): say its date may change, or it may be held, depending on their answer.
+ - **Offer the release note.** Once production runs the epic, ask whether to draft its release notes with the `release-notes` skill (one per feature it shipped; usually one). Only offer: don't write the note unless the user asks. Offer it even while a repo is waiting for the user to say where its PRs landed or what deployed them (above): say its date may change, or it may be held, depending on their answer.
## Report
Anything the user must do first, in a ๐ **Your turn** block (leave it out when there's nothing). Then one line: "PROJ-2306 closed out: 9 tickets to PROJ-2900 (follow-ups), 5 standalone, 0 left open." Then the deferred table from the closing comment. After the release ships: "PROJ-2306 shipped in API 3.20.0 and UI 3.3.0; 0 tickets left open; release syncs checked. Want me to draft its release notes?"