Skip to content

/sdlc:backlog-triage

Generated from solutions/ontological/skills/backlog-triage/SKILL.md.

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. Operates on main; no worktree.

  • Read
  • Edit
  • Bash
  • Glob
  • AskUserQuestion

Usage:

  • /sdlc:backlog-triage — walk every untriaged backlog file.
  • /sdlc:backlog-triage <slug-or-path> — triage just the named file.

Untriaged ≡ frontmatter has no status: field. Any task with status: 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}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 status: 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, a null value deletes a key); the standalone validator is sdlc 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 directly on main. There is no worktree and no PR.

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>*.md then docs/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, exit NO BACKLOG FOUND for "<arg>".

If no argument, list docs/planning/backlog/*.md and read frontmatter from each. Filter to the untriaged set (no status: 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.

For each file in turn:

  1. Read the file. Capture:

    • Frontmatter (current status, 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.
  2. 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.

  3. Ask via AskUserQuestion (4 top-level options):

    • Promote to task — create a task via sdlc task create, then set this backlog to status: promoted/task with result: [[<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 to status: promoted/milestone with result: [[M-<NNNN>-<slug>]]. For larger outcomes spanning multiple tasks.
    • Close — discard without promotion. A second AskUserQuestion asks the reason: abandoned (won’t do / no longer relevant; no result: needed) or duplicate (followed by a plain-text ask for the canonical backlog slug → result: [[<other-slug>]]).
    • Defer — bump last_reviewed: to today; no other change. Use when the item is still relevant but you’re not ready to promote.
  4. 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.

  1. Preview the slug + id task create would 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 json

    The op is read-only: it derives the slug (the same capped deriveSlug create uses with --slug omitted) and reports the would-be id, plus exists/exact_match (an existing task with the identical slug) and similar[] (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 exists is true (or exact_match is non-null), an existing task already carries this exact slug — offer reuse (point this backlog’s result: at exact_match.id and 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.
  2. Create the task. With --slug omitted, create DERIVES the slug from the headline (the same derivation preview-id just 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>" \
    --status planning/draft \
    --impact medium \
    --complexity medium

    Capture the printed path and the minted id. The task’s canonical id is the T-<NNNN> form preview-id reported (and create prints) — the schema-assigned id, not a dated basename. Use it for the result: wikilink. (When the user chose reuse, skip this step and point result: at the existing exact_match.id instead.)

  3. 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 '{"status":"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 --set payload and re-run before committing.

  4. 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>"

Mirror step 3, but:

  • Use sdlc milestone create instead of sdlc task create. The milestone op auto-assigns the next M-<NNNN> id — capture it from the printed path.

  • Update the backlog via sdlc backlog update with status: promoted/milestone and result: [[M-<NNNN>-<slug>]]:

    ${CLAUDE_PLUGIN_ROOT}cli/sdlc backlog update <backlog-path> \
    --set '{"status":"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>" \
--status 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.)

The user picked Close in step 2.3. Ask via AskUserQuestion which reason applies (2 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 against SLUG_RE from ${CLAUDE_PLUGIN_ROOT}lib/util/slug.ts. Confirm the target file exists at docs/planning/backlog/<slug>.md; if not, ask again or offer to abandon instead.

Then update the backlog frontmatter via sdlc backlog update:

  • Abandoned: the schema FORBIDS result: on closed/abandoned, so null-delete any prior result: in the same call.

    ${CLAUDE_PLUGIN_ROOT}cli/sdlc backlog update <backlog-path> \
    --set '{"status":"closed/abandoned","result":null,"last_reviewed":"<today>"}'
  • Duplicate:

    ${CLAUDE_PLUGIN_ROOT}cli/sdlc backlog update <backlog-path> \
    --set '{"status":"closed/duplicate","result":"[[<other-slug>]]","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>"
  1. Update the backlog frontmatter via sdlc backlog update: set last_reviewed: to today. Do not change status:. The op is schema-validated and leaves the body byte-untouched.

    ${CLAUDE_PLUGIN_ROOT}cli/sdlc backlog update <backlog-path> \
    --set '{"last_reviewed":"<today>"}'
  2. 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."

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>, deferred).
  • If anything was committed, hint that the user can git log --oneline to review or git reset HEAD~<n> to undo a contiguous batch.
  • Every promoted backlog file has status: promoted/<entity> AND a canonical result: wikilink ([[T-<NNNN>-<slug>]] for a task, [[M-<NNNN>-<slug>]] for a milestone — never a date-prefixed basename) AND last_reviewed: set to today.
  • Every closed/duplicate backlog file has status: closed/duplicate AND result: [[<other-backlog-slug>]] AND last_reviewed: set to today, and the referenced backlog exists.
  • Every closed/abandoned backlog file has status: closed/abandoned AND last_reviewed: set to today, and NO result: field.
  • 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-new or /sdlc:milestone-new skills — they ask too many follow-up questions for a triage flow. The new artifact lands with sensible defaults (task: status: planning/draft, impact: medium, complexity: medium; milestone: status: open/draft, no version or target_date). The author refines them later via /sdlc:task-define and /sdlc:milestone-new follow-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-define or 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 status: 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 — clearing status and result returns 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; the mktemp + quoted-heredoc + git commit -F form stays documented there only as the no-CLI-available fallback. Stage the exact paths with git add <paths> first — commit create commits the staged index, it does not narrow scope.