A lightweight Go daemon that monitors a workspace for ready beads issues and spawns Claude workers.
- Scoped to workspace: One daemon per mono-repo copy, only manages local issues
- Epic-aware: Only spawns workers for issues that are children of in-progress epics
- Crash recovery: Detects dead workers and resumes them using stored session IDs
- Idle detection: Nudges workers that have been idle too long
- State persistence: Survives daemon restarts
cd daemon
make build
# Binary at bin/beadsd
# Or install to GOPATH
make install# Run in a workspace
beadsd --workspace /path/to/monorepo
# With options
beadsd \
--workspace /path/to/monorepo \
--max-workers 3 \
--poll-interval 30s \
--idle-timeout 10m| Flag | Default | Description |
|---|---|---|
--workspace |
. |
Workspace root directory (must have .beads/) |
--max-workers |
3 |
Maximum concurrent Claude workers |
--poll-interval |
30s |
How often to check for ready issues |
--idle-timeout |
10m |
Time before nudging an idle worker |
Every poll interval, the daemon:
- Checks worker health - Detects dead/idle sessions
- Finds ready issues - Runs
bd ready --unassigned - Filters by epic - Only issues with in-progress epic parents
- Spawns workers - Up to max-workers limit
Ready Issue Found
│
▼
┌─────────────────┐
│ Claim Issue │ bd update <id> --assignee --status in_progress
│ Store Session ID│ bd update <id> --external-ref session:<uuid>
└────────┬────────┘
│
▼
┌─────────────────┐
│ Spawn tmux │ tmux new-session -d -s beadsd-<id>
│ Run Claude │ claude --session-id <uuid> -p '/beads-issue <id>'
└────────┬────────┘
│
┌────┴────┐
│ Working │
└────┬────┘
│
┌────┴─────┐ ┌────────────────┐
│ │ │ │
▼ ▼ ▼ │
Completes Dies Goes Idle │
│ │ │ │
▼ ▼ ▼ │
bd close Resume Nudge ──────────────┘
│ │
▼ │
Cleanup ◄─────┘
When a worker session dies unexpectedly:
- Daemon detects missing tmux session
- Checks if issue was closed (success case)
- If not closed, resumes with
claude --resume <session-id> - Full conversation context is restored
The daemon persists worker state to .beads/daemon-state.json:
{
"po-abc123": {
"issue_id": "po-abc123",
"session_name": "beadsd-po-abc12",
"session_id": "e0a1966f-e266-4230-b625-72c86f783d25",
"started_at": "2024-01-24T10:00:00Z",
"last_active": "2024-01-24T10:30:00Z"
}
}To have the daemon pick up work:
-
Create an epic and set it to in-progress:
bd create --type epic --title "Feature X" bd update <epic-id> --status in_progress
-
Create child tasks:
bd create --title "Implement component A" --parent <epic-id> bd create --title "Implement component B" --parent <epic-id>
-
The daemon will automatically spawn workers for ready children.
To pause the daemon from processing an epic:
# Move epic back to open (or any status other than in_progress)
bd update <epic-id> --status openWorkers for that epic's children won't be spawned until the epic is back to in_progress.
Run separate daemons for each workspace:
# Terminal 1: Main development
beadsd --workspace ~/code/polymer
# Terminal 2: Experimental branch
beadsd --workspace ~/code/polymer-experiment --max-workers 1Each daemon only manages issues in its workspace.
The daemon logs to stdout:
2024/01/24 10:00:00 beadsd starting in /home/user/code/polymer (max workers: 3, poll: 30s)
2024/01/24 10:00:00 Patrol: checking for work...
2024/01/24 10:00:00 Found 2 ready issues under in-progress epics
2024/01/24 10:00:01 Spawned worker beadsd-po-abc12 for issue po-abc123: Implement feature
2024/01/24 10:00:01 Patrol complete: 1 active workers
Workers are organized in tmux by epic:
- Sessions: One per epic (named
beadsd-<epic-id-prefix>) - Windows: One per worker/issue (named by issue ID prefix)
This allows easy navigation between workers on the same epic:
# List all epic sessions
tmux list-sessions | grep beadsd
# Attach to an epic's session
tmux attach -t beadsd-po-abc12
# Cycle between workers in an epic
# (use Ctrl-B n for next window, Ctrl-B p for previous)The current nudging is simple: send a text message after idle timeout. For more sophisticated nudging, consider:
Instead of a static message, use an LLM to analyze:
- Worker's recent output: Capture last N lines from tmux pane
- Issue context: What the worker was supposed to accomplish
- Stuck patterns: Detect repeated errors, loops, or lack of progress
The LLM could then generate targeted nudges:
- "I see you're getting a 404 error. Have you checked if the endpoint path changed?"
- "You've been modifying the same file for 20 minutes. Would it help to break this into smaller steps?"
- "The build is failing on line 45. Try checking the import statement."
- Gentle reminder (first nudge): "Status check, are you still working?"
- Context-aware prompt (second nudge): LLM analyzes output and suggests next step
- Supervisor escalation (third nudge): Alert a parent agent or human
- Auto-reassign (final): Kill worker, clear assignee, let another worker pick it up
// Future: LLM-based nudge generation
func (d *Daemon) generateSmartNudge(worker *Worker) string {
// Capture recent pane output
output := d.capturePaneOutput(worker, 50) // last 50 lines
// Get issue context
issue := d.getIssue(worker.IssueID)
// Call LLM to analyze and generate nudge
prompt := fmt.Sprintf(`
Worker is processing issue: %s
Description: %s
Recent terminal output:
%s
Generate a helpful nudge message if the worker seems stuck.
If progress looks normal, respond with "OK".
`, issue.ID, issue.Title, output)
return callLLM(prompt)
}- Error loops: Same error message repeated 3+ times
- No output: No new lines in pane for extended period
- Build failures: Detecting common failure patterns
- Prompt waiting: Claude waiting for input (shows
>prompt)
- Check for in-progress epics:
bd list --type=epic --status=in_progress - Check for ready issues:
bd ready --unassigned - Verify issues have parent epic set:
bd show <issue-id>
- Check if session ID was stored:
bd show <issue-id>should showexternal-ref: session:<uuid> - Try manual resume:
claude --resume <uuid>
Reduce max-workers:
beadsd --max-workers 1