fix: use --robot-next for task selection to prevent picking blocked tasks#329
Conversation
…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
|
@mwarger is attempting to deploy a commit to the plgeek Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughReplaces 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/plugins/trackers/builtin/beads-bv/index.ts (1)
374-384: Consider caching epic children to avoid repeatedbd listcalls.
getEpicChildrenIdsspawns a child process on everygetNextTaskinvocation 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 oncompleteTask/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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/plugins/trackers/builtin/beads-bv/index.ts (1)
389-399: Epic membership check spawns a child process on everygetNextTaskcall.
getEpicChildrenIdsrunsbd list --json --parent ...each timegetNextTaskis invoked with anepicIdorfilter.parentId. IfgetNextTaskis 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/plugins/trackers/builtin/beads-bv/index.test.tssrc/plugins/trackers/builtin/beads-bv/index.ts
There was a problem hiding this comment.
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 tosuper.getNextTask(filter).Lines 360–367, 426–428, and 443–445 all return
super.getNextTask(filter)instead ofundefined, 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.
BvRobotNextOutputasBvRobotNextTask | BvRobotNextEmptyand the narrowing guard'message' in nextOutputat line 402 properly resolve the previously flagged issue withidbeing 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 cachinggetEpicChildrenIdsresults to avoid a redundant process spawn per call.When an epic is configured, every
getNextTaskinvocation spawns two child processes:bv --robot-nextandbd 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
completeTaskandupdateTaskStatuswhere 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-forgetrefreshTriage()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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/plugins/trackers/builtin/beads-bv/index.ts (1)
160-227:execBvandexecBdare 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 forbv --robot-nextreturning a non-zero exit code.The
getNextTaskmethod falls back tosuper.getNextTask(filter)whenexitCode !== 0(line 390 inindex.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 forBeadsTrackerPlugin.prototype.getNextTaskis restored safely viafinally.This pattern is acceptable since tests within a
describeblock run sequentially in Bun. Thetry/finallyensures 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
📒 Files selected for processing (2)
src/plugins/trackers/builtin/beads-bv/index.test.tssrc/plugins/trackers/builtin/beads-bv/index.ts
Summary
getNextTask()frombv --robot-triagetobv --robot-nextto prevent selecting blocked tasks--robot-nextis guaranteed by BV to only return an unblocked task--robot-triagefor background metadata enrichment (getTasks,cacheTaskReasoning,getTriageStats)Fixes #327
Problem
bv --robot-triagereturnsrecommendations[]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 theblocked_byfield, causing ralph-tui to select and execute tasks whose dependencies hadn't been completed yet.Real-world example from an epic with 26 beads:
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-nextfor 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-triageis still used viarefreshTriage()(fired in background after selection) to populate the metadata cache thatgetTasks(),cacheTaskReasoning(), andgetTriageStats()rely on.Testing
bd dep add): correct task ordering through the full dependency chainSummary by CodeRabbit
New Features
Improvements
Tests