| name | pm-status |
| description | Use this skill when the user asks "where are we", "what's in progress", "what should I work on next", "project status", "catch me up", "what's open", "what's stale", "what's stuck", "anything dragging", "old in-progress issues", "what's been sitting", "what needs triage", "any untriaged issues", "what's not labeled", "what needs my attention", or "anything I missed". Provides a cache-first session briefing with in-progress issues (with HITL/AFK execution-mode prefix), next suggested work, **stale-issue detection** (issues stuck in-progress longer than --stale-days, default 30), a **triage view** with four buckets (in-triage / untriaged / unknown-tag / needs-info), and delta sync on demand. Supports --refresh for full re-pull. Do NOT include past session history unless the user explicitly asks. This skill is whole-backlog BREADTH ("across everything, where are things?"). For DEPTH on one named parent/epic/project — its child issues' commit-by-commit progress, a phase ledger, or resuming where you left off — use pm-ledger instead, even if the user says "status" or "progress". |
| version | 0.44.4 |
PM Status Briefing
Deliver a concise, actionable briefing of current project state using a cache-first approach with delta sync on demand.
Usage
/pm [--refresh] [--scope <expr>] [--stale-days <int>] [--needs-info-days <int>]
--refresh (flag, optional) — force a full re-pull from the issue tracker, ignoring cache freshness.
--scope <expr> (string, optional) — scope the briefing; grammar repo:org/foo, team:KEY[+subteams], project:KEY[+deps[:upstream|downstream]], workspace:NAME, or all. Defaults to the active project's workspace.
--stale-days <int> (int, optional) — in-progress age (days) before an issue is flagged STALE. Default 30.
--needs-info-days <int> (int, optional) — days without movement before an operator-assigned issue lands in the Needs-info triage bucket. Default 7.
Read-only briefing: this skill only reads cache and syncs from the tracker — it makes no tracker writes.
All cache operations go through one CLI: node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" <verb>. Every verb prints a single JSON object to stdout and uses exit code to signal success/failure. Don't embed node -e snippets — the CLI is the only supported entry point.
Scope-aware briefings (v0.21.0+)
When the user passes --scope <expr>, run the scope-based flow instead of the legacy per-slug flow. Scope grammar:
| Form | Meaning |
|---|
repo:org/foo | One repo (back-compat) |
team:ENG | Issues with team_key == ENG |
team:ENG+subteams | ENG + transitive children |
| `project:CHK[+deps[:upstream | downstream]]` |
workspace:nthplus | Whole workspace |
all | Every known workspace |
Default scope when no --scope is given: workspace:<active-project's-workspace> if in a registered repo, else prompt the user. Skills like /pm-report document the same grammar.
Scope-based flow: skip Steps 2-5 below; run portfolio-sync --workspace <ws> for each touched workspace, then scope-briefing --scope <expr> to get the merged issue set. Apply the same Step 6 formatting (in-progress / stale / up-next) over the resolved issues.
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" portfolio-sync \
--workspace "<workspace>" --mode delta
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" scope-briefing \
--scope "<expression>"
The rest of this skill describes the legacy per-slug flow, still used when no --scope is passed and the Active Project has a slug.
Step 1: Check for --refresh Flag
If the user invoked /pm --refresh, skip directly to Step 5 (Force Full Sync). The --refresh flag forces a complete re-pull from the issue tracker regardless of cache freshness.
Step 2: Read Cache Status
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" status --slug "<slug>"
Replace <slug> with the project slug from the Active Project context.
The CLI returns one of:
{ ok: true, tier: "NONE", hasData: false, ... } — never synced; skip to Step 5
{ ok: true, tier: "FRESH" | "STALE" | "EXPIRED", message, lastSyncedAt, provider, ... }
{ ok: false, error, code: "NOT_CONFIGURED" } — missing projects.json; tell the user to run /pm-setup and stop.
Step 3: Decide Sync Path
- FRESH (synced < 1h ago): skip to Step 6 (no sync needed).
- STALE (synced > 1h, full sync within TTL): run Step 4 (delta sync), then Step 6.
- EXPIRED (full sync older than TTL): run Step 5 (full sync), then Step 6.
- NONE (no cache yet): run Step 5.
Step 4: Delta Sync
For GitHub projects (issue_tracker is "github"):
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" delta-sync \
--slug "<slug>" --provider github --repo "<org/repo>"
For Linear projects (uses GraphQL transport — requires LINEAR_API_KEY or project.local.json):
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" delta-sync \
--slug "<slug>" --provider linear \
--team-key "<team_key>" \
[--project-name "<project_name>"]
Omit --project-name if linear_project_name is unset for the project.
All paths return:
{ "ok": true, "syncType": "delta", "provider": "...", "changes": [...],
"changeSummary": "...", "deltaCount": N, "totalCount": M }
After running delta sync, display changeSummary to the user (it already says "No changes since last sync" when empty), then proceed to Step 6.
Step 5: Force Full Sync (--refresh, EXPIRED, or No Cache)
For GitHub projects:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" full-sync \
--slug "<slug>" --provider github --repo "<org/repo>"
For Linear projects (uses GraphQL transport — requires LINEAR_API_KEY or project.local.json):
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" full-sync \
--slug "<slug>" --provider linear \
--team-key "<team_key>" \
[--project-name "<project_name>"]
All paths return:
{ "ok": true, "syncType": "full", "provider": "...", "issueCount": N, "syncedAt": "..." }
Proceed to Step 6.
Step 6: Format the Briefing
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" briefing --slug "<slug>"
Returns { ok: true, freshness, issues, stale_threshold_days, stale_issues }.
Output format (keep it tight — no padding, no extra explanation):
Project: <displayName> · <org/repo> · gh: <gh_user> · <workspace> / <team_key> · <project_name if set>
Synced: <freshness>
IN PROGRESS
<ID> · [<mode>] <title> (<priority label>)
<ID> · [<mode>] <title>
TRIAGE — needs operator attention (only shown if any bucket below is non-empty)
In Triage (<N>): <ID> · <title> (<priority label>) ← Linear-native Triage state
Untriaged (<N>): <ID> · <title> ← no recognized type tag
Unknown tag (<N>): <ID> · <title> [tagged "<X>"] ← tag not in active manifest
Needs info (<N>): <ID> · <title> <ageDays>d · <assignee>
STALE (in-progress > <stale_threshold_days> days — only shown if stale_issues is non-empty)
<ID> · [<mode>] <title> <ageDays>d (<anchor>) · <assignee or "(unassigned)">
...
UNPROJECTED — team-tagged but unassigned to a project, often from auto-integrations
<ID> · [<mode>] <title> (<priority label>) · created <YYYY-MM-DD>
...
WIP AGING 0-3d: <n> · 4-7d: <n> · 8-14d: <n> · 15-30d: <n> · 30d+: <n> (mean <X>d)
UP NEXT
<ID> · [<mode>] <title> (<priority label>)
<ID> · [<mode>] <title>
Rules:
- Show at most 5 in-progress issues (status === "started")
- Show at most 3 suggested next issues (status === "unstarted"). Issues with
status === "triage" are excluded from both IN PROGRESS and UP NEXT — they belong in the TRIAGE section's "In Triage" bucket, not in ready work (NTH-562).
<mode> column. Show [hitl], [afk], or [??] if the execution-mode label is missing. The [??] is itself the signal that the issue needs triage — it tells the operator "this one slipped past Step 2b of pm-issues and needs a label applied." The mode prefix appears in IN PROGRESS, STALE, and UP NEXT views so the operator can see at a glance which work is agent-runnable vs needs them at the wheel.
- TRIAGE section: omit entirely if all four buckets are empty. When any bucket has entries, show only the non-empty ones with their count. See "Triage view" below for the bucket definitions.
- STALE section: omit entirely if
stale_issues is empty. When present, show all entries (they're already filtered + sorted descending by age). The anchor field is "started" (Linear startedAt) or "created" (GitHub fallback) — render as a parenthetical to clarify which timestamp drives the count.
- UNPROJECTED section (NTH-506): omit entirely if no such issues exist. When the briefing scope is project-bound (e.g. running
/pm inside a repo with a configured linear_project_name), also surface issues in the SAME team where project_name == null. These typically come from Linear's GitHub integration auto-mirroring GH issues into the team without project classification, or from manual filings that skipped project tagging. Sort newest-first by createdAt; cap at 5 entries with ... and N more if longer. Suppress this section when the briefing scope is already team- or workspace-wide (no project filter, so nothing to be "missed").
- WIP AGING line. Always shown when
aging_wip.total > 0. One row; do not break across lines.
- The
stuck and agingWIP lens computations live in hooks/lib/report-lenses.js. The briefing verb invokes them; this skill only renders.
- Priority labels: P0 = Critical, P1 = Urgent, P2 = Medium, P3 = Low
- Do NOT show closed or cancelled issues
- Do NOT show other team members' work unless asked
- Do NOT summarize past sessions unless explicitly requested
Triage view — four buckets
The triage view answers a different question than IN PROGRESS / UP NEXT: "what needs me right now to unstick the work, regardless of who's assigned?" Compute it from the cached issue set + the active manifest:
Bucket 0 — In Triage (Linear-native). Open issues with status === "triage" — i.e. sitting in Linear's native Triage inbox, not yet accepted into the workflow. Detection:
status === "triage" on the cached record (set by normalizeLinearState when Linear reports state.type: "triage" — NTH-562).
These are the most authoritative triage signal: Linear itself parked them for review. They are deliberately excluded from UP NEXT (which filters status === "unstarted"), so without this bucket they'd be invisible. The fix is to accept them into the workflow (move to Todo/Backlog) or decline them. Sort by priority, then ID. GitHub-tracked projects never populate this bucket (GitHub has no triage state).
Bucket 1 — Untriaged. Open issues with no recognized type tag. Detection:
- Title doesn't start with a
[<tag>] prefix that matches a manifest entry, AND
- None of the issue's tracker labels appear in any manifest entry's
labels.<tracker> array
These are issues that the reviewer can't even classify, let alone validate. The fix is to apply a label or rewrite the title.
Bucket 2 — Unknown tag. Open issues that DO have a type-shaped signal (a [<tag>] prefix or a label that looks tag-ish) but the tag isn't in the active _manifest.json. Detection:
- Some classification signal present, AND
- The resulting tag string doesn't match any
manifest.tags[].tag value
These are taxonomy-drift issues — a tag the project either retired or never registered. The fix is either to register the tag (/pm-templates) or re-classify the issue under an existing tag.
Bucket 3 — Needs info. Open issues assigned to the current operator that haven't moved in --needs-info-days days (default: 7) AND don't appear in the STALE section. Detection:
assignee === <current gh_user or linear me>, AND
updatedAt older than --needs-info-days days ago, AND
- Not already in
stale_issues (avoid double-listing)
These are issues sitting in the operator's queue without recent activity — easy to forget, easy to unblock with a single comment.
The buckets are computed at briefing time from the cached issues.json + the manifest. No new CLI verb is required for v0.20.3; if the briefing logic grows we can promote it to pm-cache.js briefing in a later version.
To override the staleness threshold, pass --stale-days N (e.g., --stale-days 14 for a stricter cadence). The default is 30 days, which catches "drifted" work without flagging issues that are simply long-running.
Proactive handoff nudge (v0.19.0+)
When stale_issues is non-empty and the project has handoff_channels configured, surface a single-line nudge in the briefing output:
N in-progress issues haven't moved in <stale_threshold_days>+ days. Want to write a handoff for one of them? (/pm-handoff --to <login>)
Don't auto-invoke pm-handoff — let the user decide. The signal is "something's drifting; consider whether it should be handed off to someone with more time or context." The STALE table already shows the issues; this is just the prompt.
Suppress the nudge when handoff_channels is absent — no point pointing at a skill the project hasn't set up.
Step 7: Offer Next Action
After the briefing, offer:
"Want to pick up [top suggested issue], start something new, or is there a specific issue to look at?"
Diagnostics
If any verb returns { ok: false, ... }:
code: "NOT_CONFIGURED" → tell the user to run /pm-setup. Don't retry.
code: "NO_CACHE" → run Step 5 (full sync), then retry.
code: "NO_META" → same as NO_CACHE; full sync first.
code: "PROJECT_NOT_FOUND" → the slug doesn't match any project. Confirm the slug from the Active Project context.
code: "FETCH_FAILED" → tooling problem (CLI missing, network, auth). Probe with node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" self-check.
The self-check verb prints { healthy, checks, projectsCount, tooling: { linearCli, gh } } and is the canonical way to diagnose install/config issues.