Skip to content

feat: add brainstorm verb with score-only iteration and controversial selection#27

Merged
El-Fitz merged 14 commits into
mainfrom
feat/brainstorm-verb
Apr 4, 2026
Merged

feat: add brainstorm verb with score-only iteration and controversial selection#27
El-Fitz merged 14 commits into
mainfrom
feat/brainstorm-verb

Conversation

@El-Fitz

@El-Fitz El-Fitz commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New refinery brainstorm CLI verb that produces a panel of diverse, high-quality answers
  • Score-only iteration: models see only their own prior answers + average scores between rounds — no other models' content, no reviews, no suggestions
  • Brainstorm evaluation rubric: scores on originality, insight, depth, and feasibility rather than converge's accuracy/correctness
  • Controversial selection: panel answers ranked by mean * stddev — answers that evaluators genuinely disagree about rank highest
  • Returns a panel of --panel-size answers (default 3) with full score distribution metadata

Architecture

Brainstorm orchestrates its own propose/evaluate loops via JoinSet + provider.send_message() (same pattern as synthesize's Phase 3-4). Does NOT reuse Engine::run(), phases::propose::run(), or phases::evaluate::run() — all three are hardcoded to converge's own+reviews iteration strategy.

New crate additions

  • refinery_core::scoringcontroversy_score(), PanelCandidate, select_panel() with two-key sort
  • refinery_core::prompts::brainstormBRAINSTORM_EVAL_SCHEMA, brainstorm_evaluate_prompt()
  • refinery_core::prompts::propose_with_score_history_prompt() — score-only iteration prompt builder
  • refinery_core::types::ScoreHistory, Phase::Brainstorm, ConvergenceStatus::Brainstormed

Also in this PR

  • Brainstorm verb requirements doc and implementation plan
  • TODO 011 (evolve verb sketch) and TODO 013 (strategy benchmark research)
  • Updated TODO 004 with iteration/selection strategy taxonomy

Test plan

  • 128 tests passing (cargo test --workspace)
  • cargo clippy --workspace -- -D warnings clean
  • cargo fmt --all --check clean
  • refinery brainstorm --help shows correct flags
  • refinery brainstorm --dry-run -m claude-code,codex-cli,gemini-cli "prompt" estimates calls
  • Manual test with real models to validate panel diversity

Post-Deploy Monitoring & Validation

No additional operational monitoring required — new CLI verb, no runtime services.

🤖 Generated with Claude Code

Thomas and others added 6 commits March 31, 2026 20:13
Add brainstorm-specific iteration support:
- ScoreHistory = Vec<(String, f64)> for score-only round tracking
- propose_with_score_history_prompt() presents own answers + scores only
- brainstorm_system_prompt() encourages originality over correctness
- Sanitizers for new <score> XML tags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BRAINSTORM_EVAL_SCHEMA scores on originality, insight, depth, and
feasibility rather than converge's accuracy/correctness focus.
Rationale field ordered before score for autoregressive anti-manipulation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
controversy_score = mean * stddev for evaluator disagreement.
select_panel uses two-key sort (controversy, mean) for deterministic
tiebreaking. PanelCandidate carries full score distribution metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Score-only iteration (models see own answers + scores, no reviews),
brainstorm-specific evaluation rubric (originality, insight, depth,
feasibility), and controversial panel selection (mean * stddev).

Returns a panel of diverse answers ranked by evaluator disagreement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Requirements doc for brainstorm verb (score-only + controversial)
- Implementation plan with 5 units
- Updated TODO 004 with iteration/selection strategy terminology
- New TODO 011 (evolve verb) and 013 (strategy benchmarks)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 31, 2026 20:22
@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a new brainstorm command for multi-round ideation with score-only feedback to models.
    • Panel selection prioritizes high evaluator disagreement combined with strong average scores.
    • Output includes per-answer score statistics: mean, standard deviation, and per-evaluator scores.
    • Configurable --max-rounds (1–20, default 5) and --panel-size (1–20, default 3).
    • Supports dry-run mode and both text and JSON output formats.

Walkthrough

Adds a new "brainstorm" verb and CLI subcommand implementing a multi-round propose/evaluate loop (score-only feedback), controversy-based panel selection (mean × stddev), new prompts/sanitization, scoring utilities, types/status additions, tests, docs, and JSON/text CLI output with dry-run estimation.

Changes

Cohort / File(s) Summary
CLI: Brainstorm command & dispatch
crates/refinery_cli/src/commands/brainstorm.rs, crates/refinery_cli/src/commands/mod.rs, crates/refinery_cli/src/main.rs, crates/refinery_cli/src/commands/converge.rs
Adds BrainstormArgs and async run, exposes commands::brainstorm, wires Command::Brainstorm to dispatcher, updates converge exit mapping to include ConvergenceStatus::Brainstormed, implements dry-run call estimation and JSON/text output handling.
Core verb implementation
crates/refinery_core/src/brainstorm.rs, crates/refinery_core/src/lib.rs, crates/refinery_core/src/types.rs
New async run orchestration with propose+evaluate rounds, concurrency/timeouts, per-model ScoreHistory, round/result/error types (BrainstormConfig/Result/Error), Phase/Status additions, artifact saving, and unit tests.
Prompts & sanitization
crates/refinery_core/src/prompts/mod.rs, crates/refinery_core/src/prompts/brainstorm.rs
Adds BRAINSTORM_EVAL_SCHEMA, brainstorm_evaluate_prompt, brainstorm_system_prompt, propose_with_score_history_prompt, and sanitize_for_score_tag plus tests covering schema validity, escaping, and prompt contents.
Scoring & selection
crates/refinery_core/src/scoring.rs
Implements mean, stddev, controversy_score, PanelCandidate, and select_panel which ranks by controversy (mean×stddev) with mean tiebreaker; includes tests for metrics, ordering, ties, and edge cases.
CLI output & UX helpers
crates/refinery_cli/src/commands/brainstorm.rs
Manages display tick lifecycle, calls core brainstorm::run, handles success/error branches, emits pretty JSON or human-readable text including per-answer stats and metadata, and supports dry-run validation of models.
Docs, plans & todos
docs/brainstorms/*, docs/plans/*, todos/*
Adds requirement, plan, benchmarking, and multiple todo documents describing brainstorm verb design, iteration/selection strategies, constraints, output spec, and open questions.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as CLI (brainstorm)
    participant Orch as Orchestrator (refinery_core::brainstorm)
    participant Providers as Model Providers
    participant Scoring as Scoring Module
    participant Selector as Panel Selector
    participant Output as Output Formatter

    User->>CLI: run brainstorm (--max-rounds, --panel-size)
    CLI->>Orch: build config, resolve prompt, init providers

    loop rounds (1..N)
        Orch->>Providers: propose requests (prompt + own score history)
        Providers-->>Orch: proposals (answers)
        Orch->>Providers: evaluate requests (all-vs-all, no self-eval)
        Providers-->>Orch: evaluation scores (JSON)
        Orch->>Scoring: compute mean/stddev/controversy per answer
        Scoring-->>Orch: updated ScoreHistory
    end

    Orch->>Scoring: build PanelCandidates from final answers
    Scoring-->>Selector: sort by controversy (mean × stddev), tiebreak by mean
    Selector-->>Output: selected panel + metadata (total_calls, rounds, elapsed_ms)
    Output-->>User: emit JSON or text
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I nibbled prompts and thumped out rounds of cheer,

Scores danced like carrots, novel, bright, and queer,
Controversy crowned the oddest, boldest view,
A panel of sparks — strange, true, and new,
Hop! the brainstorm burrow applauds you.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately summarizes the main change: a new brainstorm verb with score-only iteration and controversial selection as the core feature.
Description check ✅ Passed The pull request description comprehensively explains the brainstorm feature, its architecture, new crate additions, and test plan—all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/brainstorm-verb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot 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: 1

🧹 Nitpick comments (8)
crates/refinery_core/src/types.rs (1)

15-19: Prefer a named entry type over tuple-based ScoreHistory.

Lines [15-19] use positional tuple fields ((String, f64)), which are easy to misuse. A small struct with named fields (e.g., proposal, mean_score) is clearer and easier to evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/types.rs` around lines 15 - 19, Replace the tuple
alias ScoreHistory = Vec<(String, f64)> with a named entry struct and use that
struct in the type alias; specifically, define a small struct (e.g., ScoreEntry
or ScoreHistoryItem) with named fields like proposal: String and mean_score:
f64, then change pub type ScoreHistory to Vec<YourStructName> and update any
code using ScoreHistory to access .proposal and .mean_score instead of tuple
indexing.
todos/011-verb-evolve.md (1)

29-29: Use “different from” for formal docs wording.

Line [29] reads “different than”; “different from” is the cleaner phrasing for technical docs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/011-verb-evolve.md` at line 29, Replace the phrase "different than"
with "different from" in the sentence that currently reads "A model starting
from scratch in round 4 will produce something different than round 1" — update
that sentence (in todos/011-verb-evolve.md) to read "A model starting from
scratch in round 4 will produce something different from round 1" to use the
preferred formal wording.
crates/refinery_cli/src/commands/converge.rs (1)

209-211: Avoid reporting Brainstormed as success in the converge command.

Lines [209-211] treat a brainstorm-specific terminal state as successful converge output, which can hide routing/state bugs. Consider returning a non-success code for this status in this command path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/converge.rs` around lines 209 - 211, The
converge command currently treats ConvergenceStatus::Brainstormed as a
successful outcome; update the converge command's exit handling so that
ConvergenceStatus::Brainstormed does not map to ExitCode::SUCCESS. Modify the
match/return logic that maps ConvergenceStatus (the branch that currently lists
ConvergenceStatus::Converged | ConvergenceStatus::SingleModel |
ConvergenceStatus::Brainstormed => ExitCode::SUCCESS) to instead return a
non-success exit code for Brainstormed (e.g., ExitCode::FAILURE or a distinct
code) while keeping Converged and SingleModel as success.
todos/004-verb-brainstorm.md (1)

30-35: Document the exact v0 controversy formula explicitly.

Lines [30-35] describe controversy conceptually, but v0 implementation is mean * stddev. Stating that formula here will avoid ambiguity between current behavior and future alternatives.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/004-verb-brainstorm.md` around lines 30 - 35, Explicitly state the v0
controversy formula as "controversy = mean * stddev", where mean is the average
evaluator score for an answer and stddev is the standard deviation of those
scores; update the document text to replace the conceptual descriptions with
this exact formula, briefly define the variables (mean, stddev) and the
interpretation (higher values = more controversial), and note that this is the
current v0 implementation to avoid ambiguity with future alternatives.
crates/refinery_cli/src/commands/brainstorm.rs (2)

142-146: First-round prompt missing brainstorm framing in user message.

When score_histories.get(&model_id) returns None, the code uses prompt.clone() (the raw user prompt). However, propose_with_score_history_prompt already handles empty history by returning a brainstorm-framed prompt: "Please provide your best answer... Aim for originality and depth."

This means first-round proposals get the brainstorm system prompt but not the brainstorm user prompt framing.

♻️ Suggested fix
-            let user_content = if let Some(history) = score_histories.get(&model_id) {
-                prompts::propose_with_score_history_prompt(&prompt, history)
-            } else {
-                prompt.clone()
-            };
+            let history = score_histories.get(&model_id);
+            let empty_history: ScoreHistory = vec![];
+            let user_content = prompts::propose_with_score_history_prompt(
+                &prompt,
+                history.unwrap_or(&empty_history),
+            );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 142 - 146, When
building user_content, don't use prompt.clone() when
score_histories.get(&model_id) is None because that skips the brainstorm user
framing; instead always call prompts::propose_with_score_history_prompt(&prompt,
history) and pass an empty/None history when there is no entry in
score_histories so the function can return the brainstorm-framed prompt (ensure
you reference score_histories, model_id,
prompts::propose_with_score_history_prompt and the user_content variable and
replace the prompt.clone() branch accordingly).

345-358: Single-model case always scores 0.0 (no short-circuit).

With a single model, self-evaluation is skipped (line 250-251), so round_scores.get(model_id) returns None, resulting in mean = 0.0 for every round. The model's answers still accumulate in history with 0.0 scores.

The plan mentioned: "single model — returns that model's answer as sole panel member (no evaluation, short-circuit)" but there's no short-circuit implemented. The current behavior works (single model ends up in panel with 0.0 scores), but the score history feedback to the model will always show 0.0 which may confuse the model.

Consider either:

  1. Short-circuiting single-model case (skip evaluation rounds)
  2. Or documenting this as expected behavior
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 345 - 358,
Detect the single-model case before iterating round_proposals and short-circuit
so we don't read from round_scores and record a misleading 0.0; i.e., if
round_proposals.len() == 1, extract the sole (model_id, answer) pair, update
score_histories and latest_answers for that model without computing mean from
round_scores (either store no score or an explicit None/sentinel instead of
0.0), and skip the normal scoring path that uses round_scores.get(model_id).
Ensure the logic touches the same symbols shown here: round_proposals,
round_scores, score_histories, and latest_answers so the single-model flow never
falls through to the code that computes mean from round_scores.
crates/refinery_core/src/prompts/mod.rs (1)

142-146: First-round fallback inconsistency with propose_with_history_prompt.

When score_histories.get(&model_id) returns None (first round), the code falls back to prompt.clone() which is just the raw user prompt. However, propose_with_score_history_prompt already handles empty history by returning a brainstorm-framed prompt with "Aim for originality and depth."

This means first-round proposals won't get the brainstorm framing in the user message (though they do get the brainstorm system prompt). For consistency, consider always calling propose_with_score_history_prompt:

♻️ Suggested fix
-            let user_content = if let Some(history) = score_histories.get(&model_id) {
-                prompts::propose_with_score_history_prompt(&prompt, history)
-            } else {
-                prompt.clone()
-            };
+            let history = score_histories.get(&model_id).map(|h| h.as_slice()).unwrap_or(&[]);
+            let user_content = prompts::propose_with_score_history_prompt(&prompt, history);

Note: This comment applies to crates/refinery_cli/src/commands/brainstorm.rs lines 142-146, but the fix relates to the prompt function defined here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/prompts/mod.rs` around lines 142 - 146, The
empty-history branch currently returns propose_prompt(user_prompt, round_ctx)
which yields a raw user prompt; instead, ensure first-round prompts get the same
"brainstorm" framing by calling propose_with_score_history_prompt (or the
existing propose_with_history_prompt helper) so that empty histories produce the
brainstorm-framed user message; update both the history.is_empty() case in the
function and the place that uses score_histories.get(&model_id) returning None
(where prompt.clone() is used) to call
propose_with_score_history_prompt(user_prompt, round_ctx, model_id) so
formatting is consistent across first and later rounds.
docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md (1)

1-7: Missing plan date headers per coding guidelines.

The plan frontmatter has date: 2026-03-31 but is missing the structured date headers described in the coding guidelines. Consider adding an Enhanced: or Reviewed: date header to track the plan's lifecycle.

As per coding guidelines: "When a plan is deepened, update the plan header with Enhanced: date (via /deepen-plan). When a plan is reviewed, update the plan header with Reviewed: date."

📝 Suggested addition
 ---
 title: "feat: brainstorm verb — score-only iteration with controversial selection"
 type: feat
 status: active
 date: 2026-03-31
 origin: docs/brainstorms/2026-03-30-brainstorm-verb-requirements.md
+reviewed: 2026-03-31
 ---
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md` around lines 1 - 7,
The plan frontmatter currently only has "date: 2026-03-31" and is missing
lifecycle headers; update the frontmatter for the plan titled "feat: brainstorm
verb — score-only iteration with controversial selection" by adding an
appropriate lifecycle header such as "Enhanced: YYYY-MM-DD" when deepened (via
/deepen-plan) or "Reviewed: YYYY-MM-DD" when reviewed, placing the new header
alongside the existing "date:" field in the YAML frontmatter so it follows the
project's coding guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/refinery_cli/src/commands/brainstorm.rs`:
- Around line 30-32: The panel_size field lacks validation allowing 0; update
the struct's panel_size argument (symbol: panel_size) to include a value_parser
range that enforces panel_size >= 1 (e.g., mirror max_rounds and add #[arg(long,
value_parser = clap::value_parser!(1..=20), default_value = "3")] or at minimum
value_parser = clap::value_parser!(1..) so the CLI rejects 0), ensuring
downstream code that builds the panel won't receive an empty size.

---

Nitpick comments:
In `@crates/refinery_cli/src/commands/brainstorm.rs`:
- Around line 142-146: When building user_content, don't use prompt.clone() when
score_histories.get(&model_id) is None because that skips the brainstorm user
framing; instead always call prompts::propose_with_score_history_prompt(&prompt,
history) and pass an empty/None history when there is no entry in
score_histories so the function can return the brainstorm-framed prompt (ensure
you reference score_histories, model_id,
prompts::propose_with_score_history_prompt and the user_content variable and
replace the prompt.clone() branch accordingly).
- Around line 345-358: Detect the single-model case before iterating
round_proposals and short-circuit so we don't read from round_scores and record
a misleading 0.0; i.e., if round_proposals.len() == 1, extract the sole
(model_id, answer) pair, update score_histories and latest_answers for that
model without computing mean from round_scores (either store no score or an
explicit None/sentinel instead of 0.0), and skip the normal scoring path that
uses round_scores.get(model_id). Ensure the logic touches the same symbols shown
here: round_proposals, round_scores, score_histories, and latest_answers so the
single-model flow never falls through to the code that computes mean from
round_scores.

In `@crates/refinery_cli/src/commands/converge.rs`:
- Around line 209-211: The converge command currently treats
ConvergenceStatus::Brainstormed as a successful outcome; update the converge
command's exit handling so that ConvergenceStatus::Brainstormed does not map to
ExitCode::SUCCESS. Modify the match/return logic that maps ConvergenceStatus
(the branch that currently lists ConvergenceStatus::Converged |
ConvergenceStatus::SingleModel | ConvergenceStatus::Brainstormed =>
ExitCode::SUCCESS) to instead return a non-success exit code for Brainstormed
(e.g., ExitCode::FAILURE or a distinct code) while keeping Converged and
SingleModel as success.

In `@crates/refinery_core/src/prompts/mod.rs`:
- Around line 142-146: The empty-history branch currently returns
propose_prompt(user_prompt, round_ctx) which yields a raw user prompt; instead,
ensure first-round prompts get the same "brainstorm" framing by calling
propose_with_score_history_prompt (or the existing propose_with_history_prompt
helper) so that empty histories produce the brainstorm-framed user message;
update both the history.is_empty() case in the function and the place that uses
score_histories.get(&model_id) returning None (where prompt.clone() is used) to
call propose_with_score_history_prompt(user_prompt, round_ctx, model_id) so
formatting is consistent across first and later rounds.

In `@crates/refinery_core/src/types.rs`:
- Around line 15-19: Replace the tuple alias ScoreHistory = Vec<(String, f64)>
with a named entry struct and use that struct in the type alias; specifically,
define a small struct (e.g., ScoreEntry or ScoreHistoryItem) with named fields
like proposal: String and mean_score: f64, then change pub type ScoreHistory to
Vec<YourStructName> and update any code using ScoreHistory to access .proposal
and .mean_score instead of tuple indexing.

In `@docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md`:
- Around line 1-7: The plan frontmatter currently only has "date: 2026-03-31"
and is missing lifecycle headers; update the frontmatter for the plan titled
"feat: brainstorm verb — score-only iteration with controversial selection" by
adding an appropriate lifecycle header such as "Enhanced: YYYY-MM-DD" when
deepened (via /deepen-plan) or "Reviewed: YYYY-MM-DD" when reviewed, placing the
new header alongside the existing "date:" field in the YAML frontmatter so it
follows the project's coding guidelines.

In `@todos/004-verb-brainstorm.md`:
- Around line 30-35: Explicitly state the v0 controversy formula as "controversy
= mean * stddev", where mean is the average evaluator score for an answer and
stddev is the standard deviation of those scores; update the document text to
replace the conceptual descriptions with this exact formula, briefly define the
variables (mean, stddev) and the interpretation (higher values = more
controversial), and note that this is the current v0 implementation to avoid
ambiguity with future alternatives.

In `@todos/011-verb-evolve.md`:
- Line 29: Replace the phrase "different than" with "different from" in the
sentence that currently reads "A model starting from scratch in round 4 will
produce something different than round 1" — update that sentence (in
todos/011-verb-evolve.md) to read "A model starting from scratch in round 4 will
produce something different from round 1" to use the preferred formal wording.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3cf53072-4dfc-409c-9749-f61ca6800349

📥 Commits

Reviewing files that changed from the base of the PR and between 37fa493 and abb2db2.

📒 Files selected for processing (14)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_cli/src/commands/converge.rs
  • crates/refinery_cli/src/commands/mod.rs
  • crates/refinery_cli/src/main.rs
  • crates/refinery_core/src/lib.rs
  • crates/refinery_core/src/prompts/brainstorm.rs
  • crates/refinery_core/src/prompts/mod.rs
  • crates/refinery_core/src/scoring.rs
  • crates/refinery_core/src/types.rs
  • docs/brainstorms/2026-03-30-brainstorm-verb-requirements.md
  • docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md
  • todos/004-verb-brainstorm.md
  • todos/011-verb-evolve.md
  • todos/013-brainstorm-strategy-benchmarks.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_cli/src/commands/mod.rs
  • crates/refinery_core/src/lib.rs
  • crates/refinery_cli/src/commands/converge.rs
  • crates/refinery_core/src/types.rs
  • crates/refinery_cli/src/main.rs
  • crates/refinery_core/src/prompts/brainstorm.rs
  • crates/refinery_core/src/prompts/mod.rs
  • crates/refinery_core/src/scoring.rs
  • crates/refinery_cli/src/commands/brainstorm.rs
docs/plans/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

docs/plans/**/*.md: When a plan is deepened, update the plan header with Enhanced: date (via /deepen-plan)
When a plan is reviewed, update the plan header with Reviewed: date (via appropriate skill/command)
When a plan is completed, update the plan header with Completed: date
When a gap is discovered during execution, update the plan with an addendum (eg Addendum: date — description of what was added and why)

Files:

  • docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md
🧠 Learnings (1)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md
  • crates/refinery_core/src/prompts/brainstorm.rs
  • crates/refinery_core/src/prompts/mod.rs
🪛 LanguageTool
docs/brainstorms/2026-03-30-brainstorm-verb-requirements.md

[style] ~53-~53: You have already used ‘right’ in nearby sentences. Consider using an alternative word to let your writing stand out and sound more polished.
Context: ...[Affects R5][Needs research] What's the right default for --panel-size? Too small (...

(REP_RIGHT)

todos/011-verb-evolve.md

[style] ~29-~29: Did you mean ‘different from’? ‘Different than’ is often considered colloquial style.
Context: ...ound 4 will produce something different than round 1. - The wall: variation (pro...

(DIFFERENT_THAN)

docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md

[style] ~62-~62: Consider an alternative for the overused word “exactly”.
Context: ...pts/types and cannot be reused. This is exactly what synthesize does for its Phase 3-4 ...

(EXACTLY_PRECISELY)

🪛 markdownlint-cli2 (0.22.0)
docs/brainstorms/2026-03-30-brainstorm-verb-requirements.md

[warning] 48-48: Reference links and images should use a label that is defined
Missing link or image reference definition: "technical"

(MD052, reference-links-images)


[warning] 49-49: Reference links and images should use a label that is defined
Missing link or image reference definition: "technical"

(MD052, reference-links-images)


[warning] 50-50: Reference links and images should use a label that is defined
Missing link or image reference definition: "technical"

(MD052, reference-links-images)


[warning] 51-51: Reference links and images should use a label that is defined
Missing link or image reference definition: "technical"

(MD052, reference-links-images)


[warning] 52-52: Reference links and images should use a label that is defined
Missing link or image reference definition: "needs research"

(MD052, reference-links-images)


[warning] 53-53: Reference links and images should use a label that is defined
Missing link or image reference definition: "needs research"

(MD052, reference-links-images)


[warning] 54-54: Reference links and images should use a label that is defined
Missing link or image reference definition: "technical"

(MD052, reference-links-images)

🔇 Additional comments (11)
crates/refinery_core/src/lib.rs (1)

6-6: Public scoring module export looks correct.

pub mod scoring; cleanly exposes the new scoring utilities from the crate root.

crates/refinery_cli/src/commands/mod.rs (1)

1-1: Command module wiring is clean.

pub mod brainstorm; is correctly exposed for command dispatch.

crates/refinery_cli/src/main.rs (1)

33-38: Subcommand registration and dispatch are consistent.

Command::Brainstorm(...) is defined and routed to the corresponding run function cleanly.

Also applies to: 71-71

crates/refinery_core/src/types.rs (1)

351-362: New variant coverage tests are solid.

The added tests correctly lock in serialization for ConvergenceStatus::Brainstormed and display formatting for Phase::Brainstorm.

todos/013-brainstorm-strategy-benchmarks.md (1)

12-33: Benchmark matrix is well-scoped for post-v0 evaluation.

The iteration/selection strategy breakdown is clear and actionable for follow-up experiments.

crates/refinery_core/src/prompts/mod.rs (1)

186-251: Clean implementation of score-only prompt utilities.

The new sanitize_for_score_tag, brainstorm_system_prompt, and propose_with_score_history_prompt functions follow established patterns:

  • Sanitization mirrors sanitize_for_review_tag
  • XML structure is consistent with propose_with_history_prompt
  • Tests cover edge cases including tag injection attempts
docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md (1)

9-292: Comprehensive implementation plan.

The plan provides thorough context, architectural analysis, and well-decomposed implementation units. Key strengths:

  • Clear rationale for not reusing Engine::run() or existing phases
  • Explicit requirements tracing for each unit
  • Good test scenario coverage
  • Risks and mitigations documented
crates/refinery_core/src/prompts/brainstorm.rs (2)

3-8: Schema follows established patterns.

The BRAINSTORM_EVAL_SCHEMA correctly:

  • Omits minimum/maximum bounds on integers (runtime validation is authoritative, per existing patterns)
  • Places rationale before score in both properties and required (anti-manipulation measure)
  • Sets additionalProperties: false

Based on learnings: "SYNTHESIS_EVAL_SCHEMA intentionally omits JSON Schema minimum/maximum bounds... Runtime validation in parse_evaluation is the authoritative range check instead."


10-51: Well-structured evaluation prompt.

The prompt clearly differentiates brainstorm evaluation from convergence by emphasizing originality, insight, depth, and feasibility over conventional correctness. The rubric anchoring is explicit and follows chain-of-thought best practices.

crates/refinery_core/src/scoring.rs (1)

1-63: Solid scoring implementation with good edge-case handling.

The scoring functions correctly implement:

  • Population standard deviation (divide by N, appropriate for evaluator scores)
  • Controversy = mean × stddev as specified
  • NaN-safe sorting via total_cmp
  • Graceful fallback for empty/single-element inputs

The test coverage is comprehensive, including the specific scenarios from the plan (high-variance beating low-variance at same mean).

crates/refinery_cli/src/commands/brainstorm.rs (1)

62-476: Overall implementation follows established patterns.

The CLI command implementation:

  • Correctly orchestrates propose/evaluate phases via JoinSet
  • Uses semaphore for concurrency control
  • Properly parses and validates scores (1.0-10.0 range filter at line 302)
  • Builds PanelCandidates with all required fields
  • Handles errors consistently with other commands
  • Produces both text and JSON output with appropriate metadata

Comment thread crates/refinery_cli/src/commands/brainstorm.rs Outdated

Copilot AI 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.

Pull request overview

Adds a new refinery brainstorm CLI verb to generate a panel of diverse answers via multi-round, score-only self-iteration and final “controversial” selection (mean × stddev), alongside the supporting core prompt/scoring/types and planning docs.

Changes:

  • Introduces brainstorm orchestration in the CLI (custom propose/evaluate loops via provider.send_message()).
  • Adds core building blocks: ScoreHistory, brainstorm prompts/schema, and controversy scoring + panel selection utilities.
  • Adds design/requirements/strategy documentation and updates brainstorm-related TODOs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
todos/013-brainstorm-strategy-benchmarks.md New TODO describing benchmark axes (iteration vs selection) post-v0.
todos/011-verb-evolve.md New TODO sketching a future “evolve” verb concept.
todos/004-verb-brainstorm.md Updates brainstorm TODO to clarify the two orthogonal axes and v0 choices.
docs/plans/2026-03-31-001-feat-brainstorm-verb-plan.md Implementation plan and architectural rationale for brainstorm.
docs/brainstorms/2026-03-30-brainstorm-verb-requirements.md Requirements + scope boundaries for brainstorm.
crates/refinery_core/src/types.rs Adds ScoreHistory, Phase::Brainstorm, and ConvergenceStatus::Brainstormed (+ tests).
crates/refinery_core/src/scoring.rs New scoring utilities: mean/stddev/controversy + select_panel() (+ tests).
crates/refinery_core/src/prompts/mod.rs Exposes brainstorm module; adds brainstorm system prompt + score-history propose prompt builder.
crates/refinery_core/src/prompts/brainstorm.rs New brainstorm evaluation prompt + JSON schema (+ tests).
crates/refinery_core/src/lib.rs Exports the new scoring module.
crates/refinery_cli/src/main.rs Adds Command::Brainstorm routing.
crates/refinery_cli/src/commands/mod.rs Exposes the new brainstorm command module.
crates/refinery_cli/src/commands/converge.rs Updates exit-code mapping to include Brainstormed (though converge won’t emit it).
crates/refinery_cli/src/commands/brainstorm.rs New brainstorm CLI implementation (round loop + evaluation + selection + output).
Comments suppressed due to low confidence (1)

crates/refinery_cli/src/commands/converge.rs:216

  • ConvergenceStatus::Brainstormed is included in the converge command’s exit-code mapping, but converge can’t produce that status (it’s brainstorm-specific). Keeping this arm makes the status semantics less clear; consider removing it here (or centralizing exit-code mapping per-verb) so each command only handles statuses it can actually emit.
            match outcome.status {
                ConvergenceStatus::Converged
                | ConvergenceStatus::SingleModel
                | ConvergenceStatus::Brainstormed => ExitCode::SUCCESS,
                ConvergenceStatus::MaxRoundsExceeded | ConvergenceStatus::NoQualifyingAnswers => {
                    ExitCode::from(2)
                }
                ConvergenceStatus::InsufficientModels => ExitCode::from(3),
                ConvergenceStatus::Cancelled | ConvergenceStatus::Synthesized => ExitCode::from(1),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/refinery_cli/src/commands/brainstorm.rs
Comment thread crates/refinery_cli/src/commands/brainstorm.rs Outdated
Comment thread crates/refinery_cli/src/commands/brainstorm.rs Outdated
Comment thread crates/refinery_cli/src/commands/brainstorm.rs Outdated
Comment thread crates/refinery_cli/src/commands/brainstorm.rs Outdated
Move the core brainstorm round loop from the CLI command into
refinery_core::brainstorm::run() for testability with mock providers.

4 integration tests (no real model calls):
- single_model_returns_answer_directly
- round_loop_accumulates_across_rounds
- controversial_answer_ranks_higher
- all_models_fail_returns_error

CLI command now delegates to the core function, keeping only arg
parsing, progress display, and output formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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: 1

🧹 Nitpick comments (4)
crates/refinery_core/src/brainstorm.rs (1)

50-60: Consider validating non-empty providers list.

If providers is empty, the loop completes without proposals, leading to round_proposals.is_empty() returning an error. While this works, an early check could provide a clearer error message.

💡 Optional: Early validation
 pub async fn run(
     providers: &[Arc<dyn ModelProvider>],
     prompt: &str,
     config: &BrainstormConfig,
 ) -> Result<BrainstormResult, BrainstormError> {
+    if providers.is_empty() {
+        return Err(BrainstormError {
+            round: 0,
+            message: "No providers supplied.".to_string(),
+        });
+    }
+
     let permits = if config.max_concurrent == 0 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 50 - 60, The run
function should early-validate that the providers slice is not empty to surface
a clearer error; at the top of pub async fn run(...) check if
providers.is_empty() and return a descriptive BrainstormError (or add a new
variant like BrainstormError::EmptyProviders) instead of letting downstream
logic produce an empty-result error—update any callers/tests accordingly to
expect this explicit error from run.
crates/refinery_cli/src/commands/brainstorm.rs (3)

60-85: Dry-run output ignores --output json flag.

The dry-run path always prints text output via println!, ignoring shared.output_format. Other commands may follow a similar pattern, but for consistency with the JSON output contract, consider respecting the format flag.

💡 Optional: JSON dry-run output
+        match shared.output_format {
+            OutputFormat::Json => {
+                let json = serde_json::json!({
+                    "dry_run": true,
+                    "models": n,
+                    "max_rounds": args.max_rounds,
+                    "calls_per_round": calls_per_round,
+                    "total_calls_max": total,
+                    "panel_size": args.panel_size,
+                });
+                println!("{}", serde_json::to_string_pretty(&json).unwrap_or_default());
+            }
+            OutputFormat::Text => {
-        println!("Dry run estimate:");
-        println!("  Models: {n}");
-        println!("  Max rounds: {}", args.max_rounds);
-        println!("  Calls per round: {calls_per_round}");
-        println!("  Total calls (max): {total}");
-        println!("  Panel size: {}", args.panel_size);
+                println!("Dry run estimate:");
+                println!("  Models: {n}");
+                println!("  Max rounds: {}", args.max_rounds);
+                println!("  Calls per round: {calls_per_round}");
+                println!("  Total calls (max): {total}");
+                println!("  Panel size: {}", args.panel_size);
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 60 - 85, The
dry-run branch under the shared.dry_run check currently prints human-readable
text with println! and ignores shared.output_format; update that block (the code
that builds model_ids via super::common::parse_model_spec and computes
calls_per_round, total, etc.) to branch on shared.output_format (e.g.,
OutputFormat::Json) and, when JSON is requested, serialize a small struct/object
containing models: n (or model IDs), max_rounds: args.max_rounds,
calls_per_round, total, and panel_size using serde_json::to_string (or
to_string_pretty) and print that instead of the multiline println! output; keep
the existing text output path for non-JSON formats and still return
ExitCode::SUCCESS.

162-167: total_rounds reports max_rounds, not actual rounds completed.

The metadata reports max_rounds as total_rounds, but if the brainstorm loop completes all rounds, this is accurate. However, the current core implementation doesn't expose actual round count in BrainstormResult, so this is the best available. Consider adding rounds_completed to BrainstormResult for accurate reporting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 162 - 167, The
metadata currently sets MetadataOutput.total_rounds to max_rounds which can be
inaccurate; update the BrainstormResult struct to include a rounds_completed (or
similar) field, populate it from the brainstorming loop where the actual rounds
completed is known, and then use that rounds_completed value when constructing
MetadataOutput instead of max_rounds (refer to BrainstormResult and
MetadataOutput in brainstorm.rs and the code path that builds the result
variable named result). Ensure any callers/constructors that create
BrainstormResult are updated to pass the new field so the real completed round
count is reported.

169-171: Silent failure on JSON serialization error.

If serde_json::to_string_pretty fails (unlikely but possible), no output is produced but ExitCode::SUCCESS is still returned at line 193. Consider logging or falling back to text output.

💡 Optional: Handle serialization failure
-            if let Ok(json) = serde_json::to_string_pretty(&json_output) {
-                println!("{json}");
-            }
+            match serde_json::to_string_pretty(&json_output) {
+                Ok(json) => println!("{json}"),
+                Err(e) => {
+                    eprintln!("Failed to serialize output: {e}");
+                    // Fall back to text output or return error
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 169 - 171, The
current JSON serialization with serde_json::to_string_pretty(&json_output)
silently drops output on Err; update the block around
serde_json::to_string_pretty and println!("{json}") so that on Err you (1) log
the serialization error (e.g., via eprintln! or the crate logger) including the
Err value, and (2) fall back to a safe textual representation of json_output
(for example Debug/pretty-print of json_output) so the user still gets output;
keep the normal ExitCode::SUCCESS behavior unless you deliberately want to
change it. Ensure you modify the code that calls serde_json::to_string_pretty
and uses println to implement these two steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/refinery_core/src/brainstorm.rs`:
- Around line 79-83: The code treats first-round prompts inconsistently by
passing raw prompt when score_histories lacks model_id; instead call
prompts::propose_with_score_history_prompt with an empty history so the same
framing ("Please provide your best answer...") is applied. Replace the else
branch that sets user_content = prompt.to_string() with a call like
prompts::propose_with_score_history_prompt(prompt, &Vec::new()) (or equivalent
empty-history value) so user_content is always produced by
propose_with_score_history_prompt for both existing and first-round cases.

---

Nitpick comments:
In `@crates/refinery_cli/src/commands/brainstorm.rs`:
- Around line 60-85: The dry-run branch under the shared.dry_run check currently
prints human-readable text with println! and ignores shared.output_format;
update that block (the code that builds model_ids via
super::common::parse_model_spec and computes calls_per_round, total, etc.) to
branch on shared.output_format (e.g., OutputFormat::Json) and, when JSON is
requested, serialize a small struct/object containing models: n (or model IDs),
max_rounds: args.max_rounds, calls_per_round, total, and panel_size using
serde_json::to_string (or to_string_pretty) and print that instead of the
multiline println! output; keep the existing text output path for non-JSON
formats and still return ExitCode::SUCCESS.
- Around line 162-167: The metadata currently sets MetadataOutput.total_rounds
to max_rounds which can be inaccurate; update the BrainstormResult struct to
include a rounds_completed (or similar) field, populate it from the
brainstorming loop where the actual rounds completed is known, and then use that
rounds_completed value when constructing MetadataOutput instead of max_rounds
(refer to BrainstormResult and MetadataOutput in brainstorm.rs and the code path
that builds the result variable named result). Ensure any callers/constructors
that create BrainstormResult are updated to pass the new field so the real
completed round count is reported.
- Around line 169-171: The current JSON serialization with
serde_json::to_string_pretty(&json_output) silently drops output on Err; update
the block around serde_json::to_string_pretty and println!("{json}") so that on
Err you (1) log the serialization error (e.g., via eprintln! or the crate
logger) including the Err value, and (2) fall back to a safe textual
representation of json_output (for example Debug/pretty-print of json_output) so
the user still gets output; keep the normal ExitCode::SUCCESS behavior unless
you deliberately want to change it. Ensure you modify the code that calls
serde_json::to_string_pretty and uses println to implement these two steps.

In `@crates/refinery_core/src/brainstorm.rs`:
- Around line 50-60: The run function should early-validate that the providers
slice is not empty to surface a clearer error; at the top of pub async fn
run(...) check if providers.is_empty() and return a descriptive BrainstormError
(or add a new variant like BrainstormError::EmptyProviders) instead of letting
downstream logic produce an empty-result error—update any callers/tests
accordingly to expect this explicit error from run.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0a07c37f-b48e-4453-ba6e-dc2161f51ccb

📥 Commits

Reviewing files that changed from the base of the PR and between abb2db2 and d7c54ce.

📒 Files selected for processing (3)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
  • crates/refinery_core/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/refinery_core/src/lib.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
🧠 Learnings (1)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
🔇 Additional comments (12)
crates/refinery_cli/src/commands/brainstorm.rs (4)

24-26: Missing validation for panel_size >= 1.

The panel_size argument lacks a validation constraint, allowing 0 which would always produce an empty panel and trigger the error at line 129.


29-52: LGTM on JSON output type definitions.

Clean separation between core PanelCandidate and CLI-specific PanelAnswerOutput allows controlled serialization format without coupling the core library to specific output requirements.


87-121: LGTM on main execution flow.

The implementation correctly follows the established pattern from converge.rs and synthesize.rs: resolve prompt, build providers, start tick, run core function, abort tick, finish display. The Duration::from_secs(shared.timeout) conversion matches the expected BrainstormConfig type.


196-218: LGTM on error emission.

The emit_error function follows the established pattern for JSON/text error output. The hardcoded retryable: true is reasonable for brainstorm errors (typically transient provider failures).

crates/refinery_core/src/brainstorm.rs (8)

1-42: LGTM on module structure and error types.

The imports, configuration struct, result type, and error implementation are well-organized. BrainstormError correctly implements Display and Error traits for proper error handling.


104-120: LGTM on propose result handling.

The triple-nested Ok pattern correctly unwraps JoinSet result, timeout result, and send_message result. Failed proposals are counted but silently skipped, which is appropriate for a core library—the CLI layer can add logging as needed.


131-144: LGTM on single-model skip logic.

Correctly skips self-evaluation when only one model proposes. The default score of 0.0 is appropriate since there's no evaluator feedback to incorporate.


206-223: LGTM on score parsing and validation.

The parsing correctly handles malformed JSON via unwrap_or_default(), attempts both integer and float extraction, and clamps scores to [1.0, 10.0]. This aligns with the runtime validation pattern mentioned in learnings. Based on learnings: "runtime validation in parse_evaluation is the authoritative range check instead."


246-274: LGTM on panel selection logic.

The panel selection correctly constructs PanelCandidate with all required fields as defined in crates/refinery_core/src/scoring.rs. Using only the final round's evaluations for panel selection aligns with the "score-only iteration" design where models see only their historical scores, not cross-model evaluations.


276-411: LGTM on test coverage.

The tests cover key scenarios: single-model passthrough, multi-round accumulation, controversy-based ranking, and error propagation. The start_paused = true test flavor with deterministic EchoProvider makes tests reproducible. Test comments clearly document expected behavior.


228-241: No issue found: scoring::mean() safely handles empty scores.

The scoring::mean() function explicitly returns 0.0 when the scores slice is empty, so there are no issues with NaN or panics in the code at these lines.


176-179: No action needed. The use of different system prompts is intentional: brainstorm_system_prompt() for the propose phase (emphasizing novelty and creative thinking) and the generic system_prompt() for the evaluate phase (emphasizing accuracy and consensus quality). This is a correct design choice that aligns each phase with its purpose.

Comment thread crates/refinery_core/src/brainstorm.rs Outdated
- Add value_parser range (1..=20) for --panel-size to reject 0
- Always use propose_with_score_history_prompt (handles empty history
  internally) instead of passing raw prompt on first round

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (4)
crates/refinery_cli/src/commands/brainstorm.rs (3)

92-95: Consider removing unused _model_ids binding.

The _model_ids variable from build_providers is not used. If it's not needed for this command, consider pattern matching with _ or just .. if the return type allows.

♻️ Suggested simplification
-    let (_model_ids, providers, display) = match build_providers(shared).await {
+    let (_, providers, display) = match build_providers(shared).await {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 92 - 95, The
tuple binding captures an unused `_model_ids` from
build_providers(shared).await; change the pattern to ignore that element (e.g.,
use `(_, providers, display)` or `(.., providers, display)` in the match) so the
unused variable is removed and the intent is clear when matching the Ok result
of build_providers in the brainstorm command.

169-171: Silent failure on JSON serialization could hide errors.

If serde_json::to_string_pretty fails, the code silently continues without printing anything. Consider logging or printing a fallback error message.

♻️ Suggested improvement
-            if let Ok(json) = serde_json::to_string_pretty(&json_output) {
-                println!("{json}");
-            }
+            match serde_json::to_string_pretty(&json_output) {
+                Ok(json) => println!("{json}"),
+                Err(e) => eprintln!("Failed to serialize output: {e}"),
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 169 - 171, The
serde_json::to_string_pretty call on json_output is currently ignored on error,
causing silent failures; change the block that uses
serde_json::to_string_pretty(&json_output) so it matches both Ok and Err — on Ok
print the formatted JSON as before, and on Err log or print a clear error
message (including the serde_json::Error) and optionally fall back to a less
strict representation (e.g., debug format of json_output) so the user sees
output; look for the serde_json::to_string_pretty usage and the json_output
binding in brainstorm.rs to apply this change.

162-167: total_rounds shows configured max, not necessarily actual rounds completed.

The total_rounds field reports max_rounds (the configured maximum), which is accurate for the current implementation since the loop always runs all rounds. However, if early termination is added in the future (e.g., convergence detection), this would become misleading.

Also, models_dropped is hardcoded to an empty vector. Consider either populating it with models that failed to propose in any round, or removing it from the output if brainstorm doesn't track dropped models.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 162 - 167, The
MetadataOutput currently sets total_rounds to the configured max_rounds and
models_dropped to an empty vec, which can be misleading; change the code that
constructs MetadataOutput so total_rounds reflects the actual rounds completed
(e.g., use the runtime counter/variable that tracks completed rounds or derive
it from result or loop state instead of max_rounds) and either populate
models_dropped with the real list of models that failed to propose during
brainstorming (track failures in the proposal loop and pass that vec into
MetadataOutput) or remove the models_dropped field from the output if you decide
not to track dropped models; update the construction that uses MetadataOutput {
total_rounds: ..., total_calls: result.total_calls, elapsed_ms:
elapsed.as_millis(), models_dropped: ... } accordingly.
crates/refinery_core/src/brainstorm.rs (1)

133-143: Single-model case uses sentinel value 0.0 that may skew score history.

When only one model proposes, the code pushes 0.0 as the score. This sentinel value is included in subsequent score history and affects downstream calculations. While the context snippet confirms ScoreHistory accepts any f64, consider whether a neutral placeholder (e.g., 5.0 as midpoint of 1-10 scale) or NaN would better represent "not evaluated" semantics.

This is a design choice rather than a bug—document the rationale if 0.0 is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 133 - 143, The code
currently inserts a sentinel score 0.0 into score_histories when only one model
proposes (inside the round_proposals loop), which can skew downstream
calculations; change the inserted score to f64::NAN (push (answer.clone(),
f64::NAN)) to represent "not evaluated" semantics and update any downstream
aggregations/readers of score_histories (where averages/weights are computed) to
explicitly ignore NaN values (e.g., filter out is_nan before summing/averaging)
and ensure latest_answers and last_round_eval_scores logic remains unchanged;
document this behavior in comments beside the score_histories update so future
readers know NaN means "no evaluation" rather than a real score.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/refinery_cli/src/commands/brainstorm.rs`:
- Around line 92-95: The tuple binding captures an unused `_model_ids` from
build_providers(shared).await; change the pattern to ignore that element (e.g.,
use `(_, providers, display)` or `(.., providers, display)` in the match) so the
unused variable is removed and the intent is clear when matching the Ok result
of build_providers in the brainstorm command.
- Around line 169-171: The serde_json::to_string_pretty call on json_output is
currently ignored on error, causing silent failures; change the block that uses
serde_json::to_string_pretty(&json_output) so it matches both Ok and Err — on Ok
print the formatted JSON as before, and on Err log or print a clear error
message (including the serde_json::Error) and optionally fall back to a less
strict representation (e.g., debug format of json_output) so the user sees
output; look for the serde_json::to_string_pretty usage and the json_output
binding in brainstorm.rs to apply this change.
- Around line 162-167: The MetadataOutput currently sets total_rounds to the
configured max_rounds and models_dropped to an empty vec, which can be
misleading; change the code that constructs MetadataOutput so total_rounds
reflects the actual rounds completed (e.g., use the runtime counter/variable
that tracks completed rounds or derive it from result or loop state instead of
max_rounds) and either populate models_dropped with the real list of models that
failed to propose during brainstorming (track failures in the proposal loop and
pass that vec into MetadataOutput) or remove the models_dropped field from the
output if you decide not to track dropped models; update the construction that
uses MetadataOutput { total_rounds: ..., total_calls: result.total_calls,
elapsed_ms: elapsed.as_millis(), models_dropped: ... } accordingly.

In `@crates/refinery_core/src/brainstorm.rs`:
- Around line 133-143: The code currently inserts a sentinel score 0.0 into
score_histories when only one model proposes (inside the round_proposals loop),
which can skew downstream calculations; change the inserted score to f64::NAN
(push (answer.clone(), f64::NAN)) to represent "not evaluated" semantics and
update any downstream aggregations/readers of score_histories (where
averages/weights are computed) to explicitly ignore NaN values (e.g., filter out
is_nan before summing/averaging) and ensure latest_answers and
last_round_eval_scores logic remains unchanged; document this behavior in
comments beside the score_histories update so future readers know NaN means "no
evaluation" rather than a real score.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fed6901f-a776-4a98-bd0a-04e777bd5911

📥 Commits

Reviewing files that changed from the base of the PR and between d7c54ce and 35efb18.

📒 Files selected for processing (2)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_core/src/brainstorm.rs
  • crates/refinery_cli/src/commands/brainstorm.rs
🧠 Learnings (1)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
🔇 Additional comments (9)
crates/refinery_core/src/brainstorm.rs (6)

14-20: LGTM!

The BrainstormConfig struct is well-designed with clear, appropriate fields for controlling the brainstorm loop behavior.


79-82: LGTM!

The prompt handling now consistently uses propose_with_score_history_prompt with an empty vector fallback, addressing the previous review feedback.


156-165: LGTM!

The evaluation logic correctly ensures only models that successfully proposed can participate as evaluators, and self-evaluation is properly skipped. This maintains fairness in the cross-evaluation scheme.


275-293: LGTM!

Test utilities eval_json and default_config are well-designed helpers that reduce boilerplate and make tests more readable.


247-268: No issue: scoring functions safely return 0.0 for empty evaluation vectors, not NaN.

The mean(), stddev(), and controversy_score() functions explicitly handle empty input by returning 0.0 (see scoring.rs lines 6-37 and the test at line 70-72). Candidates with no evaluations will have mean_score, stddev, and controversy_score all equal to 0.0, which naturally sorts them to the bottom of the panel during select_panel() sorting. No NaN values are produced, and no guard or additional handling is needed.

			> Likely an incorrect or invalid review comment.

346-393: No test flakiness concern—evaluation pairs are correctly tracked.

The spawned evaluation tasks capture both evaluator_id and evaluatee_id in their closures (lines 29–30) and return these identifiers with each result (line 42). When results are collected (lines 50–62), scores are matched to the correct evaluatee via the returned identifiers, not inferred from completion order. HashMap iteration order affects only the spawn sequence, not result attribution.

Additionally, each evaluator in this test returns uniform scores (A:7, B:3, C:9 regardless of evaluatee), so the test outcome is deterministic regardless of any iteration order variance.

crates/refinery_cli/src/commands/brainstorm.rs (3)

21-26: LGTM!

Both max_rounds and panel_size now have proper range validation (1..=20) via value_parser, addressing the previous review feedback about rejecting invalid values like 0.


74-77: LGTM!

The dry-run estimation correctly calculates n + n*(n-1) calls per round (proposals + cross-evaluations), and the truncation allow is appropriate given the small expected model counts.


196-218: LGTM with minor note.

Error emission is straightforward. The retryable: true hardcoding is acceptable for now since most brainstorm errors (timeouts, provider failures) are indeed retryable. The same JSON serialization silent-failure pattern exists here as in emit_success.

Thomas and others added 2 commits April 1, 2026 21:49
BrainstormResult now tracks rounds_completed so metadata reflects
real work done, not the configured maximum.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
crates/refinery_core/src/brainstorm.rs (1)

211-217: Out-of-range scores are silently discarded rather than clamped.

The .filter(|s| (1.0..=10.0).contains(s)) rejects scores outside [1.0, 10.0] entirely. If a model returns a valid evaluation with score: 0 or score: 11, the entire evaluation is dropped. This may be intentional (invalid responses should be ignored), but it differs from clamping behavior that some systems use.

Consider whether this is the desired behavior, especially since the schema (per context snippet 3) doesn't enforce min/max bounds, so models might occasionally produce boundary violations.

🔧 Alternative: clamp instead of discard
-                    let score_val = parsed
-                        .get("score")
-                        .and_then(|v| v.as_u64().map(|u| u as f64).or_else(|| v.as_f64()))
-                        .filter(|s| (1.0..=10.0).contains(s));
+                    let score_val = parsed
+                        .get("score")
+                        .and_then(|v| v.as_u64().map(|u| u as f64).or_else(|| v.as_f64()))
+                        .map(|s| s.clamp(1.0, 10.0));

Based on learnings: "In crates/refinery_core/src/prompts/synthesize.rs, SYNTHESIS_EVAL_SCHEMA intentionally omits JSON Schema minimum/maximum bounds... Runtime validation in parse_evaluation is the authoritative range check instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 211 - 217, The code in
the score extraction path (the parsed -> score_val logic in brainstorm.rs)
currently filters out scores outside 1.0..=10.0 causing out-of-range evaluations
to be dropped; change this to clamp values into the allowed range instead of
discarding them: after extracting the numeric score from parsed.get("score")
(the value currently assigned to score_val) apply a clamp to the 1.0–10.0
interval and then push the (from, clamped_score) into
round_scores.entry(to).or_default().push(...); keep the existing numeric parsing
logic but replace the .filter(|s|...) drop behavior with a clamping step so
malformed non-numeric values are still ignored but numeric out-of-range values
are preserved as clamped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/refinery_core/src/brainstorm.rs`:
- Around line 211-217: The code in the score extraction path (the parsed ->
score_val logic in brainstorm.rs) currently filters out scores outside
1.0..=10.0 causing out-of-range evaluations to be dropped; change this to clamp
values into the allowed range instead of discarding them: after extracting the
numeric score from parsed.get("score") (the value currently assigned to
score_val) apply a clamp to the 1.0–10.0 interval and then push the (from,
clamped_score) into round_scores.entry(to).or_default().push(...); keep the
existing numeric parsing logic but replace the .filter(|s|...) drop behavior
with a clamping step so malformed non-numeric values are still ignored but
numeric out-of-range values are preserved as clamped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e043b434-69db-46b5-9043-63a4918bb30b

📥 Commits

Reviewing files that changed from the base of the PR and between 35efb18 and b4f71d3.

📒 Files selected for processing (3)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
  • todos/014-brainstorm-r4-wording-clarification.md
✅ Files skipped from review due to trivial changes (1)
  • todos/014-brainstorm-r4-wording-clarification.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
🧠 Learnings (1)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
🔇 Additional comments (13)
crates/refinery_core/src/brainstorm.rs (6)

14-43: LGTM!

The struct definitions are clean and well-organized. BrainstormConfig captures all necessary parameters, BrainstormResult includes useful metadata tracking (rounds_completed), and BrainstormError correctly implements Display and Error traits.


56-68: LGTM!

The semaphore permit calculation (providers.len().pow(2)) appropriately handles the worst-case concurrency of n propose + n*(n-1) evaluate tasks. The state initialization is clean.


80-83: LGTM!

The fix from the previous review is correctly implemented—always calling propose_with_score_history_prompt with an empty vec on first round ensures consistent framing across all rounds.


133-144: LGTM!

Correctly skips self-evaluation when only one model produces a valid proposal. The 0.0 score assignment and clearing of last_round_eval_scores maintain consistent state.


273-277: LGTM!

The rounds_completed accurately reflects config.max_rounds since the only early exit from the loop is via the error path (line 125-129). All PanelCandidate fields are correctly populated per the contract in scoring.rs.


280-415: LGTM!

Comprehensive test coverage for key scenarios: single-model behavior, multi-round accumulation, controversy-based ranking, and error propagation. The use of start_paused = true ensures deterministic async behavior.

crates/refinery_cli/src/commands/brainstorm.rs (7)

15-27: LGTM!

Both max_rounds and panel_size have appropriate range validation (1..=20) via value_parser, addressing the previous review feedback about panel_size >= 1.


31-52: LGTM!

Clean separation between core types and CLI-specific JSON output structures. This provides flexibility for CLI output formatting without coupling to internal types.


74-84: LGTM!

The calls_per_round formula n + n * (n - 1) correctly accounts for n proposals plus n*(n-1) evaluations (each model evaluates all others).


100-107: LGTM!

The BrainstormConfig construction correctly handles type conversions: panel_size as usize is safe, and Duration::from_secs(shared.timeout) properly converts the u64 seconds value.


109-121: LGTM!

Proper cleanup pattern: abort the tick handle if present, finish the display, then handle the result. Error mapping to ExitCode::from(1) is consistent with CLI conventions.


123-193: LGTM!

Good defensive check for empty panel (line 128). The JSON and text output formats are well-structured and include all relevant metadata. The models_dropped: vec![] makes sense since brainstorm doesn't have model-dropping logic.


195-217: LGTM!

Consistent error formatting with proper use of stderr for both JSON and text output modes. The retryable: true default is consistent with other CLI commands in the codebase.

… single model

- Dry-run now rejects empty --models before computing n*(n-1)
- Single-model rounds no longer record 0.0 mean score in history

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/refinery_cli/src/commands/brainstorm.rs`:
- Around line 79-81: The dry-run estimate truncates because calls_per_round is
computed as (n + n * (n - 1)) as u32 then multiplied into total, so switch the
intermediate arithmetic to u64: compute calls_per_round as a u64 using n as u64
(e.g., cast n to u64 before the expression), compute total as u64 by multiplying
calls_per_round by args.max_rounds converted to u64 (or otherwise ensure
args.max_rounds is used as u64), and only cast down if absolutely needed for
downstream APIs; update the variables named calls_per_round and total in
brainstorm.rs to use u64 to avoid silent truncation.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52618866-b22c-445b-9137-5af7335fa5cf

📥 Commits

Reviewing files that changed from the base of the PR and between b4f71d3 and ff99d83.

📒 Files selected for processing (2)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/refinery_core/src/brainstorm.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_cli/src/commands/brainstorm.rs
🧠 Learnings (1)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
🔇 Additional comments (1)
crates/refinery_cli/src/commands/brainstorm.rs (1)

172-174: and

Comment on lines +79 to +81
#[allow(clippy::cast_possible_truncation)]
let calls_per_round = (n + n * (n - 1)) as u32;
let total = calls_per_round * args.max_rounds;

@coderabbitai coderabbitai Bot Apr 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file around lines 79-81
head -100 crates/refinery_cli/src/commands/brainstorm.rs | tail -50

Repository: Lightless-Labs/refinery

Length of output: 1670


🏁 Script executed:

# Let's also check the full context of this function
sed -n '1,150p' crates/refinery_cli/src/commands/brainstorm.rs

Repository: Lightless-Labs/refinery

Length of output: 4414


🏁 Script executed:

# Check the BrainstormArgs struct to understand the types
rg "struct BrainstormArgs|max_rounds|panel_size" crates/refinery_cli/src/commands/brainstorm.rs -A 2

Repository: Lightless-Labs/refinery

Length of output: 832


🏁 Script executed:

# Search for tests related to brainstorm command
find crates -name "*.rs" -path "*/tests/*" -o -path "*test*" | xargs rg "brainstorm|dry.run" 2>/dev/null | head -20

Repository: Lightless-Labs/refinery

Length of output: 49


🏁 Script executed:

# Check if there are any limits on the number of models in SharedArgs
rg "struct SharedArgs" crates/refinery_cli -A 30

Repository: Lightless-Labs/refinery

Length of output: 2500


🏁 Script executed:

# Look for any documentation or comments about model limits
rg "models" crates/refinery_cli/src/commands/brainstorm.rs -B 2 -A 2 | head -40

Repository: Lightless-Labs/refinery

Length of output: 613


Use a wider integer for the dry-run estimate.

(n + n * (n - 1)) as u32 truncates when the model list exceeds ~65,536 entries, causing Total calls (max) to silently underreport. Since this is a cost-estimation path, keep the arithmetic in u64 all the way through.

💡 Proposed fix
-        #[allow(clippy::cast_possible_truncation)]
-        let calls_per_round = (n + n * (n - 1)) as u32;
-        let total = calls_per_round * args.max_rounds;
+        let n = n as u64;
+        let calls_per_round = n * n;
+        let total = calls_per_round * u64::from(args.max_rounds);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 79 - 81, The
dry-run estimate truncates because calls_per_round is computed as (n + n * (n -
1)) as u32 then multiplied into total, so switch the intermediate arithmetic to
u64: compute calls_per_round as a u64 using n as u64 (e.g., cast n to u64 before
the expression), compute total as u64 by multiplying calls_per_round by
args.max_rounds converted to u64 (or otherwise ensure args.max_rounds is used as
u64), and only cast down if absolutely needed for downstream APIs; update the
variables named calls_per_round and total in brainstorm.rs to use u64 to avoid
silent truncation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The u32 truncation would only matter with 65K+ models. Practical model counts are 2-10 (max we've ever tested is 5). The --max-rounds cap is 20, so the maximum realistic total is 10*10*20 = 2000 calls — well within u32. This matches the same pattern used in converge and synthesize's dry-run paths. Not fixing — but acknowledged as theoretically valid for extreme inputs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@El-Fitz, that's a fair call — with --max-rounds hard-capped at 20 by clap and practical model counts staying in single digits, the arithmetic is safe in u32 and the consistency with converge/synthesize's dry-run paths is a good reason to keep it as-is. Acknowledged!

(╯°□°)╯ ┬─┬


🧠 Learnings used
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_cli/src/commands/common.rs:420-429
Timestamp: 2026-03-27T14:40:09.403Z
Learning: In `crates/refinery_cli/src/commands/common.rs`, model IDs used in `save_round_artifacts` artifact filenames are constrained to the pattern `[a-z0-9-]+/[a-z0-9-.]+` because all model IDs are produced exclusively through `parse_model_spec()`. The simple `replace('/', "_")` sanitization is intentionally sufficient for the current model ID format. Hash-suffix hardening was deferred until arbitrary model IDs from config files or environment variables are supported. (PR `#26`)

Artifacts are written incrementally — each round's proposals and
eval scores are saved as soon as the round finishes, so data survives
crashes. Panel summary (panel.json) written after final selection.

Saving logic moved from CLI into refinery_core::brainstorm via
output_dir config field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@El-Fitz El-Fitz force-pushed the feat/brainstorm-verb branch from fec3fa2 to 743a359 Compare April 2, 2026 15:04
Thomas and others added 2 commits April 3, 2026 22:00
- Fix "closing strategy" → "selection strategy" in TODO 004
- Remove stale --diversity-threshold reference
- Create TODO 015 (upvote/downvote terminology clarification)
- Create TODO 016 (silent JSON serialization across all verbs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Brainstormed status no longer maps to SUCCESS in converge exit code
- Document v0 controversy formula (mean * stddev) in TODO 004
- Fix "different than" → "different from" in evolve TODO
- Create TODO 017 for remaining P3 nitpicks (dry-run JSON, empty
  providers validation, ScoreHistory named struct)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@El-Fitz

El-Fitz commented Apr 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressing nested nitpicks from CodeRabbit review bodies

Read all 4 review bodies' nested nitpick sections. Here's the triage:

Fixed (commit e84776f):

  • Brainstormed status in converge exit code → moved to error arm (was masking potential routing bugs)
  • "different than" → "different from" in evolve TODO
  • Documented v0 controversy formula (mean * stddev) in TODO 004

Already fixed in prior commits:

  • panel_size validation → 35efb18
  • First-round prompt inconsistency (CLI + core) → 35efb18
  • Single-model 0.0 scores → ff99d83
  • total_rounds vs rounds_completed37f85c7
  • Silent JSON serialization → tracked in TODO 016

Triaged to TODOs:

  • todos/017-brainstorm-p3-nitpicks.md — dry-run JSON output, empty providers validation, ScoreHistory named struct
  • todos/016-silent-json-serialization-failure.md — affects all verbs
  • todos/015-brainstorm-todo-upvote-terminology.md — upvote/downvote wording
  • todos/014-brainstorm-r4-wording-clarification.md — R4 requirement text

Acknowledged, not fixing:

  • u32 truncation for 65K+ models — replied on inline comment
  • Plan missing reviewed date — plan is status:completed, adding lifecycle dates retroactively adds no value

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (4)
crates/refinery_core/src/brainstorm.rs (3)

130-133: Silent failure handling is intentional but lacks observability.

Failed proposals and evaluations are silently counted without logging which models failed. This is appropriate for resilience (partial failures don't abort the run), but consider adding debug-level logging for troubleshooting when models fail.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 130 - 133, The match arm
that increments propose_count on failures (Ok((_, Ok(Err(_)) | Err(_))) |
Err(_)) is silently swallowing which model/evaluation failed; update that arm in
brainstorm.rs to emit a debug-level log before incrementing propose_count that
includes the model/evaluation identifier and error detail (use the existing
context variables available in the match to include model name/id and the Err
payload). Use the project's logging facility (e.g., debug! or
process_logger.debug()) so it remains non-intrusive in production but available
for troubleshooting.

318-323: rounds_completed reports configured max, not actual rounds.

Line 321 always sets rounds_completed: config.max_rounds, but if an early round fails (all proposals empty), the function returns early with an error and never reaches this point. However, if some models fail but at least one succeeds, rounds continue — so rounds_completed should reflect reality.

Currently this is accurate because the loop always completes all rounds when at least one model succeeds. However, if early-exit logic is ever added (e.g., convergence detection), this would silently misreport.

🔧 Suggested defensive fix

Track actual completed rounds:

     let mut round_data: Vec<BrainstormRound> = Vec::new();
+    let mut rounds_completed: u32 = 0;

     for round in 1..=config.max_rounds {
+        // ... existing code ...
+        rounds_completed = round;  // at end of each round's processing
     }
     // ...
     Ok(BrainstormResult {
         panel,
         total_calls,
-        rounds_completed: config.max_rounds,
+        rounds_completed,
         rounds: round_data,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 318 - 323, The code
currently sets BrainstormResult.rounds_completed to config.max_rounds which can
misreport if fewer rounds actually ran; change it to compute the real completed
rounds (e.g., use round_data.len() or a dedicated counter incremented when each
round finishes) and set rounds_completed to that value in the BrainstormResult
constructor (replace the reference to config.max_rounds). Ensure this uses the
same scope where round_data is available so BrainstormResult.rounds_completed
accurately reflects actual completed rounds.

326-355: Artifact saving uses blocking I/O in async context.

std::fs::write and std::fs::create_dir_all are blocking calls. While this is acceptable given artifacts are optional and saved at round boundaries (not on hot paths), consider documenting this trade-off or using tokio::fs for consistency if artifact saving becomes performance-sensitive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 326 - 355, The
save_round_artifacts function currently uses blocking std::fs calls; change it
to async by making save_round_artifacts async and replace
std::fs::create_dir_all and std::fs::write with tokio::fs::create_dir_all and
tokio::fs::write (await each call), keep the serde_json creation logic the same,
and update all callers to .await the new async function; also adjust the Result
error bound if necessary (e.g., Box<dyn std::error::Error + Send + Sync>) so it
compiles in async contexts.
crates/refinery_cli/src/commands/brainstorm.rs (1)

178-181: Silent serialization failure could hide issues.

Lines 178-180 use if let Ok(json) which silently drops serialization errors. While unlikely to fail for these simple structs, consider logging or falling back to text output if JSON serialization fails.

-            if let Ok(json) = serde_json::to_string_pretty(&json_output) {
-                println!("{json}");
-            }
+            match serde_json::to_string_pretty(&json_output) {
+                Ok(json) => println!("{json}"),
+                Err(e) => eprintln!("Failed to serialize JSON output: {e}"),
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_cli/src/commands/brainstorm.rs` around lines 178 - 181, The
current use of serde_json::to_string_pretty(&json_output) is wrapped in `if let
Ok(json)` which drops serialization errors silently; update the code around that
call (the block handling `json_output` in brainstorm.rs) to handle the Err case
by logging the serialization error (e.g., via eprintln! or process logger) and
falling back to a readable text dump of `json_output` (e.g., debug/pretty-print)
so failures are visible instead of being ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/refinery_cli/src/commands/brainstorm.rs`:
- Around line 178-181: The current use of
serde_json::to_string_pretty(&json_output) is wrapped in `if let Ok(json)` which
drops serialization errors silently; update the code around that call (the block
handling `json_output` in brainstorm.rs) to handle the Err case by logging the
serialization error (e.g., via eprintln! or process logger) and falling back to
a readable text dump of `json_output` (e.g., debug/pretty-print) so failures are
visible instead of being ignored.

In `@crates/refinery_core/src/brainstorm.rs`:
- Around line 130-133: The match arm that increments propose_count on failures
(Ok((_, Ok(Err(_)) | Err(_))) | Err(_)) is silently swallowing which
model/evaluation failed; update that arm in brainstorm.rs to emit a debug-level
log before incrementing propose_count that includes the model/evaluation
identifier and error detail (use the existing context variables available in the
match to include model name/id and the Err payload). Use the project's logging
facility (e.g., debug! or process_logger.debug()) so it remains non-intrusive in
production but available for troubleshooting.
- Around line 318-323: The code currently sets BrainstormResult.rounds_completed
to config.max_rounds which can misreport if fewer rounds actually ran; change it
to compute the real completed rounds (e.g., use round_data.len() or a dedicated
counter incremented when each round finishes) and set rounds_completed to that
value in the BrainstormResult constructor (replace the reference to
config.max_rounds). Ensure this uses the same scope where round_data is
available so BrainstormResult.rounds_completed accurately reflects actual
completed rounds.
- Around line 326-355: The save_round_artifacts function currently uses blocking
std::fs calls; change it to async by making save_round_artifacts async and
replace std::fs::create_dir_all and std::fs::write with
tokio::fs::create_dir_all and tokio::fs::write (await each call), keep the
serde_json creation logic the same, and update all callers to .await the new
async function; also adjust the Result error bound if necessary (e.g., Box<dyn
std::error::Error + Send + Sync>) so it compiles in async contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b378588e-6b7e-419b-a113-b1b49572f1ff

📥 Commits

Reviewing files that changed from the base of the PR and between b4f71d3 and b6b96de.

📒 Files selected for processing (5)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
  • todos/004-verb-brainstorm.md
  • todos/015-brainstorm-todo-upvote-terminology.md
  • todos/016-silent-json-serialization-failure.md
✅ Files skipped from review due to trivial changes (3)
  • todos/015-brainstorm-todo-upvote-terminology.md
  • todos/016-silent-json-serialization-failure.md
  • todos/004-verb-brainstorm.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
🧠 Learnings (2)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
📚 Learning: 2026-03-27T14:40:09.403Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_cli/src/commands/common.rs:420-429
Timestamp: 2026-03-27T14:40:09.403Z
Learning: In `crates/refinery_cli/src/commands/common.rs`, model IDs used in `save_round_artifacts` artifact filenames are constrained to the pattern `[a-z0-9-]+/[a-z0-9-.]+` because all model IDs are produced exclusively through `parse_model_spec()`. The simple `replace('/', "_")` sanitization is intentionally sufficient for the current model ID format. Hash-suffix hardening was deferred until arbitrary model IDs from config files or environment variables are supported. (PR `#26`)

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
🔇 Additional comments (7)
crates/refinery_core/src/brainstorm.rs (3)

1-56: LGTM — Types and error handling are well-structured.

The config, result, round data, and error types are cleanly designed with appropriate derives. BrainstormError implementing Display and Error traits follows Rust conventions.


286-307: Panel selection handles edge cases correctly.

When a model has no evaluation scores (single-model case or all evaluations failed), the scoring functions return 0.0 (per context snippets 2-4), and the candidate is still included in the panel. This is the intended behavior — a single successful proposer should still populate the panel.


382-517: LGTM — Tests cover key scenarios.

The test suite validates single-model behavior, multi-round accumulation, controversy-based ranking, and error propagation. The use of start_paused = true ensures deterministic async execution.

crates/refinery_cli/src/commands/brainstorm.rs (4)

15-27: LGTM — CLI arguments are properly validated.

Both max_rounds and panel_size have appropriate range constraints (1..=20) via clap's value parser, preventing invalid inputs at parse time.


61-89: Dry-run validation and estimation look correct.

The empty models check (lines 75-78) and call estimation formula (n + n*(n-1) = n² calls per round for propose + cross-eval) are accurate. The u32 truncation concern was previously acknowledged as acceptable given practical model count limits.


91-131: LGTM — Main execution path is well-structured.

Prompt resolution, provider building, config construction, and result handling follow the established patterns from other commands. The panel_size as usize cast on line 111 is safe since u32 is bounded by the value parser to 1-20.


205-227: LGTM — Error emission follows established patterns.

The emit_error function provides consistent JSON/text error output with appropriate metadata. Setting retryable: true is reasonable since brainstorm failures are typically transient provider issues.

@coderabbitai coderabbitai Bot 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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/refinery_core/src/brainstorm.rs`:
- Around line 149-150: The code currently accumulates answers into
latest_answers across rounds then builds PanelCandidate objects from that global
map; instead, change the construction to use only the final round's
round_proposals (or filter latest_answers to keys present in the final round) so
stale answers from prior rounds are excluded; locate uses of latest_answers and
round_proposals in brainstorm.rs (variables latest_answers, round_proposals, and
the PanelCandidate creation) and replace the insertion/lookup logic so
candidates are derived solely from the last completed round_proposals, update
the other occurrences similarly, and add a regression test where a model answers
in round 1 but not in the final round to assert it is not included in the panel.
- Around line 249-259: The loop that pushes (answer, mean) into score_histories
currently computes scoring::mean(&scores) even for empty scores (from
round_scores.get(model_id) falling back to empty), producing a misleading zero;
update the logic in the loop over round_proposals to first fetch
round_scores.get(model_id) and skip adding to score_histories when that entry is
missing or its collected scores Vec is empty (only call scoring::mean and push
when scores.is_empty() is false), referencing the variables round_proposals,
round_scores, score_histories and the scoring::mean call so you only record
evaluated answers.

In `@todos/004-verb-brainstorm.md`:
- Around line 54-67: Update the TODO to match the shipped CLI and core outputs:
change the `--panel-size` default wording from "3-5" to the concrete default `3`
(to match the CLI flag default), and replace the mentioned output "variance"
with the actual emitted fields `stddev` and `controversy_score` (which are
produced by the brainstorm result emission in the core). Reference the
`--panel-size` flag and the core output fields `stddev` and `controversy_score`
so the spec text aligns with the implementation.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 79521fc7-f6b8-4412-b9f9-5e5a5a57a461

📥 Commits

Reviewing files that changed from the base of the PR and between b4f71d3 and e84776f.

📒 Files selected for processing (8)
  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_cli/src/commands/converge.rs
  • crates/refinery_core/src/brainstorm.rs
  • todos/004-verb-brainstorm.md
  • todos/011-verb-evolve.md
  • todos/015-brainstorm-todo-upvote-terminology.md
  • todos/016-silent-json-serialization-failure.md
  • todos/017-brainstorm-p3-nitpicks.md
✅ Files skipped from review due to trivial changes (4)
  • todos/015-brainstorm-todo-upvote-terminology.md
  • todos/016-silent-json-serialization-failure.md
  • todos/017-brainstorm-p3-nitpicks.md
  • todos/011-verb-evolve.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/refinery_cli/src/commands/converge.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use Cargo for building and dependency management in the Rust library
Run all tests using cargo test --workspace

Files:

  • crates/refinery_cli/src/commands/brainstorm.rs
  • crates/refinery_core/src/brainstorm.rs
🧠 Learnings (2)
📚 Learning: 2026-03-19T05:56:03.585Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_core/src/prompts/synthesize.rs:6-7
Timestamp: 2026-03-19T05:56:03.585Z
Learning: In `crates/refinery_core/src/prompts/synthesize.rs`, `SYNTHESIS_EVAL_SCHEMA` intentionally omits JSON Schema `minimum`/`maximum` bounds on `integration`, `coherence`, `completeness`, `fidelity`, and `score` (all 1–10 integers) because provider CLI enforcement of these constraints varies. Runtime validation in `parse_evaluation` is the authoritative range check instead.

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs
📚 Learning: 2026-03-27T14:40:09.403Z
Learnt from: El-Fitz
Repo: Lightless-Labs/refinery PR: 26
File: crates/refinery_cli/src/commands/common.rs:420-429
Timestamp: 2026-03-27T14:40:09.403Z
Learning: In `crates/refinery_cli/src/commands/common.rs`, model IDs used in `save_round_artifacts` artifact filenames are constrained to the pattern `[a-z0-9-]+/[a-z0-9-.]+` because all model IDs are produced exclusively through `parse_model_spec()`. The simple `replace('/', "_")` sanitization is intentionally sufficient for the current model ID format. Hash-suffix hardening was deferred until arbitrary model IDs from config files or environment variables are supported. (PR `#26`)

Applied to files:

  • crates/refinery_cli/src/commands/brainstorm.rs

Comment on lines +147 to +165
// Single model: skip evaluation (no self-eval).
if round_proposals.len() == 1 {
for (model_id, answer) in &round_proposals {
latest_answers.insert(model_id.clone(), answer.clone());
}
let rd = BrainstormRound {
round,
proposals: round_proposals,
eval_scores: HashMap::new(),
};
if let Some(ref dir) = config.output_dir {
if let Err(e) = save_round_artifacts(dir, &rd) {
eprintln!("Warning: failed to save round {round} artifacts: {e}");
}
}
round_data.push(rd);
last_round_eval_scores.clear();
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t skip evaluation just because one proposal survived.

Line 148 treats “one surviving proposal” as “no evaluation possible”, and Line 180 then filters out every provider that did not propose. With multiple providers, those two checks drop legitimate external evaluations for the surviving answer, so the round loses its score history and panel metadata even though other models could still judge it.

🔧 Suggested fix
-        // Single model: skip evaluation (no self-eval).
-        if round_proposals.len() == 1 {
+        // Only skip evaluation when there is actually one provider.
+        if providers.len() == 1 && round_proposals.len() == 1 {
             let rd = BrainstormRound {
                 round,
                 proposals: round_proposals,
                 eval_scores: HashMap::new(),
             };
@@
         for evaluator_provider in providers {
             let evaluator_id = evaluator_provider.model_id().clone();
-            if !round_proposals.contains_key(&evaluator_id) {
-                continue;
-            }
+            let evaluator_proposed = round_proposals.contains_key(&evaluator_id);
 
             for (evaluatee_id, answer_text) in &round_proposals {
-                if *evaluatee_id == evaluator_id {
+                if evaluator_proposed && *evaluatee_id == evaluator_id {
                     continue;
                 }

Also applies to: 178-186

Comment on lines +149 to +150
for (model_id, answer) in &round_proposals {
latest_answers.insert(model_id.clone(), answer.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Only select candidates from the final completed round.

Line 149 and Line 261 only ever insert into latest_answers. If a model answers in round N-1 and then disappears in the last round, Line 286 still turns that stale answer into a PanelCandidate with no final-round scores. The panel should be built from the last round’s round_proposals, not every answer ever seen.

🔧 Suggested fix
         if round_proposals.is_empty() {
             return Err(BrainstormError {
                 round,
                 message: "All models failed to propose.".to_string(),
             });
         }
+
+        // Panel selection should only consider the latest completed round.
+        latest_answers = round_proposals.clone();
 
         // ── Evaluate ────────────────────────────────────────────────────
 
         // Single model: skip evaluation (no self-eval).
         if round_proposals.len() == 1 {
-            for (model_id, answer) in &round_proposals {
-                latest_answers.insert(model_id.clone(), answer.clone());
-            }
             let rd = BrainstormRound {
                 round,
                 proposals: round_proposals,
                 eval_scores: HashMap::new(),
             };
@@
             score_histories
                 .entry(model_id.clone())
                 .or_default()
                 .push((answer.clone(), mean));
-
-            latest_answers.insert(model_id.clone(), answer.clone());
         }

A regression test where one model proposes in round 1 but fails in round 2 would lock this down.

Also applies to: 261-261, 286-307

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 149 - 150, The code
currently accumulates answers into latest_answers across rounds then builds
PanelCandidate objects from that global map; instead, change the construction to
use only the final round's round_proposals (or filter latest_answers to keys
present in the final round) so stale answers from prior rounds are excluded;
locate uses of latest_answers and round_proposals in brainstorm.rs (variables
latest_answers, round_proposals, and the PanelCandidate creation) and replace
the insertion/lookup logic so candidates are derived solely from the last
completed round_proposals, update the other occurrences similarly, and add a
regression test where a model answers in round 1 but not in the final round to
assert it is not included in the panel.

Comment on lines +249 to +259
for (model_id, answer) in &round_proposals {
let scores: Vec<f64> = round_scores
.get(model_id)
.map(|s| s.iter().map(|(_, score)| *score).collect())
.unwrap_or_default();
let mean = scoring::mean(&scores);

score_histories
.entry(model_id.clone())
.or_default()
.push((answer.clone(), mean));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep unevaluated answers out of ScoreHistory.

Line 254 falls back to mean([]) when every evaluation for a proposal fails, and Line 259 records that 0.0 as if it were a real score. The brainstorm rubric is 1–10, so this tells the model it bombed instead of went unevaluated. This is the same misleading-zero case already avoided in the single-model path, and it still exists here for multi-model rounds with no valid scores.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/refinery_core/src/brainstorm.rs` around lines 249 - 259, The loop that
pushes (answer, mean) into score_histories currently computes
scoring::mean(&scores) even for empty scores (from round_scores.get(model_id)
falling back to empty), producing a misleading zero; update the logic in the
loop over round_proposals to first fetch round_scores.get(model_id) and skip
adding to score_histories when that entry is missing or its collected scores Vec
is empty (only call scoring::mean and push when scores.is_empty() is false),
referencing the variables round_proposals, round_scores, score_histories and the
scoring::mean call so you only record evaluated answers.

Comment on lines 54 to +67
- `--panel-size` — number of diverse answers to return (default: 3-5)
- `--diversity-threshold` — how different answers must be to count as distinct
- `--max-rounds` — rounds for breeding diversity
- Standard shared flags (models, timeout, output-format, etc.)

## Prompts

Different from converge:
- System prompt encouraging original thinking and novel perspectives
- Propose prompt that explicitly asks for unique angles (round 2+ shows what's already been said)
- Propose prompt presenting own prior answers + scores, asking for unique angles
- Evaluate prompt that scores on originality and insight, not just correctness

## Output

Panel of `--panel-size` answers, selected for quality + disagreement. Each with score distribution metadata (mean, variance, per-evaluator scores).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sync this spec with the shipped CLI/output.

Line 54 still says --panel-size defaults to 3-5, but crates/refinery_cli/src/commands/brainstorm.rs Line 25 sets a concrete default of 3. Line 67 says the output includes variance, while crates/refinery_core/src/brainstorm.rs Lines 366-368 emit stddev and controversy_score. The TODO should match the interface that just landed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/004-verb-brainstorm.md` around lines 54 - 67, Update the TODO to match
the shipped CLI and core outputs: change the `--panel-size` default wording from
"3-5" to the concrete default `3` (to match the CLI flag default), and replace
the mentioned output "variance" with the actual emitted fields `stddev` and
`controversy_score` (which are produced by the brainstorm result emission in the
core). Reference the `--panel-size` flag and the core output fields `stddev` and
`controversy_score` so the spec text aligns with the implementation.

@El-Fitz El-Fitz added this pull request to the merge queue Apr 4, 2026
Merged via the queue into main with commit 36b63f1 Apr 4, 2026
4 checks passed
El-Fitz added a commit that referenced this pull request Apr 6, 2026
- Brainstormed status no longer maps to SUCCESS in converge exit code
- Document v0 controversy formula (mean * stddev) in TODO 004
- Fix "different than" → "different from" in evolve TODO
- Create TODO 017 for remaining P3 nitpicks (dry-run JSON, empty
  providers validation, ScoreHistory named struct)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to 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.

2 participants