Skip to content

Commit 8ba5422

Browse files
cpcloudclaude
andauthored
Add auto_close_passing_reviews config option (#595)
## Summary - Add `auto_close_passing_reviews` config option (global and per-repo) that automatically marks reviews as closed when they pass with no findings - Per-repo `.roborev.toml` overrides global `~/.roborev/config.toml` using the standard `resolveBool` chain - Auto-close logic in the worker is gated on `IsReviewJob()` so it only fires for review/range/dirty jobs, not task/insights/compact/fix - Tests cover enabled/disabled toggle, per-repo override, and a regression test for non-review job types 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 269b7e2 commit 8ba5422

4 files changed

Lines changed: 146 additions & 0 deletions

File tree

internal/config/config.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ type Config struct {
165165
// Analysis settings
166166
DefaultMaxPromptSize int `toml:"default_max_prompt_size"` // Max prompt size in bytes before falling back to paths (default: 200KB)
167167

168+
// Behavior
169+
AutoClosePassingReviews bool `toml:"auto_close_passing_reviews" comment:"Automatically close reviews that pass with no findings."`
170+
168171
// UI preferences
169172
HideClosedByDefault bool `toml:"hide_closed_by_default" comment:"Hide closed reviews by default in the TUI queue."`
170173
HideAddressedByDefault bool `toml:"hide_addressed_by_default"` // deprecated: use hide_closed_by_default
@@ -674,6 +677,9 @@ type RepoConfig struct {
674677
SecurityBackupModel string `toml:"security_backup_model" comment:"Backup model for security review in this repo."`
675678
DesignBackupModel string `toml:"design_backup_model" comment:"Backup model for design review in this repo."`
676679

680+
// Behavior
681+
AutoClosePassingReviews *bool `toml:"auto_close_passing_reviews" comment:"Automatically close reviews that pass with no findings in this repo."`
682+
677683
// Hooks configuration (per-repo)
678684
Hooks []HookConfig `toml:"hooks"`
679685

@@ -1118,6 +1124,20 @@ func ResolveJobTimeout(repoPath string, globalCfg *Config) int {
11181124
return resolve(30, repoVal, globalVal)
11191125
}
11201126

1127+
// ResolveAutoClosePassingReviews returns whether passing reviews should
1128+
// be automatically closed. Per-repo config overrides global.
1129+
func ResolveAutoClosePassingReviews(repoPath string, globalCfg *Config) bool {
1130+
var repoVal *bool
1131+
if repoCfg, err := LoadRepoConfig(repoPath); err == nil && repoCfg != nil {
1132+
repoVal = repoCfg.AutoClosePassingReviews
1133+
}
1134+
var globalVal bool
1135+
if globalCfg != nil {
1136+
globalVal = globalCfg.AutoClosePassingReviews
1137+
}
1138+
return resolveBool(globalVal, repoVal)
1139+
}
1140+
11211141
// ResolveExcludePatterns returns the merged exclude patterns from
11221142
// repo config and global config. Repo patterns are read from the
11231143
// default branch (like review guidelines) to prevent untrusted

internal/config/config_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,45 @@ func TestResolveJobTimeout(t *testing.T) {
347347
}
348348
}
349349

350+
func TestResolveAutoClosePassingReviews(t *testing.T) {
351+
tests := []struct {
352+
name string
353+
repoConfig string
354+
globalConfig *Config
355+
want bool
356+
}{
357+
{
358+
name: "default false",
359+
want: false,
360+
},
361+
{
362+
name: "global enabled",
363+
globalConfig: &Config{AutoClosePassingReviews: true},
364+
want: true,
365+
},
366+
{
367+
name: "repo overrides global to true",
368+
repoConfig: `auto_close_passing_reviews = true`,
369+
globalConfig: &Config{AutoClosePassingReviews: false},
370+
want: true,
371+
},
372+
{
373+
name: "repo overrides global to false",
374+
repoConfig: `auto_close_passing_reviews = false`,
375+
globalConfig: &Config{AutoClosePassingReviews: true},
376+
want: false,
377+
},
378+
}
379+
380+
for _, tt := range tests {
381+
t.Run(tt.name, func(t *testing.T) {
382+
tmpDir := newTempRepo(t, tt.repoConfig)
383+
got := ResolveAutoClosePassingReviews(tmpDir, tt.globalConfig)
384+
assert.Equal(t, tt.want, got)
385+
})
386+
}
387+
}
388+
350389
func TestResolveReasoning(t *testing.T) {
351390
type resolverFunc func(explicit string, dir string) (string, error)
352391

internal/daemon/worker.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,16 @@ func (wp *WorkerPool) processJob(workerID string, job *storage.ReviewJob) {
610610
}
611611
}
612612

613+
// Auto-close passing reviews when configured.
614+
if job.IsReviewJob() && storage.ParseVerdict(output) == "P" {
615+
cfg := wp.cfgGetter.Config()
616+
if config.ResolveAutoClosePassingReviews(job.RepoPath, cfg) {
617+
if err := wp.db.MarkReviewClosedByJobID(job.ID, true); err != nil {
618+
log.Printf("[%s] Warning: auto-close passing review for job %d: %v", workerID, job.ID, err)
619+
}
620+
}
621+
}
622+
613623
// Fetch token usage from agentsview (best-effort).
614624
// Only collect for fresh sessions (where we captured a new session ID).
615625
// Resumed sessions report cumulative totals across all turns, which

internal/daemon/worker_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,3 +1156,80 @@ func TestFailOrRetryInner_RetryExhaustedPassesBackupModel(t *testing.T) {
11561156
}, "model=%q, want backup-model", updated.Model)
11571157
}
11581158
}
1159+
1160+
func TestAutoClosePassingReviews(t *testing.T) {
1161+
t.Parallel()
1162+
1163+
// Register a test agent whose output parses as a clear pass verdict.
1164+
const passAgentName = "auto-close-pass-agent"
1165+
agent.Register(&agent.FakeAgent{
1166+
NameStr: passAgentName,
1167+
ReviewFn: func(_ context.Context, _, _, _ string, w io.Writer) (string, error) {
1168+
out := "No issues found."
1169+
_, _ = w.Write([]byte(out))
1170+
return out, nil
1171+
},
1172+
})
1173+
t.Cleanup(func() { agent.Unregister(passAgentName) })
1174+
1175+
tests := []struct {
1176+
name string
1177+
enabled bool
1178+
wantClosed bool
1179+
}{
1180+
{"enabled", true, true},
1181+
{"disabled", false, false},
1182+
}
1183+
1184+
for _, tt := range tests {
1185+
t.Run(tt.name, func(t *testing.T) {
1186+
t.Parallel()
1187+
tc := newWorkerTestContext(t, 1)
1188+
cfg := config.DefaultConfig()
1189+
cfg.AutoClosePassingReviews = tt.enabled
1190+
tc.reconfigurePool(cfg)
1191+
1192+
sha := testutil.GetHeadSHA(t, tc.TmpDir)
1193+
job := tc.createAndClaimJobWithAgent(t, sha, testWorkerID, passAgentName)
1194+
1195+
tc.Pool.processJob(testWorkerID, job)
1196+
1197+
tc.assertJobStatus(t, job.ID, storage.JobStatusDone)
1198+
review, err := tc.DB.GetReviewByJobID(job.ID)
1199+
require.NoError(t, err)
1200+
assert.Equal(t, tt.wantClosed, review.Closed)
1201+
})
1202+
}
1203+
1204+
// Non-review job types must never be auto-closed, even with the setting enabled.
1205+
t.Run("skips_non_review_jobs", func(t *testing.T) {
1206+
t.Parallel()
1207+
tc := newWorkerTestContext(t, 1)
1208+
cfg := config.DefaultConfig()
1209+
cfg.AutoClosePassingReviews = true
1210+
tc.reconfigurePool(cfg)
1211+
1212+
sha := testutil.GetHeadSHA(t, tc.TmpDir)
1213+
commit, err := tc.DB.GetOrCreateCommit(tc.Repo.ID, sha, "Author", "Subject", time.Now())
1214+
require.NoError(t, err)
1215+
job, err := tc.DB.EnqueueJob(storage.EnqueueOpts{
1216+
RepoID: tc.Repo.ID,
1217+
CommitID: commit.ID,
1218+
GitRef: sha,
1219+
Agent: passAgentName,
1220+
JobType: "task",
1221+
Prompt: "test prompt",
1222+
})
1223+
require.NoError(t, err)
1224+
claimed, err := tc.DB.ClaimJob(testWorkerID)
1225+
require.NoError(t, err)
1226+
require.Equal(t, job.ID, claimed.ID)
1227+
1228+
tc.Pool.processJob(testWorkerID, claimed)
1229+
1230+
tc.assertJobStatus(t, job.ID, storage.JobStatusDone)
1231+
review, err := tc.DB.GetReviewByJobID(job.ID)
1232+
require.NoError(t, err)
1233+
assert.False(t, review.Closed, "task job should not be auto-closed")
1234+
})
1235+
}

0 commit comments

Comments
 (0)