catchup
Summarize and review what changed while you were away. Use after a weekend, vacation, or flight to check missed PRs, git commits, Linear tickets, and meetings — one prioritized brief, not a firehose.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/catchup/skills/catchup
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Catchup — Async-Team Return Briefing
You've been away. This skill is a thin orchestrator: it resolves
the window + sources here, then delegates the I/O fan-out, impact
analysis, and brief assembly to the catchup-runner agent (Sonnet) so
your (often Opus) session does not pay for summarization. You only
print what the agent returns.
Usage
/catchup # since you were last active
/catchup --since "friday"
/catchup --since "2h" --focus reviews-requested
/catchup --since last-commit --depth deep
/catchup --scope all # include cross-repo pings/reviews
Default is repo-scoped: every GitHub signal (reviews requested,
notifications, mentions) is filtered to the repo you ran it in. Pass
--scope all to also include cross-repo activity, which is then listed
in its own separate section — never mixed into this repo's lists.
Iron Laws
- Delegate the heavy work — spawn
catchup-runner(sonnet) for fan-out + assembly. Do NOT run thegh/gitfan-out in this session; that defeats the cost/speed purpose. - Resolve the window here, once — the agent must never re-resolve
it. Pass absolute
SINCE_*values. - MCP runs here, not in the agent — Linear/Calendar MCP tools are unreliable in subagents. If present, fetch in this context and pass the text to the agent; else mark absent.
- Validate
--sincebefore any shell — match the grammar; on no match fall back to 24h and note the assumption. - Stop after the brief — print the agent's summary, never auto-transition to another command.
- Repo-scoped by default — pass
SCOPE=repounless the user passed--scope all. A brief run inside one repo must not leak another repo's reviews/notifications. Cross-repo is opt-in only.
Workflow
1. Parse arguments
From $ARGUMENTS: --since, --scope, --sources, --depth,
--focus. Defaults: --since last-active, --scope repo, all
detected sources, --depth standard, no focus. --scope accepts
repo (default — every GitHub signal filtered to the current repo) or
all (cross-repo allowed, listed in its own section).
2. Resolve the time window (here, in this context)
Read ${CLAUDE_SKILL_DIR}/references/time-window.md. Resolve calendar
words (friday, yesterday, a date) in the user's local timezone
(this machine = the user's TZ), pivot through SINCE_EPOCH, derive
SINCE_ISO (UTC) + SINCE_LABEL (with TZ abbrev) + LOCAL_TZ.
Default last-active = MAX of: newest Claude session mtime for this
repo, your last own commit (git log --author=<you> -1 --format=%ct),
your last own PR in this repo (gh pr list --repo <repo> --author @me --state all, repo-scoped — a global search would anchor to other
repos). The latest footprint is "you were last here". Record which
signal won. Variants: last-session (sessions only),
last-commit/last-mine (your git/PR only). No signal → 24h, noted.
3. Detect sources + pull MCP data (here)
gh: command -v gh && gh auth status → github ON
git: git rev-parse --is-inside-work-tree → git ON
linear/calendar: a Linear / Google-Calendar MCP tool present?
If Linear/Calendar MCP is present, query it in this context now
(assigned/updated tickets since SINCE_ISO; missed + today's
meetings in LOCAL_TZ) and keep the short text as LINEAR_DATA /
CALENDAR_DATA. If absent, set them to absent (the agent will
proxy-harvest XXX-#### refs for Linear; skip calendar with a note).
4. Delegate to catchup-runner (Sonnet)
Spawn one agent, foreground, passing a self-contained prompt:
Agent(subagent_type: "catchup-runner", prompt: """
SINCE_EPOCH={…} SINCE_ISO={…Z} SINCE_LABEL="{… local TZ}"
LOCAL_TZ={…} SOURCES={github,git} SCOPE={repo|all} DEPTH={…} FOCUS={…}
OUT_PATH={cwd}/.claude/catchup/brief-{YYYY-MM-DD}.md # local date (date +%F), not UTC
LINEAR_DATA={text or "absent"}
CALENDAR_DATA={text or "absent"}
Window anchor signal: {which one won, for the Risks note}
Do the gh+git fan-out, impact analysis, and brief assembly per your
instructions. Write the file. Return ONLY the inline summary.
""")
The agent inlines all recipes (it cannot read this plugin's references). Do not re-implement its work here.
5. Present + stop
Print the agent's returned summary verbatim and the brief path.
If the agent returned no summary (e.g. it hit its turn budget
mid-assembly — the brief file is usually already written): do NOT
re-summarize the brief yourself; that pulls the expensive step back
into this (often Opus) session, defeating the delegation. Instead
SendMessage the agent by the agentId from its stop usage:
"Return only the inline summary now." — it finishes cheaply in
Sonnet. Only if that also fails, read the brief's Intent + Top
priorities section (not the whole file) and print that.
Do NOT auto-invoke any other command. The user decides what's first.
Sources at MVP
GitHub (gh), Git (git), Linear MCP (optional), Google Calendar MCP
(optional). Slack/Gmail are v2 opt-in — never queried, never piped
raw. Scheduling + per-project config are designed in
${CLAUDE_SKILL_DIR}/references/config-schema.md, not built at MVP.
Graceful degradation contract
A missing source degrades the brief, never breaks it. git log alone
(always available in a repo) is a valid minimum brief. Every absent or
failed source becomes one honest line under the brief's
Risks/assumptions block, so the reader knows what it does not cover.
Files (claude-elixir-phoenix)
-
references
-
brief-format.md 6.2 KB
# Brief Format — Context Brief Framework, Catch-up Scoped The output **is** the product. `/catchup` is not an aggregator that dumps links; it produces a decision-ready brief in the discipline of the 10-element Context Brief Framework (Intent, Audience, Scope In, Scope Out, Deliverable, Constraints, Evidence, Acceptance, Risks & assumptions, Timeline, Privacy). Each element is reinterpreted for a *personal returning-to-work* brief below. ## Format constraint Tight brief, not a novel. **Two screens, tops** for the file; the inline summary is ~25 lines. Excerpts and one-liners — never raw bodies. Ranked, not exhaustive. If it doesn't need *you*, it doesn't go above the fold. ## Element mapping | Framework element | Catch-up meaning | |-------------------|------------------| | Intent | "You've been off N days. Do these first." + the 3-item ranked priority list | | Audience | Just you — your TZ, your role, your in-flight work | | Scope In | PRs/reviews/tickets/branches/meetings that touch *you* in the window | | Scope Out | Bot PRs, your own already-merged work, green CI, items needing nobody | | Deliverable | One markdown brief + a ≤25-line inline summary | | Constraints | Excerpt-only, prioritized, one file, honest about gaps | | Evidence | Links + one-line excerpts (PR #, ticket id, commit sha) — never thread dumps | | Acceptance | Reader knows what to do first and what the brief does *not* cover, in < 2 min | | Impact on your work | Upstream changes that touch *your* in-flight files/areas — the differentiator, not just "what moved" | | Risks/assumptions | Conflict risks ("unmerged migration may clash with your branch") + every skipped source | | Timeline | Window anchored to the user's local TZ (which Friday) + today's meetings in that TZ | | Privacy | No raw Slack/email/issue bodies off-device; v2 sources opt-in only | ## File template ```markdown # Catch-up Brief — {Www, Mmm DD} ({SINCE_LABEL}) ## Intent You've been off {SINCE_LABEL}. Do these first: 1. {highest-priority action — usually a review requested of you, or red CI on your PR} 2. {second} 3. {third} ## Top priorities | # | What | Why it needs you | Link | |---|------|------------------|------| | 1 | Review PR #1234 "…" | review-requested 2d ago, blocking {author} | {url} | | 2 | CI red on your PR #1180 | {check} failed after merge of #1150 | {url} | | 3 | PROJ-412 moved to In Review | assigned to you, awaiting your input | {url} | ## Impact on your work _Upstream changes that touch files/areas you have in flight._ - ⚠ **Direct** — `lib/foo/bar.ex` changed by {sha}/PR #{n} ({ticket}) and is also in your open PR #{mine} / branch `feat/foo`. {deep: one-line semantic note — what about your work it affects.} - **Adjacent** — `{module}/` touched by {sha}; your `feat/foo` works in the same module — review before rebasing. - _(or)_ No overlap with your in-flight scope this window. ## What moved (Scope In) **GitHub** ({n} PRs, {m} pinged you) — _scoped to {repo}_ - #1150 "…" merged by @x — touches {area} ↔ PROJ-318 - @y commented on your #1180: "{≤1-line excerpt}" **Other repos** _(only when `--scope all`; {q} cross-repo items)_ - {owner/other-repo} #42 review-requested of you 2d ago **Git** (default branch +{k} commits by others) - {sha} @x "{subject}" ⚠ migration: priv/repo/migrations/… - your branch `feat/foo` is {behind} behind origin/{def} **Linear** ({source state}) - PROJ-412 → In Review (by @x){, or: "unverified refs (no MCP): PROJ-412, PROJ-571"} **Calendar** ({source state}) - Missed: {meeting} (you were required) - Today: 14:00 {meeting} — {TZ} ## Risks & assumptions - Repo-scoped to {repo}; cross-repo pings/reviews not shown (run with `--scope all`). - Unmerged migration in #1150 may conflict with your local `feat/foo`. - Linear MCP absent — ticket signal is proxy-harvested, unverified. - Window defaulted to 24h (no prior session found). ## Timeline Anchored {SINCE_LABEL} → now (= {SINCE_ISO} absolute; all sources compared on this instant — colleagues' other-TZ events included from *your* boundary). Today ({TZ}): {meetings or "clear"}. --- _Generated by /catchup. Excerpt-only; full threads not included._ ``` ## Inline summary (printed, not the file) ``` Catch-up: off {SINCE_LABEL}. {p} PRs, {r} reviews-requested, {c} commits by others{, linear/cal notes}. Impact: {x} upstream changes touch your in-flight files{, or "none"}. Do first: 1. {action} ({link}) 2. {action} ({link}) 3. {action} ({link}) Risks: {one-liner if any}. Full brief → .claude/catchup/brief-{date}.md ``` ## Depth knobs - **quick** — Intent + Top priorities + counts + Impact (direct overlaps only, by filename). No Scope-In detail, no excerpts. - **standard** (default) — full template above, one-line excerpts, Impact = direct + adjacent overlaps by filename. - **deep** — + CI failure specifics, every PR↔ticket cross-link, and per-file *semantic* impact (read the incoming diff: what about your in-flight work it affects — schema, signature, behavior). ## Worked example (illustrative, generic) > **Intent** — You've been off 3 days (since Fri May 13 00:00 CEST). > > 1. Review **PR #1093** "Account autocomplete component" — review > requested of you Sat, blocking @teammate. > 2. Your **PR #1070** went red — `mix test` failed after #1106 merged. > 3. **PROJ-916** moved to In Review, assigned to you. > > **Impact on your work** > > - ⚠ Direct — `lib/app/properties/survey.ex` changed by #1106 > (PROJ-916) and is in your open PR #1070. The survey struct field > you read was renamed — your code won't compile after rebase. > - Adjacent — `lib/app_web/live/navbar*` touched by #1070 > (PROJ-449); your branch `feat/PROJ-880` works in the same LiveView. > > **Risks** — #1106 added `priv/repo/migrations/…_property_survey` > on `main`; your `feat/PROJ-880` branch is 14 commits behind and has a > migration of its own — rebase before generating a new one. > > **Linear** — unverified (no MCP this env): PROJ-412, PROJ-449, > PROJ-755, PROJ-318 referenced in merges since Fri. This is the bar: the reader spends two minutes, knows the first three moves, and knows exactly what the brief did **not** see. -
config-schema.md 3.2 KB
# v2 Surface — Designed, NOT Built at MVP This file pins the **stable interface** for v2 features so MVP users don't build workflows on a surface that will churn. None of this is implemented at MVP. `/catchup` ignores all of it today. ## 1. Per-project config — `.claude/catchup.local.md` Follows the standard plugin-settings pattern: YAML frontmatter + markdown notes, committed-optional, per-repo. Read at the top of the skill when present; CLI flags override file values. ```markdown --- since_default: "last-session" sources: [github, git, linear, calendar] focus: [reviews-requested] exclude_authors: [dependabot, renovate, github-actions] linear: team: ENA assignee: me slack: # v2 opt-in source watch_channels: ["#eng-platform", "#incidents"] gmail: # v2 opt-in source labels: ["inbox", "team"] quiet: bots: true # drop bot PRs unless --focus asks own_merged: true # drop your own already-merged work --- # Notes Repo-specific catch-up guidance, e.g. "ignore the `release/*` branches; the mobile team owns those." ``` Resolution order: **CLI flag → `.claude/catchup.local.md` → built-in default**. The file never enables Slack/Gmail implicitly — those require both a config block *and* the source MCP present (privacy). ## 2. Scheduling — `--schedule` ``` /catchup --schedule "monday 8am" /catchup --schedule "weekdays 8am" --deliver imessage /catchup --schedule off ``` Implemented via `CronCreate` (one routine per repo, named `catchup:<repo-slug>`). The routine re-runs the same skill and **delivers** the inline summary to a sink: - `daily-note` (default) — append the brief to today's note / drop file - `imessage` — send the summary to the user's self-chat - `stdout` — just run, leave the file Missed-run policy: a scheduled run uses `--since last-session` so a skipped Monday still covers the full gap on Tuesday. `--schedule off` deletes the routine via `CronDelete`. ## 3. Cross-project rollup — `--all-repos` ``` /catchup --all-repos ``` Walks a configured project root (default: `~/Projects`), runs the `git`/`gh` adapters per repo with activity in the window, and emits **one** brief with a per-repo section, globally ranked by "needs you" (reviews requested > red CI on your PR > assigned ticket moved > FYI). Repos with zero signal are collapsed to a single "quiet: foo, bar" line. Guardrails: hard cap on repos scanned (default 25), `git` only by default (`gh` per-repo is rate-limit-sensitive — opt in with `--all-repos --github`), and a wall-clock budget so the rollup can't run unbounded. ## 4. v2 sources — Slack / Gmail Strict opt-in, double-gated (config block **and** MCP present). Excerpt-only is **non-negotiable**: subjects, sender, one-line gist, permalink. Never the message body, never a thread dump, never piped to a remote model. This is the Privacy element of the brief, enforced in code, not left to prompt discretration. ## Why pin this now The MVP ships the narrow, reliable core (`gh` + `git`, optional Linear/Calendar, one brief). Pinning the v2 grammar means a user who writes a `.claude/catchup.local.md` today, or scripts around the flag names, won't be broken when v2 lands. The surface is a contract; the implementation is deferred. -
source-adapters.md 9.3 KB
# Source Adapters Exact recipes per source. Detect first, query second, degrade always. `$SINCE_ISO` / `$SINCE_DATE` come from `time-window.md`. ## Detection (run before any query) ```bash command -v gh >/dev/null && gh auth status >/dev/null 2>&1 # github ON git rev-parse --is-inside-work-tree >/dev/null 2>&1 # git ON ``` Linear / Calendar are MCP: source is ON only if a tool whose name contains `linear` / `calendar` is in your available tool list. Do not guess server names — inspect what is actually present this session. Anything OFF → one line in the brief's Risks/assumptions block. ## GitHub (`gh`) Identity + repo: ```bash ME=$(gh api user --jq .login) REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner) DEFBR=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ``` **Scoping (important).** Default `--scope repo`: EVERY GitHub signal below is filtered to `$REPO`. A brief run inside one repo must never silently list another repo's reviews or notifications. `--scope all` is opt-in and its cross-repo hits go in a separate **Other repos** subsection, never mixed into this repo's lists. **Timestamp discipline.** GitHub times are UTC (`Z`). Judge each item on *its own* controlling timestamp ≥ `SINCE_EPOCH` — never promote a pre-window object because a related object moved in-window; a standing review request is "pre-window, for completeness", not a Top priority. Convert `Z` → `LOCAL_TZ` before printing a clock time; never label a UTC value with a local TZ (`06:36:23Z` is `08:36 CEST`). The four signals that matter on return: 1. **Pinged you while away** — repo-scoped notifications endpoint: ```bash gh api "/repos/$REPO/notifications?since=$SINCE_ISO&all=true" \ --jq '.[] | {reason, title: .subject.title, type: .subject.type, url: .subject.url}' ``` `reason` ∈ `review_requested`, `mention`, `assign`, `comment`, `team_mention`. This is "what asked for me, here". Lead with it. `--scope all` also: `gh api "/notifications?since=$SINCE_ISO&all=true"` then `select(.repository.full_name != $REPO)` for the Other-repos subsection. 2. **Review requested of you (open), in this repo:** ```bash gh pr list --repo "$REPO" --search "review-requested:@me" \ --state open --json number,title,url,updatedAt --limit 30 ``` `--scope all` also: `gh search prs --review-requested=@me --state=open --json number,title,repository,url --limit 30`, then filter out `$REPO` rows into the Other-repos subsection. 3. **Your PRs with new activity / CI state:** ```bash gh pr list --repo "$REPO" --author @me --state open \ --search "updated:>=$SINCE_DATE" \ --json number,title,url,reviewDecision,statusCheckRollup,updatedAt --limit 30 ``` `reviewDecision=APPROVED` + green checks → "ready to merge". `statusCheckRollup` with `conclusion=FAILURE` → "CI broke while away". 4. **PRs others moved in this repo (context, not action):** ```bash gh pr list --repo "$REPO" --state all \ --search "updated:>=$SINCE_DATE -author:$ME" \ --json number,title,author,state,url,mergedAt --limit 40 ``` Drop bot authors (`dependabot`, `renovate`, `github-actions`) unless `--focus` explicitly asks for them. `--depth quick` → counts + top 3 only, skip calls 3–4. OFF (no `gh`/auth): skip the whole GitHub source, note it. Do not try to scrape GitHub over the web. ## Git (always available in a repo — the floor) ```bash GME_E=$(git config user.email); GME_N=$(git config user.name) DEFBR=${DEFBR:-$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')} DEFBR=${DEFBR:-main} git fetch --quiet origin "$DEFBR" 2>/dev/null || true ``` Commits by **others** on the default branch in the window: ```bash # TAB sep (%x09): commit subjects often contain '|' (e.g. # "feat(a|b):") — '|' as -F would shift fields. Tab never appears in # a git subject; macOS awk handles -F'\t' but NOT -F'\x1f'. git log "origin/$DEFBR" --since="$SINCE_ISO" --no-merges \ --pretty=format:'%h%x09%an%x09%ae%x09%ad%x09%s' --date=short \ | awk -F'\t' -v me="$GME_E" '$3 != me' ``` Risk scan — migrations / lockfiles / CI config touched while away (these are the "may conflict with my branch" items): ```bash git log "origin/$DEFBR" --since="$SINCE_ISO" --name-only --pretty=format:'%h %s' \ | grep -iE 'migrations?/|\.lock$|mix\.lock|package-lock|go\.(mod|sum)|Cargo\.lock|\.github/workflows/' \ | sort -u ``` Your local branches that diverged from an updated default: ```bash for b in $(git for-each-ref --format='%(refname:short)' refs/heads); do base=$(git merge-base "$b" "origin/$DEFBR" 2>/dev/null) || continue behind=$(git rev-list --count "$b..origin/$DEFBR" 2>/dev/null) [ "${behind:-0}" -gt 0 ] && echo "$b is $behind behind origin/$DEFBR" done ``` ## Impact — your in-flight scope ∩ what moved The differentiator (issue #47, druyang): not just "what did I miss" but "how do these changes impact *my* current/future work". Three steps. **A. Files that moved upstream by others** in the window: ```bash # DO NOT use `git log --name-only` here: log history-simplification # silently drops the file list for many commits (verified on a busy # real repo — one-pass gave 44 files, the true union was 140). Get the non-me # commit hashes first (no --name-only, tab sep — hashes/emails never # contain a tab), then union per-commit `git diff-tree`, which is # exact and parent-aware. MOVED=$(git log "origin/$DEFBR" --since="$SINCE_ISO" --no-merges \ --pretty=format:'%H%x09%ae' \ | awk -F'\t' -v me="$GME_E" '$2!=me{print $1}' \ | while read -r h; do git diff-tree --no-commit-id --name-only -r "$h"; done \ | sort -u) ``` **B. Your in-flight scope** — the union of: ```bash # 1. files in your open PRs (GitHub ON) for n in $(gh pr list --repo "$REPO" --author @me --state open \ --json number --jq '.[].number'); do gh pr diff "$n" --name-only 2>/dev/null done # 2. local branches — BOUNDED to your own, active in the last 60d. # Never iterate every branch: big repos have hundreds of stale # ones (real repos: 400+) → unbounded scan is a firehose and slow. CUT=$(( $(date +%s) - 60*86400 )) for b in $(git for-each-ref --sort=-committerdate refs/heads \ --format='%(refname:short)%09%(committerdate:unix)%09%(authoremail)' \ | awk -F'\t' -v me="$GME_E" -v def="$DEFBR" -v cut="$CUT" \ '$1!=def && $2>cut && index($3,me)>0 {print $1}' | head -15); do mb=$(git merge-base "$b" "origin/$DEFBR" 2>/dev/null) || continue git diff --name-only "$mb" "$b" done # always include the current branch + working tree (even if older) cur=$(git branch --show-current) [ -n "$cur" ] && [ "$cur" != "$DEFBR" ] && \ git diff --name-only "$(git merge-base "$cur" origin/$DEFBR)" "$cur" # 3. uncommitted working tree git status --porcelain | awk '{print $2}' ``` Collect all of B into `MINE` (sorted unique). State the bound in the brief: *"scanned your N branches active in 60d, not all 400."* **C. Intersect and classify:** ```bash comm -12 <(printf '%s\n' "$MOVED" | sort -u) <(printf '%s\n' "$MINE" | sort -u) # DIRECT ``` - **Direct overlap** (exact path in both) → name the incoming commit/PR/ticket *and* which of your PRs/branches owns the file. This is a real conflict/semantic risk — promote it into Top priorities. - **Adjacent** — no exact match but a shared top-level dir/module (`dirname` to 2 levels) → "may affect your work", lower rank. - At `--depth deep`: for each direct-overlap file, read the incoming change and write one semantic line — what *about* your work it affects (API/schema/signature/behavior), not just "it changed". If your scope is empty (no open PRs, on default branch, clean tree), say so in one line and skip the Impact block — don't fabricate risk. ## Linear **MCP ON:** use the available Linear tool(s) to fetch, scoped to `updatedAt >= $SINCE_ISO`: - issues assigned to the current user - issues whose state changed in the window - new comments on issues you're assigned to or created Keep each to one line: `PROJ-1234 "title" → InProgress (by @x)`. Never dump full descriptions/comment threads (Iron Law 2). **MCP OFF (no-Linear proxy):** harvest ticket refs from the GitHub/git output you already have: ```bash grep -oE '[A-Z]{2,}-[0-9]+' <<<"$ALL_PR_AND_COMMIT_TITLES" | sort -u ``` Present them as *"tickets referenced in recent merges (unverified — no Linear MCP): PROJ-412, PROJ-449…"*. This still tells the user which work areas moved, without Linear access. ## Calendar **MCP ON:** list events from `$SINCE_ISO` to end of today in the user's TZ. Split by `now`: - **Missed** — ended during the window (flag if you were an organizer or required attendee). - **Today** — upcoming, with start time in local TZ for the Timeline. **MCP OFF:** skip; note *"Calendar MCP absent — meeting signal not included."* Do not attempt ICS/web fallback at MVP. ## Cross-source linking (depth: standard/deep) When a PR title and a Linear ticket (or harvested ref) share an `XXX-####` token, link them in the brief: `PR #1093 ↔ PROJ-318`. This is the bit generic digests cannot do and is the plugin's differentiator — a unified view, not four parallel inboxes. ## Failure policy Any single command failing (network, auth scope, rate limit) → `echo` a one-line degraded note for that signal and continue. The brief must still render from whatever succeeded. `git log` alone is a valid (minimum) brief. -
time-window.md 7.6 KB
# Time Window Resolution Turn `--since` into an unambiguous instant, then derive everything else from it. **Epoch seconds is the single pivot** — it is timezone-free, and both GNU (`date -d`) and BSD/macOS (`date -v`/`-r`) can read and write it. Never resolve calendar words straight to UTC; that is the timezone bug. - `SINCE_EPOCH` — Unix seconds. The one source of truth. - `SINCE_ISO` — `date -u` of `SINCE_EPOCH`, e.g. `2026-05-12T22:00:00Z` (for `gh api` / MCP filters; compares on absolute time). - `SINCE_DATE` — UTC `YYYY-MM-DD` of `SINCE_EPOCH` (for `gh search`). - `SINCE_LABEL` — human, with the anchor TZ shown so it is unambiguous: `since Fri May 13 00:00 CEST (3 days)`. ## The timezone model (read this) The person running `/catchup` is on their own machine, so the machine's **local timezone is the user's timezone**. Calendar words resolve in that local TZ: - `--since "friday"` → the user's most recent Friday, **00:00 local**. - `--since "yesterday"` → start of yesterday, **local**. - `--since "2026-05-13"` → that date **00:00 local**. That local wall-clock is converted **once** to `SINCE_EPOCH` (an absolute instant). Every source (git author/commit time, GitHub API UTC timestamps, Linear/Calendar) is then compared on that absolute instant. Consequence — and this is the desired behaviour: > A colleague in another timezone whose own "Friday" begins at a > different absolute moment is included **iff their event's absolute > timestamp ≥ the user's Friday instant**. "Since Friday" means *since > the user's Friday started*, not "since each author's local Friday". > Their Friday-morning commit counts only if it happened at/after the > user's Friday 00:00 in real time — which is exactly right. Relative durations (`2h`, `3d`) are TZ-agnostic deltas: `now - N`. ## Grammar for `--since` | Input | Resolution | |--------------------|-----------------------------------------------------| | `last-active` | (default) smartest: latest evidence *you* were here | | `last-session` | newest Claude session mtime, this repo only | | `last-commit` / `last-mine` | your last own commit / PR / review, whichever newest | | `2h`, `90m`, `3d` | `now - duration` (TZ-agnostic delta) | | `yesterday` | yesterday 00:00 **local TZ** | | `friday`, `monday` | most recent past occurrence, 00:00 **local TZ** | | `"2026-05-13"` | that date 00:00 **local TZ** | | `"last week"` | `now - 7d` | Validate `--since` against this grammar **before** it touches a shell. No match → fall back to 24h and note the assumption in the brief's Risks block. ## Resolving each form to `SINCE_EPOCH` ```bash LOCAL_TZ=$(date +%Z) # user's TZ abbrev, for the label NOW=$(date +%s) # relative duration: now - N (TZ-agnostic) # parse 2h/90m/3d -> seconds, SINCE_EPOCH=$((NOW - secs)) # yesterday / explicit date: local midnight -> epoch # GNU : date -d 'yesterday 00:00' +%s # date -d '2026-05-13 00:00:00' +%s # BSD : date -v-1d -v0H -v0M -v0S +%s # date -j -f '%Y-%m-%d %H:%M:%S' '2026-05-13 00:00:00' +%s # weekday name: most-recent-past occurrence at local 00:00. # Use day-of-week arithmetic (do NOT rely on `date -d 'last friday'` — # GNU/BSD disagree, and behaviour on the named day itself differs): TARGET=5 # Mon=1..Sun=7 (here: Friday) DOW=$(date +%u) BACK=$(( (DOW - TARGET + 7) % 7 )) # 0 if today IS that weekday # GNU : date -d "$BACK days ago 00:00" +%s # BSD : date -v-"${BACK}"d -v0H -v0M -v0S +%s # BACK=0 ⇒ today 00:00 local ("since friday" said on a Friday = today). ``` Then derive the rest from the pivot (portable both ways): ```bash SINCE_ISO=$(date -u -d "@$SINCE_EPOCH" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ || date -u -r "$SINCE_EPOCH" +%Y-%m-%dT%H:%M:%SZ) SINCE_DATE=${SINCE_ISO%%T*} ``` - **git**: pass the absolute instant — `git log --since="$SINCE_ISO"` (UTC `…Z`). Git filters on each commit's own absolute timestamp, so cross-timezone colleagues are handled correctly. Do **not** pass a bare `--since="friday"` to git: git would re-resolve it in the machine's local TZ *and* with its own weekday quirks — exactly the inconsistency this pivot removes. - **`gh api` / notifications / MCP**: use full `SINCE_ISO` (exact, timestamp-granular). - **`gh search` / `gh pr list --search`**: `updated:>=$SINCE_DATE` only — GitHub search is **UTC, date-granular**. Near a TZ/midnight boundary this can be off by up to a day, so treat search hits as a coarse pre-filter and confirm precise inclusion with the `SINCE_EPOCH`/`SINCE_ISO` timestamp on each item before it enters the brief. ## `last-active` auto-detect (default) — "since I was last here" The default must answer "what changed **while I was away**", so the anchor is *the most recent moment we have hard evidence the user was working*. A commit or a PR is stronger proof of presence than a session file (which can be a background/scheduled run). Take the **MAX** of these absolute instants — the latest footprint is the correct lower bound: you were definitely here then; everything after is "while away". All are already absolute, so no TZ handling. ```bash SLUG=$(pwd | sed 's@/@-@g'); SDIR="$HOME/.claude/projects/$SLUG" # 1. newest Claude session mtime for THIS repo (skip the live session: # if newest mtime is within ~5 min of now, use the second-newest) S1=$(ls -t "$SDIR"/*.jsonl 2>/dev/null | head -1) E_SESS=$( [ -n "$S1" ] && { date -r "$S1" +%s 2>/dev/null || stat -c %Y "$S1"; } ) # 2. your last own commit anywhere in this repo (committer date, abs) GME=$(git config user.email) E_COMMIT=$(git log --all --author="$GME" -1 --format=%ct 2>/dev/null) # 3. your last own PR activity IN THIS REPO (repo-scoped — a global # `gh search prs --author=@me` is wrong here: it would anchor to # activity in some *other* repo and miss a week of changes in this # one). Often empty (you commit but don't author PRs) → just skip. REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null) E_PR=$( [ -n "$REPO" ] && gh pr list --repo "$REPO" --author @me \ --state all --limit 1 --json updatedAt \ --jq '.[0].updatedAt' 2>/dev/null \ | { read d; [ -n "$d" ] && { date -u -d "$d" +%s 2>/dev/null \ || date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$d" +%s; }; }) # MAX of whatever resolved = "you were last here" SINCE_EPOCH=$(printf '%s\n' "$E_SESS" "$E_COMMIT" "$E_PR" \ | grep -E '^[0-9]+$' | sort -n | tail -1) ``` Record in the brief which signal won, e.g. *"window anchored to your last commit `a1b2c3d` (Fri 18:42 CEST) — more recent than your last session here."* That transparency lets the reader sanity-check the boundary. **Variants:** `last-session` = signal 1 only. `last-commit` / `last-mine` = MAX of signals 2 and 3 only (ignore session files — useful when you want "since I last *worked*", not "since Claude last ran here"). No signal at all → `SINCE_EPOCH=$((NOW - 86400))` and note *"No activity signal — defaulted to 24h."* in the Risks block. Optional cross-check: if ccrider MCP is present, its last-session-for-cwd timestamp can confirm signal 1. Not required. ## Producing `SINCE_LABEL` ``` days = round((NOW - SINCE_EPOCH) / 86400) SINCE_LABEL = "since {Www Mmm DD} {HH:MM} {LOCAL_TZ} ({days}d)" ``` Sub-day windows use hours: `since 09:12 CEST today (5h)`. Always show the anchor **in the user's local TZ with the TZ abbrev** — the reader must see which Friday the brief means. Goes in the brief's **Intent** line and **Timeline** block.
-
-
SKILL.md 6 KB
--- name: catchup description: "Summarize and review what changed while you were away. Use after a weekend, vacation, or flight to check missed PRs, git commits, Linear tickets, and meetings — one prioritized brief, not a firehose." effort: medium disable-model-invocation: true argument-hint: "[--since \"friday\"|\"2h\"|\"last-active\"|\"last-commit\"] [--scope repo|all] [--sources github,git,linear,calendar] [--depth quick|standard|deep] [--focus prs,reviews-requested,mentions,impact]" --- # Catchup — Async-Team Return Briefing You've been away. This skill is a **thin orchestrator**: it resolves the window + sources here, then delegates the I/O fan-out, impact analysis, and brief assembly to the `catchup-runner` agent (Sonnet) so your (often Opus) session does not pay for summarization. You only print what the agent returns. ## Usage ``` /catchup # since you were last active /catchup --since "friday" /catchup --since "2h" --focus reviews-requested /catchup --since last-commit --depth deep /catchup --scope all # include cross-repo pings/reviews ``` Default is **repo-scoped**: every GitHub signal (reviews requested, notifications, mentions) is filtered to the repo you ran it in. Pass `--scope all` to also include cross-repo activity, which is then listed in its own separate section — never mixed into this repo's lists. ## Iron Laws 1. **Delegate the heavy work** — spawn `catchup-runner` (sonnet) for fan-out + assembly. Do NOT run the `gh`/`git` fan-out in this session; that defeats the cost/speed purpose. 2. **Resolve the window here, once** — the agent must never re-resolve it. Pass absolute `SINCE_*` values. 3. **MCP runs here, not in the agent** — Linear/Calendar MCP tools are unreliable in subagents. If present, fetch in this context and pass the text to the agent; else mark absent. 4. **Validate `--since` before any shell** — match the grammar; on no match fall back to 24h and note the assumption. 5. **Stop after the brief** — print the agent's summary, never auto-transition to another command. 6. **Repo-scoped by default** — pass `SCOPE=repo` unless the user passed `--scope all`. A brief run inside one repo must not leak another repo's reviews/notifications. Cross-repo is opt-in only. ## Workflow ### 1. Parse arguments From `$ARGUMENTS`: `--since`, `--scope`, `--sources`, `--depth`, `--focus`. Defaults: `--since last-active`, **`--scope repo`**, all detected sources, `--depth standard`, no focus. `--scope` accepts `repo` (default — every GitHub signal filtered to the current repo) or `all` (cross-repo allowed, listed in its own section). ### 2. Resolve the time window (here, in this context) Read `${CLAUDE_SKILL_DIR}/references/time-window.md`. Resolve calendar words (`friday`, `yesterday`, a date) in the **user's local timezone** (this machine = the user's TZ), pivot through `SINCE_EPOCH`, derive `SINCE_ISO` (UTC) + `SINCE_LABEL` (with TZ abbrev) + `LOCAL_TZ`. Default `last-active` = MAX of: newest Claude session mtime for this repo, your last own commit (`git log --author=<you> -1 --format=%ct`), your last own PR **in this repo** (`gh pr list --repo <repo> --author @me --state all`, repo-scoped — a global search would anchor to other repos). The latest footprint is "you were last here". Record which signal won. Variants: `last-session` (sessions only), `last-commit`/`last-mine` (your git/PR only). No signal → 24h, noted. ### 3. Detect sources + pull MCP data (here) ``` gh: command -v gh && gh auth status → github ON git: git rev-parse --is-inside-work-tree → git ON linear/calendar: a Linear / Google-Calendar MCP tool present? ``` If Linear/Calendar MCP is present, query it **in this context now** (assigned/updated tickets since `SINCE_ISO`; missed + today's meetings in `LOCAL_TZ`) and keep the short text as `LINEAR_DATA` / `CALENDAR_DATA`. If absent, set them to `absent` (the agent will proxy-harvest `XXX-####` refs for Linear; skip calendar with a note). ### 4. Delegate to `catchup-runner` (Sonnet) Spawn one agent, foreground, passing a self-contained prompt: ``` Agent(subagent_type: "catchup-runner", prompt: """ SINCE_EPOCH={…} SINCE_ISO={…Z} SINCE_LABEL="{… local TZ}" LOCAL_TZ={…} SOURCES={github,git} SCOPE={repo|all} DEPTH={…} FOCUS={…} OUT_PATH={cwd}/.claude/catchup/brief-{YYYY-MM-DD}.md # local date (date +%F), not UTC LINEAR_DATA={text or "absent"} CALENDAR_DATA={text or "absent"} Window anchor signal: {which one won, for the Risks note} Do the gh+git fan-out, impact analysis, and brief assembly per your instructions. Write the file. Return ONLY the inline summary. """) ``` The agent inlines all recipes (it cannot read this plugin's references). Do not re-implement its work here. ### 5. Present + stop Print the agent's returned summary verbatim and the brief path. **If the agent returned no summary** (e.g. it hit its turn budget mid-assembly — the brief file is usually already written): do NOT re-summarize the brief yourself; that pulls the expensive step back into this (often Opus) session, defeating the delegation. Instead `SendMessage` the agent by the `agentId` from its stop usage: *"Return only the inline summary now."* — it finishes cheaply in Sonnet. Only if that also fails, read the brief's Intent + Top priorities section (not the whole file) and print that. **Do NOT** auto-invoke any other command. The user decides what's first. ## Sources at MVP GitHub (`gh`), Git (`git`), Linear MCP (optional), Google Calendar MCP (optional). Slack/Gmail are **v2 opt-in** — never queried, never piped raw. Scheduling + per-project config are designed in `${CLAUDE_SKILL_DIR}/references/config-schema.md`, not built at MVP. ## Graceful degradation contract A missing source degrades the brief, never breaks it. `git log` alone (always available in a repo) is a valid minimum brief. Every absent or failed source becomes one honest line under the brief's Risks/assumptions block, so the reader knows what it does *not* cover.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.