Caail pr wrapup
Use when a finished, locally-green CAAIL feature branch is ready to ship — run the multi-round code review, push it, open a PR to main, watch CI, merge (after confirming), watch the GitHub Pages deploy to green, verify the live site, clean up the worktree/branch, and close out the GitHub issue + Jira ticket. Invoke whenever the user says to ship / wrap up / finish / "open a PR and merge" / "merge and deploy" a branch, asks to watch the deploy, asks to clean up a worktree after merge, or asks to close the ticket for shipped work — even if they don't name the skill. The CAAIL realization of the "Finish & Ship" stage.From its SKILL.md
npx -y skills add tucca-cellag/caail --skill caail-pr-wrapupAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
29.9 KB, as published. Nobody here has run it
CAAIL PR wrap-up
Overview
This is the Ship stage for CAAIL: it takes a feature branch whose work is done, committed, and
locally green, and lands it on main and the live site — review rounds → push → PR → cross-model
second angle → checks → merge → GitHub Pages deploy → verify → clean up → close the trackers. CAAIL
deploys only on push to main (via .github/workflows/docs.yml, which gates on Lighthouse), so
"shipped" means merged and the deploy is green, not just merged.
Review here is a phase, not a step. Step 1 runs /code-review high, applies or triages the findings,
and then reviews again on the updated diff, repeating until one of the stop rule's two endings is
reached. The level is fixed; the depth comes from the number of rounds, and reference/review-phase.md
is where that floor is set out in full. The one part
restated below is its default, that a diff matching no row is a 2, and it is restated because an agent that
reads only this file still has to get that right; every other part of the rule lives there alone. The reasoning, and the condition under which the extra
rounds can come back down, is there too. The cross-model pass (step 4) stays, as a cheap extra angle
rather than the safety gate.
Step 1 usually does not run unattended. It carries two kinds of AskUserQuestion pause, not two pauses, and
a run whose floor rounds come back quiet, surface no pre-existing findings and change nothing in the
diff fires neither and finishes without asking anything. All three matter, because "quiet" is condition
2 alone: a round that found a defect and fixed it is quiet while condition 3 is unmet, and a quiet round
that surfaced a pre-existing finding still fires the scope gate. Either gate can fire more than once and
neither is a budget of one, and each fires only on its own condition rather than on every round, so read
the gates rather than this sentence for when. They are the only two decisions
in the phase that belong to the maintainer rather than to the agent: whether to keep reviewing once the
floor is met (the stop gate) and whether a finding this diff did not cause belongs in the PR at all
(the scope gate). Both are prose in a phase with no mechanism, so they are worth exactly the reading
of them and no more; reference/ship-pr-reference.md's opening note, that step 1 has no subcommand and
nothing in CI enforces it, says why that matters.
The two fire at different moments, and collapsing them defeats one of them. The scope gate runs at every round's triage, including rounds below the floor, because a finding's disposition is decided the moment the finding exists. The stop gate runs only once the floor is met, and never below it. So "rounds 1..floor run unprompted" is true of the stop gate alone, and reading it as covering both hands the agent back the scope decision the scope gate exists to take away from it.
The brittle, repeatable machinery lives in ship-pr.sh (in this skill's directory): pushing,
opening the PR, watching checks, merging with the known worktree gotcha handled, finding + watching
the deploy run, and curling the live routes. This manual keeps the judgment with you: re-running the
local gate, the review rounds and when they stop, composing the PR body, pausing to confirm
before the merge (it triggers a public deploy) and before deleting a hand-made worktree, the worktree
cleanup itself, and closing the trackers once the deploy is actually green.
This is a skill, not an agent, on purpose: it acts (push/merge/deploy), and the cleanup uses
ExitWorktree, which only works in the main session — a subagent can't switch the parent session's
directory. Run the phases below in order, in the main session, pausing where noted.
Run the helper from the repo (worktree) root: bash .claude/skills/caail-pr-wrapup/ship-pr.sh <sub>.
Preconditions (stop if any fail)
- On a feature branch, never
main. Ideally a worktree created byEnterWorktreethis session. - Working tree clean. Commit first. Stashing is fine for unrelated work in progress, but never for a
review fix: it clears the check while leaving the fix out of the diff (
reference/review-phase.md, and the matching row inreference/gotchas.md). - The local gate is green. Re-verify it now — don't ship on faith. With Node 22 (
source ~/.nvm/nvm.sh && nvm use 22):pnpm --dir site test, and when the change touchessite/**alsopnpm --dir site buildandpnpm --dir site test:e2efor the affected specs. A red gate means the branch isn't ready; fix it before shipping. ghis authenticated (gh auth status).- If the branch touches
workers/**at all, deploy the Worker by hand before step 2:pnpm --dir workers/events run deploy. No workflow deploys it, so shipping the code does not ship the change, and a push publishes a commit message describing behaviour that is not live yet. Check this against the whole branch, not just what the review rounds edited: the re-gate table inreference/review-phase.mdonly covers files a round touched, so a branch whose original commits changed the Worker and whose rounds did not would otherwise never be reminded. - The PR body is publishable.
caailis a public repo, so the body, every commit message, and the branch name become world-readable, and unlike issues a PR cannot be deleted. Before composing the body, confirm every quoted path, code block and architectural detail originates in this repo — anything read from a private repo or a third party's source is not publishable, and paraphrase discloses as much as a quote. Nothing may describe an unpatched weakness in a live service; that goes to its owner privately. Full rule:.claude/rules/publishing.md(in this repo). Do not expect the hook to catch it on this path..claude/hooks/check-public-publish.shis registered in the committed.claude/settings.jsonand does protect anyone who clones this repo, but it is a PreToolUse Bash hook whose tripwire is anchored togh pr createat command position, and step 3 opens the PR from insideship-pr.sh. It therefore sees nothing here: no deny, and not even the visibility announce. On this path you are the only check, which is whypreflightprints the destination's visibility by hand. - (optional) For the step 4 cross-model review, the Cross-Model Adversarial Reviewer agent must be
configured and whatever CLI it wraps must be authenticated. Don't test for a particular binary, and
never call one directly: the agent owns which non-Claude backend it uses and enforces that the model is
not a Claude one, so probing for a named CLI both mis-reports availability and invites bypassing that
check. Ask the agent; if it reports itself unavailable or unauthenticated, surface exactly what it said
and let the operator decide whether to ship without the pass. It's an optional extra angle, not a
blocker, but the call is theirs (consistent with step 4 and
reference/gotchas.md; the agent never skips it autonomously).
Procedure
0. Pre-flight
bash .claude/skills/caail-pr-wrapup/ship-pr.sh preflight
This confirms the branch/tree/auth, lists the changed paths, and predicts (from ship-pr.sh's mirrored
lists, not the YAML: see step 5) which of lint-papers, test and guards will run on the PR and whether docs.yml
will deploy on merge, plus the routes worth verifying live. It tells you what to expect in steps 5
and 7. The changed-path list it prints is also what step 1 reads to pick its floor on rounds. Then re-run
the local gate (above) if you haven't this session.
1. Review rounds
Read reference/review-phase.md now and follow it. It is the whole phase
and it is not optional; what follows here is orientation, not a substitute for it.
Four things that are true whatever the diff, so that skipping the file is visibly wrong rather than merely undocumented:
- The level is always
high./code-review high origin/main...HEAD, level first, range named. Depth comes from the number of rounds, never from reaching for a deeper level. - There is a floor on rounds, set by blast radius, and a diff matching no row is a 2, never a 1.
- Two
AskUserQuestiongates live in this phase, so it may not run unattended: a scope gate each round for findings this diff did not cause, and a stop gate once the floor is met for whether to keep reviewing. Neither is an agent's decision to take. - Severity is the maintainer's to weigh, never yours to act on. Findings are graded and shown at the stop gate; nothing in the phase withholds an option or forces a round on the strength of a grade.
2. Push
bash .claude/skills/caail-pr-wrapup/ship-pr.sh push
3. Open the PR
Compose the title and body yourself, then write the body to a temp file and open the PR:
bash .claude/skills/caail-pr-wrapup/ship-pr.sh open-pr "<title>" /tmp/pr-body.md
- Title: Conventional Commits, Angular flavor —
<type>(<scope>): <subject>. CAAIL scopes:papers,software,data/datasets,databases,resources,research-areas,site,docs,chore,fix. Reuse the lead commit's subject when it already fits. - Body: what changed and why; the research area(s)/AI method(s) or routes it touches; and the
verification you already ran (tests/build/e2e, reviewer agents). Say how the review went: the
level and how many rounds, and then which of the stop rule's two endings this run reached. For
ending 1, say so by naming its three conditions, never as "a quiet round". Either way give the
rounds run against the floor for the diff's final shape, since step 6 checks condition 1 for both
endings and has no other source for it. For ending 2, say that
the maintainer answered "ship now" at the stop gate, name everything left outstanding, and if the last round changed the diff, say that those
fixes were never reviewed and give the reason they gave. That is the one ending that ships code no
round has read, so a body that omits it is not merely thin, it is wrong. Write this here, because the
body is composed here and step 6 checks for it:
ship-pr.shhas noeditsubcommand, and by step 6 the PR is already open. Findings routed to a ticket by the scope gate are named here too, by key. The publishing carve-out binds here, where the body is actually written, and it is defined once in the Definitions inreference/review-phase.md; for it alone, treat "declined" and "routed to a ticket" as one category. Any finding declined rather than fixed, in any round, gets named with its reason, since a reader cannot tell a triaged finding from an unnoticed one and the rounds it came from are invisible to them. The exception is the publishing carve-out in the Definitions inreference/review-phase.md, which governs its own scope; this body is world-readable and permanent, and publishing what that carve-out withholds would breach.claude/rules/publishing.md. Nothing will stop you, so decide before you write it:check-public-publish.shis a PreToolUse Bash hook that matchesgh pr createat command position, and step 3 runsgh pr createinsideship-pr.sh, where the hook cannot see it. On this path there is no deny and not even the visibility announce. The judgment is entirely yours. If the change added a guard, state that it was seen failing on the defect first (step 1). No AI attribution — CAAIL commits and PRs never carry "Co-Authored-By: Claude" or "Generated with" lines. - Link the trackers. If this PR resolves a public GitHub issue, include a
Closes #Nline — GitHub then closes it on merge, so the close is declarative and can't be forgotten or fail after the merge is already irreversible. Name the Jira key (CAAIL-NNN) too, so the public record points back at the durable one. The key only, never the ticket's contents — the CAAIL project is private, and adisclosure-privateticket gets no quotation, paraphrase or summary in a public PR body (.claude/rules/publishing.md). If the work has no ticket on either tracker, that is a process miss worth saying out loud rather than inventing a reference.
4. Cross-model second angle (optional)
A cheap extra angle on the diff, and not the safety gate. Step 1 is the gate. The reason for the demotion is in step 1's rationale: the reachable non-Claude models return few findings, and a thin report from a weak reviewer is indistinguishable from a clean diff, so nothing load-bearing may rest on it. It stays in the procedure because the decorrelation is real when it fires and the pass costs little, not because it is what catches the misses.
Dispatch a cross-model adversarial reviewer agent on this PR's diff — on this maintainer's machine that is a user-global agent, so it may simply not exist in a fresh clone. If no such agent is configured, say so and move on; this step is optional by design and must never block a ship. Example dispatch:
Adversarially review the diff for PR #
<pr>in this repo (rungh pr diff <pr>, orgit diff origin/main...HEADfrom the worktree root). Return confirmed issues with file:line and a net recommendation.
The agent runs its own non-Claude backend read-only through its wrapper script, verifies every finding against the actual source (that backend's output is untrusted, so the agent filters out its hallucinations), and returns severity-ranked confirmed issues plus a net recommendation: ship / fix-first / needs-human-call. Feed that into the step 6 merge confirmation:
-
ship → proceed, but read it as "this reviewer found nothing", not as a clean bill of health. It is the weaker of the two reviewers, and a thin report is its normal output whether or not the diff is sound. Step 1's ending, whichever of the two it was, is what "reviewed" rests on.
-
fix-first (confirmed correctness/security issues) → stop and fix them, in this order:
- Re-gate the fixes before pushing. They are ungated code exactly as a review round's are, so run
the re-gate table in
reference/review-phase.mdfor whatever they touched and commit what it regenerates. Read that table's timings relative to the push you are about to make, not to step 2, which is already behind you: aworkers/**fix has to be deployed by hand now, because no workflow deploys the Worker and the table's "before step 2" wording is unreachable from here. Nothing downstream catches any of this.preflight's dirty-tree guard ran before step 2, and a fix undersite/src/lib/**changes what the parser emits intosite/public/api/**whilelint-papers.yml's API sync guard does not fire onsite/src/**, so a stale endpoint would ship with every check green. - Commit and push. This updates the open PR.
- Re-check the floor, because a fix-first fix is exactly the kind that widens the shape: one
touching
site/src/**or a hook turns a prose PR into a 3-round diff. If the re-checked floor is no longer met, run the remaining floor rounds and do not ask, since the stop gate may never fire below the floor. Otherwise the gate fires as usual (the fixes changed the diff, so condition 3 is unmet) and the maintainer may answer "ship now" there and end the run without a further round. - Any fix those rounds produce goes back through items 1 to 3, re-gated, committed, pushed, and the
floor re-checked, before you go anywhere near step 5. Routing it through items 1 and 2 alone is not
enough: those cover the gate and the push, and skipping item 3 leaves a run where condition 3 is unmet
with the floor met, which is one of the states the stop gate must fire in (it fires whenever the floor is
met and condition 2 or condition 3 is unmet; see the gate itself rather than this line), so the procedure would reach
step 5 with fixes no round has read, no "ship now" on the record, and an ending that is neither 1
nor 2 for step 6 to validate. Nothing downstream will catch it if you don't:
preflightandpushare both behind you, andmergechecks only that localHEADmatches the PR head, never that the tree is clean, so an uncommitted fix from a step-4 round merges silently. That is exactly the "PR that looks right and is missing the fix" casereference/ship-pr-reference.mdsays nothing downstream can detect. - Amend the PR body with
gh pr edit, whichever way the floor re-check went. The body was composed back at step 3 of this skill and can no longer be right: a further round can change how the review ended, and a "ship now" here makes this an ending 2 on a PR whose body still claims ending 1. The rounds-against-floor line goes stale here too, since item 3 can add rounds and a fix-first fix can widen the shape, and step 6 checks condition 1 against exactly that line. Step 6 checks ending 2's disclosure against the same body, so leaving it unamended either fails a check that should pass or passes one that should fail. This is the only place the body is written after the PR exists, andship-pr.shhas no subcommand for it, which is why the command is named.
Don't merge over confirmed real findings.
And note the hook property here is the inverse of step 3's.
gh pr editis run by you, at command position, socheck-public-publish.shdoes see it, unlike thegh pr createinsideship-pr.shthat step 3 says nothing will stop. A fix-first is defined here as confirmed correctness or security issues, so an amended body describing them is a likely denial on the security vocabulary or a fenced code block. That is the guard working. Answer its three questions and rewrite the body so it says what changed without describing an unpatched weakness in a live service; reaching for the single-use override because the amend is inconvenient is the one response that is always wrong. - Re-gate the fixes before pushing. They are ungated code exactly as a review round's are, so run
the re-gate table in
-
needs-human-call → surface the report and let the user decide.
When to run: most valuable for code diffs (site/**). For docs-/config-/.claude-only diffs the
code review adds little — either run it in the agent's design/prose mode (it still catches logical
gaps in a procedure doc or prose page) or skip it with a one-line reason. The agent guards against piping
secrets into its backend. If it reports an expired or missing credential (it can't re-auth headlessly),
pass along what it reported and let the operator decide whether to ship without the cross-model pass: an
unavailable optional reviewer must not hard-block the whole ship.
5. Watch the checks
bash .claude/skills/caail-pr-wrapup/ship-pr.sh watch-checks <pr>
Per reference/ci-paths.md, lint-papers runs only when the diff touches content/parser paths, and the
deploy is post-merge, so not every PR has every check. A PR with no checks at all is now a
narrow case: guards.yml fires on .claude/hooks/**, .claude/settings.json, this skill, and any
.github/workflows/** edit. The check-free paths are enumerated once, in reference/ci-paths.md, and deliberately not repeated
here. That list has already been wrong in both directions once each, so a second copy in this file is
the exact hand-typed-fact-beside-another defect the enumeration keeps causing. preflight computes its answer from the
*_PATHS lists in ship-pr.sh, not from the YAML, and check-ci-paths.py is the only thing that
pins those lists to the YAML. It runs in guards.yml, and both files that can desync the
lists, ship-pr.sh and the workflows, are inside that trigger set on pull_request and push. That is
narrower than it sounds and is not a guarantee. check-ci-paths.py builds its workflow set from the
files that have a paths: block, so a PR deleting one drops that workflow out of the comparison, prints
ALL PASS, leaves its *_PATHS list untouched, and merges green while preflight goes on predicting "no
job" for a job that now runs on everything. Nothing asserts the reverse direction, and nothing asserts that
guards.yml's own filter still covers both sides. So drift can also predate the guard or land through a
red or bypassed merge, and preflight states its consequence just as confidently either way.
When a check you expected is missing, open the workflow rather than trusting either the list above or
preflight.
The helper reports "no checks reported" and proceeds when that is genuinely the case. If you expected
a guard to run and it did not, and the diff is not in that list, treat it as a paths-filter gap rather
than expected quiet: that is the exact failure reference/ci-paths.md documents twice over.
If a check fails, stop — surface it and fix the branch; do not merge red.
6. Confirm, then merge
Pause here. Merging triggers the public deploy, so confirm with the user before proceeding (unless
they've already said to merge autonomously this run). Weigh three inputs, in this order of weight:
step 1's review rounds must have genuinely reached an ending rather than merely stopped, and "it ended" is
not evidence, since every run ends somehow. For either ending, condition 1 must have been met: the stop
gate never fires below the floor, so a sub-floor ship is invalid however the body labels it. If the body
claims ending 1, additionally check conditions 2 and 3. Condition 1 is the floor for the diff as it
stands now, so re-derive the shape from the final diff rather than reusing step 0's prediction, which
predates every fix. Do not use conditions 2 and 3 as a label check on an
ending 2: the gate fires when condition 2 or condition 3 is unmet (see the gate itself
rather than this line), so an ending 2 needs only one of them to fail and the other may well hold. Treating both as failing by construction would have you call a
correctly labelled body mislabelled, and refuse the merge over it.
For either ending, the body names every finding that was ticketed or declined along the way, since the
Definitions say both endings disclose all of it and a reader cannot tell an unmentioned finding from an
unnoticed one. For ending 2 additionally, the findings left outstanding are exactly the ones the body
names and no others, and if the last round changed the diff the body also says those fixes went
unreviewed. The exception is the publishing
carve-out in the Definitions in reference/review-phase.md, so a finding it withholds satisfies this check by being
disclosed as a disclosure-private triage without the weakness or the endpoint named, rather than by
being absent. An outstanding finding that appears nowhere at all fails this check like any other; the
carve-out withholds the details, never the fact that something was triaged. Do not "fix" a failing check here by naming it; that publishes the weakness in a PR that
cannot be deleted. CI must be green (step 5); and the step 4 cross-model pass must not have left unresolved confirmed issues
(a "fix-first"). A "ship now" answer is not itself an autonomous-merge waiver: it authorised ending
the review, not merging unasked, so it alone never substitutes for the confirmation above, though a waiver
the user granted separately still stands. A green CI plus a thin cross-model report is not a substitute
for the first of those:
neither of them reads the diff the way step 1 does. Then:
bash .claude/skills/caail-pr-wrapup/ship-pr.sh merge <pr>
This merges with a merge commit, deletes the remote branch, tolerates the benign "main already checked
out" gotcha (see reference/gotchas.md), verifies the PR is actually MERGED, and prints the merge
commit SHA you
need for step 7.
7. Watch the deploy
bash .claude/skills/caail-pr-wrapup/ship-pr.sh watch-deploy <merge-sha>
This finds the docs.yml run for that SHA and blocks until it finishes. Green = build + Lighthouse gate +
Deploy to Pages all passed, and that is a successful ship. If preflight predicted no deploy (the diff
touched no deploy paths), the helper says so and returns cleanly.
Which Lighthouse categories block is deliberately not written here. CLAUDE.md carries that sentence,
site/scripts/lighthouse-gate.ts generates it from site/lighthouserc.json, and a test asserts it
verbatim, so it cannot drift; a second hand-typed copy can, and this one did, calling performance blocking
when it has been warn-level since e627e97 and scoping it to the landing page when the assertion matches
every collected URL. Read CLAUDE.md's sentence, or run the generator. If a blocking category fails,
stop: read the lhci report and fix the regression rather than re-running and hoping.
8. Verify live
bash .claude/skills/caail-pr-wrapup/ship-pr.sh verify-live <route> [<route> ...] # '' = homepage
Use the routes preflight suggested. Beyond the 200 check the helper does, add a content assertion for
what you changed — e.g. curl -s <url> | grep for a new heading, the corrected link target, or the
absence of a stale ./X.md link — so you confirm the content shipped, not just that the page exists.
9. Clean up
Check where you are first — git branch --show-current. If step 6 took gh's succeeding path it
already moved you to main and deleted the branch, so the first two items below are done and
re-running them reports confusing errors rather than doing anything (see reference/gotchas.md).
- Fast-forward local
mainso the primary checkout matches the deploy:git fetch origin main && git merge --ff-only origin/main(run it in the primary checkout; you can't fast-forwardmainwhile a worktree holds another branch). Whenmainisn't checked out anywhere,git fetch origin main:mainupdates the ref without switching to it. - This session's managed worktree (made by
EnterWorktree): callExitWorktreewithaction: "remove"anddiscard_changes: true— safe because the commits are now onorigin/main. It removes the worktree + branch and returns the session to the primary checkout. - A plain branch (no worktree):
git branch -d <branch>(use-Dif it was squashed/cherry-picked so the SHA differs from what landed). - A stale/superseded worktree someone made by hand (e.g. a predecessor of this work): confirm
with the user first, then
git worktree remove <path>andgit branch -D <branch>. - Stop every background preview server this work started, on whatever port it took. Naming one port
is not enough:
pnpm previewis given a free port per run, so real leaks sit on 4370, 4399, 4402-4406 and anywhere else, and one worktree was found holding five at once, days old, one per e2e attempt.pgrep -f 'astro.mjs (preview|dev)'finds them regardless of port. Matchdevas well aspreview: a leakedastro devis what the bogus lhci score inreference/gotchas.mdis usually blamed on, and a pattern that only sayspreviewreports a clean machine while one is still holding the port.psthe PIDs to see which worktree each belongs to before killing anything, since a peer session may have one mid-run.
10. Close the loop on the trackers
Do this last, after step 8 verified the live site — not at merge time. CAAIL only counts as shipped
once the deploy is green, so a ticket moved to Done at merge is a lie whenever Lighthouse fails
afterwards. Both trackers, in this order:
- GitHub. The
Closes #Nline from step 3 already closed the issue on merge; confirm rather than assume, since a typo'd or missing line fails silently:
If it's stillgh issue view <N> --json number,state,stateReasonOPEN, close it now with a comment naming the merge SHA —gh issue close <N> --comment "Shipped in <sha>, live at <url>". - Jira.
CAAILis the durable record and nothing else transitions it, so this step is the only thing standing between the board and a permanent backlog of finished work. The Rovo MCP was removed 2026-08-18, so this goes throughacli, which needs no cloud id:acli jira workitem transition --key CAAIL-nn --status "Done" --yes, thenacli jira workitem comment create --key CAAIL-nn --body "Shipped in <sha>, live at <url>"— so the ticket records where the work landed. If the ticket is aTaskunder aWorkstream, check whether it was the last open child; a Workstream whose children are allDoneshould be transitioned too.
Both calls happen after the irreversible part of the ship, so a failure here is bookkeeping, not a broken deploy: report exactly which tracker is out of date and let the user fix it, rather than retrying in a loop or unwinding anything. If the branch had no ticket on either tracker, say so plainly here — that is the "Jira first, always" rule having been missed at the start of the work, and it is worth naming so the next piece of work doesn't repeat it.
Reference files
One level deep, read when the step that names them says to:
reference/review-phase.md— step 1 in full: the definitions, the floor, the stop rule, both gates, the re-gate matrix, and the evidence for multiple rounds.reference/ci-paths.md— which workflow fires on which paths, for steps 5 and 7.reference/gotchas.md— symptom to meaning, read when something looks wrong.reference/ship-pr-reference.md— the subcommand table and what is actually enforced in code.