| name | pm-notes |
| version | 0.44.4 |
| description | Use this skill when the user shares a meeting transcript or notes file and wants to know which tracked issues the conversation touched, what adjustments the discussion implies (status changes, scope updates, comments, sub-issues, closures, pivots), or wants a markdown report of proposed changes. Trigger phrases "process this meeting", "any updates from this transcript", "match this transcript to issues", "review my meeting notes", "summarize what we decided", "/pm-notes". Also use proactively when the user shares a .srt, .vtt, or transcript-shaped markdown file in a context where the project-manager plugin is configured — even if they don't explicitly ask for adjustment matching, offer to run pm-notes against it. Builds on the existing project-manager workspace cache, dedup.js similarity, and conventions/vocabulary customization layers. After rendering the dry-run report it can optionally apply accepted proposals (Step 8) by routing each through the existing pm-* verbs/skills, with provenance stamped on every write. |
PM Notes — Transcript-to-Issue Matching
Turn a meeting transcript or a notes file into a dry-run markdown report of proposed tracker adjustments — which tracked issues the conversation touched, what status changes the discussion implies, what scope updates were called out, what comments / sub-issues / closures / pivots are warranted. The report itself writes nothing; it is a proposal. The operator then decides what to apply, and Step 8 carries each accepted proposal into the existing pm-* verbs/skills (with provenance stamped on every write). Nothing reaches the tracker without the operator picking it.
The pipeline is deterministic for the easy cases and asks Claude for help only where the deterministic pass is structurally unable to decide — paraphrased references ("the Polaris work", "what Megan flagged") and judgment-call action items. Everything else runs without LLM involvement, so the report is reproducible across runs.
When to run this
- The user shares a
.srt, .vtt, transcript-shaped .md, or a meeting-notes file and asks anything resembling "what's in here?" or "what should I update?"
- The user says "process this meeting", "any updates from this transcript", "match this transcript to issues", "review my meeting notes", "summarize what we decided", or invokes
/pm-notes.
- Proactive trigger. The user drops a transcript file path into the conversation in a repo where
.claude/pm/project.json exists, even if they don't explicitly ask. Offer: "Want me to run pm-notes against that and surface any tracker adjustments it implies?"
When NOT to run this
- The user wants a project status briefing → route to
pm-status.
- The user wants to file a new issue from scratch (not from a transcript) → route to
pm-issues.
- The user wants to enrich a specific issue with repo context → route to
pm-improve.
- The user wants to send a handoff brief to a teammate → route to
pm-handoff.
- The transcript is a live stream / not yet a file — pm-notes is file-input only.
- The conversation is purely social with no tracker-relevant content — running pm-notes will produce an empty report, which is fine, but don't burn cycles on it if you can tell up front.
How it works — six stages, two Claude-in-the-loop boundaries
- Ingest. Read the file, autodetect format (SRT / VTT / markdown), reassemble into
{ speaker, timestamp, text } segments.
- Pass 2a — deterministic extraction. Walk the segments with regex + customization cues and emit
issue-ref, decision, action-item, scope-change, close-signal, pivot-signal units. Segments with weak signal but enough length are flagged for Pass 2b.
- Pass 2b — Claude-in-the-loop refinement (boundary 1). Read the flagged segments + the project's vocabulary / patterns prose, decide whether each is a paraphrased reference or a judgment-call unit, write
pass2.json, and re-run the CLI.
- Match. Each unit's text is scored against the workspace cache via
dedup.js (body-aware weighting, containment, bigrams), banded into confirmed (≥ pm_notes.confidence_threshold) and near-miss (grey zone) candidates.
- Rerank — Claude-in-the-loop precision pass (boundary 2, Phase B). The matcher hands its banded candidates back; Claude judges each (unit, candidate) pair (
related + calibrated confidence), promoting real grey-zone links and rejecting keyword coincidences. Confirmed matches feed the self-teaching vocabulary loop. Skippable via pm_notes.rerank: false.
- Classify + render. Each surviving matched unit becomes an adjustment proposal (
status-change, comment, sub-issue, close, scope-update, pivot), rendered into one markdown report grouped by tracked issue, with Near-misses and Unmatched units tails.
Speaker attribution (roster-aware)
Each segment carries a speaker string. During extraction, pm-notes resolves it to a team-roster member via team-roster.resolveSpeaker (full name / alias / unique first name), falling back to the manual pm_notes.speaker_identities map in project.json (the roster wins when both match). The resolved @handle rides each discussion unit through classification, and the report attributes proposals with "— raised by @handle". This degrades silently: if the roster isn't synced (/pm-team sync hasn't run) and no speaker_identities entry matches, attribution is simply omitted — nothing breaks. Run /pm-team to populate the roster and /pm-proficiency to enrich it. Troubleshooting: if attribution is missing despite a populated team.md, confirm /pm-team sync ran under this project's slug — pm-notes reads the roster cache keyed by project.json's slug, so a team-sync invoked with a different --slug (e.g. the org/repo key) writes a team.json pm-notes won't find.
How to run pm-notes
Step 1: Confirm the transcript path
The user should give you a path on disk. If they paste the transcript inline, save it first so the CLI has a file to read:
cat > "$CLAUDE_JOB_DIR/notes-input.md" <<'EOF'
<pasted transcript content>
EOF
Pick the extension that matches the content — .srt for SubRip, .vtt for WebVTT, .md otherwise. The ingest stage autodetects from content shape, not extension, but a sane extension keeps the report header readable.
Step 2: Invoke the deterministic pipeline
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-notes.js" report \
--file <transcript-path> \
[--scope <expr>] \
[--save <output-path>]
Default scope is team:<linear_team_key> from project.json (filled in by pm-setup). Pass --scope to override — same grammar as pm-status / pm-report:
| Form | Meaning |
|---|
team:ENG | Issues with team_key == ENG |
team:ENG+subteams | ENG + transitive children |
project:CHK | One project |
workspace:nthplus | Whole workspace |
all | Every known workspace |
The CLI:
- Writes the rendered markdown report to stdout.
- Writes status / Pass 2b handoff hints to stderr.
- Exits non-zero on hard failures, emitting
{ ok:false, error, code, hint? } JSON on stderr.
If you see CACHE_STALE in the error, run /pm --refresh first — the matcher reads the workspace cache and refuses to guess against a missing one.
Step 3: Handle Pass 2b refinement (if needed)
If stderr contains a line like:
pm-notes: 3 segment(s) flagged for Pass 2b refinement
pm-notes: wrote handoff -> /tmp/notes/pass1.json
pm-notes: invoke the pm-notes SKILL to draft Pass 2b units, then re-run with --pass2-file <path>
…the deterministic pass found segments that look like they might contain a paraphrased reference or a judgment-call unit but couldn't decide on its own. This is the load-bearing Claude-in-the-loop step. Don't skip it — the deterministic units are already in the report, but paraphrased references like "the Polaris work" will only be matched if you emit them here.
3a. Read the handoff.
cat "$CLAUDE_JOB_DIR/notes/pass1.json"
The shape is:
{
"flaggedSegments": [
{ "index": 5, "segment": { "speaker": "Megan", "timestamp": "00:04:12", "text": "..." } },
...
],
"vocabularyProse": "...free-form context from conventions.md ## Notes vocabulary...",
"patternsProse": "...free-form context from conventions.md ## Notes patterns...",
"pass2OutputShape": {
"additionalUnits": "[ { kind, sourceSegmentIdx, sourceExcerpt, extractedText, refCandidate } ]"
}
}
3b. Read vocabularyProse first. This is the project's hand-tuned context for paraphrased-ref matching. If it says "Polaris = operations-adjustment work, see issues tagged finance:polaris", then a flagged segment mentioning "Polaris" is a strong paraphrased-ref candidate — emit it. If vocabularyProse is empty, you're working from general English knowledge; be more conservative.
3c. For each flagged segment, decide.
| Question | If yes, emit unit kind | Notes |
|---|
| Is this a paraphrased reference to a tracked issue? (e.g., "the Polaris work", "what Megan flagged last week", "that auth thing") | paraphrased-ref with refCandidate set to the descriptive phrase | The matcher will fuzzy-match refCandidate against issue titles in the workspace cache. |
| Is this a decision the deterministic cues missed? (no "decided"/"agreed"/etc. but the meaning is clearly a decision) | decision | refCandidate = null |
| Is this a scope change with implications? ("we're not going to support X after all", "let's also include Y") | scope-change | refCandidate = null |
| Is this a judgment-call action item? ("Megan, can you take a look?" without "TODO" / "follow up") | action-item | refCandidate = null |
| None of the above | Skip | Not every flagged segment produces a unit — most are noise. |
Be conservative. The cost of an emitted-but-wrong unit is a noisy report; the cost of a missed paraphrased-ref is a missed adjustment. Prefer to skip when uncertain — the operator will catch missing items in review more easily than they'll catch a fabricated reference.
3d. Each emitted unit needs:
| Field | Value |
|---|
kind | One of: paraphrased-ref, decision, scope-change, action-item |
sourceSegmentIdx | The index field from the flag entry (NOT a counter you increment) |
sourceExcerpt | Verbatim segment text, or a meaningful subset if the segment is very long |
extractedText | The specific phrase carrying the meaning (e.g., "the Polaris work") |
refCandidate | String for paraphrased-ref; null for everything else |
3e. Write pass2.json to the same directory.
cat > "$CLAUDE_JOB_DIR/notes/pass2.json" <<'EOF'
{
"additionalUnits": [
{
"kind": "paraphrased-ref",
"sourceSegmentIdx": 5,
"sourceExcerpt": "Megan walked us through where the Polaris work landed and what's still open.",
"extractedText": "the Polaris work",
"refCandidate": "the Polaris work"
}
]
}
EOF
Important: even if zero additional units are warranted, write pass2.json with { "additionalUnits": [] }. The re-run command in Step 4 expects the file; an empty array is a valid answer ("we looked, nothing of note").
Step 4: Re-run pm-notes with --pass2-file
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-notes.js" report \
--file <transcript-path> \
--pass2-file "$CLAUDE_JOB_DIR/notes/pass2.json" \
[--save <output-path>]
Same flags as Step 2, plus --pass2-file. The CLI merges the additional units into the Pass 2a output, runs the matcher + classifier, and emits the final report on stdout.
stderr will confirm: pm-notes: merged N Pass 2b units from <path>.
Step 5: Semantic rerank (Phase B — the precision pass)
The CLI writes a rerank handoff (rerank.json) on every run where rerank is enabled and there's anything to judge — including the first Step 2 run. It then prints:
pm-notes: wrote rerank handoff -> /tmp/notes/rerank.json
pm-notes: invoke the pm-notes SKILL to draft rerankVerdicts, then re-run with --pass2-file <path>
The deterministic matcher deliberately casts a wide net — confirmed matches and grey-zone near-misses — and hands you the borderline set to judge. Always rerank after paraphrase resolution (Step 3): if a transcript had flagged segments, ignore the rerank.json from the first run (it was scored on an incomplete unit set) and use the one written by the Step 4 re-run, which includes the paraphrase units. To skip the pass for a project, set pm_notes.rerank: false (the report then renders the raw lexical bands).
5a. Read the handoff.
cat "$CLAUDE_JOB_DIR/notes/rerank.json"
Shape — one entry per matched unit that had any candidate, each listing the lexical candidates (confirmed + near-miss):
{
"items": [
{ "unitIndex": 0, "unitText": "…the dynamic plug single cell update…",
"candidates": [
{ "identifier": "INT-482", "title": "User inputs adjustment values into Dynamic Plug Sources", "labels": [], "lexicalScore": 0.47 },
{ "identifier": "INT-788", "title": "center map spurious hierarchy level", "labels": [], "lexicalScore": 0.28 }
] }
]
}
5b. Judge each (unit, candidate) pair. Read the project's vocabulary / patterns prose (same context as Step 3) for domain grounding, then for each candidate decide whether the unit is genuinely about that issue:
| Field | Value |
|---|
unitIndex | the entry's unitIndex (NOT a counter you increment) |
identifier | the candidate's identifier |
related | true if the unit is genuinely about this issue, else false |
confidence | your calibrated 0–1 certainty — this REPLACES the lexical score in the report |
rationale | one line — why related / unrelated |
The value of this pass is judgment the lexical scorer can't make: promote a grey-zone near-miss (low lexicalScore) when the meaning is clearly the issue, and reject a high-lexical hit that's just a keyword coincidence.
5c. Write the verdicts into pass2.json — alongside any additionalUnits from Step 3. One file carries both:
cat > "$CLAUDE_JOB_DIR/notes/pass2.json" <<'EOF'
{
"additionalUnits": [],
"rerankVerdicts": [
{ "unitIndex": 0, "identifier": "INT-482", "related": true, "confidence": 0.9, "rationale": "single-cell plug insert flow" },
{ "unitIndex": 0, "identifier": "INT-788", "related": false, "confidence": 0.1, "rationale": "different surface (center map)" }
]
}
EOF
(If Step 3 produced paraphrase units, keep them in additionalUnits here — don't drop them.)
5d. Re-run with --pass2-file (same command as Step 4). The CLI applies the verdicts: for each unit you judged, only related candidates survive and each confidence becomes your calibrated score; related: false candidates are dropped and the grey-zone band collapses. stderr confirms pm-notes: applied N rerank verdict(s).
Judge every item in rerank.json. A unit you omit from rerankVerdicts is not reranked — it passes through with its original lexical candidates (no verdict = pass-through). Sweep the whole items[] list so nothing slips through unjudged. (The calibrated scores drive the report's confidence numbers. The surviving matches are flagged internally as semantic-origin for the Step 7 vocabulary loop, but the rendered report shows the confidence number, not a [semantic] label.)
Step 6: Surface the report to the user
Pipe the report through cleanly. No paraphrasing. The operator wants the verbatim proposal so they can decide what to apply. Don't summarize, don't editorialize — just present the markdown the CLI produced, optionally with a one-line lead-in like "Here's the pm-notes report for <file>:".
If --save <path> was used, also confirm the saved location.
After surfacing the report, offer the natural next step:
"Want me to file any of these as comments / sub-issues / status changes? I can route through pm-issues or pm-handoff."
Don't auto-apply — the dry-run posture is the whole point.
Step 7: Review the self-taught vocabulary suggestions
When the rerank pass (Step 5) confirms matches, the CLI mines the confirmed units for recurring multiword domain phrases and writes proposals to vocab-suggestions.json:
cat "$CLAUDE_JOB_DIR/notes/vocab-suggestions.json"
The file shape is { "proposals": [ { "term": "…", "issues": [...] } ] }. Each proposal proposes a notes-vocabulary.json alias — e.g. "dynamic plug" → [INT-482, INT-462]. Surface the useful ones and offer to add them to <repo>/.claude/pm/notes-vocabulary.json. Aliases the operator accepts make the next run match those paraphrases deterministically in Pass 2a — no rerank round-trip needed. That's the self-teaching loop: the expensive semantic pass trains the cheap lexical layer, so quality compounds run over run.
Step 8: Apply accepted proposals (optional)
The report is still a dry run. Step 8 lets the operator act on it — nothing here writes until the operator picks proposals.
8a. Offer. After surfacing the report, offer the confirmed proposals (the ## <ID> groups — not Near-misses or Unmatched units, which are below threshold or unlinked):
"Want me to apply any of these? e.g. 'comment on FUNC-42 and close FUNC-50', 'all', or 'none'."
8b. Route each accepted proposal through the verb/skill that owns that write. Every rendered proposal line carries issue-ID · kind · payload · @handle:
| kind | Route through | Invocation |
|---|
append-comment | pm-improve | enrich the issue with the comment via its --apply path |
update-description | pm-issue.js update | --id <ID> --append-description "<addendum + footer>" (Linear only; on GitHub, post as a comment instead) |
change-status | pm-issue.js update | --id <ID> --status "<state>" --comment "<footer>" |
retitle | pm-issue.js update | --id <ID> --title "<new title>" --comment "<footer>" |
spawn-sub-issue | pm-issues | create a child issue under <parent> (footer in the body) |
close | pm-issue.js update | --id <ID> --status "Done" or "Canceled" --comment "<reason + footer>" (there is no separate close verb — closing is a terminal-state transition) |
pivot | pm-pivot | run the pivot flow for <ID> |
The CLI form for the pm-issue.js update rows:
node "$CLAUDE_PLUGIN_ROOT/hooks/bin/pm-issue.js" update \
--id <ID> [--title "…" | --status "…" | --append-description "…"] --comment "<provenance footer>"
Each verb/skill runs its own confirmation and validation — the gate is per-write, where the context lives, not one blind batch.
8c. Stamp provenance on every applied write. Build the footer from the transcript file name and the proposal's @handle:
— via pm-notes · <transcript-filename> · raised by @<handle>
For free-text writes (append-comment, update-description, spawn-sub-issue) append the footer to the body/addendum. For field changes (change-status, retitle, close) pass the footer as the same update call's --comment so the meeting trail is recorded in one atomic write. Omit raised by @<handle> when the proposal has no resolved speaker.
8d. Apply sequentially, fail-soft. Tracker writes are not transactional. Apply one proposal at a time; if a write fails or its confirmation is declined, report it and continue with the rest. Close with a one-line summary, e.g. FUNC-42 ✓ comment · FUNC-50 ✓ closed · FUNC-30 ⊘ skipped.
Customization the user can do per project
Three layers, ordered by impact:
-
<repo>/.claude/pm/notes-vocabulary.json — structured aliases. Highest impact for paraphrased-ref matching because aliases feed both the deterministic Pass 2a (exact-match cues) and the Pass 2b prompt context. Shape: { aliases: { "Polaris": ["polaris work", "the polaris stuff"] }, statusCues: { "blocked": ["stuck on", "waiting for"] } }.
-
<repo>/.claude/pm/conventions.md ## Notes vocabulary and ## Notes patterns sections — free-form prose that you (Claude) read during Pass 2b. Lower setup cost than the structured JSON — anything the operator can write about how the team talks goes here. Example: "When someone says 'the X work' without naming an issue, they usually mean whatever's currently in-progress in the X project."
-
project.json pm_notes block — operational knobs:
scope_default (string) — default --scope expression when the operator omits it
confidence_threshold (number, 0–1) — minimum matcher confidence to confirm a candidate (default 0.45)
rerank (boolean, default true) — whether to run the Phase B semantic rerank pass; set false to render raw lexical bands without the Claude round-trip
speaker_identities (object) — map of transcript speaker names to tracker handles (e.g., { "Megan": "megan@nthplus.io" })
Every report's Customization: header line shows which layers are active — that's how you tell the operator at a glance whether the project is hand-tuned or running on defaults.
What this skill does NOT do
- Apply adjustments silently or in bulk. Step 8 applies accepted proposals only — the operator picks which, and each write routes through the existing pm-* verb/skill that owns it (so all template/label/create-gate validation and confirmation still apply). Near-misses and unmatched units are never auto-applied.
- Spawn new issues automatically. Sub-issue proposals are proposals — they appear in the report; the operator decides whether to file.
- Real-time / streaming transcripts. File input only. If the user has a live meeting, save the transcript when it ends and run pm-notes against the file.
- Cross-tracker matching. One tracker per invocation (Linear or GitHub, not both at once). The matcher reads whichever the scope expression resolves to.
- Read code or repos for context. That's
pm-improve's job. pm-notes is purely transcript → tracker.