Review: release (synthesis)
Harness note. Skill names are written
/name(Claude Code slash form); in Codex invoke the same skill as$name. Sub-agent dispatch is shown as Claude Code'sAgent({ subagent_type: "general-purpose", … })— in Codex spawn a sub-agent with the same prompt, or apply the contracts inline, in sequence, when sub-agents are unavailable.
This lane produces one decision and at most three next actions. Nothing else.
SHIP · FIX FIRST · REFACTOR · CONTINUE BUILDING · RETHINK
How this lane differs from the other three
change · experience · health |
release |
|
|---|---|---|
| Execution | fans out to subagents | inline, no fan-out |
| Evidence | the codebase, the browser, the dependency tree | only the report comments on the issue + what you tell it |
| Output | findings | one verdict + ≤3 actions |
Never re-scan the codebase. Re-scanning re-discovers exactly what the other three profiles just found, at full cost, with a weaker contract. If evidence is missing, the answer is "coverage is 2/3", not "let me go look".
Evidence boundary — read this literally:
- ALLOWED:
list_commentson the named issue (Linear MCP), and the contents of the report comments it returns — those whose body starts with## Review:and carries the template §2yamlblock. - ALLOWED: git metadata —
git rev-parse,git status --porcelain,git rev-list --count,git log --oneline. Freshness math needs these; they are not a codebase scan. - FORBIDDEN: reading, grepping or globbing source files, tests, configs, lockfiles, or any file under
docs/. Forbidden even to "just double-check" one finding. - FORBIDDEN: spawning subagents. This lane is inline.
The issue id comes from $issue-id or the conversation; none → ask. Without the Linear MCP, ask the user to paste the report comments and treat the pasted text as the evidence set — never go looking in the repo for them.
Step 1 — anchor the current state
git rev-parse HEAD
git status --porcelain=v1 | shasum -a 1 | awk '{print $1}' # current tree_digest
git status --porcelain | head -1 # empty output => dirty: false
Hold these three values. Every freshness test compares against them.
Step 2 — select which reports to read
list_comments { issueId: "<ISSUE-ID>", limit: 250 } # follow `cursor` until hasNextPage=false
Keep top-level comments whose body starts with ## Review: and carries the §2 yaml block; drop replies and every other comment.
Selection rule: for each of change, experience, health, take the single most recent report of that profile, ranked by the reviewed_at field in its header — not by the comment's createdAt and not by list order.
Rationale, and why not the obvious alternative: selecting only reports whose head_sha matches the current head would return nothing in the ordinary case, because reports are written before the follow-up commits that a release decision is about. That would make the freshness law dead code and this lane useless. So freshness classifies evidence, it does not filter it — a stale health report is still worth reading, it just cannot support SHIP.
reviewed_at over createdAt because a report reposted late (a harness without the MCP, pasted by the user afterwards) carries a newer createdAt than the run it records; reviewed_at is the recorded moment of the run. If a report has no reviewed_at or it is malformed, fall back to createdAt, and mark that report ordering-uncertain in the coverage table.
Ignore prior release reports for evidence purposes — they are decisions, not findings. They are used only to bound Step 4.
Step 3 — classify freshness
Apply ~/.claude/templates/review-report.md §6 exactly. Two rules, chosen by the report's source_kind:
git-range / working-tree — fresh only if ALL THREE hold:
- report
head_sha== currentHEAD - report
dirty: falseAND the working tree is clean right now - report
tree_digest== currentgit status --porcelaindigest
url (this is how experience reports are anchored) — fresh only if:
- report
deploy_rev== thehead_shaunder judgement, OR - the user confirms in this conversation that the reviewed deployment was built from that head.
deploy_rev: null is not fresh by default. Ask: "The experience report reviewed <url> but recorded no deploy revision. Was that deployment built from <head7>?" An unanswered question is a stale, not a fresh.
Compute distance for every stale report so the number is concrete:
git cat-file -e <report_head_sha>^{commit} 2>/dev/null && \
git rev-list --count <report_head_sha>..HEAD
If the sha is not in the repo (rebased, dropped, different clone), record distance: unknown — sha not in this repo rather than guessing.
Any deviation on any condition → stale. A stale report can never support SHIP.
Step 4 — carry unresolved blockers forward
The latest report of a profile does not contain blockers that an earlier run raised and nobody fixed. Those still count.
Scan back through same-profile reports newer than the most recent release report (if there is no prior release report, cap the lookback at 10 report comments). Collect findings with severity: blocker whose key does not appear in the selected latest report.
Each carried blocker is carried — not re-verified. This lane cannot verify a fix; verification requires reading code, which is forbidden here. It clears only when:
- the user states it is fixed, or
- a fresh report of the same profile at the current head no longer lists that key.
Blockers only. Do not carry high or below — that turns a synthesis into an unbounded backlog replay.
Step 5 — talk to the user
This lane needs conversation; it is the reason it runs inline. Ask, in this order, and stop for answers:
- Product direction. "What is this release for, and what does 'done' mean for it right now?" Without this,
CONTINUE BUILDINGandRETHINKare indistinguishable fromSHIP. urlconfirmations — anyexperiencereport whosedeploy_revis null or mismatched (Step 3).- Coverage acknowledgment — for each missing or stale profile, name what evidence is absent and get an explicit answer. Not a generic warning; one question per gap:
- "
experiencehas never run. You would be shipping with no evidence that a user can complete the flow. Accept that?" - "
changelast ran 47 commits ago. Nothing has reviewed correctness of those 47 commits. Accept that?"
- "
- Carried blockers — for each, "still open, or fixed since?"
Step 6 — decide
SHIP is the only verdict with a mechanical gate. All five conditions must hold:
- at least 2 of the 3 source profiles have a report, and
- every report consulted is
fresh, and - no open blockers in any consulted report, and
- no carried blocker still open (Step 4), and
- every coverage gap was explicitly acknowledged by the user in this conversation (Step 5.3).
Fail any one → SHIP is off the table. Say which condition failed.
The other four are judgement, settled in the dialogue. Signals, not a lookup table:
| Signal | Points toward |
|---|---|
| Any open blocker, anywhere | FIX FIRST |
Findings are mostly arch/* at high, code is described as hard to change |
REFACTOR |
| Few findings, and the product direction says the feature is not finished | CONTINUE BUILDING |
The same key recurs unresolved across 3+ runs |
RETHINK — the approach is not converging, not the fix |
experience shows users cannot complete the core task |
RETHINK, not FIX FIRST |
| Findings contradict the stated product direction | RETHINK |
Then at most 3 next actions, each concrete enough to start today, ordered. If there are more than three candidates, that ranking is the value this lane adds — do not widen the list to avoid choosing.
Preconditions — how little evidence is too little
| Reports found | Behaviour |
|---|---|
| 0 | blocked. Emit no verdict. Say: no review comment exists on <ISSUE-ID>. Cold start per the framework: run /review-health first (cheapest, needs only the repo), then /review-change or /review-experience depending on what you are working on. |
| 1 | Verdict allowed, marked provisional. SHIP is forbidden regardless of freshness — gate condition 1 fails. |
| 2 or 3 | Full synthesis. |
blocked is a precondition failure, not a sixth verdict.
Output format
The first line of the output is the coverage declaration. Always. No preamble above it.
Coverage: 2/3 profiles. `experience` has never run. `health` ran at `a3f2c1`, 47 commits behind head. `change` fresh at head.
Without that line first, this lane is a confidence generator: a verdict that sounds informed while resting on evidence nobody counted. Then:
VERDICT: FIX FIRST
Why: <2-4 sentences tied to specific finding keys and the product direction>
Next:
1. <action> — <which finding key it closes>
2. <action>
3. <action>
Carried blockers (not re-verified): sec/auth/token-in-log
Acknowledged gaps: experience never ran — user accepted
Post the report
Every run posts one report comment, per the framework's rule that each run leaves a record.
save_comment { issueId: "<ISSUE-ID>", body } (run_id = 6 random base36 chars). Never a file in the repo; without the MCP, print the body in chat for the user to post.
Header per ~/.claude/templates/review-report.md §2, with:
source_kind: working-tree— the decision accounts for uncommitted state, so the working tree is the thing judged.scope: worktree— the template defines three scope forms and none of them is release-specific.worktreeis the accurate one of the three; the head anchor lives inhead_sha, andrun_idkeeps runs apart.head_sha,dirty,tree_digest— the values captured in Step 1.
Body order: coverage table first (template §3, adapted — rows are the three source profiles, with fresh / stale / never-ran and the commit distance), then verdict, then reasoning, then the ≤3 actions, then carried blockers, then acknowledged gaps.
This lane is inline, so it is its own parent: it posts the comment itself. That does not contradict template §7 — the rule there prevents parallel subagents racing on one report, and there are no subagents here.
Known limitation — stated, not solved
This command only runs when you remember to type it, or when an agent recognises the moment and calls it. No hook enforces it, and none can — the framework is manual by design.
The failure mode is specific: the moment you most need this lane is when you are stuck and going in circles, and being stuck is exactly the state in which you do not step back and notice you are stuck. An agent mid-task has the same blind spot. The description names "stuck, unsure whether to ship or fix" precisely so that model-invocation has a chance to fire in that state — but that is a mitigation, not a fix, and it depends on the agent noticing. Treat a missed /review-release as expected, not exceptional.
Contract
Load reviewers/strategy.md and follow it. It runs inline here — do not pass it to a subagent.