Skip to content

fix: use --robot-next for task selection to prevent picking blocked tasks#329

Merged
subsy merged 9 commits intosubsy:mainfrom
mwarger:fix/beads-bv-blocked-task-selection
Feb 24, 2026
Merged

fix: use --robot-next for task selection to prevent picking blocked tasks#329
subsy merged 9 commits intosubsy:mainfrom
mwarger:fix/beads-bv-blocked-task-selection

Conversation

@mwarger
Copy link
Copy Markdown
Contributor

@mwarger mwarger commented Feb 22, 2026

Summary

  • Switch getNextTask() from bv --robot-triage to bv --robot-next to prevent selecting blocked tasks
  • --robot-next is guaranteed by BV to only return an unblocked task
  • Keep --robot-triage for background metadata enrichment (getTasks, cacheTaskReasoning, getTriageStats)

Fixes #327

Problem

bv --robot-triage returns recommendations[] sorted by composite score (PageRank, betweenness, blocker ratio, etc.). This array includes both actionable and blocked tasks. A blocked task with high graph importance (e.g., a review bead at a phase boundary that unblocks many downstream tasks) can outscore all actionable tasks.

The previous code took recommendations[0] without checking the blocked_by field, causing ralph-tui to select and execute tasks whose dependencies hadn't been completed yet.

Real-world example from an epic with 26 beads:

Rank Task Score Blocked?
1 BUGSCAN-002: Integration fresh-eyes review 0.515 Yes (by REVIEW-002)
2 US-001: Initialize project 0.420 No

The agent executed the bugscan on a greenfield project with no code, completed it, and marked it closed — wasting an iteration and corrupting the dependency graph.

Fix

Use bv --robot-next for task selection, which only returns the single best unblocked task. This is the simplest and most robust fix since BV handles all dependency filtering internally.

--robot-triage is still used via refreshTriage() (fired in background after selection) to populate the metadata cache that getTasks(), cacheTaskReasoning(), and getTriageStats() rely on.

Testing

  • Typecheck passes clean
  • All existing tests pass (3264 pass, 3 pre-existing failures in template engine tests)
  • Manually tested with a synthetic epic (3 impl beads → REVIEW → BUGSCAN → AUDIT, all wired with bd dep add): correct task ordering through the full dependency chain

Summary by CodeRabbit

  • New Features

    • Switches to a robot-driven "next task" selection and exposes blocked-by on recommendations.
  • Improvements

    • Uses robot-next output for task selection, validating epic membership and falling back when mismatched or empty.
    • Enriches tasks with robot-provided score, reasons, unblocks and breakdown metadata.
    • Schedules background triage refreshes and reuses cached breakdowns when available.
    • Builds a fallback task when full task details are unavailable.
  • Tests

    • Adds robust tests for robot-next scenarios, fallbacks, caching and refresh behaviour.

…asks

bv --robot-triage returns recommendations that include blocked tasks
ranked by score. A blocked task with high graph importance (e.g., a
review bead that unblocks many downstream tasks) can outscore actionable
tasks, causing ralph-tui to select tasks whose dependencies haven't
been completed yet.

Switch getNextTask() to use bv --robot-next, which is guaranteed to
return only an unblocked task. Keep --robot-triage for background
metadata enrichment (getTasks, cacheTaskReasoning, getTriageStats).

Fixes subsy#327
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 22, 2026

@mwarger is attempting to deploy a commit to the plgeek Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces BV --robot-triage with --robot-next for selection, adds BvRobotNext output types, validates epic membership, enriches or constructs tasks from nextOutput (including bv metadata), keeps scheduled triage refresh, removes legacy mapping helpers, and extends tests with spawn-mocking and robot-next scenarios.

Changes

Cohort / File(s) Summary
BV Workflow & Types
src/plugins/trackers/builtin/beads-bv/index.ts
Adds BvRobotNextTask, BvRobotNextEmpty, BvRobotNextOutput; adds blocked_by?: string[] to BvRecommendation; removes legacy mapping helpers (mapPriority, mapStatus, mapStatusToBd, recommendationToTask).
Selection: --robot-next & parsing
src/plugins/trackers/builtin/beads-bv/index.ts
Replaces --robot-triage usage with --robot-next; parses nextOutput; handles empty/no-actionable-items by falling back to base beads; guards on message in nextOutput.
Epic validation & fallback
src/plugins/trackers/builtin/beads-bv/index.ts
Checks nextOutput.id against cached epic children; falls back to base beads when not a member; removes previous epic/status filtering in favour of epic-membership validation.
Metadata enrichment & fallback task construction
src/plugins/trackers/builtin/beads-bv/index.ts
Enriches full task with bvScore, bvReasons, bvUnblocks when available; constructs fallback TrackerTask from nextOutput when full retrieval fails; preserves cached bv breakdown usage.
Background triage refresh & reasoning cache
src/plugins/trackers/builtin/beads-bv/index.ts
Renames/keeps background triage refresh flow (scheduleTriageRefresh), retains TaskReasoning cache and related helpers adapted to new output shape.
Tests: spawn-mocking & robot-next scenarios
src/plugins/trackers/builtin/beads-bv/index.test.ts
Adds spawn response queue and mocked child_process.spawn, beforeEach reset; tests for --robot-next flows: no-actionable fallback, epic-membership fallback, fallback task construction with bv metadata, epic-children caching, and triage refresh scheduling.
Test types & imports
src/plugins/trackers/builtin/beads-bv/index.test.ts
Introduces MockSpawnResponse/queue helpers; imports BeadsTrackerPlugin alongside BeadsBvTrackerPlugin; exposes plugin type for targeted overrides.

Sequence Diagram(s)

sequenceDiagram
  participant Plugin as beads-bv plugin
  participant BV as BV CLI (--robot-next)
  participant Tracker as Tracker API/DB
  participant Base as base beads fallback

  Plugin->>BV: run `bv --robot-next ...`
  BV-->>Plugin: return nextOutput (task or empty message)
  alt nextOutput empty
    Plugin->>Base: fallback to base beads flow
    Base-->>Plugin: base beads selection
  else nextOutput task
    Plugin->>Tracker: fetch full task by nextOutput.id
    alt full task found
      Tracker-->>Plugin: full task
      Plugin->>Plugin: enrich task with bvScore/bvReasons/bvUnblocks
      Plugin-->>Tracker: return enriched next task
    else full task missing
      Plugin->>Plugin: construct fallback TrackerTask from nextOutput (pending/priority defaults + bv metadata)
      Plugin-->>Tracker: return constructed task
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarises the main change: switching from --robot-triage to --robot-next to prevent selecting blocked tasks.
Linked Issues check ✅ Passed The implementation fully addresses issue #327 by replacing --robot-triage with --robot-next, which guarantees only unblocked tasks are returned.
Out of Scope Changes check ✅ Passed All code changes are directly related to implementing the --robot-next switch and supporting infrastructure (epic caching, triage refresh logic), with no unrelated modifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 (1)
src/plugins/trackers/builtin/beads-bv/index.ts (1)

374-384: Consider caching epic children to avoid repeated bd list calls.

getEpicChildrenIds spawns a child process on every getNextTask invocation when an epic is active. If task selection is called frequently (e.g. polling), this could add latency. A short-lived cache (TTL or invalidated on completeTask/updateTaskStatus) would eliminate redundant subprocess calls.

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

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 374 - 384,
getNextTask calls getEpicChildrenIds on every invocation which spawns a
subprocess; add a short-lived cache to avoid repeated bd list calls by caching
results inside the tracker instance and invalidating it on mutations. Implement
a simple TTL-backed cache field (e.g. epicChildrenCache with {ids, expiresAt})
used by getEpicChildrenIds so it returns cached ids when not expired, and ensure
cache is cleared/updated in completeTask and updateTaskStatus (and whenever
getEpicId changes) so stale results aren't used; keep fallback to
super.getNextTask unchanged when id not in children.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 48-61: The BvRobotNextOutput interface declares id as required but
the "no actionable items" response can omit id; update the type to reflect that
by either making id (and other fields absent when message is present) optional
or replacing BvRobotNextOutput with a discriminated union (the two-shape union
shown in the review) so callers such as the code that checks !nextOutput.id (and
any use in claim/show logic) must narrow with 'message' in nextOutput or 'id' in
nextOutput before accessing required fields; choose one approach and ensure all
usages of nextOutput (e.g., the runtime check at the current !nextOutput.id
site) are updated to use proper type narrowing.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 374-384: getNextTask calls getEpicChildrenIds on every invocation
which spawns a subprocess; add a short-lived cache to avoid repeated bd list
calls by caching results inside the tracker instance and invalidating it on
mutations. Implement a simple TTL-backed cache field (e.g. epicChildrenCache
with {ids, expiresAt}) used by getEpicChildrenIds so it returns cached ids when
not expired, and ensure cache is cleared/updated in completeTask and
updateTaskStatus (and whenever getEpicId changes) so stale results aren't used;
keep fallback to super.getNextTask unchanged when id not in children.

Split BvRobotNextOutput into BvRobotNextTask | BvRobotNextEmpty
discriminated union. The guard changes from `!nextOutput.id || nextOutput.message`
to `'message' in nextOutput`, which lets TypeScript narrow the type
properly instead of relying on runtime checks against a required field
that may be undefined.

Addresses code review feedback from PR subsy#329.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/plugins/trackers/builtin/beads-bv/index.ts (1)

389-399: Epic membership check spawns a child process on every getNextTask call.

getEpicChildrenIds runs bd list --json --parent ... each time getNextTask is invoked with an epicId or filter.parentId. If getNextTask is called frequently (e.g., in a loop or polling scenario), this could be a noticeable overhead. Consider caching the epic children list and invalidating it when tasks are completed or statuses change.

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

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 389 - 399,
getNextTask is calling getEpicChildrenIds every invocation which shells out via
bd list; add an in-memory cache to avoid spawning a child process on every call
by caching results keyed by parentId inside the tracker (e.g.,
this._epicChildrenCache) and update getEpicChildrenIds to read/write that cache;
invalidate or refresh the cache when tasks change (on task completion, status
updates, or after a short TTL) by adding an explicit
invalidateEpicChildren(parentId) and/or TTL-based refresh logic so getNextTask
reads from the cache and only invokes the bd list subprocess when the cache is
missing/stale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 389-399: getNextTask is calling getEpicChildrenIds every
invocation which shells out via bd list; add an in-memory cache to avoid
spawning a child process on every call by caching results keyed by parentId
inside the tracker (e.g., this._epicChildrenCache) and update getEpicChildrenIds
to read/write that cache; invalidate or refresh the cache when tasks change (on
task completion, status updates, or after a short TTL) by adding an explicit
invalidateEpicChildren(parentId) and/or TTL-based refresh logic so getNextTask
reads from the cache and only invokes the bd list subprocess when the cache is
missing/stale.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 23, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ralph-tui Ignored Ignored Preview Feb 24, 2026 9:46am

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 71.59091% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.46%. Comparing base (d71cee6) to head (1b232ca).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
src/plugins/trackers/builtin/beads-bv/index.ts 71.59% 50 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #329      +/-   ##
==========================================
+ Coverage   46.00%   46.46%   +0.46%     
==========================================
  Files         102      102              
  Lines       33158    33219      +61     
==========================================
+ Hits        15254    15436     +182     
+ Misses      17904    17783     -121     
Files with missing lines Coverage Δ
src/plugins/trackers/builtin/beads-bv/index.ts 41.50% <71.59%> (+30.72%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 355-367: The code currently returns undefined when the normalized
statusFilter excludes actionable statuses (or when a BV pick falls outside the
filter); instead call and return super.getNextTask(filter) to fall back to the
base tracker and preserve filter semantics. Update the check that uses
normalizeStatusFilter and the conditional that currently returns undefined
inside getNextTask to return super.getNextTask(filter) and make the same
replacement for the two other analogous conditionals in this file (the other
places flagged by the reviewer), keeping the existing statusFilter logic and
using the same filter argument when calling super.getNextTask.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b113032 and 5cb52ff.

📒 Files selected for processing (2)
  • src/plugins/trackers/builtin/beads-bv/index.test.ts
  • src/plugins/trackers/builtin/beads-bv/index.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/plugins/trackers/builtin/beads-bv/index.ts (2)

360-367: All three status-filter fall-throughs now correctly delegate to super.getNextTask(filter).

Lines 360–367, 426–428, and 443–445 all return super.getNextTask(filter) instead of undefined, preserving filter semantics as previously requested.

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

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 360 - 367, The
status-filter fall-throughs in getNextTask incorrectly returned undefined
previously; update each branch that checks statusFilter (the checks around the
status array logic inside getNextTask in
src/plugins/trackers/builtin/beads-bv/index.ts) to delegate back to the parent
by returning super.getNextTask(filter) when the statusFilter excludes 'open' and
'in_progress', ensuring all three occurrences use super.getNextTask(filter) to
preserve filter semantics.

49-76: Discriminated union correctly addresses the prior type-safety concern.

BvRobotNextOutput as BvRobotNextTask | BvRobotNextEmpty and the narrowing guard 'message' in nextOutput at line 402 properly resolve the previously flagged issue with id being required on a response shape that omits it.

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

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 49 - 76, The
discriminated-union fix using BvRobotNextOutput = BvRobotNextTask |
BvRobotNextEmpty is correct; ensure any consumer uses the narrowing guard
('message' in nextOutput) before accessing task-only fields like id, and remove
the duplicate/extra review comment that was posted (the one repeating approval)
so the PR comments are not duplicated.
🧹 Nitpick comments (2)
src/plugins/trackers/builtin/beads-bv/index.ts (2)

406-416: Consider caching getEpicChildrenIds results to avoid a redundant process spawn per call.

When an epic is configured, every getNextTask invocation spawns two child processes: bv --robot-next and bd list --json --parent <epicId>. Epic membership rarely changes between successive calls, so the second spawn is effectively redundant.

♻️ Suggested approach
+  private epicChildrenCache: Map<string, { ids: string[]; fetchedAt: number }> = new Map();
+  private readonly EPIC_CHILDREN_TTL_MS = 60_000; // 1 minute

   private async getEpicChildrenIds(epicId: string): Promise<string[]> {
+    const cached = this.epicChildrenCache.get(epicId);
+    if (cached && Date.now() - cached.fetchedAt < this.EPIC_CHILDREN_TTL_MS) {
+      return cached.ids;
+    }
+
     const { stdout, exitCode } = await execBd(
       ['list', '--json', '--parent', epicId, '--limit', '0'],
       this.getWorkingDir()
     );
     if (exitCode !== 0) { return []; }
     try {
       const beads = JSON.parse(stdout) as Array<{ id: string }>;
-      return beads.map((b) => b.id);
+      const ids = beads.map((b) => b.id);
+      this.epicChildrenCache.set(epicId, { ids, fetchedAt: Date.now() });
+      return ids;
     } catch {
       return [];
     }
   }

The cache can be invalidated in completeTask and updateTaskStatus where task state changes occur.

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

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 406 - 416,
getNextTask calls getEpicChildrenIds each time which spawns an extra process;
cache getEpicChildrenIds results (keyed by epicId) in the tracker instance to
reuse between getNextTask calls and avoid redundant bd list spawns. Implement a
simple in-memory cache (e.g. a Map<string, string[]> or { ids, timestamp }) and
consult it in getNextTask before calling getEpicChildrenIds; fall back to
calling getEpicChildrenIds when cache miss or expired. Invalidate or update the
cache inside completeTask and updateTaskStatus whenever tasks move between epics
or their parent changes so epic membership stays correct, and keep the existing
fallback to super.getNextTask when nextOutput.id is not in the cached epic
children.

751-753: Add a .catch() to the fire-and-forget refreshTriage() promise.

refreshTriage() is currently designed to always resolve, but without an explicit .catch() any future change that allows a rejection to escape would become an unhandled promise rejection. Starting with Node.js 15, unhandled rejections crash the application by default.

♻️ Suggested fix
-    this.triageRefreshInFlight = this.refreshTriage().finally(() => {
-      this.triageRefreshInFlight = null;
-    });
+    this.triageRefreshInFlight = this.refreshTriage()
+      .catch((err) => {
+        console.error('Unexpected error in background triage refresh:', err);
+      })
+      .finally(() => {
+        this.triageRefreshInFlight = null;
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 751 - 753, The
fire-and-forget assignment to this.triageRefreshInFlight uses refreshTriage()
without a .catch(), which risks unhandled promise rejections if refreshTriage
ever throws; update the expression that sets this.triageRefreshInFlight (the
call to refreshTriage() followed by .finally(...)) to chain a .catch(err => { /*
log or handle error */ }) before the .finally so any rejection is handled (use
your logger e.g. this.logger.error or console.error and include the error) and
leave the existing .finally(() => { this.triageRefreshInFlight = null; })
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 733-754: scheduleTriageRefresh currently ignores the force=true
intent because it returns early when triageRefreshInFlight is non-null; change
the logic so a forced refresh bypasses the in-flight guard and the rate-limit
check: in scheduleTriageRefresh(force = false) check force before returning for
triageRefreshInFlight and before applying the TRIAGE_REFRESH_MIN_INTERVAL_MS
time check (or alternatively set lastTriageRefreshAt = 0 when force is true so
refreshTriage() will run immediately after the current in-flight completes);
ensure triageRefreshInFlight is still set to the promise returned by
refreshTriage() and cleared in its finally handler so behavior of
refreshTriage() and lastTriageRefreshAt remains correct.

---

Duplicate comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 360-367: The status-filter fall-throughs in getNextTask
incorrectly returned undefined previously; update each branch that checks
statusFilter (the checks around the status array logic inside getNextTask in
src/plugins/trackers/builtin/beads-bv/index.ts) to delegate back to the parent
by returning super.getNextTask(filter) when the statusFilter excludes 'open' and
'in_progress', ensuring all three occurrences use super.getNextTask(filter) to
preserve filter semantics.
- Around line 49-76: The discriminated-union fix using BvRobotNextOutput =
BvRobotNextTask | BvRobotNextEmpty is correct; ensure any consumer uses the
narrowing guard ('message' in nextOutput) before accessing task-only fields like
id, and remove the duplicate/extra review comment that was posted (the one
repeating approval) so the PR comments are not duplicated.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 406-416: getNextTask calls getEpicChildrenIds each time which
spawns an extra process; cache getEpicChildrenIds results (keyed by epicId) in
the tracker instance to reuse between getNextTask calls and avoid redundant bd
list spawns. Implement a simple in-memory cache (e.g. a Map<string, string[]> or
{ ids, timestamp }) and consult it in getNextTask before calling
getEpicChildrenIds; fall back to calling getEpicChildrenIds when cache miss or
expired. Invalidate or update the cache inside completeTask and updateTaskStatus
whenever tasks move between epics or their parent changes so epic membership
stays correct, and keep the existing fallback to super.getNextTask when
nextOutput.id is not in the cached epic children.
- Around line 751-753: The fire-and-forget assignment to
this.triageRefreshInFlight uses refreshTriage() without a .catch(), which risks
unhandled promise rejections if refreshTriage ever throws; update the expression
that sets this.triageRefreshInFlight (the call to refreshTriage() followed by
.finally(...)) to chain a .catch(err => { /* log or handle error */ }) before
the .finally so any rejection is handled (use your logger e.g. this.logger.error
or console.error and include the error) and leave the existing .finally(() => {
this.triageRefreshInFlight = null; }) intact.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb52ff and 35d2e12.

📒 Files selected for processing (1)
  • src/plugins/trackers/builtin/beads-bv/index.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/plugins/trackers/builtin/beads-bv/index.ts (1)

160-227: execBv and execBd are near-identical — consider extracting a shared helper.

These two functions differ only in the spawned binary name ('bv' vs 'bd'). A single parameterised helper (e.g. execCommand(bin: string, args, cwd)) would eliminate the duplication.

♻️ Proposed refactor
-async function execBv(
-  args: string[],
-  cwd?: string
-): Promise<{ stdout: string; stderr: string; exitCode: number }> {
-  return new Promise((resolve) => {
-    const proc = spawn('bv', args, {
-      cwd,
-      env: { ...process.env },
-      stdio: ['ignore', 'pipe', 'pipe'],
-    });
-    ...
-  });
-}
-
-async function execBd(
-  args: string[],
-  cwd?: string
-): Promise<{ stdout: string; stderr: string; exitCode: number }> {
-  return new Promise((resolve) => {
-    const proc = spawn('bd', args, {
-      cwd,
-      env: { ...process.env },
-      stdio: ['ignore', 'pipe', 'pipe'],
-    });
-    ...
-  });
-}
+async function execCommand(
+  bin: string,
+  args: string[],
+  cwd?: string
+): Promise<{ stdout: string; stderr: string; exitCode: number }> {
+  return new Promise((resolve) => {
+    const proc = spawn(bin, args, {
+      cwd,
+      env: { ...process.env },
+      stdio: ['ignore', 'pipe', 'pipe'],
+    });
+
+    let stdout = '';
+    let stderr = '';
+
+    proc.stdout.on('data', (data: Buffer) => {
+      stdout += data.toString();
+    });
+
+    proc.stderr.on('data', (data: Buffer) => {
+      stderr += data.toString();
+    });
+
+    proc.on('close', (code) => {
+      resolve({ stdout, stderr, exitCode: code ?? 1 });
+    });
+
+    proc.on('error', (err) => {
+      stderr += err.message;
+      resolve({ stdout, stderr, exitCode: 1 });
+    });
+  });
+}
+
+const execBv = (args: string[], cwd?: string) => execCommand('bv', args, cwd);
+const execBd = (args: string[], cwd?: string) => execCommand('bd', args, cwd);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-bv/index.ts` around lines 160 - 227, Both
execBv and execBd duplicate the same spawn-and-collect logic; extract a single
parameterized helper (e.g. execCommand or execSpawn) that takes the binary name
(bin: string), args: string[], and optional cwd and returns Promise<{ stdout:
string; stderr: string; exitCode: number }>, then implement execBv and execBd as
thin wrappers that call this helper (execCommand('bv', args, cwd) and
execCommand('bd', args, cwd)). Ensure the helper reproduces the existing
behavior: spawn with env: { ...process.env } and stdio ['ignore','pipe','pipe'],
accumulates stdout/stderr from data events, resolves on 'close' with code ?? 1,
and handles 'error' by appending err.message to stderr and resolving with
exitCode 1.
src/plugins/trackers/builtin/beads-bv/index.test.ts (2)

211-243: No test for bv --robot-next returning a non-zero exit code.

The getNextTask method falls back to super.getNextTask(filter) when exitCode !== 0 (line 390 in index.ts). This path isn't exercised by the current test suite. Consider adding a test that queues a response with a non-zero exit code and verifies the fallback.

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

In `@src/plugins/trackers/builtin/beads-bv/index.test.ts` around lines 211 - 243,
Add a test that covers the branch where bv --robot-next returns a non-zero exit
code so getNextTask falls back to the base tracker: in the existing
describe('getNextTask with --robot-next') block create a test using
BeadsBvTrackerPlugin with bvAvailable=true and scheduleTriageRefresh stubbed,
call queueSpawnResponse with command 'bv' and a non-zero exitCode (e.g.
exitCode: 1) (and optional stdout/stderr), stub
BeadsTrackerPlugin.prototype.getNextTask to return a fallback TrackerTask,
invoke plugin.getNextTask() and assert it equals the fallback, then restore the
original BeadsTrackerPlugin.prototype.getNextTask.

234-242: Prototype mutation for BeadsTrackerPlugin.prototype.getNextTask is restored safely via finally.

This pattern is acceptable since tests within a describe block run sequentially in Bun. The try/finally ensures restoration even if the assertion throws. Repeated across multiple tests (lines 275-283 as well) — if more tests adopt this pattern, consider extracting a small helper to reduce boilerplate.

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

In `@src/plugins/trackers/builtin/beads-bv/index.test.ts` around lines 234 - 242,
Tests repeatedly mutate BeadsTrackerPlugin.prototype.getNextTask and restore it
in a try/finally; extract a small reusable helper (e.g., stubPrototypeMethod or
withTemporaryPrototypeStub) to encapsulate saving the original (from
BeadsTrackerPlugin.prototype.getNextTask), assigning the stubbed async
implementation (returning fallbackTask), running the test callback, and
restoring the original in a finally block so all tests can call the helper
instead of duplicating the try/finally logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 396-410: The parsed JSON (nextOutput) is only checked for a
'message' property, so malformed JSON that has neither 'message' nor 'id' can
flow through and produce a task with undefined id; update the handling after
JSON.parse (BvRobotNextOutput / nextOutput) to validate the runtime shape—ensure
nextOutput has either a 'message' field or a valid 'id' (and that id is
non-undefined) before treating it as a BvRobotNextTask, and if validation fails
log the error and return super.getNextTask(filter) (or otherwise bail out) so
getTask isn't called with an undefined id.
- Around line 546-548: The timestamp this.lastTriageRefreshAt is updated
unconditionally in refreshTriage(), which prevents scheduleTriageRefresh's 30s
rate-limiter from allowing immediate retries after failures; change
refreshTriage() so that this.lastTriageRefreshAt = Date.now() is set only when
the refresh actually succeeded (i.e., after verifying exitCode === 0 and
successful parsing), leaving it unchanged on errors so scheduleTriageRefresh can
retry sooner.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-bv/index.test.ts`:
- Around line 211-243: Add a test that covers the branch where bv --robot-next
returns a non-zero exit code so getNextTask falls back to the base tracker: in
the existing describe('getNextTask with --robot-next') block create a test using
BeadsBvTrackerPlugin with bvAvailable=true and scheduleTriageRefresh stubbed,
call queueSpawnResponse with command 'bv' and a non-zero exitCode (e.g.
exitCode: 1) (and optional stdout/stderr), stub
BeadsTrackerPlugin.prototype.getNextTask to return a fallback TrackerTask,
invoke plugin.getNextTask() and assert it equals the fallback, then restore the
original BeadsTrackerPlugin.prototype.getNextTask.
- Around line 234-242: Tests repeatedly mutate
BeadsTrackerPlugin.prototype.getNextTask and restore it in a try/finally;
extract a small reusable helper (e.g., stubPrototypeMethod or
withTemporaryPrototypeStub) to encapsulate saving the original (from
BeadsTrackerPlugin.prototype.getNextTask), assigning the stubbed async
implementation (returning fallbackTask), running the test callback, and
restoring the original in a finally block so all tests can call the helper
instead of duplicating the try/finally logic.

In `@src/plugins/trackers/builtin/beads-bv/index.ts`:
- Around line 160-227: Both execBv and execBd duplicate the same
spawn-and-collect logic; extract a single parameterized helper (e.g. execCommand
or execSpawn) that takes the binary name (bin: string), args: string[], and
optional cwd and returns Promise<{ stdout: string; stderr: string; exitCode:
number }>, then implement execBv and execBd as thin wrappers that call this
helper (execCommand('bv', args, cwd) and execCommand('bd', args, cwd)). Ensure
the helper reproduces the existing behavior: spawn with env: { ...process.env }
and stdio ['ignore','pipe','pipe'], accumulates stdout/stderr from data events,
resolves on 'close' with code ?? 1, and handles 'error' by appending err.message
to stderr and resolving with exitCode 1.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35d2e12 and 130beac.

📒 Files selected for processing (2)
  • src/plugins/trackers/builtin/beads-bv/index.test.ts
  • src/plugins/trackers/builtin/beads-bv/index.ts

@subsy subsy merged commit 0451df2 into subsy:main Feb 24, 2026
7 checks passed
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.

beads-bv tracker picks blocked tasks — missing blocked_by filter in getNextTask()

2 participants