Walk untriaged backlog files in docs/planning/backlog/ and decide what to do with each: promote to task, promote to milestone, close, defer (bump last_reviewed), or leave it for an open PR. Before each decision, checks the item against main: the code it names, merged and open PRs, and existing planning entities, and recommends an action with quoted evidence. Promotion calls `sdlc task create` / `sdlc milestone create` directly, or points at existing work that already covers the item, and updates the backlog frontmatter to match. Commits each decision atomically on the current branch, honouring the project's branching conventions.
Resources
1Install
npx skillscat add osdlc/osdlc/backlog-triage Install via the SkillsCat registry.
$sdlc:backlog-triage
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/* orclosed/*) 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/<basename>.md, where the basename isB-<id>-<slug>. 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 getstate:set; for those, this
skill offers "defer" (bump last_reviewed) but the per-item promotion is up to the author. - A backlog file records an idea at the time it was written. The code may have changed since. Never
decide from the backlog text alone; step 2 checks each item against main first. - 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
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. Check against main
Check every item before deciding it. Fetch first so the check sees current main:
git fetch -q origin mainFor each item:
Code. Find the files, functions, commands or config the item names on
origin/main
(git show origin/main:<path>,git grep <term> origin/main). Quote the current lines that
show whether the problem still exists. If a path is gone, follow the rename
(git log --follow --diff-filter=R origin/main -- <path>) and quote the new location.PRs. Search merged and open PRs for the item's key terms:
gh pr list --state merged --search "<terms>" --limit 20 gh pr list --state open --search "<terms>" --limit 20Commits. Search history since the backlog file was created (its
created:field, or the
date of the commit that added it):git log origin/main --since <created> --oneline --grep "<term>" git log origin/main --since <created> --oneline -S "<identifier>"Planning. Search
docs/planning/for tasks, milestones, decisions and other backlog entries
that already cover the item. Match on content — the problem and the code it touches — not on
slug or headline alone.
Pick one verdict per item from its evidence:
| Verdict | Meaning |
|---|---|
| delivered | Main already does what the item asks; cite the PR or commit. |
| abandoned | The item no longer applies (the code it names is gone, or moot). |
| duplicate/existing-work | A task, milestone or backlog entry already covers it; name it. |
| in-progress | An open PR addresses it; name the PR. |
| promote | The problem still exists on main and nothing covers it. |
| defer | Still real, but not ready to act on. |
| unsure | The evidence does not settle it; say what is missing. |
A promote verdict needs quoted current lines from main that show the problem exists. Without
them, the verdict is unsure.
Record what the check found that changes the item's scope: moved paths, renamed things, and parts
already shipped. Step 4 writes this into the new artifact.
Batch mode
For a run over more than a few files, do the check in read-only subagents before asking the user
anything. Give each subagent a few items. Each subagent runs the four checks above and returns, per
item: the verdict, the quoted evidence from main for it, any PR or entity it names, and the scope
notes. A verdict without quoted evidence from main counts as unsure. Subagents never edit files
or commit.
Then present the decisions grouped by verdict, one line of evidence per item, and confirm each
group with the user before step 3 applies it.
3. 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. Then show the step 2 findings: the quoted evidence, the PRs and
entities found, and the recommended action with the reason for it.If the verdict is in-progress, ask via AskUserQuestion: Leave for the open PR
(recommended) or Triage now. Leaving it means no edit and no commit; the backlog stays
untriaged, and the PR that closes it can update it without a clash. Name the PR in the wrap-up.
On Triage now, continue to the next sub-step.Ask via AskUserQuestion (4 top-level options), marking the recommended one:
- Promote to task — point this backlog at a task: an existing one that already covers the
item, or a new one viasdlc task create. Setsstate: promoted/taskwithresult: [[<task-id>]]. Default action for items that map to one concrete change. - Promote to milestone — point this backlog at a milestone: an existing one, or a new one
viasdlc milestone create. Setsstate: promoted/milestonewithresult: [[<milestone-id>]]. 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(another backlog entry covers it →result: [[<other-backlog-basename>]]), ordelivered(the idea was built directly, no
task/milestone in between →result:is the PR link or a short note of what shipped). - 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 — point this backlog at a task: an existing one that already covers the
Execute the chosen action (steps 4–7 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.
4. Promote to task
Decide whether a task already covers this backlog. Start from the step 2 planning check. Then
preview the slug + idtask createwould assign for the backlog headline, and surface any
collision:scripts/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:
- If step 2 found an existing task that covers the item by content, offer reuse of that
task. This is the usual case; slug matching alone misses it. - 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 match of any kind) proceed straight to creation — no override needed.
- If step 2 found an existing task that covers the item by content, offer reuse of that
Create the task, unless the user chose reuse. 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:scripts/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-<id>formpreview-idreported (andcreateprints) — the schema-assigned id, not a dated basename. Use
it for theresult:wikilink.If step 2 found moved paths, renamed things, or parts already shipped, write the current scope
into the new task body so the next reader does not redo the check. Fill the sections it informs
(usually## Todayand## Proposed), replacing each filled section's whole<...>template
placeholder. Cite the quoted lines and the PRs found. Leave other sections as the template made
them. Skip this sub-step on reuse; the existing task is not edited.Update the backlog frontmatter via
sdlc backlog update(schema-validated; the body is left
byte-untouched):scripts/sdlc backlog update <backlog-path> \ --set '{"state":"promoted/task","result":"[[T-<id>-<slug>]]","last_reviewed":"<today>"}'The schema accepts
[[T-<id>]]or[[T-<id>-<slug>]], with a 4-character base-36 id (for
example[[T-77TR-learning-p8-retire-augmented]]). It rejects a date-prefixed basename. On
reuse, write the existing task's id the same way.<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 touched files; don't sweep in unrelated changes. Stage the exact
paths (the backlog alone on reuse), then author the message throughsdlc commit create
(subject + body; no shell re-parse of the conventional-commit parens):git add <task-path> <backlog-path> scripts/sdlc commit create \ --subject "docs(planning): promote backlog/<backlog-slug> to task/<task-basename>" \ --body "<backlog-headline>"
5. Promote to milestone
Mirror step 4, but:
If step 2 found an existing milestone that covers the item, offer reuse of it and skip
creation. Otherwise usesdlc milestone createinstead ofsdlc task create. The milestone op
auto-assigns the nextM-<id>— capture it from the printed path.Write the current scope into the new milestone body the same way as step 4.3 (usually its
## Goal).Update the backlog via
sdlc backlog updatewithstate: promoted/milestoneand a milestone
wikilink. The schema accepts[[M-<id>]]or[[M-<id>-<slug>]](for example[[M-DCPV]]):scripts/sdlc backlog update <backlog-path> \ --set '{"state":"promoted/milestone","result":"[[M-<id>-<slug>]]","last_reviewed":"<today>"}'Stage the touched files and commit through
sdlc commit create:git add <milestone-path> <backlog-path> scripts/sdlc commit create \ --subject "docs(planning): promote backlog/<backlog-slug> to milestone <M-id>" \ --body "<backlog-headline>"
The milestone create call:
scripts/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.)
6. Close
The user picked Close in step 3.4. Ask via AskUserQuestion which reason applies (3 options):
- Abandoned — won't do, no longer relevant. No
result:needed. - Duplicate — another backlog entry already captures it. Use the entry step 2 found, or ask in
plain text: "Which backlog entry is this a duplicate of? (its basename, no[[...]])". Confirm
the target file exists atdocs/planning/backlog/<basename>.md; if not, ask again or offer to
abandon instead. When a task or milestone covers the item, promote to it (step 4 or 5) instead;closed/duplicateaccepts only a backlog wikilink. - Delivered — built directly, without ever going through
promoted/taskorpromoted/milestone. Use the PR or commit step 2 found, or ask in plain text: "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.scripts/sdlc backlog update <backlog-path> \ --set '{"state":"closed/abandoned","result":null,"last_reviewed":"<today>"}'Duplicate: the schema requires a backlog wikilink,
[[B-<id>]]or[[B-<id>-<slug>]].scripts/sdlc backlog update <backlog-path> \ --set '{"state":"closed/duplicate","result":"[[<other-backlog-basename>]]","last_reviewed":"<today>"}'Delivered: the schema REQUIRES
result:onclosed/delivered, freeform (not
wikilink-shaped).scripts/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>
scripts/sdlc commit create \
--subject "docs(planning): close backlog/<backlog-slug> as <reason>" \
--body "<one-line note: backlog headline for abandoned; '\"<headline>\" — duplicate of <other-basename>' for duplicate; '\"<headline>\" — delivered in <PR link>' for delivered>"7. 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.scripts/sdlc backlog update <backlog-path> \ --set '{"last_reviewed":"<today>"}'Commit only that file through
sdlc commit create:git add <backlog-path> scripts/sdlc commit create \ --subject "docs(planning): defer backlog/<backlog-slug>" \ --body "Reviewed but not yet promotion-worthy."
8. Wrap up
After the loop finishes (or the user stops it), print a short summary:
- N files considered.
- For each: the verdict from step 2, what happened (
promoted to task/<basename>orpromoted to existing task <id>,promoted to milestone M-<id>orpromoted to existing milestone <id>,closed/abandoned,closed/duplicate of <basename>,closed/delivered,deferred,left for <PR link>), and the one piece of evidence it rests
on. - Items left for an open PR, each with its PR link, so someone can triage them after the PR merges.
- Items still marked unsure, with what the check could not settle.
- If anything was committed, hint that the user can
git log --onelineto review orgit reset HEAD~<n>to undo a contiguous batch.
Acceptance criteria
- Every decided item was checked against a freshly fetched
origin/mainfirst, and its
recommendation was shown with that evidence. - Every promotion to a new task or milestone rests on quoted current lines from main that show the
problem still exists. - Every promoted backlog file has
state: promoted/<entity>AND a canonicalresult:wikilink
([[T-<id>]]or[[T-<id>-<slug>]]for a task,[[M-<id>]]or[[M-<id>-<slug>]]for a
milestone — never a date-prefixed basename) ANDlast_reviewed:set to today. The wikilink
names a new artifact or existing work that covers the item. - When the check found moved paths, renamed things, or shipped parts, the new task or milestone
body records the current scope in place of the template placeholders it filled. - Every closed/duplicate backlog file has
state: closed/duplicateANDresult: [[<other-backlog-basename>]]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 to a new artifact produced exactly one new artifact file AND one updated backlog
file, both in the same commit. Every promotion to existing work and every close updated exactly
one backlog file in its commit. - Deferred backlog files have only their
last_reviewed:changed. - Items left for an open PR, and files left unprocessed by the session, are byte-identical to their
pre-skill state, and the wrap-up names the PR for each item left for one. - No file outside
docs/planning/was modified.
Notes
- 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 copied into the new task/milestone. The body stays in place on the
backlog file as the origin story. The new artifact carries only the current scope from step 2,
when the check changed it; follow up with$sdlc:task-definefor the rest. - 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 create
commits the staged index, it does not narrow scope.