Claude Skill

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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-plugins_catchup_skills_catchup-9767a82.zip · 16 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/catchup/skills/catchup
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git 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

  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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related