/sdlc:backlog-triage
Generated from solutions/ontological/plugin/plugins/sdlc/skills/backlog-triage/backlog-triage.md.
Description
Section titled “Description”Walk untriaged backlog files in docs/planning/backlog/ and decide what to
do with each: promote to task, promote to milestone, defer (bump
last_reviewed), or skip. Promotion calls sdlc task create / sdlc milestone create
directly and updates the backlog frontmatter to point at the new artifact.
Commits each decision atomically on the current branch, honouring the
project’s branching conventions.
Allowed tools
Section titled “Allowed tools”ReadEditBashGlobAskUserQuestion
Source
Section titled “Source”Usage:
/sdlc:backlog-triage— walk every untriaged backlog file./sdlc:backlog-triage <slug-or-path>— triage just the named file.
Untriaged ≡ frontmatter has no state: field. Any task with state: set (whether promoted/* or
closed/*) is considered already triaged and skipped by the no-arg pickup.
Project context (don’t re-derive every run):
- Backlog files live at
docs/planning/backlog/<slug>.md. Schema at${CLAUDE_PLUGIN_ROOT}/lib/model/entities/backlog/schema.ts. - Two usage patterns coexist (see the template head comment). This skill targets
single-candidate files — those representing one idea that may get promoted. Multi-item dump
files (where the body holds many ideas) typically don’t ever get
state:set; for those, this skill offers “defer” (bump last_reviewed) but the per-item promotion is up to the author. - Frontmatter mutations go through
sdlc backlog update <backlog> --set '<json>'(schema-validated before write, body byte-untouched, anullvalue deletes a key); the standalone validator issdlc entities validate. - Promotion writes both a new task/milestone file AND updates the backlog frontmatter. Both go in one commit per decision so the link is atomic.
- Commit each decision on the branch you are already on. Do NOT switch branches, and do NOT assume
mainis committable — a project may require that its main checkout stay clean and that all work happen on a branch or worktree. Read the project’s own conventions before the first commit. If they forbid committing onmainand that is where you are standing, stop and ask the user where to work rather than committing anyway. Whether the run ends in a PR is the caller’s decision, not this skill’s.
1. Survey
Section titled “1. Survey”If an argument was given, resolve it:
- Absolute path → use as-is if the file exists under
docs/planning/backlog/. - Otherwise, glob
docs/planning/backlog/<arg>*.mdthendocs/planning/backlog/*<arg>*.md. If exactly one matches, use it. If multiple match, present the list via AskUserQuestion and let the user pick. If none, exitNO BACKLOG FOUND for "<arg>".
If no argument, list docs/planning/backlog/*.md and read frontmatter from each. Filter to the
untriaged set (no state: field at all). Sort by filename (alphabetical). If the untriaged set is
empty, exit NO UNTRIAGED BACKLOG FILES.
Report at the start: how many files will be triaged, and their slugs.
2. Per-file decision loop
Section titled “2. Per-file decision loop”For each file in turn:
-
Read the file. Capture:
- Frontmatter (current
state,tags,last_reviewed). - Headline (the first
#line in the body, stripped). - Body summary — the first ~200 chars after the headline. If the body is short, show the whole thing.
- Frontmatter (current
-
Present the file to the user as a short text block: slug, headline, current tags, last_reviewed (if set), then the body summary. Keep this concise — three to six lines.
-
Ask via AskUserQuestion (4 top-level options):
- Promote to task — create a task via
sdlc task create, then set this backlog tostate: promoted/taskwithresult: [[<new-task-basename>]]. Default action for items that map to one concrete change. - Promote to milestone — create a milestone via
sdlc milestone create, then set this backlog tostate: promoted/milestonewithresult: [[M-<NNNN>-<slug>]]. For larger outcomes spanning multiple tasks. - Close — discard without promotion, or record that the item shipped without ever being
promoted. A second AskUserQuestion asks the reason:
abandoned(won’t do / no longer relevant; noresult:needed),duplicate(followed by a plain-text ask for the canonical backlog slug →result: [[<other-slug>]]), ordelivered(the idea was built directly, no task/milestone in between — followed by a plain-text ask for the PR link or a short note of what shipped →result: <that text>). - Defer — bump
last_reviewed:to today; no other change. Use when the item is still relevant but you’re not ready to promote.
- Promote to task — create a task via
-
Execute the chosen action (steps 3–6 below). Then continue the loop.
There is no explicit “skip” option. To leave an item undecided, interrupt the session: committed decisions stay and the unprocessed files remain untriaged, resurfacing in the next run.
3. Promote to task
Section titled “3. Promote to task”-
Preview the slug + id
task createwould assign for the backlog headline, and surface any collision so the user can decide whether this backlog is already a task:${CLAUDE_PLUGIN_ROOT}/cli/sdlc task preview-id "<backlog-headline>" --output jsonThe op is read-only: it derives the slug (the same capped
deriveSlugcreateuses with--slugomitted) and reports the would-beid, plusexists/exact_match(an existing task with the identical slug) andsimilar[](existing tasks with a close slug). If the input has no alphanumeric content it exits non-zero (INVALID_INPUT) — that’s the only “empty slug” case; when it happens, ask the user via AskUserQuestion for a different headline to derive from.Then make the load-bearing “is this backlog already a task?” decision via AskUserQuestion, keyed off the preview output:
- If
existsis true (orexact_matchis non-null), an existing task already carries this exact slug — offer reuse (point this backlog’sresult:atexact_match.idand skip creation) vs override (free-form text via Other for a distinct slug to pass as the positional below). - Else if
similar[]is non-empty, surface those neighbors (their ids + slugs) and offer the same reuse-vs-proceed choice. - Else (no exact, no similar) proceed straight to creation — no override needed.
- If
-
Create the task. With
--slugomitted,createDERIVES the slug from the headline (the same derivationpreview-idjust showed), so don’t hand-pass it. Only pass an explicit<slug>positional when the user chose override above:${CLAUDE_PLUGIN_ROOT}/cli/sdlc task create \--headline "<backlog-headline>" \--state planning/draft \--impact medium \--complexity mediumCapture the printed path and the minted id. The task’s canonical id is the
T-<NNNN>formpreview-idreported (andcreateprints) — the schema-assigned id, not a dated basename. Use it for theresult:wikilink. (When the user chose reuse, skip this step and pointresult:at the existingexact_match.idinstead.) -
Update the backlog frontmatter via
sdlc backlog update(schema-validated; the body is left byte-untouched):${CLAUDE_PLUGIN_ROOT}/cli/sdlc backlog update <backlog-path> \--set '{"state":"promoted/task","result":"[[T-<NNNN>-<slug>]]","last_reviewed":"<today>"}'The
result:wikilink MUST be the canonical[[T-<NNNN>-<slug>]]form (the schema-assigned id, NOT the dated basename); the op rejects a date-prefixed shape.<today>is today (UTC, YYYY-MM-DD). The op fails non-zero if the resulting frontmatter does not validate — fix the--setpayload and re-run before committing. -
Commit atomically — only the two files; don’t sweep in unrelated changes. Stage the exact paths, then author the message through
sdlc commit create(subject + body; no shell re-parse of the conventional-commit parens):git add <task-path> <backlog-path>${CLAUDE_PLUGIN_ROOT}/cli/sdlc commit create \--subject "docs(planning): promote backlog/<backlog-slug> to task/<task-basename>" \--body "<backlog-headline>"
4. Promote to milestone
Section titled “4. Promote to milestone”Mirror step 3, but:
-
Use
sdlc milestone createinstead ofsdlc task create. The milestone op auto-assigns the nextM-<NNNN>id — capture it from the printed path. -
Update the backlog via
sdlc backlog updatewithstate: promoted/milestoneandresult: [[M-<NNNN>-<slug>]]:${CLAUDE_PLUGIN_ROOT}/cli/sdlc backlog update <backlog-path> \--set '{"state":"promoted/milestone","result":"[[M-<NNNN>-<slug>]]","last_reviewed":"<today>"}' -
Stage both files and commit through
sdlc commit create:git add <milestone-path> <backlog-path>${CLAUDE_PLUGIN_ROOT}/cli/sdlc commit create \--subject "docs(planning): promote backlog/<backlog-slug> to milestone <M-id>" \--body "<backlog-headline>"
The milestone create call:
${CLAUDE_PLUGIN_ROOT}/cli/sdlc milestone create \ --title "<backlog-headline>" \ --state open/draft(Don’t seed tasks or other fields — the milestone will be fleshed out later by /sdlc:milestone-new
follow-ups or by hand.)
5. Close
Section titled “5. Close”The user picked Close in step 2.3. Ask via AskUserQuestion which reason applies (3 options):
- Abandoned — won’t do, no longer relevant. No
result:needed. - Duplicate — already captured elsewhere. Follow up with a plain-text ask: “Which backlog slug
is this a duplicate of? (just the slug, no
[[...]])”. Validate the response againstSLUG_REfrom${CLAUDE_PLUGIN_ROOT}/lib/util/slug.ts. Confirm the target file exists atdocs/planning/backlog/<slug>.md; if not, ask again or offer to abandon instead. - Delivered — built directly, without ever going through
promoted/taskorpromoted/milestone. Follow up with a plain-text ask: “PR link or short note of what shipped?” →result:carries that text verbatim (a markdown PR link, e.g.[#1880](https://github.com/sksizer/dev/pull/1880), or a short freeform note — no wikilink shape required).
Then update the backlog frontmatter via sdlc backlog update:
-
Abandoned: the schema FORBIDS
result:onclosed/abandoned, so null-delete any priorresult:in the same call.${CLAUDE_PLUGIN_ROOT}/cli/sdlc backlog update <backlog-path> \--set '{"state":"closed/abandoned","result":null,"last_reviewed":"<today>"}' -
Duplicate:
${CLAUDE_PLUGIN_ROOT}/cli/sdlc backlog update <backlog-path> \--set '{"state":"closed/duplicate","result":"[[<other-slug>]]","last_reviewed":"<today>"}' -
Delivered: the schema REQUIRES
result:onclosed/delivered, freeform (not wikilink-shaped).${CLAUDE_PLUGIN_ROOT}/cli/sdlc backlog update <backlog-path> \--set '{"state":"closed/delivered","result":"<PR link or note>","last_reviewed":"<today>"}'
The op is schema-validated; it fails non-zero if the result does not validate. Then commit only that
file through sdlc commit create:
git add <backlog-path>${CLAUDE_PLUGIN_ROOT}/cli/sdlc commit create \ --subject "docs(planning): close backlog/<backlog-slug> as <reason>" \ --body "<one-line note: backlog headline for abandoned; '\"<headline>\" — duplicate of <other-slug>' for duplicate; '\"<headline>\" — delivered in <PR link>' for delivered>"6. Defer
Section titled “6. Defer”-
Update the backlog frontmatter via
sdlc backlog update: setlast_reviewed:to today. Do not changestate:. The op is schema-validated and leaves the body byte-untouched.${CLAUDE_PLUGIN_ROOT}/cli/sdlc backlog update <backlog-path> \--set '{"last_reviewed":"<today>"}' -
Commit only that file through
sdlc commit create:git add <backlog-path>${CLAUDE_PLUGIN_ROOT}/cli/sdlc commit create \--subject "docs(planning): defer backlog/<backlog-slug>" \--body "Reviewed but not yet promotion-worthy."
7. Wrap up
Section titled “7. Wrap up”After the loop finishes (or the user stops it), print a short summary:
- N files considered.
- For each: what happened (
promoted to task/<basename>,promoted to milestone M-<NNNN>,closed/abandoned,closed/duplicate of <slug>,closed/delivered,deferred). - If anything was committed, hint that the user can
git log --onelineto review orgit reset HEAD~<n>to undo a contiguous batch.
Acceptance criteria
Section titled “Acceptance criteria”- Every promoted backlog file has
state: promoted/<entity>AND a canonicalresult:wikilink ([[T-<NNNN>-<slug>]]for a task,[[M-<NNNN>-<slug>]]for a milestone — never a date-prefixed basename) ANDlast_reviewed:set to today. - Every closed/duplicate backlog file has
state: closed/duplicateANDresult: [[<other-backlog-slug>]]ANDlast_reviewed:set to today, and the referenced backlog exists. - Every closed/abandoned backlog file has
state: closed/abandonedANDlast_reviewed:set to today, and NOresult:field. - Every closed/delivered backlog file has
state: closed/deliveredAND a non-emptyresult:(a PR link or short note of what shipped) ANDlast_reviewed:set to today. - Every modified backlog file passes
sdlc entities validate. - Every promotion produced exactly one new artifact file AND one updated backlog file, both in the same commit. Every close updated exactly one backlog file in its commit.
- Deferred backlog files have only their
last_reviewed:changed. - Files left unprocessed by the session are byte-identical to their pre-skill state.
- No file outside
docs/planning/was modified.
- Promotion does NOT invoke the interactive
/sdlc:task-newor/sdlc:milestone-newskills — they ask too many follow-up questions for a triage flow. The new artifact lands with sensible defaults (task:state: planning/draft,impact: medium,complexity: medium; milestone:state: open/draft, no version or target_date). The author refines them later via/sdlc:task-defineand/sdlc:milestone-newfollow-up edits. - The backlog body is NOT carried over into the new task/milestone. The body stays in place on the
backlog file as the origin story; the new artifact starts empty. To seed the body content into the
task, follow up with
/sdlc:task-defineor paste it in by hand. - This skill never changes the body of a backlog file — only frontmatter. Strike-through / deletion of promoted items inside multi-item dump files is the author’s job.
- Backlog files with
state:already set are NOT re-triaged. To re-promote (e.g. you decide the task it pointed at should actually be a milestone), edit the backlog frontmatter manually — clearingstateandresultreturns the file to the untriaged set. - Committing model-generated messages. Each decision’s commit is authored through
sdlc commit create(--subject/--body, or--message -for whole messages on stdin) so the conventional-commit parens never reach a re-parsing shell. See${CLAUDE_PLUGIN_ROOT}/conventions/commit-messages.md— the op is the canonical pattern; themktemp+ quoted-heredoc +git commit -Fform stays documented there only as the no-CLI-available fallback. Stage the exact paths withgit add <paths>first —commit createcommits the staged index, it does not narrow scope.