standup — Summarize work since last standup
Summarize work done in the user's configured repo directory since the last standup, grouped by category. The output is meant to be read aloud at a standup, so keep it concise.
Priority of the output — put the user's own work first:
- PRs the user created within the window (any state).
- Other PRs the user authored that had activity in the window (updates, new commits, state changes).
- Activity on other people's PRs (reviews, merges, comments).
- Local branches with a summary of the user's own commits (ignore commits by others).
Step 1: Load configuration
Read ~/.claude/standup.json. If the file does not exist, stop and instruct the user to create it with this shape:
{
"base_path": "~/code/your-project-root",
"default_branch": "main",
"preferences": "Optional freeform instructions applied to the output."
}
Only base_path is required. Extract the values with jq:
config=~/.claude/standup.json
base_path=$(jq -r '.base_path // empty' "$config")
default_branch=$(jq -r '.default_branch // "main"' "$config")
preferences=$(jq -r '.preferences // empty' "$config")
# Expand a leading ~ in base_path
base_path="${base_path/#\~/$HOME}"
If base_path is empty, stop with an error.
Step 2: Determine the since date
Priority:
- If
$ARGUMENTSis non-empty, use it verbatim as the since date. - Else if
~/.claude/standup-last-timestampexists, read its contents and use that as the since date. The file stores an ISO 8601 timestamp written by a previous run of this skill. - Else default to
24 hours ago.
Store the chosen value in $since. Also compute a full ISO 8601 timestamp for gh search — GitHub search accepts >=YYYY-MM-DDTHH:MM:SSZ, and we want time-of-day precision so reruns in the same day don't re-list already-reported PRs.
# If $since is already an ISO timestamp (from the last-timestamp file), normalize to UTC Z form.
# Otherwise resolve the relative phrase via BSD date.
if [[ "$since" == ????-??-??T* ]]; then
# e.g. 2026-04-13T11:11:39-0700 → 2026-04-13T18:11:39Z
since_ts=$(date -j -u -f "%Y-%m-%dT%H:%M:%S%z" "$since" +%Y-%m-%dT%H:%M:%SZ)
else
# Parse forms like "36 hours ago", "24 hours ago", "2 days ago", "yesterday",
# "last friday", "past 36 hours" — normalize to BSD `date -v` adjustments.
case "$since" in
"yesterday") adj="-1d" ;;
"last friday") adj="-fri" ;;
*"hour"*|*"hours"*) n=$(echo "$since" | grep -oE '[0-9]+'); adj="-${n}H" ;;
*"day"*|*"days"*) n=$(echo "$since" | grep -oE '[0-9]+'); adj="-${n}d" ;;
*"week"*|*"weeks"*) n=$(echo "$since" | grep -oE '[0-9]+'); adj="-${n}w" ;;
*) adj="-1d" ;; # safe default
esac
since_ts=$(date -j -u -v"$adj" +%Y-%m-%dT%H:%M:%SZ)
fi
Use $since_ts for all gh search >= filters. For git log --since=, pass $since verbatim (git accepts both relative phrases and ISO timestamps).
Step 3: Determine identities
You need two separate identities:
- GitHub login (for
gh searchand filtering reviewed-by results):login=$(gh api user --jq .login) - Git author email (for
git log --author— the GitHub login usually does NOT match the git author name/email):email=$(git -C <any-repo-from-step-4> config user.email)
Step 4: Find recently-changed repos
~/.claude/skills/standup/find-repos.sh "$base_path" "$since"
This prints absolute paths of git repos under $base_path that have any commits since the given date.
Step 5: Gather data
Run these in parallel where possible.
Per repo from Step 4 — commits authored by the user on the current branch (not --all, which produces duplicates from post-rebase branch tips):
git -C <repo> log --author="$email" --since="<since>" \
--pretty=format:"%h%x09%s" --no-merges
Also capture a short stat to summarize changes:
git -C <repo> log --author="$email" --since="<since>" \
--no-merges --pretty=format:"%n---%n%h %s%n" --stat
PRs (global gh search, limited to the relevant orgs) — identify the orgs by looking at remotes of the repos found in Step 4 (e.g. git -C <repo> remote get-url origin → extract owner). Collect distinct owners and pass one --owner flag per owner.
Important gh search prs flag quirks:
--mergedis a boolean, not a date. Use--merged --merged-at=">=$since_ts"together.gh search prsJSON has nomergedAtfield for open queries — useupdatedAtinstead.--reviewed-by=@me/--commenter=@meinclude your own PRs if you acted on them — always filterauthor.login != $loginwhen looking for "other people's PRs".
5a. PRs the user authored
- Created in window (the priority #1 bucket):
gh search prs --author=@me --created=">=$since_ts" \ --owner <owner1> --owner <owner2> \ --json title,url,repository,state,isDraft,createdAt,updatedAt --limit 50 - Authored & updated in window (priority #2 — any state, including merged, closed, draft). De-dup against the "created in window" set by URL so each PR appears once:
gh search prs --author=@me --updated=">=$since_ts" \ --owner <owner1> --owner <owner2> \ --json title,url,repository,state,isDraft,createdAt,updatedAt --limit 100
5b. Activity on other people's PRs (priority #3)
Run each query, filter out author.login == $login, then merge + de-dup by URL. Track which bucket(s) each PR came from so the output can label the action(s) taken.
- Reviewed (any state — open, merged, or closed):
gh search prs --reviewed-by=@me --updated=">=$since_ts" \ --owner <owner1> --owner <owner2> \ --json title,url,repository,author,state,updatedAt --limit 50 \ | jq --arg me "$login" '[.[] | select(.author.login != $me)]' - Commented:
gh search prs --commenter=@me --updated=">=$since_ts" \ --owner <owner1> --owner <owner2> \ --json title,url,repository,author,state,updatedAt --limit 50 \ | jq --arg me "$login" '[.[] | select(.author.login != $me)]' - Merged while the user was involved — catches PRs the user merged, plus merged PRs they reviewed/commented on.
gh searchhas nomerged-byfilter, so use--involves=@meand cross-reference:
For each PR in this set that was not already tagged as reviewed/commented (i.e. the user's only involvement was the merge itself), fetchgh search prs --involves=@me --merged --merged-at=">=$since_ts" \ --owner <owner1> --owner <owner2> \ --json title,url,repository,author,updatedAt --limit 50 \ | jq --arg me "$login" '[.[] | select(.author.login != $me)]'mergedByto confirm — this is usually a short list:
Ifgh pr view <url> --json mergedBy --jq '.mergedBy.login'mergedBy == $login, tag the PR as "merged". Otherwise drop it (the user was merely mentioned/assigned).
If gh fails (not authed, etc.), report the error and continue with the git data.
Step 6: Output
Produce a markdown report in this exact section order — the user's own work leads, then activity on others' PRs, then local branch work. Omit any section that ends up empty. Use clickable links for PRs ([title](url)). Keep each bullet to one line.
# Standup — since <since>
## My PRs — created
- [<title>](<url>) — <repo> (<state>)
## My PRs — updated
- [<title>](<url>) — <repo> (<state>)
## Activity on others' PRs
- [<title>](<url>) — <repo> (by @<author>, <actions>)
## Code changes by repo
### <repo>
- **`<branch-name>`** — <1-sentence summary of the user's commits on this branch>
- **`<branch-name>`** — <1-sentence summary>
Rules for the PR sections:
- My PRs — created: PRs authored by the user with
createdAt >= $since_ts. Show<state>asopen,draft,merged, orclosed. - My PRs — updated: PRs authored by the user with
updatedAt >= $since_tsthat are not already in the "created" section. Same state label. - Activity on others' PRs: union of the reviewed / commented / merged sets from Step 5b, de-duplicated by URL. For each PR, list the action(s) the user took, comma-separated, e.g.
(by @drnic, reviewed, merged). Include the PR's current state inside the actions list only if it adds information (e.g.mergedalready implies state).
Branch-summary rule: one bullet per branch — every local branch in the repo that was updated since $since. Do not merge multiple branches into a single bullet. Each bullet is the branch name followed by a brief human-language summary of what the user changed (not a raw list of commit messages). Commits authored by other people on the same branch must be ignored — see Step 5 for the --author="$email" filter that enforces this.
Identify candidate branches using git only (no gh):
# List local branches whose tip commit is newer than $since_ts (ISO 8601).
git -C <repo> for-each-ref refs/heads/ \
--format='%(committerdate:iso-strict)%x09%(refname:short)' \
| awk -v ts="$since_ts" 'BEGIN{FS="\t"} $1 >= ts {print $2}'
For each branch returned, list the commits on it authored by the user that aren't on the repo's default branch (detect via git symbolic-ref refs/remotes/origin/HEAD; if that fails, fall back to $default_branch from config):
default=$(git -C <repo> symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
default="${default:-$default_branch}"
git -C <repo> log "$branch" "^$default" \
--author="$email" --since="$since" \
--pretty=format:"%s" --no-merges
If the branch IS the default branch, use --since alone (no ^default exclusion) to capture direct commits there.
Feed the resulting commit subjects into a one-sentence human summary per branch — e.g. "Aborts in-flight checkout async ops on unmount; consolidates cleanup refs and fixes a stale paymentSettings closure."
Skip branches that end up with no commits by the user in the window.
Step 7: Apply preferences
If $preferences is non-empty, apply those instructions to the report — reorder, exclude, group, highlight, or rephrase sections as described. Preserve the markdown structure and link format.
Step 8: Update the timestamp
After successfully producing the report, write the current time (ISO 8601, with timezone offset) to ~/.claude/standup-last-timestamp so the next run picks up from here:
date +%Y-%m-%dT%H:%M:%S%z > ~/.claude/standup-last-timestamp
Write the timestamp whenever the report was produced without fatal errors, including when the user passed an explicit $ARGUMENTS override.