| name | pm-setup |
| description | Use this skill when the user asks to "set up project manager", "register this project", "configure pm", "onboard this repo", "configure Linear API key", "linear api key", "linear authentication for project-manager", "set up linear access", "configure linear api access", "initialize conventions", "set up conventions.md", or invokes the `/pm-setup` slash command. Also use proactively when the SessionStart hook nudges the user that "project-manager" is installed but no projects are configured and they ask how to fix it. Guided wizard that registers a repo as a managed project, configures Linear API access (personal API key, no CLI install required), and optionally initializes a `conventions.md` file. |
| version | 0.44.4 |
PM Setup
Guided wizard to (a) register the current repo as a managed project and (b) configure Linear API access. Either half can run on its own — if PM is already configured but the user just needs to set up Linear access, jump to Step 6.
When to use this skill
- User asks to set up, register, or onboard a project for project-manager
- User asks how to configure Linear API access or an API key for this plugin
- The SessionStart hook surfaced the nudge
Linear access not configured for v0.32.0+ and the user wants to act on it
- The SessionStart hook surfaced the nudge
project-manager: installed but no projects configured and the user wants to act on it
- The user invoked
/pm-setup
Inputs needed before starting
- A git repo with
origin set (the wizard reads git remote get-url origin)
- One or more
gh auth profiles for the GitHub username they want to use
- Linear MCP available (used to validate workspace/team)
- A Linear personal API key (generated at https://linear.app/<workspace>/settings/api) — collected and verified in Step 6
Step 1: Detect the repo
git remote get-url origin
Parse org/repo (strip .git, handle SSH and HTTPS forms). If this fails, stop with: "Not a git repository or no remote configured. Please run this from inside your project."
Step 2: Check for an existing profile
Look for an existing config in two places:
- Canonical (v0.20.0+):
<repo>/.claude/pm/project.json. If present, the project is already registered — ask whether to update it or just configure the Linear API key.
- Legacy (pre-v0.20.0):
~/.claude/project-manager/projects.json with an entry for this repo. If present, offer to migrate (see Step 4's migration block) — the wizard will write .claude/pm/project.json + .claude/pm/project.local.json, then strip the old entry. Do not skip migration — the rest of the plugin no longer reads from the old location.
"This project is already registered as <name>. Update it, or just configure the Linear API key?"
If they pick "just API key", jump to Step 6.
Step 3: Collect project details
Ask in sequence — one question at a time, not a wall.
a) Display name — "What's the display name for this project?"
b) GitHub username — list available accounts:
gh auth status 2>&1 | grep "Logged in to github.com account"
Then ask which to use. Validate by switching:
gh auth switch --user <chosen_user> && gh api user --jq '.login'
c) Linear workspace — probe via Linear MCP:
list_teams
The workspace name appears in team results. List detected workspace(s) and let the user confirm. Store both the workspace name and slug (lowercased + hyphenated). If they have access to multiple workspaces and the wrong one is connected, they may need to reconnect Linear MCP first.
d) Linear team key — ask for the team key (e.g., ENG, INT, UI). Match it against the list_teams response. If not found, list available teams and ask again.
e) Linear project (optional) — ask if this work is scoped to a specific Linear project. If yes, validate via:
list_projects { team: "<team_key>" }
Match by name (case-insensitive). Skip if not provided — issues will be team-scoped.
f) Issue tracker — default linear. Only ask if the user has reason to override.
g) Spec source (only ask if the user might use the pm-spec skill — playground rendering):
"Where do you typically write specs for this project? (markdown files / brainstorming output / Linear documents / GitHub issue bodies / pasted in chat — pick all that apply, comma-separated, default markdown)"
Map answers to this list (used by pm-spec's spec_source field): markdown, brainstorming, linear-doc, github-spec, pasted. If the user shrugs or says "just markdown", record ["markdown"] and move on.
h) Feedback channel (only ask if g was answered):
"When reviewers play with a spec playground and want to share their settled choices back, where do they leave them? (Linear comment / GitHub comment / Teams channel / email / chat — default matches your issue tracker)"
Record as feedback_channel: { type, target? }. Examples:
- Linear:
{ "type": "linear" } (no target — pm-spec resolves the initiative ID at render time)
- GitHub:
{ "type": "github" } (same — resolved per render)
- Teams:
{ "type": "teams", "target": "#checkout-spec" } (fixed channel name)
- Email:
{ "type": "email", "target": "alex@example.com" } (fixed recipient)
- Chat:
{ "type": "chat" } (just paste back to the operator)
If neither g nor h is answered, leave both fields out — pm-spec falls back to ["markdown"] and { type: <issue_tracker> } defaults at runtime, so omitting them is safe and idempotent (re-running /pm-setup later can fill them in).
Step 4: Write the config (all of it in <repo>/.claude/pm/)
All PM config lives in one place: <repo>/.claude/pm/. There's no user-home projects.json anymore — the only thing in ~/.claude/project-manager/ is runtime state (cache, reports).
The directory holds two JSON files:
project.json — checked into git. Everything the team should share: slug, displayName, issue_tracker, linear_*, spec_source, feedback_channel, handoff_channels.
project.local.json — gitignored. Per-machine overrides; the only field most operators ever need is gh_user (their gh auth profile name). Same *.local.json pattern as .env / .env.local, settings.json / settings.local.json.
Step 4a: Create the directory
mkdir -p "$REPO_ROOT/.claude/pm"
Step 4b: Write project.json (shared, checked-in)
{
"$schema": "https://nthplusio.github.io/functional-claude/pm-project.schema.json",
"slug": "<repo-name>",
"displayName": "<user input>",
"issue_tracker": "linear",
"linear_workspace": "<workspace name>",
"linear_workspace_slug": "<workspace slug>",
"linear_team_key": "<team key>",
"linear_team_id": "<team id from MCP validation>",
"linear_project_id": "<project id if provided, or null>",
"linear_project_name": "<project name if provided, or null>",
"spec_source": ["markdown"],
"feedback_channel": { "type": "linear" }
}
spec_source / feedback_channel only appear if Step 3g/h were answered; omit them otherwise — runtime defaults are safe.
Tell the operator the file is safe to commit. No secrets live here: gh auth profiles are managed by gh itself, the Linear API key lives in project.local.json (gitignored) or their shell profile as LINEAR_API_KEY.
Step 4c: Write project.local.json (per-machine, gitignored)
{
"gh_user": "<chosen github user>"
}
That's the whole file for most operators. It's the slot for any future per-machine overrides (e.g., a contributor whose Linear API key targets a different workspace than the team's default).
Auto-add to .gitignore. Check whether the repo's .gitignore already excludes this file. If not, append:
GITIGNORE="$REPO_ROOT/.gitignore"
LINE=".claude/pm/*.local.json"
grep -qxF "$LINE" "$GITIGNORE" 2>/dev/null || echo "$LINE" >> "$GITIGNORE"
Show the operator the appended line and confirm it before staging. They may already have a broader *.local.json rule; if so, don't double up.
Resolution at runtime
When the session-start hook (or any PM skill) needs the active project's config, it reads both files and merges them:
const shared = readJsonSafe(`${repoRoot}/.claude/pm/project.json`);
const local = readJsonSafe(`${repoRoot}/.claude/pm/project.local.json`);
const config = { ...shared, ...local };
If project.json doesn't exist → "this project isn't registered, run /pm-setup."
If project.local.json doesn't exist → fine, the merge just uses shared values. The operator gets a one-time nudge to set gh_user if multi-account gh auth is detected.
Step 4d: Configure handoff channels (v0.19.0+)
Ask the user how handoffs from this project should land. Multi-select via AskUserQuestion:
- Linear (Linear-tracked projects) — comment + reassign + status transition
- GitHub (GitHub-tracked projects) — comment + reviewer request + ready-for-review
- Markdown — file in
docs/pm/handoffs/, committed to the repo as the audit trail
- Chat/email — payload handed off to resend-cli (requires
.resend.md)
Write the selections into project.json as handoff_channels (shared per-project):
"handoff_channels": [
{ "type": "linear", "default_status": "In Review", "assignee_from": "recipient" },
{ "type": "markdown", "path": "docs/pm/handoffs/" }
]
For chat/email entries, ask for the target (email address or channel name) and an optional subject_prefix. For GitHub, ask whether request_review and ready_for_review should be set (default both true). For Linear, ask which default_status to transition to (default "In Review").
If the user picks nothing, omit handoff_channels entirely — pm-handoff isn't relevant for every project. The skill will fail with NO_HANDOFF_CONFIG pointing back at /pm-setup if invoked later.
Step 4e: Issue templates and conventions — single offer
Ask once whether to scaffold the rest of the in-repo config. Both live under .claude/pm/ (you already created project.json and project.local.json there), so adopting them is a small directory-fill.
.claude/pm/
├── project.json # ✓ written in Step 4b
├── project.local.json # ✓ written in Step 4c
├── conventions.md # ← offer here
└── templates/ # ← offer here
├── _manifest.json
└── <tag>.md
Three options, multi-select:
-
Initialize conventions.md — copies the starter to <repo>/.claude/pm/conventions.md. Operator fills in house-style rules. Picked up automatically; no config field needed.
cp "${CLAUDE_PLUGIN_ROOT}/skills/pm-conventions/templates/conventions.md" \
"$REPO_ROOT/.claude/pm/conventions.md"
-
Initialize the templates directory — copies the plugin's _manifest.json and template bodies. The in-repo manifest fully replaces the plugin manifest for this project, so the operator can drop tags they don't use or add new ones.
mkdir -p "$REPO_ROOT/.claude/pm/templates"
cp "${CLAUDE_PLUGIN_ROOT}/skills/pm-templates/templates/"*.{json,md} \
"$REPO_ROOT/.claude/pm/templates/"
Tell the operator: "Templates copied. Edit .claude/pm/templates/_manifest.json to drop tags you don't use or add new ones, then edit individual <tag>.md bodies to match your style."
-
Use plugin defaults only — the plugin's bundled templates and zero conventions still work. The operator can re-run /pm-setup later.
Both 1 and 2 are independent — adopting templates without conventions, or vice versa, is fine.
Migration path (existing projects pre-v0.20.0)
If ~/.claude/project-manager/projects.json already has an entry for this repo, offer to migrate the whole entry into .claude/pm/:
Detected pre-v0.20.0 profile for nthplusio/functional-claude.
Migrating to <repo>/.claude/pm/:
→ project.json (slug, displayName, issue_tracker, linear_*, spec_source, feedback_channel, handoff_channels)
→ project.local.json (gh_user)
The old projects.json entry will be removed after migration.
Migrate? [yes/no/show diff]
On yes: write both files, then remove projects.<org/repo> from the user-home projects.json (preserving any other projects). If the user-home projects.json becomes { "projects": {} }, leave it — other projects may register there before they migrate.
Legacy conventions_path / templates_dir fields from the old profile: if either points at an existing file/directory, offer to move it into the canonical .claude/pm/ location. Otherwise, drop the field — it's no longer honored in v0.20.0.
Step 5: Create the runtime cache directory
This is the one directory still in user-home — it holds runtime state (issue cache, snapshots, generated reports), not config. Per-machine because syncs and report generation differ across machines.
mkdir -p ~/.claude/project-manager/cache/<slug>
Step 5b (v0.21.0+, optional): Portfolio discovery
Run this once per Linear workspace to populate the team-hierarchy and project-dependency graph used by scoped queries (--scope team:ENG+subteams, --scope project:CHK+deps).
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" discover-portfolio \
--workspace "<workspace_slug>"
The CLI issues two GraphQL queries via linear api (team hierarchy and project relations). Results merge additively into projects.json — manual overrides under overrides.team_parents and overrides.project_deps always win over Linear-sourced fields. Re-running is safe; it picks up newly added teams/relations without disrupting overrides.
When Linear MCP is configured the CLI falls back to it; otherwise the raw GraphQL passthrough is the primary path. If no workspaces are populated after this step, the user can either re-run with a different workspace slug or hand-edit projects.json.
Step 6: Configure Linear API access
Linear writes go through direct GraphQL with a personal API key. No CLI install required.
- Open
https://linear.app/<workspace>/settings/api in a browser.
- Generate a personal API key (or paste an existing one).
- Paste the key when prompted below.
The wizard verifies the key works by calling the viewer query against Linear:
node -e "
const { runQuery } = require('${CLAUDE_PLUGIN_ROOT}/hooks/lib/linear-graphql.js');
runQuery('query { viewer { id name email } }', {}, { apiKey: '<pasted-key>' })
.then(d => console.log('OK', d.viewer))
.catch(e => { console.error('FAIL', e.code); process.exit(1); });
"
On success, write the key to .claude/pm/project.local.json (which is already gitignored from Step 4c):
{
"gh_user": "your-gh-handle",
"linear_api_key": "<the-key>"
}
If verification fails, re-prompt — do not save a bad key.
Note: If the user already exports LINEAR_API_KEY in their shell profile, skip this step — the plugin reads it automatically at runtime via process.env.LINEAR_API_KEY. Only store the key in project.local.json if the env var isn't set.
6e. Reconcile manifest label casing against the workspace (NTH-514)
The pm-templates manifest ships with a default casing convention for the Linear label arrays (bug/feature/task), but Linear's UI-default workspace auto-creates capitalized labels (Bug/Feature/Task). Without reconciliation, the first pm-issue.js create against the new repo silently bootstraps duplicate lowercase labels (NTH-513). This step prevents that.
Run the dry-run first:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" reconcile-labels --team-key <team_key>
The verb compares the in-repo .claude/pm/templates/_manifest.json against linear label list --team <team_key> --json (case-insensitive). It returns:
rewrites — labels in the manifest with casing different from the tracker (e.g., manifest "bug" vs Linear "Bug"). Each is {tag, current, proposed}.
missing — labels in the manifest with no case-insensitive match in the tracker (e.g., the default spike tag's "spike" / "investigation" against a stock workspace that has neither).
unchanged — count of manifest entries that already match the tracker exactly.
Show the operator the results as a one-line summary:
"Found 3 casing rewrites (bug→Bug, feature→Feature, task→Task) and 3 missing labels (chore, spike, investigation). Apply rewrites? Create missing labels?"
These are two separate decisions — operators often want the casing fix but not the tracker writes.
Apply rewrites (manifest file write, no tracker mutation):
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" reconcile-labels --team-key <team_key> --apply
Only on operator confirm. Updates the in-repo _manifest.json to use the tracker's actual casing. Safe — no tracker writes.
Apply rewrites AND create missing labels (tracker mutation):
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" reconcile-labels --team-key <team_key> --apply --create-missing
Only on operator confirm. Adds --create-missing to also create each missing label in the workspace via linear label create -n <name> --team <team_key>. Safe — confirm-gated; preserves any failures in the applied.createFailures array (e.g., if a label exists with a name the CLI considers different).
On re-run (operator runs /pm-setup against a repo that already has an in-repo manifest): re-run the same dry-run + offer-and-confirm flow. The reconciliation is idempotent — if the manifest already matches the tracker, unchanged == N and rewrites == [] / missing == [], and the wizard says "no drift detected" and moves on.
Escape hatch: pass --manifest-path <path> to operate on a specific manifest file instead of the in-repo override. Useful for auditing the plugin default.
Step 7: Confirm
Print a single confirmation block:
✓ Project registered: <displayName>
Repo: <org/repo>
Config dir: .claude/pm/ (commit project.json, conventions.md, templates/; project.local.json is gitignored)
project.json ✓ written (shared facts)
project.local.json ✓ written (gh_user — gitignored)
conventions.md <✓ | -—> (or "not initialized — using plugin defaults")
templates/ <✓ | -—> (or "not initialized — using plugin defaults")
Linear workspace: <workspace_name>
Linear team: <team_key> (<team_id>)
Linear project: <project_name> (or "none — issues will be team-scoped")
Linear auth: <source> (env / local-config / not configured)
Spec source: <list> (or "default — markdown only")
Feedback channel: <type:target> (or "default — matches issue tracker")
Handoff channels: <list> (or "not configured — pm-handoff disabled")
Suggested next step:
git add .claude/pm/project.json .claude/pm/conventions.md .claude/pm/templates/ .gitignore
git commit -m "chore(pm): adopt in-repo project-manager config"
Run /pm to get your first project briefing.
Re-run /pm-setup later to update the config, add the CLI, wire up pm-spec preferences, initialize conventions, or migrate from a pre-v0.20.0 profile.
Diagnostics
If something doesn't take, the canonical health check is:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-cache.js" self-check
Returns { healthy, checks, projectsCount, tooling: { linearAuth, gh } }. Use this to confirm projects.json exists, the lib is intact, Linear API key auth is configured, and gh is detectable on PATH.