Skip to main content

M5: Multi-Project Labels and Routing

2026-02-20T23:02:52Z by Showboat 0.6.0

What we are testing

M5 adds compound label routing. Beads can carry multiple labels (e.g. frontend + dev). The watcher sorts labels alphabetically and joins them with a dash to form a single compound WAKE key: WAKE dev-frontend <id>. Agents filter with multiple --label flags, which are likewise sorted and joined. Unlabeled beads still route to eng-mgr for triage.

export BEADS_DIR=$(pwd)/beads/.beads && bd list --status open --json | jq -r '.[].id' | while read id; do bd close "$id" --reason 'cleanup'; done 2>/dev/null; echo 'Cleanup done'
✓ Closed beads-zz0: cleanup
Cleanup done

Cleaned up any leftover beads. Now we create four beads with different label combinations to test compound routing.

export BEADS_DIR=$(pwd)/beads/.beads && bd create 'OAuth login flow' --labels frontend,dev -d 'Implement OAuth2 for the frontend app' --json
warning: beads.role not configured. Run 'bd init' to set.
{
"id": "beads-zt4",
"title": "OAuth login flow",
"description": "Implement OAuth2 for the frontend app",
"status": "open",
"priority": 2,
"issue_type": "task",
"owner": "test@b4arena.dev",
"created_at": "2026-02-20T23:04:00.254539Z",
"created_by": "b4arena",
"updated_at": "2026-02-20T23:04:00.254539Z"
}

Bead beads-zt4: labels [frontend, dev] — expected compound WAKE key: dev-frontend. (Note: bd's JSON output omits the labels field, but they are stored.)

export BEADS_DIR=$(pwd)/beads/.beads && bd create 'REST API pagination' --labels backend,dev -d 'Add cursor-based pagination to REST API' --json
warning: beads.role not configured. Run 'bd init' to set.
{
"id": "beads-53o",
"title": "REST API pagination",
"description": "Add cursor-based pagination to REST API",
"status": "open",
"priority": 2,
"issue_type": "task",
"owner": "test@b4arena.dev",
"created_at": "2026-02-20T23:04:08.616914Z",
"created_by": "b4arena",
"updated_at": "2026-02-20T23:04:08.616914Z"
}

Bead beads-53o: labels [backend, dev] — expected compound key: backend-dev.

export BEADS_DIR=$(pwd)/beads/.beads && bd create 'E2E test suite' --labels frontend,qa -d 'Write Cypress E2E tests for login' --json
warning: beads.role not configured. Run 'bd init' to set.
{
"id": "beads-ml6",
"title": "E2E test suite",
"description": "Write Cypress E2E tests for login",
"status": "open",
"priority": 2,
"issue_type": "task",
"owner": "test@b4arena.dev",
"created_at": "2026-02-20T23:04:15.8788Z",
"created_by": "b4arena",
"updated_at": "2026-02-20T23:04:15.8788Z"
}

Bead beads-ml6: labels [frontend, qa] — expected compound key: frontend-qa.

export BEADS_DIR=$(pwd)/beads/.beads && bd create 'Triage: unknown request' -d 'No labels, should route to eng-mgr' --json
warning: beads.role not configured. Run 'bd init' to set.
{
"id": "beads-qfd",
"title": "Triage: unknown request",
"description": "No labels, should route to eng-mgr",
"status": "open",
"priority": 2,
"issue_type": "task",
"owner": "test@b4arena.dev",
"created_at": "2026-02-20T23:04:23.858532Z",
"created_by": "b4arena",
"updated_at": "2026-02-20T23:04:23.858532Z"
}

Bead beads-qfd: no labels — should route to eng-mgr for triage.

Verify labels are stored

bd show should confirm labels are present on multi-label beads.

export BEADS_DIR=$(pwd)/beads/.beads && bd show beads-zt4 --json | jq '.[0] | {id, title, labels}'
{
"id": "beads-zt4",
"title": "OAuth login flow",
"labels": [
"dev",
"frontend"
]
}

Labels [dev, frontend] confirmed (stored sorted alphabetically by bd).

Watcher output — compound WAKE lines

Running beads-watcher.sh to see how multi-label beads produce compound WAKE keys.

export BEADS_DIR=$(pwd)/beads/.beads && bash scripts/beads-watcher.sh
WAKE backend-dev beads-53o
WAKE dev-frontend beads-zt4
WAKE frontend-qa beads-ml6
WAKE eng-mgr beads-qfd

Each multi-label bead appears as a single compound WAKE line with labels sorted and dash-joined:

  • WAKE backend-dev beads-53o — from labels [backend, dev]
  • WAKE dev-frontend beads-zt4 — from labels [dev, frontend]
  • WAKE frontend-qa beads-ml6 — from labels [frontend, qa]
  • WAKE eng-mgr beads-qfd — unlabeled bead routed to triage

Agent filtering with compound labels

A frontend-dev agent uses --label frontend --label dev. These are sorted and joined to form filter key dev-frontend, matching only the OAuth bead.

export BEADS_DIR=$(pwd)/beads/.beads && bash scripts/beads-watcher.sh | BD_ACTOR=fe-dev-agent bash scripts/agent-sim.sh --label frontend --label dev

Agent ran silently (no errors). Let's verify which beads were claimed.

export BEADS_DIR=$(pwd)/beads/.beads && bd list --json | jq '[.[] | {id, title, status, labels}]'
[
{
"id": "beads-53o",
"title": "REST API pagination",
"status": "open",
"labels": [
"backend",
"dev"
]
},
{
"id": "beads-jnm",
"title": "M2-QA: mixed-claimed",
"status": "in_progress",
"labels": [
"dev"
]
},
{
"id": "beads-ml6",
"title": "E2E test suite",
"status": "open",
"labels": [
"frontend",
"qa"
]
},
{
"id": "beads-npk",
"title": "M2-QA: dev task A",
"status": "in_progress",
"labels": [
"dev"
]
},
{
"id": "beads-pen",
"title": "Implement OAuth login",
"status": "in_progress",
"labels": [
"dev"
]
},
{
"id": "beads-qfd",
"title": "Triage: unknown request",
"status": "open",
"labels": null
}
]
export BEADS_DIR=$(pwd)/beads/.beads && bd show beads-zt4 --json | jq '.[0] | {id, title, status, assignee}'
{
"id": "beads-zt4",
"title": "OAuth login flow",
"status": "closed",
"assignee": "fe-dev-agent"
}

The OAuth bead (beads-zt4) is now closed and assigned to fe-dev-agent. The other three beads remain open — the agent correctly filtered on compound key dev-frontend and ignored backend-dev, frontend-qa, and eng-mgr lines.

DELEGATE with multi-label

A backend-dev agent delegates a follow-up with labels frontend,qa. The agent-comm.sh DELEGATE command passes comma-separated labels to bd create --labels.

export BEADS_DIR=$(pwd)/beads/.beads && OPEN_ID=$(bd list --status open --json | jq -r '.[0].id') && echo "Delegating from bead: $OPEN_ID" && echo "DELEGATE $OPEN_ID frontend,qa 'Review pagination UX'" | BD_ACTOR=be-dev-agent bash scripts/agent-comm.sh && bd list --status open --json | jq '[.[] | {id, title, labels}]'
Delegating from bead: beads-53o
[
{
"id": "beads-53o",
"title": "REST API pagination",
"labels": [
"backend",
"dev"
]
},
{
"id": "beads-ml6",
"title": "E2E test suite",
"labels": [
"frontend",
"qa"
]
},
{
"id": "beads-mx2",
"title": "'Review pagination UX'",
"labels": [
"frontend",
"qa"
]
},
{
"id": "beads-qfd",
"title": "Triage: unknown request",
"labels": null
}
]

DELEGATE created a new bead beads-mx2 ('Review pagination UX') with labels [frontend, qa]. This follow-up will produce WAKE key frontend-qa, routing it to a QA+frontend agent.

Updated watcher output after DELEGATE

The new follow-up bead should appear alongside the existing open beads.

export BEADS_DIR=$(pwd)/beads/.beads && bash scripts/beads-watcher.sh
WAKE backend-dev beads-53o
WAKE frontend-qa beads-ml6 beads-mx2
WAKE eng-mgr beads-qfd

Key observations:

  • WAKE dev-frontend is gone — that bead was claimed and closed by fe-dev-agent.
  • WAKE frontend-qa now lists two bead IDs: the original E2E test suite (beads-ml6) and the delegated follow-up (beads-mx2). Beads with the same compound key are grouped on a single WAKE line.
  • WAKE backend-dev and WAKE eng-mgr remain unchanged.

Summary

M5 compound label routing is working correctly:

  1. Compound WAKE keys — Multi-label beads produce a single WAKE line with sorted, dash-joined labels (dev-frontend, backend-dev, frontend-qa).
  2. Agent filtering — Multiple --label flags are sorted and joined to form a compound filter key. The fe-dev-agent with --label frontend --label dev matched only the dev-frontend WAKE line.
  3. Grouping — Beads sharing the same compound key appear on one WAKE line (e.g., WAKE frontend-qa beads-ml6 beads-mx2).
  4. Triage routing — Unlabeled beads still route to eng-mgr.
  5. DELEGATE with multi-label — Follow-up beads created via DELEGATE with comma-separated labels inherit the correct compound routing.