standup
Reconstruct what you actually did over a time window from Claude session logs, GitHub, Linear, and daily notes; group it by Linear ticket; write a standup-ready markdown report to ~/Notes; and copy a Range.co-ready version to the clipboard. Use when the user says 'standup', '/standup', 'what did I d
Install
mkdir -p .claude/skills/standup && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/17142" && unzip -o skill.zip -d .claude/skills/standup && rm skill.zipInstalls to .claude/skills/standup
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Reconstruct what you actually did over a time window from Claude session logs, GitHub, Linear, and daily notes; group it by Linear ticket; write a standup-ready markdown report to ~/Notes; and copy a Range.co-ready version to the clipboard. Use when the user says 'standup', '/standup', 'what did I do today', or 'write my standup'.About this skill
/standup
A collect → merge → narrate → deliver pipeline keyed on the git branch name.
Deterministic scripts read everything; you read almost nothing — only the small
buckets.json of structured facts. Never open raw session logs; the Go extractor is their
only reader. The full data contract is in CONTRACT.md next to this file.
${CLAUDE_SKILL_DIR} is this skill's directory. Fallback: ~/.claude/skills/standup.
0. Set up a run directory and build the extractor
SK="${CLAUDE_SKILL_DIR:-$HOME/.claude/skills/standup}"
RUN="$(mktemp -d)"
# Build the Go extractor on first use (hard dependency: Go toolchain).
( cd "$SK/extractor" && go build -o "$SK/standup-collect" ./cmd/standup-collect )
1. Resolve the window — compute (since, now) exactly ONCE
Compute both values a single time and pass that same pair to every collector and to the
report header (Correctness Invariant #2). now is the current time in RFC3339.
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
LAST_RUN="$HOME/Notes/.standup-last-run"
# Priority: --since arg > .standup-last-run > 24h before now.
if [ -n "$SINCE_ARG" ]; then
SINCE="$SINCE_ARG" # accept YYYY-MM-DD or full RFC3339
elif [ -f "$LAST_RUN" ]; then
SINCE="$(cat "$LAST_RUN")"
else
SINCE="$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)"
fi
# Normalize a bare date to start-of-day RFC3339.
case "$SINCE" in *T*) ;; *) SINCE="${SINCE}T00:00:00Z" ;; esac
echo "window: $SINCE .. $NOW"
$SINCE_ARG comes from $ARGUMENTS (strip a leading --since). If none, leave empty.
2. Collect (each emits structured JSON into $RUN/)
Core sources — a failure here is a HARD ABORT (Behavioral Constraint 3): do not write
.standup-last-run, so a later run re-captures this window.
# Sessions: the Go extractor is the ONLY reader of raw .jsonl logs.
"$SK/standup-collect" sessions --since "$SINCE" --now "$NOW" > "$RUN/sessions.json"
# GitHub + local commits: the Go collector fans out `gh` and per-worktree `git log`
# concurrently. PRs/commits are windowed and attributed to you; a gh failure is a
# HARD ABORT. (Pass --no-local to skip the worktree scan.)
"$SK/standup-collect" github --since "$SINCE" --now "$NOW" > "$RUN/github.json"
Enrichment sources — degrade to a warning, never abort (Behavioral Constraint 3):
- Linear (soft). Discover via the Linear MCP
list_issues(assignee="me", updatedAt>=linear_since)wherelinear_sinceis the start of the current calendar day (i.e.NOWtruncated toYYYY-MM-DDT00:00:00Z), NOT$SINCE. Using start-of-day ensures work completed or updated earlier in the day (before$SINCE) is always captured — e.g. a ticket closed at 01:00 when the standup window starts at 07:00. For each discovered issue, enrich title/status (andget_issue/list_commentsas needed). Normalize toCONTRACT.md §3and write$RUN/linear.json:{ "issues": [ {domId,title,status,url,updatedAt} ] }. If Linear is unreachable or auth fails, print a warning and write{ "issues": [] }and continue. - Notes (soft). Read dated files under
~/Noteswithin the window (~/Notes/<YYYY-MM>/<YYYY-MM-DD>.md). Write$RUN/notes.json={ "notes": [ {date,text} ] }; empty array if none (silent).
# If you could not produce a Linear/notes file, fall back so merge still runs:
[ -f "$RUN/linear.json" ] || echo '{"issues":[]}' > "$RUN/linear.json"
[ -f "$RUN/notes.json" ] || echo '{"notes":[]}' > "$RUN/notes.json"
3. Merge → buckets.json
python3 "$SK/scripts/merge.py" \
--sessions "$RUN/sessions.json" --github "$RUN/github.json" \
--linear "$RUN/linear.json" --notes "$RUN/notes.json" \
--since "$SINCE" --now "$NOW" > "$RUN/buckets.json"
merge.py unions on branch → DOM-id, routes orphan PRs to otherPRs and orphan notes to
notes, attaches CI sub-bullets, and orders tickets by recent activity (CONTRACT §5).
4. Narrate — read ONLY buckets.json
Read $RUN/buckets.json (never raw logs). For each ticket bucket, and for the otherPRs
and notes groups, write one grounded, first-person, past-tense line and add it as a
summary field (and otherPRsSummary / notesSummary for the two groups), changing nothing
else, then write the result to $RUN/narrated.json.
Voice — a standup, not a changelog. The line says what you worked on, how it went, and where it landed — the arc of the effort — NOT a compressed list of what changed. Git already has the commit log; do not restate it. Aim for the altitude of something you'd say out loud: "Spent the session getting the custody migration green" / "Shipped the DWARF strip and moved on to review comments" / "Still wrestling CI on the verify-generate fan-out."
Infer the arc from signals in the bucket — never fabricate (Behavioral Constraint 1). The tone must be derivable from the facts present, not invented:
- Many
fix:/address review/Copilot/ revert-style commit subjects, or a highciEvents.failCount→ the work was a grind: "spent the day getting X green", "ran into repeated CI failures on X", "chased down daggerWatcherTest breakage". - A single feature commit (or few) with a merged PR → a clean ship: "shipped X".
- An open / in review PR → in flight: "got X up for review", "still iterating on X".
- A no-PR bucket carrying
humanTurns→ investigation/design, not code: "dug into how X is wired", "reviewed the ADR for X", "scoped X". - A Linear-only bucket (no PR, no commits, no turns) → tracking: "opened/filed X", or reflect its status ("closed out X").
Name the concrete thing the effort was about (the subsystem, the failure, the goal) so the line is specific, but pull that from the commit subjects / turns / titles; do not enumerate them as a list. One sentence. If genuinely nothing but a status change happened, a short line is correct, so don't pad it.
Don't name the ticket's own id in its summary. The line is already grouped under that ticket (and downstream tooling posts it as a comment on that ticket), so "Opened DOM-1635 to track configuration" is redundant — write "Opened this ticket to track configuration" or just "Started tracking configuration." Cross-references to other tickets are fine and useful ("Topic 3 of DOM-1622").
No dashes anywhere in the report. Em-dashes (—), en-dashes (–), and the spaced hyphen
clause break (-) are banned from all prose. Join clauses with ; or ,, or set an aside
in parentheses ( ), instead. Write the summaries this way directly — the formatter also
strips any dash that slips through (rewriting it to ; ), but a mechanically patched line
reads worse than one you punctuated correctly in the first place. (Hyphens inside identifiers
and URLs like DOM-1546 are fine — they are not clause-break punctuation.)
5. Deliver — write report, set clipboard, THEN advance .standup-last-run
python3 "$SK/scripts/deliver.py" --narrated "$RUN/narrated.json" \
--notes-dir "$HOME/Notes" --now "$NOW" --last-run "$LAST_RUN"
deliver.py builds the report via format_report, the clipboard text via format_range,
writes ~/Notes/<YYYY-MM>/standup-<YYYY-MM-DD>.md, copies the Range text to the clipboard
(wl-copy → xclip → xsel; if none, it prints a copy-fence block), and only then
writes $NOW to .standup-last-run (Invariant #1). Re-running the same window overwrites
the same dated file (idempotent).
6. Report back
Print a one-line TL;DR (ticket / PR / note counts) + the report file path + the clipboard status to the chat.
Correctness notes (for the implementer / reviewer)
- Window consistency (Inv #2):
(SINCE, NOW)is computed once in step 1 and threaded through every collector,merge.py, and the report header. Never recomputenow. - Last-run advances only on success (Inv #1): only
deliver.pywrites.standup-last-run, as its final step, after the report file exists. Any earlier error (including a core-source hard abort) leaves the timestamp untouched. - Never fabricate (Constraint 1): narration consumes only
buckets.json; facts are attached verbatim as sub-bullets by the formatters. - No raw logs to the model (Constraint 2): only
standup-collectreads.jsonl. - Local-only (Constraint 4): the sole side effects are writing the report file and setting the local clipboard.