Skip to content

beads-bv: label filter has no verify-and-fallback (unlike epic path) — getNextTask can silently return an out-of-scope task #401

Description

@djbclark

Summary

beads-bv's getNextTask() verifies that bv --robot-next's top pick actually belongs to the configured epic before returning it (falling back to the base beads tracker if not) — but performs no equivalent verification when a label filter is configured. Combined with bv --robot-next/--robot-triage silently ignoring the --label flag (see below), this means a beads-bv tracker configured with labels = [...] can silently hand an agent a task that doesn't carry that label at all, with no error, no fallback, and no indication anything went wrong.

Where

src/plugins/trackers/builtin/beads-bv/index.ts, getNextTask():

// Apply label filter if configured
const labels = this.getLabels();
if (filter?.labels && filter.labels.length > 0) {
  args.push('--label', filter.labels[0]!);
} else if (labels.length > 0) {
  args.push('--label', labels[0]!);
}

const { stdout, exitCode, stderr } = await execBv(args, this.getWorkingDir());
// ... parsed and returned directly, no re-check against the label ...

Compare to the epic path a few lines later in the same function:

// Verify the selected task belongs to the epic if epicId is set
const epicId = this.getEpicId();
if (filter?.parentId || epicId) {
  const parentId = filter?.parentId ?? epicId;
  const epicChildren = await this.getCachedEpicChildrenIds(parentId);
  if (!epicChildren.includes(nextOutput.id)) {
    // bv's top pick isn't in our epic — fall back to base beads
    // which filters by epic natively
    return super.getNextTask(filter);
  }
}

The epic path re-verifies via a real bd list --parent <epicId> query and has a safe fallback. The label path has neither.

Why this matters (contributing root cause, not the only one)

The underlying reason the label path can return an out-of-scope task at all: bv --help (v0.18.0) documents -l/--label as scoping only --robot-insights/--robot-plan/--robot-priority--robot-next and --robot-triage are not on that list, and empirically bv --robot-next --label <anything> returns the identical top pick regardless of the label value. So beads-bv is passing a flag to bv that bv itself doesn't apply to the code path being used — a bv-side gap. That part may be worth reporting separately against Dicklesworthstone/beads_viewer (or already known there — I didn't find an existing issue).

Filing this one against ralph-tui specifically because even if/when bv fixes its side, beads-bv's lack of a verify-and-fallback step for labels (unlike the existing epic path) is still a real gap — the tracker plugin has the pattern for exactly this kind of defense already built for epics, it's just not applied to labels too.

Impact (real, not hypothetical)

Hit this live: a beads-bv tracker configured with labels = ["repo:site-djbclark"] (part of a multi-controller setup sharing one Beads DB across several repos) returned a task labeled for a different repo. The coding agent proceeded to investigate that other repo's working directory — including evaluating whether to delete a branch there — before being caught by a supervising process. No damage occurred, but nothing in beads-bv itself would have prevented it from going further.

Suggested fix

Apply the same verify-and-fallback pattern already used for epics to labels: after getting bv --robot-next's pick, check the returned task's actual labels (from bd show <id> --json or similar) against the configured label filter, and fall back to super.getNextTask(filter) (which correctly filters by label via bd list --label, per the working listing-path code) if it doesn't match — mirroring exactly what already happens for epicId/parentId.

Workaround in use

Switching from label-based to epic-based scoping — organizing each consumer's tasks under a dedicated epic and using --epic <id> instead of labels = [...], since the epic path has the verification this issue is about. Epic scoping seems to be the officially primary mechanism per the docs (labels are described as "optional and complementary" with minimal documented behavior around the robot-* flags), so this isn't really a workaround so much as switching to the intended-primary path — but it'd be good for the docs and/or a runtime warning to say so explicitly, since nothing currently signals that label-based scoping lacks this safety net.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions