choose-next
Use when the user wants to pick the next epic to work on from the status dashboard's queue — "/choose-next", "suggest something to do", "pick an epic", "what should I work on next", "give me 3 options
SKILL.mdskills/choose-next/SKILL.md
GitHubOrc's Choice
Overview
A queue accumulates more spec/plan placeholders than any one session can run through. This skill is the "give me three reasonable picks" routine — it reads the dashboard, applies blocking-aware filtering, and proposes a small diverse set rather than a long list. Once picked, it hands off to /project.
The name is a tip of the hat to "orchestrator's choice": the pick it hands over is what the next orchestrator run takes. Pronounce however you like.
When to use
- User invokes
/choose-nextdirectly - User says: "suggest something to do", "pick an epic", "give me 3 options", "what's next?", "what should I work on", "find me work", "I'm idle, what should I run", or similar
- User has just shipped an epic and wants the next one teed up
- User has 5 minutes and wants a small win pre-loaded
When NOT to use
- User asks "what's next?" while there are pending/in-progress items in the current chat's TaskList — suggest THOSE first; this skill is the fallback when the chat queue is empty
- User is mid-conversation about a specific topic and wants to defer — that's
backlog, not this - User wants to browse the whole queue — this skill is for picking ONE thing to do, not enumerating everything; survey the disk directly if a full listing is needed
- The survey turns up nothing (inbox-zero) — say so plainly; don't fabricate suggestions from memory
Procedure
Step 1: Survey queue state from disk
from agentflow.config import load_environment
from agentflow.epic_survey import (
survey_epics, ready_set, blocked_status, blocks_count, is_candidate, handoff_path,
)
env = load_environment()
records = survey_epics(env.data_root, env.mtime_cutoff_iso, include_archived=True)
include_archived=True is load-bearing, not tidiness. Without it survey_epics deletes
archived records at the end of pass 4, before ready_set ever sees them — while _done_slugs
counts lifecycle_state in ("done", "archived"). So an epic whose prerequisite was archived
is never READY, permanently, with nothing logged. Worse than merely hidden: blocked_status
buckets it MISSING-DEP, which reads as this dependency does not exist rather than this
dependency is finished, pointing the reader at a phantom problem. And it is free. include_archived=True adds rows and no measurable time: the archived specs are walked and parsed either way — the flag only decides whether they survive a list comprehension at the very end. Widening the corpus here is a correctness fix with no cost to trade against it.
survey_epics() returns one EpicRecord per slug (the disk replacement for the old work-discovery API). Each record carries at minimum: slug, lifecycle_state, current_state, depends_on, priority, domain, title, source_path, last_modified, progress, paused.
This call is deliberately PROJECT-WIDE — project= is omitted on purpose, not by oversight. The builder loops pass project=<name> so they can only build their own fleet's work; you are a human-facing selection aid, and selection needs the full set — the operator asking "what should I work on next" may well pick an epic from another project on this data_root. Nothing here dispatches: /project is invoked on the chosen spec with a human in the seat, and dispatchable(..., project=<name>) is what keeps a foreign-project record out of an autonomous builder's claim.
If the survey returns 0 records, that's inbox-zero — nothing is pending. It is NOT an error: the survey reads local disk, which can't be "down". Report inbox-zero plainly; do NOT fabricate suggestions from memory.
Step 2: Bucket the epics
Build these sets:
- In-flight:
lifecycle_state == "running"— these are blockers for anything that depends on them (transitively) - Terminal: what
_done_slugscounts (done,archivedand the rest ofTERMINAL_CURRENT_STATES) — the only states that satisfy a dep - Ended boards: an orchestrator run that ended without delivering.
abandonedandfailedwill never land.done_on_holdended with its PR open for a human merge decision: not in flight, not done, and it satisfies nothing until the merge - Unreadable boards:
unrecognised-board-status(the board'sstatus:is outside the vocabulary) orduplicate-status-boards(more than one board for the slug). The survey refuses to guess a run out of either: the epic is not a candidate, satisfies no dep, and a dependant buckets MISSING-DEP. The survey's warning names the board, or every board; the fix is a knownstatus:, or exactly one board left - Candidates:
[r for r in records if is_candidate(r) and not r.paused]— these are what you'll suggest from.is_candidateis the one definitionblocked_statusalso uses:speccing,drafted,approved-for-autonomousandplanning, the four stages before a run. Placeholder states (pinned,parked,reference) are not candidates; do not widen the set by hand. Aspeccingordraftedcandidate has no approval for autonomous builds yet, so Steps 6 and 7 say so wherever it appears
Skip any epic with paused: true (the list above already does).
Step 3: Compute blocked status per candidate
For each candidate, classify its dependency posture with blocked_status(record, records). It
evaluates in this order, first match wins (quoted from its docstring):
* any dep is forbidden (cross-project) → ``MISSING-DEP``
* every dep terminal (``_done_slugs``) → ``READY``
* any dep's board ended undelivered (``abandoned``/``failed``) → ``MISSING-DEP``
* any dep is a running epic → ``NEAR-READY``
* any dep is a candidate (``is_candidate``) → ``CHAINED``
* any dep is unknown (not surveyed) → ``MISSING-DEP``
* otherwise, e.g. a ``done_on_hold`` dep awaiting a human merge → ``CHAINED``
What each bucket tells the operator:
- READY — every dep is terminal.
- NEAR-READY — a dep is running; it unblocks when that epic finishes.
- MISSING-DEP — no dep the survey can resolve, for one of five reasons; name which:
- a cross-project dep, which is unsatisfiable by construction;
- an
abandonedorfailedparent, which will never land.warn_stranded_terminal_dependenciesnames the stranded edge; the fix is to re-plan the parent or re-point the edge, not to wait; - a parent on an unreadable board (
unrecognised-board-statusorduplicate-status-boards), which nothing can say will land. The survey's warning names the board, or every board; the fix is to give it a knownstatus:or leave exactly one board, not to wait; - dep spelled as a file stem; maps to
<slug>(done) — the dep carries a date prefix, a-designsuffix or a.mdthat the record's slug does not, so it never matches. Report the state the mapped record is at; when that is done, the prerequisite is finished and the fix is one frontmatter line, not waiting; - a dep slug that is not surveyed at all, even once normalized, which may be a future epic that does not exist yet.
- CHAINED — deeper queue ahead: a dep is itself a candidate (
is_candidate), or adone_on_holdparent is awaiting a human merge (it unblocks when that PR merges), or a dep sits at another unfinished state.
To tell the file-stem cause from the not-surveyed one, check every candidate's deps, not only the dependency warnings: warn_unresolvable_dependencies looks at dispatchable records only, so it is silent about most candidates.
from agentflow.epic_survey import resolves_after_normalization
known = {r.slug for r in records}
by_slug = {r.slug: r for r in records}
for dep in candidate.depends_on:
if dep not in known:
mapped = resolves_after_normalization(dep, known)
# mapped is None -> not surveyed at all; otherwise a file stem for by_slug[mapped]
The fully-unblocked subset is ready_set(records) — use it to prefer READY candidates.
Also call blocks_count(record, records) for each candidate — how many other surveyed epics that are still to run list this candidate as a dep (a dependent that is terminal or on an ended board is not counted). High count = strategic value (working on it unblocks downstream work).
Step 4: Pick 3 with deliberate diversity
Don't pick 3 candidates of the same flavor. Aim for variety across these axes:
Quick win — a candidate that looks small (read the spec body's
**Tier:**line if present, or estimate from acceptance criteria count). READY status preferred. These ship quickly and clear the queue.Strategic unblocker — high
blocks_countOR a candidate whosedepends_onis small/READY and which itself blocks 2+ downstream epics. Doing this opens up future work. CHAINED is OK if the candidate itself is unblocked.Drift recovery — a candidate whose
last_modifiedis oldest among the candidates (most stale). These tend to fall off the radar; surfacing them prevents permanent rot. READY or NEAR-READY only — don't drag up something with deep chained deps.
If the candidate set has fewer than 3 items: present what's there with a note. Don't pad with archived/done epics.
If two of the three "best" candidates are the same item (e.g., a small quick-win that also unblocks a lot): pick that one + two distinct alternatives, don't repeat.
Step 5: Read each picked candidate's spec or plan to enrich the suggestion
For each of the 3 picks:
- Locate the file with
handoff_path(record, env.data_root). It returnsrecord.source_path— the spec or plan the survey read, which is a plan for a plan-only record — and only when that file has moved does it fall back to the record's own<data_root>/projects/<name>/design/{specs,plans}/, returning a file there only when exactly one stem normalizes to the slug. It returns None rather than guess: for a path outsidedata_root, a record with no plainproject:name, two matching files, or none. Do not rebuild the path fromprojectandslugyourself: a plan-only record has no spec, a record with noproject:has no<name>, and a-designfilename does not end in the slug - Read the first ~40 lines (frontmatter + Problem + Goal sections)
- Extract: tier hint, problem one-liner, key context
This gives the user enough to make a real choice without re-reading specs themselves.
Step 6: Present 3 options via AskUserQuestion
Use AskUserQuestion with one question and 3 options. Each option's label is the slug; each description is one short line stating: tier, blocking status, lifecycle state, and the why-pick-this rationale. For a speccing or drafted pick, write the state as drafted - not yet approved for autonomous builds (or speccing - ...), so the human sees the approval gap before choosing.
Format example:
- Label:
aggregator-peer-federation-resilience - Description:
small (single-function fix, READY, approved-for-autonomous). Quick win — federation timeout fix shippable in ~30 min, unblocks no one but cleans up dashboard noise.
The 4th "Other" option is auto-included by AskUserQuestion — lets user say "show me different ones" or pick an epic the skill didn't suggest.
Step 7: On selection, invoke /project
After the user picks one of the 3:
- Use the path
handoff_pathreturned in Step 5 (the same file you read there), spec or plan - If the pick is
speccingordrafted, say so in one line when handing off: "note:<slug>is drafted - not yet approved for autonomous builds; /project will plan it, but no builder may claim it until a human approves it." - Invoke
/project <path>via the Skill tool withskill: project, args: <path>;/projectroutes a spec path and a plan path alike - This routes the chosen epic into the brainstorm-or-plan-then-orchestrator pipeline
If the user picks "Other" with a custom slug:
- Look up that slug in the survey records
- If found: take
handoff_path(record, env.data_root), invoke/project <path>; if it returns None, tell the user that epic can't be handed off (see thehandoff_pathNone row under Failure modes) and ask for another pick - If not found: ask the user to clarify (slug typo? new placeholder needed via
backlogfirst?)
If the user picks "Other" asking for different filtering (e.g., "show me speccing only" / "show me only blockers" / "give me a bigger one"): re-run Steps 4-6 with the requested filter. Don't iterate forever — if a third re-roll happens, ask the user to just pick one.
Examples
Example 1: routine "what's next" with empty TaskList
User: "What's next?"
You first check TaskList — empty (or all completed). Fall through to this skill.
You survey the disk, find 5 candidates (2 speccing, 1 drafted, 2 planning). Pick 3 with diversity:
aggregator-peer-federation-resilience(small, READY, quick win)platform-cutover-pre-flight-blockers(medium, READY, blocks 1 other epic — strategic)production-bug-fixes-plan(planning state, last_modified 4 days ago, drift recovery)
Present via AskUserQuestion. User picks the first. You invoke /project <data_root>/projects/<name>/design/specs/2026-05-10-aggregator-peer-federation-resilience.md.
Example 2: blocked-heavy queue
User: "/choose-next"
You survey, find 8 candidates but 5 of them depend on platform-cutover-pre-flight-blockers which is itself a candidate (not in-flight). Most candidates are CHAINED.
Pick:
platform-cutover-pre-flight-blockers(READY, strategic — unblocks 5 downstream epics)pythonw-silent-death-fix(small, READY, quick win, unrelated chain)polish-round-post-cutover-verify(CHAINED via cutover-blockers, small, drift recovery)
Present with rationale visible. User likely picks the first to unblock the queue.
Example 3: "what's next" with active TaskList
User: "What's next?"
You check TaskList — there's a pending task "Restart the app after cutover-blocker fix". Suggest THAT inline, do NOT invoke this skill:
"You've got a pending task in this session: 'Restart the app after cutover-blocker fix.' Want to do that, or look at the dashboard for something else?"
If user picks the dashboard option, then invoke this skill.
Example 4: empty candidate pool
User: "Pick an epic"
You survey, find 0 candidates (everything is done or running). Surface plainly:
"Nothing pending — 0 speccing, 0 drafted, 0 approved, 0 planning. The 3 in-flight epics are: ... You're either at inbox-zero or behind the orchestrator catching up. Want to pin something new with
/backlog?"
Don't invent suggestions.
Constraints
- Always read live survey data — never suggest from memory or recent conversation context. Memory is stale; the disk survey is current.
- Diversity is non-negotiable — three quick wins is a worse suggestion set than one quick win + one strategic + one drift recovery. The whole value of the skill is the contrast.
- Honor the "what's next?" routing rule — check TaskList first when that phrase is used. The user explicitly asked for this routing; respect it.
- Don't auto-pick — even if one candidate is obviously best, present 3 and let the user choose. The user's judgment about what they want to do right now beats your scoring.
- Don't bundle picks — if the user picks one, hand it off to
/project. Don't try to set up a chain of multiple/projectinvocations across the picks. - Stop at one re-roll if filter is requested — open-ended "show me different ones" loops are a footgun.
- Empty means inbox-zero — the disk survey can't be "down", so zero records unambiguously means nothing is pending; there is no broken-vs-empty ambiguity to resolve. Surface inbox-zero clearly, with no health probe to tell "broken" from "empty".
Failure modes
| Failure | Action |
|---|---|
| Survey returns 0 records | Inbox-zero — report "nothing pending — pin something or queue planning." (The disk survey can't be "down", so empty unambiguously means inbox-zero; if you suspect a mis-config, confirm data_root points at the right tree rather than probing a health endpoint.) |
| All candidates are CHAINED behind one stuck dep | Suggest the dep itself as #1 (strategic unblocker). Note explicitly that the rest of the queue waits on it. |
handoff_path returns None for a pick |
Skip that candidate, pick another. It returns None rather than guess: for a path outside data_root, a record with no plain project: name, two matching files, or none. Note in the final summary: "couldn't hand off <slug> — no single spec or plan for it inside data_root." |
| User picks "Other" without specifying | Ask once: "different filter, or a specific slug?" Don't loop. |
| User picks an epic whose deps aren't met | /project will accept it anyway; the orchestrator's gate #2 will surface the issue. Add a one-line warning when handing off: "note: this epic's dep <slug> isn't done yet — orchestrator may flag it." |