Use when running the retrospective on a piece of work that has just finished — the retro, normally the step straight after `prep-pr` opens the pull request — to write and commit the session-metrics card for the branch, read what the session actually cost against the difficulty declared before it started, and turn that into concrete changes to the skills, `AGENTS.md`, the wiki, the tests or the brief. Also the owner of the card — nothing else writes one carrying the pull request's identity.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Run the retrospective for the branch named in $ARGUMENTS, or the current
branch if it is empty.
Two things, always, whatever else fails:
.claude/metrics/<branch-slug>.json — or
<branch-slug>-<pr>.json on a branch name an earlier pull request used —
carrying this branch's pull request number, issue number and issue labels — committed, pushed, and
rendered onto the pull-request body as a <details> block. This skill owns
that file. Step 1 is not optional and is not the part to drop when the run
gets long.RETRO.md at the repo root: the findings, each one naming the evidence
in the card or the session that produced it, and each one landing somewhere
— an issue, a file changed here, or a sentence addressed to the person who
wrote the brief. It is gitignored scratch, like every other skill's report;
the card is the half that the repository keeps.A run that produces analysis and no card has failed. A run that produces a card and no findings has not failed — "this session cost what the work was worth and nothing here needs changing" is a real and common result, and inventing three suggestions to avoid writing it is the single worst thing this skill can do.
new-feat, implement, prep-pr, then this. Under orchestrate, once per task,
after that task's pull request is open and before the shepherd watches it.
After prep-pr and not inside it, because prep-pr dispatches three review
agents: a card written before they finish measures building the change rather
than shipping it.
Three answers, once, exactly as prep-pr establishes them:
git ls-remote --exit-code origin HEAD >/dev/null 2>&1 && echo "network: yes" || echo "network: no"
command -v gh >/dev/null && gh auth status >/dev/null 2>&1 && echo "gh: yes" || echo "gh: no"
ls ~/.claude/projects >/dev/null 2>&1 && echo "transcripts: yes" || echo "transcripts: no"transcripts: no is the one that changes this run's shape, and it governs
what the collector can write, never what a card already on disk says. The
collector reads ~/.claude/projects; with that directory absent a card it
writes now carries the diff and zero spend — true, but able to answer nothing
about cost. Say so at the top of RETRO.md, write it anyway, and confine the
findings to what the session showed rather than what that card counted. Never
present a zero-spend card as a cheap session.
A card already in .claude/metrics/ is a record, not collector output to be
distrusted. Its spend was measured when the transcripts still existed, and it
stays true after they are gone — that is the entire reason the file is committed
rather than recomputed. So read it and use its numbers, whatever the probe above
answered and whoever wrote it. "There are no transcripts" is never a reason to
decline to report a figure the card already holds.
If prep-pr ran in this same session, it already has these three answers.
Re-probing costs a turn; read them out of its report instead.
This is the ownership step. Everything below is analysis; this is the durable record, and it is written first so a run that ends early still leaves it behind.
BRANCH=$(git branch --show-current)
PR=$(gh pr view --json number --jq .number)Then the two runs, which do different jobs:
# The diff is measured against `--base`, which defaults to origin/main. On a
# stacked branch that counts the parent's whole diff as this one's.
BASE="origin/$(git config --get "branch.$BRANCH.gh-merge-base" || echo main)"
# 1. The committed card. `--resolve-issue` finds the pull request, names the
# issue the branch was cut for and sums the `complexity:` ratings of every
# issue in this repository the pull request closes.
bun run --cwd tools/pr-metrics card -- --resolve-issue --base "$BASE"
# 2. The `<details>` block for the pull-request body, on stdout, warnings excluded.
bun run --cwd tools/pr-metrics card -- --resolve-issue --base "$BASE" --format markdown > /tmp/card.mdFive rules about that pair:
--resolve-issue do the lookup; never hand-roll it in the shell.
.closingIssuesReferences[0].number drops the repository, so a follow-up
gh issue view <n> --repo englishstreetventures/osn succeeds against a different
repository's issue of the same number and writes a stranger's complexity:
label in as this branch's denominator. resolveIdentity in
tools/pr-metrics/index.ts compares the reference's own repository.englishstreetventures/osn-tracker, and that issue's
severity:/area: labels must never reach a card committed to a public
repository. The flag withholds them and records
complexity.method: "not-fetched", which is not "none" — a rating may
well exist there and nobody looked. "lookup-failed" is its third answer,
for a call that did not land.NEW-FEAT.md when the pull request links it, else the first Closes line of
the body, else the first Part of line; with several closing references and
none of those, it names none and warns. Read issue.number and
complexity.issues off the card: when the issue is not the one the branch
was cut for, run both commands again with --issue <n> added. --issue
names the issue and nothing else; the rating stays the sum of what the pull
request closes. It takes this repository's issue only — never a tracker
number.gh does. --pr,
--issue-labels and --complexity win over the lookup; --issue-labels
turns the rating lookup off and declares that one issue's rating. The usual
reason is a branch whose pull request is not open yet.~/.claude/projects is local,
unversioned and dies with a remote container. The committed file is the
record; an uncommitted one is nothing. Stage the one card, never the
directory: .claude/metrics/ holds every other branch's card too, and a
git add of the whole of it sweeps in anything a concurrent session left
there.Before the push or the body edit below, read the pull request's state:
gh api repos/englishstreetventures/osn/pulls/"$PR" --jq '{state, merged}'On merged: true or state: closed, stop before the push and the body edit. Do not push to the branch, and do not edit the body: a merged pull request's body is the record of what merged. Cut a new branch from origin/main, commit the card there, and open a follow-up pull request as write-pr Step 6 says.
SLUG=$(printf '%s' "$BRANCH" | tr -c 'a-zA-Z0-9._-' '-' | sed 's/^-*//;s/-*$//')
CARD=".claude/metrics/$SLUG.json"
[ -f ".claude/metrics/$SLUG-$PR.json" ] && CARD=".claude/metrics/$SLUG-$PR.json"
git add "$CARD"
git commit -m "chore: session-metrics card for $BRANCH"
git push
gh pr view "$PR" --json body --jq .body > /tmp/body.md
cat /tmp/card.md >> /tmp/body.md
gh pr edit "$PR" --body-file /tmp/body.mdRead the body out and append to it — never compose a new one. The six sections
prep-pr wrote are the pull request's contract, and an edit that rebuilds the
body from memory loses them.
Check the card is not already on the body before appending — a re-run of
this skill would otherwise stack a second block. Match the summary line, not the
tag: a body that describes what this step does mentions <details> in its own
prose, and a bare grep -c '<details>' then reports a block that is not there.
gh pr view "$PR" --json body --jq .body | grep -c '^<details><summary>Session metrics'The block is a <details>, never a ## section: prep-pr's body shape check
permits exactly six top-level headings, and appending a seventh fails a body
that is otherwise correct.
When a step here cannot run, record which and carry on — a missing card
never blocks anything. The exception is card exiting 1 because it cannot keep
this pull request's sessions apart from another's: a card on disk that names
another pull request, or an earlier pull request on this branch name whose merge
time is unknown or that closed without merging. If its message shows gh named
the wrong pull request, rerun with --pr <n>; if it asks for --resolve-issue,
rerun with it; otherwise stop — no block, no commit — and quote
it in RETRO.md. No gh: write the card with whatever identity you know and
leave the block in a file, naming it in RETRO.md. No transcripts: as above. No
network: commit the card locally; it pushes with the next push.
The SessionEnd hook is a fallback, not a second writer. It runs
card -- --if-absent --resolve-issue, so it writes a card for a branch that has
none and never touches one this skill committed.
A card with pr.number: null is one the hook wrote before a pull request
existed. Its spend and interaction numbers are as good as any other card's —
only its identity is missing, and Step 1 fills that in where it can run. Where
it cannot, say the identity is missing and go on to read the rest: an incomplete
card is still the measurement, and the comparison in Step 2 is the reason this
skill exists.
cat "$CARD" # the file Step 1 staged
bun run --cwd tools/pr-metrics report -- --coverage # where this card sits in the corpusWrite these down before going further. They are the evidence every finding below has to cite:
| From the card | What it is |
|---|---|
complexity.declared + spend.usd_equivalent | The whole comparison. Spend against difficulty declared before anyone knew the cost |
interaction.corrective_turns | Human turns after the agent had already picked up tools. The one number here that measures the brief, not the model |
interaction.tokens_before_first_edit and sessions_with_observed_edit | Exploration. null means unobserved, not "all of it" |
interaction.edit_churn.max_edits_one_file | An agent building, or an agent lost |
interaction.skills / interaction.subagents | What was invoked — and what conspicuously was not |
window.compactions and window.sessions | Whether the context ran out, and how many restarts |
diff.files split by bucket | Where the change actually landed |
spend.by_actor | Whether delegation paid |
interaction.requests | Model calls, how many ran one tool, and the context each carried. Cost is calls times context |
review | Whether tests and docs findings led to a change, against a control. null means no report was read; the first run that reads the reports is the record |
references/card-signals.md maps each of those to the question it answers and
the artefact a finding about it belongs in. Read it before Step 3.
Three readings that are wrong every time:
A null rating means the work is unrated. complexity.declared is the
sum of the complexity: ratings of every issue in this repository the pull
request closes, and complexity.issues says how many it adds up (a card
older than schema_version 3 has no issues and rates one issue). A null has a reason, and
complexity.method names it. Read it before you conclude anything:
lookup-failed, or none on a card with pr.number: null — the card did
not read a rating. Look at the issues the pull request closes; where each
carries a complexity: label, their sum is the denominator.not-fetched — the pull request closes only a private tracker issue, whose
labels the card never reads. That issue's complexity: label is the
denominator; read it there, and never copy the issue or its labels into
anything public.part-of — the branch only advances issue.number, and that issue's
rating describes the whole of it. Never divide by that label: there is
no denominator.none with a pull request — a closed issue carries no rating (the run's
warning named it), or nothing is linked.Only for part-of, or an issue with no complexity: label, is the work
genuinely unrated, and only then do you say "unrated, so I cannot tell".
Cost is not waste without the declared rating. A small diff, a high
rating and a large spend is a hard problem that ended in a one-line fix, and
it is fine. The same diff at complexity:1 is the thing this whole system
exists to find.
One card is a sample of one. Comparing it to the corpus is
analyse-sessions, not this skill, and a claim about the repository needs
that skill's coverage step first. Findings here are about this session.
Every finding names a moment. Not an impression, not a feeling that the docs could be better — a specific thing that happened in this session, and the artefact that would have prevented it.
The five sources, in the order they are worth mining:
AGENTS.md row or a skill could have stated. This is the
highest-yield source and the reason tokens_before_first_edit is on the
card.corrective_turn is a course correction that
the brief did not prevent. Read the actual prompts — what did the person have
to say that the task should have carried?review-security, review-performance or
review-tests finding that a test, an oxlint house rule or a CI step could
have caught before a review agent had to. stress-plan findings count
double: a plan defect found by a second model is one the plan template
could have asked about.prep-pr's docs pass may already have done it — check before filing.SKILL.md gap, and it
is the only evidence that reliably justifies editing one.| Signal | Usually means | Where the fix goes |
|---|---|---|
High tokens_before_first_edit, package with no wiki page | No map for that surface | A new wiki/ page, and a line in wiki/index.md |
| A fact re-derived that one line could have stated | AGENTS.md is missing a row, or the wiki page is missing a heading | Prefer the wiki page; AGENTS.md only for something every session needs |
corrective_turns ≥ 3 | The brief was unclear | The prompt. Say what the task should have carried. No issue — there is nobody to assign it to |
| A skill ran and the mistake happened anyway | The skill is missing a trap | That SKILL.md, as a named failure mode, not a general exhortation |
| A review finding a gate could have caught | Missing test, missing house rule, missing CI step | tests/, tools/oxlint/house, or .github/workflows |
edit_churn.max_edits_one_file high on one file | The agent was lost in that file | Usually the file, not the docs — a shape nobody can hold in their head |
window.compactions > 0 on a small task | The context surface is too wide for the job | What was loaded and did not need to be |
spend.by_actor.subagent near zero on a big task | Delegation did not happen, or TASK-BRANCH: was missing | orchestrate, or the attribution marker |
| Rising cache-read share across recent cards | The context surface is bloating | analyse-sessions — a corpus question, not this one |
1. Do not rate the work you just did. The same contamination that makes a
complexity rating worthless at pull-request time applies here: an agent scores
the task it struggled with as hard and the documentation it read as adequate.
The defence is that every finding names an artefact and a moment, never a
difficulty. "This was tricky" is not a finding. "The session read
osn/api/src/lib/origin-guard.ts three times because
wiki/shared/rate-limiting.md does not say which routes are exempt" is.
2. A new skill is almost never the answer. Skills are context that every future session pays for, they are measured by evals, and one nobody invokes twice is bloat with a scoring loop attached. Propose editing an existing skill, or a wiki page. A new skill needs a procedure that will recur and that a reader would otherwise get wrong — say which, or do not propose it.
3. Do not retro an environment. A gate recorded not run — no package manager is a fact about the container, not a gap in the repository. Findings
about missing dependencies in a remote session are noise; the repository is not
where that is fixed.
4. A suggestion with no owner is not a finding. Same rule as a deferral: it
is a change made on this branch, or in a follow-up pull request this session
opens, or a sentence said to the person — and an issue only when none of those
fits (prep-pr Step 6). A paragraph in a chat reply is discovered by nobody.
5. Do not re-file what prep-pr filed. Its Step 7 already opened issues for
every S-, P- and C- finding. This skill files what those findings imply
about the process — the missing gate, not the defect.
6. Three findings is a lot. Rank by what someone would actually do, cut the rest, and say how many you cut. A list of nine is a list nobody reads.
| Kind | Where | Labels |
|---|---|---|
| A wiki page change | Make it on this branch while the pull request is open — a plain wiki edit needs no fresh review | — |
An AGENTS.md, skill or agent-definition change | Make it on the follow-up pull request (prep-pr Step 6), which gets its own reviews; never on this branch after its reviews have run | — |
| A missing test, house rule or CI gate | Make it on the follow-up pull request when it fits in one package; an issue in englishstreetventures/osn only when it is a project of its own | area:ops or area:schema as it fits, --type Task |
| Anything about the brief | No issue. It goes in RETRO.md and is said plainly in the reply | — |
| A security, performance or compliance defect | englishstreetventures/osn-tracker, and only if prep-pr did not already file it | per wiki/conventions/review-findings.md |
A retro that ends with more issues than changes has filed instead of fixed.
What may stay open is in wiki/conventions/review-findings.md §Fix findings;
do not file them, and it holds here too.
Write each issue with the write-issue skill, which also rates it through
rate-complexity before work starts (complexity:unconfirmed on an unattended
run). The body spells out what the evidence was; "the retro found this" is not a
body.
Where the next step needs a choice only the repo owner can make, write what you
propose, apply needs:decision, and move on.
RETRO.md, in this shape:
# Retro — <branch>
**Card** — `<the file Step 1 staged>`, committed as <sha>. <One line: what it
cost, against what was declared, and whether those agree.>
**Gates** — transcripts: yes/no. gh: yes/no. <Anything the card could not see.>
## Findings
### 1. <what to change> — <area>
- **Evidence** — the moment in this session, and the card field or the turn that
shows it.
- **Cost** — what it cost this time. A number where the card has one.
- **Fix** — the concrete change, in a named file.
- **Landed** — `englishstreetventures/osn#N`, or "changed on this branch", or "told the user".
### 2. …
**Cut** — <n> findings not worth the reader's time. <One line on the kind.>Then say it in the reply, shortest first, and put anything about the brief to the person directly — that one never reaches an issue, so if it is not in the reply it is nowhere.
wiki/conventions/session-metrics.md is the reference for every field and
for why the card holds exactly one scalar judgement.
tools/pr-metrics/README.md has the commands. analyse-sessions is the
corpus-level counterpart: this skill looks at one session and may propose a
change to one artefact; that one looks at every card and is the only thing
entitled to say a trend exists. A retro finding repeated across three
branches is an analyse-sessions question, not a stronger retro.
englishstreetventures/osn
6786570
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.