Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ type Config struct {
// Analysis settings
DefaultMaxPromptSize int `toml:"default_max_prompt_size"` // Max prompt size in bytes before falling back to paths (default: 200KB)

// Behavior
AutoClosePassingReviews bool `toml:"auto_close_passing_reviews" comment:"Automatically close reviews that pass with no findings."`

// UI preferences
HideClosedByDefault bool `toml:"hide_closed_by_default" comment:"Hide closed reviews by default in the TUI queue."`
HideAddressedByDefault bool `toml:"hide_addressed_by_default"` // deprecated: use hide_closed_by_default
Expand Down Expand Up @@ -673,6 +676,9 @@ type RepoConfig struct {
SecurityBackupModel string `toml:"security_backup_model" comment:"Backup model for security review in this repo."`
DesignBackupModel string `toml:"design_backup_model" comment:"Backup model for design review in this repo."`

// Behavior
AutoClosePassingReviews *bool `toml:"auto_close_passing_reviews" comment:"Automatically close reviews that pass with no findings in this repo."`

// Hooks configuration (per-repo)
Hooks []HookConfig `toml:"hooks"`

Expand Down Expand Up @@ -1117,6 +1123,20 @@ func ResolveJobTimeout(repoPath string, globalCfg *Config) int {
return resolve(30, repoVal, globalVal)
}

// ResolveAutoClosePassingReviews returns whether passing reviews should
// be automatically closed. Per-repo config overrides global.
func ResolveAutoClosePassingReviews(repoPath string, globalCfg *Config) bool {
var repoVal *bool
if repoCfg, err := LoadRepoConfig(repoPath); err == nil && repoCfg != nil {
repoVal = repoCfg.AutoClosePassingReviews
}
var globalVal bool
if globalCfg != nil {
globalVal = globalCfg.AutoClosePassingReviews
}
return resolveBool(globalVal, repoVal)
}

// ResolveExcludePatterns returns the merged exclude patterns from
// repo config and global config. Repo patterns are read from the
// default branch (like review guidelines) to prevent untrusted
Expand Down
39 changes: 39 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,45 @@ func TestResolveJobTimeout(t *testing.T) {
}
}

func TestResolveAutoClosePassingReviews(t *testing.T) {
tests := []struct {
name string
repoConfig string
globalConfig *Config
want bool
}{
{
name: "default false",
want: false,
},
{
name: "global enabled",
globalConfig: &Config{AutoClosePassingReviews: true},
want: true,
},
{
name: "repo overrides global to true",
repoConfig: `auto_close_passing_reviews = true`,
globalConfig: &Config{AutoClosePassingReviews: false},
want: true,
},
{
name: "repo overrides global to false",
repoConfig: `auto_close_passing_reviews = false`,
globalConfig: &Config{AutoClosePassingReviews: true},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := newTempRepo(t, tt.repoConfig)
got := ResolveAutoClosePassingReviews(tmpDir, tt.globalConfig)
assert.Equal(t, tt.want, got)
})
}
}

func TestResolveReasoning(t *testing.T) {
type resolverFunc func(explicit string, dir string) (string, error)

Expand Down
10 changes: 10 additions & 0 deletions internal/daemon/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,16 @@ func (wp *WorkerPool) processJob(workerID string, job *storage.ReviewJob) {
}
}

// Auto-close passing reviews when configured.
if job.IsReviewJob() && storage.ParseVerdict(output) == "P" {
cfg := wp.cfgGetter.Config()
if config.ResolveAutoClosePassingReviews(job.RepoPath, cfg) {
if err := wp.db.MarkReviewClosedByJobID(job.ID, true); err != nil {
log.Printf("[%s] Warning: auto-close passing review for job %d: %v", workerID, job.ID, err)
}
}
}

// Fetch token usage from agentsview (best-effort).
// Only collect for fresh sessions (where we captured a new session ID).
// Resumed sessions report cumulative totals across all turns, which
Expand Down
77 changes: 77 additions & 0 deletions internal/daemon/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1156,3 +1156,80 @@ func TestFailOrRetryInner_RetryExhaustedPassesBackupModel(t *testing.T) {
}, "model=%q, want backup-model", updated.Model)
}
}

func TestAutoClosePassingReviews(t *testing.T) {
t.Parallel()

// Register a test agent whose output parses as a clear pass verdict.
const passAgentName = "auto-close-pass-agent"
agent.Register(&agent.FakeAgent{
NameStr: passAgentName,
ReviewFn: func(_ context.Context, _, _, _ string, w io.Writer) (string, error) {
out := "No issues found."
_, _ = w.Write([]byte(out))
return out, nil
},
})
t.Cleanup(func() { agent.Unregister(passAgentName) })

tests := []struct {
name string
enabled bool
wantClosed bool
}{
{"enabled", true, true},
{"disabled", false, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tc := newWorkerTestContext(t, 1)
cfg := config.DefaultConfig()
cfg.AutoClosePassingReviews = tt.enabled
tc.reconfigurePool(cfg)

sha := testutil.GetHeadSHA(t, tc.TmpDir)
job := tc.createAndClaimJobWithAgent(t, sha, testWorkerID, passAgentName)

tc.Pool.processJob(testWorkerID, job)

tc.assertJobStatus(t, job.ID, storage.JobStatusDone)
review, err := tc.DB.GetReviewByJobID(job.ID)
require.NoError(t, err)
assert.Equal(t, tt.wantClosed, review.Closed)
})
}

// Non-review job types must never be auto-closed, even with the setting enabled.
t.Run("skips_non_review_jobs", func(t *testing.T) {
t.Parallel()
tc := newWorkerTestContext(t, 1)
cfg := config.DefaultConfig()
cfg.AutoClosePassingReviews = true
tc.reconfigurePool(cfg)

sha := testutil.GetHeadSHA(t, tc.TmpDir)
commit, err := tc.DB.GetOrCreateCommit(tc.Repo.ID, sha, "Author", "Subject", time.Now())
require.NoError(t, err)
job, err := tc.DB.EnqueueJob(storage.EnqueueOpts{
RepoID: tc.Repo.ID,
CommitID: commit.ID,
GitRef: sha,
Agent: passAgentName,
JobType: "task",
Prompt: "test prompt",
})
require.NoError(t, err)
claimed, err := tc.DB.ClaimJob(testWorkerID)
require.NoError(t, err)
require.Equal(t, job.ID, claimed.ID)

tc.Pool.processJob(testWorkerID, claimed)

tc.assertJobStatus(t, job.ID, storage.JobStatusDone)
review, err := tc.DB.GetReviewByJobID(job.ID)
require.NoError(t, err)
assert.False(t, review.Closed, "task job should not be auto-closed")
})
}
Loading