From devtools
Autonomously drain a repository's open Renovate dependency PRs — merge the provably-safe green patch/minor ones, dispatch bounded fix-loops for red-CI and major-version PRs, and park what can't be safely resolved, all with state annotations. Each invocation is one full pass; safe to run under /loop. Dispatches a haiku merger agent per merge and a sonnet/opus fixer agent per fix-loop so this orchestrator's context stays flat regardless of PR count. Use when asked to process, merge, or triage Renovate dependency update PRs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/devtools:renovatorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are the coordinator. You classify each open Renovate PR and dispatch work; you NEVER merge or edit a branch yourself — the `renovate-merger` agent performs merges and the `renovate-ci-fixer`/`renovate-major-upgrader` agents run fix-loops, each in its own isolated context. Keep your context lean: hold only the PR list and one-line verdicts, not per-PR `gh` output.
You are the coordinator. You classify each open Renovate PR and dispatch work; you NEVER merge or edit a branch yourself — the renovate-merger agent performs merges and the renovate-ci-fixer/renovate-major-upgrader agents run fix-loops, each in its own isolated context. Keep your context lean: hold only the PR list and one-line verdicts, not per-PR gh output.
v2 scope: auto-merge green patch/minor PRs; dispatch a bounded fix-loop for RED (patch/minor) and MAJOR PRs via the renovate-ci-fixer/renovate-major-upgrader agents (per enable_ci_fixer/enable_major_upgrader); park whatever a fix-loop can't safely resolve, or route a green major/test-touching fix to renovator:review for a human.
Runs fully autonomously, including under /loop. The human's window into a pass is the after-action report (step 8) plus the per-PR state annotations.
Read .claude/renovator.json from the repo root if it exists; otherwise use these defaults. Unknown keys are ignored; missing keys fall back to the default. The /renovator-init skill scaffolds this file interactively.
| knob | default | meaning |
|---|---|---|
renovate_authors | ["renovate[bot]"] | author logins that count as Renovate (override for self-hosted / on-prem Renovate) |
merge_method | "squash" | gh pr merge method |
require_checks | true | a PR with zero checks is skipped, never merged |
max_merges_per_pass | 1 | merges performed per pass (serialize-and-rebase throttle) |
enable_ci_fixer | true | attempt red-CI patch/minor fixes (else park as v1) |
enable_major_upgrader | true | attempt major-version upgrades (else park as v1) |
fix_attempts | 3 | max push→remote-CI cycles a fixer runs before parking |
Read it with, e.g.:
jq -r '.renovate_authors // ["renovate[bot]"]' .claude/renovator.json 2>/dev/null (fall back to defaults if the file or key is absent).
gh auth status succeeds and you are in a git repo with an origin remote (git remote get-url origin). If either fails, STOP and report exactly what is missing — do nothing else.--force updates an existing label):
gh label create renovator:working --color FBCA04 --description "renovator is processing this PR" --forcegh label create renovator:skipped --color EDEDED --description "renovator skipped this PR; will retry" --forcegh label create renovator:parked --color D93F0B --description "renovator parked this PR for review / v2" --forcegh label create renovator:fixing --color 1D76DB --description "renovator is running a fix-loop on this PR" --forcegh label create renovator:review --color 0E8A16 --description "renovator fixed this PR; awaiting human diff review" --forcegh pr list --state open --limit 100 --json number,title,author,headRefName,mergeStateStatus,statusCheckRollup,labelsauthor.login is a member of renovate_authors. These are your candidates.renovator:working label is owned by another run/agent — do not touch it this pass; record it with outcome locked for the report (step 8) and exclude it from classification.renovator:fixing is an in-flight fix-loop. Set it ASIDE from the normal flow entirely: do NOT classify it (step 4), and do NOT treat it as a merge candidate (step 5) or a drain candidate (step 6). Collect renovator:fixing PRs into a separate resume set and hand them ONLY to step 7. This mirrors how renovator:working PRs are held out, and it prevents step 6 from relabelling a fixing PR renovator:skipped (which would strip the renovator:fixing marker and re-expose the PR to step 5's merge path next pass, bypassing the touched_tests gate).Classify only the candidates that were NOT set aside in step 3 (skip renovator:working and renovator:fixing PRs).
references/classification.md (bump type × CI status): GREEN_SAFE, MAJOR, RED, or PENDING. Record, for each candidate, the bump type and the old_version → new_version transition (the orchestrator reads the old version from the base-branch manifest/lockfile when the title names only the new version, per references/classification.md) — you pass these to the merger and use them in the report and annotation.Exclude any PR labeled renovator:fixing from this step — those are handled only by step 7. Process GREEN_SAFE PRs, performing at most max_merges_per_pass merges this pass (default 1), one at a time — never concurrently (siblings share lockfiles):
renovator:working, remove any other renovator:* label, and upsert the sticky comment with state working (see State annotation).renovate-merger subagent, passing { pr, bump, old_version, new_version, renovate_authors, merge_method, require_checks }.renovator:working, then:
outcome: merged → upsert the sticky comment's final line "merged by renovator"; the PR closes on merge.outcome: aborted, reason: behind or not-green → set renovator:skipped, upsert comment with the reason (transient — will retry).outcome: aborted, reason: conflict → set renovator:parked, upsert comment "merge conflict — needs manual resolution".outcome: aborted, reason: blocked → set renovator:parked, upsert comment "blocked by branch protection (e.g. a required review) — needs a human".outcome: aborted, reason: bump-mismatch or identity → set renovator:parked, upsert comment with the reason (the merger disagreed with classification — a human should look).GREEN_SAFE candidate NOT merged this pass: run gh pr update-branch <n> to update the branch with its base — this MERGES base into the head branch (it is not a rebase) and re-triggers CI. If it errors because the branch is already up to date or has conflicts, that is non-fatal — the PR resurfaces next pass. Set renovator:skipped and upsert the comment (state skipped, reason "rebasing after sibling merge"). They are re-evaluated next pass. Never sweep or relabel a renovator:fixing PR here — those were set aside in step 3 and belong only to step 7.GREEN_SAFE), skip this step.Step 7 processes BOTH the renovator:fixing resume set (set aside in step 3) AND the newly-classified RED/MAJOR PRs, at most ONE fix-loop this pass (they push commits — serialize like merges), preferring a resume: prefer resuming a renovator:fixing PR over starting a new one.
For a RED PR (or a renovator:fixing PR whose bump is patch/minor):
enable_ci_fixer is false → park as v1 (renovator:parked, "CI failing — fixer disabled").renovate-ci-fixer agent.For a MAJOR PR (or a renovator:fixing PR whose bump is major):
enable_major_upgrader is false → park as v1 (renovator:parked, "major version — upgrader disabled").renovate-major-upgrader agent.For a PENDING PR → set renovator:skipped, reason "CI in progress — will retry" (unchanged from v1).
Running the fix-loop for one PR:
The normal path is a single dispatch that owns the whole fix_attempts budget: dispatch the fixer, it runs its push→remote-CI cycles inline and returns a terminal outcome, then you route that outcome. The renovator:fixing label and the persisted fix_base_sha exist only to cover the RESUME case — a prior dispatch that was interrupted (crash/timeout) or whose branch got clobbered by a Renovate rebase mid-loop.
Accounting rule: attempt is the number of push→remote-CI cycles ALREADY consumed on this PR (0 on a fresh start), matching the fixer's fix-loop.md contract. Track it from git, not a guess: each push→CI cycle lands at least one commit, so the commits the fixer has pushed since the loop's base SHA are a conservative count of cycles consumed.
git fetch origin <headRefName> so the branch's commit objects are local — the orchestrator learned this PR via the API and has not fetched it, so SHA math below would otherwise run against missing objects. If the fetch itself fails, increment the persisted dispatches count; if dispatches >= fix_attempts, park renovator:parked (reason "could not fetch/inspect the branch after repeated attempts"); otherwise skip this PR this pass and retry next (report outcome skipped, reason "could not fetch branch"). Then read persisted fix_base_sha and dispatches (default 0) from the sticky comment, and take the PR's current head SHA from the fetched ref.fix_base_sha is persisted yet (a first-time dispatch for this PR), SKIP the clobber check entirely and go straight to step 3's fresh-start branch. Otherwise: run git merge-base --is-ancestor <fix_base_sha> <head> and branch on its EXIT CODE (do not conflate error with "not an ancestor"): exit 0 → still an ancestor, normal resume; exit 1 → the branch no longer contains fix_base_sha (Renovate force-rebased away the fixer's commits), so treat this as a clobber — a fresh start AND reset dispatches to 0; exit 2 or higher → a git error (e.g. a missing object), NOT a clobber — increment the persisted dispatches count; if dispatches >= fix_attempts, park renovator:parked (reason "could not fetch/inspect the branch after repeated attempts"); otherwise skip this PR this pass and retry next (do not clear fix_base_sha).attempt):
fix_base_sha): attempt is 0; set fix_base_sha to the current head SHA (the base before any fixer commit).fix_base_sha set and still an ancestor): attempt is git rev-list --count <fix_base_sha>..<head> — the commits pushed since the loop began.attempt >= fix_attempts (cycles consumed) OR dispatches >= fix_attempts (dispatches made — this catches a fixer that keeps crashing before it can push a commit) → park renovator:parked; reason "attempts exhausted" when attempt-based, or "no progress after repeated fix attempts" when dispatches-based; keep the last summary. Stop.dispatches count (do this now, before dispatching, so an interrupted dispatch still counts against the bound). Set renovator:fixing; upsert the sticky comment with state fixing, fix: <attempt>/<fix_attempts>, fix_base_sha, and dispatches.renovate-ci-fixer or renovate-major-upgrader) with { pr, bump, old_version, new_version, fix_attempts, attempt, fix_loop_path }. fix_loop_path is the absolute path to references/fix-loop.md inside this skill's own directory (this skill's base directory is provided to you when the skill is invoked; join it with references/fix-loop.md) — the fixer reads the shared doctrine from this path, not a bare relative path. Because attempt carries the already-consumed count, the fixer's own bounding (it sums attempt + its new pushes against fix_attempts) keeps the total across dispatches within budget.outcome:
green + bump is major → set renovator:review (a major upgrade NEVER auto-merges); upsert comment "fixed — awaiting human diff review (renovator authored these changes)", include the agent summary and its returned attempts.green + bump is patch/minor: independently derive whether a test was touched — run git diff --name-only <fix_base_sha>..<head> and match each changed path against the repo's test paths (treat as a test if the path contains a test/, tests/, spec/, or __tests__/ segment, or the filename matches *_test.* / *.test.* / *.spec.* / *_spec.*, or the filename begins with test_ (e.g. test_foo.py, common in flat Python repos) or test-). Auto-merge ONLY if BOTH the agent returned touched_tests: false AND the diff shows no test path changed → dispatch renovate-merger exactly as in step 5 (its independent re-verify gates the merge); on merged, remove renovator:* and record fixed-merged.green + bump is patch/minor but EITHER the agent returned touched_tests: true OR the diff shows a test path changed → set renovator:review (do not merge); upsert comment "fixed with test changes — awaiting human diff review", include the agent summary.exhausted → set renovator:parked; upsert comment with summary + "attempts exhausted".needs-human → set renovator:parked; upsert comment: "needs human — reproducible but not safely fixable: " + summary.cannot-reproduce → set renovator:parked; upsert comment: "can't reproduce CI locally — " + summary.renovator:fixing and fix_base_sha in place; the NEXT pass recomputes attempt from the commit count (steps 1–3 of this fix-loop procedure), which accurately reflects cycles actually consumed — do NOT guess an increment. If that recomputed attempt has reached fix_attempts, this procedure's step 4 (the budget gate above, not the top-level Classify step) parks it.Print a compact table — one row per candidate: PR # | title | bump | bucket | outcome (outcome ∈ merged / fixed-merged / review / fixing / parked / skipped / rebasing / locked). This is the human's after-action view of the pass.
Two layers per PR:
Labels — mutually exclusive: exactly one of renovator:working / renovator:skipped / renovator:parked / renovator:fixing / renovator:review at a time. Before adding one, remove the others:
gh pr edit <n> --add-label renovator:parked --remove-label renovator:working --remove-label renovator:skipped --remove-label renovator:fixing --remove-label renovator:review
Sticky comment — exactly ONE renovator comment per PR, found by the hidden marker <!-- renovator-state --> on its first line. Upsert it:
gh pr view <n> --json comments --jq '.comments[] | select(.body | startswith("<!-- renovator-state -->")) | .url' (empty ⇒ none yet).gh pr comment <n> --body "<body>".url (…#issuecomment-123456), NOT the .id field from --json comments (that is a GraphQL node id and will not work). Extract it from the url and PATCH:
url=$(gh pr view <n> --json comments --jq '.comments[] | select(.body | startswith("<!-- renovator-state -->")) | .url' | head -n1)
id=${url##*issuecomment-}
gh api -X PATCH repos/{owner}/{repo}/issues/comments/"$id" -f body="<body>"
Never post a second renovator comment.
Comment <body> template (first line is the marker):
<!-- renovator-state -->
**renovator** · state: `<working|skipped|parked|fixing|review>` · <reason>
- update: `<dep>` <old> → <new> (<bump>)
- CI: <last conclusion>
- fix: `<attempt>/<fix_attempts>` · base `<short fix_base_sha>` · dispatches `<dispatches>`
- summary: <agent summary, when parked/review>
- updated: <UTC timestamp>
Timestamp via date -u +%Y-%m-%dT%H:%M:%SZ (portable; avoid the GNU-only -Iseconds date flag).
Idempotent by construction: every pass sets labels to the target state and upserts the one comment, so re-running never duplicates.
Each invocation is one full pass. PENDING/rebasing/parked PRs resolve over subsequent passes as Renovate rebases and CI re-runs. No state persists outside the PRs themselves (labels + the one comment).
max_merges_per_pass per pass; never run more than one fix-loop per pass.MAJOR upgrade or any fix that edited a test — those go to renovator:review for a human.fix_attempts push→CI cycles on a PR — park instead.renovate_authors.Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
npx claudepluginhub stebennett/nyx-claude --plugin devtools