The Farm
The Farm
Section titled “The Farm”BoB’s night shift. The farm is a fleet machine that works while you sleep. It runs one bounded maintenance cycle, the tick, on a timer. It takes only the work you handed it, merges only what its CI proved green, and leaves a written record of the night. This page explains each of those choices. The A Night on the Farm tutorial hands a project to the farm for one night, and takes every command below from this page. The operational detail, step by step, is in Farm Tick (the night shift).
What the farm is
Section titled “What the farm is”Four pieces, each with one job:
- farm-01 is the machine: a Linux box on the tailnet, declared in the operator
repo’s
machines.jsonwith the rolefarm-worker. It has its own BoB checkout, its own~/.claudeand its own GitHub login. - The runner is the self-hosted GitHub Actions runner on the same machine. It runs the CI that a farm pull request must pass before it merges. See CI Locus.
- The tick is
bob tick: one cycle that grooms, builds a plan of delegated work, runs one warp-drive session on it, reconciles the plan, publishes a fleet snapshot and writes a digest. A systemd user timer, rendered from the machine’s declaredbob_workload, starts it once a night. - The lock is
$BOB_HOME/.bob-tick.lock. A tick takes it first; a second tick that finds it held exits 3 at once. Two ticks never share a machine.
From any machine you can see the farm’s declaration and whether it is alive:
node ~/.claude/scripts/fleet/machine.js get farm-01bob status --scope fleet --no-liveThe first prints farm-01’s entry: its role, its tailscale_name and its
bob_workload (the tick’s schedule, memory limit and whether ticks are still dry
runs). The second reads the snapshots each machine publishes. The farm’s row is
cached when its last snapshot is under a day old.
Why the farm takes only what you delegate
Section titled “Why the farm takes only what you delegate”An unattended run has nobody to ask. So the question “may the farm take this?” is
answered once, by one rule, before the night starts: eligibility.js. An issue is
eligible when it is an approved req (or a bug with a reproduction) that carries
the farm label. A project can drop the label requirement with
_workflow.farm_eligible: true. Nothing serial-only, blocked, unapproved or
narrative is ever eligible. The full rule is in
What it may take.
The project itself must be live: its manifest must not declare
_meta.active: false, because fleet readers skip an archived project. From the
project’s directory:
gh issue edit <N> --add-label farmnode ~/.claude/scripts/warp-drive/eligibility.js check <N>bob operator showbob plan build --from farmgh issue editdelegates one approved requirement. Only a human adds thefarmlabel; tooling never does it on its own.eligibility.js checkanswers for that one issue: exit 0 eligible, exit 3 excluded, with the reason.bob operator shownames the operator repo, whereprovisions/<project>.jsonlives._meta.activemust be absent ortruethere.bob plan build --from farmbuilds the plan the farm would take, as aFlight plan: farm (unattended)issue. Every issue it leaves out is printed as[dropped]with its reason. The tick rebuilds the same plan when it starts, so this is a preview you can read before bed.
Reaching the farm
Section titled “Reaching the farm”The farm is on the tailnet. You reach it over Tailscale SSH, from a laptop, with
the transport BoB already has: audit.js pull. It runs one command on the farm
over SSH with no prompt, an 8-second connect timeout and a 30-second limit. That
suits a probe. Anything longer, such as a tick, runs on the farm inside tmux.
tailscale ping --c 1 --timeout 3s farm-01node ~/.claude/scripts/fleet/audit.js pull farm-01node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'gh auth status'ssh farm-01bob ready <owner>/<project>tailscale pingchecks the tailnet path, and says whether it is direct or relayed.audit.js pull farm-01fetches the farm’s snapshot and caches it, so your fleet view is current. It reads the snapshot the farm’s own timer last published when that copy is fresh, and only otherwise runs a new one there (how a live pull reads a remote machine).audit.js pull … --cmd 'gh auth status'checks that the farm has its own GitHub login. The farm never borrows yours: a secret is checked for presence on the machine that needs it, never copied there from another. If it is missing, log in on the farm itself.sshgives you a shell on the farm. The last line runs there:bob readyclones the project into the farm’s projects directory and makes it warp-ready, with no prompts.
Two prerequisites, both one-time and both a human’s to do:
- SSH into the farm needs your laptop’s key in the farm’s
~/.ssh/authorized_keys(or Tailscale SSH enabled for the farm). - SSH from the farm into a Mac needs Remote Login on the Mac, which macOS ships off. The farm only needs this to audit your laptop. BoB reports it as a blocker and never turns it on for you. See tailscale-remote-ops.
Handing the night over
Section titled “Handing the night over”The timer runs one tick a night on the project its declaration names. To give the farm a different project for one night, run one tick for it by hand, on the farm, in tmux, so it outlives your SSH session. See first what it would do:
ls ~/.claude/.bob-tick.pausebob tick --dry-run --cwd ~/projects/<project>tmux new-session -d -s <project>-night 'bob tick --cwd ~/projects/<project>'- The pause file is machine-wide. While it exists, every tick exits 3 and does
nothing, the timer’s included.
bob tick --resumeremoves it for all of them. --dry-runruns the same pipeline read-only: it lists the work it would take and creates no issue, branch, PR or commit.- The last line starts the real tick and returns at once. The tick waits on its own warp-drive run, which works the plan at Level 3 and merges through pull requests that must pass CI.
Watching one tick
Section titled “Watching one tick”You do not need to stay on the farm. From the laptop:
node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'tmux ls'node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'node ~/.claude/scripts/warp-drive/state-machine.js status ~/projects/<project>'bob viztmux lson the farm lists the tick’s session and the warp-drive session it started.state-machine.js statuson the farm prints the project’s run state as JSON: itsphase, therequirementit is working and itschunk. It reads the same state filebob warp statusreads on your laptop.bob vizshows the farm’s tiles from its last snapshot. The tick publishes a new snapshot near its end, so the tiles catch up then.
The morning read
Section titled “The morning read”A tick leaves four records, each in its own place:
node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'cat "$(ls -t ~/.claude/fleet/ticks/*.md | head -1)"'gh issue list --label flight-plan --state all --search "farm (unattended) in:title"gh issue list --label session-report --state all --limit 3gh pr list --state merged --limit 5node ~/.claude/scripts/fleet/audit.js pull farm-01bob status --scope fleet --no-livegit -C ~/projects/bigbrain tag --list 'nightly-*' --sort=-creatordate- The digest.
$BOB_HOME/fleet/ticks/<date>.mdon the farm: the verdict and one row each for the plan, the work taken and merged, todos filed, the release, rituals, tokens and the budget. The same digest is commented on the plan issue and sent to your phone withbob notify. - The run’s issues. The farm plan carries the digest comment. The warp-drive
run filed its chunk reports and its session summary. Each merged PR has
fixes #Nand passed CI. - The fleet snapshot. The tick’s last act is a snapshot, so after a pull the
farm’s row is
cachedand fresh. - The nightly tag. When the tick maintains BoB itself, it tags BoB’s master
nightly-YYYY-MM-DDif master moved and CI is green. Your laptops update to it withbob update --channel nightly. A tick on any other project cuts no tag.
The /status report in the project reads the same tracker, so the farm’s merged
work and filed todos appear in its catch-up next to your own.
What the farm never does
Section titled “What the farm never does”An unattended run has floors that no level, flag or tick loosens. Each is declared in one place, and you can read each one:
jq -r '.permissions.deny[]' ~/.claude/profiles/autonomous.jsonnode ~/.claude/scripts/promotion/promotion.js decide --unattended --jsonnode ~/.claude/scripts/fleet/deploy-pending.js status- No force-delete. The tick’s run works at Level 3, under the
autonomouspermission profile. Its deny list refusesgit push --force,git reset --hard,git branch -Dandrm -rf, and a branch is only ever removed withgit branch -d, which refuses an unmerged one. - No deploy past the promotion ceiling.
promotion.js decide --unattendedprints what an unattended run may do in this project:mergeis alwayspr(merged only when CI is green), anddeploynever goes past the ceiling in the project’sdev.json. A deploy above it is refused by the shared deploy entrypoint and by thepromotion-ceiling-guardhook. - No redeploy under a live session. Rewriting
~/.claude/settings.jsonwould silence the hooks of every Claude Code session already running on the farm. Sodeploy.shandbob updaterefuse while a session is live, and queue the deploy in$BOB_HOME/.deploy-pendinginstead. The queue drains itself when the last session ends, at the next tick, or at the next fleet snapshot, under the tick lock.deploy-pending.js statusexits 0 when nothing is queued and 3 while a deploy waits;bob doctorshows it asdeploy queued since <time>. See Queued deploys.
The eligibility rule is the floor under all three: work you did not delegate never reaches the run at all.
When a night goes wrong
Section titled “When a night goes wrong”A tick that could not finish says why, in its digest and in its exit code. The four ways a night most often goes wrong, and the one command that tells you which:
node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'tmux ls'gh issue list --label todo --state opennode ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'bob doctor'node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'bob forge queue status'node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'systemctl --user list-timers bob-tick.timer'| What the morning shows | What happened | What to do |
|---|---|---|
Digest verdict exit 3, another tick holds …/.bob-tick.lock |
A tick was still running, or the timer’s own tick had started first. The lock is released when its tick ends. | tmux ls on the farm shows the tick that holds it. Wait for it to end, then start yours again. Never delete the lock file. |
The run stopped in blocked, a new todo filed |
The farm’s preflight gate failed: the farm’s BoB is unusable, or the project is not ready on the farm. The run filed one todo per cause, in the repo that owns it. | Read the todo. For a host cause, bob doctor on the farm names the fix. Do what the todo says and add completed; the next tick retries. |
| Merged row empty, writes waiting | GitHub was unreachable or rate-limited overnight. The run kept working, queued the writes it could not make, and parked at merging. |
bob forge queue status on the farm lists the waiting writes. The next tick replays them; nothing is lost. |
The farm’s fleet row is cached(stale) or the pull fails |
The farm is off, off the tailnet, or its timer stopped, so it has not published a snapshot for over a day. | tailscale ping farm-01 first. If it answers, systemctl --user list-timers on the farm shows whether the tick timer is still scheduled. |
None of these needs you to repair the farm by hand from the laptop. Each one either fixes itself on the next tick or files a todo that says what only you can do.