Fleet Feed
Fleet Feed
Section titled “Fleet Feed”scripts/dashboard/fleet-feed.js builds one bob-fleet-feed/1 document
listing every project on every machine in machines.json (#2069). It is the
data behind bob viz.
- This machine is read live. It has
source: "live"andage_s: 0. - Every other machine comes from its newest snapshot under
$BOB_FLEET_SNAPSHOTS(default$BOB_HOME/fleet/snapshots). It hassource: "snapshot", the snapshot’sage_s, andstale. - A machine with no snapshot is still listed. It has
source: null, no projects, and areason. - A value the source did not record is
null, and renders asnot measured. It is never filled with a default such as0tokens or anidlephase.
Commands
Section titled “Commands”node scripts/dashboard/fleet-feed.js # one block per machine, "not measured" where a value is nullnode scripts/dashboard/fleet-feed.js --json # the bob-fleet-feed/1 documentnode scripts/fleet/project-live.js <root> # one project's live condition, the reader both paths useDocument
Section titled “Document”{ "schema": "bob-fleet-feed/1", "generated_at": "2026-09-13T21:10:00.000Z", "stale_after_s": 86400, "refresh_s": null, "machines": [ { "id": "farm-01", "label": "The Farm (worker 01)", "current": true, "source": "live", "as_of": "2026-09-13T21:10:00.000Z", "age_s": 0, "stale": false, "reason": null, "counts": { "projects": 1, "running": 1, "attention": 0 }, "projects": [ { "name": "bigbrain", "session": "active", "phase": "coding", "elapsed_s": 14100, "tokens": 957861, "issue": 2056, "chunk": { "index": 1, "total": 3 }, "last_commit": { "sha": "da0bba1…", "subject": "feat(#2056): …", "at": "2026-09-13T20:40:00+00:00", "age_s": 1800 }, "project_ready": null, "preflight": null, "state": "running", "reasons": ["active-session"] } ] }, { "id": "macbook-air", "label": "Paul's MacBook Air", "current": false, "source": null, "as_of": null, "age_s": null, "stale": null, "reason": "no snapshot published (audit.js snapshot --install-timer on that machine)", "counts": { "projects": 0, "running": 0, "attention": 0 }, "projects": [] } ], "active_now": [{ "machine": "farm-01", "name": "bigbrain" }], "projects_by_repo": { "octo/bigbrain": [{ "machine": "farm-01", "name": "bigbrain" }] }, "repos": { "octo/bigbrain": { "state": "running", "reasons": ["active-session"], "last_active_at": "2026-09-13T20:40:00.000Z", "running_on": ["farm-01"] } }}active_now (#2928) is what is moving across the fleet, for the
Active now strip: {machine, name}
references into machines[].projects[], never copies of the rows. It lists
every running project, then every attention project, then every project
an open todo waits on (#3038: one per repo, the current machine’s checkout
first, never from a stale machine), then the other
projects by last_active_at, newest first, until it holds
DASH_ACTIVE_NOW_MAX (default 6; a whole number, 0 lists only running and
attention). The cap bounds only the recent fill-ins: a running or attention
project is never cut. Ties break by machine id, then name, in codepoint order.
A project whose recency is not measured, or that is parked or archived, never
fills in. A stale snapshot’s project is never running: it enters only as
attention, and only when a fact of its own (a run, a blocked preflight, a
dead run, a failing bob ready) puts it there, so a stale machine does not
flood the strip. activeNow() in scripts/dashboard/fleet-feed.js derives it,
and tests/test-fleet-feed.js covers it.
projects_by_repo (#2929) groups the checkouts of one repo across machines:
repo (owner/name) → [{machine, name}], machines in registry order, repos in
codepoint order. A checkout joins its repo’s list whatever its directory is
called on that machine. A project whose repo is null is in no list and is
never merged. repos carries each repo’s combined figures, so the page never
derives them: state is running when any checkout runs, else the worst of
the rest (attention, then needs-you, then idle), so a laptop’s stale copy does not paint a
live repo attention; reasons is every checkout’s, once each;
last_active_at is the newest across the checkouts (null when none is
measured); running_on names the machines holding a live run; needs_you is the
repo’s todo fact (#3038), the same on every checkout, since a todo is filed on the repo. The
bob viz grid is built on both.
groupByRepo() derives them, and tests/test-fleet-feed.js covers them.
Machine keys
Section titled “Machine keys”| Key | Meaning |
|---|---|
id, label |
From machines.json. Machines are listed in registry order. A snapshot from a machine that is not in the registry is ignored. |
current |
true for the machine that built the document. |
source |
live: read on this machine just now. snapshot: the newest published snapshot. null: no snapshot exists. If live state can’t be read, this machine falls back to its own snapshot. |
as_of |
When the rows were read: now for live, the snapshot’s generated_at for snapshot. null when not known. |
age_s |
Seconds since as_of. Always 0 for live. A future-dated snapshot counts as 0. null when there is no snapshot, or when its generated_at can’t be parsed. |
stale |
true when age_s is more than stale_after_s. null when age_s is null. |
reason |
Why a machine has no rows or no age. null otherwise. |
quota |
The GitHub quota as this machine’s gh shim last read it (#3190): the compact bob-forge-quota/1, as {measured, fetched_at, window_seconds, resources: {graphql, core, search: {remaining, limit, reset_at, state, spent_in_window}}, calls_in_window, callers: [{caller, session, calls, cost}], held_seconds, rate_limited: {week, target, defect, last_at}}. Read live on the machine that built the document, from summary.quota of the snapshot for the others. null when not measured: no snapshot, or one published before #3190. Left out of the push signature, since it moves every minute. |
counts |
{projects, running, attention, needs_you}: how many projects the machine lists, how many of them have each state (#2477), and how many an open todo waits on (#3038). A collapsed machine section on /bob says these in words. |
Project keys
Section titled “Project keys”| Key | Meaning |
|---|---|
session |
active means a run is alive. stale means a state file exists but the engine’s probe says the run is dead. none means there is no state file. null means not measured: the state file was unreadable, or the snapshot predates #2069. |
phase |
The warp-drive phase in the state file, or null. A stale session still shows the phase where it stopped. |
elapsed_s |
Only for an active run: seconds from the run’s start to as_of. A snapshot row gives elapsed time as of when the snapshot was taken, not up to now. |
tokens |
Tokens the run has spent: the larger of token_usage.session_total.total_tokens and the sum of its chunk snapshots. |
issue, chunk |
What the run is working on (#2925), for the bob viz card’s now row: the requirement number and {index, total} (index counts from 0), from the state file as scripts/fleet/project-live.js reads it. null when there is no run, the state file does not record it, or the snapshot predates #2924. |
flightplan, plan_progress |
The run’s flight plan number, and how far that plan is as {done, total}: its work items ticked and in all (#3917). The engine records the progress on the run’s state file each time it ticks an item at an item boundary, from what flightplan.js check read of the plan issue; no forge call is made for the feed. plan_progress is null for a run with no plan, and until the run crosses its first item boundary. |
last_commit |
{sha, subject, at, age_s} from git log -1, or null. age_s is seconds from at to the document’s generated_at, measured against now even on a snapshot row, since a commit time is absolute (#2070). It is null when at can’t be parsed. |
activity |
Commit activity (#2154): {dirty, commits_by_day}, 30 daily commit counts, oldest first, the last being the 24 hours before the source read the project, and whether the tree is dirty. From the snapshot or the live reader, both activityCondition in scripts/fleet/project-live.js. null for a missing checkout, a directory outside git, a malformed record, or a snapshot published before #2154. |
project_ready |
The last bob ready verdict from .claude/.project-ready.json ({verdict, ok, at, blockers}). It is never re-run. null if the project has never been graded. |
preflight |
Only for a run in blocked (#1923): {checked_at, failures: [{check, fix}], todo: {number, url, repo} | null}, from the failed preflight that blocked it (the kickoff gate or a mid-session re-check). null for any other phase, and for a snapshot published before #2071. Notices turn it into a blocked notice. |
handoffs |
What waits on Paul in this checkout (#3058): the handoff records the engine wrote to .claude/.warp-handoff.json, as [{key, kind, what, why, text, command, at, todo: {number, repo, url} | null, plan}]. They outlive the warp state file, so a run that ended waiting still shows. [] when there are none. null when the file is unreadable, and for a snapshot published before #3058. Notices attach each one to its todo or blocked notice. |
sessions |
Every session in the checkout that is not the warp run (#2978): bob decompose / bob do machines and bob groom / bob plan build / bob ritual prepare runs, read through bob-session-state/1 as {active: [{kind, phase, target, tokens, host, elapsed_s}], stale: [{kind, phase, target, tokens, host}], last: {kind, phase, target, tokens, host, finished_at, age_s, outcome: {status, summary}} | null}. stale holds sessions whose process is gone. elapsed_s is measured like the run’s, and last.age_s like last_commit.age_s. Neither is part of the push signature. A live session makes the project running, and a stale one adds the dead-run reason. null for a snapshot published before #2978. |
last_active_at |
When the project last moved (#2923): the newest of last_commit.at and, for an active run only, the run’s last phase transition (the newest history[].at in its state file, else budgets.phase_started_at). ISO 8601, or null when no fact is readable. null is not measured, never “long ago”: bob viz sorts it last. A closed session report would be a third fact, but no snapshot carries one yet, so the feed does not read it. |
active_from |
Which fact set last_active_at: last-commit or warp-transition. null with it. |
active_age_s |
Seconds from last_active_at to generated_at, measured against now like last_commit.age_s. null with last_active_at. Left out of the push signature, since it moves with the clock. |
portfolio |
The project manifest’s _meta.portfolio (invest, maintain, park or archive, written by bob ritual decide, #2235), or null when none is declared or the manifest is unreadable. A park or archive project steps aside on the bob viz grid (#2923). A snapshot published before #2923 has null. |
repo |
Which repo the checkout is (#2929): owner/name, lower-cased, from its origin remote. The SSH, HTTPS and ssh:// forms normalise to the same value, and a credential in the URL is never kept. null when there is no origin remote, or the snapshot predates #2929. A null repo is never merged with another checkout. |
branch |
The checked-out branch (git rev-parse --abbrev-ref HEAD), else the run’s branch. null when neither is read. |
default_head |
{branch, sha}: the default branch (origin/HEAD’s, else main or master) and its local SHA, else origin’s. null when not read. The reader also records the branch’s last 50 SHAs; the feed compares them and never publishes them. |
divergence |
For a repo with more than one checkout (#2929), where this checkout’s default branch stands against the others: {status: "behind", of: <machine>, by: N} when its SHA is N commits down that machine’s recent SHAs (the largest wins), {status: "current"} when every other measured checkout is on its SHA or behind it, {status: "not-measured"} when there is no SHA to compare, or two SHAs are in neither list (diverged, or more than 50 commits apart). null for a repo with one checkout, or no repo. Read from the snapshots only: nothing is fetched. |
state |
The project at a glance (#2477), derived by the feed from the keys above. attention when any attention fact holds, else running for an active session, else needs-you when an open todo waits on it (#3038), else idle. attention wins over a live session, because a blocked run is still active; a todo never outranks either. |
needs_you |
The project’s open todo issues (#3038), from the notices document the Notices column reads (bob-notices/1, the same filter as projectNotices): {count, p1, oldest: {ref, title, priority, since, age_s, url, command, text}}. oldest is the longest-waiting todo; its command and text are its handoff record’s (#3058) when it has one, else the Notices row’s command and null. A todo carrying completed is never a notice, so it never counts. {count: 0, p1: 0, oldest: null} when none waits; null when the todo source was not read (or the server has not built notices yet): not measured, never zero. The dashboard server stamps it from its last notices build and restamps the feed as soon as a build sees the todo set change. |
reasons |
The facts that set state, in this order: blocked (phase is blocked: a failed preflight), dead-run (session is stale), not-ready (project_ready.ok is false), stale-snapshot (the machine’s stale is true, so a row that says running can’t be trusted). A running row has ["active-session"], a needs-you row ["open-todo"], an idle row []. A null session or staleness is not measured and never sets attention by itself. |
A project the source recorded as absent (exists: false) is not listed. Of
this machine’s entries in projects.json, only active projects that are
present on disk are listed.
Served by the dashboard
Section titled “Served by the dashboard”bob dash serve serves the document at /fleet.json. It also pushes it as
event: fleet on /warp/events, the SSE stream the /warp page already holds
open. bob viz is the page built on it. The server
is scripts/dashboard/server.js, the one dashboard server (#2081).
The Launchpad reads the same document for its project list: this machine’s
live row names the projects it shows, built with the same loader
(loadFleetFeed) and reused for the same 10 s default
(LAUNCHPAD_FEED_REFRESH_S). See the
Launchpad Links Index.
| Behaviour | Detail |
|---|---|
| Refresh | Rebuilt at most once every refresh_s seconds (DASH_FLEET_REFRESH_S, default 10), however many tabs are open, and only while a stream is connected. |
| Push | A stream gets the feed when it connects. After that, it gets it again only when the feed changes (feedSignature). A change is a machine’s source, a snapshot’s as_of, stale, reason, or any project value except elapsed_s and last_commit.age_s. The clock alone is not a change: a viewer that shows ages re-reads /fleet.json once per refresh_s, as the bob viz page does. |
| Snapshot pickup | A .json written to the snapshot directory wakes the check early (fs.watch, debounced 500 ms). The interval tick is the guarantee: a published snapshot reaches every open stream within one refresh interval. |
| Reads only | The server reads published snapshots. It never pulls over SSH, probes a machine, or creates the directory. audit.js snapshot writes each file to a temp name and renames it into place, so a reader never sees a half-written file. |
| Failure | If the feed cannot be built, the response has machines: [] and an error, never a made-up fleet. An unreadable machines.json is one of these: error is machine registry unreadable: … naming the file, and the page shows fleet feed failed: … instead of an empty fleet. The registry is found through operatorPath and the source-resolution chain ($BOB_SOURCE, then this copy’s own checkout, then ~/projects/bigbrain), so the deployed copy the server runs reads the same file the checkout does (#2544). |
Running now: last activity and the stuck threshold
Section titled “Running now: last activity and the stuck threshold”The Now page of BoB Control lists every live session in the feed and marks a quiet one stuck.
| Fact | Value |
|---|---|
| Threshold | 60 minutes: STUCK_AFTER_MINUTES in scripts/dashboard/control-running.js. A session quiet for longer is stuck. |
| Last-activity source | A warp run’s project last_active_at: the newer of its last commit and the run’s last phase transition (active_from names which). |
| Clock | Quiet time is measured at the source’s clock: now for this machine, the snapshot’s as_of for another. An old snapshot never makes a run look quieter than it was seen to be. |
| Not measured | A decompose or do session, and a groom, plan or ritual run, carry no activity fact in the feed. Their row says last activity not measured and they are never marked stuck. |
| Stale machine | A machine whose snapshot is past the freshness window has no row: its sessions are last-known, not live. |
Staleness
Section titled “Staleness”stale_after_s comes from BOB_FLEET_STALE_SECONDS (default 86400, 24 h).
It is the same window audit.js view uses. scripts/fleet/project-live.js
owns both that window and the snapshot directory, and audit.js reads them
from there. A snapshot exactly at the window is not stale; one second older
is.
Where the rows come from
Section titled “Where the rows come from”scripts/fleet/project-live.js reads one project from disk. It makes one
state-file read, one .project-ready.json read and one git log -1. The
session probe is the engine’s own isStale (pid, 24 h age, recent-write
grace). The feed calls this reader for this machine’s rows. audit.js snapshot
calls it too, so every published snapshot carries projects[].warp,
projects[].last_commit, and projects[].repo and projects[].default_head
(repoIdentity(): git remote get-url origin, the default branch and one
git rev-list --max-count=50, #2929). A snapshot row and a live row are always derived the same way.
See also
Section titled “See also”- Fleet Snapshot Timer: keeps each machine’s snapshot fresh
- Project Monitor Exposure Posture: the monitor is tailnet-only
tests/test-fleet-feed.js: live and snapshot merge, staleness thresholds, a machine with no snapshot, and the reader