Skip to content

feat: best-of-N runs and /yolo mode - #65

Merged
A-x6 merged 6 commits into
devfrom
best-of-n
Jul 31, 2026
Merged

feat: best-of-N runs and /yolo mode#65
A-x6 merged 6 commits into
devfrom
best-of-n

Conversation

@A-x6

@A-x6A-x6 commented Jul 31, 2026

Copy link
Copy Markdown

Issue for this PR

Closes#64

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Implements the roadmap's top "Now" item: bolt run --best-of "anthropic/claude-sonnet-4,openai/gpt-5" "task" fires the same task at every listed model in parallel (each in its own fresh session), then a judge model ranks the anonymized outputs and the winner's text is printed. The judge is --model when given, otherwise the first candidate.

Each candidate runs through the existing blocking session.prompt call, so the whole race is one Promise.all; the judge sees only labeled outputs (no model names) to avoid reputation bias, and its verdict is parsed from a machine-readable trailer:

// Pulls the last RANKING line out of the judge's reply. Returns the label// order, or undefined when the judge did not produce a usable verdict.exportfunctionparseVerdict(text: string,labels: string[]): string[]|undefined{constmatches=[...text.matchAll(/RANKING:\s*([A-Z](?:\s*>\s*[A-Z])*)/gi)]constlast=matches.at(-1)if(!last)returnundefinedconstorder=last[1].split(">").map((item)=>item.trim().toUpperCase())// ... validates it is a complete permutation of the candidate labels}

Design choices a reviewer can't infer from the diff:

  • The winner's text goes to stdout and everything else (progress notes, the ranked scoreboard with per-candidate session IDs) goes to stderr via UI, so --best-of stays pipeable exactly like a plain run. --format json emits one best_of JSON object instead.
  • Every candidate session survives the race: the scoreboard prints each session ID so any candidate (not just the winner) can be resumed with bolt run --session.
  • If the judge fails, times out, or returns an unparseable verdict, the first finished candidate wins with a warning; if only one candidate succeeds the judge is skipped; per-candidate failures don't abort the race. Exit code is 1 only when every candidate fails.
  • --best-of is rejected with --mini, --command, --session, --continue, and --fork (it always runs fresh sessions), capped at 8 models, and works with --attach since it goes through the same SDK client.

New logic lives in packages/opencode/src/cli/cmd/run/best-of.ts (lazily imported); run.ts only grows the flag declaration, validation, and a small branch in execute().

Also adds /yolo to the TUI. The permission auto-approve machinery already existed (permission.mode === "auto" auto-replies to permission.asked in context/sync.tsx before the dialog renders, toggled by /auto-approve); this PR adds yolo as a slash alias of that command and closes the missing half: while yolo mode is on, every prompt carries a system-level instruction so the model stops asking in prose for approval it would get automatically anyway:

system:
local.permission.mode==="auto"
? "Yolo mode is active: every permission request is granted automatically. Do not ask the user for permission or confirmation before acting; proceed directly."
: undefined,

The server appends the per-message system field after the base system prompt, so this is additive per prompt and turns off cleanly the moment the toggle flips back.

How did you verify your code works?

  • bun test ./test/cli/run/best-of.test.ts in packages/opencode: 11 tests covering candidate parsing (dedupe, nested model paths, limits, malformed entries), judge prompt construction, and verdict parsing (case/spacing tolerance, repeated rankings, incomplete or duplicated verdicts)
  • bun test ./test/cli/run/ (full run-command suite): 211 pass
  • bun typecheck in packages/opencode: clean
  • bunx tsgo --noEmit and bun test in packages/tui for the /yolo change: 198 pass

Screenshots / recordings

N/A (CLI; end-to-end output requires live provider credentials)

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

Summary by CodeRabbit

  • New Features

    • Added a --best-of option to compare responses from multiple model candidates.
    • Runs candidates in parallel and selects the strongest response through automated ranking.
    • Supports human-readable and JSON output, with fallback handling when ranking is unavailable.
    • Validates candidate lists and prevents use with incompatible interactive or session modes.
  • Tests

    • Added coverage for candidate parsing, ranking, output selection, and invalid input handling.

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
bolt-cli-appSkippedSkippedJul 31, 2026 5:25am

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI adds --best-of for parallel runs across provider/model candidates. It validates candidates, ranks successful outputs with a judge model, supports JSON and human-readable output, and integrates the flow with existing run modes.

Changes

Best-of-N model runs

Layer / File(s)Summary
Candidate and ranking protocol
packages/opencode/src/cli/cmd/run/best-of.ts, packages/opencode/test/cli/run/best-of.test.ts
The module parses and validates candidates, builds judge prompts, parses ranking verdicts, and tests these behaviors.
Parallel execution and result selection
packages/opencode/src/cli/cmd/run/best-of.ts
Candidate sessions run concurrently. Successful outputs are ranked, failures are reported, and the winner is emitted as human-readable or JSON output.
CLI option and execution wiring
packages/opencode/src/cli/cmd/run.ts
The command adds --best-of, rejects incompatible modes, invokes runBestOf, propagates its exit code, and supplies undefined values from runMini.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant RunCommand
participant runBestOf
participant CandidateSessions
participant JudgeModel
RunCommand->>runBestOf: task and candidate models
runBestOf->>CandidateSessions: run candidate sessions in parallel
CandidateSessions-->>runBestOf: candidate outputs and errors
runBestOf->>JudgeModel: anonymized outputs for ranking
JudgeModel-->>runBestOf: ranking verdict
runBestOf-->>RunCommand: winning output and exit code
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes implement issue #64 by adding concurrent model runs, judge-based ranking, winner output, CLI syntax, stderr scoreboard, and JSON support.
Out of Scope Changes check✅ PassedThe changes are limited to best-of-N CLI wiring, orchestration, and focused tests described in issue #64.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title clearly identifies the main best-of-N feature, although it also mentions /yolo mode not represented in the provided changeset.
Description check✅ PassedThe description completes the required sections, explains the implementation and rationale, documents verification, and marks the checklist items.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch best-of-n

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

The following comment was made by an LLM, it may be inaccurate:

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/opencode/src/cli/cmd/run/best-of.ts (1)

46-49: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove the unnecessary as Model cast.

Session2.prompt already defines this model as { providerID: string; modelID: string }. Let TypeScript infer the object so future shape changes produce a compile-time error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/opencode/src/cli/cmd/run/best-of.ts` around lines 46 - 49, Remove
the unnecessary `as Model` assertion from the object returned by the unique.map
callback in Session2.prompt, allowing TypeScript to infer the `{ providerID,
modelID }` shape and catch future model-shape changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/opencode/src/cli/cmd/run.ts`:
- Around line 682-699: Update the best-of branch’s SDK selection around
runBestOf to resolve the attach directory before calling attachSDK: when
args.attach is set and directory is undefined, obtain the directory via the
existing current(sdk) fallback used by the single-session path. Pass the
resolved directory to attachSDK while preserving the existing SDK behavior for
explicit directories and non-attach runs.
In `@packages/opencode/src/cli/cmd/run/best-of.ts`:
- Around line 163-164: Update the fallback note in the best-of selection flow
around verdict and order to state that the first successful candidate in
submission order is retained, rather than referring to the first finished
candidate. Keep the selection logic unchanged.
- Around line 135-138: Update the total-failure branch guarded by done.length
=== 0 in the best-of command to honor input.json: emit one structured best_of
JSON object containing the failure details instead of relying on UI.error, while
preserving the existing human-readable error output for non-JSON runs and
returning 1.
---
Nitpick comments:
In `@packages/opencode/src/cli/cmd/run/best-of.ts`:
- Around line 46-49: Remove the unnecessary `as Model` assertion from the object
returned by the unique.map callback in Session2.prompt, allowing TypeScript to
infer the `{ providerID, modelID }` shape and catch future model-shape changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e201d89-f5dc-4025-8a67-40ee9f03397f

📥 Commits

Reviewing files that changed from the base of the PR and between 9d17df6 and 352220d.

📒 Files selected for processing (3)
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/cli/cmd/run/best-of.ts
  • packages/opencode/test/cli/run/best-of.test.ts

Comment threadpackages/opencode/src/cli/cmd/run.ts
Comment threadpackages/opencode/src/cli/cmd/run/best-of.ts
Comment threadpackages/opencode/src/cli/cmd/run/best-of.ts Outdated
@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/adevloper152s-projects?upgradeToPro=build-rate-limit

@deepsource-io

deepsource-ioBot commented Jul 31, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 9d17df6...ce3afbe on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade

Focus Area: Reliability
Security

Reliability

Complexity

Hygiene

Feedback

Null assertions and early use of values

  • The non-null assertions and the costAlert usage-before-definition are both about assuming values exist in edge cases.
  • This matters because best-of-N and prompts are inherently about branching behavior; tightening those assumptions in one pass will likely make the flows more predictable under odd inputs.

Browser console usage and unused state

  • console usage in browser components and the unused moveLabelWidth both look like temporary debugging hooks.
  • Worth treating these as a single “debug leftovers” pass so the TUI surface stays clean and easier to reason about.

Code Review Summary

AnalyzerStatusUpdated (UTC)Details
JavaScriptJul 31, 2026 5:25a.m.Review ↗
ShellJul 31, 2026 5:25a.m.Review ↗
SecretsJul 31, 2026 5:25a.m.Review ↗
DockerJul 31, 2026 5:25a.m.Review ↗
PythonJul 31, 2026 5:25a.m.Review ↗
CSSJul 31, 2026 5:25a.m.Review ↗
RustJul 31, 2026 5:25a.m.Review ↗
RubyJul 31, 2026 5:25a.m.Review ↗
SwiftJul 31, 2026 5:25a.m.Review ↗
PHPJul 31, 2026 5:25a.m.Review ↗
LuaJul 31, 2026 5:25a.m.Review ↗
JavaJul 31, 2026 5:25a.m.Review ↗
GoJul 31, 2026 5:25a.m.Review ↗
C & C++Jul 31, 2026 5:25a.m.Review ↗
AnsibleJul 31, 2026 5:25a.m.Review ↗
ApexJul 31, 2026 5:25a.m.Review ↗
ElixirJul 31, 2026 5:25a.m.Review ↗
GroovyJul 31, 2026 5:25a.m.Review ↗
Objective-CJul 31, 2026 5:25a.m.Review ↗
PowerShellJul 31, 2026 5:25a.m.Review ↗
TerraformJul 31, 2026 5:25a.m.Review ↗
VB.NETJul 31, 2026 5:25a.m.Review ↗
SQLJul 31, 2026 5:25a.m.Review ↗
ScalaJul 31, 2026 5:25a.m.Review ↗
PerlJul 31, 2026 5:25a.m.Review ↗
KotlinJul 31, 2026 5:25a.m.Review ↗
HelmJul 31, 2026 5:25a.m.Review ↗
ErlangJul 31, 2026 5:25a.m.Review ↗
DartJul 31, 2026 5:25a.m.Review ↗
C#Jul 31, 2026 5:25a.m.Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@A-x6A-x6 changed the title feat(cli): best-of-N runsfeat: best-of-N runs and /yolo modeJul 31, 2026
@@ -668,6 +679,24 @@ export const RunCommand = effectCmd({
}

async function execute(sdk: OpencodeClient) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`execute` has a cyclomatic complexity of 22 with "high" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complexity is pre-existing in the run command handler; this PR only adds one small branch, and refactoring the whole function would be unrelated churn against the repo's keep-things-in-one-function style.

json: args.format === "json",
})
if (exit) process.exitCode = exit
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async function 'execute' expected a return value


Any code paths that do not have explicit returns will return undefined. It is recommended to replace any implicit dead-ends that return undefined with a return null statement.

Comment on lines +27 to +29
export function format(model: Model) {
return `${model.providerID}/${model.modelID}`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +33 to +50
export function parseCandidates(value: string): Model[] | string {
const entries = value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0)
const unique = [...new Set(entries)]
if (unique.length < 2) return "--best-of needs at least two comma-separated provider/model entries"
if (unique.length > LIMIT) return `--best-of supports at most ${LIMIT} models`
const invalid = unique.find((item) => {
const slash = item.indexOf("/")
return slash <= 0 || slash === item.length - 1
})
if (invalid) return `--best-of entries must be in provider/model format, got: ${invalid}`
return unique.map((item) => {
const [providerID, ...rest] = item.split("/")
return { providerID, modelID: rest.join("/") } as Model
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +54 to +75
export function judgePrompt(task: string, outputs: { label: string; text: string }[]) {
const blocks = outputs
.map((item) => `### Candidate ${item.label}\n\n${item.text.trim() || "(empty output)"}`)
.join("\n\n")
return [
"You are judging anonymous candidate responses to the same task. Answer directly and do not use any tools.",
"Rank the candidates from best to worst by correctness first, then completeness, then clarity.",
"",
"## Task",
"",
task,
"",
"## Candidates",
"",
blocks,
"",
"Reply with one short sentence per candidate explaining its rank.",
`End with a final line of the form "RANKING: X > Y" listing every candidate label (${outputs
.map((item) => item.label)
.join(", ")}) from best to worst.`,
].join("\n")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

return order
}

export async function runBestOf(input: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`runBestOf` has a cyclomatic complexity of 7 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cyclomatic complexity of 7 is modest for the orchestration entry point, and the repo style guide explicitly prefers keeping logic in one function unless helpers name a real reusable concept.

if (!verdict) note("judge produced no usable verdict; keeping the first successful candidate in submission order")
const order = verdict?.order ?? done.map((item) => item.label)
const ranked = new Map(settled.map((item) => [item.label, item]))
const winner = ranked.get(order[0])!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forbidden non-null assertion


Using non-null assertions cancels out the benefits of strict null-checking, and introduces the possibility of runtime errors. Avoid non-null assertions unless absolutely necessary. If you still need to use one, write a skipcq comment to explain why it is safe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safe by construction: order is either a verdict validated as a complete permutation of the done labels or the done labels themselves, and ranked contains every settled label, so the lookup cannot return undefined. A fallback would be dead defensive code.


UI.empty()
order.forEach((label, i) => {
const item = ranked.get(label)!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forbidden non-null assertion


Using non-null assertions cancels out the benefits of strict null-checking, and introduces the possibility of runtime errors. Avoid non-null assertions unless absolutely necessary. If you still need to use one, write a skipcq comment to explain why it is safe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safe by construction: every label in order comes from done (a subset of settled), and ranked is built from all settled candidates, so the lookup cannot return undefined.

UI.empty()
order.forEach((label, i) => {
const item = ranked.get(label)!
const marker = i === 0 ? UI.Style.TEXT_SUCCESS + "★" : UI.Style.TEXT_DIM + `${i + 1}.`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected string concatenation


In ES2015 (ES6), we can use template literals instead of string concatenation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sibling finding: the UI.Style token concatenation matches the codebase-wide convention, so changing it would reduce consistency without functional benefit.

UI.empty()
order.forEach((label, i) => {
const item = ranked.get(label)!
const marker = i === 0 ? UI.Style.TEXT_SUCCESS + "★" : UI.Style.TEXT_DIM + `${i + 1}.`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected string concatenation


In ES2015 (ES6), we can use template literals instead of string concatenation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concatenating UI.Style tokens with text (e.g. UI.Style.TEXT_DIM + text) is the established pattern throughout this file and run.ts; converting to a template literal here would be inconsistent churn.

@A-x6
A-x6 merged commit 4e7aa18 into devJul 31, 2026
61 of 62 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: best-of-N runs

1 participant

@A-x6