| name | pm-issues |
| description | Use this skill when creating, updating, or closing **an issue** (a bug, feature, task, or spike) in Linear or GitHub; when the user describes a specific untracked **issue** that should be filed; when drafting issue content; or when the user says "create an issue", "log this issue in Linear", "add this issue to Linear", "open a github issue", "file an issue", "create issue |
| version | 0.44.4 |
PM Issue Management
Usage
/pm-issues create|update|close [<issue-id>] [flags…]
create — file a new issue via pm-issue.js create (needs --tag, --title, --draft-file, --exec-mode hitl|afk; optional --priority 1-4, --assignee, --parent, --project, --milestone, --dry-run; gated by the create-gate / template+label validation).
update <issue-id> — change labels/fields/comment via pm-issue.js update --id <id> (--add-label, --remove-label, --status, --assignee, --priority, --project, --milestone, --parent, --comment, --dry-run). All Linear label writes route through this NTH-511-safe update verb — it fetches existing labels and merges add/remove rather than replacing the set.
close <issue-id> — close/cancel (Linear via web UI / PR Closes <ID>; GitHub via gh issue close --reason completed|"not planned").
<issue-id> (issue key e.g. NTH-12 / GitHub #123, required for update/close).
Also: relate --from --to --type duplicate|related (link issues) and dedup-check --slug --draft (pre-create duplicate scan).
Listing issues across a scope (v0.21.0+)
When the user asks "what's open in team:X" / "show me everything for project:Y+deps" / "list all open NTH issues", run scope-briefing against the workspace cache instead of the legacy per-slug briefing:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" scope-briefing --scope "<expr>"
The returned issues array carries every issue matching the scope (filtered by team_key / project_slug predicates), already merged across the touched workspaces. Format it as a table with ID / title / status / assignee / age.
Writes (create / update / close) stay single-issue — scope is for read views only. The create/update/close instructions below are unchanged.
Create, update, and close issues with consistent, clean formatting. Routes by the project's issue_tracker field in <repo>/.claude/pm/project.json:
issue_tracker: "linear" → Linear GraphQL API (requires LINEAR_API_KEY or project.local.json)
issue_tracker: "github" → gh CLI
Read the active project's tracker before composing — the templates and workflow are the same, only the transport differs.
Upstream: pm-spec
For spec-driven initiatives (where the user has a markdown spec, brainstorming output, Linear doc, or GitHub issue body and wants reviewers to weigh in before the tracker entry exists), the pm-spec skill is the natural upstream. pm-spec turns the spec into a playground, captures reviewer choices, and writes a ## Settled values from playground appendix to the spec. By the time the spec lands here, the description includes both the user's intent and the reviewer-confirmed choices — file it directly.
After this skill creates the tracker entry, the operator should run pm-spec promote --slug <X> --to-id <ID> to move the playground from docs/pm/_drafts/ to docs/pm/<id>/ so the artifact is filed under the same ID as the tracker entry.
Issue Templates
Templates live in the pm-templates skill — a tag-keyed registry. Canonical location is the in-repo <repo>/.claude/pm/templates/; plugin defaults are the only fallback. Defaults ship with bug, feature, task, and spike; projects with an in-repo manifest can drop tags or add new ones.
Resolution (do not inline the templates here):
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -f "$REPO_ROOT/.claude/pm/templates/_manifest.json" ]; then
MANIFEST="$REPO_ROOT/.claude/pm/templates/_manifest.json"
else
MANIFEST="${CLAUDE_PLUGIN_ROOT}/skills/pm-templates/templates/_manifest.json"
fi
cat "$MANIFEST"
TAG=bug
for path in \
"$REPO_ROOT/.claude/pm/templates/${TAG}.md" \
"${CLAUDE_PLUGIN_ROOT}/skills/pm-templates/templates/${TAG}.md"
do
[ -f "$path" ] && cat "$path" && break
done
If a project asks for a tag not in the active manifest, do not invent a generic template — surface the unknown tag and offer to register it via pm-templates (which will edit the in-repo manifest if one exists, or bootstrap one from the plugin defaults).
Conventions
If <repo>/.claude/pm/conventions.md exists, load it before drafting (see pm-conventions). The relevant sections for this skill are ## Issue titles and ## Issue labels. Apply rules over the plugin's baked-in heuristics when they differ; when applying a rule changes the output materially, mention it in your draft summary so the operator sees the rule firing.
Creating an Issue
Step 1: Classify the issue type
Apply pm-templates' classifier — explicit [tag] prefix in the title → tracker label match → inferred from body. If inferred, surface that to the operator and confirm before treating it as classified.
Step 2: Draft content using the resolved template
Fill in the template body resolved above. Ask clarifying questions only if the problem statement or acceptance criteria are unclear.
Quality checklist before creating:
Scoping rule: Each issue should be completable in a single PR. If the user describes a large feature (e.g., "add auth system"), suggest decomposing it into a parent issue with sub-issues (e.g., "add middleware", "add token refresh", "add logout"). The parent issue needs no PR — it closes when all sub-issues close.
Step 2b: Tag HITL or AFK
Every issue carries one of two execution-mode labels:
hitl (human-in-the-loop) — the work needs a human at the wheel: design decisions, manual verification, UX judgment, decisions that can't be tested mechanically.
afk (away-from-keyboard) — the work is well-enough specified that an agent can grind through it end-to-end: refactors with passing tests, dep bumps, docs cleanup, mechanical migrations.
Don't skip this — it's a load-bearing distinction. When the plugin is used to feed work to agents, the difference between "this needs me" and "this can run while I sleep" is the difference between productive parallelism and shipping bad code.
Default from the manifest hint. Read hitl_default_hint from the tag's manifest entry: most bug/feature/spike issues default to hitl; task defaults to afk. Pre-fill that as the suggestion, then ask the operator:
"This <tag> will be filed as HITL by default — agent will draft, but you'll be in the loop. Confirm or switch to AFK?"
The operator's answer becomes a label applied at create time alongside the type-tag label.
Step 3: Validate and create — pm-issue.js create
All issue creation funnels through one verb. Do not call linear issue create, gh issue create, or the save_issue MCP tool directly — pm-issue.js create validates the draft against the tag's template and labels before it writes, and is the only sanctioned create path. The verb resolves the tracker, team, and project from .claude/pm/project.json, so those are never passed as flags.
Team selection goes through the shared team resolver (hooks/lib/team-resolver.js):
- Explicit
--team in the request → use it.
- Otherwise → fall back to
.linear.toml team_id (via linear_team_key in project.json).
This is the same resolver the project-tier skills use, so any audit / convention
that applies to one applies to the other.
-
Write the filled template body to a temp file:
DRAFT=$(mktemp --suffix=.md)
cat > "$DRAFT" <<'EOF'
<filled template body — every required section, real content, no placeholders>
EOF
-
Dry-run to validate before showing the user:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-issue.js" create \
--tag <tag> --title "<concise title>" --draft-file "$DRAFT" \
--exec-mode <hitl|afk> --priority <1-4> --assignee self --dry-run
On { "ok": false, "code": "VALIDATION_FAILED" } the JSON also lists missingSections, unfilledSections, and missingLabels. Fix the draft and re-run the dry-run until the response has "ok": true and "dryRun": true (it also reports "validation": "pass" and the resolvedLabels that will be applied). priority is optional: 1=urgent, 2=high, 3=normal, 4=low — omit the flag to leave the tracker default.
Workspace label preflight (NTH-544, Linear only). A passing structural validation is not the whole story: dry-run also checks that every resolvedLabel actually exists in the target Linear workspace (one GraphQL call, case-insensitive). If a manifest maps a tag to a label the workspace doesn't ship — e.g. task → ["task", "chore"] against a workspace that only has Task — dry-run returns { "ok": false, "code": "LABEL_NOT_IN_WORKSPACE", "missingLabelsInWorkspace": ["chore"] } instead of "ok": true. This fails fast, before any issue is filed, rather than letting a 15-issue batch surface as 9 per-issue create failures. Remediation: create the label in Linear, or fix the tag's label mapping in the manifest. A passing preflight reports "labelPreflight": "pass" and "bootstrap": false. If the label query itself fails (auth/network), dry-run still returns "ok": true with "labelPreflight": "skipped" so a transient outage can't block structural validation. GitHub is not preflighted — it auto-creates labels on first use.
-
Show the validated draft to the user and confirm (for a multi-issue batch, show one consolidated summary and confirm once).
-
On confirmation, create for real — the same command without --dry-run:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-issue.js" create \
--tag <tag> --title "<concise title>" --draft-file "$DRAFT" \
--exec-mode <hitl|afk> --priority <1-4> --assignee self
rm -f "$DRAFT"
The verb re-validates, resolves the labels, creates the issue, refreshes the cache, and returns { "ok": true, "id": "...", "url": "...", "labels": [...], "cached": true }. If "cached": false the issue was created but the cache refresh failed — run /pm --refresh before the next briefing. Add --parent <id> to file the issue as a sub-issue.
Labels are not auto-bootstrapped. If a resolved label doesn't exist in the workspace, the live create fails with TRACKER_WRITE_FAILED and a message naming the unknown label(s) (NTH-542 guard) — it does not silently create them. This is exactly what the step-2 dry-run preflight catches first, so a confirmed dry-run ("labelPreflight": "pass") guarantees the create won't trip on labels.
Creation uses the Linear GraphQL API. If no API key is configured the verb returns TRACKER_WRITE_FAILED with code NO_API_KEY or INVALID_API_KEY — run /pm-setup to configure your key. That error surfaces only on the live create (step 4), not during --dry-run.
Updating an Issue
When work in progress diverges from the issue description:
Linear (GraphQL)
All updates go through the pm-issue.js update verb. A single invocation can combine label changes, field updates, and a comment — they are applied in sequence and the cache is reconciled once at the end.
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-issue.js" update \
--id <ISSUE_ID> \
[--add-label "<label>" ...] [--remove-label "<label>" ...] \
[--status "<workflow state name>"] \
[--assignee "<email>"] \
[--priority <1-4>] \
[--project "<project name>"] \
[--milestone "<milestone name>"] \
[--parent "<parent identifier, e.g. NTH-5>"] \
[--comment "<comment body>"] \
[--dry-run]
Flag reference:
| Flag | Maps to | Notes |
|---|
--add-label / --remove-label | tracker.updateLabelsByName | Repeatable; idempotent (add-already-present + remove-not-present are no-ops) |
--status | patch.status → stateId | Resolved by name via fetchWorkflowStateByName |
--assignee | patch.assigneeEmail → assigneeId | Resolved by email |
--priority | patch.priority | Integer 1=urgent 2=high 3=normal 4=low |
--project | patch.projectName → projectId | Resolved by name |
--milestone | patch.milestone → projectMilestoneId | Requires --project to be set or resolvable from context |
--parent | patch.parentIdentifier → parentId | Pass the issue identifier, e.g. NTH-5 |
--comment | tracker.addComment | Routed separately from field update |
Labels are fetched from the API before merging — the verb never replaces the full label set silently (NTH-511, NTH-542). Returns { ok: true, id, tracker, before: [...], after: [...], changed: bool }. With --dry-run, no tracker writes happen.
GitHub
GitHub supports label changes, title/body edits, and comments via the update verb. Workflow states (--status) are not supported on GitHub — the verb returns TRACKER_UNSUPPORTED if attempted; use gh issue close/reopen or workflow labels instead.
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-issue.js" update \
--id <ISSUE_NUMBER> \
[--add-label "<label>" ...] [--remove-label "<label>" ...] \
[--comment "<comment body>"] \
[--dry-run]
For title/body edits not supported by the verb, use gh issue edit directly:
gh issue edit <NUMBER> --repo <org/repo> --title "<new title>" --body-file "$DESC_FILE"
To assign someone on GitHub, use gh issue edit <NUMBER> --repo <org/repo> --add-assignee <login>.
Closing an Issue
Linear and GitHub both auto-close issues via PR descriptions using the Closes <ID> / Closes #<num> syntax. Do not manually close unless:
- The work was abandoned
- It was resolved without a PR
- An auto-close didn't fire and needs a manual nudge
Linear (GraphQL)
Close via the Linear web UI (mark the issue as Done or Cancelled), or instruct the user to close it there. Auto-close via PR Closes <ID> description is the preferred path.
GitHub
gh issue close <NUMBER> --repo <org/repo> --comment "<optional closing note>"
Use --reason "completed" (default) or --reason "not planned" to distinguish abandonment. If the issue was resolved by a PR but didn't auto-close, add a closing comment that links the PR for future readers: --comment "Resolved by #123.".
Proactive Issue Detection
When the user describes work, check if it matches an open issue.
Linear
Check the issue cache via briefing or lens:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" briefing --slug "<slug>"
Search issues array by title/description keywords. If the cache is stale, run delta-sync first.
GitHub
gh issue list --repo <org/repo> --search "<keywords>" --state open --limit 10 --json number,title,labels,assignees,url
gh search issues is also available for cross-repo queries, but gh issue list --search is scoped to the repo and matches the cache shape we already use.
Then, regardless of tracker:
- If no match found → suggest creating one
- If a match found → confirm: "Is this related to [ID · Title]?"