Skip to content

fix(beads-rust): enrich getTasks with dependency IDs for parallel execution#321

Merged
subsy merged 7 commits intosubsy:mainfrom
thunter009:fix/parallel-dependency-filtering
Feb 20, 2026
Merged

fix(beads-rust): enrich getTasks with dependency IDs for parallel execution#321
subsy merged 7 commits intosubsy:mainfrom
thunter009:fix/parallel-dependency-filtering

Conversation

@thunter009
Copy link
Copy Markdown
Contributor

@thunter009 thunter009 commented Feb 19, 2026

Summary

  • br list --json only provides dependency_count/dependent_count but omits actual dependency IDs
  • The parallel executor's analyzeTaskGraph therefore sees all tasks as independent and dispatches them simultaneously, ignoring the br dep add dependency chain
  • Fix: after getTasks fetches from br list, call br dep list per task (only those with dependency_count > 0) to populate dependsOn fields
  • Calls are parallelized via Promise.all and only made for tasks that actually have deps

Test plan

  • Existing 74 tests pass (0 fail)
  • Build succeeds
  • Manual test: create epic with linear dep chain, run ralph-tui run --epic <id> --parallel 3, verify tasks execute in dependency order

Fixes #320

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Dependency lists are now populated only for tasks that remain after filtering, ensuring parent/child relationships display reliably.
  • Chores

    • Enrichment now runs in batched calls with a concurrency cap (max 8) to reduce process fan‑out and unnecessary background lookups.
    • Documentation updated to reflect dependency enrichment and readiness behaviour.
  • Tests

    • Added tests simulating delayed responses and verifying enrichment runs only for filtered tasks and that concurrency is limited.

…cution

br list --json only provides dependency_count/dependent_count but omits
the actual dependency IDs. The parallel executor's task graph therefore
sees all tasks as independent and dispatches them simultaneously,
ignoring the dependency chain.

Fix: after getTasks fetches from br list, call br dep list per task
(only those with dependency_count > 0) to populate dependsOn fields.
This allows analyzeTaskGraph to correctly compute depth-ordered
parallel groups.

Fixes subsy#320

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 19, 2026

@thunter009 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 19, 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

Adds in-method dependency enrichment to the BeadsRust tracker: after listing and filtering tasks, the plugin queries br dep list --json for tasks with non‑zero dependency counts (batched with a concurrency cap) and populates each task's dependsOn before returning tasks.

Changes

Cohort / File(s) Summary
Dependency enrichment implementation
src/plugins/trackers/builtin/beads-rust/index.ts
Adds private enrichDependencies(tasks, rawTasks) and enrichTaskDependencies(task); builds a dep-count map from raw tasks, enriches only tasks present in that map, runs br dep list --json per task in batched groups with a max concurrency of 8, parses blocking dependency IDs (type: blocks) and assigns deduplicated values to task.dependsOn; invoked from getTasks() after filtering; spawn failures are logged and enrichment for that task is skipped.
Tests & spawn mocks
src/plugins/trackers/builtin/beads-rust/index.test.ts
Extends mock spawn responses with delayMs; adds concurrency-tracking hooks (trackEnrichmentDepConcurrency, activeEnrichmentDepCalls, maxEnrichmentDepCalls) and resets them in beforeEach; tests validate enrichment runs only for tasks surviving parent filtering and that concurrent dep-list calls are capped (≤8).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Ralph as Ralph-TUI
participant Plugin as BeadsRustTrackerPlugin
participant BR as br CLI
participant Scheduler as Scheduler/Workers

Ralph->>Plugin: request getTasks()
Plugin->>BR: br show --json rgba(0,128,255,0.5)
BR-->>Plugin: raw task JSON
Plugin->>Plugin: convert to TrackerTask[], apply parent filtering
Plugin->>BR: br dep list --json (batched, max 8 concurrent) rgba(0,128,255,0.5)
BR-->>Plugin: dep list JSON
Plugin->>Plugin: populate task.dependsOn
Plugin-->>Scheduler: return enriched tasks

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 title accurately describes the main change: enriching getTasks with dependency IDs to enable proper parallel execution respecting dependency chains.
Linked Issues check ✅ Passed The implementation addresses the core requirement from issue #320 by enriching tasks with dependency IDs via br dep list calls, ensuring the parallel executor respects dependency chains.
Out of Scope Changes check ✅ Passed All changes in both files are directly related to implementing dependency enrichment for parallel execution as specified in the linked issue.

✏️ 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.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 58.24176% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.39%. Comparing base (22d56d0) to head (f467954).
⚠️ Report is 8 commits behind head on main.

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

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #321      +/-   ##
==========================================
+ Coverage   45.34%   45.39%   +0.04%     
==========================================
  Files          98       98              
  Lines       31325    31412      +87     
==========================================
+ Hits        14205    14258      +53     
- Misses      17120    17154      +34     
Files with missing lines Coverage Δ
src/plugins/trackers/builtin/beads-rust/index.ts 68.81% <58.24%> (-1.13%) ⬇️
🚀 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: 2

🧹 Nitpick comments (2)
src/plugins/trackers/builtin/beads-rust/index.ts (2)

452-461: enrichDependencies is inserted between getNextTask's JSDoc and its implementation.

The JSDoc block at lines 445–451 documents getNextTask (which is defined at line 537). Inserting enrichDependencies here creates an 86-line gap between that comment and the method it annotates, making the JSDoc appear orphaned and the class structure confusing to navigate.

Move enrichDependencies immediately before or after getChildIds (line 511) so that getNextTask's JSDoc remains directly adjacent to getNextTask.

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

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 452 - 461, The
JSDoc for getNextTask was orphaned by inserting enrichDependencies in the
middle; move the enrichDependencies function so the getNextTask comment sits
immediately above getNextTask again—specifically cut the enrichDependencies
implementation and paste it immediately before or immediately after getChildIds
so getNextTask's JSDoc and the getNextTask method (function getNextTask) are
adjacent.

503-503: Unbounded Promise.all may exhaust process/FD limits for large epics.

All br dep list spawns are issued concurrently with no cap. For an epic with a large number of tasks whose dependency_count > 0, this fans out to that many simultaneous child processes. Consider chunking or using a concurrency limiter (e.g., p-limit) to keep the fan-out bounded.

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

In `@src/plugins/trackers/builtin/beads-rust/index.ts` at line 503, The unbounded
await Promise.all(enrichPromises) fans out all 'br dep list' child processes at
once; change this to a bounded concurrency approach by replacing the direct
Promise.all(enrichPromises) with a concurrency-limited runner (e.g., use p-limit
or a simple chunked loop). Create a limiter (const limit = pLimit(CONCURRENCY))
and build limitedPromises = enrichPromises.map(p => limit(() => p)) then await
Promise.all(limitedPromises), or iterate over enrichPromises in fixed-size
chunks and await each chunk before proceeding; reference the enrichPromises
array and the place where Promise.all(enrichPromises) is awaited to apply the
change and pick a sensible CONCURRENCY (e.g., 5-20).
🤖 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-rust/index.ts`:
- Around line 495-497: brTaskToTask/BrTaskJson already sets task.dependsOn from
BrTaskJson.dependencies, but the code at the end overwrites it with dependsOn;
change the assignment to merge instead: when dependsOn.length > 0, combine
existing task.dependsOn (may be undefined) with dependsOn into a deduplicated
array and assign that back to task.dependsOn so we preserve any IDs populated
earlier by brTaskToTask and avoid dropping data.
- Around line 478-493: The code calls execBr(['dep','list', task.id, '--json'],
...) and then pushes dep.depends_on_id into the dependsOn array without guarding
direction, which can include rows where depends_on_id === task.id
(self-blocking); fix by adding an explicit direction flag (e.g., include
'--direction','up' in the execBr args) or, if you prefer a defensive check, only
push when dep.issue_id === task.id (and dep.type === 'blocks') so dependsOn is
populated with things the current task actually depends on; refer to execBr,
BrDepListItem, dependsOn and getChildIds to locate where to apply the change.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 452-461: The JSDoc for getNextTask was orphaned by inserting
enrichDependencies in the middle; move the enrichDependencies function so the
getNextTask comment sits immediately above getNextTask again—specifically cut
the enrichDependencies implementation and paste it immediately before or
immediately after getChildIds so getNextTask's JSDoc and the getNextTask method
(function getNextTask) are adjacent.
- Line 503: The unbounded await Promise.all(enrichPromises) fans out all 'br dep
list' child processes at once; change this to a bounded concurrency approach by
replacing the direct Promise.all(enrichPromises) with a concurrency-limited
runner (e.g., use p-limit or a simple chunked loop). Create a limiter (const
limit = pLimit(CONCURRENCY)) and build limitedPromises = enrichPromises.map(p =>
limit(() => p)) then await Promise.all(limitedPromises), or iterate over
enrichPromises in fixed-size chunks and await each chunk before proceeding;
reference the enrichPromises array and the place where
Promise.all(enrichPromises) is awaited to apply the change and pick a sensible
CONCURRENCY (e.g., 5-20).

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugins/trackers/builtin/beads-rust/index.ts (1)

473-479: ⚠️ Potential issue | 🟡 Minor

getNextTask JSDoc is now orphaned by the insertion of enrichDependencies.

Lines 473–479 contain the JSDoc for getNextTask, but the new enrichDependencies method (lines 480–532) was inserted immediately after it, separating the comment from its implementation at line 565. Most doc tools (TypeDoc, IDE hover, etc.) will associate this JSDoc with the next declaration — enrichDependencies — leaving getNextTask undocumented.

Move the getNextTask JSDoc to sit directly above the getNextTask override at line 565, or relocate enrichDependencies to after getChildIds (lines 534–563) to restore the original adjacency.

🐛 Proposed fix — move `getNextTask` JSDoc to its correct position
-  /**
-   * Get the next task to work on using br ready.
-   *
-   * This overrides the base implementation to leverage br's server-side readiness
-   * selection (dependency-aware), since br list output may not contain enough
-   * dependency information for client-side readiness filtering.
-   */
   /**
    * Enrich TrackerTask objects with dependency IDs from br dep list.
    * ...
    */
   private async enrichDependencies(...) { ... }

   /**
    * Get child IDs for a parent epic/task.
    * ...
    */
   private async getChildIds(...) { ... }

+  /**
+   * Get the next task to work on using br ready.
+   *
+   * This overrides the base implementation to leverage br's server-side readiness
+   * selection (dependency-aware), since br list output may not contain enough
+   * dependency information for client-side readiness filtering.
+   */
   override async getNextTask(...) { ... }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 473 - 479, The
JSDoc block for getNextTask was left separated by the newly added
enrichDependencies method, so TypeDoc/IDEs will attach the docs to
enrichDependencies instead of getNextTask; fix by moving the JSDoc comment so it
sits immediately above the getNextTask override (the method named getNextTask)
or alternatively relocate the enrichDependencies method to come after
getChildIds so the original adjacency is restored; ensure the JSDoc precedes
getNextTask and not enrichDependencies.
🧹 Nitpick comments (2)
src/plugins/trackers/builtin/beads-rust/index.ts (2)

495-500: depCountMap stores count values that are never read — use Set<string> instead.

The Map<string, number> stores dependency_count values (line 498) but the only usage is depCountMap.has(t.id) (line 504); the numeric value is never accessed. A Set<string> is semantically cleaner and avoids the misleading implication that counts are consumed.

♻️ Proposed refactor
-    const depCountMap = new Map<string, number>();
+    const tasksWithDeps = new Set<string>();
     for (const raw of rawTasks) {
       if (typeof raw.dependency_count === 'number' && raw.dependency_count > 0) {
-        depCountMap.set(raw.id, raw.dependency_count);
+        tasksWithDeps.add(raw.id);
       }
     }

     const enrichPromises = tasks
-      .filter((t) => depCountMap.has(t.id))
+      .filter((t) => tasksWithDeps.has(t.id))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 495 - 500, The
code currently creates depCountMap as a Map<string, number> but never reads the
numeric values; change it to a Set<string> named e.g. depWithDeps and populate
it from rawTasks by checking raw.dependency_count > 0 and calling
depWithDeps.add(raw.id) (replace depCountMap.set(...)). Update downstream checks
that call depCountMap.has(t.id) to use depWithDeps.has(t.id). Ensure the symbol
depWithDeps (or a renamed Set) replaces depCountMap everywhere in this module.

383-387: enrichDependencies fires before parent filtering, causing unnecessary br dep list calls for tasks that will be discarded.

enrichDependencies is called at line 387 on the full, unfiltered tasks array. The parent/child filter is applied at lines 391–394 via getChildIds, which may discard the majority of tasks. Every task with dependency_count > 0 in the entire tracker (not just the epic's children) gets its own br dep list subprocess call, even though those results are thrown away.

Reorder so that parent filtering happens first, then enrich only the surviving tasks:

♻️ Proposed reordering
     let tasks = tasksJson.map(brTaskToTask);

-    // Enrich tasks with dependency data from br dep list.
-    await this.enrichDependencies(tasks, tasksJson);
-
     // Filter by parent (br list doesn't support --parent)
     if (parentId) {
       const childIds = await this.getChildIds(parentId);
       tasks = tasks.filter((t) => childIds.has(t.id));
     }

+    // Enrich surviving tasks with dependency data from br dep list.
+    await this.enrichDependencies(tasks, tasksJson);
+
     // Apply remaining filters (excluding parentId which we handled above)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 383 - 387, The
call to enrichDependencies is happening on the full tasks array so br dep list
is invoked for tasks that will be filtered out later; change the order so you
first compute the parent/child filtered set using getChildIds (apply the same
filtering logic that produces the subset of tasks/children) and then call
enrichDependencies only with that filtered tasks array (and the corresponding
filtered tasksJson) so subprocess calls are only made for surviving tasks;
ensure any later code that expects enrichment uses the filteredTasks variable
(update references that previously assumed the original tasks array).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 473-479: The JSDoc block for getNextTask was left separated by the
newly added enrichDependencies method, so TypeDoc/IDEs will attach the docs to
enrichDependencies instead of getNextTask; fix by moving the JSDoc comment so it
sits immediately above the getNextTask override (the method named getNextTask)
or alternatively relocate the enrichDependencies method to come after
getChildIds so the original adjacency is restored; ensure the JSDoc precedes
getNextTask and not enrichDependencies.

---

Duplicate comments:
In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 523-525: The assignment "task.dependsOn = dependsOn" overwrites
any IDs previously set by brTaskToTask; instead merge the existing
task.dependsOn (if any) with the computed dependsOn array to avoid discarding
values (use a deduplicating merge like Array.from(new Set([...existingDepends,
...dependsOn]))). Update the code around the dependsOn assignment in the
function where task is built (referencing task.dependsOn and the dependsOn local
variable) so it preserves existing entries from brTaskToTask while adding the
new ones.
- Around line 506-521: The dep list call can return bidirectional rows and may
include self-blocking IDs; update the execBr invocation that builds dependsOn
(the ['dep', 'list', task.id, '--json'] call in this block) to include the same
direction filter used in getChildIds (add '--direction', 'up') and also add a
defensive check inside the deps loop (where you iterate BrDepListItem and push
into dependsOn) to only push when dep.issue_id === task.id and dep.type ===
'blocks' so self-references are ignored.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 495-500: The code currently creates depCountMap as a Map<string,
number> but never reads the numeric values; change it to a Set<string> named
e.g. depWithDeps and populate it from rawTasks by checking raw.dependency_count
> 0 and calling depWithDeps.add(raw.id) (replace depCountMap.set(...)). Update
downstream checks that call depCountMap.has(t.id) to use depWithDeps.has(t.id).
Ensure the symbol depWithDeps (or a renamed Set) replaces depCountMap everywhere
in this module.
- Around line 383-387: The call to enrichDependencies is happening on the full
tasks array so br dep list is invoked for tasks that will be filtered out later;
change the order so you first compute the parent/child filtered set using
getChildIds (apply the same filtering logic that produces the subset of
tasks/children) and then call enrichDependencies only with that filtered tasks
array (and the corresponding filtered tasksJson) so subprocess calls are only
made for surviving tasks; ensure any later code that expects enrichment uses the
filteredTasks variable (update references that previously assumed the original
tasks array).

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 20, 2026

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

Project Deployment Actions Updated (UTC)
ralph-tui Ready Ready Preview, Comment Feb 20, 2026 11:58am

Request Review

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 (2)
src/plugins/trackers/builtin/beads-rust/index.test.ts (2)

509-543: Consider adding a lower-bound assertion on maxEnrichmentDepCalls.

The test validates the upper bound (<= 8) but would still pass if the implementation regressed to serial execution. A complementary toBeGreaterThan(1) assertion would guard against accidentally losing parallelism.

💡 Proposed addition
       expect(depEnrichmentCalls.length).toBe(taskCount);
       expect(maxEnrichmentDepCalls).toBeLessThanOrEqual(8);
+      expect(maxEnrichmentDepCalls).toBeGreaterThan(1);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.test.ts` around lines 509 -
543, Add a lower-bound assertion to the test 'limits concurrent dependency
enrichment calls to avoid process fan-out' to ensure parallelism isn't lost:
after the existing upper-bound check on maxEnrichmentDepCalls, add an assertion
that maxEnrichmentDepCalls is greater than 1 (e.g.,
expect(maxEnrichmentDepCalls).toBeGreaterThan(1)). This should be added in the
same test that constructs BeadsRustTrackerPlugin, calls plugin.getTasks(), and
inspects depEnrichmentCalls to validate concurrency behavior.

468-543: Consider adding a test for enrichment failure resilience.

The implementation silently ignores br dep list failures during enrichment (per the code summary). A test that returns a non-zero exit code or invalid JSON for one task's enrichment call — while succeeding for another — would confirm that partial enrichment failures don't break the overall getTasks flow and that unresolvable tasks simply have no dependsOn.

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

In `@src/plugins/trackers/builtin/beads-rust/index.test.ts` around lines 468 -
543, Add a test to verify enrichment failure resilience: in
src/plugins/trackers/builtin/beads-rust/index.test.ts create a new test that
uses BeadsRustTrackerPlugin and getTasks with multiple tasks where
mockSpawnResponses includes a successful list response for some tasks and a
failing enrichment response for at least one task (set exitCode != 0 or invalid
JSON for that task's 'br dep list' entry). After calling plugin.getTasks(),
assert that all tasks are returned (tasks.length), that the task with the
failing enrichment has no dependsOn (undefined or empty), and that another task
with a successful enrichment has its dependsOn populated; also assert
mockSpawnArgs contains the expected 'dep list' calls to ensure enrichment was
attempted. Ensure you reset/prepare mockSpawnArgs and toggle
trackEnrichmentDepConcurrency as needed to reproduce concurrent behavior.
🤖 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-rust/index.test.ts`:
- Around line 509-543: Add a lower-bound assertion to the test 'limits
concurrent dependency enrichment calls to avoid process fan-out' to ensure
parallelism isn't lost: after the existing upper-bound check on
maxEnrichmentDepCalls, add an assertion that maxEnrichmentDepCalls is greater
than 1 (e.g., expect(maxEnrichmentDepCalls).toBeGreaterThan(1)). This should be
added in the same test that constructs BeadsRustTrackerPlugin, calls
plugin.getTasks(), and inspects depEnrichmentCalls to validate concurrency
behavior.
- Around line 468-543: Add a test to verify enrichment failure resilience: in
src/plugins/trackers/builtin/beads-rust/index.test.ts create a new test that
uses BeadsRustTrackerPlugin and getTasks with multiple tasks where
mockSpawnResponses includes a successful list response for some tasks and a
failing enrichment response for at least one task (set exitCode != 0 or invalid
JSON for that task's 'br dep list' entry). After calling plugin.getTasks(),
assert that all tasks are returned (tasks.length), that the task with the
failing enrichment has no dependsOn (undefined or empty), and that another task
with a successful enrichment has its dependsOn populated; also assert
mockSpawnArgs contains the expected 'dep list' calls to ensure enrichment was
attempted. Ensure you reset/prepare mockSpawnArgs and toggle
trackEnrichmentDepConcurrency as needed to reproduce concurrent behavior.

@subsy
Copy link
Copy Markdown
Owner

subsy commented Feb 20, 2026

Thanks for this @thunter009

Implemented follow-up fixes in beads-rust dependency enrichment:

  • merged enriched dependency IDs with existing dependsOn (deduped) instead of overwriting
  • filtered out reverse/self dep list rows (issue_id === task.id and not self-referential)
  • moved the enrichment block so getNextTask JSDoc is adjacent again
  • kept bounded concurrency for enrichment calls.
    Also added focused tests for merge/dedupe and reverse/self-row filtering.

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

481-506: Optional: rawTasks parameter can be dropped — the data is already on task.metadata.

brTaskToTask already writes dependency_count into task.metadata.dependencyCount (lines 216–218). The depCountMap built here from rawTasks replicates that information. enrichDependencies could filter tasks directly and remove the second parameter entirely:

♻️ Proposed simplification
-  private async enrichDependencies(
-    tasks: TrackerTask[],
-    rawTasks: BrTaskJson[]
-  ): Promise<void> {
-    // Build a map of task ID → raw dependency_count for quick lookup
-    const depCountMap = new Map<string, number>();
-    for (const raw of rawTasks) {
-      if (typeof raw.dependency_count === 'number' && raw.dependency_count > 0) {
-        depCountMap.set(raw.id, raw.dependency_count);
-      }
-    }
-
-    const tasksToEnrich = tasks.filter((t) => depCountMap.has(t.id));
+  private async enrichDependencies(tasks: TrackerTask[]): Promise<void> {
+    const tasksToEnrich = tasks.filter(
+      (t) => typeof t.metadata?.dependencyCount === 'number' && (t.metadata.dependencyCount as number) > 0
+    );
     if (tasksToEnrich.length === 0) {
       return;
     }

And the call site:

-    await this.enrichDependencies(tasks, tasksJson);
+    await this.enrichDependencies(tasks);

This is a style preference — the current approach is also correct and slightly more type-safe.

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

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 481 - 506,
enrichDependencies currently takes rawTasks and builds depCountMap from
raw.dependency_count but brTaskToTask already populates the same value into
task.metadata.dependencyCount; remove the rawTasks parameter from
enrichDependencies, change its signature to enrichDependencies(tasks:
TrackerTask[]), update the filtering to use tasks.filter(t => typeof
t.metadata?.dependencyCount === 'number' && t.metadata.dependencyCount > 0') (or
similar), and adjust the caller(s) to stop passing rawTasks; keep the batching
logic and the call to enrichTaskDependencies(task) unchanged.
src/plugins/trackers/builtin/beads-rust/index.test.ts (2)

67-87: The enrichment-call detection heuristic is implicitly coupled to the absence of --direction.

isEnrichmentDepList identifies enrichment calls by the absence of --direction rather than positively asserting an expected arg set. If enrichTaskDependencies ever adds a direction flag, this test silently stops tracking what it claims to track — maxEnrichmentDepCalls would never increment and the concurrency cap assertion would trivially pass.

Consider adding a brief comment to surface this coupling, or matching more precisely:

♻️ Proposed tighter detection
-        const isEnrichmentDepList =
-          args[0] === 'dep' &&
-          args[1] === 'list' &&
-          !args.includes('--direction');
+        // Enrichment calls: ['dep', 'list', <taskId>, '--json']
+        // Parent-filter calls include '--direction up'; exclude those.
+        const isEnrichmentDepList =
+          args[0] === 'dep' &&
+          args[1] === 'list' &&
+          args[args.length - 1] === '--json' &&
+          !args.includes('--direction');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.test.ts` around lines 67 - 87,
The test's enrichment-call detection (isEnrichmentDepList) relies on negating
'--direction' which couples it to enrichTaskDependencies' implementation; update
the heuristic to match the exact expected arg pattern instead (e.g., assert args
include 'dep' and 'list' and include any other expected flags/values) or add an
explicit comment documenting the coupling so future changes don't silently break
tracking; edit the detection in the mock spawn stub where isEnrichmentDepList is
defined (and adjust the close handler logic that decrements
activeEnrichmentDepCalls) to either perform a positive match of the exact arg
set or add the clarifying comment and tests to ensure the '--direction' change
fails loudly.

575-609: Concurrency cap test only validates the upper bound; a regression to serial execution would go undetected.

The assertion expect(maxEnrichmentDepCalls).toBeLessThanOrEqual(8) would trivially pass if the batch size regressed to 1 (fully serial). Adding a lower-bound assertion (e.g. toBeGreaterThan(1) or toBeGreaterThanOrEqual(8) for the first full batch of 18 tasks split into two full batches of 8) would make the test a genuine two-sided guard.

  expect(maxEnrichmentDepCalls).toBeLessThanOrEqual(8);
+ // Confirm the first batch is actually processed concurrently (not serial)
+ expect(maxEnrichmentDepCalls).toBeGreaterThan(1);

Separately, there is no test for the enrichment failure path (exitCode !== 0 or invalid JSON from br dep list). Adding a test that supplies a failing response for the enrichment call and asserts task.dependsOn is still defined (from the BrTaskJson.dependencies field) would cover the silent-fallback behaviour discussed in the implementation review.

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

In `@src/plugins/trackers/builtin/beads-rust/index.test.ts` around lines 575 -
609, Update the concurrency-cap test to assert both an upper and a lower bound
so serial regression is caught: in the test that constructs
BeadsRustTrackerPlugin, calls plugin.initialize and plugin.getTasks and inspects
maxEnrichmentDepCalls, add a lower-bound expectation (e.g.
expect(maxEnrichmentDepCalls).toBeGreaterThan(1) or
expect(...).toBeGreaterThanOrEqual(8) depending on intended batch semantics) in
addition to the existing toBeLessThanOrEqual(8). Also add a new test that
injects a failing enrichment response into mockSpawnResponses (simulate exitCode
!== 0 or invalid JSON for the 'br dep list' calls) and assert that tasks
returned by plugin.getTasks still have task.dependsOn populated from the
original BrTaskJson.dependencies field (verify fallback behavior), referencing
the existing test setup pieces like mockSpawnResponses, mockSpawnArgs,
BeadsRustTrackerPlugin, plugin.initialize and plugin.getTasks to locate where to
add these checks.
🤖 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-rust/index.ts`:
- Around line 511-542: enrichTaskDependencies currently swallows failures which
recreates the race in issue `#320`; update the two failure paths in
enrichTaskDependencies (the exitCode !== 0 branch after calling execBr(...) and
the catch block around JSON.parse) to emit a console.warn including the task.id
and failure details (for the execBr path include exitCode and stdout, and for
the catch include the caught error and stdout) so callers can distinguish "no
deps" from enrichment failures while preserving the existing graceful fallback
that leaves task.dependsOn unchanged.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-rust/index.test.ts`:
- Around line 67-87: The test's enrichment-call detection (isEnrichmentDepList)
relies on negating '--direction' which couples it to enrichTaskDependencies'
implementation; update the heuristic to match the exact expected arg pattern
instead (e.g., assert args include 'dep' and 'list' and include any other
expected flags/values) or add an explicit comment documenting the coupling so
future changes don't silently break tracking; edit the detection in the mock
spawn stub where isEnrichmentDepList is defined (and adjust the close handler
logic that decrements activeEnrichmentDepCalls) to either perform a positive
match of the exact arg set or add the clarifying comment and tests to ensure the
'--direction' change fails loudly.
- Around line 575-609: Update the concurrency-cap test to assert both an upper
and a lower bound so serial regression is caught: in the test that constructs
BeadsRustTrackerPlugin, calls plugin.initialize and plugin.getTasks and inspects
maxEnrichmentDepCalls, add a lower-bound expectation (e.g.
expect(maxEnrichmentDepCalls).toBeGreaterThan(1) or
expect(...).toBeGreaterThanOrEqual(8) depending on intended batch semantics) in
addition to the existing toBeLessThanOrEqual(8). Also add a new test that
injects a failing enrichment response into mockSpawnResponses (simulate exitCode
!== 0 or invalid JSON for the 'br dep list' calls) and assert that tasks
returned by plugin.getTasks still have task.dependsOn populated from the
original BrTaskJson.dependencies field (verify fallback behavior), referencing
the existing test setup pieces like mockSpawnResponses, mockSpawnArgs,
BeadsRustTrackerPlugin, plugin.initialize and plugin.getTasks to locate where to
add these checks.

In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 481-506: enrichDependencies currently takes rawTasks and builds
depCountMap from raw.dependency_count but brTaskToTask already populates the
same value into task.metadata.dependencyCount; remove the rawTasks parameter
from enrichDependencies, change its signature to enrichDependencies(tasks:
TrackerTask[]), update the filtering to use tasks.filter(t => typeof
t.metadata?.dependencyCount === 'number' && t.metadata.dependencyCount > 0') (or
similar), and adjust the caller(s) to stop passing rawTasks; keep the batching
logic and the call to enrichTaskDependencies(task) unchanged.

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

486-491: depCountMap stores values that are never read — prefer Set<string>.

The count stored in the Map is accessed nowhere; only .has() is used on line 493. A Set<string> is both cleaner in intent and avoids the spurious numeric storage.

♻️ Proposed refactor: Map → Set
-    const depCountMap = new Map<string, number>();
-    for (const raw of rawTasks) {
-      if (typeof raw.dependency_count === 'number' && raw.dependency_count > 0) {
-        depCountMap.set(raw.id, raw.dependency_count);
-      }
-    }
-
-    const tasksToEnrich = tasks.filter((t) => depCountMap.has(t.id));
+    const depsTaskIds = new Set<string>();
+    for (const raw of rawTasks) {
+      if (typeof raw.dependency_count === 'number' && raw.dependency_count > 0) {
+        depsTaskIds.add(raw.id);
+      }
+    }
+
+    const tasksToEnrich = tasks.filter((t) => depsTaskIds.has(t.id));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 486 - 491,
depCountMap is a Map<string, number> that only uses .has() later, so replace it
with a Set<string> to reflect intent and avoid storing unused numbers: when
iterating rawTasks check typeof raw.dependency_count === 'number' &&
raw.dependency_count > 0 and add raw.id to a new Set (e.g., depIdSet) instead of
depCountMap.set(...), then update subsequent checks that call
depCountMap.has(...) to use depIdSet.has(...); ensure symbols referenced are
depCountMap -> depIdSet, rawTasks, raw.dependency_count, and raw.id.

498-505: Consider semaphore-based concurrency, but p-limit is not a direct dependency and would need to be added first.

The batch-slicing approach does stall when a single br dep list call is slow. A semaphore pattern (e.g., p-limit) maintains a rolling window, starting the next call as soon as any slot frees. However, p-limit is not currently a direct dependency; it exists only as a transitive dependency. Using it without explicit declaration is unreliable. Either add p-limit to package.json dependencies, or consider a native alternative using a queue-based approach with Promise.allSettled() or a custom semaphore implementation.

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

In `@src/plugins/trackers/builtin/beads-rust/index.ts` around lines 498 - 505,
Replace the batch-slicing concurrency in the loop with a semaphore-based
limiter: add p-limit to package.json and use it to enforce
maxConcurrentEnrichCalls, creating a limiter via
pLimit(maxConcurrentEnrichCalls) and mapping tasksToEnrich to limiter(() =>
this.enrichTaskDependencies(task)) then await Promise.all on those promises;
target the loop and the this.enrichTaskDependencies calls so the limiter issues
a new call as soon as any slot frees. Alternatively, if adding a dependency is
undesired, implement a tiny in-file semaphore/queue that maintains a rolling
window, dequeues the next task when a slot frees, and invokes
this.enrichTaskDependencies(task) until tasksToEnrich is exhausted.
🤖 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-rust/index.ts`:
- Around line 511-522: The warning in enrichTaskDependencies loses useful error
details because execBr returns stderr but the code only destructures stdout and
exitCode; update the destructuring of execBr in enrichTaskDependencies to also
capture stderr and include that stderr value in the process warning (or
console.warn) message/object so that when br dep list exits non-zero the
human-readable error from stderr is logged alongside taskId, exitCode and
stdout.

---

Nitpick comments:
In `@src/plugins/trackers/builtin/beads-rust/index.ts`:
- Around line 486-491: depCountMap is a Map<string, number> that only uses
.has() later, so replace it with a Set<string> to reflect intent and avoid
storing unused numbers: when iterating rawTasks check typeof
raw.dependency_count === 'number' && raw.dependency_count > 0 and add raw.id to
a new Set (e.g., depIdSet) instead of depCountMap.set(...), then update
subsequent checks that call depCountMap.has(...) to use depIdSet.has(...);
ensure symbols referenced are depCountMap -> depIdSet, rawTasks,
raw.dependency_count, and raw.id.
- Around line 498-505: Replace the batch-slicing concurrency in the loop with a
semaphore-based limiter: add p-limit to package.json and use it to enforce
maxConcurrentEnrichCalls, creating a limiter via
pLimit(maxConcurrentEnrichCalls) and mapping tasksToEnrich to limiter(() =>
this.enrichTaskDependencies(task)) then await Promise.all on those promises;
target the loop and the this.enrichTaskDependencies calls so the limiter issues
a new call as soon as any slot frees. Alternatively, if adding a dependency is
undesired, implement a tiny in-file semaphore/queue that maintains a rolling
window, dequeues the next task when a slot frees, and invokes
this.enrichTaskDependencies(task) until tasksToEnrich is exhausted.

@subsy subsy merged commit 25b5107 into subsy:main Feb 20, 2026
9 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.

Parallel workers ignore beads-rust dependency chain

2 participants