From 3300dfb3e9f1b318bd6cd1e37a55c06f9a0f8949 Mon Sep 17 00:00:00 2001 From: kristofferR Date: Mon, 27 Jul 2026 20:34:51 +0200 Subject: [PATCH 1/2] Rename the drain to autofix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Drain" was doing three unrelated jobs — the installer, the per-repository permission to write code, and the fix-before-review invariant — while the same word, correctly, also meant "consume the shared REST quota" on adjacent lines. Nothing in the chain but the name was ever called anything other than fix: the action is `fix`, the report says `fix`, the session is a fix session. The feature is now autofix, which pairs with autoreview: one keeps open pull requests reviewed, the other keeps them fixed. crq drain install -> crq autofix install crq drain [on|off|default] crq autofix [on|off|default] A hard break, with no compatibility path. The persisted keys move with it — drain -> autofix, drains -> autofix_by_host, repo_drain -> repo_autofix — so a pre-rename binary sharing the state ref sees no autofix switches and no dispatch health. Every host has to be upgraded together. The installed artifacts move too: crq-autofix, crq-autofix.service, no.kristofferr.crq-autofix, autofix.log/autofix.err. The invariant is fix-first now, and findings are cleared rather than drained. "Drain" survives only where it means draining a resource or a stream: the REST quota, a child's stdout, git's credential-helper lines. Three fixes the preflight surfaced in the files this touches: - the install plan took its repository list straight off a map, so a re-install rewrote the unit in a new order and --dry-run described a different service than the install that followed it; - Environment= assignments were not percent-escaped, and systemd expands specifiers there, so a '%' in a path or a review command was eaten; - the repository key trimmed ".git" before lowercasing, so a ".GIT" suffix survived into the key and matched nothing. --- README.md | 12 +- cmd/crq/main.go | 85 ++++++------ examples/dispatch/README.md | 10 +- internal/crq/{drain.go => autofix.go} | 126 +++++++++--------- .../crq/{drain_test.go => autofix_test.go} | 112 +++++++++------- .../crq/{drainswitch.go => autofixswitch.go} | 38 +++--- ...inswitch_test.go => autofixswitch_test.go} | 36 ++--- internal/crq/claimrace_test.go | 8 +- internal/crq/config.go | 4 +- internal/crq/config_test.go | 2 +- internal/crq/dismiss.go | 6 +- internal/crq/dispatch/fix-prompt.txt | 2 +- internal/crq/feedback.go | 2 +- internal/crq/feedback_test.go | 6 +- internal/crq/next.go | 10 +- internal/crq/next_test.go | 16 +-- internal/crq/service.go | 4 +- internal/crq/skipmarker_test.go | 2 +- internal/crq/state.go | 8 +- internal/crq/watch.go | 26 ++-- internal/crq/watch_test.go | 32 ++--- internal/engine/next.go | 2 +- .../state/{drain_test.go => autofix_test.go} | 58 ++++---- internal/state/autofixswitch.go | 76 +++++++++++ internal/state/dashboard.go | 4 +- internal/state/drainswitch.go | 74 ---------- internal/state/state.go | 94 ++++++------- internal/state/statusline.go | 4 +- llms.txt | 16 +-- skills/coderabbit-queue/SKILL.md | 18 +-- 30 files changed, 461 insertions(+), 432 deletions(-) rename internal/crq/{drain.go => autofix.go} (80%) rename internal/crq/{drain_test.go => autofix_test.go} (78%) rename internal/crq/{drainswitch.go => autofixswitch.go} (65%) rename internal/crq/{drainswitch_test.go => autofixswitch_test.go} (74%) rename internal/state/{drain_test.go => autofix_test.go} (62%) create mode 100644 internal/state/autofixswitch.go delete mode 100644 internal/state/drainswitch.go diff --git a/README.md b/README.md index c9d9efc6..d2fed2ef 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ Read `.action`, do exactly that, call it again. | `done` | converged | | `blocked` | needs a human; `.reason` says why | -Every rule this used to spell out is now a value crq computes. Drain-before-review is `fix` coming +Every rule this used to spell out is now a value crq computes. Fix-before-review is `fix` coming before everything else. Hold-the-head is the `hold` action, including the exception where a CodeRabbit rate-limit degrade releases the head because the queued review will fire on whatever head exists when the window opens. The wait is `.recheck_after`, derived from the account-quota window, @@ -397,12 +397,12 @@ crq feedback # current normalized findings as JSON, WITHOUT trigger crq threads # every unresolved thread, outdated included crq resolve [...] # resolve addressed review threads crq decline [...] --reason "" [--resolve] # record why a finding is declined -crq drain install # ⭐ unattended: watch every PR and fix what needs fixing -crq watch # what the drain runs: drive open PRs through crq next, one JSON +crq autofix install # ⭐ unattended: watch every PR and fix what needs fixing +crq watch # what autofix runs: drive open PRs through crq next, one JSON # line each. Fixing is ON by default (--no-dispatch observes only) -crq drain # which repositories crq may fix -crq drain off --reason "" # stop fixing there; watching and reviewing continue -crq drain on | crq drain default +crq autofix # which repositories crq may fix +crq autofix off --reason "" # stop fixing there; watching and reviewing continue +crq autofix on | crq autofix default crq hold --reason "" # persistently stop reviews for a PR crq unhold # resume reviews for a held PR crq hold # list held PRs diff --git a/cmd/crq/main.go b/cmd/crq/main.go index 72543950..2c4a4b25 100644 --- a/cmd/crq/main.go +++ b/cmd/crq/main.go @@ -64,20 +64,20 @@ func run(ctx context.Context, args []string) int { return 1 case "preflight": return preflight(ctx, args[1:]) - case "drain": + case "autofix": // A dry run is documented as a PREVIEW: it writes nothing and reads no // GitHub state. Deciding it here, before the authenticated client is // built, is what lets somebody inspect the setup before finishing it — // otherwise the one command for looking at the plan was itself another // thing to set up first. A real install still goes through the // authenticated path below. - if opts, perr := parseDrainArgs(args[1:]); perr == nil && opts.dryRun { + if opts, perr := parseAutofixArgs(args[1:]); perr == nil && opts.dryRun { cfg, cerr := crq.LoadConfig() if cerr != nil { fatal(cerr) return 1 } - plan, ierr := crq.DrainPlan(cfg, opts.agent, crq.SplitArgv(opts.agentArgs), opts.repos, true) + plan, ierr := crq.AutofixPlan(cfg, opts.agent, crq.SplitArgv(opts.agentArgs), opts.repos, true) if ierr != nil { fatal(ierr) return 1 @@ -276,17 +276,17 @@ func run(ctx context.Context, args []string) int { } printJSON(result) return 0 - case "drain": - switch sub := drainSubcommand(args[1:]); sub { + case "autofix": + switch sub := autofixSubcommand(args[1:]); sub { case "?": - fatal(fmt.Errorf("unknown drain subcommand %q (try: crq drain, crq drain on|off|default , crq drain install)", args[1])) + fatal(fmt.Errorf("unknown autofix subcommand %q (try: crq autofix, crq autofix on|off|default , crq autofix install)", args[1])) return 1 case "", "list": if err := cfg.RequireState(); err != nil { fatal(err) return 1 } - settings, derr := service.DrainSettings(ctx) + settings, derr := service.AutofixSettings(ctx) if derr != nil { fatal(derr) return 1 @@ -294,9 +294,9 @@ func run(ctx context.Context, args []string) int { printJSON(settings) return 0 case "on", "off", "default": - rest, reason, ok := parseDrainReason(args[2:]) + rest, reason, ok := parseAutofixReason(args[2:]) if !ok || len(rest) != 1 { - fatal(errors.New(`usage: crq drain on|off|default [--reason ""]`)) + fatal(errors.New(`usage: crq autofix on|off|default [--reason ""]`)) return 1 } if err := cfg.RequireState(); err != nil { @@ -304,7 +304,7 @@ func run(ctx context.Context, args []string) int { return 1 } if sub == "default" { - cleared, cerr := service.ClearDrainEnabled(ctx, rest[0]) + cleared, cerr := service.ClearAutofixEnabled(ctx, rest[0]) if cerr != nil { fatal(cerr) return 1 @@ -312,7 +312,7 @@ func run(ctx context.Context, args []string) int { printJSON(map[string]any{"repo": crq.NormalizeRepo(rest[0]), "cleared": cleared, "enabled": true, "default": true}) return 0 } - setting, serr := service.SetDrainEnabled(ctx, rest[0], sub == "on", reason) + setting, serr := service.SetAutofixEnabled(ctx, rest[0], sub == "on", reason) if serr != nil { fatal(serr) return 1 @@ -320,7 +320,7 @@ func run(ctx context.Context, args []string) int { printJSON(setting) return 0 } - opts, perr := parseDrainArgs(args[1:]) + opts, perr := parseAutofixArgs(args[1:]) if perr != nil { fatal(perr) return 1 @@ -329,7 +329,7 @@ func run(ctx context.Context, args []string) int { fatal(err) return 1 } - plan, ierr := service.InstallDrain(ctx, opts.agent, crq.SplitArgv(opts.agentArgs), opts.repos, opts.dryRun) + plan, ierr := service.InstallAutofix(ctx, opts.agent, crq.SplitArgv(opts.agentArgs), opts.repos, opts.dryRun) if ierr != nil { fatal(ierr) return 1 @@ -626,9 +626,10 @@ USAGE crq decline [...] --reason "" [--keep-open] reply on a thread to record why a finding is declined (resolves it; --keep-open leaves it open) - crq drain install [--agent ] [--dry-run] [...] - crq drain [on|off|default ] which repositories crq may fix (on by default) - install and start the unattended review drain + crq autofix install [--agent ] [--dry-run] [...] + install and start unattended autofix + crq autofix [on|off|default ] + which repositories crq may fix (on by default) crq watch [--no-dispatch] [--once] [...] [-- ] drive open PRs through crq next; --dispatch starts a session to fix the ones that need it @@ -731,7 +732,7 @@ more of the shared budget — run the daemon. Review round primitive for humans and agents. crq coordinates the review trigger, waits for real feedback on the current PR head, and emits one JSON report to stdout. -It returns unresolved findings before queueing a new round, so agents must drain +It returns unresolved findings before queueing a new round, so agents must clear current feedback before waiting for another review. Exit codes: @@ -797,17 +798,17 @@ replies contesting the decline, crq re-surfaces that reply as its own finding. Pass --keep-open to leave it unresolved anyway (an on-the-record disagreement you intend to keep working). Thread IDs come from .findings[].thread_id. `) - case "drain": - fmt.Print(`crq drain install [--agent ] [--dry-run] [...] -crq drain (which repositories crq may fix) -crq drain off [--reason ""] -crq drain on -crq drain default (back to the fleet default, which is on) - -Install and start the unattended review drain: crq watches every open PR and + case "autofix": + fmt.Print(`crq autofix install [--agent ] [--dry-run] [...] +crq autofix (which repositories crq may fix) +crq autofix off [--reason ""] +crq autofix on +crq autofix default (back to the fleet default, which is on) + +Install and start unattended autofix: crq watches every open PR and starts a fix session for the ones that need one. -Draining is ON for every repository in scope. That is the point of watching: a +Autofix is ON for every repository in scope. That is the point of watching: a pull request nobody fixes is a queue that reports work and does none. Turn it off where you do not want crq writing code — a release branch, a repository you are hand-tuning — and watching continues there regardless, so reviews still arrive @@ -841,7 +842,7 @@ line per PR per pass, and start a fix session for each PR whose action is "fix". Fixing is the default — watching a pull request nobody fixes is a queue that reports work and does none. Three things turn it off: --no-dispatch for one run, -"crq drain off " for one repository, and having no fix command configured +"crq autofix off " for one repository, and having no fix command configured at all, which observes and says so rather than refusing to start. A dispatched session runs in a worktree crq checked out at that head, with: @@ -969,7 +970,7 @@ stops blocking the round. crq resolve and crq decline both act on a thread. A review-body finding, a review-skipped notice or an outside-diff remark has none, so neither command can -touch it — and a finding that can never drain blocks every future round, leaving +touch it — and a finding that can never be cleared blocks every future round, leaving a PR whose current head no review was ever requested for. Finding IDs come from .findings[].id. They are content-derived, not GitHub node @@ -1776,18 +1777,18 @@ func configPath() string { return home + "/.config/crq/env" } -// drainArgs is `crq drain install`'s parsed command line. -type drainArgs struct { +// autofixArgs is `crq autofix install`'s parsed command line. +type autofixArgs struct { agent string agentArgs string dryRun bool repos []string } -// parseDrainArgs is shared by the pre-authentication dry-run path and the +// parseAutofixArgs is shared by the pre-authentication dry-run path and the // install itself, so the two cannot disagree about what was asked for. -func parseDrainArgs(args []string) (drainArgs, error) { - fs := flag.NewFlagSet("drain", flag.ContinueOnError) +func parseAutofixArgs(args []string) (autofixArgs, error) { + fs := flag.NewFlagSet("autofix", flag.ContinueOnError) fs.SetOutput(os.Stderr) agent := fs.String("agent", "", "fix agent to run: claude or codex (default: claude on PATH)") agentArgs := fs.String("agent-args", "", "extra flags for the agent, e.g. model and reasoning effort") @@ -1797,22 +1798,22 @@ func parseDrainArgs(args []string) (drainArgs, error) { sub, rest = rest[0], rest[1:] } if err := fs.Parse(rest); err != nil { - return drainArgs{}, err + return autofixArgs{}, err } if sub != "install" { - return drainArgs{}, errors.New("usage: crq drain install [--agent ] [--dry-run] [...]") + return autofixArgs{}, errors.New("usage: crq autofix install [--agent ] [--dry-run] [...]") } - return drainArgs{agent: *agent, agentArgs: *agentArgs, dryRun: *dryRun, repos: fs.Args()}, nil + return autofixArgs{agent: *agent, agentArgs: *agentArgs, dryRun: *dryRun, repos: fs.Args()}, nil } -// drainSubcommand names what `crq drain ...` was asked to do. An empty string +// autofixSubcommand names what `crq autofix ...` was asked to do. An empty string // means the bare command, which lists; "?" means something this command does not // have. // -// A typo must not list. `crq drain of owner/name` reading as the bare listing -// would report the repository as drained and leave it drained — an answer to a +// A typo must not list. `crq autofix of owner/name` reading as the bare listing +// would report the repository as fixable and leave it fixable — an answer to a // question nobody asked, in place of the instruction that was meant. -func drainSubcommand(args []string) string { +func autofixSubcommand(args []string) string { if len(args) == 0 { return "" } @@ -1823,10 +1824,10 @@ func drainSubcommand(args []string) string { return "?" } -// parseDrainReason splits `` from an optional --reason value. An +// parseAutofixReason splits `` from an optional --reason value. An // unrecognized flag is an error rather than a positional: a typo like --resaon // must fail loudly, not silently become the repository name. -func parseDrainReason(args []string) (rest []string, reason string, ok bool) { +func parseAutofixReason(args []string) (rest []string, reason string, ok bool) { for i := 0; i < len(args); i++ { switch arg := args[i]; { case arg == "--reason": diff --git a/examples/dispatch/README.md b/examples/dispatch/README.md index edc73b92..cb37efe0 100644 --- a/examples/dispatch/README.md +++ b/examples/dispatch/README.md @@ -1,13 +1,13 @@ -# Unattended review drain +# Unattended autofix `crq watch --dispatch` turns "this PR needs fixing" into a session that fixes it. ```bash -crq drain install # prompt + wrapper + service, started -crq drain install --dry-run # print what it would write first +crq autofix install # prompt + wrapper + service, started +crq autofix install --dry-run # print what it would write first # a different agent, and its own model/effort settings -crq drain install --agent codex --agent-args '-c model_reasoning_effort=high' +crq autofix install --agent codex --agent-args '-c model_reasoning_effort=high' ``` crq knows how to CALL claude and codex, and nothing about which model either @@ -45,6 +45,6 @@ somebody else actively holds stops a session now. - `$CRQ_WORKSPACE/logs///--