Skip to content

Commit 14d6cb4

Browse files
wesmclaude
andauthored
Improve robustness of roborev refine, get working with Claude Code (#61)
`claude` required a lot of twiddling to get working, whereas Codex was fairly straightforward. Even if you have ANTHROPIC_API_KEY set in your environment, roborev refine won't use it unless you set it in `config.toml` to avoid surprise bills (it will use your subscription if available, otherwise, you must opt in to API billing). --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 12bf9d6 commit 14d6cb4

File tree

10 files changed

+456
-45
lines changed

10 files changed

+456
-45
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,19 @@ Override the data directory with `ROBOREV_DATA_DIR`:
236236
export ROBOREV_DATA_DIR=/custom/path # Default: ~/.roborev
237237
```
238238

239+
### Authentication
240+
241+
**Claude Code** uses your Claude subscription by default. roborev deliberately ignores `ANTHROPIC_API_KEY` from the environment to avoid unexpected API charges.
242+
243+
To use Anthropic API credits instead:
244+
245+
```toml
246+
# ~/.roborev/config.toml
247+
anthropic_api_key = "sk-ant-..."
248+
```
249+
250+
**Codex** uses the authentication configured in your Codex installation.
251+
239252
### Priority Order
240253

241254
1. CLI flags (`--agent`, `--reasoning`)

cmd/roborev/main_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ func TestRefineLoopFindFailedReviewPath(t *testing.T) {
699699
}
700700

701701
commits := []string{"commit1sha", "commit2sha", "commit3sha"}
702-
review, err := findFailedReviewForBranch(client, commits)
702+
review, err := findFailedReviewForBranch(client, commits, nil)
703703

704704
if err != nil {
705705
t.Fatalf("unexpected error: %v", err)
@@ -721,7 +721,7 @@ func TestRefineLoopFindFailedReviewPath(t *testing.T) {
721721
}
722722

723723
commits := []string{"commit1sha"}
724-
review, err := findFailedReviewForBranch(client, commits)
724+
review, err := findFailedReviewForBranch(client, commits, nil)
725725

726726
if err != nil {
727727
t.Fatalf("unexpected error: %v", err)
@@ -836,7 +836,7 @@ func TestRefineLoopBranchReviewPath(t *testing.T) {
836836
}
837837

838838
commits := []string{"commit1", "commit2"}
839-
review, err := findFailedReviewForBranch(client, commits)
839+
review, err := findFailedReviewForBranch(client, commits, nil)
840840

841841
// Should return nil since all pass
842842
if err != nil {

cmd/roborev/refine.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ func runRefine(agentName, reasoningStr string, maxIterations int, quiet bool, al
209209
resolvedAgent := config.ResolveAgent(agentName, repoPath, cfg)
210210
allowUnsafeAgents = resolveAllowUnsafeAgents(allowUnsafeAgents, unsafeFlagChanged, cfg)
211211
agent.SetAllowUnsafeAgents(allowUnsafeAgents)
212+
if cfg != nil {
213+
agent.SetAnthropicAPIKey(cfg.AnthropicAPIKey)
214+
}
212215

213216
// Resolve reasoning level from CLI or config (default: fast)
214217
resolvedReasoning, err := config.ResolveRefineReasoning(reasoningStr, repoPath)
@@ -228,6 +231,8 @@ func runRefine(agentName, reasoningStr string, maxIterations int, quiet bool, al
228231
// Track current failed review - when a fix fails, we continue fixing it
229232
// before moving on to the next oldest failed commit
230233
var currentFailedReview *storage.Review
234+
// Track reviews we've given up on this run to avoid re-selecting them
235+
skippedReviews := make(map[int64]bool)
231236

232237
for iteration := 1; iteration <= maxIterations; {
233238
// Get commits on current branch
@@ -244,7 +249,7 @@ func runRefine(agentName, reasoningStr string, maxIterations int, quiet bool, al
244249
// Only search for a new failed review if we don't have one to work on
245250
// (either first iteration, or previous fix passed)
246251
if currentFailedReview == nil {
247-
currentFailedReview, err = findFailedReviewForBranch(client, commits)
252+
currentFailedReview, err = findFailedReviewForBranch(client, commits, skippedReviews)
248253
if err != nil {
249254
return fmt.Errorf("error finding reviews: %w", err)
250255
}
@@ -436,12 +441,11 @@ func runRefine(agentName, reasoningStr string, maxIterations int, quiet bool, al
436441
}
437442
if noChangeAttempts >= 2 {
438443
// Tried 3 times (including this one), give up on this review
439-
fmt.Println("Marking as addressed after multiple failed attempts")
444+
// Do NOT mark as addressed - the review still needs attention
445+
fmt.Println("Giving up after multiple failed attempts (review remains unaddressed)")
440446
client.AddResponse(currentFailedReview.JobID, "roborev-refine", "Agent could not determine how to address findings (attempt 3, giving up)")
441-
if err := client.MarkReviewAddressed(currentFailedReview.ID); err != nil {
442-
fmt.Printf("Warning: failed to mark review %d as addressed: %v\n", currentFailedReview.ID, err)
443-
}
444-
currentFailedReview = nil // Move on to next oldest failed commit
447+
skippedReviews[currentFailedReview.ID] = true // Don't re-select this run
448+
currentFailedReview = nil // Move on to next oldest failed commit
445449
} else {
446450
// Record attempt but don't mark addressed - might work on retry with different context
447451
client.AddResponse(currentFailedReview.JobID, "roborev-refine", fmt.Sprintf("Agent could not determine how to address findings (attempt %d)", noChangeAttempts+1))
@@ -527,7 +531,8 @@ func resolveAllowUnsafeAgents(flag bool, flagChanged bool, cfg *config.Config) b
527531
// findFailedReviewForBranch finds an unaddressed failed review for any of the given commits.
528532
// Iterates oldest to newest so earlier commits are fixed before later ones.
529533
// Passing reviews are marked as addressed automatically.
530-
func findFailedReviewForBranch(client daemon.Client, commits []string) (*storage.Review, error) {
534+
// Reviews in the skip set are ignored (used for reviews we've given up on this run).
535+
func findFailedReviewForBranch(client daemon.Client, commits []string, skip map[int64]bool) (*storage.Review, error) {
531536
// Iterate oldest to newest (commits are in chronological order)
532537
for _, sha := range commits {
533538
review, err := client.GetReviewBySHA(sha)
@@ -543,6 +548,11 @@ func findFailedReviewForBranch(client daemon.Client, commits []string) (*storage
543548
continue
544549
}
545550

551+
// Skip reviews we've given up on this run
552+
if skip[review.ID] {
553+
continue
554+
}
555+
546556
verdict := storage.ParseVerdict(review.Output)
547557
if verdict == "F" {
548558
return review, nil

cmd/roborev/refine_test.go

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func TestFindFailedReviewForBranch_OldestFirst(t *testing.T) {
263263
// Commits in chronological order (oldest first, as returned by git log --reverse)
264264
commits := []string{"oldest123", "middle456", "newest789"}
265265

266-
found, err := findFailedReviewForBranch(client, commits)
266+
found, err := findFailedReviewForBranch(client, commits, nil)
267267
if err != nil {
268268
t.Fatalf("findFailedReviewForBranch failed: %v", err)
269269
}
@@ -289,7 +289,7 @@ func TestFindFailedReviewForBranch_SkipsAddressed(t *testing.T) {
289289

290290
commits := []string{"commit1", "commit2", "commit3"}
291291

292-
found, err := findFailedReviewForBranch(client, commits)
292+
found, err := findFailedReviewForBranch(client, commits, nil)
293293
if err != nil {
294294
t.Fatalf("findFailedReviewForBranch failed: %v", err)
295295
}
@@ -300,6 +300,55 @@ func TestFindFailedReviewForBranch_SkipsAddressed(t *testing.T) {
300300
}
301301
}
302302

303+
func TestFindFailedReviewForBranch_SkipsGivenUpReviews(t *testing.T) {
304+
client := newMockDaemonClient()
305+
306+
client.reviews = map[string]*storage.Review{
307+
"commit1": {ID: 1, JobID: 100, Output: "Bug found."}, // Fail (oldest, but in skip set)
308+
"commit2": {ID: 2, JobID: 200, Output: "Another bug."}, // Fail (should be returned)
309+
"commit3": {ID: 3, JobID: 300, Output: "No issues found."}, // Pass
310+
}
311+
312+
commits := []string{"commit1", "commit2", "commit3"}
313+
314+
// Skip review ID 1 (simulates "giving up" after 3 failed attempts)
315+
skip := map[int64]bool{1: true}
316+
317+
found, err := findFailedReviewForBranch(client, commits, skip)
318+
if err != nil {
319+
t.Fatalf("findFailedReviewForBranch failed: %v", err)
320+
}
321+
322+
// Should return commit2 (job 200), skipping commit1 which is in the skip set
323+
if found == nil || found.JobID != 200 {
324+
t.Errorf("expected job 200 (skipping given-up review), got %v", found)
325+
}
326+
}
327+
328+
func TestFindFailedReviewForBranch_AllSkippedReturnsNil(t *testing.T) {
329+
client := newMockDaemonClient()
330+
331+
client.reviews = map[string]*storage.Review{
332+
"commit1": {ID: 1, JobID: 100, Output: "Bug found."}, // Fail (in skip set)
333+
"commit2": {ID: 2, JobID: 200, Output: "Another."}, // Fail (in skip set)
334+
}
335+
336+
commits := []string{"commit1", "commit2"}
337+
338+
// Skip both reviews (simulates giving up on all failed reviews)
339+
skip := map[int64]bool{1: true, 2: true}
340+
341+
found, err := findFailedReviewForBranch(client, commits, skip)
342+
if err != nil {
343+
t.Fatalf("findFailedReviewForBranch failed: %v", err)
344+
}
345+
346+
// Should return nil since all failures are in skip set
347+
if found != nil {
348+
t.Errorf("expected nil (all skipped), got job %d", found.JobID)
349+
}
350+
}
351+
303352
func TestFindFailedReviewForBranch_AllPass(t *testing.T) {
304353
client := newMockDaemonClient()
305354

@@ -310,7 +359,7 @@ func TestFindFailedReviewForBranch_AllPass(t *testing.T) {
310359

311360
commits := []string{"commit1", "commit2"}
312361

313-
found, err := findFailedReviewForBranch(client, commits)
362+
found, err := findFailedReviewForBranch(client, commits, nil)
314363
if err != nil {
315364
t.Fatalf("findFailedReviewForBranch failed: %v", err)
316365
}
@@ -386,7 +435,7 @@ func TestFindFailedReviewForBranch_NoReviews(t *testing.T) {
386435

387436
commits := []string{"unreviewed1", "unreviewed2"}
388437

389-
found, err := findFailedReviewForBranch(client, commits)
438+
found, err := findFailedReviewForBranch(client, commits, nil)
390439
if err != nil {
391440
t.Fatalf("findFailedReviewForBranch failed: %v", err)
392441
}
@@ -407,7 +456,7 @@ func TestFindFailedReviewForBranch_MarksPassingAsAddressed(t *testing.T) {
407456

408457
commits := []string{"commit1", "commit2"}
409458

410-
found, err := findFailedReviewForBranch(client, commits)
459+
found, err := findFailedReviewForBranch(client, commits, nil)
411460
if err != nil {
412461
t.Fatalf("findFailedReviewForBranch failed: %v", err)
413462
}
@@ -443,7 +492,7 @@ func TestFindFailedReviewForBranch_MarksPassingBeforeFailure(t *testing.T) {
443492

444493
commits := []string{"commit1", "commit2"}
445494

446-
found, err := findFailedReviewForBranch(client, commits)
495+
found, err := findFailedReviewForBranch(client, commits, nil)
447496
if err != nil {
448497
t.Fatalf("findFailedReviewForBranch failed: %v", err)
449498
}
@@ -473,7 +522,7 @@ func TestFindFailedReviewForBranch_DoesNotMarkAlreadyAddressed(t *testing.T) {
473522

474523
commits := []string{"commit1", "commit2"}
475524

476-
found, err := findFailedReviewForBranch(client, commits)
525+
found, err := findFailedReviewForBranch(client, commits, nil)
477526
if err != nil {
478527
t.Fatalf("findFailedReviewForBranch failed: %v", err)
479528
}
@@ -507,7 +556,7 @@ func TestFindFailedReviewForBranch_MixedScenario(t *testing.T) {
507556

508557
commits := []string{"commit1", "commit2", "commit3", "commit4", "commit5"}
509558

510-
found, err := findFailedReviewForBranch(client, commits)
559+
found, err := findFailedReviewForBranch(client, commits, nil)
511560
if err != nil {
512561
t.Fatalf("findFailedReviewForBranch failed: %v", err)
513562
}
@@ -544,7 +593,7 @@ func TestFindFailedReviewForBranch_StopsAtFirstFailure(t *testing.T) {
544593

545594
commits := []string{"commit1", "commit2", "commit3"}
546595

547-
found, err := findFailedReviewForBranch(client, commits)
596+
found, err := findFailedReviewForBranch(client, commits, nil)
548597
if err != nil {
549598
t.Fatalf("findFailedReviewForBranch failed: %v", err)
550599
}
@@ -573,7 +622,7 @@ func TestFindFailedReviewForBranch_MarkAddressedError(t *testing.T) {
573622

574623
commits := []string{"commit1"}
575624

576-
found, err := findFailedReviewForBranch(client, commits)
625+
found, err := findFailedReviewForBranch(client, commits, nil)
577626

578627
// Should return an error and not continue processing
579628
if err == nil {
@@ -598,7 +647,7 @@ func TestFindFailedReviewForBranch_GetReviewBySHAError(t *testing.T) {
598647

599648
commits := []string{"commit1", "commit2"}
600649

601-
found, err := findFailedReviewForBranch(client, commits)
650+
found, err := findFailedReviewForBranch(client, commits, nil)
602651

603652
// Should return an error and not continue processing
604653
if err == nil {

internal/agent/agent.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type CommandAgent interface {
5959
// Registry holds available agents
6060
var registry = make(map[string]Agent)
6161
var allowUnsafeAgents atomic.Bool
62+
var anthropicAPIKey atomic.Value
6263

6364
func AllowUnsafeAgents() bool {
6465
return allowUnsafeAgents.Load()
@@ -68,6 +69,19 @@ func SetAllowUnsafeAgents(allow bool) {
6869
allowUnsafeAgents.Store(allow)
6970
}
7071

72+
// AnthropicAPIKey returns the configured Anthropic API key, or empty string if not set
73+
func AnthropicAPIKey() string {
74+
if v := anthropicAPIKey.Load(); v != nil {
75+
return v.(string)
76+
}
77+
return ""
78+
}
79+
80+
// SetAnthropicAPIKey sets the Anthropic API key for Claude Code
81+
func SetAnthropicAPIKey(key string) {
82+
anthropicAPIKey.Store(key)
83+
}
84+
7185
// aliases maps short names to full agent names
7286
var aliases = map[string]string{
7387
"claude": "claude-code",

0 commit comments

Comments
 (0)