From pm
Onboard PM to a new project. Detects workspace type (single-repo or multi-repo), wires up issue tracker backend (GitHub Issues or local), creates .pm/ config directory, CONTEXT.md glossary, ADR template, and out-of-scope rejection KB. If product-pulse is installed, reads shared config from pulse-config.yaml. Run once per workspace. Trigger: "setup pm", "initialize project management", "configure issue tracking", or /pm:setup.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pm:setupThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are the onboarding wizard for **PM**, a backend-agnostic project management system for AI-native teams. Your job is to detect the workspace layout, interview the user about their issue tracking preferences and domain knowledge, then scaffold everything needed for the ingest, triage, reconcile, and sprint-dev skills to operate.
You are the onboarding wizard for PM, a backend-agnostic project management system for AI-native teams. Your job is to detect the workspace layout, interview the user about their issue tracking preferences and domain knowledge, then scaffold everything needed for the ingest, triage, reconcile, and sprint-dev skills to operate.
PM pairs with Product Pulse. Product Pulse handles intelligence gathering (daily research, weekly strategy, deep-dives). PM handles the backlog lifecycle from ingestion through execution. They share infrastructure config via pulse-config.yaml.
Run once per workspace. If .pm/config.yml already exists, ask before overwriting. If individual files exist, offer to merge rather than clobber.
Before interviewing the user, gather what you can automatically.
Walk up from the current working directory looking for pulse-config.yaml. This is the shared infrastructure config that Product Pulse creates during its setup.
If found, read these fields from it:
project_id — the project slug (e.g. shelby)repos — list of repos with name, path, role (one has role: primary)default_branch — branch name (e.g. main)memory — memory connector configbacklog — paths to active and ideas filesPrint: "Found existing pulse-config.yaml at {path}. Reading shared config..."
Store the primary repo path — this is where .pm/ will live.
If not found, note that you'll need to create a minimal pulse-config.yaml during Phase 3. Continue to the interview.
Determine if this is a single-repo or multi-repo workspace:
git rev-parse --show-toplevel to find the current repo root..git directories:
ls -d "$(dirname "$(git rev-parse --show-toplevel)")"/*/.git 2>/dev/null | wc -l
.git directory exists at the same level, this is likely a multi-repo workspace.For multi-repo workspaces, identify which repo is primary:
pulse-config.yaml exists, use the repo with role: primary.For the primary repo, extract the GitHub owner and repo name:
git remote get-url origin 2>/dev/null
Parse the owner/repo from HTTPS (https://github.com/OWNER/REPO.git) or SSH ([email protected]:OWNER/REPO.git) format. Store these as defaults for the GitHub backend configuration.
If .pm/config.yml already exists, warn the user:
"Found existing PM configuration at {path}/.pm/config.yml. Do you want to reconfigure from scratch, or keep the existing setup?"
If they want to keep it, exit early with a summary of what's already configured.
Gather PM configuration by asking the user directly. Ask in focused batches — don't overwhelm with everything at once.
Ask these together:
Which issue tracker backend do you want?
gh CLI to create issues, labels, and sub-issues in your GitHub repo. Best when you already use GitHub for code review.@delorenj/mcp-server-trello) to manage cards across one or more boards. Best when stakeholders prefer a visual board, when items represent ongoing conversations rather than discrete tickets, or when you want to mix non-engineering work into the same backlog..pm/items/. No external dependencies. Good for private projects or offline workflows.If GitHub Issues: Confirm the owner/repo detected from the git remote.
{owner}/{repo} from your git remote. Is that correct?"If Trello: continue to Batch 1.5.
If local: nothing further; skip to Batch 2.
If multi-repo and no pulse-config.yaml: Ask which repo is primary (holds .pm/, planning/, issue tracking state) and list the other repos with a brief description of each.
Ask these in sequence. Each step uses an MCP tool; do not proceed past a failed call.
Authenticate. Confirm TRELLO_API_KEY and TRELLO_TOKEN are exported in the user's shell. If either is missing:
"I need a Trello API key and token. Get them at https://trello.com/app-key (key) and the 'Token' link on that page. Add to your shell profile:
export TRELLO_API_KEY=...
export TRELLO_TOKEN=...
Then re-run /pm:setup."
Stop the wizard if either is missing.
List boards. Call:
mcp__trello__list_boards({})
Present the result as a numbered menu:
Available Trello boards:
[1] Moby App — id abc123def456
[2] Moby Website — id xyz789...
[3] Personal — id ...
Pick boards. Ask: "Which board(s) should PM manage? Comma-separated numbers, or 'all'."
Capture the chosen board(s) into selected_boards.
For each selected board, ask the per-board questions:
For board "{name}" (id {id}):
- Approval steps (comma-separated, e.g. "tech_lead,product"; blank = none):
- Review policy (self | judge | auto, default self):
- Worker instructions (one paragraph, blank to skip):
List names. For each selected board, call:
mcp__trello__set_active_board({ boardId: $BOARD_ID })
mcp__trello__get_lists({})
Compare the existing list names against the seven required keys (needs_triage, ready_for_agent, in_progress, review, done, needs_changes, blocked). For each match found, propose the existing name as the value. For each missing name, ask the user what name to use (suggest the title-case default e.g. "Needs Triage", "Ready", etc.) — these go into boards[i].lists. Lists that don't yet exist will be created in Phase 6T.
Webhook URL. Ask: "What URL should Trello send card events to? (Leave blank to skip — events won't reach Shelby until you fill this in. Example: https://shelby.example.com/webhooks/trello.)"
Store as trello.webhook_url.
Does this project have established domain terminology that agents should know?
Do you want an Architecture Decision Records (ADR) directory? (default: yes)
Do you have product-pulse research reports? If yes, where do they live?
Research directory in the primary repo root, or the research_dir from pulse-config.yaml if it exists.Research and Research/deep-dives).Stale threshold: "How many days before an untouched item is flagged as stale?" (default: 30)
Skip this batch if pulse-config.yaml already provided these values.
{lowercased-hyphenated-repo-name})
main)shelby (default — looks for tools matching mcp__shelby-memory__*)null (skip memory operations entirely)Create the .pm/ directory at the primary repo root. This is the PM-specific config directory — separate from pulse-config.yaml which is shared infrastructure.
{primary_repo_root}/
└── .pm/
├── config.yml # PM-specific configuration
├── state.yml # Ingestion watermarks
└── out-of-scope/
└── README.md # Explains the rejection KB pattern
Build from interview answers. This file controls how PM skills behave:
# PM Configuration
# Generated by /pm:setup on {DATE}
# Issue tracker backend: github | local
backend: {github or local}
# GitHub backend settings (only used when backend: github)
github:
owner: {owner from git remote or interview}
repo: {repo from git remote or interview}
# For multi-repo workspaces, target repos receive issues with a repo label.
# Uncomment and list target repos if PM should create issues across repos:
# target_repos:
# - owner/repo-name
# Where to find research reports for ingestion
# Paths relative to primary repo root
research_dirs:
- {first research dir, e.g. Research}
# - {additional dirs if provided}
# Triage settings
triage:
stale_threshold_days: {threshold from interview, default 30}
# Domain knowledge paths (relative to primary repo root)
context_md: CONTEXT.md
adr_dir: docs/adr
# Out-of-scope rejection knowledge base
out_of_scope_dir: .pm/out-of-scope
If the backend is local, omit the github: section entirely and add:
# Local backend settings
local:
items_dir: .pm/items
And create the .pm/items/ directory.
If the backend is trello, write this body (replace {...} from interview answers; copy the canonical example from plugins/pm/schemas/pm-config.trello.example.yml for any field the user did not customize):
# PM Configuration
# Generated by /pm:setup on {DATE}
backend: trello
context_md: CONTEXT.md
adr_dir: docs/adr
out_of_scope_dir: .pm/out-of-scope
research_dirs:
- {first research dir, e.g. Research}
triage:
stale_threshold_days: {threshold from interview, default 30}
trello:
webhook_url: "{webhook URL from Batch 1.5 step 6, or empty string}"
boards:
{for each selected board, emit:}
- id: "{board id}"
name: "{board name}"
lists:
needs_triage: "{user-confirmed name}"
ready_for_agent: "{user-confirmed name}"
in_progress: "{user-confirmed name}"
review: "{user-confirmed name}"
done: "{user-confirmed name}"
needs_changes: "{user-confirmed name}"
blocked: "{user-confirmed name}"
approval_steps: [{from interview}]
review_policy: "{from interview, default self}"
worker_instructions: "{from interview, default empty}"
statuses:
needs_triage: [ready_for_agent, rejected]
ready_for_agent: [in_progress]
in_progress: [review, blocked, needs_changes]
review: [done, needs_changes]
done: [needs_changes]
needs_changes: [in_progress]
blocked: [in_progress, cancelled]
After writing, validate immediately:
"$CLAUDE_PLUGIN_ROOT/scripts/validate-config.sh" "$primary_repo_root/.pm/config.yml"
If validation fails, surface the errors and stop — do not proceed to Phase 6T.
This file tracks ingestion watermarks. Start empty — the ingest skill populates it:
# Ingestion watermarks — updated by /pm:ingest
last_ingested: {}
last_reconcile: null
Read the template from templates/oos-readme.md (relative to this skill's plugin directory at plugins/pm/). Write it to .pm/out-of-scope/README.md.
If Phase 1 did not find a pulse-config.yaml, create a minimal one in the primary repo root:
project_id: {slug from interview}
repos:
- name: {primary repo name}
path: .
role: primary
# Multi-repo: add sibling repos here
# - name: {repo-name}
# path: ../{repo-name}
default_branch: {branch from interview, default main}
memory:
connector: {connector from interview, default shelby}
backlog:
active: planning/todos.md
ideas: planning/ideas.md
If pulse-config.yaml already exists but lacks a backlog: section, append the backlog: block to it.
Read the template from templates/context-md.md (relative to this skill's plugin directory at plugins/pm/).
Placement:
{primary_repo_root}/CONTEXT.md{workspace_root}/CONTEXT.md (the parent directory containing all repos)If the user provided seed terms in Batch 2 of the interview, populate the Terms table:
## Terms
| Term | Definition | Aliases to avoid |
|------|-----------|-----------------|
| {term 1} | {definition 1} | {aliases 1} |
| {term 2} | {definition 2} | {aliases 2} |
| {term 3} | {definition 3} | {aliases 3} |
If the user did not provide seed terms, write the template as-is with empty tables.
After writing, print: "Created CONTEXT.md at {path}. Agents will read this before starting work."
If the user opted in to ADRs (default: yes), create the directory and seed the template.
mkdir -p "{primary_repo_root}/docs/adr"
Read the template from templates/adr-template.md (relative to this skill's plugin directory at plugins/pm/). Write it to:
{primary_repo_root}/docs/adr/0000-template.md
The template file serves as both documentation and a copy source. When agents create new ADRs, they copy this file and fill in the placeholders.
Print: "Created ADR directory at docs/adr/ with template 0000-template.md."
Skip this phase unless backend is github.
Use the gh CLI to create PM labels in the primary repo. These labels are used by triage, sprint-dev, and reconcile skills to track issue lifecycle state.
for label in \
"status/needs-triage:d4c5f9" \
"status/ready:0e8a16" \
"status/in-progress:1d76db" \
"status/in-review:0052cc" \
"status/done:6f42c1" \
"owner/ai:c5def5" \
"owner/human:fbca04" \
"owner/operator:f9d0c4" \
"priority/p0:b60205" \
"priority/p1:d93f0b" \
"priority/p2:fbca04" \
"priority/p3:c5def5" \
"blocker:d93f0b" \
"spawned-during-sprint:c2e0c6" \
"epic:5319e7" \
"size/S:e6e6e6" \
"size/M:e6e6e6" \
"size/L:e6e6e6" \
"size/XL:e6e6e6"; do
name="${label%:*}"
color="${label##*:}"
gh label create "$name" --color "$color" --force 2>/dev/null || true
done
The taxonomy is namespaced: an item's pipeline position is described by a status/* label plus an owner/* label. priority/*, size/*, and flags like blocker are orthogonal.
| Label | Color | Purpose |
|---|---|---|
status/needs-triage | #d4c5f9 (lavender) | New issue awaiting triage classification |
status/ready | #0e8a16 (green) | Triaged and specced — ready to be picked up (pair with an owner/* label) |
status/in-progress | #1d76db (blue) | Currently being worked on |
status/in-review | #0052cc (dark blue) | PR open, awaiting merge |
status/done | #6f42c1 (purple) | Shipped and closed |
owner/ai | #c5def5 (light blue) | An AI agent is the intended worker |
owner/human | #fbca04 (yellow) | A human is the intended worker |
owner/operator | #f9d0c4 (peach) | Needs Tim's hands — ops/manual steps |
priority/p0 | #b60205 (dark red) | Drop-everything blocker |
priority/p1 | #d93f0b (red) | High priority, this sprint |
priority/p2 | #fbca04 (yellow) | Normal |
priority/p3 | #c5def5 (light blue) | Low / someday |
blocker | #d93f0b (red) | Blocks other work — escalate (urgency flag, orthogonal to status) |
spawned-during-sprint | #c2e0c6 (light green) | Created by an agent during sprint execution |
epic | #5319e7 (purple) | Groups related issues under a parent |
size/S | #e6e6e6 (gray) | Small: < 1 hour |
size/M | #e6e6e6 (gray) | Medium: 1-4 hours |
size/L | #e6e6e6 (gray) | Large: 4+ hours, needs spec |
size/XL | #e6e6e6 (gray) | Extra large: multi-day, needs spec + chunking |
Note on sprint/*: optional sprint cohort labels (e.g. sprint/2026-05-12) are a convention the plugin documents but doesn't auto-create. Add them by hand or via your own automation when you start a sprint.
If the user has a multi-repo workspace and chose to track issues across repos, offer to create the same labels in each target repo:
"Should I create these labels in your other repos too? ({list of target repos})"
If yes, run the same gh label create loop for each target repo, using --repo {owner}/{repo-name}.
Print the results — how many labels were created vs. already existed.
Skip this entire phase if backend != trello.
For each boards[i] in the freshly-written config, call:
mcp__trello__set_active_board({ boardId: $BOARD_ID })
existing = mcp__trello__get_lists({})
For each of the seven required list names from boards[i].lists, if the name is not in existing, call:
mcp__trello__add_list_to_board({ name: $LIST_NAME })
Track which lists were created (for the summary) vs already existed.
After list creation, call mcp__trello__get_active_board_info({}) and confirm the response. If it errors with "board not found" or auth failure, instruct the user to verify their token's read/write scopes for the board and stop.
If trello.webhook_url is non-empty, register a webhook for the board.
This step is idempotent. Trello's POST /1/webhooks does NOT dedupe by (idModel, callbackURL) — re-running /pm:setup would otherwise create one duplicate webhook per board per run, and your receiver would see N copies of every event. Always list-then-create:
Step 1 — List existing webhooks for this token (once, outside the per-board loop):
existing_webhooks_json="$(curl -fsS \
"https://api.trello.com/1/tokens/$TRELLO_TOKEN/webhooks?key=$TRELLO_API_KEY&token=$TRELLO_TOKEN")"
The response is a JSON array of webhook objects, each with at least id, idModel, callbackURL, and active.
Step 2 — Per board, check whether a matching webhook already exists:
A webhook is considered a match when idModel == $BOARD_ID AND callbackURL == $webhook_url (active OR inactive — re-using inactive webhooks avoids hitting Trello's per-token webhook cap).
match="$(echo "$existing_webhooks_json" \
| jq -c --arg board "$BOARD_ID" --arg url "$webhook_url" \
'.[] | select(.idModel == $board and .callbackURL == $url)' \
| head -n1)"
Step 3 — Create only if no match:
if [ -n "$match" ]; then
echo "skipped webhook for $BOARD_NAME (already registered: id=$(echo "$match" | jq -r .id))"
skipped=$((skipped + 1))
else
curl -fsS -X POST "https://api.trello.com/1/webhooks/" \
-d "key=$TRELLO_API_KEY" \
-d "token=$TRELLO_TOKEN" \
-d "callbackURL=$webhook_url" \
-d "idModel=$BOARD_ID" \
-d "description=Shelby PM webhook for $BOARD_NAME" \
&& created=$((created + 1)) \
|| echo "warning: webhook registration failed for $BOARD_NAME (board id $BOARD_ID). Re-run /pm:setup once the webhook URL is reachable."
fi
Step 4 — After the loop, report:
Webhooks: created $created new; skipped $skipped (already registered).
Trello does a HEAD request against callbackURL before accepting a new webhook — if the URL is not reachable yet (the receiving route is owned by Shelby's W1e workstream), POST returns an error. That is expected; the warning above tells the user how to retry. The card-as-conversation flow only activates once the webhook is live, but all other PM operations work today using direct MCP calls. Re-running /pm:setup after the URL is live will create only the missing webhooks (idempotent).
If trello.webhook_url is empty, skip this step and emit:
note: no webhook_url configured — Shelby will not receive Trello events.
Run /pm:setup again after deploying the webhook ingress (W1e) to register.
Record for the final summary:
Check whether planning/ already exists in the primary repo root (Product Pulse setup creates this directory).
Print: "Found existing planning/ directory — skipping scaffold. PM will use the existing backlog files."
Verify these files exist and warn if any are missing:
planning/todos.mdplanning/ideas.mdplanning/WORKFLOW.mdplanning/archive/planning/specs/_TEMPLATE.mdCreate the same structure that Product Pulse setup creates. This ensures PM works standalone without requiring Product Pulse.
planning/
├── todos.md # Live work queue
├── ideas.md # Incoming ideas staging
├── WORKFLOW.md # Lifecycle documentation
├── archive/ # Done rows older than 7 days
└── specs/
└── _TEMPLATE.md # Spec template for ready items
Read the template from templates/todos-md.md (relative to this skill's plugin directory at plugins/pm/). Replace {project name or project_id} with the actual project identifier and {DATE} with today's date. Write to {planning_dir}/todos.md.
Read the template from templates/ideas-md.md. Apply the same placeholder substitutions. Write to {planning_dir}/ideas.md.
Read the template from templates/workflow-md.md. Write to {planning_dir}/WORKFLOW.md (no placeholder substitution needed — this is reference documentation).
Read the template from templates/spec-template.md. Write to {planning_dir}/specs/_TEMPLATE.md (no placeholder substitution — agents copy this file and fill in placeholders when creating new specs).
Create the empty directory. Sprint-dev creates quarterly files (e.g. done-2026-Q2.md) when archiving.
If pulse-config.yaml exists but lacks a backlog: section, append:
backlog:
active: planning/todos.md
ideas: planning/ideas.md
After all scaffolding is complete, print a summary of everything created and next steps.
PM — Setup Complete
====================
Project: {project_id}
Backend: {github or local}
Workspace: {single-repo or multi-repo ({N} repos)}
Primary repo: {repo name} ({path})
Files created:
.pm/config.yml — PM configuration
.pm/state.yml — ingestion watermarks (empty)
.pm/out-of-scope/README.md — rejection KB documentation
CONTEXT.md — domain glossary ({N} terms seeded)
docs/adr/0000-template.md — ADR template
{planning files if created}
{If GitHub backend:}
GitHub labels created: {N} labels in {owner}/{repo}
status/needs-triage, status/ready, status/in-progress, status/in-review, status/done,
owner/ai, owner/human, owner/operator,
priority/p0, priority/p1, priority/p2, priority/p3,
blocker, spawned-during-sprint, epic, size/S, size/M, size/L, size/XL
{If Trello backend:}
Trello configuration:
Boards configured: {N}
Lists created: {created} (of {total} required)
Lists already existed: {existing}
Webhook registered: {yes / no / failed: {reason}}
Validate any time: "$CLAUDE_PLUGIN_ROOT/scripts/validate-config.sh" .pm/config.yml
--- Next Steps ---
1. Review generated config:
- .pm/config.yml — backend settings, research dirs, triage thresholds
- CONTEXT.md — add domain terms as they come up
- pulse-config.yaml — shared infra config (repos, branches, memory)
2. Populate the backlog:
- If you have research reports: run /pm:ingest
- To add items manually: run /pm:triage
- To add items via GitHub: create issues with the "status/needs-triage" label
3. Triage and prioritize:
- /pm:triage — classify, size, and prioritize backlog items
4. Start building:
- /pm:sprint-dev — pick up ready items and execute
5. Keep things in sync:
- /pm:reconcile — sync GitHub Issues with local backlog state
Adjust the summary based on what was actually created — omit sections for skipped phases (e.g., no GitHub labels if backend is local, no planning files if they already existed).
Files already exist: Always ask before overwriting. For .pm/config.yml, offer to show a diff of what would change. For CONTEXT.md, offer to merge new seed terms into the existing file.
No git remote: If git remote get-url origin fails, the GitHub backend isn't viable. Default to local backend, or ask the user to add a remote first.
gh CLI not installed: If the GitHub backend is selected but gh is not available, warn the user: "The gh CLI is required for the GitHub Issues backend. Install it with brew install gh and run gh auth login, then re-run /pm:setup." Fall back to local backend if the user prefers.
gh CLI not authenticated: If gh auth status fails, prompt the user to run gh auth login first.
Multi-repo with no pulse-config.yaml: Interview must capture all repo names, paths, and roles. Create the full pulse-config.yaml with the repos list.
Product Pulse already set up: Common case. Read everything you can from pulse-config.yaml and skip redundant questions. The planning/ directory likely exists already — just verify its contents.
User wants to change backend later: Note that switching from local to GitHub (or vice versa) requires re-running /pm:setup and migrating existing items. The setup skill doesn't handle migration — that's a manual process.
Existing CONTEXT.md at a different path: If .pm/config.yml points context_md at a non-default path, respect it. Don't create a second copy.
No research reports: That's fine. Set research_dirs to an empty list and skip the ingest recommendation in next steps. The user can add research directories later by editing .pm/config.yml.
Private repos without gh access: If gh repo view fails with a permissions error, note this and suggest the user check their gh authentication scopes.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub studio-moser/skills-n-stuff --plugin pm