| name | pm-triage |
| description | Use this skill to triage a raw or untriaged issue — one that lacks a type tag, an execution-mode label, or template structure — and turn it into workable, labeled, reviewed work. pm-triage orchestrates classification (pm-templates), structural review (pm-review), and enrichment (pm-improve), then applies the type and hitl/afk labels on the operator's confirmation, and hands the now-workable issue off to pm-organize so it leaves the Triage state instead of parking there. It is the action counterpart to pm-status's triage view. Trigger whenever the user says "triage this issue", "triage NTH-42", "triage the backlog", "clean up untriaged issues", "what needs triage", "/pm-triage", or when pm-status has just surfaced Untriaged / Unknown-tag / Needs-info buckets and the operator wants to act on them. Also use it proactively after a pm-status briefing flags untriaged work, and whenever an issue clearly needs labels plus a structural pass before it can be picked up — even if the user never says the word "triage". |
| version | 0.44.4 |
PM Triage
Take a raw or untriaged issue and make it workable. pm-triage is the action counterpart to the triage view that pm-status renders: pm-status finds what needs triage (the Untriaged / Unknown-tag / Needs-info buckets); pm-triage performs it.
Usage
/pm-triage [<issue-id>]
<issue-id> (issue key e.g. FUNC-12, optional) — the issue to triage; omit to work the whole triage backlog.
Writes are confirmation-gated: the type + hitl/afk labels (and any enrichment comment) are applied only on the operator's confirmation.
It does that by orchestrating three skills that already exist — it does not reimplement them:
pm-templates — classify the issue's type tag
pm-review — structural check against that tag's template
pm-improve — enrichment (repo context, related issues, clarifying questions)
…and then it adds the one thing none of those three do: on the operator's confirmation, it applies the triage decision — the type-tag label and the hitl/afk execution-mode label — so the issue actually leaves the untriaged state. (pm-review only flags a missing execution-mode label; pm-improve only posts a comment. Neither labels the issue. That gap is pm-triage's reason to exist.)
When to use this
- An issue is raw — filed by a stakeholder, auto-mirrored from GitHub, or filed in a hurry — and lacks a type tag, an execution-mode label, or template structure.
pm-status surfaced a triage bucket and the operator wants to act on it.
- The operator says "triage this issue", "triage NTH-42", "clean up the triage backlog", "/pm-triage".
When not to use it. If the issue is already tagged, labeled, and structured, there is nothing to triage — running pm-triage just produces noise. If the operator wants only a structural review, that is pm-review; only enrichment, that is pm-improve. pm-triage is the bundle — reach for it when you want the whole pass, not one slice.
The triage pipeline (one issue)
Steps 1-6 are read-only and run without confirmation — they cost nothing but tokens, and the operator needs the whole proposal before deciding anything. Step 7 applies the labels (always confirmed). Step 8 hands the now-workable issue off to pm-organize so it leaves the Triage state — also confirmed, and routed entirely through pm-organize's existing write path (pm-triage adds no new write codepath of its own).
Step 1: Fetch the issue
Read it from the tracker — the same fetch pm-review uses (linear issue view <ID> --json / gh issue view <NUM> --repo <org/repo> --json ...). If the tracker is unreachable, stop with a transport error; do not triage from cached state — the cache can carry stale labels, and triage decisions hinge on the current label set.
Step 2: Classify the type tag
Delegate to pm-templates' classifier (the logic lives there — do not duplicate it). It resolves in order: an explicit [tag] title prefix → a tracker label that maps to a manifest tag → inference from the body against each tag's description.
The classification result determines the path:
- Explicit or label-matched → the tag is firm; continue.
- Inferred → a soft classification. Surface it — "I read this as a
bug; confirm or correct" — and wait. Everything downstream (which template pm-review checks against, which enrichment streams pm-improve weights) depends on the tag being right, so a wrong guess wastes the whole pass.
- A tag-shaped signal that is not in the manifest (e.g. a
[migration] prefix, but the project's manifest has no migration tag) → this is the Unknown-tag case. Do not invent a tag. Surface it as a taxonomy decision: register the tag via pm-templates, or re-classify the issue under an existing tag. pm-triage stops here for this issue until the operator decides.
- No signal at all → propose the best-fit tag from the manifest descriptions and confirm before continuing.
Step 3: Structural review
Run pm-review against the issue at the classified tag. Capture its findings — which required sections are present, missing, or placeholder-only; whether the acceptance criteria are testable.
pm-triage does not rewrite the body to close those gaps. Carry the gap list into the proposal instead — the operator decides which gaps to fill, and fills them by editing the issue (a pm-issues update) or by answering the clarifying questions from the next step. Auto-rewriting someone's issue body is a surprise; reporting what is missing is a service.
Step 4: Enrichment
Run pm-improve against the issue. Capture its enrichment draft — repo context, related issues, clarifying questions. Hold it as a proposed comment; do not post it yet.
Step 5: Propose an execution mode
Every workable issue carries a hitl (human-in-the-loop) or afk (agent-runnable) label. pm-review deliberately refuses to guess this for an existing issue — and it is right not to, because a silent guess that lands wrong routes work to the wrong queue.
pm-triage's job is to propose, not to guess silently. Start from the tag's hitl_default_hint in the manifest, then adjust for what the issue actually says:
- Needs manual verification, a design decision, or UX judgment →
hitl.
- Well-specified, mechanically checkable, no judgment call →
afk.
Present the proposal with its reason so the operator can correct it in one word. The reason is what makes the proposal reviewable instead of a coin flip.
Step 6: Assemble the triage proposal
Combine Steps 2-5 into one proposal (format below) — one screen with everything the operator needs to say yes or adjust.
Step 7: Confirm and apply
Show the proposal. On the operator's confirmation:
- Apply the labels. Add the type-tag label (the canonical/first entry from the manifest's
labels.<tracker>) and the chosen hitl/afk label to the issue:
- Linear:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-issue.js" update --id <ID> --add-label "<type>" --add-label "<hitl|afk>"
The verb fetches existing labels via GraphQL, merges (idempotent on both add and remove), and writes the union back in one call. Do not call linear issue update --label directly — its --label flag is replace-semantics, not add-semantics, and any labels not explicitly listed are silently dropped (NTH-511).
- GitHub:
gh issue edit <NUM> --repo <org/repo> --add-label "<type>" --add-label "<hitl|afk>"
gh has native add/remove flags, so the raw CLI is safe here.
- If a label does not exist in the tracker yet, create it first (
linear label create -n <name> / gh label create <name>) — a label named in the manifest but absent from the tracker matches nothing.
- Offer the enrichment comment separately. Posting the
pm-improve draft as a comment is a second, distinct write — ask for it on its own ("post the enrichment as a comment on ?"), because the operator may want the labels without the comment.
- Verify the apply landed (optional, Linear only). If verification is needed, run
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" verify-issue --slug <slug> --id <ID> — it fetches fresh state via GraphQL, compares to the cache, and reports any divergence. Add --update to also merge the fresh state back so the next briefing sees it without a full delta-sync.
For a single issue, one confirmation covers the labels and the enrichment comment is its own yes/no. For a batch, see below.
Label application on Linear MUST go through pm-issue.js update (NTH-511, landed in 0.28.0). The verb wraps the fetch+merge so the replace-semantics of linear issue update --label can't accidentally drop labels. GitHub's gh issue edit --add-label/--remove-label are add/remove-semantics natively, so the raw gh call is fine.
Step 8: Hand off to pm-organize
Labeling makes the issue workable. It does not move it out of the tracker's Triage state — without this step the issue stays parked in Triage even though it is now typed, labeled, and reviewed. Step 8 closes that gap by handing the workable issue off to pm-organize, the placement skill, so triage is a continuation into placement rather than a dead end.
pm-triage adds no new write codepath here. The handoff reuses the existing pm-organize skill, its placement-resolver.js tiering, and the pm-issue.js update --id <ID> --status <state> verb that pm-organize already drives. There is no direct linear, gh, or MCP call added in this step — the status move and everything beyond it route through machinery that already exists.
Minimum action is status-only. The smallest thing Step 8 does is move the issue out of Triage by setting its workflow status — pm-organize's lowest, auto-safe tier (tierFor('status') → 'auto-safe'). Full placement (project/milestone, parent, assignee, workstream) is offered as a pm-organize follow-on, never forced by triage.
Do not hardcode the target state. pm-triage does not name the post-Triage state. Defer the Triage→? mapping to pm-organize's status resolver — it owns which workflow state a workable issue lands in, and that mapping is project-specific.
The autonomy contract mirrors pm-intake's tier contract exactly:
- Interactive single-issue mode. After Step 7's labels apply, offer: "move
<ID> out of Triage and place it now?" Proceed only on the operator's yes. On yes, route to pm-organize — at minimum the auto-safe status move; the operator can continue into the reversible/always-confirm tiers from there. The issue keeps its freshly applied triage labels and leaves the Triage state.
- Batch mode. Fold the Step 8 handoff into the existing single batch-level confirmation (see Batch mode below) — one decision for the whole backlog, not one prompt per issue.
- Explicit
--autonomous / batch grant. Auto-apply only the auto-safe status move out of Triage. Return project/milestone/parent (reversible) and assignee/new-workstream (always-confirm) as deferred proposals in the report — never auto-applied. A casual go-ahead ("ok", "do it") is not an autonomous grant; when unsure, stay interactive and surface the move as a proposal. (This matches pm-intake's write posture and pm-agent's err-toward-confirm tiebreaker.)
- GitHub degradation.
pm-issue.js update --status throws TRACKER_UNSUPPORTED on GitHub (mirrored in pm-organize's tier table). On GitHub, Step 8 must not error — surface "move <ID> out of Triage" as a manual follow-on for the operator instead, and proceed.
pm-intake boundary. Step 8 is the single-issue interactive path from triage into placement; the pm-intake agent is the batch/background entry point that runs triage→organize over a target out-of-thread. Both consume the same pm-triage + pm-organize skills, so their tiered-autonomy rules are identical (auto-safe applied, reversible deferred unless autonomous, always-confirm never auto). Reach for pm-intake when you want the pipeline dispatched and your session unblocked; Step 8 is what happens when you triage one issue inline.
Batch mode — working a triage backlog
pm-status renders the triage view in four buckets. pm-triage consumes them — but each bucket wants different handling, so do not treat the backlog as one undifferentiated list:
| Bucket | What it means | What pm-triage does |
|---|
| Untriaged | No recognized type tag | Full pipeline (Steps 2-7): classify, review, enrich, propose and apply labels. |
| Unknown-tag | Has a tag-shaped signal, but the tag is not in the manifest | Surface as a taxonomy decision — route to pm-templates to register the tag or re-tag the issue. No label is applied automatically. |
| Needs-info | The operator's own issues, stale with no recent activity | Run enrichment (pm-improve) and flag them. These are usually already typed — do not re-label; the goal is to unstick them. |
| Likely duplicates | Clusters of issues that the duplicates lens judged are likely duplicates of each other | Surface each cluster with its members and matched terms, then defer to pm-dedup for the merge / relate / close offer. |
Likely-duplicates bucket — operational detail
Clusters in this bucket come from the duplicates lens:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" lens --slug <slug> --lens duplicates
Each entry: { members: [id...], pairs: [{a, b, score, matchedTerms}] }. members is the flattened list of issue IDs in the cluster (sorted oldest-first as the suggested canonical); pairs carries the per-pairwise score + matched terms.
Action surface is single-sourced in pm-dedup. When the triage flow reaches this bucket, surface the clusters with their members + matched terms, then defer to pm-dedup for the actual merge / relate / close offer. Do not duplicate the action vocabulary here.
The lens already excludes pairs where either member carries the duplicate label — resolved stays resolved across triage passes.
Resolve the buckets by running pm-status first — it owns that computation, and pm-triage does not re-derive it. Then run the per-bucket handling above across the issue set.
One confirmation for the batch. Assemble every issue's proposal into a single consolidated summary, show it once, and apply on one confirmation — not a prompt per issue. A ten-issue triage backlog should cost the operator one decision, not ten. This matches pm-agent's batch posture. The Step 8 handoff folds into this same single confirmation: on the batch yes, each issue's auto-safe status move out of Triage applies along with its labels — there is no separate Step 8 prompt per issue.
The triage proposal — output format
Use this shape, scaled down when a section is empty:
# Triage proposal — <ID> · <title>
**Type:** <tag> (<explicit | label | inferred — confirm>)
**Execution mode:** <hitl | afk> (proposed — <one-line reason>)
**Labels to apply:** `<type-label>`, `<hitl|afk>`
## Structural review (via pm-review)
- [PASS] <section> — <one-liner>
- [FAIL] <section> — <what is missing>
Verdict: <ready to start | needs more information>
## Enrichment (via pm-improve — proposed as a comment)
<condensed: repo context / related issues / open questions — or "none material">
## Needs a human before this is workable
- <gap the operator must close, e.g. "fill the empty acceptance criteria">
- <or "nothing — structurally ready once labels are applied">
## Apply
Applying adds `<labels>` to <ID>. The enrichment comment is a separate confirm.
The proposal is honest about its own confidence: an inferred tag says so, the execution mode shows its reasoning, and "needs a human" names the gaps pm-triage cannot close itself. The operator should be able to trust a clean proposal and scrutinize a hedged one — and that is only possible when the hedges are visible.
What this skill does NOT do
- It does not rewrite the issue body. Structural gaps are reported; closing them is a
pm-issues edit the operator makes.
- It does not create or close issues — that is
pm-issues.
- It does not register new tags — for an Unknown-tag issue it routes the decision to
pm-templates.
- It does not reimplement classification, review, enrichment, or placement — it sequences
pm-templates, pm-review, and pm-improve, then (Step 8) hands off to pm-organize. When one of those changes, pm-triage inherits the change for free. It adds no write codepath of its own for placement — the status move and all placement route through pm-organize + pm-issue.js update.
- It does not apply anything without confirmation. The analysis is free; the write is gated.
Where pm-triage sits
pm-status → pm-triage → pm-organize
(finds the (classify · review · enrich · (move out of Triage ·
triage buckets) apply type + hitl/afk labels · place: status / project /
Step 8 hands off placement) parent / assignee / workstream)
│
├─→ pm-issues (edit the body to close gaps)
└─→ pm-templates (register an unknown tag)
pm-status answers "what needs triage?"; pm-triage answers "triage it" and then hands the workable issue off to pm-organize so it leaves the Triage state (Step 8) — triage flows into placement, it is not a dead end. When pm-triage hands back a "needs a human" list, that is the cue for a pm-issues body edit; when it hits an Unknown-tag issue, that is the cue for pm-templates.