# Understanding the Handoff Daemon (handoffd.bb) in SwarmForge

> Discover the SwarmForge handoff daemon (handoffd.bb). Learn how it monitors outboxes, routes messages, manages approvals, and syncs task boards for distributed work-trees.

- Repository: [Robert C. Martin/swarm-forge](https://github.com/unclebob/swarm-forge)
- Tags: internals
- Published: 2026-08-31

---

**The handoff daemon (handoffd.bb) is the core background service in SwarmForge that continuously monitors role-specific outboxes, routes handoff messages between distributed work-trees, manages approval workflows for git handoffs, and synchronizes the project task board.**

The SwarmForge project (unclebob/swarm-forge) relies on `handoffd.bb` to act as the central message router that keeps multiple role-based repositories synchronized. Written in Babashka, this daemon operates as a persistent process (or one-shot scanner) that bridges communication gaps between isolated work-trees without requiring manual file copying.

## Configuration and Daemon Initialization

When `handoffd.bb` starts, the **`configure!`** function parses command-line arguments to determine operational mode. The daemon accepts either a path for continuous operation or the **`--once`** flag for single-scan execution. During initialization, it establishes critical filesystem paths for the project state, role definitions, tmux socket location, and logging directories.

According to the source code in `swarmforge/scripts/handoffd.bb`, the configuration phase sets up:
- The **daemon folder** for PID and log files (`.swarmforge/daemon/`)
- Path resolution for the **tmux socket** used for notifications
- **Role table** loading via `load‑roles` from `.swarmforge/roles.tsv`

## The Polling Loop and Message Detection

In continuous mode, the **`run-daemon!`** function implements the main event loop. The daemon repeatedly invokes **`poll-once!`** followed by a configurable sleep interval (`poll-ms`), creating a lightweight polling mechanism that minimizes resource usage while maintaining responsiveness.

The **`poll-once!`** function (lines 99-107) performs three critical actions:
1. Loads the role table to understand the project topology
2. Reads the current tmux socket configuration
3. Recursively scans all `.handoff` files in each role's **outbox** directory and the project-wide outbox

This scanning approach ensures that any role—from **master** to specialized development roles—can deposit handoff files that the daemon will detect and route.

## Message Processing and Delivery Pipeline

For each detected handoff file, the daemon calls **`process-outbox-file!`**, which initiates a sophisticated delivery workflow defined in **`deliver!`** (lines 50-71).

### The Delivery Workflow

The **`deliver!`** function executes several sequential operations:

- **Parsing**: Extracts headers using `parse-message` to determine routing (from, to, type)
- **Board Updates**: For **`git_handoff`** types, triggers **`update-board!`** which calls [`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) to move tasks between lanes on the physical board
- **Routing**: Copies the message to each recipient's **inbox** (`target-path`) and appends delivery timestamps via `add‑delivery‑headers`
- **Notification**: Sends tmux alerts to recipient sessions using **`notify!`**
- **Archival**: Moves the original to the sender's **sent** folder and archives the sender's board state if needed

### Approval and Hold Logic

Not all handoffs proceed immediately. The **`should-hold?`** function (lines 92-98) implements a gatekeeping mechanism specifically for **git_handoff** messages originating from the **master** role. When such a handoff targets a single recipient who hasn't yet approved the change, the daemon moves the file to the **pending_approval** directory via **`hold!`**, preventing premature delivery until explicit approval is granted.

## Task Board Integration

The handoff daemon maintains synchronization between code changes and project management through **`update-board!`**. This function invokes [`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) to manipulate the task board data stored in `.swarmforge/board/tasks.tsv`, automatically moving tasks between columns (such as from "In Progress" to "Done") when associated handoffs are processed.

## Running the Handoff Daemon

The daemon supports two operational modes depending on your workflow requirements.

**Continuous mode** (default behavior):

```bash

# From the root of a SwarmForge project

./swarmforge/scripts/handoffd.bb /path/to/project

```

**One-shot execution** (ideal for CI/CD pipelines):

```bash
./swarmforge/scripts/handoffd.bb --once /path/to/project

```

**Manual handoff creation** for testing:

```bash
cat > .swarmforge/handoffs/outbox/example.handoff <<EOF
id: 12345
from: master
to: dev
type: git_handoff
task_id: task-42
message: Deploy latest changes
EOF

```

**Monitoring daemon activity**:

```bash
tail -f .swarmforge/daemon/handoffd.log

```

## Graceful Shutdown and Process Management

The daemon implements clean shutdown semantics through **`shutdown!`** and the **`should-stop?`** predicate. When a stop file is detected or an internal flag is set, the daemon:
1. Completes the current polling cycle
2. Removes its PID file
3. Writes final entries to `.swarmforge/daemon/handoffd.log`

This ensures that no handoff files are left in a partially processed state during restart or deployment operations.

## Summary

- **The handoff daemon** (`handoffd.bb`) serves as the central message bus for SwarmForge, routing `.handoff` files between role-specific work-trees.
- **Continuous polling** via `poll-once!` and `run-daemon!` ensures real-time message delivery with configurable intervals.
- **Approval workflows** prevent master-to-single-recipient git handoffs from auto-delivering until explicitly approved.
- **Board synchronization** occurs automatically through `update-board!` and the [`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) helper script.
- **Tmux integration** provides real-time notifications to developers when handoffs arrive in their inboxes.

## Frequently Asked Questions

### What triggers the handoff daemon to process a message?

The daemon processes messages based on filesystem polling. When `poll-once!` detects `.handoff` files in any role's outbox directory (`.swarmforge/handoffs/outbox/`), it immediately queues them for processing through `process-outbox-file!`. Unlike event-driven systems, SwarmForge uses intentional polling to avoid filesystem watcher limitations across different operating systems.

### How does the daemon handle messages that require approval?

When `should-hold?` determines a git handoff from the master role requires approval (typically when sent to a single recipient who hasn't approved), the daemon moves the file to `pending_approval/` using `hold!` instead of calling `deliver!`. The message remains there until manual approval moves it back to the outbox or the recipient status changes.

### Can I run the handoff daemon without continuous polling?

Yes. Pass the **`--once`** flag when invoking the script. This executes a single scan of all outboxes, processes any pending handoffs, and immediately exits. This mode is particularly useful in automated CI pipelines where you want to trigger handoff processing as a discrete step rather than maintaining a background process.

### Where does the daemon store its logs and PID information?

The daemon writes operational logs to `.swarmforge/daemon/handoffd.log` and maintains its process ID in `.swarmforge/daemon/handoffd.pid`. These paths are established during the `configure!` phase and are used by `shutdown!` to manage graceful termination and debug logging.