Claude
Skills
Sign in
Back

pm-setup

Included with Lifetime
$97 forever

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.

Backend & APIs

What this skill does


# 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

```bash
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:

1. **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.
2. **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:

```bash
gh auth status 2>&1 | grep "Logged in to github.com account"
```

Then ask which to use. Validate by switching:

```bash
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": "[email protected]" }` (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

```bash
mkdir -p "$REPO_ROOT/.claude/pm"
```

### Step 4b: Write `project.json` (shared, checked-in)

```jsonc
// <repo-root>/.claude/pm/project.json
{
  "$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)

```jsonc
// <repo-root>/.claude/pm/project.local.json
{
  "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:

```bash
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:

```javascript
const shared = readJsonSafe(`${repoRoot}/.claude/pm/project.json`);
const local  = readJsonSafe(`${repoRoot}/.claude/pm/project.local.json`);
const config = { ...shared, ...local };   // local wins per-key
```

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 nudg

Related in Backend & APIs