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-frontendis gone — that bead was claimed and closed byfe-dev-agent.WAKE frontend-qanow 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-devandWAKE eng-mgrremain unchanged.
Summary
M5 compound label routing is working correctly:
- Compound WAKE keys — Multi-label beads produce a single WAKE line with sorted, dash-joined labels (
dev-frontend,backend-dev,frontend-qa). - Agent filtering — Multiple
--labelflags are sorted and joined to form a compound filter key. Thefe-dev-agentwith--label frontend --label devmatched only thedev-frontendWAKE line. - Grouping — Beads sharing the same compound key appear on one WAKE line (e.g.,
WAKE frontend-qa beads-ml6 beads-mx2). - Triage routing — Unlabeled beads still route to
eng-mgr. - DELEGATE with multi-label — Follow-up beads created via DELEGATE with comma-separated labels inherit the correct compound routing.