Orchestrate release preparation. Reconcile the changelog, code, and docs to the net release state, then commit, push, and babysit CI until the release PR is built and `main` is green. Stop before the merge. Review-gated in normal use, fully autonomous under `--dangerously-skip-permissions`.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Orchestrate release preparation. Reconcile the changelog, code, and docs to the net release state, then commit, push, and babysit CI until the release PR is built and `main` is green. Stop before the merge. Review-gated in normal use, fully autonomous under `--dangerously-skip-permissions`.
compatibility
Designed for Claude Code. Recommended model: Opus.
You drive a release from a working tree to a ready-to-merge release PR: reconcile the tree to its net state since the last tag, validate it locally, commit and push, then babysit CI until the auto-generated release PR is green. You stop there: the human marks the draft release PR ready for review and performs the final "Rebase and merge".
The release is push-driven: the prepare-release job in changelog.yaml runs repomatic prepare-release on push to main to build the freeze and unfreeze commits and open the release PR. : a local run previews a freeze that must not be committed (it marks the changelog "released", and on the canonical repo rewrites every workflow action ref). Your job is to make clean enough that the auto-generated release PR is correct, then keep green.
Do not run prepare-release yourself
main
main
How this skill runs
The review gate is the permission system, not a behavioral stop. Normal runs prompt on each git commit, git push, and subagent write; step 4 shows the consolidated changelog diff before the first commit prompt, so approving that commit is the review gate and denying it stops the run. --dangerously-skip-permissions mutes the prompts so the full sequence runs autonomously; the skill cannot detect the mode and does not need to.
Invocation method. When the context shows CANONICAL_REPO, use uv run repomatic. Otherwise use uvx --exclude-newer '1 week' --exclude-newer-package repomatic=P0D -- repomatic, which applies the supply-chain cooldown to repomatic's dependency tree while keeping a fresh release installable (see claude.md § Cooldown on every install). References to <cmd> below resolve to one or the other.
You hold no Edit/Write of your own: the changelog skill and the spawned agents do the editing.
Sub-agent rules
The sweep agents (step 1) and the babysitter (step 6) all follow these rules. Restate them in every spawn prompt: a spawned agent only sees what the prompt carries.
Commit attribution. Every commit this skill or any spawned agent makes carries a Co-Authored-By: Claude <noreply@anthropic.com> trailer by default, so unattended changes stay traceable, and that default holds even where a downstream CLAUDE.md has not synced repomatic's claude.md § Agent behavior policy. It is a default, not an absolute: a maintainer's explicit standing rule against AI attribution outranks it, because the trailer lands in their repository's permanent history and that call is theirs. Check for such a rule before the first commit, not after the push: stripping a trailer from an already-pushed commit needs a force-push, which is off-limits, so the anomaly is then permanent. When an exemption applies, write it into the spawn prompt of every agent that may commit. An agent sees only what its prompt carries, so a withdrawal issued mid-run can arrive after it has already committed.
Reports are sent, not written. A background agent's end-of-turn text is never delivered, so every spawn prompt must require the final report as a SendMessage to the spawning (main) session, naming that recipient explicitly: a bare label like "orchestrator" may not resolve to an address, leaving the agent to guess where to route the report. A "return a report" instruction alone yields a silent idle even when the report was composed. On an idle notification without a report, chase once; the tree (git diff) stays the authoritative record either way.
Expand <cmd> before it reaches a spawn prompt.<cmd> is this document's placeholder, not a shell command, and a prompt that carries it verbatim (or half-expanded) hands the agent something that cannot run. The trap is run <tool>: the tool registry supplies most of them, so dropping the repomatic run prefix does not merely drift from the pinned version, it fails outright — a 7.8.0 spawn brief that expanded <cmd> run mypy -- into uv run --frozen -- mypy -- died on Failed to spawn: mypy, since mypy is not a project dependency. Write the invocation out in full (uv run repomatic run mypy --) and let the agent report back if it does not resolve.
Trust the tree, not the report. A mid-run message to a busy agent is delivery without receipt: it can land after the agent composed its final report and be silently dropped. After tasking a running agent, confirm the tree reflects the request (git diff the target file) before moving on. Read fresh every time rather than reusing an earlier capture — an edit can land in the gap between two checks, and a stale capture then reports a live fix as still missing. Grep for the absence of the old text (grep -c '<old phrase>' <file> returning 0) instead of eyeballing a diff: an empty match is unambiguous where a diff read mid-scroll is not.
Never revert the shared working tree. The agents share one tree, so disjoint lanes do not make them race-free: no agent may run a working-tree-reverting git command (checkout, restore, stash, reset, clean), which silently discards the other agents' uncommitted edits. For full isolation instead, spawn with isolation: "worktree" and merge on join: disjoint files won't conflict.
Fix commits stage narrowly (git commit <path>, never -a). Uncommitted files an agent did not create are the maintainer's in-progress work: never revert them and never sweep them into a commit.
Degrade gracefully. A cross-referenced skill excluded from this repo is a fallback path, not a blocker: apply its principle via an Agent or inline. When an Agent spawn itself fails (a terminal API error), do the work in the main thread.
1. Reconciliation sweep
A release materializes the net state since the last tag, not the path taken to reach it: after a long cycle, the changelog, code, and docs all drift toward describing the journey. Reconcile all three against git diff v<last>..HEAD. Order matters: the changelog describes the net change, so reconcile the substance first (code and docs in parallel), then summarize it (changelog). A change introduced and then reverted before release is a no-op for users: no changelog entry, no scaffolding, no docs mention.
Before spawning, capture the unstaged diff (git diff against HEAD): those lines are the maintainer's in-progress drafts, not cycle work. Pass both diffs to each agent with the rule: preserve every line present only in the unstaged set (a curated TODO, a scratch note in a docstring) unless the maintainer explicitly asked for cleanup. Without the guard an agent strips unstaged scratch as "cycle scaffolding" and the draft silently vanishes. When in doubt, leave it.
Capture the job-level red inventory in the same breath: the latest conclusive run of each monitored workflow on main (gh run list --workflow tests.yaml --branch main --json databaseId,conclusion,headSha, then gh run view <id> --json jobs), listing every job at conclusion == "failure", ⁉️ probes included: continue-on-error folds their crashes into a green run-level conclusion, so no run-level read ever surfaces them. Under step 6's genuinely-green goal those reds are release work, they are visible now from history, and every one fixed before the first push saves a 40-90-minute babysit round-trip: seed the code agent's brief with the list.
Scan the open autofix PRs in the same breath, and read their diffs rather than their titles. An unattended fix-*/format-*/sync-* PR is a pending write to main that no one reviewed, and merging one mid-release silently reverts committed work: gh pr list --state open --json number,headRefName,title then gh pr diff <n> on every automated branch. What you are looking for is a false positive, a "fix" that is wrong in this repository and that the job will keep re-proposing until the underlying rule is taught otherwise. The tell is a diff that undoes something a human deliberately wrote. Fix it at the rule, not the file: add the word to [tool.typos] default.extend-words, the path to the linter's ignore list, the pattern to extend-ignore-re. Reverting the file alone guarantees the same PR returns on the next run. The archetype: fix-typos rewrote a 10b-quater check label to 10b-quarter, breaking the Latin ordinal series (bis, ter, quater) numbering a run of sibling checks; the maintainer reverted it by hand, the job re-proposed it, and the second PR merged during a release run, undoing the revert.
Judge that diff against current main, not against the PR's head. An open PR is pinned to the commit it branched from, so its diff can faithfully describe a file the maintainer has since fixed. Rewording is the third option beside allowlisting and reverting, and it is the one that leaves nothing behind: dropping the trigger word entirely also stops the job re-proposing, so a rule landed on top of it allowlists a word that now appears nowhere — dead config that reads as a live exception and quietly contradicts the fix the maintainer chose. So git grep the trigger before writing a rule for it, and when they have already solved it their way, leave it alone. Closing or merging the PR is theirs to decide: surface it, land the rule-level fix only while the trigger is still live in the tree, and say either way in the step-7 report.
Always read that run's headSha, and treat a red as live only if nothing since it could have fixed it. This is why the query above asks for headSha and not just conclusion. Most monitored workflows carry a paths: filter, so a commit that touches nothing in the filter triggers no run — and if that commit is the one that fixed the red, the latest conclusive run stays pinned to the superseded parent and keeps reporting a failure that no longer exists. It can sit there for hours looking like live release work. The failure mode is not hypothetical and it is expensive: it sends the code agent chasing a fixed bug, and it makes the whole matrix look broken. Before adding a red to the brief, run git log --oneline <run headSha>..HEAD and check whether an intervening commit touched the failing area; when the answer is unclear, settle it for the cost of one dispatch (gh workflow run <workflow> --ref main, no commit, no PR churn) rather than reasoning about it. The archetype: a docs-only commit regenerated a checked-in generated block, fixing the test that asserts it is in sync, but tests.yaml's paths: filter meant no Tests run ever observed the fix.
A green conclusive run proves nothing when supersession cancelled every run between it and HEAD. The rule above guards the direction where history over-reports a red; this is the direction where it under-reports, and it is the more expensive one, since a false red costs a wasted round-trip while a false green ships the break. Walking back past cancelled runs to reach the newest conclusive one skips exactly the commits a busy cycle pushed most recently, so that success can predate every line the cycle added. Before recording an empty red inventory, diff the gap: git log --oneline <newest success headSha>..HEAD. When cycle commits sit inside it, the workflow has never run on them and the green is stale by construction. Settle it with a dispatch (gh workflow run <workflow> --ref main, no commit, no PR churn) or by waiting out HEAD's own run, never by recording "no reds". The archetype: a feature commit interpolated a metadata value straight into a run: block, the two pushes behind it cancelled its Lint run before it ever dequeued, and the newest conclusive Lint run sat back on the post-release bump, green, while main was already failing 🔒 Lint workflow security.
The three substance passes own disjoint lanes (code owns Python including docstrings, docs owns prose under docs/ and readme.md, bundled assets owns .claude/), so spawn them as three Agent calls in a single tool-call block: sequential spawns waste the wall-clock of whichever finishes first.
Code: an Agent that reviews every file changed since the last tag for reuse, quality, simplification, and deduplication, and fixes what it finds (CLAUDE.md § Common maintenance pitfalls, "Simplify before adding"). Two layers: first strip scaffolding from reverted or superseded work within the cycle diff (abandoned workarounds, dead branches, WIP notes that never shipped); then harmonize what remains (collapse duplication, lift repeated literals to their canonical source, align new code with module patterns). Its constraints:
Every edit stays behavior-preserving: step 2 is the safety net, a failing test vetoes.
Type checks use the CI-equivalent <cmd> run mypy (pinned version and --python-version), never a bare mypy whose newer interpreter raises false positives CI never sees. Pass it no arguments, exactly as step 2's Types gate does: the runner then resolves the same tracked-*.py list CI's lint job checks, tests/ and docs/conf.py included, and the two cannot diverge. A downstream CLAUDE.md "type checking" command is often the narrow dev-loop form scoped to the package only; do not inherit that scope when prompting the code agent, since a package-only run stays green on a tests/ or docs/ type error that reddens Lint post-push.
Failures the pass believes pre-existing get reported, not silently scoped out: that verdict belongs to step 2's CI check.
Adopting features from upgraded dependencies stays in /repomatic-deps modernize.
On the canonical repo, workflow invocations reading uv --no-progress run --frozen -- repomatic are the intended unfrozen state (the freeze commit rewrites them to a uvx 'repomatic=={version}' PyPI pin at release): never flag the local form as a pin regression or downstream breakage. The invariant to check instead is that every uv-invoking job provisions setup-uv in its own steps. Do not "restore" an isolated uvx --from . here: the lockfile path is deliberate, since an index resolution can be made unsatisfiable by the install cooldown while a lockfile cannot.
Docstring rendering belongs to this pass: build the docs and fix any broken cross-reference role a docstring introduced (the docs pass can surface but not fix them). Build only into the gitignored docs/_build, never an ad-hoc path: a stray build tree pollutes git status and trips tool scans like run typos.
Shortening an over-long workflow line to satisfy yamllint's 120-char limit must not lift hashFiles(...) (or any runner.*) into a workflow-level env: var: that context exposes only github/secrets/inputs/vars, so the expression resolves to empty at run-init before checkout and silently breaks the value — a cache key: shortened this way ships a broken key to every downstream repo. Shorten the literal itself instead (trim a shared key prefix, say).
The red inventory from the pre-spawn capture is part of its brief: root-cause each repo-fixable red (a chronic platform break, a flaky live-registry install, a crashing ⁉️ probe) and fix it at the source per step 6's taxonomy, even when the failing file saw no change this cycle.
Docs: an Agent that verifies prose docs against current behavior, not the journey (version references, CLI output, removed or renamed features go stale every cycle). Its constraints:
Manually-maintained version examples (install commands, binary download URLs, uses: refs) track the latest released tag, never the version being prepared, because the docs site deploys on every push to main. "Manually-maintained" is the load-bearing word and it is not a synonym for "under docs/": the freeze reaches into that tree in at least one repo (the canonical one rewrites docs/install.md), so decide file by file from the freeze's actual scope per the next bullet, never from the path. The tracking runs both ways: advance a sample that lags the released tag (still at N-1 after release N published) up to it, applying the bump directly rather than deferring it as a version advisory; only bumping a sample forward to the not-yet-released version is off-limits. A stale sample hides in plain sight, so grep every version string in docs/ and readme.md rather than trusting a sub-agent's list.
What the freeze rewrites varies by repo (the canonical repo pins workflow refs and CLI invocations; a downstream freeze may touch only changelog.md, citation.cff, __init__.py, and pyproject.toml). Read the last freeze commit's actual diff (git show <last-freeze-sha>, never just --stat) and treat every version sample it does not rewrite, readme.md quick-start output included, as hand-maintained tracking the released tag: samples presumed freeze-managed have shipped stale through a release. Freeze-management is per line, not per file, and a file list is exactly the wrong granularity to decide it: a file the freeze touches can still carry hand-maintained samples it never rewrites, so --stat clears the whole file and the stale ones survive. The archetype: docs/install.md sits in the freeze commit, which rewrites precisely one line of it (the uvx <cli>@X.Y.Z pin), while the >>> <pkg>.__version__ REPL capture further down the same page is hand-maintained and had been a full release behind since the previous cycle. Settle each sample individually with git log -S '<the literal string>' -- <file>, which shows whether a freeze commit or a human last moved it. On a cycle that migrated the release tooling itself, the historical freeze under-predicts the new one (a pre-repomatic freeze touching only says nothing about the repomatic freeze, which also rewrites , , and ): treat every version sample as hand-maintained until the regenerated release PR's diff shows the new freeze's actual scope.
Bundled assets: an Agent (use qa-engineer, the gatekeeper claude.md names for agent and skill definitions) that checks .claude/skills/** and .claude/agents/** against what the cycle actually changed. These files deploy verbatim to every downstream repo through repomatic init, so a claim this cycle falsified ships as confidently as it did when it was true, and neither substance lane owns them: the code pass sees no Python and the docs pass is scoped to docs/. Its constraints:
Scope it to claims the cycle invalidated, not a general review. Grep the cycle diff for the behaviors these files describe (cadences, gating conditions, job names, default values, config keys) and verify each surviving statement against its source, rather than reading for style.
Verify against the workflow or module, never against the summary in the brief. The point of the pass is that prose drifted from code; a second-hand description is the same failure one level up.
Fix the claim, do not rewrite the surrounding strategy: a stale fact inside good advice is a fact bug.
A pre-existing error in the same class, found while checking, is in scope — these ship downstream too, and nothing else audits them. Report it separately from the cycle-caused ones so the maintainer can tell which the release introduced.
Cross-references must degrade gracefully; skills stay self-contained (no upstream-only docs/ URLs or paths), since downstream repos have neither.
The archetype: 7.9.0 narrowed ordinary pushes to a canary binary subset, and two babysit-ci claims plus a repomatic-deps cadence line kept describing the old behavior. Nothing in the code or docs lanes would have caught them.
Changelog: once the three passes settle, invoke /repomatic-changelog consolidate through the Skill tool, so the consolidated entries (and the version advisory reading them) reflect the reconciled tree, renames included. It collapses superseded values and drops intra-cycle reverts. Consolidation assumes the entries already exist, though: when the unreleased section under-represents the net cycle (a maintainer left one stub bullet for a multi-feature cycle), run add first, or the bare /repomatic-changelog default that runs add then consolidate, so the shipped changes are drafted before they are collapsed. When both add and consolidation legitimately find nothing user-facing (a purely mechanical cycle), consolidation now backfills one generic maintenance bullet rather than leaving the section empty (/repomatic-changelog consolidation rule 08): a published release heading with no bullets reads as broken. If the skill is excluded here, degrade gracefully (sub-agent rules).
If the sweep made no edits
A clean cycle, where every change since the last tag is already at its net end-state, is a normal outcome. With no working-tree edits, the commit-and-push spine collapses and three steps change shape:
Step 2 becomes redundant: CI already ran on this exact commit (it is HEAD of main), so verify that run's conclusion (gh run list --branch main) instead of paying for a fresh gate. Still quick-run the time-dependent external smoke checks (<cmd> run typos, <cmd> audit --fix): re-published binaries and new CVEs drift independently of code.
Step 5 is a no-op: never force an empty commit.
Step 6 reduces to verifying the existing run. When gh pr list --head prepare-release shows a PR whose freeze commit sits on the current HEAD, confirm every stable job on HEAD is green and go to step 7, spawning /babysit-ci only on a real failure. When no current PR exists (the last push missed changelog.yaml's paths: filter), trigger one with gh workflow run changelog.yaml --ref main, still with no commit.
Steps 3, 4, and 7 are unchanged: the version advisory and the (empty) changelog diff still inform the maintainer.
If the sweep touched only prose
When the sweep's edits are confined to prose and Markdown (docs/, readme.md, changelog.md, .claude/; no .py, no pyproject.toml, no uv.lock), the full step-2 gate is disproportionate: tests, mypy, ruff, the binary self-test, and fresh resolution have no new surface to check. Narrow to what step 1's docs and bundled-asset passes do not already own: <cmd> run mdformat --verify -- <file> over the changed Markdown, plus <cmd> lint-changelog when changelog.md changed. Run it in the same position as the full gate — before the step-5 commit and push, never after. A lighter gate is still a pre-push gate: verifying format only once the push is already out defeats the point.
2. Validate locally (pre-push gate)
When the sweep rewrote code, prove it green before paying for a CI round-trip (no edits: see above). This is the same fast local channel /babysit-ci polls, run ahead of the first push. Launch the slow checks (tests, types, changelog lint) in parallel in the background, act on the fastest failure first (mypy and ruff in seconds, pytest in minutes), fix in the working tree, re-run only what failed, and iterate until every check is green. A check earns a blocking seat only while it reports faster than CI would surface the same failure: the push is what starts the 40-90-minute matrices, so holding it for a check CI's fast platforms reproduce at the same latency delays the release without adding earliness.
First read CI's conclusions on HEAD (gh run list --branch main): every red job there is cycle work this release must fix, and no "pre-existing failure" claim from the sweep is valid until checked against it. Read at the job level (gh run view <run> --json jobs), never the run level: continue-on-error hides a crashed ⁉️ probe inside a green run conclusion, and this read doubles as the check that the step-1 red-inventory fixes actually landed. An in-cycle lockfile bump can invalidate type: ignore comments and override signatures with zero source changes, so "the source did not change" never proves "the check still passes" (a dependency re-lock once widened a parent method, and CI Lint was red with exactly the 7 mypy errors the sweep had rationalized as pre-existing). When HEAD's own runs are still queued/in_progress (rapid pin/lock auto-commits plus hosted-runner backlog routinely leave them unfinished here), HEAD has no conclusions to read: read instead the latest conclusive run of each monitored workflow on an ancestor (gh run list --workflow tests.yaml --branch main --json conclusion,headSha,createdAt, skipping the cancelled/skipped supersession noise a busy cycle piles up). A failure there is a pre-existing red carried on main for several commits: fix it before the first push. A success there is not the mirror verdict, though: read it against step 1's rule on a stale green, since the supersession noise you just skipped is where the cycle's newest commits were tested. Miss it and a platform-gated failure the single-OS local gate cannot run surfaces only in step 6 (babysit), still fixed but at the cost of an extra CI round-trip.
The checks:
Tests: uv run pytest --no-header -q. Exception: an integration-heavy suite driving real external tooling can outrun a local background timeout and need tools not installed locally, so it is not a fast gate. Skip it, keep the rest of the gate, and treat the CI matrix on the exact commit as the authoritative test signal (step 6 covers dispatching one). Between the extremes, a suite whose local runtime approaches CI's fast platforms (~5-8 minutes from push) stops blocking. Start it with the gate, push once every fast check is green, and fold the still-running suite into step 6 as the first babysit channel: a failure lands as an immediate tight-loop fix at the same absolute time CI would have reported it, while a pass cost zero wall-clock.
Types: <cmd> run mypy, with no arguments. The runner resolves the same tracked-file list CI's lint job checks, docs/conf.py included. Do not pass directory names instead (<cmd> run mypy -- repomatic tests docs): directories change module resolution enough that mypy follows an installed dependency's own source, so a package pulled in by the docs group and written for a newer Python fails the run under --python-version 3.10 with a syntax error in a file this project does not own, which reads as a real failure and is not one.
Changelog: <cmd> lint-changelog. A ⚠ X.Y.Z: not found on PyPI warning for the still-unreleased version is expected and not a blocker.
Shippable deps: <cmd> lint-deps. Offline and instant, and it covers the one release failure nothing else in this gate can see: a [tool.uv.sources] override never reaches the published metadata, so tests, types, formatting and the build all pass on a tree whose wheel every user then fails to install. Run it even on a docs-only cycle, where a lockfile bump can still have moved a source. A blocker naming a git source paired with a .dev floor is the sync-dep-sources idiom mid-flight: the fix is to wait for that swap PR, not to edit pyproject.toml by hand. The release lane carries the same check as a hard gate, but it fires after the freeze commit is already on main, where the only recovery is to burn the version and ship the next one, so a red here is cheap and a red there is not.
Formatting, reproduced with the pinned tools, never the dev-env uv run ruff (a newer local ruff once silently disagreed on a PERF401 fix): git diff --name-only HEAD -- '*.py' | xargs <cmd> run autopep8 -- over the cycle's changed Python files (it wraps long-line comments ruff leaves), never as a shell variable holding the space-separated list, which the runner takes as one literal path and rejects with [Errno 2] No such file or directory: 'a.py b.py c.py '. Then <cmd> run ruff -- check and <cmd> run ruff -- format, and read git diff. Both write in place, but check only does so because the resolved ruff config sets fix = true — the runner injects no flag of its own, so a repo carrying a [tool.ruff] section without that key gets a read-only check and an empty diff that means nothing. An empty diff past your reconciliation edits is green; fold a legitimate style fix into the reconciliation. For any Markdown the reconciliation touched (changelog.md, docs/), ask <cmd> run mdformat --verify -- <file>, which reports what the write path would change without touching the tree; never a bare mdformat/mdformat --with mdformat-myst, whose plugin set rewrites MyST directive colon-options (a {list-table}'s :header-rows:/:widths:) to --- frontmatter form, diverging from CI's autofix.
Landmine: autopep8 relocates a trailing # type: ignore[...] off a >88-char line onto its own line, voiding the suppression (Lint red under warn_unused_ignores); ruff format usually reverts the relocation, so only wraps that survive the full pinned sequence are real formatting debt. Never commit the relocation: fix the line length at the source so the comment rides the opening line.
Workflow YAML, whenever the cycle touched .github/workflows/: <cmd> run actionlint --, <cmd> run zizmor -- ., and <cmd> run yamllint -- over every workflow. Nothing else in this gate reads workflow YAML, yet step 1's code pass is told to edit it, so a regression the sweep itself introduces stays invisible until CI — and on this repo a bundled workflow carries it to every downstream repo, which gets the workflow without the conformance tests guarding it here. The class worth knowing, because a reviewer's eye slides straight past it: GitHub Actions evaluates an if: as an expression only when the value is entirely one ${{ … }}. A folded block scalar (if: >) wrapped around one appends a trailing newline, so the value interpolates to a non-empty string and the step is truthy forever, gating nothing. Write a multi-line condition bare, with no wrapper (a leading ! rules out the bare single-line form, since YAML reads it as a tag indicator). Eleven autofix.yaml steps stopped gating this way and sat red on main across two commits before a release run caught them.
Autofix externals: smoke-run <cmd> run typos, every formatter that downloads a checksum-pinned binary (<cmd> run biome and peers), and the vulnerable-deps scan <cmd> audit --fix (parses live uv audit output). An upstream re-publish flips a pinned SHA-256 and kills the step; pytest mocks these, so the drift (or a changed output schema) surfaces only here or in CI's autofix run. A pin living upstream in repomatic breaks every downstream repo and cannot be patched here: surface it for the step-8 upstream report. Invocation rules:
Run each tool bare. A bare <cmd> run <tool> now builds the invocation CI performs, resolving the tool's own file list, so it is both the correct smoke test and the one that cannot be got wrong. Passing . instead is what to avoid: biome check --write . and shfmt -- . resemble what autofix.yaml runs and are not it, and biome ships no .gitignore awareness by default, so one . once walked a docs/_build tree into 79,553 reported errors while retabbing two tracked files no CI job formats.
A tool the repo has no files for is skipped by the runner rather than invoked pathless, so a bare run is safe even where an xargs pipe was not. To check a pin alone without running the tool over anything, <cmd> run <tool> -- --version still downloads and checksum-verifies the binary.
Not every non-zero smoke is drift, and only drift is worth reporting upstream. Three failure shapes are environmental and block nothing: No binary for <platform> <arch>. Available: … (an UnsupportedPlatformError) means the pinned release ships no build for the dev machine's platform, which says nothing about CI's Linux runners — and it is the interpreter's arch that decides, so an Apple-silicon host running an x86-64 interpreter under Rosetta resolves to macOS x86-64 and fails on a tool that ships only macOS ARM64, where the native interpreter would have succeeded; Truncated download for <url>: got N of M bytes is a short body from a proxy or a sandboxed network, not a bad artifact (the runner raises this deliberately so it cannot be mistaken for a bad digest, so trust the wording); and PermissionError: [Errno 1] Operation not permitted under the binary cache dir is a sandbox denial. Retry the last two with the sandbox off before concluding anything. Real drift reads SHA-256 mismatch for <url>: expected …, got …: that one alone breaks every downstream repo and belongs in the step-8 report.
Binary self-test plan, against the source build: uv run -- click-extra test-suite --command <source-entrypoint> --jobs max, the same engine tests.yaml (--command) and release.yaml (--binary) drive. It catches the two failures otherwise hidden until the ~90-minute matrix: a case assertion drifted from current CLI output (colors stripped by the piped harness, a moved string), and a plan that cannot load under the binary runner's stdlib-only base deps (a YAML or json5 plan raises "format support disabled" and silently falls back to a trivial suite: keep the plan TOML or JSON, and confirm a non-TOML plan parses under stdlib tomllib, since the full-venv source run has the format extras and hides the gap). It cannot catch a case whose expected output depends on the target platform's identity, though: the source run is single-OS, so an assertion keyed on the release runner's distro (a manylinux container correctly detected as its RHEL/AlmaLinux base) or a manager present only there passes locally and still reds the binary matrix. A cycle that bumps a platform-detection dependency (extra-platforms) can silently shift such attributions — expect test-plan breakage in the binary matrix and check it there, not just in the source self-test.
Fresh resolution: uvx --no-progress --from . <cmd-bare> --version. A fresh isolated env resolves [project.dependencies] from scratch, surfacing transitive conflicts the already-synced venv hides; CI's 🧬 Project metadata job runs exactly this on every workflow, and end users installing via uvx hit the same resolution, so a failure is release-blocking. Fix at the dependency level (drop, swap, or wait on upstream), never with environment-scoped overrides: uvx --from . does not read [tool.uv] override-dependencies.
A cooldown filter is not a resolution failure — check for it before declaring one.uvx reads the user-level~/.config/uv/uv.toml, which on a maintainer's machine typically carries a supply-chain exclude-newer (repomatic recommends one). A release that raises a dependency floor to a version published inside that window then fails to resolve on the maintainer's machine only, for a reason no end user shares and no dependency conflict caused. The tell is in uv's own output: a hint: <pkg> was filtered by exclude-newer to only include packages uploaded before <date>. The latest version satisfying the requirement is vX, published at <later date> — that is environment, not a conflict. Re-run with the cutoff lifted (uvx --no-progress --exclude-newer 2100-01-01 --from . <cmd-bare> --version) and judge the gate on that result. Lifting it per-package (--exclude-newer-package <pkg>=0d) just surfaces the next floor in the chain, one round-trip at a time, so lift the cutoff wholesale. Note this is self-clearing: the same command passes untouched once the dependency ages past the window. The build prints a uv-build ... missing an upper bound warning here: it is expected and intentional, not a resolution failure. repomatic deliberately leaves uv-build uncapped across managed repos (rationale in repomatic's own pyproject.toml[build-system] comment: pure-Python projects with no packaging edge cases, and a hard ceiling risks a chicken-and-egg deadlock when a project's uv moves ahead of repomatic's next release), so never flag it or add a cap.
The local gate is single-OS, so platform-specific failures surface only in CI. Shrink that window pre-push:
The usual culprits: path resolution (Path.resolve() canonicalizes Windows 8.3 names and POSIX symlinks), home-directory expansion, env-var casing, filesystem case-sensitivity, text-I/O encoding (Windows defaults to cp1252, so a bare open()/read_text()/write_text() breaks on the first non-ASCII character, and only in Windows CI: pass encoding="utf-8", and when the cycle touched file I/O, run the suite once with PYTHONWARNDEFAULTENCODING=1 to surface calls ruff's inference-limited PLW1514 cannot see), and direct execution of a generated script (Windows honors neither the executable bit nor the shebang, dispatching on file extension, so a chmod +x'd shebang script a test runs by bare path fails with WinError 193: emit a .cmd launcher beside a .py sidecar on Windows, or invoke the interpreter explicitly).
The structural fix is to mirror the production transformation, not reconstruct it: a test asserting on a derived value should run the same pipeline the code runs, so the expectation matches by construction on every platform. Where expectations must diverge by platform, the CI matrix is authoritative: read every cell, not just your OS.
Name what the gate cannot run: grep the cycle's changed test files for pytestmarks that exclude the local platform (unless_*, skip_*, skipif) and diff-review those tests' expectations by hand, since a green local run says nothing about them. Extend the review to the inputs those tests consume, not just the test files: new docs prose or docstrings can redden a platform-gated conformance test whose skip list never met that reference class (a reworded docstring a Sphinx test asserts on, a first-ever stdlib cross-reference missing from a skip list). The cycle's earlier pushes already ran those tests in CI, which is why the read of CI's conclusions on HEAD above is what actually catches them pre-push.
Reproducing a platform-specific failure churns the shared venv, and the wrong re-sync then reddens the rest of the gate with artifacts. Confirming a free-threaded or version-specific break with uv run --python <other> (e.g. 3.14t for a free-threading race) recreates and repoints .venv to that interpreter. Restore it with uv sync --frozen --all-extras --group test --group typing: test mirrors tests.yaml, and typing — stubs-only, so it cannot perturb a test at runtime — is what keeps the gate's own <cmd> run mypy honest, since a venv synced without it floods import-untyped/import-not-found in files the cycle never touched (even pytest reads as import-not-found), a false red the restore itself manufactures. Never --all-groups: it additionally pulls in the docs group whose imports perturb process-global-state-dependent tests (logging config, default theme) into spurious failures, while a default-only uv sync strips both needed groups (no pytest at all). All of these are venv-provisioning artifacts, not code regressions: re-sync to the CI-matching groups before re-running the gate, and trust CI's lint.yaml/tests.yaml over a local gate re-run against a churned venv (lint.yaml on the prior HEAD reporting exactly the real error set, and none of the stub noise, is the authoritative mypy signal).
3. Version advisory (never bumps, never blocks)
Read the consolidated unreleased section and classify the bump the net diff implies:
A **Breaking:** entry, or any removed or renamed public API: major.
A new feature, command, or config key: minor.
Only fixes, dependency bumps, and internal changes: patch.
State the classification and the single strongest reason, then keep going on the patch default (the unfreeze commit bumps the patch automatically). Do not merge a version-increment PR, and do not stop: for minor or major, surface an advisory ("this release looks like a minor: merge the minor-version-increment PR if you want that bump") and proceed. The maintainer merges that PR out of band, which re-triggers the release PR on its own.
4. Present the sweep
Show git diff of changelog.md plus a one-line summary of the code and docs changes the agents made. Consolidation drops and merges entries: surfacing this is what lets you catch an over-eager drop at the commit prompt before it ships.
5. Commit and push
Commit the reconciled tree with a message describing the net reconciliation (plus the attribution trailer), then push to main: the push regenerates the release PR through prepare-release.
Keep that message short, per claude.md § Commit messages: an imperative subject, and at most one paragraph of body. A reconciliation sweep touches many files and invites a paragraph per theme; resist it. The changelog already carries the user-facing story and the diff carries the rest, so a body earns its place only by explaining a decision neither of those shows. This commit is one of the rare ones where a paragraph is justified at all: most commits in this repository have no body.
Re-read git status immediately before staging, and never reach for git add -A. A sweep agent that has gone quiet is not necessarily finished: it can resume editing minutes later, after the step-2 gate has already run and while you are drafting the commit. git add -A then sweeps in files you never reviewed and whose changes the gate never covered. Diff every path that was not in the tree when you ran the gate, and re-run the gate before committing if any appeared: an edit landing after the gate is an ungated edit, whoever made it. This is not hypothetical bookkeeping — one such late edit dropped an explicit name= from a Click command, which would have silently renamed a CLI command the changelog advertises had the framework not happened to derive the same name from the function. The narrow-staging rule the sub-agent rules impose (git commit <path>, never -a) applies to the orchestrator's own reconciliation commit for exactly this reason.
Signed commits: sandbox off, and a hardware key is not a retry loop. With SSH signing (gpg.format = ssh), the harness sandbox blocks the key or socket under ~/.ssh/* (Operation not permitted): disable the sandbox for the git commit and git push calls only. A hardware-backed key (Secretive, YubiKey, TPM) additionally prompts the maintainer per signature, and one unanswered prompt wears three faces, sometimes in sequence across retries: agent refused operation?, Couldn't sign message (signer): communication with agent failed? (exit 128, then failed to write commit object), or no output whatsoever until something kills the command. The first two look like a real failure; the third is the expensive one, because a signing command silent for minutes is a prompt nobody answered rather than a slow command, so bound it with a timeout and hand off instead of waiting it out. Stop after one or two retries and ask the maintainer rather than burning prompts they may not be watching. When the fix already exists as an open, CI-green autofix PR (a sync-* or format-* branch), prefer merging that PR over authoring your own signed commit: GitHub signs the merge commit server-side, sidestepping the local key entirely. And gh pr merge may itself be walled — not by a live prompt but by a standing permissions.deny rule in the operator's settings, which no in-chat authorization can override; when both the merge and the signed commit are blocked, "ask the maintainer" means asking them to run the merge or push themselves, outside this session, not merely to approve in chat. The same applies to the babysitter in step 6: its skill carries the explicit hand-off contract.
6. Babysit CI to green
Step 2 cleared every locally-reproducible failure and step 1's red inventory pre-paid the debt already visible in past runs, so the first run should be close to green: babysit handles what only CI surfaces, platform-specific breaks and, when the project builds binaries, the slow Nuitka matrix.
The goal of this step is a genuinely green suite, not a catalogue of which reds are "non-blocking." A release is when test-suite debt gets paid down: fix everytests.yaml failure surfaced here, including flaky and pre-existing ones carried on main for months (a chronic environment-specific break, a "known-flaky" live-registry install, an allowed-failure ⁉️ probe that actually crashes), not only this cycle's regressions. Root-cause each red to its mechanism: the manager argv it builds, the dependency it imports, the assertion that drifted. Then fix it at the source: a real code bug gets the code fix, a genuinely-flaky live-registry install is folded into the test's tolerated-exit set (with the reasoned comment its peers carry), a partial-wheel import crash is converted to a clean skip gated on the library's own availability sentinel. A red's ⁉️/non-blocking status governs only whether it gates the merge, never whether it gets fixed: the glyph test in the line below decides what blocks the release, not what you leave broken.
Run a tight fix-loop: act on the first failing job, fix and push immediately, restart CI on the new commit, never wait out a 40-90-minute matrix per fix. The instant any job turns conclusion=="failure" (gh run view <run> --json jobs, broken on the earliest failure rather than the run's overall conclusion), fetch its log, reproduce and fix locally against the pinned gate on the touched files, commit with the attribution trailer, and push. The fresh push supersedes the obsolete in-progress run; cancelling it and letting the new commit trigger a clean run converges in fewer wall-clock hours than serially waiting for each full run to drain. Batch only fixes already root-caused and verified together; never wait to accumulate more failures before acting on one you already understand.
Time each push by what its diff rebuilds. The immediate push above is for source-affecting fixes (repomatic/**, tests/**, pyproject.toml, uv.lock): the matrix they cancel was verifying an obsolete tree, and their own run rebuilds everything. A matrix-skipping commit (changelog-only, docs-only: paths outside Metadata.binary_affecting_paths) inverts the economics on a binaries-enabled project, because release.yaml runs on every push in a per-branch cancel-in-progress group: pushed mid-drain, it cancels an in-flight binary matrix that its own run then skips rebuilding (skip_binary_build), and the lost verification is only re-buyable with a full re-dispatch. That cost scales with what is in flight: an ordinary push builds only the [tool.repomatic] nuitka.dev-targets canary subset, and no push cancels a full fleet, since release commits, schedule and workflow_dispatch runs each sit in their own concurrency group. Hold such commits (the post-babysit changelog reconciliation below included) until the heavy matrices on the current tree are terminal, or bundle them into the next source-affecting push; with binaries off, or only a canary build in flight, push freely: a cancelled release.yaml re-runs in minutes, and a prose push does not even cancel tests.yaml (paths-filtered, so no new run enters its concurrency group).
Spawn Agent, named so it stays addressable, on the sonnet model to run /babysit-ci to completion (the loop is mechanical: fetch logs, match patterns, fix, commit, push). Leave it on the default background mode, not run_in_background: false: the idle/chase cycle described below (a capped gh run watch ending its turn, a chase message resuming it) is exactly how a background agent behaves in this harness, and a literal foreground spawn would instead block the orchestrator's own turn for a wait that can span hours. It monitors tests.yaml, lint.yaml, autofix.yaml, docs.yaml, and release.yaml (whose engine runs the per-platform Nuitka matrix when the project enables binaries; its job names are templated, like ✅ {os}, {sha} build, so key the watch on the workflow, not a literal job id, and the ✅/⁉️ glyph across the test and release matrices marks cell stability, not outcome: a red ✅ (required) cell is release-blocking, a red ⁉️ (an allowed-failure probe, like the newest dev Python) is noise, so triage a red matrix on the absence of the unstable glyph — select(.conclusion=="failure" and (.name|contains("⁉️")|not)) — and not by which Python version failed. Never anchor that test at the start of the name: a release-engine job arrives through the reusable call as release / ✅ {os}, {sha} build, so startswith("✅") matches none of them and drops every binary-matrix red on the floor. <cmd> ci-status --branch main applies the same rule for you). If /babysit-ci is excluded here, the agent runs the same fetch-logs/fix/commit/push/re-poll loop inline (and the sub-agent rules cover a failed spawn). Its prompt restates the sub-agent rules (the trailer and narrow staging especially: its commits are exactly the unattended ones those rules exist for) and adds:
The loop condition, verbatim: "re-poll after each push; do not return after a push without re-polling". The turn ends only when every monitored workflow on the latest main HEAD has conclusion: success (or skipped for benign reasons), or on a real blocker it cannot resolve. Terser phrasings get misread as "report after first fix", and the agent returns while the slow jobs still build, doubling wall-clock when you re-spawn it.
The poll cadence: every poll loop sleeps at least 45-60 seconds between iterations, with the sleep inside the loop command. Zero-delay spins exhaust the shared REST quota (5,000 requests/hour) in minutes, and the exhaustion resurfaces as PAT-permission-shaped workflow failures and prepare-release hangs (see babysit's § GitHub API rate-limit exhaustion).
Poll in-process; never detach a monitor: the poll loop must block inside the agent's turn (a foreground loop or gh run watch), never a run_in_background Bash poller or a Monitor-tool stream the agent idles on awaiting notification. Name the Monitor tool in the prohibition: an agent told only "no background poller" does not classify Monitor as one, reaches for it, and idles mid-watch. Babysit itself forbids detached monitors, but a spawn prompt with an "as a background task" aside overrides that, and the agent follows the prompt: a failure landing in the idle window then goes unhandled.
Hand it the in-turn mechanism, not just the prohibition. A bare shell sleep is blocked in some harness shells, and an agent that hits that block will reach for Monitor as the only thing that appears to work — the prohibition alone leaves it no way to comply. Name both alternatives explicitly: gh run watch <run-id> --interval 60 (blocks in-turn until that run is terminal, satisfying the cadence without a sleep), and python3 -c "import time; time.sleep(60)" when a raw delay is genuinely needed.
Expect to take the loop over for a long matrix; this is structural, not agent failure. The Bash tool caps a foreground command at ~10 minutes and auto-backgrounds it on expiry, which ends the agent's turn. A 40-90-minute Nuitka matrix therefore cannot be watched to completion by a sub-agent: each gh run watch is capped and backgrounded, and the agent idles once per cap, needing a chase to resume. Re-spawning or re-chasing does not fix it — the next watch hits the same cap. Delegate babysitting for the fast workflows, and when the remaining wait is the binary matrix, stop the agent and own the wait yourself with a Monitor (the orchestrator's own sleep is blocked, which is exactly why the tool is right here and wrong there). Give that monitor a filter that emits only what you would act on — a failing stable job the moment it lands, and each run reaching a terminal state — since per-cell progress across a dozen cells will trip the event-volume cap.
Interim status pings: a message at startup (local gate result, which runs it watches) and on every state change (fix pushed, workflow landed, re-run triggered), not only at the end. A silent babysitter is indistinguishable from a dead one.
The tight fix-loop above: act on the first failing job, not the run's overall conclusion; fix locally, commit with the trailer, push, and let the fresh run supersede the obsolete one, cancelling it rather than waiting out the full matrix. Fix every red it surfaces (flaky and pre-existing included), not only this cycle's regressions. Time pushes by what they rebuild: a matrix-skipping (changelog- or docs-only) commit holds until the in-flight binary matrix drains, or rides the next source-affecting push.
Treat its return as a claim, not proof. Even with the verbatim prompt it can stop early (the long Nuitka wait is where it gives up). Re-poll gh run list --branch main yourself and read each monitored workflow's conclusion; anything still queued/in_progress or non-green means it stopped early: take over the loop inline rather than re-spawning it into the same idle. On takeover:
Inspect the tree before improvising a fix (git status, git diff --cached): a stood-down or idled agent often leaves a correct fix staged but uncommitted (a hardware signing prompt it could not answer). Adopt and verify that fix instead of re-deriving a parallel one that then collides with it.
Poll at the job level (gh run view <run> --json jobs), per the tight fix-loop above, never the run's conclusion alone: a run-level read hides an already-failed fast job for as long as the slowest cell runs (a failed once-tests job once hid for half an hour behind a 40-minute macOS cell that ultimately passed). Break the instant any required job turns conclusion == "failure" — required meaning any name without⁉️ in it, which covers the glyph-less non-matrix jobs (🛡️ Lint types, 1️⃣ Run-once tests, 📦 Package install) as well as the ✅ cells.
Log fetches may need the sandbox off: gh run view --log-failed caches under ~/.cache/gh, which the sandbox denies; the creating cache entry ... operation not permitted failure masquerades as a gh bug.
Foreground sleep is blocked in the orchestrator's shell, so you cannot run the in-turn while … sleep poll loop the spawned babysitter uses. Drive the wait with the Monitor tool (an until-loop command that emits each workflow's terminal state and exits) or a bounded gh run watch <run-id>, acting on each landing. The babysitter's "never detach a monitor" rule governs the sub-agent polling in-turn; the orchestrator taking over must reach for Monitor/gh run watch precisely because its own sleep is disallowed.
Write that watcher in Python, and never trust a green it reports without confirming against gh. The failure that matters here is not a watcher that crashes, it is one that exits silently declaring success: its "all workflows terminal" line is indistinguishable from the real thing, and it arrives early, which reads as good news. A shell watcher is the easy way to get one, because macOS ships bash 3.2: declare -A is a hard error there, so the obvious "have I already reported this workflow?" map fails on the first iteration, every workflow falls through as already-seen, and the loop announces completion while the whole matrix is still queued. A stderr line scrolling past in the monitor's output file is the only hint. Python has no such trap and gives you a set() that works on any machine. Whatever you write, the confirmation is the same: re-read gh run list --branch main yourself before acting on a terminal claim, exactly as you do for the babysitter's own return.
Verify the Nuitka run yourself, starting with whether the project builds binaries at all:
A project with [tool.repomatic] nuitka.enabled = false or no CLI entry point emits no nuitka_matrix (see Metadata.nuitka_matrix in metadata.py): the engine's per-platform build and test jobs skip by design on every push, release commits included, and past releases carry no binary assets (gh release view <last-tag> --json assets settles the regime in one call). There is no binary signal to verify and none appears on merge either (the release_commits_matrix gate drives the publish/tag lanes, not the binary matrix): wheel-plus-sdist is the complete release shape.
When binaries are enabled, babysit's "every stable job passes" never covers them: its early exit declares success once the fast platforms are green, while macOS and the entire release.yaml matrix still build. Independently confirm the release.yaml run reached a terminal green state (gh run watch <release-run-id>, then read its conclusion); never infer the Nuitka result from babysit's summary. If a binary build fails, re-spawn babysit or fix inline.
A green conclusion also proves nothing on a HEAD that touched no Python source: release.yaml skips the entire compile-binaries matrix on such pushes (workflow-only, docs-only). Read the run's jobs and confirm the per-platform build and test jobs ran rather than skipped; when they skipped, the authoritative binary signal is the last run that actually compiled, valid only while its source tree matches the release tree. Tell a wholesale skip from individually-filtered cells by whether the job name expanded. The two are indistinguishable in any status count — both read as "N success, M skipped" — and they mean opposite things: filtered cells are the expected nuitka.dev-targets canary behavior, while an unevaluated matrix means zero binaries were built. The tell is the job name itself. A matrix that expanded yields named cells (✅ ubuntu-26.04, abc1234 build); a matrix that never evaluated leaves the raw template verbatim (${{ (((matrix.state == 'stable') && '✅') || '⁉️') }} ${{ matrix.os }}, ${{ matrix.short_sha }} build). Unexpanded ${{ }} in a job name always means the stage never ran. This misreads silently and expensively: a sub-agent reported a content-skipped matrix as "dev-canary subset built, rest correctly skipped" twice in one run, which would have shipped a release whose binaries no CI job ever compiled. When no compiled run matches the release tree (a binary-relevant commit's run cancelled by supersession, then the reconciliation HEAD content-skipped the matrix), you may dispatch one: gh workflow run release.yaml --ref main. A dispatch carries no push diff for the skip heuristic to read, so it compiles and self-tests the full per-platform matrix on the current tree, with no commit and no PR churn.
That dispatch is information, never a gate, and on a busy pool it is often the wrong call.claude.md § A published release freezes what is missing from it is explicit that shipping short is intended behavior and that you never hold a release or sit on a draft waiting for a build cell, so a missing binary signal must not delay a merge that is otherwise green. Three things make the wait even less attractive than that rule alone suggests: a skipped matrix is the normal steady state here rather than a new gap, since ordinary pushes routinely fall outside binary_affecting_paths and supersession cancels the rest; the wheel and sdist that PyPI actually ships are built by 📦 Build package, which involves no Nuitka at all, so package distribution is already verified without it; and merging triggers the release commit's own full-fleet build, which a pre-merge dispatch then competes with for runners, slowing the build that produces the real assets. So weigh the dispatch against the source delta since the last compile: a new dataclass field or a bumped version literal does not earn a 40-90-minute fleet build, while a dependency swap, a packaging change or a new native import does. When you dispatch and then decide to merge anyway, cancel the dispatch first so it stops taxing the release it was meant to protect.
Close the coverage holes a busy cycle opens:
Refresh the release PR after non-trigger pushes — verify the branch, not the run. A green changelog.yaml run titled with your commit does not prove the PR moved onto it: the prepare-release job regenerates the PR only when it is triggered, and a push touching only files outside changelog.yaml's paths: filter (a tests/- or packaging-only reconciliation commit) may not trigger it. Recent repomatic adds a workflow_run backstop — prepare-release also runs after every Build & release — that re-bases the PR onto the current HEAD regardless of paths, so on a current pin this is usually already handled by the time main is green. Verify rather than assume: gh api repos/<owner>/<repo>/compare/<HEAD>...prepare-release --jq .behind_by must be 0. If it is non-zero (an older repomatic pin without the backstop, or a supersession that outran it), run gh workflow run changelog.yaml --ref main (a workflow_dispatch rebases onto the current HEAD), then re-check before step 7. This does not block the release either way — a "Rebase and merge" replays the freeze onto current main, and the PR's own checks are skipped by design — but a refreshed PR shows the reviewer the tree that will actually ship.
A racing version-increment merge can leave your commit's heavy CI uncompleted. Merging the minor-/major-version-increment PR mid-build cancels your in-flight tests/lint (shared concurrency group) while the bump commit is itself gated out of them, so the release PR can show them skipped. This is by design: step 2 is the authoritative pre-merge check, so read skipped tests/lint on a bump commit as expected and do not re-push to force a run.
An autofix sync-tool-versions commit landing mid-babysit leaves its new checksums unproven, and a green Autofix does not clear them. Only a job that actually runs the bumped tool exercises its SHA-256, so the green lane to check is the tool's consumer, not the workflow that happened to commit the bump. Grep for the tool's call sites (<cmd> run <tool>) and read whether any executed: several are gated (the gh attestation step is if: … release_commits_matrix, so it is skipped on every non-release commit and its checksum sails through the whole cycle untested, then runs for the first time on the release commit — where a bad hash fails attestation and strands the release as a draft). Read this at step level, not job level: the enclosing job is green while the gated step inside it is skipped. When no consumer ran, verify directly rather than reasoning from provenance: <cmd> run <tool> -- --version downloads and checksum-verifies one platform, and <cmd> update-checksums followed by an empty git diff re-derives every platform hash from the live URL and proves the committed table. Both need the sandbox disabled (it blocks the tool cache and can truncate a download into a false mismatch).
When no Tests run ever completed on the release tree, dispatch one. Superseded-run cancellation, the version-bump gate, and tests.yaml's paths: filter can leave every attempt on a busy cycle cancelled or skipped, and recent Tests activity is not a completed run on the current tree. If the local pytest was also skipped (the integration-heavy exception), no test signal exists anywhere: get one with gh workflow run tests.yaml --ref main (a workflow_dispatch adds no commit and does not regenerate the PR), then verify the stable jobs and ignore the continue-on-error probes. A docs-only HEAD shares its source tree with the prior commit, so the dispatched run validates the frozen tree. The same hole opens for every paths-filtered workflow, Docs included (a cancelled docs.yaml run followed by a workflow-only HEAD leaves the cycle's docstring edits unbuilt): close it the same way (gh workflow run docs.yaml --ref main) and confirm the dispatched run lands green.
Reconcile the changelog against every fix babysit committed: entries it added and omitted. Walk its commits and blame each fix against the last release tag: a bug that only ever existed in code introduced this same cycle is a user no-op (drop any entry babysit added for it); a bug that reached an earlier release deserves the entry babysit may have skipped. Re-run step 1.4's consolidation with both corrections and present the diff (step 4) before committing. Draft this pass during the slow tail (the work is local), but push it by the timing rule above: only once the heavy matrices on the final code HEAD are terminal green, so its changelog-only diff inherits the drained tree's signal instead of cancelling a binary build mid-run. Then confirm changelog.yaml regenerated the PR onto it (the refresh check above) before the step-7 confirmation.
Surface maintainer work that appeared during the wait. The 30+ minute CI wait gives the maintainer time to keep coding: those uncommitted files are theirs (sub-agent rules). List them in the step-7 report so the maintainer decides whether each belongs in this release (commit and push before merge) or the next.
7. Confirm and stop
Once main is green and the release PR exists (gh pr list --head prepare-release), report:
the release PR URL,
the version it will cut, plus the bump advisory from step 3,
that the PR is opened as a draft (the prepare-release PR template's frontmatter carries draft: true, which pr-sync re-applies on every refresh, not just at creation), so the remaining human actions are to mark it "Ready for review", then "Rebase and merge" (never squash).
Do not merge the PR, and do not mark it ready yourself. That final human action is the boundary this skill stops at.
Repairing a short ship
When the binary matrix came up short, say so here and name the platforms the version will lose. Publishing is what locks the asset list, so this is the last moment the gap is cheap to record; afterwards it is a permanent property of that version. Shipping short is intended behavior and never a reason to hold the release, per claude.md § A published release freezes what is missing from it.
Three artifacts then keep advertising binaries that are not there, each needing a hand after the merge:
The version's changelog section, which takes a > [!WARNING] naming the gap. Write it as a hand-written admonition (anything not starting with > `X.Y.Z` is), which lands in the editorial slot fix-changelog preserves rather than the availability slot it regenerates.
The GitHub release body, rebuilt from that section. Immutability locks the assets and the tag, not the notes, so this stays editable after publishing. sync-github-releases is the mechanism, but it skips drafts and caches the release list for 24h, so a same-day fix goes through gh release edit --notes-file with the body build_expected_body renders. Both converge on the same text, so a later CI sync is a no-op rather than a clobber.
docs/install.md, whose download URLs the freeze pins to the version being released, optimistically: the freeze commit is what triggers the build, so it cannot know whether the binaries will land. Re-point them at the last release that carries binaries via PrepareRelease.freeze_install_download_urls, and the next release's freeze ratchets them forward again. lint-repo's check_install_guide_downloads reports the gap but never repairs it, since an automated rewrite driven by a single API read could downgrade a healthy install page on a flaky response.
Report the three as follow-ups. Do not perform them: they land after a merge this skill does not make.
Upstream only: the release PR is rebase-merged, so its freeze and unfreeze commits arrive in a single push, and GitHub Actions reads workflow files from that push's head. `kdeldycke/repomatic`'s own release lane therefore always runs the **unfrozen** workflow content, whatever the freeze wrote into the release commit. A job that assumes the frozen `uvx 'repomatic==X.Y.Z'` form is what executes (and drops its checkout on that basis) dies on `Failed to spawn: repomatic`. Only downstream repos, which call the reusable workflow at its tag, ever run the frozen form.
8. Reflect and contribute back
This skill, the workflows it drives, and the conventions it enforces live upstream in kdeldycke/repomatic and sync down to each caller; a release is when their rough edges show. Before finishing, review the session and for each finding point at the exact source with a concrete fix: ../repomatic from a downstream repo, this very tree when the repo is repomatic (see § Shipping the findings).
A skill instruction that misled you or forced a judgment call you got wrong: a dangling cross-reference, a missing step, an instruction a sub-agent should have inherited but didn't (archetype: the Co-Authored-By trailer dropped because the attribution note leaned on an unsynced CLAUDE.md section).
A workflow "failure" that turned out to be a real upstream bug: trace it to its template in repomatic/data/ or .github/workflows/ instead of waving it off (archetype: release.yaml red on every push from a strategy.matrix evaluating fromJSON('')).
A reconciliation the skill should have anticipated (archetype: the step-6 second consolidation pass, added after babysit fixes shipped spurious or missing entries).
Surfacing these is how the skill improves release-over-release.
Implement each finding in ../repomatic when that sibling checkout exists, and stop before the commit. Describing a fix leaves the maintainer the whole implementation; a diff already sitting in their working tree leaves them only the review, which is the step they were going to do regardless. So edit the upstream code, update whatever tests the change breaks, and verify it there (uv run --project ../repomatic --frozen -- pytest …). Then do not commit, do not push, do not open a PR or an issue — the same boundary this skill stops at downstream. Report which files you touched and what verification you ran, so the review starts from evidence rather than from your summary.
Fix at the mechanism, not the symptom: a wording repeated across many generated files has one generator, and editing the generated copies leaves the next render to undo you. Check first whether the text you are about to hand-edit is emitted by a function or asserted verbatim by a test. When no ../repomatic checkout exists, fall back to describing the finding precisely enough to act on: the file, the mechanism, and the failure it causes.
Shipping the findings when the repo is repomatic
Inside kdeldycke/repomatic there is no ../repomatic, and the boundary above inverts. The canonical repo is both the subject of the release and the home of every skill, workflow and convention these findings target, so "propose it upstream" collapses into "fix it here". Leaving the diff uncommitted there buys nothing and costs the release: the next cycle ships the same rough edge, and the diff sits in the tree collecting conflicts against whatever the machinery rewrites meanwhile. Detect the case the way the invocation rule already does (the context shows CANONICAL_REPO), then land the findings as an ordinary reconciliation commit rather than a working-tree diff.
A late commit still ships, which is what makes folding them in possible at all.prepare-release regenerates the release PR on every push to main, replaying the freeze onto the new HEAD, so anything pushed before the maintainer merges lands in this release instead of the next. That is also why the pass stays here rather than moving before step 5: the findings worth shipping mostly do not exist until babysit has surfaced what CI does and the local gate cannot.
Treat that commit as a reconciliation like any other, which means four things the uncommitted-diff path never had to handle:
Verify it with the same gate. Run the part of step 2 the change touches: the pinned <cmd> run mdformat over an edited Markdown asset, pytest tests/test_claude_assets.py tests/test_skills.py over a bundled skill or agent, the full gate over Python. Bundled assets carry conformance tests that a docs page does not, and that difference decides how much you run.
Give it a changelog bullet only when a user can observe it. A bundled skill, agent or workflow deploys verbatim to every downstream repo, so correcting one is a shipped fix and earns an entry. A change to this skill's own release choreography is not something a downstream user consumes, and earns none. When you do add one, re-run consolidation and present the diff (step 4) before committing.
Time the push by what it rebuilds (step 6): a .claude/-only or docs-only commit skips the matrices yet still cancels an in-flight release.yaml on a binaries-enabled project. Hold it until the heavy matrices are terminal, or bundle it into the next source-affecting push.
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section.Voir sur GitHub
changelog.md
citation.cff
__init__.py
pyproject.toml
The mirror of the hand-maintained rule: a freeze-managed field legitimately shows the dev version between releases, so never flag one as stale against the released tag or as a dead link. pyproject.toml's urls.Download reading .../releases/tag/vX.Y.Z.devN (a tag that has no GitHub release yet) is the expected post-bump state that the freeze rewrites to the release tag at cut. Hand-maintained samples track the released tag while freeze-managed fields track the dev version, so classify a version string by which mechanism owns it before judging it stale.
A third owner sits beside hand-maintained and freeze-managed: artifacts regenerated by _release-engine.yaml's update-dep-graph job, whose if: gates on release_commits_matrix, so it fires only on release commits (never on ordinary pushes, to avoid noise from transitive dependency churn). It sits in the release engine rather than autofix.yaml because a release push is its only firing moment, and that is exactly the push autofix.yaml now skips wholesale, so do not go looking for it there. docs/assets/dependencies.mmd therefore lags pyproject.toml for the whole cycle: a runtime dependency added since the last release is expected to be missing from it, and the graph catches up through its own PR once the release lands. Never hand-forge the file, and never run <cmd> update-dep-graph to "fix" it mid-sweep: a local run uses whatever repomatic version the sweep resolved rather than the version the job pins, so it produces churn the next regeneration reverts. Classify a stale-looking generated artifact by the job that owns it before reporting it as drift.
Executable doc blocks fail open: a {click:run} invocation that no longer parses renders the usage error into the published page instead of failing the build (docs.yaml stayed green while a stale option printed No such option; only click:tree and click:config hard-error). Verify each {click:run} invocation against the current CLI, or grep the built HTML for Error: No such option-class output.
Correcting one description of a convention means correcting every description of it in the same pass: a rule restated in more than one place (an overview line and its worked example, two docs pages) drifts as a set, so grep for the sibling statements and align them together — fixing one in isolation leaves the others contradicting the fix (a freeze-cutoff overview still said "the day after" while its worked example had been corrected to "the second day after", reconciled only on a second pass).
The same "align the siblings" rule governs quantities, not just wording. A cycle that measured the same thing twice ships two answers, so cross-check every timing, size, count and ratio a page states against the other statements of it, the changelog's copy included (a performance page opened "every timing below comes from one batch of runs" while two of its tables disagreed 2.3x on the same step at the same settings, each table internally consistent, because two commits had each measured their own batch; the changelog repeated the wrong figure). Surrounding prose that quotes a ratio the tables no longer support is the tell. Report the contradiction with the conflicting values and which one the other statements corroborate, rather than silently re-deriving numbers on hardware that is not the maintainer's: the figures are theirs to own, but they cannot fix what nobody flagged.
Commit provenance decides whether a clashing figure is reported or fixed, and the two need opposite handling. Blame each one (git log -S, or read the commit that introduced it). When the older figure entered earlier in this same cycle and a later commit measured the thing again, it is not a rival result: it is superseded intra-cycle scaffolding, which layer 1 of the code pass strips like any other. Replace it, and cite the surviving measurement's source beside it so the two cannot drift apart again. Only when neither figure obsoletes the other are they independent measurements, and only then does the report-don't-touch rule above apply. Getting the direction wrong fails both ways: it either ships a stale number dressed as a competing result, or silently overwrites a real measurement nobody asked you to re-take.
The docs build has a single owner, the code agent (which already builds for docstring cross-references): verify prose against that build instead of launching a second sphinx-build into the same output dir.
Any docs-pass edit touching a .py file (typically docs/conf.py) is re-verified with a bare <cmd> run mypy before the agent returns: docs/conf.py may import from the docs group's higher Python floor while mypy checks the project minimum, and the break otherwise surfaces only in CI's lint job. Give it no arguments here too — naming docs/ as a directory is the form step 2 warns against, since a directory changes module resolution enough that mypy follows an installed dependency's own source.
Changelog released sections (## [X.Y.Z] blocks) are immutable history: a command, option, or config key named there was correct for that release, so never flag or rewrite a since-renamed name in one. When the changelog seeds the checklist for a rename, reconcile only the unreleased section (the docs pass once flagged update-deps-graph in three released sections that a 7.4.0 rename had superseded).
These run in write mode (run typos defaults to --write-changes; audit --fix rewrites pins), and a smoke run can still touch files outside CI's autofix scope: revert any mutation that is not part of this release's net diff before the step-5 commit; a fix that genuinely belongs to the cycle can be kept and folded into the reconciliation. Revert a formatter move as a move: it is a paired add-plus-delete, and reverting only the added copy silently deletes the block from the file (a [tool.uv] key nearly vanished this way). A mutated file carrying no reconciliation edit reverts whole with git checkout -- <file>: safe at this gate, where the sweep agents have already joined (their no-revert rule protects a live shared tree).