standup

Read-only release + backlog recap. Reports the last release, commits-since-tag classified by conventional-commit type with the semver bump they trigger and pushed-vs-origin state, the backlog bucketed

SKILL.md

GitHub

standup — where are we, what's next

A read-only recap utility in the family of audit-baseline / rca: it reads state and reports, it never mutates. Run it whenever you sit down to plan a release or pick the next piece of work. It is not a workflow phase — it does not enter the Track Guard ordering and never blocks a commit.

What it answers

  1. Shipped — the last released version (from CHANGELOG.md / the latest tag).
  2. Staged but unreleased — every commit since the last tag, classified by conventional-commit type, with the aggregate semver bump those commits will trigger (read from .releaserc.json at runtime, so it never drifts from the release config) and the pushed-vs-origin state.
  3. Backlog — entries bucketed open / picked-up / dropped, with epic children nested under their parent.
  4. Open questions — pending-questions.md condensed to id + question + blocker.
  5. Roadmap — the execution plan's epics with their status and per-task tallies, the rows still open under each, plus the Progress bullets, read from project.json → roadmap.path.
  6. Recommended next pickup — assembled in main context (see Article II below).

How to run

One invocation returns the whole recap. Use the CLI front door:

node .claude/skills/standup/cli.mjs recap [--json] [--root <repo-root>] [--remote]

Without --json it prints the bounded rendering; with --json it prints the raw StandupRecap.

The bound is a threshold, not a blanket collapse. Detail renders while the pile is small and degrades to counts once printing it back would reproduce the cost the CLI exists to remove:

Section At or below the bound Above it
Unreleased commits one line per commit: type, bump, subject counts-by-type plus the aggregate bump (COMMIT_DETAIL_MAX = 20)
Roadmap epics each epic's OPEN rows listed beneath it — planned and in-progress, never done per-task tallies only (OPEN_TASK_DETAIL_MAX = 20, measured across the whole plan)

Collapsing at every size was the older behaviour, and it cost a second --json pass to answer "what is actually in this pile?" for a four-commit pile. Done rows are never listed at either size: they are the bulk of a finished epic and carry no pickup signal.

Alongside those, the pushed-vs-origin state and the declared completeness gate each render as their own line, so the two questions that decide whether to cut — is this even pushed? and may a half-wired feature ship? — are answered without opening git or project.json.

Every rendered detail line is whitespace-collapsed, clipped to 96 characters, and stripped of C0/C1 control characters. Roadmap titles, commit subjects and question bodies are all repository-controlled content on its way to a terminal, and a row title in this repo already runs past 1000 characters.

--remote — prove the release picture against the remote

Every release figure in the recap is read from local refs: git describe --tags for the last tag, rev-list @{upstream}...HEAD for the pushed-vs-origin state. On a clone that has not fetched, both answer from a stale view. On 2026-08-13 that reported v0.21.0 with 70 unreleased commits while v0.22.0 was already tagged, published to npm, and released — the operator read a shipped release as an unshipped pile.

The default run therefore states its own limitation in the Release block (Figures read local refs, not fetched) and names the remedy. Pass --remote to check instead of caveat:

  • git ls-remote --tags origin and --heads origin <branch> are read-only; nothing is fetched and no ref is mutated.
  • A newer remote tag or a diverged branch head adds stale-remote-refs to degraded[] and fills release.remote with {probed, stale, remoteTag, remoteHead, reason}.
  • A probe that cannot run — offline, no remote, auth, timeout — adds remote-probe-failed instead, keeps the local figures untouched, and still exits 0. "I could not check" never renders as "you are current", and never as "you are stale" either.
  • release.remote.headState reports the branch-head comparison as one of diverged / matched / unreachable / not-comparable. The last two are distinct on purpose: unreachable means the branch tracks a remote and the probe failed, while not-comparable means there was no upstream to compare against (a detached HEAD, a branch never pushed, or a branch the remote does not carry). A checkout with nothing to compare is never reported as one whose refs match.
  • Tag comparison is numeric on strict semver, so v10.0.0 beats v9.0.0; any ref the remote advertises that does not parse is discarded before it can influence the answer.

release.remote is null on the default path, which is how a reader tells not checked from checked and current.

StandupRecap carries six keys. All six come back in that one call:

Key What it holds
release lastVersion, lastTag, commitsSinceTag[] classified by conventional-commit type with the semver bump each triggers, and upstream — {state, ahead, behind}, where state is ahead / behind / up-to-date / no-upstream
releaseModel the declared release POLICY from project.json → release — drives the regime-aware recommendation below
backlog entries bucketed open / picked-up / dropped, epic children nested under their parent
pendingQuestions pending-questions.md condensed to id + question + blocker
roadmap the execution plan's epics[] (number, title, tag, status, per-task tallies, plus openTasks[] of {id, status, title} for the rows still open) and its Progress bullets
degraded one marker per source that could not be read — no-roadmap-plan, no-release-model, and so on

Do not re-derive any of these by hand afterwards. Re-reading the roadmap file or re-running git log after this call is the multi-pass behaviour the CLI replaced; every key above is already in the result.

Six is the whole list, and it stays six. New data nests inside an existing key rather than becoming a seventh — openTasks sits on the roadmap epic, upstream and remote sit on release. tests/standup-cli-recap.test.mjs:71 asserts the count, so widening the top level breaks a contract other readers depend on.

It degrades gracefully — on a non-git tree, a repo with no tags, or missing memory files it names the missing precondition in degraded[], still exits 0, and never throws. gather.mjs remains importable as the collector for in-process callers such as the session-start hook.

Article II — where the judgment lives

Per CLAUDE.md Article II, decisions live in main context. The helper only gathers the mechanical recap; it does not pick what to build next. After reading the helper's JSON, the recommendation — the single suggested next pickup and its one-line rationale (smallest unblocker first) — is reasoned out in main context, not emitted by the helper. The session-start surface (the memory_session_start hook) shows only the compact mechanical recap plus a pointer back to /standup; the full judgment recommendation is on-demand here.

Release-model-aware recommendation (AC-303)

When gatherSync surfaces a releaseModel (from project.json → release), the "can this unreleased pile ship?" judgment becomes regime-aware — the model tells main context which regime it is in, the way git.workflow_model overrides generic branching instincts:

  • release_trigger: on-push / release_cycle: continuous / consumer_upgrade_cadence: frequent → recommend pushing early and often; an unpushed pile is the risk. Fix-forward is cheap.
  • release_trigger: on-tag|manual / release_cycle: sprint-based|manual / consumer_upgrade_cadence: rare → a release is a CD pressing: recommend gating the cut on a completeness/coherence audit. When release.completeness_gate.half_wired_blocks_release is true, do not recommend cutting while any feature in the unreleased set is half-wired-on-the-disc (shipped-dark but opt-in-broken).

A no-release-model marker in degraded means no policy is declared — fall back to the pre-model behavior (report the semver bump from .releaserc and let the human decide). The judgment stays in main context (Article II); collectReleaseModel only surfaces the config.

Constraints

  • Read-only. Reads git, .releaserc.json, CHANGELOG.md, and the memory files. Writes nothing.
  • Never writes CHANGELOG.md — semantic-release owns it in CI.
  • Never starts, stages, or commits work — it recommends; you act.
  • Deterministic, offline core — identical repo + memory state yields identical helper output (no clock read, and no network call, in the core path). --remote is an explicitly non-deterministic opt-in that sits outside this guarantee: its result depends on a remote that can change between two otherwise identical runs. The guarantee is narrowed in scope, not weakened — every caller that does not pass --remote, which is every in-process caller on disk including the memory_session_start hook, keeps exactly the property it had before.
  • Read-only over the network too — the probe runs git ls-remote only. It never fetches, never writes a ref, and never changes what a later git status or /commit sees.