M2: Watcher Script Routes by Label
2026-02-20T19:16:35Z by Showboat 0.6.0
QA Session: M2 Watcher Script Routes by Label
Starting M2 QA. The watcher script (scripts/beads-watcher.sh) polls for open beads, groups by label, and outputs WAKE lines. Unlabeled beads route to eng-mgr.
Pre-existing State
Found 1 existing bead: beads-84n (open, label=dev, unassigned). Will work around existing state.
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Current beads ---' && bd list --json 2>/dev/null | jq -r '.[] | "\(.id) status=\(.status) labels=\(.labels // []) assignee=\(.assignee // "none")"' && echo '--- Watcher output ---' && bash scripts/beads-watcher.sh
--- Current beads ---
beads-84n status=open labels=["dev"] assignee=none
--- Watcher output ---
WAKE dev beads-84n
export BEADS_DIR=$(pwd)/beads/.beads && bd create 'M2-QA: dev task A' --labels dev -d 'Test bead for M2 QA - same label grouping A' --json 2>/dev/null | jq -r '.id'
beads-npk
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Test: Multiple beads same label ---' && bash scripts/beads-watcher.sh
--- Test: Multiple beads same label ---
WAKE dev beads-84n beads-npk
export BEADS_DIR=$(pwd)/beads/.beads && bd create 'M2-QA: pm task' --labels pm -d 'Test bead for M2 QA - pm label' --json 2>/dev/null | jq -r '.id'
beads-hmz
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Test: Different labels → separate WAKE lines ---' && bash scripts/beads-watcher.sh
--- Test: Different labels → separate WAKE lines ---
WAKE dev beads-84n beads-npk
WAKE pm beads-hmz
export BEADS_DIR=$(pwd)/beads/.beads && bd create 'M2-QA: unlabeled task' -d 'Test bead with no labels for eng-mgr routing' --json 2>/dev/null | jq -r '.id'
beads-e1h
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Test: Unlabeled bead → WAKE eng-mgr ---' && bash scripts/beads-watcher.sh
--- Test: Unlabeled bead → WAKE eng-mgr ---
WAKE dev beads-84n beads-npk
WAKE pm beads-hmz
WAKE eng-mgr beads-e1h
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Claiming beads-npk ---' && bd update beads-npk --claim --json 2>/dev/null | jq -r '.[0] | "id=\(.id) status=\(.status) assignee=\(.assignee)"' && echo '--- Test: Claimed bead excluded ---' && bash scripts/beads-watcher.sh
--- Claiming beads-npk ---
id=beads-npk status=in_progress assignee=Marcel Hild
--- Test: Claimed bead excluded ---
WAKE dev beads-84n
WAKE pm beads-hmz
WAKE eng-mgr beads-e1h
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Closing beads-hmz ---' && bd close beads-hmz --reason 'M2 QA: testing closed exclusion' --json 2>/dev/null | jq -r '.[0] | "id=\(.id) status=\(.status)"' && echo '--- Test: Closed bead excluded ---' && bash scripts/beads-watcher.sh
--- Closing beads-hmz ---
id=beads-hmz status=closed
--- Test: Closed bead excluded ---
WAKE dev beads-84n
WAKE eng-mgr beads-e1h
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Creating multi-label bead (dev + qa) ---' && bd create 'M2-QA: multi-label task' --labels dev --labels qa -d 'Test bead with both dev and qa labels' --json 2>/dev/null | jq -r '.id'
--- Creating multi-label bead (dev + qa) ---
beads-0e6
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Test: Multi-label bead in multiple WAKE lines ---' && bash scripts/beads-watcher.sh
--- Test: Multi-label bead in multiple WAKE lines ---
WAKE dev beads-0e6 beads-84n
WAKE qa beads-0e6
WAKE eng-mgr beads-e1h
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Closing all open beads ---' && bd close beads-84n --reason 'M2 QA: testing empty state' --json 2>/dev/null | jq -r '.[0].status' && bd close beads-0e6 --reason 'M2 QA: testing empty state' --json 2>/dev/null | jq -r '.[0].status' && bd close beads-e1h --reason 'M2 QA: testing empty state' --json 2>/dev/null | jq -r '.[0].status' && echo '--- Test: No open beads → no output ---' && output=$(bash scripts/beads-watcher.sh) && if [ -z "$output" ]; then echo 'PASS: No output (empty)'; else echo "FAIL: Got output: $output"; fi
--- Closing all open beads ---
closed
closed
closed
--- Test: No open beads → no output ---
PASS: No output (empty)
Edge Case Tests
Now testing scenarios beyond the brief: labels with special characters, many beads, WAKE line ordering determinism.
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Edge: Label with hyphens and dots ---' && bd create 'M2-QA: hyphenated label' --labels front-end -d 'Label with hyphen' --json 2>/dev/null | jq -r '.id' && bd create 'M2-QA: dotted label' --labels 'team.alpha' -d 'Label with dot' --json 2>/dev/null | jq -r '.id'
--- Edge: Label with hyphens and dots ---
beads-edz
beads-qjv
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Test: Special character labels ---' && bash scripts/beads-watcher.sh
--- Test: Special character labels ---
WAKE front-end beads-edz
WAKE team.alpha beads-qjv
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Edge: Label with space ---' && bd create 'M2-QA: space label' --labels 'dev ops' -d 'Label with embedded space' --json 2>/dev/null | jq -r '.id'
--- Edge: Label with space ---
beads-gdp
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Test: Space-in-label WAKE line ---' && bash scripts/beads-watcher.sh
--- Test: Space-in-label WAKE line ---
WAKE dev ops beads-gdp
WAKE front-end beads-edz
WAKE team.alpha beads-qjv
Finding: Space-in-label breaks WAKE line parsing. Label 'dev ops' produces 'WAKE dev ops beads-gdp' — a consumer parsing 'WAKE
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Edge: Ordering determinism (3 runs) ---' && r1=$(bash scripts/beads-watcher.sh) && r2=$(bash scripts/beads-watcher.sh) && r3=$(bash scripts/beads-watcher.sh) && if [ "$r1" = "$r2" ] && [ "$r2" = "$r3" ]; then echo 'PASS: All 3 runs identical'; echo "$r1"; else echo 'FAIL: Output differs between runs'; echo "Run 1: $r1"; echo "Run 2: $r2"; echo "Run 3: $r3"; fi
--- Edge: Ordering determinism (3 runs) ---
PASS: All 3 runs identical
WAKE front-end beads-edz
WAKE team.alpha beads-qjv
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Setup: Mixed state scenario ---' && echo 'Creating 3 new beads...' && id1=$(bd create 'M2-QA: mixed-open' --labels dev -d 'stays open' --json 2>/dev/null | jq -r '.id') && id2=$(bd create 'M2-QA: mixed-claimed' --labels dev -d 'will be claimed' --json 2>/dev/null | jq -r '.id') && id3=$(bd create 'M2-QA: mixed-closed' --labels qa -d 'will be closed' --json 2>/dev/null | jq -r '.id') && id4=$(bd create 'M2-QA: mixed-unlabeled' -d 'no label, stays open' --json 2>/dev/null | jq -r '.id') && echo "Created: $id1(open/dev) $id2(will-claim/dev) $id3(will-close/qa) $id4(open/unlabeled)" && bd update $id2 --claim --json 2>/dev/null | jq -r '.[0].status' && bd close $id3 --reason 'M2 QA mixed test' --json 2>/dev/null | jq -r '.[0].status' && echo '--- Test: Mixed state watcher ---' && bash scripts/beads-watcher.sh
--- Setup: Mixed state scenario ---
Creating 3 new beads...
Created: beads-wd9(open/dev) beads-jnm(will-claim/dev) beads-xe6(will-close/qa) beads-3lj(open/unlabeled)
in_progress
closed
--- Test: Mixed state watcher ---
WAKE dev beads-wd9
WAKE front-end beads-edz
WAKE team.alpha beads-qjv
WAKE eng-mgr beads-3lj
export BEADS_DIR=$(pwd)/beads/.beads && bash scripts/beads-watcher.sh; echo "Exit code: $?"
WAKE dev beads-wd9
WAKE front-end beads-edz
WAKE team.alpha beads-qjv
WAKE eng-mgr beads-3lj
Exit code: 0
echo '--- Edge: BEADS_DIR unset ---' && unset BEADS_DIR && bash scripts/beads-watcher.sh 2>&1; echo "Exit code: $?"
--- Edge: BEADS_DIR unset ---
Exit code: 1
Finding: BEADS_DIR unset causes silent exit code 1. When BEADS_DIR is unset, bd fails and the script exits with code 1 but no error message (stderr suppressed by 2>/dev/null). A Tier 1 consumer would see a non-zero exit but no explanation. Minor — acceptable for a watcher that runs in a loop, but could add a BEADS_DIR check at the top for better diagnostics.
export BEADS_DIR=$(pwd)/beads/.beads && echo '--- Edge: Multiple unlabeled beads ---' && id5=$(bd create 'M2-QA: unlabeled-2' -d 'second unlabeled bead' --json 2>/dev/null | jq -r '.id') && echo "Created $id5" && bash scripts/beads-watcher.sh | grep eng-mgr
--- Edge: Multiple unlabeled beads ---
Created beads-1sy
WAKE eng-mgr beads-1sy beads-3lj
QA Summary
Tests Passed (8/8)
- Single labeled bead → correct WAKE line
- Multiple beads same label → grouped into one WAKE line
- Different labels → separate WAKE lines (dev, pm)
- Unlabeled bead → routes to WAKE eng-mgr
- Multiple unlabeled beads → grouped into single eng-mgr line
- Claimed bead excluded → not in watcher output
- Closed bead excluded → not in watcher output
- Multi-label bead → appears in multiple WAKE lines (dev + qa)
- Mixed state → correct filtering across open/claimed/closed
- Output determinism → 3 consecutive runs produce identical output
- Special-char labels → hyphens and dots work correctly
- No open beads → silent exit, no output (exit code 0)
Findings (non-blocking)
- Space-in-label breaks WAKE format. Label 'dev ops' produces ambiguous WAKE line where the consumer cannot distinguish label tokens from bead IDs. Recommend: reject spaces in labels or add quoting to WAKE format.
- BEADS_DIR unset causes silent exit code 1. The watcher exits non-zero with no diagnostic output. A pre-flight check for BEADS_DIR would improve debuggability.