Work summary
Turn a window of git commits into a short, honest, plain-language summary of what got done.
Resolve repos and integration refs via shared/config.md.
Workflow
- [ ] 1. Resolve the date window from the user's wording (default: today)
- [ ] 2. Resolve the repo scope (default: every configured repo)
- [ ] 3. Pull commits for the window across each repo (git -C <repo> …)
- [ ] 4. Collapse squash-merge duplicates → one item per feature/PR
- [ ] 5. Translate to plain language; group by repo; pick the output shape asked for
- [ ] 6. If a STANDUP was asked for: render the three-section shape (§ Standup) —
and derive the Blockers section, which is not in the commit log
- [ ] 7. If a DECK was asked for: fill deck-template.md, copy the stylesheet, tell
them how to preview (§ Deck)
1. Date window
Use today's date from the environment, never a remembered one.
| Wording | Window |
|---|---|
| "today" | just today |
| "yesterday" | just yesterday |
| "yesterday and today" | both, in separate sections |
| a named date | that day |
| "this week" / "Friday update" | since the previous Friday |
| "standup" | two days: the last working day and today, separate sections (§ Standup) |
⚠️ On a Monday, "the last working day" is Friday, not the empty Sunday.
The window is a half-open day range: --since = 00:00 of the first day, --until =
00:00 of the day after the last day, so the whole last day is included.
2. Repo scope
Default: cover every repo in workflow.json → repos and group the output by repo.
With no repos key that is just the repo you are in, which is the common case and a
complete answer — say so rather than implying a wider sweep happened. For a monorepo,
split by the paths in workflow.json → workstreams. Narrow only when the user names
one.
⚠️ Resolve each repo to a real checkout path before reading its log. A workspace root
holding several repos is usually not itself a git repo, so git -C <path> is right
there — but <path> must be proven, not guessed from the repo name
(shared/config.md § Repo scope):
git rev-parse --show-toplevel # the repo you are in
git -C "<candidate>" rev-parse --show-toplevel # prove a sibling before using it
A configured repo with no checkout here is a gap to report, not a quiet day. It has no commits to you; that is not the same as no commits. List it under "not covered" with the reason — this is the same failure as the stale-workstream-path one below, and it reads identically in the output.
🚨 A sibling's integration ref and workstreams come from ITS workflow.json, never this
one. Run the resolution block from the sibling's checkout and take integrationBranch
and workstreams from what it prints. MEASURED: judging a sibling's commits against
this file's origin/dev — a branch the sibling also had, 815 commits stale — reported
months of merged work as unmerged, with no error.
⚠️ Read the workstream paths from config, not from memory. An app that was split or renamed leaves the old path in every doc and half the skills; a stale path silently reports zero commits for a live workstream.
Cross-cutting commits. One commit often touches several paths — a tooling or
design-system sweep can hit all of them — so it will appear under more than one
workstream. Attribute it to the workstream it is about and mention it once; don't
repeat it under every app it touched. The mirror case: a commit touching no
workstream path — a runbook under documentation/, a root config — is invisible to the
per-path pull. MEASURED: a cutover-runbook fix reported every stream empty for its day.
Always run the unscoped pull too, and attribute by what the commit is about.
3. Pull the commits
🚨 Fetch first. A local checkout is routinely dozens of commits behind, and a stale checkout silently under-reports the day — which reads exactly like a quiet day.
Resolve the identities BEFORE filtering
--author matches a regex against author name and email, so the email is the precise
key. One --author is not enough, and the failure is silent.
A squash merge rewrites the author to whichever identity is on the merging GitHub
account. Where that differs from the checkout's git config user.email — a personal
address on the GitHub account, a work address in the clone — filtering on
git config user.email alone returns the un-merged branch commit and hides its merged
twin. The work is then reported as still in flight on the very day it shipped, which is
the opposite of what happened.
Discover the window's identities first, then pass every one of yours as a repeated
--author flag; git ORs them:
# 1. Who committed in this window, under which identities?
git -C <repo> log --all --since="$START" --pretty=tformat:"%an <%ae>" \
| sort | uniq -c | sort -rn
# 2. Filter on all of the ones that are yours
AUTHORS=(--author="you@work.example" --author="you@personal.example")
git -C <repo> log --all "${AUTHORS[@]}" \
--since="$START" --until="$END_EXCL" --pretty=tformat:"%h|%ad|%s"
⚠️ Sanity check: if a stream looks emptier than the window felt, or something you know merged still reads as un-merged, re-run step 1 before writing it up. It is almost always a second identity, not a quiet day.
⚠️ Drop --author entirely when summarizing the team's work rather than your own.
Say which you did — "my commits" and "the team's commits" are different reports and the
difference is invisible in the output.
The pull
git -C <repo> fetch --all
START="2026-06-11 00:00"; END_EXCL="2026-06-12 00:00"
AUTHORS=(--author="you@work.example" --author="you@personal.example")
INTEGRATION="<integrationBranch>" # workflow.json -> integrationBranch, e.g. origin/main.
# Set it HERE — a fresh shell has no value from an earlier block.
# Single-stream repo, read from the REMOTE integration ref:
git -C <repo> log "$INTEGRATION" "${AUTHORS[@]}" \
--since="$START" --until="$END_EXCL" \
--pretty=tformat:"%h|%ad|%s" --date=format:"%a %m-%d %H:%M"
# Monorepo, split by workstream:
for P in <workstream paths>; do
echo "--- $P ---"
git -C <repo> log "$INTEGRATION" "${AUTHORS[@]}" \
--since="$START" --until="$END_EXCL" \
--pretty=tformat:"%h|%ad|%s" --date=format:"%a %m-%d %H:%M" -- "$P"
done
🚨 Read the REMOTE integration ref, not the local branch or HEAD. A checkout parked
on someone's feature branch reports that branch's history as the team's day. This is the
single most common way a summary comes out wrong.
🚨 One exception — a standup reads --all, not $INTEGRATION. A standup answers
"what I worked on", not "what shipped", so work still sitting on an un-merged feature
branch has to appear or the day reads as half-empty. Substitute --all for
"$INTEGRATION" in both commands above, then establish merge status per commit rather
than trusting a branch name:
git -C <repo> merge-base --is-ancestor <sha> "<integrationBranch>" # exit 0 = merged
Anything that is not an ancestor is un-merged and must be marked as such — see § Standup. Skipping this check is how a standup claims something shipped that is still sitting on a branch.
🚨 Use --pretty=tformat:, never format:, whenever the output is piped. format:
omits the trailing newline on the final record, so ... | while read never runs the loop
body for it and silently drops the oldest commit in the window, once per repo. It
fails plausibly — the list looks complete and is short by one real item. tformat:
terminates every line. Print a count alongside any list you build and reconcile it
against git log … --oneline | wc -l before writing anything up.
4. Collapse squash-merge duplicates
A squash merge re-lands the same work under a new hash and a PR-shaped subject, so the feature commits and the merge commit both fall in the window. One item per feature, not one per commit.
One feature typically leaves three kinds of commit in the window:
- the feature commit, carrying its PR number —
... (#6); - its review-fix commits —
Address PR review …,fix(...): … (PR #34 review); - a double-numbered merge —
... (#6) (#70)— where the squash of a branch that already had a number in its subject picks up the merge's number too.
Match on the PR number in the subject and on subject similarity, and prefer the merge subject — it is the one written for a reader.
Drop pure merge commits (Merge pull request …) and review-nit commits outright.
Neither is its own deliverable, and a review-fix commit reported as a bullet is how a
day's list fills up with work nobody outside the PR needed to hear about.
5. Write it
Plain language, no jargon, no commit hashes in prose. The reader is deciding what to ask about, not auditing the log.
Output shapes, chosen by what the user asked for:
- One-liner — a single sentence covering everything.
- Short paragraph — 2–4 sentences, grouped by theme not by repo.
- Day-grouped bullets — a heading per day, bullets under it, repo in bold.
- Standup — a fixed three-section shape (Yesterday / Today / Blockers). It has its own rules and they are not optional: see § Standup below.
- Deck — a Slidev presentation. See § Deck below.
Skip any repo or workstream with no commits — but if everything is empty, say so plainly in one line rather than producing an empty scaffold. A quiet day should read quiet.
Honesty rules
These are what make the summary worth reading:
- A commit is not a shipped feature. Say "opened a PR for X" or "landed X behind a flag" when that is what happened. Verify against the code, not the commit subject.
- A merged PR is not a deployed one unless you have checked that merging deploys —
see
shared/execution.md§ 7. - Never infer a status from a label. Labels lag.
- ⚠️ Report, don't accuse. A zero or low lane for a person is usually allocation, not underperformance. Ask the lead before inferring, and make any target conditional.
- ⚠️ Frame decisions, not retreats. If an approach changed and the prior one was never actually deployed, write it as the decision it is — not as "instead of X".
- If you could not verify something, say the summary is from commit subjects alone.
Standup — the three-section shape
Triggered by "standup", or by a scheduled daily-update task. Everything above still runs
first: the standup is a rendering of the commit pull, not a different investigation.
Read it with --all and check merge status per commit (§ 3).
Before writing one, read reference/standup.md in
full. It owns the shape — Yesterday / Today / Blockers, always all three — the
heading derivation, the keyword-led bullets, the selection test that keeps it to
~6 bullets, the status markers, and how Blockers is derived read-only rather than
invented. It is the file that was tuned against real drafts; this skill only decides
when a standup is the output.
Three rules are load-bearing enough to repeat here:
- 🚨 A standup is YOUR work, and only yours. Keep the
--authorfilter on with every one of your identities (§ 3). Never a "Team:" bullet, never someone else's landing. - 🚨 Selection, not ordering. Ask of every candidate "who else needs to know this, and what would they do differently?" — nobody means it does not go in.
- 🚨
- None.is the expected Blockers answer. Never pad it; an invented blocker sends someone chasing nothing.
Deck — a Slidev presentation
Triggered by "deck", "slides", "presentation", "status deck", "weekly update for ". Everything above still runs first: the deck is a rendering of the summary, not a different investigation. If the commits do not support a claim, the slide does not get to make it.
Audience
A deck has a stakeholder reader, not an engineer one. That changes the writing more than the format does:
- Translate to outcomes. Not "migrated the job runner to a queue" — "batch imports no longer time out on large files".
- No commit subjects, no hashes, no issue numbers on a slide.
- Say what is not done. A deck that only lists wins is the one nobody believes the second time.
Build it
Fill the bundled template into
slides/<name>.md. It ships with the plugin, so read it by absolute path — from an installed copy the working tree is not on disk:cat "${CLAUDE_PLUGIN_ROOT}/skills/work-summary/deck-template.md"Sections: cover → at a glance → one slide per workstream that moved → roadmap → close.
Copy the stylesheet to
slides/style.css:mkdir -p slides && cp "${CLAUDE_PLUGIN_ROOT}/skills/work-summary/assets/style.css" slides/style.css⚠️ Slidev auto-loads
style.css— singular.styles.csssilently does not load, and the deck renders unstyled with no error.Do not overwrite an existing
slides/style.css— it is probably already branded. Say it is there and leave it.Workstreams come from
workflow.json→workstreams, not from invention. One card and one slide per workstream that actually moved this period; omit the ones that did not. A card showing no progress reads as a stalled team rather than an unworked area.Tell the user how to see it:
npx @slidev/cli slides/<name>.md --openThe package is
@slidev/cli, notslidev. Export to PDF withnpx @slidev/cli export slides/<name>.md(needsnpx playwright install chromiumonce).
Rebranding
The stylesheet's first block is five CSS variables — accent, surface, ink, and two status
colours — with a light-mode swap documented beside them. Point the user at that block
rather than editing colours yourself; it is their brand, and one edit re-themes every
slide. The cover's .brand-badge holds a placeholder SVG to replace with their mark.
Progress bars need a real denominator
{{X of Y}} is the part that makes a deck credible or hollow. Use a number you can
defend — screens wired of screens planned, endpoints migrated of endpoints total. If
you cannot name the denominator, delete the bar rather than inventing a percentage.
An invented number is the fastest way to lose a stakeholder's trust in the whole deck.