Skip to content
open tomato/ rafa
GitHub
rafa pages
  1. rafa
  2. rafa epic

rafa epic

Show an epic's issues, create an epic, defer or promote it, move an issue into it, close it through a gate, or cancel it.

rafa epic manages epics: bigger issues that group specs into a feature, with acceptance criteria, an estimate and an ordered checklist of their specs. Every change to an epic is a command that leaves a comment, so the board's history answers why it changed. rafa epics prints the same as rafa epic show.

Spends: 🪙 only close: one verification planning session and one session per check. Changes: issues, labels, checklists and comments on the board. Needs: gh signed in with access to edit issues and labels; see Prerequisites.

Things to know before you use it:

  • close refuses while any member is open, then checks each acceptance criterion against origin/main in its own session; a failed check is filed as a bug and refuses the close. See Closing an epic.
  • cancel asks what becomes of each issue outside the epic that its open members block, so no other team's work is stranded.

Epics and boards explains when to use epics at all.

NOTE

In the target layout this code lives in the private package rafa-board, bundled into @open-tomato/rafa (Repository layout). In 0.47.0 it is already a package, @open-tomato/rafa-board in packages/rafa-board, but only as a facade: it re-exports this code from the root package's src/, where the files still live until their move. You install and type the same either way. [verify: the release the move lands in]

Actions

ActionDoes
showlist one epic's issues as the Roadmap table: the epic numbered, or the first now epic not done
newcreate an epic: the issue from the epic template, its line on the current board, and in labels mode its epic:<slug> label
defermove an epic to a later horizon, commenting the reason; ask whether to keep its open work
promotemove an epic to an earlier horizon, commenting the reason
movemove an issue to another epic, its label, its checklist line and a comment with the reason
closeclose an epic through the gate: every member closed, then its criteria checked against main 🪙
cancelcancel an epic, asking what becomes of each issue outside it that its open members block

rafa epic show

List one epic's issues as the Roadmap table: the epic numbered, or the first now epic not done.

rafa epic show [n] [--labels] [-t|--texts] [--refresh]

Prints one epic's issues as rafa roadmap prints its spec lines, with the spec, blocked by, has and refs columns: the unticked lines of the epic's checklist in its order, then its open members missing from the checklist by number. The epic is the type:epic issue numbered, or, with no number, the current place's epic, the one rafa switch moved to; a place naming a board alone, or no position file, takes the first epic that board (with no position file roadmap.issue, else the lowest-numbered open type:roadmap board, else the open issue titled Roadmap) names that is open, horizon:now and not done, the one rafa next walks into. The table is headed by the epic's title, computed state and done/total, and a stored state disagreeing with the computed one is printed under it. No issue of another epic is named. Each label problem about the epic and each reading that failed is warned about; an unreachable board prints the epic unknown with the reason. With --output=json the epic, its rows, its problems and the warnings are the data of the terminal result event. rafa epics and rafa epic are the same command, typed by the subject alone.

ArgumentTypeWhat it is
nstringThe epic's issue number on the GitHub board. Left out, the current place's epic, else the first now epic on its board that is not done.
FlagType and defaultWhat it does
--labelsbooleanPrint each issue's labels on a row of their own under it; they are no column.
-t, --textsbooleanSpell the spec and blocked by columns in words instead of symbols, with no legend.
--refreshbooleanRead the whole board again instead of only what changed since the listing kept under .rafa/cache/.

Examples:

  • rafa epic show: Prints the issues of the epic rafa next is walking: the first now epic on the Roadmap not done. rafa epics prints the same.
  • rafa epic show 252: Prints the issues of epic #252, whatever its horizon, in its checklist's order, as rafa epics 252 does.
  • rafa epic show 252 --output=json: Writes a result event whose data holds epic #252 as computed, its rows and its label problems.

Output formats: text, json.

rafa epic new

Create an epic: the issue from the epic template, its line on the current board, and in labels mode its epic:<slug> label.

rafa epic new <title> --slug=<string> [--horizon=<string>]

Creates one epic. The label epic:<slug> is created when the repository does not hold it, then the epic issue, titled as given, its body the epic template (acceptance criteria, estimate, date, Owns: and an empty checklist), labelled type:epic, epic:<slug> and horizon:<horizon> in the same command, and last its - [ ] #<n> <title> line on the current board, the one rafa switch moved to, else the default board. The horizon is later unless --horizon says now or next, so a new epic is not picked by the loop unasked. A slug that is not a kebab word (lowercase letters and digits joined by single hyphens) or that an issue already carries as epic:<slug> is refused with exit code 2 before anything is created, as is a board that cannot be read. With --output=json the epic, its labels and the board line are the data of the terminal result event. With board.relationships native an epic is named by its number and title: no slug is needed or read, no epic:<slug> label is created or checked, and the issue is labelled type:epic and horizon:<horizon> only.

ArgumentTypeWhat it is
titlestring, requiredThe epic's title, quoted: one line.
FlagType and defaultWhat it does
--slugstring, requiredThe epic's slug, a kebab word such as sign-in: its members carry epic:<slug>. Required in labels mode; not used with board.relationships native.
--horizonstring, default "later"The epic's horizon: one of now, next, later.

Examples:

  • rafa epic new "Sign-in without passwords" --slug=passwordless: Creates the label epic:passwordless and the epic, horizon later, and adds its line to the current board.
  • rafa epic new "Billing v2" --slug=billing-v2 --horizon=next: Creates the epic with horizon:next.

Output formats: text, json.

rafa epic defer

Move an epic to a later horizon, commenting the reason; ask whether to keep its open work.

rafa epic defer <n> --to=<string> [--reason=<string>]

Swaps the epic's horizon: label for horizon:next or horizon:later in one edit and comments "Moved <from> → <to>: <reason>" on it. The reason is --reason, or asked once where there is a terminal; with neither, nothing changes and the question is printed. When the epic is in progress its members' open branches and pull requests are named and it asks whether to keep them: a no closes each pull request with a comment and deletes no branch, and with no terminal they are kept. An issue that is not an open epic, a target equal to its horizon and one that is not later than it are refused with exit code 2 before anything is written. With --output=json the move, the work and what became of it are the data of the terminal result event.

ArgumentTypeWhat it is
nstring, requiredThe epic's issue number.
FlagType and defaultWhat it does
--tostring, requiredThe horizon it moves to: one of next, later. Required.
--reasonstringWhy it moves, commented on the epic. Asked once when left out.

Examples:

  • rafa epic defer 40 --to=later --reason="waiting on #118": Swaps horizon:now for horizon:later on #40 and comments "Moved now → later: waiting on #118".

Output formats: text, json.

rafa epic promote

Move an epic to an earlier horizon, commenting the reason.

rafa epic promote <n> --to=<string> [--reason=<string>]

Swaps the epic's horizon: label for horizon:now or horizon:next in one edit and comments "Moved <from> → <to>: <reason>" on it. The reason is --reason, or asked once where there is a terminal; with neither, nothing changes and the question is printed. An issue that is not an open epic, a target equal to its horizon and one that is not earlier than it are refused with exit code 2 before anything is written. With --output=json the move is the data of the terminal result event.

ArgumentTypeWhat it is
nstring, requiredThe epic's issue number.
FlagType and defaultWhat it does
--tostring, requiredThe horizon it moves to: one of now, next. Required.
--reasonstringWhy it moves, commented on the epic. Asked once when left out.

Examples:

  • rafa epic promote 40 --to=now --reason="the customer asked": Swaps horizon:next for horizon:now on #40 and comments "Moved next → now: the customer asked".

Output formats: text, json.

rafa epic move

Move an issue to another epic, its label, its checklist line and a comment with the reason.

rafa epic move <issue> --to=<string> [--reason=<string>]

Swaps the issue's epic: label for the target epic's in one edit, moves its checklist line from the old epic's body to the new one's keeping every other byte, and comments "Moved from epic #A to #B: <reason>" on the issue, naming its open branches and pull requests, which stay as they are. The issue is never recreated or closed. The reason is --reason, or asked once where there is a terminal; with neither, nothing changes and the question is printed. An issue with no epic: label, a target that is not an open epic and a move to its own epic are refused with exit code 2 before anything is written. With --output=json the move and what each write did are the data of the terminal result event. With board.relationships set to native it sets the issue's parent to the target epic in one edit instead, and no label or checklist line is read or written; the comment is the same.

ArgumentTypeWhat it is
issuestring, requiredThe number of the issue that moves.
FlagType and defaultWhat it does
--tostring, requiredThe number of the open epic it moves to. Required.
--reasonstringWhy it moves, commented on the issue. Asked once when left out.

Examples:

  • rafa epic move 12 --to=40 --reason="belongs with sign-in": Swaps #12's epic: label for #40's, moves its line into #40's checklist and comments the move on #12.

Output formats: text, json.

rafa epic close

Close an epic through the gate: every member closed, then its criteria checked against main.

rafa epic close <n> [--accept-unchecked]

Refuses with exit code 2, naming them, while any member of the epic is open. Then one session plans a check for each acceptance criterion, or names it uncheckable with a reason, and each check runs as its own session in a detached worktree of origin/main. Any uncheckable criterion refuses the close before a check runs, unless --accept-unchecked is passed; each failed check is filed as a bug, or added as a line to the release's stale-docs issue when its criterion names only documentation, and refuses the close. Otherwise the epic is closed as completed with a comment naming each criterion, and what its members cost, summed from the effort store, is printed beside its estimate. With --output=json the close is the data of the terminal result event.

Spends: 🪙 one verification planning session and one session per check

ArgumentTypeWhat it is
nstring, requiredThe epic's issue number.
FlagType and defaultWhat it does
--accept-uncheckedbooleanClose over the criteria the verification plan could not turn into checks, naming each in the comment.

Examples:

  • rafa epic close 40: Checks #40's criteria against origin/main and closes it when every check passes.
  • rafa epic close 40 --accept-unchecked: The same, closing over any criterion that could not be turned into a check.

Output formats: text, json.

rafa epic cancel

Cancel an epic, asking what becomes of each issue outside it that its open members block.

rafa epic cancel <n> [--reason=<string>]

Lists every open issue outside the epic whose "Blocked by:" line names one of its open members, and asks for each one: move it to another epic (the same move as rafa epic move), unblock it (an "Updated" note below its body, spec:blocked taken off when nothing else it names is open, and a comment), or cancel it (closed as not planned with a comment). Every answer is taken before any write. Then the epic is closed as not planned with a comment naming what became of each issue; an epic closed as not planned already is not closed again. With no terminal and an issue to ask about, the list is printed and nothing changes. An issue that is not an epic, and an epic closed as completed, are refused with exit code 2. With --output=json what each write did is the data of the terminal result event.

ArgumentTypeWhat it is
nstring, requiredThe epic's issue number.
FlagType and defaultWhat it does
--reasonstringWhy the epic is cancelled, written in its closing comment.

Examples:

  • rafa epic cancel 40 --reason="sign-in moved to the platform team": Asks about each issue #40's open members block, then closes #40 as not planned.

Output formats: text, json.