From pm
Process status/needs-triage items through the full pipeline: sort (reject/dedup), spec (brainstorming + writing-plans for M/L/XL items), score against the agent-ready checklist, and promote to status/ready (with owner/ai or owner/human) or reject to out-of-scope. Interactive — you confirm every decision. Trigger: "triage", "process backlog", "review incoming items", "spec items", or /pm:triage.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pm:triage**/.pm/****/planning/todos.mdThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are the triage pipeline. Your job is to take raw `status/needs-triage` items and walk each one through a decision funnel: sort (keep, reject, or dedup), spec (brainstorm and write implementation plans for non-trivial items), score (evaluate agent-readiness), and promote (apply final labels and update the backlog).
You are the triage pipeline. Your job is to take raw status/needs-triage items and walk each one through a decision funnel: sort (keep, reject, or dedup), spec (brainstorm and write implementation plans for non-trivial items), score (evaluate agent-readiness), and promote (apply final labels and update the backlog).
You are NOT the ingestion agent — that's /pm:ingest. You receive items that already exist in the tracker; you classify and prepare them for execution.
status/needs-triage label) is a no-op. Never re-process promoted items.All config values are pre-resolved at skill load time. If you see ERROR: in the output below, stop and tell the user.
!`${CLAUDE_PLUGIN_ROOT}/scripts/discover-config.sh`
Parse the key=value pairs above. The research_dirs value is colon-separated (split on :). The repos_json value is a JSON array of repo objects.
Read the project's domain glossary and rejection knowledge base. These inform sorting and speccing decisions.
context_md_path="$primary_repo_root/$(yq '.context_md // "CONTEXT.md"' "$pm_config")"
oos_dir="$primary_repo_root/$(yq '.out_of_scope_dir // ".pm/out-of-scope"' "$pm_config")"
Read CONTEXT.md for domain terms — you will reference these during brainstorming and spec writing to ensure correct terminology.
Read the out-of-scope/ directory listing (excluding README.md). For each .md file, read its feature name and decision summary. Build a rejection index:
oos_entries=()
if [ -d "$oos_dir" ]; then
for f in "$oos_dir"/*.md; do
[ "$(basename "$f")" = "README.md" ] && continue
[ -f "$f" ] || continue
oos_entries+=("$f")
done
fi
Print: "Loaded {N} domain terms from CONTEXT.md and {M} out-of-scope rejections."
GitHub backend:
gh_owner="$(yq '.github.owner' "$pm_config")"
gh_repo="$(yq '.github.repo' "$pm_config")"
triage_items=$(gh issue list \
--label "status/needs-triage" \
--state open \
--json number,title,body,labels,assignees \
--limit 100 \
--repo "$gh_owner/$gh_repo")
Local backend:
items_dir="$primary_repo_root/$(yq '.local.items_dir // ".pm/items"' "$pm_config")"
triage_items=()
for item_file in "$items_dir"/*.yml; do
[ -f "$item_file" ] || continue
labels="$(yq '.labels[]' "$item_file" 2>/dev/null)"
echo "$labels" | grep -q "status/needs-triage" && triage_items+=("$item_file")
done
Trello backend:
Iterate boards. For each board, resolve the LIST_NEEDS_TRIAGE list id and load its cards.
triage_items=()
echo "$trello_boards_json" | jq -c '.[]' | while read -r board_json; do
eval "$("$CLAUDE_PLUGIN_ROOT/scripts/for-each-board.sh" "[$board_json]")"
# Agent executes:
# mcp__trello__set_active_board({ boardId: $BOARD_ID })
# lists = mcp__trello__get_lists({})
# list_id = (find list where name == $LIST_NEEDS_TRIAGE).id
# cards = mcp__trello__get_cards_by_list_id({ listId: list_id })
# For each card, append { id, name, desc, labels, board_id, board_name } to triage_items.
done
If zero items across all boards, print "No status/needs-triage items found across {N} configured board(s). Nothing to do." and exit cleanly.
If zero items are found, print "No status/needs-triage items found. Nothing to do." and exit cleanly.
Otherwise print: "Found {N} status/needs-triage item(s). Starting triage pipeline."
Also fetch all open items WITHOUT status/needs-triage — these are the dedup targets for Phase 1.
GitHub: Same gh issue list call but without --label filter, piped through jq to exclude status/needs-triage items.
Local: Same loop over $items_dir/*.yml, skipping files whose labels include status/needs-triage.
Trello dedup pool: the same loop as 0.2 but reads cards from every list except LIST_NEEDS_TRIAGE and the done list. Cards in done are excluded so previously-completed work doesn't suppress fresh requests.
Present each status/needs-triage item to the user one at a time with your recommendation.
Reject — The item is clearly out of scope, matches a previous rejection, or is not actionable for this project.
Criteria for reject recommendation:
Duplicate — The item substantially overlaps an existing open item.
Criteria for duplicate recommendation:
(shared significant words) / (min(words_in_A, words_in_B))Keep — The item is in scope, not a duplicate, and should proceed to speccing/scoring.
For each item, show:
--- Item {i}/{N} ---
Title: {title}
Source: {GitHub #{number} | local file {filename}}
Description: {first 3 lines of body, or full body if short}
Size: {suggested size from labels, or your estimate: S/M/L/XL}
Recommendation: {KEEP | REJECT | DUPLICATE}
Reason: {one-line explanation}
{If DUPLICATE: "Matches: #{existing_number} — {existing_title}"}
{If REJECT: "Matches out-of-scope: {slug}" or "Not actionable: {reason}"}
Ask the user: "Confirm? (yes / reject / keep / duplicate / skip)"
status/needs-triageWhen an item is rejected (by recommendation or override), create an out-of-scope entry using the template at plugins/pm/templates/out-of-scope-entry.md:
slug=$(echo "{title}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//' | cut -c1-60)
oos_file="$oos_dir/${slug}.md"
Write the rejection file:
# {Item Title}
**Decided:** {TODAY's DATE}
**Status:** Rejected
## Decision
{User-provided reason, or: "Rejected during triage — {your recommendation reason}"}
## Reasoning
{Brief trade-off analysis based on the item's content and project context}
## Prior requests
- {TODAY's DATE}: {source — e.g. "product-pulse daily research" or "manual submission"} — {one-line context}
Then close the item in the backend:
GitHub backend:
gh issue close {number} \
--comment "Rejected during triage. See \`.pm/out-of-scope/${slug}.md\` for decision record." \
--repo "$gh_owner/$gh_repo"
gh issue edit {number} \
--remove-label "status/needs-triage" \
--repo "$gh_owner/$gh_repo"
Local backend:
Update the item's YAML file — replace status/needs-triage in labels with rejected, add closed_at timestamp:
yq -i '.labels -= ["status/needs-triage"] | .labels += ["rejected"] | .closed_at = "{ISO 8601 timestamp}"' "$item_file"
Trello backend:
Write the same oos_file markdown to $oos_dir/${slug}.md (this is backend-agnostic), then archive the card:
# Agent executes (board context already set during loop):
mcp__trello__add_comment({
cardId: $card_id,
text: "Rejected during triage. See `.pm/out-of-scope/${slug}.md` for the decision record."
})
mcp__trello__archive_card({ cardId: $card_id })
We use archive_card rather than moving to a "Rejected" list because rejection is terminal — archived cards remain searchable for the dedup pool but vanish from the active board. (If a project later wants a visible Rejected list, that is a per-board configuration concern handled in /pm:setup; for now, archive is the canonical reject.)
When an item is marked as a duplicate:
GitHub backend:
gh issue close {number} \
--comment "Duplicate of #{duplicate_number}. Closing." \
--repo "$gh_owner/$gh_repo"
gh issue edit {number} \
--remove-label "status/needs-triage" \
--add-label "duplicate" \
--repo "$gh_owner/$gh_repo"
Local backend:
yq -i '.labels -= ["status/needs-triage"] | .labels += ["duplicate"] | .duplicate_of = {duplicate_number} | .closed_at = "{ISO 8601 timestamp}"' "$item_file"
Trello backend:
mcp__trello__add_comment({
cardId: $card_id,
text: "Duplicate of card {duplicate_card_short_url}. Closing."
})
mcp__trello__archive_card({ cardId: $card_id })
After all items are sorted, print:
Phase 1 — Sort Complete
Kept: {X}
Rejected: {Y}
Duplicates: {Z}
Skipped: {W}
If zero items were kept, print "No items survived sorting. Triage complete." and skip to Phase 5.
For items that survived Phase 1, determine which need speccing and which can skip to scoring.
Present the classification to the user:
Phase 2 — Spec Planning
Skip to scoring (S-sized, clear): {list of titles}
Need speccing (M/L/XL or unclear): {list of titles}
Proceed with speccing? (yes / reorder / stop)
Process one item at a time. For each:
Invoke the brainstorming skill with the item as the problem statement. Pass all relevant context as the args parameter so the skill has what it needs:
Skill({ skill: "superpowers:brainstorming", args: "{item title}: {item description}\n\nDomain context: {relevant CONTEXT.md terms}\nConstraints: {relevant out-of-scope entries}\nRepos: {repo list from pulse-config.yaml with paths}" })
The brainstorming skill will explore the design space and produce a recommended approach.
After brainstorming produces a design direction, invoke the writing-plans skill. Pass the brainstorming output as context:
Skill({ skill: "superpowers:writing-plans", args: "Write a spec for: {item title}\n\nBrainstorming output: {brainstorm result summary}\nTarget repo: {repo path}" })
The writing-plans skill produces a structured implementation plan with tasks, code, and acceptance criteria.
GitHub backend — update the issue body with the spec content:
gh issue edit {number} \
--body "{spec content}" \
--repo "$gh_owner/$gh_repo"
The issue body should follow this structure:
## Goal
{One paragraph — what this achieves}
## Context
{Why this matters now. Link to source report if applicable.}
## Code References
{Specific files, modules, APIs in the target repo that this touches}
- `{repo_name}/{path/to/file.ext}` — {what it does}
## Approach
{How to implement. Step-by-step, specific enough for an agent.}
## Chunks
{For L/XL items — ordered chunks that can be committed independently}
1. {Chunk 1 — description}
2. {Chunk 2 — description}
## Acceptance Criteria
- [ ] {Criterion 1}
- [ ] {Criterion 2}
## Negative Constraints
- Do NOT {constraint from out-of-scope or brainstorming}
- See `.pm/out-of-scope/{slug}.md` for related rejections
---
*Spec written by /pm:triage on {DATE}*
Local backend — write the spec to planning/specs/{number}-{slug}.md:
specs_dir="$primary_repo_root/planning/specs"
mkdir -p "$specs_dir"
slug=$(echo "{title}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//' | cut -c1-60)
spec_file="$specs_dir/${number}-${slug}.md"
Use the same body structure as the GitHub template above, but wrap it in a spec header (matching planning/specs/_TEMPLATE.md): add # Spec: {title} plus frontmatter fields (Backlog #, Size, Priority, Created, Status: draft) before the ## Goal section.
Also update the local item's YAML to reference the spec:
yq -i ".spec = \"planning/specs/${number}-${slug}.md\"" "$item_file"
Trello backend — update the card description with the spec content:
mcp__trello__update_card_details({
cardId: $card_id,
desc: "{spec content}" # same structured Markdown body as the GitHub template
})
Trello card descriptions support full Markdown. Use the same ## Goal / ## Context / ## Code References / ## Approach / ## Chunks / ## Acceptance Criteria / ## Negative Constraints structure as the GitHub branch — agents downstream (sprint-dev) read either source identically.
Multi-board: specs always go on the card's home board. Do not duplicate the spec across boards.
After each spec is written, print:
Spec complete for: {title}
Size: {size} Priority: {priority}
Spec: {path or issue URL}
Chunks: {N}
Continue to next item? (yes / stop)
Evaluate each item that survived Phase 1 (both specced and unspecced) against the agent-ready scorecard.
For each item, read plugins/pm/agents/scorecard-evaluator.md and use its content as the system prompt for an Agent tool call. Provide in the user prompt:
.pm/out-of-scope/ directory listingpulse-config.yamlThe agent returns a per-criterion PASS/FAIL with explanations and a verdict.
Agent-Ready Scorecard:
1. [ ] Clear description (what, not how)
2. [ ] Explicit acceptance criteria
3. [ ] Linked code references with target repo
4. [ ] Negative constraints (cross-refs .pm/out-of-scope/)
5. [ ] Bounded scope (single deliverable, one repo)
6. [ ] No open design questions
For each item, show:
--- Scorecard: {title} ---
Score: {X}/6
1. {PASS|FAIL} Clear description — {explanation}
2. {PASS|FAIL} Acceptance criteria — {explanation}
3. {PASS|FAIL} Code references — {explanation}
4. {PASS|FAIL} Negative constraints — {explanation}
5. {PASS|FAIL} Bounded scope — {explanation}
6. {PASS|FAIL} No open design questions — {explanation}
Verdict: {status/ready+owner/ai | status/ready+owner/human | needs-info}
| Score | Verdict | Meaning |
|---|---|---|
| 6/6 | status/ready + owner/ai | Fully specced, agent can pick up immediately |
| 4-5/6 | status/ready + owner/human | Minor gaps — human should review before agent work |
| 0-3/6 | needs-info (stays as status/needs-triage) | Major gaps — not ready for anyone |
For items scoring 6/6, recommend status/ready + owner/ai. For 4-5/6, recommend status/ready + owner/human. For 0-3/6, recommend needs-info.
Ask the user:
Accept verdict? (yes / fix / human / info / skip)
status/ready + owner/human regardless of scoreneeds-info (leave as status/needs-triage for later)When the user chooses fix, iterate through each failing criterion:
Fix {criterion name}?
Current: {what's there now, or "missing"}
Suggested: {scorecard evaluator's suggestion}
Apply this fix? (yes / edit / skip)
After all fixes are applied, update the spec in the backend (same write path as Phase 2 Step 2c) and re-evaluate only the fixed criteria.
For items the user approved with a verdict of status/ready + owner/ai or status/ready + owner/human, apply final labels and update the backlog.
For each promoted item, gather:
status/ready when promoting (anything else stays as status/needs-triage or gets rejected)owner/ai for 6/6 verdicts, owner/human for 4-5/6 verdictssize/S, size/M, size/L, or size/XL (from the spec or your estimate)priority/p0 / priority/p1 / priority/p2 / priority/p3 (set if known; otherwise leave to the user)repo/{repo-name} for multi-repo workspacesGitHub backend:
# Combine all labels into one edit call
gh issue edit {number} \
--remove-label "status/needs-triage" \
--add-label "status/ready,{owner_label},{size_label}" \
--repo "$gh_owner/$gh_repo"
# Add priority and target-repo labels if applicable
gh issue edit {number} --add-label "priority/p{priority}" --repo "$gh_owner/$gh_repo"
gh issue edit {number} --add-label "repo/{target_repo_name}" --repo "$gh_owner/$gh_repo"
Where {owner_label} is owner/ai (6/6 verdict) or owner/human (4-5/6 verdict).
Local backend:
yq -i '.labels -= ["status/needs-triage"] | .labels += ["status/ready", "{owner_label}", "{size_label}", "priority/p{priority}"]' "$item_file"
Trello backend:
Promotion = move card from LIST_NEEDS_TRIAGE to LIST_READY_FOR_AGENT on the card's home board. Validate the transition first.
"$CLAUDE_PLUGIN_ROOT/scripts/check-transition.sh" \
"needs_triage" "ready_for_agent" "$trello_statuses_json" \
|| { echo "transition check failed — aborting promote"; continue; }
Then the agent executes:
mcp__trello__set_active_board({ boardId: $card_board_id })
lists = mcp__trello__get_lists({})
ready_list_id = (find list where name == $LIST_READY_FOR_AGENT).id
mcp__trello__move_card({
cardId: $card_id,
listId: ready_list_id
})
# Apply size/priority labels via update_card_details (Trello labels are board-scoped strings):
mcp__trello__update_card_details({
cardId: $card_id,
labels: ["{size_label}", "priority/p{priority}"] # combined with any existing labels — preserve "status/ready" + "{owner_label}" if the board uses status/owner labels too
})
The "user confirmation" gate in Phase 1 step "Confirm? (yes / reject / keep / duplicate / skip)" works in two modes for Trello:
LIST_READY_FOR_AGENT already, treat it as approved and skip the prompt — proceed to label the card and update planning files.Additionally, when reading a card's comments via mcp__trello__get_card_comments, look for natural-language approval cues from the most recent human comment, in priority order: "yes", "approve(d)", "lgtm", ":+1:", ":thumbsup:", "ship it", "go", "promote". Treat as confirmation. If a more recent comment from the human says "hold", "wait", "not yet", "reject", the skill must ask for explicit confirmation.
This implements the spec's "card-to-Ready move = approval" pattern (W2c) without losing the explicit-confirm mode for sit-down sessions.
If the item references a parent epic (an issue with the epic label), create a sub-issue relationship.
GitHub backend — use the GitHub sub-issues API:
Follow the sub-issue linking procedure in references/github-sub-issues.md (relative to this skill's plugin directory at plugins/pm/), using {epic_number} as the parent and {number} as the child. The reference includes the GraphQL mutation with a comment-based fallback.
Local backend:
yq -i '.parent_epic = {epic_number}' "$item_file"
When backend == trello, the row's
#{number}is the card's Trello short id (e.g.t-AbCdEfGh);{spec path or "—"}is the card'sshortUrl. Otherwise, the row format is identical.
If the project maintains the markdown backlog (planning/todos.md exists), add promoted items to the Ready section.
Read the current planning/todos.md. Find the ## Ready section and the ### Sprint: Unassigned subsection. Append a row for each promoted item:
| #{number} | {title} | {size} | priority/p{priority} | status/ready ({owner_label}) | {spec path or "—"} | {TODAY} | — |
For needs-info items, do NOT add to Ready — they stay in triage. For items promoted with owner/human, add to Ready with status status/ready and the owner/human label (these still need code work, but a human should review the spec gaps first):
| #{number} | {title} | {size} | priority/p{priority} | status/ready (owner/human) | {spec path or "—"} | {TODAY} | {failing criteria} |
After all items are processed, print:
PM — Triage Complete
=====================
Items processed: {total}
Rejected (out-of-scope): {X}
Duplicates closed: {Y}
Skipped: {Z}
Specced: {W}
Promoted to status/ready + owner/ai: {A}
Promoted to status/ready + owner/human: {B}
Left as needs-info (status/needs-triage): {C}
{If GitHub: "Issues updated in {owner}/{repo}"}
{If local: "Items updated in {items_dir}"}
{If A > 0: "Next: Run /pm:sprint-dev to pick up status/ready + owner/ai items."}
{If B > 0: "{B} item(s) need human review before agent work."}
{If C > 0: "{C} item(s) need more information — re-run /pm:triage after adding details."}
/product-pulse:setup or /pm:setup./pm:setup./pm:setup.gh and run gh auth login."planning/todos.md not found — skipping backlog row insertion. Run /pm:setup to create the backlog.".status/needs-triage. Print a partial summary of what was completed.LIST_NEEDS_TRIAGE and the user moved it to another list during the run, the move-to-target call from this skill will fail with "card already in list X". Surface the error, skip the item, and continue. Reconcile will catch up next run.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub studio-moser/skills-n-stuff --plugin pm