| name | pm-proficiency |
| description | 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). |
| version | 0.44.4 |
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:
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:
const evidence = pe.combineEvidence(
{ assigned: assignedIssues, commented: commentedIssues, codeAreas },
{ exclude },
);
const buckets = pe.bucketProficiency(evidence);
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:
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, (cs.readTeam(slug) || {}).members || []);
const member = roster.find(m => m.handle.toLowerCase() === handle.replace(/^@/,"").toLowerCase());
const issues = Object.values((cs.readIssues(slug) || {}).issues || {});
const mine = issues.filter(i => member && i.assignee && i.assignee === member.name);
const buckets = pe.bucketProficiency(pe.combineEvidence({ assigned: mine }));
process.stdout.write(JSON.stringify({ name: member && member.name, count: mine.length, buckets }, null, 2));
' "<slug>" "<@handle>" "<repo>/.claude/pm/team.md"
Replace <slug>, <@handle>, and <repo> with runtime values. This one-liner inspects the assigned-only slice for a quick read; the full skill flow folds in commented-issue and git code-area evidence via combineEvidence as described in Step 2.
Limits
Be honest with the operator about what the estimates can and cannot say:
- Weighted, not exhaustive. Assigned issues dominate (weight 1.0); commented issues (0.5) and code areas (0.3) only nudge. A member who only reviews or only commits without ever being assigned will still register, but more faintly than someone who owns issues outright.
- Cold or empty cache → thin estimates. If the cache has never been synced, or the last sync was before the member's recent work, the assigned/commented sources will be sparse. In that case, tell the operator: "The cache looks cold — run
/pm --refresh first and then re-run pm-proficiency for a fuller picture."
- No evidence in any source → empty buckets (not a failure). A member with no assigned issues, no comments, and no matching git authorship simply gets
strong: [] and familiar: []. Report this honestly — "no evidence found across assigned/commented/code sources; buckets are empty" — rather than leaving the field blank with no message.
- Code areas are directories, not domains.
gitAreasFromNameOnly keys on file paths, which only loosely map to skill labels (auth the label vs src/auth the dir). That's why code evidence is weighted lowest — treat it as a tiebreaker, not a source of truth. On GitHub, where the comment source is unavailable, git is the only participation signal, so weigh the preview accordingly. Note that gitAreasFromNameOnly keys on directory segments, so files that live at the repo root (no / in the path — README.md, top-level configs) contribute no code-area evidence; a member who works mostly in root-level files will under-register on the code source.
- Git email must match. Code-area evidence depends on
member.email matching the git commit author email. A member who commits under a different email than their tracker email contributes no code evidence.
- Labels are only as good as the team's labeling discipline. If the project rarely labels issues with domain areas, the issue-based sources will be thin regardless of cache completeness.
What this skill does NOT do
- Does not assign issues — that is pm-organize, which calls
rankAssignees against the already-populated roster.
- Does not edit identity fields — name, email, status, and userId are cache-only. team.md edits are limited to non-PII curated fields.
- Does not fine-score — only two coarse buckets (strong / familiar). There is no numeric skill score, no percentile, no ranking.
- Does not write without confirmation — the preview-then-apply discipline is unconditional.
- Does not manually edit the roster — for hand-editing skills, owns, or aliases, use
pm-team edit.