Skip to main content

repomatic-ship

Prepare a release. 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. Ask for review in normal use; run fully autonomous under `--dangerously-skip-permissions`.

Source facts

Repository
kdeldycke/dotfiles
Last source activity
October 29, 2026 at 09:34
Detected SKILL.md language
English
Stars
173
Forks
22

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
repomatic-ship
description
Prepare a release. 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. Ask for review in normal use; run fully autonomous under `--dangerously-skip-permissions`.
compatibility
Designed for Claude Code. Recommended model: Opus.
allowed-tools
Bash Read Grep Glob Skill Agent
## Context !`grep -m1 'version' pyproject.toml 2>/dev/null` !`awk '/^## \[/{n++} n==2{exit} {print}' changelog.md 2>/dev/null` !`git tag --sort=-v:refname | head -3 2>/dev/null` !`git log --oneline -25 2>/dev/null` !`git status --short 2>/dev/null` !`[ -f repomatic/__init__.py ] && echo "CANONICAL_REPO" || echo "DOWNSTREAM"` !`grep -rhoE 'kdeldycke/repomatic/[^@ ]+@[0-9a-f]{40} # v[0-9.]+' .github/workflows 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+$' | sort -u | sed 's/^/WORKFLOW_PIN=/'` ## Instructions 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`, once the unreleased changelog section holds an entry, to build the freeze and unfreeze commits and open the release PR. **Do not run `prepare-release` yourself**: 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 `main` clean enough that the auto-generated release PR is correct, then keep `main` green. ### 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. - **Invocation method.** When the context shows `CANONICAL_REPO`, use `uv run repomatic`. Otherwise use `uvx --exclude-newer '{window}' --exclude-newer-package repomatic=P0D --from 'repomatic==X.Y.Z' -- repomatic`, with `X.Y.Z` the `WORKFLOW_PIN` the context shows. Read `{window}` from the `UV_EXCLUDE_NEWER` value of any `.github/workflows/*.yaml`, which `repomatic init` renders from `[tool.repomatic] minimum-release-age`. This applies the supply-chain cooldown to repomatic's dependency tree while keeping a fresh release installable. The pin matters because the tool registry ships inside repomatic: a newer release pins newer `ruff`, `typos` or `mypy` than the ones CI runs, and a gate built on it stops reproducing CI. Drop the `--from` clause only when the context shows no `WORKFLOW_PIN`. References to `<cmd>` below resolve to one or the other. - **Delegate the substance edits, apply the changelog yourself.** The sweep agents own the code, docs and bundled-asset lanes, so leave those files to them even when you do hold `Edit`/`Write`. The changelog is the exception: a skill is instructions rather than an actor, and `/repomatic-changelog` tells whoever loaded it to apply the consolidation directly, so you write `changelog.md` inline. ### 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` says nothing about commit attribution. A maintainer's explicit standing rule against AI attribution outranks it, because the trailer lands in *their* repository's permanent history. Check for such a rule **before the first commit**: stripping a trailer from an already-pushed commit needs a force-push, which is off-limits. When an exemption applies, write it into the spawn prompt of every agent that may commit: a withdrawal issued mid-run can arrive after the agent has committed. - **Reports are sent, not written.** A background agent's end-of-turn text is not a channel to rely on, so every spawn prompt must require the final report as a `SendMessage` to the **spawning (main) session**, naming that recipient explicitly: the spawning session answers to `main`, while an invented label like "orchestrator" or "main-orchestrator" resolves to nothing. An agent type whose `tools:` line omits `SendMessage` (`qa-engineer`, `sphinx-docs` and `grunt-qa`) can still hold it when spawned with a `name`. When no message arrives, read the idle notification's text before chasing, since it can carry the report of an agent that could not send one. Chase once when it holds none; the tree (`git diff`) stays the authoritative record either way. Do not pair the message with a "write the same report to a file" fallback: the harness refuses a spawned agent that write. - **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 fails outright. A spawn brief that expands `<cmd> run mypy --` into `uv run --frozen -- mypy --` dies 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 can land after the agent composed its final report and be silently dropped. A named agent reads its inbox when its turn ends (Claude Code `2.1.284`, 2026-10-04): its final report then answers none of the messages sent during that turn, and a second report follows once it has read them. Before you chase, check that it shows as `running` again. After tasking a running agent, confirm the tree reflects the request (`git diff` the target file) before moving on. Read fresh every time: an edit can land in the gap between two checks. Grep for the *absence* of the old text (`grep -c '<old phrase>' <file>` returning `0`) instead of eyeballing a diff. - **Never let one lane wait on another lane's edit.** Messages cross: an agent can ask a sibling for a change while you reassign that file, then wait on an edit nobody will make. Name one owner per file in the same message to both agents, and when an agent reports it is blocked on another lane, make the edit yourself and tell it the edit landed. - **Never revert the shared working tree.** The agents share one tree, so no agent may run a working-tree-reverting git command (`checkout`, `restore`, `stash`, `reset`, `clean`), which silently discards the *other* agents' uncommitted edits. The rule binds investigation too: an agent comparing its file against the committed state reaches for `git stash`, which takes the whole tree. Say so in the spawn prompt, and name the read-only alternative beside it, `git show HEAD:<path>`. 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. So is a hunk the agent did not write inside a file it edits: report it, and never document or build on it. - **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. An agent that dies mid-run (a machine put to sleep, a lost login, a restarted session) sends no report and can stop halfway through a refactor. A `SendMessage` resumes it only while the session that spawned it is alive. Before you take its lane over, prove the package still imports, then read its diff file by file. ### 1. Reconciliation sweep A release materializes the **net state since the last tag**, not the path taken to reach it. Reconcile the changelog, the code and the docs 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". 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 api 'repos/<owner>/<repo>/actions/workflows/tests.yaml/runs?branch=main&created=>=<date>'`, 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. Read `release.yaml`'s canary `✅ <os>, <sha> build` job too: its `Self-test binary` step runs the CLI test plan against the freshly compiled binary, and a toolchain pin bump upstream (a Nuitka release miscompiling a dependency) reddens it with no line of this repository changed. Under step 6's genuinely-green goal those reds are release work, 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. **Read runs through the API, not the `gh run list` wrappers.** Both the branch-wide form and the per-workflow form have put runs weeks old at the top of the list, which invents a red inventory. Two API calls are the authoritative reads: `gh api 'repos/<owner>/<repo>/actions/runs?head_sha=<sha>'` answers "what ran on this commit", and `gh api 'repos/<owner>/<repo>/actions/workflows/<file>/runs?branch=<branch>'` answers "what ran on this branch", but only with a `created=>=<date>` filter appended: without one the page can be days stale. A date range (`created=2026-09-01..2026-09-09`) walks the older weeks. Give the `head_sha` form the **full** 40-character SHA: an abbreviated one returns an empty list, which looks identical to "nothing ran". **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: `<cmd> ci-status --automation-prs` lists them with how far each lags `main`, then `gh pr diff <n>` on every one. Look for a *false positive*: a "fix" that is wrong in this repository and that the job re-proposes until the 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. **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 describe a file the maintainer has since fixed. Rewording is the third option beside allowlisting and reverting: dropping the trigger word also stops the job re-proposing, and a rule landed on top of it then allowlists a word that appears nowhere. So `git grep` the trigger before writing a rule for it, and when the maintainer 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 `head_sha`, and treat a red as live only if nothing since it could have fixed it.** Most monitored workflows carry a `paths:` filter, so a commit that touches nothing in the filter triggers no run. When *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. Before adding a red to the brief, run `git log --oneline <run head_sha>..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. **A green conclusive run proves nothing when supersession cancelled every run between it and `HEAD`.** 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, and a false green ships the break. Before recording an empty red inventory, diff the gap: `git log --oneline <newest success head_sha>..HEAD`. When cycle commits sit inside it, the workflow has never run on them and the green is stale. 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 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 every lane that has work as **one `Agent` call each, all in a single tool-call block**: sequential spawns waste the wall-clock of whichever finishes first. **On a cycle whose diff spans hundreds of files, split the code lane into disjoint halves by package area.** Each half owns its modules and the tests that target them. Every split lane runs focused tests with `--no-cov`, so concurrent runs never share coverage data files, and step 2 runs the full suite. The docs build then moves to the orchestrator. Build `HEAD` and the last tag in throwaway worktrees at spawn time. Route each warning that only the `HEAD` build reports to the lane that owns its file. 1. **Code**: an `Agent` that reviews every file changed since the last tag for reuse, quality, simplification, and deduplication, and fixes what it finds, simplifying before adding: existing code or a tool often already covers the case. 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. - A defect in code the cycle added is the exception: it is release work. The agent fixes it in the pass, with a test that fails on the pre-fix lines. It never reached a release, so it takes no changelog entry. Write this into the brief: an agent told only to preserve behavior holds the fix back and asks, and the answer reaches it a full round later (sub-agent rules, trust the tree). A defect that predates the cycle is reported instead. - 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. A downstream `CLAUDE.md` "type checking" command is often the narrow dev-loop form scoped to the package only: do not inherit that scope, 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: 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 in a separate environment, with `{scratch}` a directory outside the repository: `UV_PROJECT_ENVIRONMENT={scratch}/docs-venv uv run --frozen --all-extras --group docs -- sphinx-build --fresh-env -b html docs docs/_build/html`. The shared `.venv` then keeps the CI-matching groups, which the `docs` group would perturb (see step 2's `--all-groups` warning). `--fresh-env` re-reads every page: an incremental build reports warnings only from the pages it re-read. A checked-in `:mirror:` block whose cells embed source-line anchors is invalidated by every edit that moves a line in the anchored modules, docstring rewraps included: expect the `update-docs` autofix PR to refresh it after the push, and take that PR's file (`gh api 'repos/<owner>/<repo>/contents/<path>?ref=update-docs' -H 'Accept: application/vnd.github.raw'`) when a local regeneration would differ from CI's, as a terminal-dependent width does, or when `gh pr merge` is permission-walled. 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`. - **A docs build that already warns needs a baseline.** Without one, the pass cannot tell the cycle's warnings from older ones, and fixes them all. Build the last tag in a throwaway `git worktree`, then compare the two warning lists with their line numbers removed. A warning the tag build lacks is one the cycle added: fix it. A warning both builds hold predates the cycle: report one count per category, and leave it. The tag worktree resolves the tag's own lockfile, so the two builds also differ by the dependency bumps the cycle carries. A new warning a bump surfaced on a reference an earlier cycle wrote is bump fallout, not cycle work: report it with the pre-cycle counts instead of fixing the docstring in the reconciliation. Tell the two apart by grepping the cycle diff for the reference: one it never touched was resolving before the bump. - 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. 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. - Diff the public surface against the last tag: for each module, list the top-level public names the cycle removed, added or gave a new value. A removed name keeps resolving through the project's deprecation mechanism, and gets a `**Breaking:**` bullet only when it cannot keep working. A public constant whose value changed gets its released value back, or a bullet. An added name that no unreleased bullet names goes in the report, for step 4. The commit trail hides all three: a deduplication pass drops a public constant as dead code, and its commit names no API change. - Pending work the cycle introduced belongs in a ```` ```{todo} ```` admonition where Sphinx renders it, not in a bare `# TODO`/`XXX` comment: a comment never reaches the published todo list. This applies to **committed** cycle-diff lines only: a `# TODO` present only in the unstaged set is the maintainer's draft and stays untouched. - Retire a `{todo}` whose trigger fired, deleting the shim it guards in the same edit. Bound the check to what moved: `git diff v<last>..HEAD -- pyproject.toml uv.lock` plus the tool-version registry names the upstream releases this cycle actually adopted, and a todo naming one of them is the only kind worth re-reading. Read the trigger's own wording against that diff: a todo waiting on the dependency *floor* fires only when `pyproject.toml` raises it, never on a `uv.lock` bump that merely installs the release, and a shim the old floor still needs gets its todo re-scoped to that floor rather than deleted. Never poll every linked ticket, and leave a todo whose trigger has not fired alone. 2. **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 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; only bumping a sample forward to the not-yet-released version is off-limits. A dev-build banner (a `--version` transcript run from a checkout) is the exception: no checkout prints a released version, so the banner shows the dev version of the cycle that captured it. Refresh it to the current dev version or leave it, and never report it as lagging. 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. - **Freeze-management is per line, not per file.** 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. - **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 `changelog.md` says nothing about the repomatic freeze, which also rewrites `citation.cff`, `__init__.py`, and `pyproject.toml`: treat every version sample as hand-maintained until the regenerated release PR's diff shows the new freeze's actual scope. - 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. Classify a version string by which mechanism owns it before judging it stale.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub