Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
52c359d
Add `roborev wait` command to wait for existing review jobs
jeremyjordan Feb 13, 2026
af99741
Fix wait command: repo scoping, exit code 4, --job validation
jeremyjordan Feb 13, 2026
d57fe0f
Fix wait command: worktree support and sentinel error for job lookup
jeremyjordan Feb 13, 2026
8a5b7be
Fix wait worktree SHA resolution and add tests
jeremyjordan Feb 13, 2026
83d03ad
Add worktree test for wait command SHA/repo resolution
jeremyjordan Feb 13, 2026
00f7280
Style: align wait command code with existing conventions
jeremyjordan Feb 13, 2026
a1e37a4
Add test for wait command lookup error path
jeremyjordan Feb 13, 2026
722e2e7
Strengthen wait lookup error test assertions
jeremyjordan Feb 13, 2026
461060a
Refactor wait command: factor out helpers, add missing tests
jeremyjordan Feb 14, 2026
431b2b4
Simplify wait command exit codes to 0 and 1
jeremyjordan Feb 14, 2026
44fdddd
Remove --timeout flag from wait command
jeremyjordan Feb 15, 2026
39f5308
Refactor job lookup error handling in wait command
jeremyjordan Feb 15, 2026
4631e2a
Validate wait command inputs: reject invalid refs and non-positive jo…
jeremyjordan Feb 15, 2026
d9f4201
Reject non-positive job IDs in positional arg fallback path
jeremyjordan Feb 15, 2026
8d3ad7b
Add test for positional zero being rejected as job ID
jeremyjordan Feb 15, 2026
a5393c9
Reuse findJobForCommit for wait command job lookup
jeremyjordan Feb 15, 2026
ef21b33
Add test for --job polling non-200 response path
jeremyjordan Feb 15, 2026
9681378
Move getDaemonAddr call closer to its use in waitCmd
jeremyjordan Feb 15, 2026
3c30b64
Reduce wait_test.go verbosity with shared fixtures and table-driven t…
wesm Feb 15, 2026
6649373
Move ensureDaemon after local validation in waitCmd
wesm Feb 15, 2026
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
154 changes: 153 additions & 1 deletion cmd/roborev/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func main() {

rootCmd.AddCommand(initCmd())
rootCmd.AddCommand(reviewCmd())
rootCmd.AddCommand(waitCmd())
rootCmd.AddCommand(statusCmd())
rootCmd.AddCommand(listCmd())
rootCmd.AddCommand(showCmd())
Expand Down Expand Up @@ -310,6 +311,9 @@ func startDaemon() error {
// ErrDaemonNotRunning indicates no daemon runtime file was found
var ErrDaemonNotRunning = fmt.Errorf("daemon not running (no runtime file found)")

// ErrJobNotFound indicates a job ID was not found during polling
var ErrJobNotFound = fmt.Errorf("job not found")

// stopDaemon stops any running daemons.
// Returns ErrDaemonNotRunning if no daemon runtime files are found.
func stopDaemon() error {
Expand Down Expand Up @@ -1185,7 +1189,7 @@ func waitForJob(cmd *cobra.Command, serverAddr string, jobID int64, quiet bool)
resp.Body.Close()

if len(jobsResp.Jobs) == 0 {
return fmt.Errorf("job %d not found", jobID)
return fmt.Errorf("%w: %d", ErrJobNotFound, jobID)
}

job := jobsResp.Jobs[0]
Expand Down Expand Up @@ -1290,6 +1294,154 @@ func (e *exitError) Error() string {
return fmt.Sprintf("exit code %d", e.code)
}

func waitCmd() *cobra.Command {
var (
shaFlag string
forceJobID bool
quiet bool
)

cmd := &cobra.Command{
Use: "wait [job_id|sha]",
Short: "Wait for an existing review job to complete",
Long: `Wait for an already-running review job to complete, without enqueuing a new one.

When using an external coding agent to perform a review-fix refinement loop,
wait provides a token-efficient method for letting the agent wait for a roborev
review to complete. The post-commit hook triggers the review, and the agent
calls wait to block until the result is ready.

The argument can be a job ID (numeric) or a git ref (commit SHA, branch, HEAD).
If no argument is given, defaults to HEAD.

Exit codes:
0 Review completed with verdict PASS
1 Any failure (FAIL verdict, no job found, job error)

Examples:
roborev wait # Wait for most recent job for HEAD
roborev wait abc123 # Wait for most recent job for commit
roborev wait 42 # Job ID (if "42" is not a valid git ref)
roborev wait --job 42 # Force as job ID
roborev wait --sha HEAD~1 # Wait for job matching HEAD~1`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// In quiet mode, suppress cobra's error output
if quiet {
cmd.SilenceErrors = true
cmd.SilenceUsage = true
}

// Validate flag/arg combinations
if len(args) > 0 && shaFlag != "" {
return fmt.Errorf("cannot use both a positional argument and --sha")
}
if forceJobID && len(args) == 0 {
return fmt.Errorf("--job requires a job ID argument")
}

// Resolve the target to a job ID (local validation first,
// daemon contact deferred until actually needed)
var jobID int64
var ref string // git ref to resolve via findJobForCommit

if shaFlag != "" {
ref = shaFlag
} else if len(args) > 0 {
arg := args[0]
if forceJobID {
// --job flag: treat as job ID directly
id, err := strconv.ParseInt(arg, 10, 64)
if err != nil || id <= 0 {
return fmt.Errorf("invalid job ID: %s", arg)
}
jobID = id
} else {
// Try to resolve as git ref first (handles numeric SHAs like "123456")
if repoRoot, err := git.GetRepoRoot("."); err == nil {
if _, err := git.ResolveSHA(repoRoot, arg); err == nil {
ref = arg
}
}
if ref == "" {
// Not a valid git ref — try as numeric job ID
if id, err := strconv.ParseInt(arg, 10, 64); err == nil && id > 0 {
jobID = id
} else {
return fmt.Errorf("argument %q is not a valid git ref or job ID", arg)
}
}
}
} else {
ref = "HEAD"
}

// Validate git ref before contacting daemon
var sha string
if ref != "" {
repoRoot, _ := git.GetRepoRoot(".")
resolved, err := git.ResolveSHA(repoRoot, ref)
if err != nil {
return fmt.Errorf("invalid git ref: %s", ref)
}
sha = resolved
}

// All local validation passed — now ensure daemon is running
if err := ensureDaemon(); err != nil {
return fmt.Errorf("daemon not running: %w", err)
}

// If we have a ref to resolve, use findJobForCommit
if sha != "" && jobID == 0 {
mainRoot, _ := git.GetMainRepoRoot(".")
if mainRoot == "" {
mainRoot, _ = git.GetRepoRoot(".")
}
job, err := findJobForCommit(mainRoot, sha)
if err != nil {
return err
}
if job == nil {
if !quiet {
cmd.Printf("No job found for %s\n", ref)
}
cmd.SilenceErrors = true
cmd.SilenceUsage = true
return &exitError{code: 1}
}
jobID = job.ID
}

addr := getDaemonAddr()
err := waitForJob(cmd, addr, jobID, quiet)
if err != nil {
// Map ErrJobNotFound to exit 1 with a user-facing message
// (waitForJob returns a plain error to stay compatible with reviewCmd)
if errors.Is(err, ErrJobNotFound) {
if !quiet {
cmd.Printf("No job found for job %d\n", jobID)
}
cmd.SilenceErrors = true
cmd.SilenceUsage = true
return &exitError{code: 1}
}
if _, isExitErr := err.(*exitError); isExitErr {
cmd.SilenceErrors = true
cmd.SilenceUsage = true
}
}
return err
},
}

cmd.Flags().StringVar(&shaFlag, "sha", "", "git ref to find the most recent job for")
cmd.Flags().BoolVar(&forceJobID, "job", false, "force argument to be treated as job ID")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress output (for use in hooks)")

return cmd
}

func statusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Expand Down
Loading
Loading