One afternoon a repeating task was due in my database, and nothing showed up on screen.
The task lived in beads - our task list for agents. It had a due time. It was set to repeat every thirty minutes. The agent that should have been woken just sat there. Quiet. Not crashed. Simply never told.
The thing that was supposed to notice was firstmate, the orchestrator that starts work and watches other agents. Firstmate had gotten stuck. So the deadline arrived in the database, and the person-shaped agent session that should have gotten a poke never did.
That was the day I stopped treating the task list like a notebook the orchestrator sometimes reads. It had to become a tiny work operating system of its own.
What broke
I run a small fleet of coding agents. Some use pi, some use Claude Code, some use Codex. They live in terminal panes managed by herdr. Think of herdr as a window manager for agents: it can list every live agent and send a message into a pane without caring which product is running inside.
Firstmate is useful. It is not a coding tool. It can start any coding tool. I had still made it the main switchboard for every due task: when something came due, firstmate was supposed to notice and nudge someone.
Two bad patterns showed up the same day:
- When firstmate got stuck, every due ping died - even though the task database was fine.
- As I added more agents, firstmate got buried under wake-ups instead of doing calm work.
If the only smoke alarm runs on the kitchen circuit, a kitchen short kills the alarm too. Firstmate was on that circuit.
The split that worked
Three jobs. Three owners.
Beads owns the work list. Tasks, due dates, repeat rules, and who is assigned. Our house copy is RooseveltAdvisors/beads. Normal tasks must have a due date. Repeats use a simple schedule (like “every half hour on the clock”). When a due time hits, beads records it and moves the due date forward so a repeating task keeps knocking instead of going silent forever.
One system timer owns time. A plain Linux user timer (bd-due-sweep.timer) runs bd due sweep on a fixed beat. That is the only clock. Not firstmate’s watcher. Not an agent loop. Not “someone will check later.” If the machine is up, the clock runs. We learned this after the watcher went dark twice in one day.
Herdr owns waking live agents. After a sweep marks dues, beads writes a small durable queue under .beads/notify/ - one line per fired task, aimed at the assignee (I call that a seat). The same timer tick then runs bd notify drain, which:
- Asks herdr for every running session and every agent pane
- Matches the assignee name to a pane (session name, a title like
π - wiseman, or a folder name) - Sends
herdr agent promptto that pane
Pi, Claude Code, Codex - herdr already knows which is which. Beads never talks to those products directly. If herdr can see a new tool, delivery works without a beads change.
beads-tui is the human board for the same list: overdue marks, repeat marks, @assignee tags, and a search box with tab-complete. It does not need firstmate or a model to open.
One timer, not two
We first built two timers: one to fire dues, one to send wakes. That was one too many.
The short version that still works:
bd-due-sweep.timer
→ bd due sweep --json # fire dues + write notify queue
→ bd notify drain --all # find pane in herdr + send prompt
→ stamp last-sweep
If the agent is offline, the queue line stays. The next sweep tries again. No second clock. No special code path for “wiseman” or “firstmate” in delivery - those are just assignee names that often match herdr session names.
Assignee is who. Tool is how.
That was the mental cleanup.
- Assignee = stable seat name (
wiseman,firstmate,portal-ops, …). Who should get the nudge. - Harness = which coding tool is running right now (pi today, Claude Code tomorrow).
- Firstmate = one seat among many, and a launcher. Not the pager for the whole fleet.
If a task has no assignee, the wake has nowhere to go. When we turned this on, hundreds of open tasks had a blank assignee. That is a data cleanup job - not a reason to send everything through firstmate “so it can decide.”
What I would tell past me
- Do not put the only clock inside the busiest orchestrator. OS timers are dull. Dull is good for deadlines.
- Separate “work is due” from “someone got poked.” A small durable queue between those two steps beats a clever live call that vanishes when a process dies.
- Deliver through the thing that already sees every tool. For us that is herdr’s agent list and agent prompt. Do not build one adapter per coding product.
- The human board and the agent wake path should read the same database. beads-tui and
bd notifyboth sit on beads. No second shadow list.
The phrase I want to keep
The task graph is the work OS. The orchestrator is an app on that OS.
When I had that backwards - firstmate as the OS, beads as a notebook - due work went dark whenever the app locked up. With beads holding time and the notify queue, and herdr holding “who is alive in which pane,” a stuck orchestrator is a local problem again. Deadlines keep moving. Nudges keep trying. The board still opens.
It is a small system. It is also the difference between a fleet that forgets its chores and one that keeps its own calendar.