Claude
Skills
Sign in
Back

pm-proficiency

Included with Lifetime
$97 forever

Use this skill when the user says "estimate skills", "what is X good at", "infer proficiency", "populate the roster from history", "estimate the team's skills", "/pm-proficiency", or wants to derive a member's skill areas from their issue and code history. Estimates a member's skill areas from three weighted evidence sources — domain labels on issues assigned to them (strongest), labels on issues they commented on (participation), and code areas they touched in git (weakest) — buckets them coarsely as strong or familiar, and writes them into team.md preview-then-apply. Never writes without confirmation. Distinct from pm-team (which manages manual curated edits to the roster) and pm-organize (which assigns issues using the roster).

General

What this skill does


# PM Proficiency

Makes the roster self-populating. Instead of hand-typing a member's skills into team.md, infer them from evidence. The result is coarse: **strong** (weighted evidence ≥3) and **familiar** (some evidence but below the threshold). No fine scores, no confidence percentages — just a signal good enough to help pm-organize make reasonable assignment recommendations.

Evidence comes from three sources, each weighted by how strongly it signals real proficiency:

| Source | Signal | Weight | How it's gathered |
|--------|--------|--------|-------------------|
| **Assigned issues** | They own the work | `1.0` | cached issues where `issue.assignee === member.name` |
| **Commented issues** | They participate in the area | `0.5` (`COMMENT_WEIGHT`) | `tracker.fetchCommentedIssueLabels({ userId })` |
| **Code areas** | They touch the module | `0.3` (`CODE_WEIGHT`) | `git log --author=<member.email> --name-only` → `gitAreasFromNameOnly` |

The three sources are folded together in one pass by `proficiency-estimator.combineEvidence`, which canonicalizes label casing (so `Backend`/`backend` never split) and applies the type/exec exclusions uniformly. The weights live as exported constants on the estimator — tune them there, not here.

## Usage

```
/pm-proficiency @handle     # estimate one member
/pm-proficiency             # sweep the entire roster
```

- `@handle` — estimate a single member; omit to run over every member in the roster.

## When to use / not

**Use when:**
- A member's `strong:` and `familiar:` lines in team.md are empty or noticeably stale.
- Onboarding a new team member who has a history in Linear but whose team.md entry is blank.
- The roster is freshly populated from `pm-team sync` and skills are all empty.

**Do not use when:**
- You want to manually edit skills, owns, or aliases — that is `pm-team edit`.
- You want to assign an issue to the best-fit member — that is `pm-organize`.
- The cache is cold or the member has fewer than a handful of assigned issues — the estimates will be thin; see Limits.

## Workflow

### Step 1 — Resolve the target member(s)

Read team.md (`team-roster.parseTeamMd`), read the synced cache (`cache-store.readTeam(slug)`), and merge them (`team-roster.mergeRoster`) to produce each member's `name` field. The `name` is the display name the tracker stores in `issue.assignee`.

If `@handle` was supplied, filter to that member. If the handle is absent from both halves of the merged roster, stop and tell the operator.

Resolve the team.md path from project config: `project-config.inRepoPaths(repoRoot).dir + '/team.md'` (same path pm-team uses).

### Step 2 — Gather evidence

Gather up to three sources. The first is mandatory; the other two are best-effort — if a source is unavailable, skip it and proceed (note which sources contributed in the preview so the operator understands the basis).

**a. Assigned issues (always).** Read the cached issues (`cache-store.readIssues(slug)`). If the cache is missing or empty AND no other source is available, stop and suggest `/pm --refresh` — never write estimates from zero evidence. Filter to issues whose `assignee` equals the member's `name` (exact string, case-sensitive).

**b. Commented issues (if the tracker supports it).** Construct a tracker — `const tracker = require(CLAUDE_PLUGIN_ROOT + '/hooks/lib/tracker.js').getTracker(provider, { apiKey })` (provider + apiKey come from the merged project config, same as every other pm skill). If `tracker.capabilities.commentEvidence` is truthy (Linear), call `await tracker.fetchCommentedIssueLabels({ userId: member.userId })` — `userId` comes from the synced cache half of the roster. Returns deduped `[{ identifier, labels }]` for issues the member commented on. On GitHub this capability is `false` (the call throws `TRACKER_UNSUPPORTED`); skip it — the git source below covers participation there.

Treat this source as best-effort: wrap the call in try/catch and, on any error (transport, auth, an unexpected schema response), skip comment evidence and proceed with the other sources rather than failing the whole estimate. The GraphQL filter (`comments(filter: { user: { id: { eq } } })`) is verified against Linear's published schema, but a live API hiccup should still degrade gracefully. If the tracker prints a "comment evidence truncated" warning, the member has more than ~5000 comments and participation is slightly understated — note that in the preview.

**c. Code areas (if the member has a known email and you're in a git repo).** Run, read-only:

```bash
git log --author="<member.email>" --name-only --pretty=format: --since="18 months ago" | sort -u
```

Pass the output to `proficiency-estimator.gitAreasFromNameOnly(output, { depth: 2 })` → `{ area: fileCount }`. `depth: 2` keys areas like `plugins/project-manager` rather than just `plugins`; pick a depth that matches how the repo is laid out. Skip this source if the member has no email or `git log` returns nothing.

### Step 3 — Estimate

Determine the exclude set: the defaults (`bug`, `feature`, `task`, `spike`, `hitl`, `afk`, `duplicate`, `triage`) plus any custom type tags the project defines in its templates manifest (`_manifest.json`), so they don't bleed into the area counts.

Fold all gathered sources together in one pass:

```js
const evidence = pe.combineEvidence(
  { assigned: assignedIssues, commented: commentedIssues, codeAreas },
  { exclude },
);
const buckets = pe.bucketProficiency(evidence); // default strongThreshold 3
```

`combineEvidence` weights assigned at 1.0, commented at `COMMENT_WEIGHT` (0.5), and each code-area file-touch at `CODE_WEIGHT` (0.3), then merges by canonical (case-insensitive) area name. Because the weights are fractional, the strong threshold of 3 now means roughly "three assigned issues, or six commented ones, or ten files in an area, or any weighted combination."

It also dedupes by issue identifier across the assigned and commented sets: a member is usually a commenter on the issues they own, so an issue that appears in both is counted once at the assignee weight — not 1.0 + 0.5. (Pass the cached `identifier`/`id` through on each issue so the dedupe can see it; the cache issues and `fetchCommentedIssueLabels` both carry it.)

### Step 4 — Preview

Show the proposed `strong:` / `familiar:` values for each member alongside the diff against their current team.md block. This step is **mandatory** — never skip straight to writing. The preview discipline mirrors `pm-improve --apply`: the operator sees exactly what will change before anything touches the file.

If there is no change (buckets match what's already in team.md), say so and stop — do not ask for confirmation on a no-op.

### Step 5 — Apply on confirmation

Ask the operator to confirm. On "yes" / "go ahead" / "do it":

Call `proficiency-estimator.applyToTeamMd(currentTeamMd, handle, buckets)` for each member, chaining the transforms if sweeping the whole roster (output of one is input of the next). Write the result to `<repo>/.claude/pm/team.md`.

**Never write without confirmation.** team.md is checked in and non-PII — strong/familiar bullets are safe to commit. Never write identity fields (name, email, status, userId) into team.md regardless of operator input; those fields belong only in the synced cache.

## The estimation node one-liner

Runnable example — inspect one member's buckets against the live cache without touching any files:

```bash
node -e '
  const fs = require("fs");
  const cs = require(process.env.CLAUDE_PLUGIN_ROOT + "/hooks/lib/cache-store.js");
  const tr = require(process.env.CLAUDE_PLUGIN_ROOT + "/hooks/lib/team-roster.js");
  const pe = require(process.env.CLAUDE_PLUGIN_ROOT + "/hooks/lib/proficiency-estimator.js");
  const slug = process.argv[1], handle = process.argv[2], teamMdPath = process.argv[3];
  const curated = tr.parseTeamMd(fs.existsSync(teamMdPath) ? fs.readFileSync(teamMdPath,"utf8") : "");
  const { roster } = tr.mergeRoster(curated, (c

Related in General