Skip to content

A Night on the Farm

The night shift, once. In Coming Back to a Project you looked for what the farm did while you were away. In Watching BoB Work you watched a run on your own machine. This tutorial puts the two together: you hand hello-links to the farm for one night, watch its tick start from your laptop, and read what it did in the morning. Every command below comes from The Farm, which also explains why the farm works the way it does.

← Tutorials · docs home

Stop When You will
1 Evening See what the farm is and whether it is alive
2 Evening Delegate one requirement to the farm
3 Evening Reach the farm and give it the project
4 Bedtime Start one tick, dry run first
5 Bedtime Watch the tick from your laptop
6 Morning Read what the farm did
7 Morning Read the limits the farm never crosses

Then a “When it breaks” section for the nights that do not go to plan.

Allow half an hour in the evening and a quarter of an hour in the morning. The farm works in between.

  • Finish Coming Back to a Project first. This tutorial reuses its hello-links project, pushed to GitHub. Watching BoB Work taught the views you use to watch the tick.
  • A farm. A fleet machine with the role farm-worker in your operator repo’s machines.json, on your tailnet. The examples call it farm-01.
  • One small approved requirement in hello-links, not serial-only, for example “The links page shows how many links there are”. File it with /requirement and approve it.
  • Replace the placeholders. <N> is that requirement’s number, <owner> is the GitHub owner of hello-links, and <project> is hello-links.

From your laptop:

Terminal window
node ~/.claude/scripts/fleet/machine.js get farm-01
bob status --scope fleet --no-live

What you should see.

  • scripts/fleet/machine.js get farm-01 prints the farm’s entry as JSON: "role": "farm-worker", its tailscale_name, and a bob_workload block with the tick’s schedule (for example daily 02:30 America/Los_Angeles) and dry_run.
  • bob status --scope fleet --no-live prints the fleet table. The farm’s row has STATUS cached and an AGE under a day.

What just happened. The farm is a machine like your laptop, with its own BoB and its own GitHub login. What makes it the farm is its declared workload: a timer that runs one tick a night, under a lock so that two ticks never overlap. The tick grooms, plans, runs one warp-drive session and writes down what it did. The Farm explains each piece.

Checkpoint. The farm’s row says cached, not cached(stale) or never. A stale farm is off or has stopped publishing; fix that before you hand it work.

From the hello-links directory on your laptop:

Terminal window
gh issue edit <N> --add-label farm
node ~/.claude/scripts/warp-drive/eligibility.js check <N>
bob operator show
bob plan build --from farm

What you should see.

  • gh issue edit prints the issue’s URL.
  • scripts/warp-drive/eligibility.js check <N> prints the issue as eligible and exits 0. If it prints excluded, the reason names the fix: approve it, remove serial-only, or add the missing label.
  • bob operator show prints your operator profile and the repo it came from. Open provisions/hello-links.json there: _meta.active is absent or true.
  • bob plan build --from farm prints the plan issue’s number. The plan, titled Flight plan: farm (unattended), lists your requirement. Any other approved issue is printed as [dropped] with the reason, such as no farm label.

What just happened. You delegated the work explicitly. The farm takes an issue only when one rule says yes, and the farm label is how you say yes for one issue. Nothing that needs you, such as a serial-only item or a blocked one, can reach the farm. The plan you built is a preview: the tick rebuilds it when it starts.

Checkpoint. The farm plan lists your requirement, and nothing you did not label.

From your laptop, then on the farm:

Terminal window
tailscale ping --c 1 --timeout 3s farm-01
node ~/.claude/scripts/fleet/audit.js pull farm-01
node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'gh auth status'
ssh farm-01
bob ready <owner>/<project>

What you should see.

  • tailscale ping prints pong from farm-01 with the address and whether the path is direct or through a relay (via DERP). Both work.
  • scripts/fleet/audit.js pull farm-01 prints the farm’s snapshot as JSON and caches it.
  • gh auth status on the farm prints Logged in to github.com with the farm’s account.
  • ssh farm-01 opens a shell on the farm. There, bob ready <owner>/hello-links prints a repo step cloned, then its other steps, and ends ready.

What just happened. Every command went over the tailnet, and none of them moved a secret. The farm has its own GitHub login; you checked that it is there, and if it were not, you would log in on the farm itself. BoB never copies a token from one machine to another. bob ready cloned hello-links next to the farm’s other projects and linked its BoB tooling, exactly as it does on a laptop.

Checkpoint. On the farm, ls ~/projects/hello-links lists the project, and bob ready ended ready.

Still on the farm, at bedtime:

Terminal window
ls ~/.claude/.bob-tick.pause
bob tick --dry-run --cwd ~/projects/<project>
tmux new-session -d -s <project>-night 'bob tick --cwd ~/projects/<project>'

What you should see.

  • ls ~/.claude/.bob-tick.pause prints No such file or directory. If the file exists, the farm is paused: every tick exits 3 and does nothing. Read why with cat before you bob tick --resume, because the pause covers the scheduled tick too.
  • bob tick --dry-run --cwd ~/projects/hello-links prints each step and ends with a digest whose Taken row reads would take 1: and your requirement.
  • The tmux new-session line returns at once. Leave the SSH session with exit.

What just happened. The dry run walked the whole tick read-only. The real tick now runs in the hello-links-night tmux session on the farm: it takes the lock, grooms, rebuilds the farm plan and starts a Level 3 warp-drive run on it, then waits for that run. The run merges through a pull request that must pass CI, never by pushing to main.

Checkpoint. The dry run’s Taken row names your requirement, and nothing else.

Back on your laptop, before you go to bed:

Terminal window
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 viz

What you should see.

  • tmux ls on the farm lists hello-links-night and the warp-drive session the tick started.
  • The run’s state on the farm, as JSON: a phase such as coding, and requirement with your requirement’s number.
  • bob viz shows the farm’s tiles. They are as old as the farm’s last snapshot, so hello-links may not show the run until the tick publishes its own.

What just happened. You watched the farm without logging in to it. Each --cmd is one short command over SSH with a 30-second limit, which is why the tick itself runs in tmux on the farm and not over your connection.

Checkpoint. The run’s state on the farm names your requirement.

The next morning, from your laptop, in the hello-links directory:

Terminal window
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 3
gh pr list --state merged --limit 5
node ~/.claude/scripts/fleet/audit.js pull farm-01
bob status --scope fleet --no-live
git -C ~/projects/bigbrain tag --list 'nightly-*' --sort=-creatordate

What you should see.

  • The digest from the farm: ## bob tick — <date> on farm-01, a Verdict, and a table with Taken, Merged, Blocked (todos filed), Tokens and Budget rows.
  • The farm plan issue, with the digest as its newest comment and your requirement ticked.
  • The session summary the run filed, and its chunk reports.
  • The merged pull request, with fixes #<N> in its body and its checks green.
  • After the pull, the farm’s fleet row with an AGE of a few hours.
  • The nightly tags of BoB itself. hello-links never cuts one; the list only moves on the nights the farm maintains BoB.

What just happened. The farm left one record in each place you already know: the digest on the farm and on the plan, the run’s reports in the tracker, the pull request in the history and a fresh snapshot in the fleet view. Nothing it did is known only to the farm. Run /status in Claude Code in hello-links: its catch-up lists the farm’s merged work next to yours, and its next-work list no longer includes the requirement.

Checkpoint. The digest’s Merged row names your requirement, the requirement is closed, and /status names the farm’s work.

Still in the morning, from hello-links:

Terminal window
jq -r '.permissions.deny[]' ~/.claude/profiles/autonomous.json
node ~/.claude/scripts/promotion/promotion.js decide --unattended --json
node ~/.claude/scripts/fleet/deploy-pending.js status

What you should see.

  • The deny list of the autonomous profile, among them Bash(git push --force*), Bash(git reset --hard*), Bash(git branch -D *) and Bash(rm -rf *).
  • One JSON line from scripts/promotion/promotion.js decide --unattended: "merge":"pr", "merge_via":"merge-on-green", and a deploy no higher than your project’s ceiling ("none" for a ceiling of external or pr).
  • deploy-pending: nothing queued with exit 0, or the deploy that is waiting and since when.

What just happened. You read the three floors under every farm run, each where it is declared. The run cannot force-delete anything, because its permission profile refuses it. It cannot deploy past the promotion ceiling, because the shared deploy entrypoint and a hook both check it. It cannot redeploy BoB under a live Claude Code session, because that would silence the session’s hooks: the deploy is queued instead, and runs itself when the last session ends or at the next tick. None of these is a setting the night can change.

Checkpoint. scripts/promotion/promotion.js decide --unattended says "merge":"pr".


A night that did not go to plan says why, in the digest’s Verdict and in the tracker. From your laptop:

Terminal window
node ~/.claude/scripts/fleet/audit.js pull farm-01 --cmd 'tmux ls'
gh issue list --label todo --state open
node ~/.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 you see in the morning Why What to do
Verdict exit 3 and another tick holds …/.bob-tick.lock Another tick was running, often the timer’s own. tmux ls on the farm names it. Wait until it ends, then start yours again. Never delete the lock file.
The run ended blocked and a new todo is open The farm’s preflight failed: its BoB is unusable, or hello-links is not ready on the farm. Read the todo; bob doctor on the farm names a host fix. Do it and add the completed label. The next tick retries.
Merged is empty and writes are waiting GitHub was down or rate-limited overnight. The run queued its writes and parked before merging. bob forge queue status on the farm lists them. The next tick replays them; nothing is lost.
The farm’s fleet row is cached(stale), or every pull fails The farm is off, off the tailnet, or its timer stopped. tailscale ping farm-01 first. If it answers, the timer list shows whether the next tick is scheduled.

The explanation page has the same table with more on each cause: When a night goes wrong.


You handed one project to the farm for one night. You delegated one requirement with a label, reached the farm over the tailnet without moving a secret, started a tick and watched it from your laptop, read four records of the night in the morning, and read the three limits no night can cross. When a night goes wrong, the digest’s verdict and the table above tell you which of four things happened.

Next tutorial: Getting Started — Stand up a project and run the autonomous loop, start to finish.