Briefing
Briefing — briefing/1 and bob brief
Section titled “Briefing — briefing/1 and bob brief”The one document a briefing is, and how it is assembled (#2243, capability #2224). (Back to reference index.)
A briefing is what BoB has done, is doing and will do across the whole
portfolio, plus what only the human can unblock — built from source data
with links, deterministically, so the page (#2244) and the Telegram message
(#2245) are views over one document and never prose of their own. bob brief
is the collector; briefing/1 is the contract it emits; --send is the
Telegram delivery.
bob brief # the human rendering, every gap namedbob brief --json # the briefing/1 documentbob brief --json --since 2026-09-15T06:00:00Z # an explicit window startbob brief --offline # local files only: no forge, no enginesbob brief --write # also store the document where /briefings lists itbob brief --send # store it and post the headline + page link through bob notifybob brief --send --scheduled # the same, unless this machine's declared quiet hours hold (#2246)bob brief --install-timer # the bob-brief systemd timer from the declared cadence (#2246)bob brief --write --retain-days 30 # store, then prune stored briefings older than 30 daysbob brief --html # render the page; stdout is one line and a URL (#2791)bob brief --index # regenerate the briefings index from the brief issues (#2792) # of every registered project, open first (#3102); its Project # column and filter name each brief's scope (#3040)bob brief --propose # what should be done first on each brief — report only (#2818)bob brief --propose --apply # record each proposal (or abstention) on its brief issuebob brief --area area:fleet # one area's page — a view; an issue only for a decision (#2820, #3101)bob brief --area all --apply # every area: label's page, a brief issue only where a decision isbob brief --project flows # one brief for one registered project, in its own repo (#3040)bob brief --project all --apply # every registered project, each brief filed or updatedbob brief --publish # what publishing the generated pages would change — report only (#2872)bob brief --publish --apply # copy them to the declared serving repo, commit, push, deploybob brief --reconcile # which brief actions the tracker already resolved — report only (#3100)bob brief --reconcile --apply # tick them, move the state label, close finished briefs, republish the index; # also briefs every area that drained (#3169)bob brief --completions # which areas drained since their last completion briefing — report only (#3169)bob brief --completions --apply # file, write, publish and announce an area-completion briefing for eachbob brief --completions --project . --apply # the project this directory is in (what a warp run starts)| Option | Effect |
|---|---|
--json |
The briefing/1 document on stdout (schemas/briefing.schema.json). |
--write |
Store the document in the briefings directory as <generated_at>.json — see Storing. |
--send |
Store it (implies --write), then post the headline and the page link through bob notify; the exit code is bob notify’s — see Telegram delivery. |
--install-timer / --uninstall-timer / --timer-status |
The bob-brief systemd user timer rendered from the declaration — see Schedule and retention. --machine <id> names the entry, --dry-run writes and runs nothing, --json prints brief-timer/1. |
--scheduled |
A scheduled briefing (the timer, the end of bob tick): inside the declared quiet hours nothing is made or stored and the exit is 0; an invalid declaration is exit 2 — see Schedule and retention. |
--retain-days <N> |
After a write, prune stored briefings older than N days — wins over the declared retention_days. |
--url <http://host:port> |
The Project Monitor base the page link uses instead of this machine’s tailnet address and the project’s ledger dashboard port. |
--on-quiet message|skip |
What a quiet briefing sends: one line (default; BOB_BRIEF_ON_QUIET declares it) or nothing. |
--since <ISO> |
The window start for Done — wins over the previous briefing. |
--now <ISO> |
The clock every window and date comparison uses (tests, replays). |
--offline |
Read local files only — every gh source, rituals and rotations become offline gaps. |
--snapshots <dir> |
The fleet snapshot directory (default $BOB_FLEET_SNAPSHOTS, else $BOB_HOME/fleet/snapshots). |
--ticks <dir> |
This machine’s tick digests (default $BOB_HOME/fleet/ticks). |
--briefings <dir> |
Where previous briefings are read from and stored (default $BOB_BRIEFINGS, else this machine’s declared bob_workload.brief.dir, else $BOB_HOME/briefings). |
--propose |
Ask the decision seam for each brief’s next action — see Proposing. Report-only; gathers no briefing. |
--apply |
With --propose, record each proposal or abstention on its brief issue. |
--issue <NN> |
With --propose, one brief instead of every open one. |
--limit <N> |
With --propose, cap how many briefs are asked about. |
--backend <name> |
With --propose, override the decision backend (jev / deterministic / off). |
--area <label> |
One area page — see Area pages. --area all walks every area:* label. A brief issue is filed only for an area that carries a decision; with --scheduled the pass writes as --apply does. |
--project <name> |
One project brief — see Project briefs. --project all walks every registered project; an unknown name exits 2 with the known ones. With --scheduled it runs after the portfolio briefing is delivered. |
--reconcile |
Tick every brief action whose named issues have all closed, and close the finished briefs — see Reconcile. Report-only; --apply writes. Exit 3 when the forge answers nothing. |
--publish |
Compare the generated pages with the declared serving target — see Publishing. Report-only; --apply publishes, --deploy also redeploys an unchanged set. |
Exit codes: 0 built and every source read in full · 2 usage error ·
3 built with at least one gaps[] entry. Exit 3 still prints the complete
document — a gap is a fact about one source, never a failed briefing. With
--send the exit code is bob notify’s (0 delivered · 3 no bridge, the
message printed · 4 transport error) and the gaps ride in the message and on
stderr — see Telegram delivery.
The contract (briefing/1)
Section titled “The contract (briefing/1)”schemas/briefing.schema.json, proved both ways under make check-schemas
(section 13: the committed sample validates, the rejected fixture is refused).
additionalProperties is refused at every level, so a renderer can rely on
exactly these keys.
| Key | What it holds |
|---|---|
schema |
briefing/1. |
generated_at |
When the document was built — the clock every comparison used. |
machine |
{id, label} of the building machine as machines.json names it, or null when the host is not in the registry. |
window |
{since, until, basis, previous} — see The window. |
sections |
needs_you, done, doing, will_do — each an array of items. |
counts |
The four section lengths, so a headline never counts the arrays itself. |
sources |
{<source name>: {status: ok | gap, items}} — what each source yielded. |
gaps |
[{source, reason, detail}] — every source that could not be read or answered partially, one entry each. |
Every item is {kind, project, title, link, machine, at, ref, detail}.
kind is a closed set and each kind belongs to one section:
| Section | Kinds | From |
|---|---|---|
needs_you |
todo, pr-review, blocked-run, harness-defect, rotation-overdue, ritual-overdue, area-complete, backup-missed, ask-source |
open todos (a completed todo awaits the consume path, #779, and is not listed), non-draft open PRs, warp runs held in blocked / budget_exceeded (linked to the preflight todo), open harness defects (#1838, linked to the first issue), overdue credential rotations (#2223), overdue rituals (#2222), held area-completion briefings still brief:new or brief:acting — the line names what is in the way (#3169, #3804), declared D1 backups past their scheduled run, one line per missed run however many checkouts report it (#3086) |
done |
pr-merged, req-closed, bug-closed, release, tick, area-shipped |
merged PRs, closed req and bug issues, releases, tick digests (#2054), area-completion briefings whose verdict is SHIP, filed brief:done (#3804) — within the window |
doing |
warp-session, pr-draft |
active warp sessions on any machine (this machine live, every other from its snapshot), draft PRs |
will_do |
plan-item, ritual-due, rotation-due |
the next 3 unchecked items of every open flight plan (#1068), rituals due or upcoming, rotations due or upcoming |
project is the registered project name (projects.json), the repo name when
the repo is not registered on the building machine, or null for a
portfolio-wide item such as a rotation. link is a URL or null — never text
pretending to be one; only sources that carry no URL (a remote session with no
issue, a rotation with no runbook URL, a ritual never held) are unlinked, and
the renderer prints such a line unlinked. machine is set where the item is
machine-bound (a session, a tick, a blocked run); at is when it happened,
started, was filed or is due; ref is owner/repo#N, a tag or a credential id;
detail is one qualifier (a priority, a phase, a token count, the plan an item
comes from).
The window applies to Done only. Needs you lists whatever is open, however old — a todo from June still needs the human.
The order is part of the contract, so the same inputs and clock give the same
bytes: needs_you grouped by kind in the order above, oldest waiting first;
done newest first; doing by kind then machine and project; will_do plan
items in plan order (plans by repo and number), then rituals and rotations by
due date. Ties break by machine, project, ref and title.
Sources
Section titled “Sources”Thirteen sources, each read once and each failing alone. sources names them;
gaps[] uses the same names.
| Source | Read | Yields |
|---|---|---|
gh:merged-prs |
gh search prs --merged --merged-at >=<since> over the registry’s owners |
pr-merged |
gh:closed-reqs / gh:closed-bugs |
gh search issues --label req|bug --state closed --closed >=<since> |
req-closed, bug-closed |
gh:open-todos |
gh search issues --label todo --state open |
todo |
gh:open-prs |
gh search prs --state open |
pr-review (non-draft), pr-draft (draft) |
gh:releases |
one gh api graphql query aliasing every registered repo’s newest 5 releases |
release |
gh:flight-plans |
gh search issues --label flight-plan --state open (the body’s unchecked - [ ] #N lines) |
plan-item |
gh:area-completions |
gh search issues --label brief --state open -- brief:area-complete (#3169) |
area-complete, area-shipped |
snapshots |
every machine’s newest bob-fleet-snapshot/1 under the snapshot directory |
warp-session, blocked-run, harness-defect, backup-missed (a project whose backups.verdict is stale; unknown makes no line), a remote machine’s tick |
"warp" |
this machine’s live warp-drive state per registered project, through scripts/fleet/project-live.js (the fleet feed’s reader) plus the issue the state file records on requirement |
warp-session, blocked-run |
ticks |
this machine’s tick-digest/1 files |
tick |
rituals |
ritual.js due --all --json (bob-rituals-due/1) |
ritual-overdue, ritual-due |
rotations |
cred-check.js due --no-file --json (bob-rotation-due/1) |
rotation-overdue, rotation-due |
ask-sources |
asks/sources.js --json (bob-asks-sources/1, bob asks sources) |
ask-source: one per filing path whose share of asks closed without completed exceeds _workflow.ask_moot_threshold (#3810). Its ref is the source name and it has no link |
The gh calls go through the gh shim (bin/gh), so the forge cache serves them
when the forge is down (#1749). Owners come from the projects registered on
the building machine whose origin names a GitHub repo; a search row from a
repo that is not registered here is still a project (its repo name). The
subprocess readers accept a complete document over a non-zero exit code —
cred-check.js due exits 3 on unknown with a full document, which is data,
not a gap.
A source that is missing, unreachable, invalid or partial is one gaps[]
entry {source, reason, detail}; the other sections are built regardless.
reason is a closed enum:
| Reason | When |
|---|---|
gh-missing / unauthenticated / rate-limited / unavailable |
The gh outcome, by the #1968 names — detail carries the stderr tail. |
timeout |
The read overran its budget (20 s per gh call, 120 s per engine). |
invalid |
The source answered, but not with its contract (non-JSON, a wrong schema, a wrong shape). |
truncated |
A full search page (100 rows) — the rows are consumed and the section may be incomplete. |
not-measured |
Nothing to read: no projects.json, no registered repo, no snapshot or tick directory, an engine script not beside brief.js. |
stale |
A machine’s newest snapshot is older than the stale window (BOB_FLEET_STALE_SECONDS). Named snapshots:<machine>. |
never |
A fleet machine has never published a snapshot. Named snapshots:<machine>; the building machine is exempt (its rows are live). |
offline |
--offline skipped the read. |
error |
The reader itself threw. |
The window
Section titled “The window”window.since is, in order: --since (basis: explicit); the generated_at
of the newest briefing/1 file in the briefings directory (basis: previous-briefing, previous naming the file); else 24 h before generated_at
(basis: default). window.until is generated_at. --write and --send
store a briefing there under its clock name, so each sent briefing is the next
one’s window start; the schedule and retention that keep the directory are
declared — see Schedule and retention. .claude/briefings/ is declared machine-local in
scripts/lib/derived-paths.json, so a briefing never lands in a repo and never
counts as in-flight work at a checkpoint.
The page (/briefings, #2244)
Section titled “The page (/briefings, #2244)”Where a briefing is read: on a phone, over the tailnet. The Project Monitor
(the dashboard server bob dash serve runs, the one bob warp viz and
bob viz open) serves three routes on the same port and bind as every other
page — no port, listener or DNS of their own, so the tailnet-only posture
(decision #1773,
exposure posture) holds by
construction:
| Route | What it serves |
|---|---|
/briefings |
The page (scripts/dashboard/briefings-page.html): the latest briefing, or the one ?b=<file> names, and the index of past ones beside it. |
/briefings.json |
bob-briefings/1: the index — every stored briefing/1 under the briefings directory, newest by its own generated_at (never by file name), each row a headline (file, generated_at, machine, window, the document’s counts, its gaps count); the newest carried inline as latest (latest_file names it) so one read paints the page; unreadable[] naming every file that is not a briefing with its reason; reason when the directory itself could not be read. |
/briefings/<file> |
One stored briefing by its file name — a plain name in the directory, never a path: 400 on a path, 404 on a name that is not a briefing. |
/bob/briefings.json |
bob-briefings/1 extended for bob viz’s Briefings pane (#2926): the index above plus areas[] — each open brief-labelled issue carrying a brief:area marker, with its number, title, area, url, page (the brief:page marker), page_url (the evidence block’s published link), status (its brief: label), regenerated date, age_days and evidence counts (open, approved, todos, prs, closed_since) — stale_after_days (7), generated_at, and sources.portfolio / sources["area-briefs"], each {status: ok | unavailable | not-measured, reason, fix, count}. It also carries projects[] (#3040) — each open project brief found by one gh search issues over the BoB checkout’s owner, deduplicated against the area read, with its project, repo, number, title, url, page, page_url, status, regenerated, age_days and evidence counts — and sources["project-briefs"] in the same shape. An unreadable forge is the #1968 gh reason with its fix, and the portfolio rows still list. |
The directory is $BOB_BRIEFINGS, else this machine’s declared
bob_workload.brief.dir, else $BOB_HOME/briefings — one resolution
(scripts/brief/schedule.js) shared with bob brief, so a declared directory
is the one the index lists. The server re-reads it at most
once per DASH_BRIEFINGS_REFRESH_S (default 300), so a briefing written later
appears within one interval. The schedule and retention that fill and trim it
are declared; the page reads what is there, and says no briefings directory or
no briefings stored rather than showing an empty page as if nothing ever
happened. node scripts/dashboard/briefings.js [--json] [--dir DIR] prints
the same index at the terminal.
scripts/dashboard/bob-briefings.js renders it (served as /bob/briefings.js,
byte-identical to the tested file): Needs you first, then Done, Doing,
Will do; each section head carries the document’s counts (never the array’s
length — a missing count is not measured, never 0); a section longer than
eight items shows its head and folds the rest under “N more”, so the top of
every section is on screen before any tail; every item links to its issue,
PR, run or release, and an item whose source has no URL says (no link)
rather than printing text that looks like one; a blocked run, harness defect
or overdue rotation/ritual reads in the bad colour and a live session in the
active one, each also a word; every gap is named under the sections with its
reason and detail beside a N of M sources read in full line. An order
control in the briefing’s head (#3040) flips each section between as listed
and by project — project name (case-folded) then date, newest first, a
portfolio-wide item after every named project — and keeps the choice in the URL
(?order=project); every item shows its project either way, or says
portfolio. The index lists
every stored briefing newest first, each linking to itself on the page with
its counts and gaps, the one shown marked. Pure, like the Reports pane
(#2073): no clock, no fetch, no DOM; every value from a document is escaped
and a link that is not http(s) is not a link.
Open it with bob viz --page briefings (--tailnet prints the URL a phone
opens); /bob and /briefings link each other in their headers. The page is
held to the bob viz design checklist by
tests/test-briefings-page.js — tokens for every size, a hairline only under
a section head, colour only for a state, the page painting its own ground and
ink, one column at phone width with every long line wrapping — and
node scripts/dashboard/bob-viz-shots.js renders it at 400 wide from the
committed fixture in both themes:


Telegram delivery (--send, #2245)
Section titled “Telegram delivery (--send, #2245)”The message is the prompt to look: short, with the counts that matter and one
link. bob brief --send builds the briefing, stores it (a link must point
at a stored document), composes at most five lines from it and posts them
through bob notify (#2048) — one sender, so the bridge configuration, the
secret handling and the degradation are bob notify’s, never a second copy.
Before the briefing, --send notices each missed backup not yet noticed
(#3086): scripts/backup/notice.js --apply sends one bob notify per missed
run found in the snapshots, deduplicated with bob tick’s in
$BOB_HOME/.backup-notices.json, and says backup notices — N sent on stderr.
The briefing itself lists the miss as a backup-missed line under Needs you.
See Dev lifecycle.
bob brief --send # store, then post the headline and the page linkbob brief --send --on-quiet skip # a quiet briefing sends nothingbob brief --send --url http://farm-01.tail.ts.net:6952 # the page base the link uses (default: resolved)bob brief --write # store only — what --send does before it composesThe message, line by line — a view over the document, never prose of its own
(composeMessage(doc, {link, linkReason, onQuiet}) in scripts/brief/brief.js,
pure):
| Line | Content | When |
|---|---|---|
| 1 | Needs you: N · <machine>, <clock> — the needs-you count first |
always |
| 2 | Done N · Doing N · Will do N [· N gaps] — the headline counts from the document’s counts, never the arrays |
always |
| 3 | Top: <title> (<project>, <detail>) — the first needs_you item, the title clipped to 120 characters |
something needs you |
| 4 | that item’s link | it has one |
| 5 | the tailnet link to this briefing on the page, or page: <why there is none> |
always, last |
Five lines is the contract: the composer refuses a longer message rather than truncating one. When nothing needs the human the Top lines are absent (three lines); when an item is unlinked its link line is absent.
Quiet. A briefing with an empty window and nothing needing the human —
counts.needs_you and counts.done both 0 — sends one line: Quiet: nothing needs you, nothing done since <window start>, followed by what is
still in flight (Doing N, Will do N), the gap count and the page link. Under
--on-quiet skip (or BOB_BRIEF_ON_QUIET=skip, the declared default a
schedule can set; the flag wins) a quiet briefing sends nothing: the
document is still stored, stderr says quiet — nothing sent (on-quiet skip),
exit 0. --on-quiet message is the default.
The link is http://<tailnet host>:<dashboard port>/briefings?b=<file> —
the ?b= the page’s own index uses (#2244), so the message opens the exact
briefing it summarises, not whichever is newest by the time it is read. The
host is this machine’s Tailscale address the way bob viz --page briefings --tailnet binds it (scripts/dashboard/bind.js: the MagicDNS name, else the
IP — never a guess, never a wider bind); the port is the dashboard slot of the
project whose monitor serves the page — the nearest .git/.claude ancestor
of the working directory, else the BoB source checkout, exactly how bob viz
chooses — read from the port ledger, never allocated (a briefing does not write
the ledger). --url http://host:port stands in for both. A link that cannot be
resolved (Tailscale stopped, no ledger band, no project) is sent as its reason
on the last line (page: tailnet address: …) — never text pretending to be a
URL — and named on stderr.
Delivery is bob notify <message> --level warn|info --session-id bob-brief
(scripts/rdb/notify.sh beside the engine): level warn when something needs
you, info otherwise; the bridge’s project label is whatever bob notify
resolves. With --send the exit code is bob notify’s, because a scheduled
sender needs the delivery verdict: 0 delivered (or quiet and skipped) ·
3 no bridge configured — bob notify prints the message instead, so it is
never lost · 4 transport error (curl failed or a non-2xx answer) · 2
usage. The gaps that would have made a plain bob brief exit 3 ride in the
message (· N gaps) and on stderr (bob brief: N gaps — gh:open-todos rate-limited, …); the briefing is still complete and stored. Without --json,
bob notify’s output is stdout — the delivered line, or the printed message when
there is no bridge; with --json the document is stdout and every delivery
line is stderr. The secret is never printed by either.
Storing (--write, implied by --send). The document lands in the
briefings directory ($BOB_BRIEFINGS, else the declared bob_workload.brief.dir, else $BOB_HOME/briefings) as
<generated_at>.json with the milliseconds dropped and the colons replaced —
2026-09-16T06-00-00Z.json — so the directory sorts by clock and the name is
one the page serves by name; it is written whole (a rename over a temp file),
so neither the page nor the next run reads half a document. The path is
reported on stderr (bob brief: wrote …), never as a key: briefing/1 refuses
extra keys. The stored briefing is the next run’s window start. The schedule
that runs --send, the quiet hours and the retention are declared — see
Schedule and retention.
Schedule and retention (#2246)
Section titled “Schedule and retention (#2246)”When and where briefings are made is declared once, in this machine’s
machines.json entry (the operator repo), beside the farm tick’s schedule —
bob_workload.brief, validated by schemas/machines.schema.json and read by
scripts/brief/schedule.js. Never a hand-typed crontab:
"bob_workload": { "source": "~/projects/bigbrain", "bob_home_channel": "nightly", "tick": { "schedule": "daily 02:30 America/Los_Angeles" }, "brief": { "schedule": "daily 07:00 America/Los_Angeles", "quiet_hours": "22:00-07:00", "retention_days": 90 }}| Key | Meaning |
|---|---|
schedule |
daily HH:MM [Area/Zone] — the cadence the bob-brief timer fires on; no zone is the host’s local time. Required. |
quiet_hours |
HH:MM-HH:MM, half-open, may wrap midnight, read in the schedule’s zone. A scheduled briefing (--scheduled: the timer, the end of a tick) is neither made nor stored inside it — not stored either, so the next briefing’s window still covers everything since the last one sent. On demand (bob brief without --scheduled) it does not apply. |
retention_days |
After each write, stored briefings older than this are pruned — only briefing/1 documents past the cutoff, never a file that is not a briefing, never the newest (the next window starts from it). Absent in a brief block = 90; --retain-days wins. |
dir |
Where they are stored, ~- or $HOME-relative, never machine-absolute (#1924). Absent = $BOB_HOME/briefings. $BOB_BRIEFINGS overrides it; /briefings reads the same resolution. |
projects |
"all" — a scheduled briefing also writes every registered project’s brief (bob brief --project all --apply) after the portfolio briefing is made and delivered, on the same cadence and inside the same quiet hours (#3040). Its outcome goes to stderr and never changes the portfolio’s delivery or exit code; a quiet project is reported. Absent = portfolio only; bob brief --send --scheduled --project all asks for it on one run. |
Every reader fails open. A machine with no machines.json entry, no
bob_workload or no brief block is undeclared — no schedule, no quiet
hours, no retention — and bob brief works there on demand exactly as before:
the portfolio view comes from the forge and the fleet snapshots every machine
publishes (#415), so a laptop briefs without farm-01. A brief block that is
present but invalid is named on stderr; a --scheduled run then refuses
(exit 2) so a broken schedule is noticed, while an on-demand run still briefs.
The timer. bob brief --install-timer renders the declaration through the
shared unit renderer (templates/systemd/render.js) into two systemd user
units under ~/.config/systemd/user/ and enables the timer
(scripts/brief/brief-timer.js, Linux only, idempotent — a unit already on
disk is unchanged):
| Unit | From the declaration |
|---|---|
bob-brief.timer |
OnCalendar=*-*-* 07:00:00 America/Los_Angeles from schedule; Persistent=true, so a briefing missed while the machine was off fires on the next boot. |
bob-brief.service |
ExecStart=%h/.claude/bin/bob brief --send --scheduled — the deployed bob, in bob_workload.source; exit 3 (no bridge, the message printed) counts as success. The header comment names the quiet hours and retention it runs under. |
bob brief --install-timer --dry-run # the units the declaration renders — writes nothingbob brief --install-timer # install + enable (idempotent)bob brief --timer-status # declared vs installed: absent / current / differs, timer state, lingerbob brief --uninstall-timer # disable it and remove both unitsA machine that declares no brief block (or an invalid one) is refused with
exit 2 naming what is missing; macOS is refused — bob brief --send works on
demand there. Exit 1 is a failed systemctl step.
The end of a tick. bob tick makes a briefing after its digest through the
same path — bob brief --send --scheduled — so the declared quiet hours apply
to both. It is never a tick step: the outcome lands in the digest’s briefing
and a briefing that could not be sent never fails the tick
(Farm tick).
Proposing the next action (--propose, #2818)
Section titled “Proposing the next action (--propose, #2818)”A brief arrives with a recommendation rather than a blank page. For each
brief-labelled issue, bob brief --propose asks one question — of the next
actions this brief lists, which should be taken first, or is none of them
the right move? — and records the answer on the issue.
bob brief --propose # every open brief, report onlybob brief --propose --issue 2803 # one briefbob brief --propose --apply # record the answers on the issuesThree rules govern it, and each is a refusal rather than a feature:
It goes through the seam. The call is scripts/lib/decide.js (#2817) as
caller brief-next-action, which declares in decide-callers.json the six
state fields that may leave the machine — title, summary, priority, age, state
and the action texts. The page, the comments and the rest of the body stay here,
and the seam refuses the call outright if that ever stops being true. There is
no jev.evaluate() in scripts/brief/; the backend is configuration. See
the decide() seam.
It is fail-visible. brief-next-action declares fail: 'visible', so an
undecided answer is not silence. The block reads No proposal — no-key, in
exactly the place a proposal would have been, and the index renders it with the
reason. A judge that abstains quietly reads as a judge that approved.
It never ticks and never transitions. #2789’s model is that the human’s tick
is the state transition. Confidence is recorded as its route — act,
confirm or human — so the human can see how firmly the proposal is held, and
that is all it does. A machine that set brief:acting because it felt confident
would make its opinion indistinguishable from the human’s decision.
The proposal is one marker-delimited block in the issue body, replaced in place on every run: it sits directly above the tick list it is about, it is idempotent by construction, and the index projects it with no second forge call per brief. Its audit trail is the issue’s own edit history.
<!-- brief:proposal -->
## Proposed next action
**1. Back-fill the session ledger from transcripts before grading anything**
Confidence 0.84 → **act** (held firmly).
_Proposed by `jev` (jev-1.13.0) at 2026-09-21T02:00:00Z. A proposal never ticks abox and never moves the state label — the human's tick is the transition (#2818)._
<!-- /brief:proposal -->Exit codes: 0 ran — a proposal, a none, and an abstention are all outcomes
· 2 usage error · 3 the forge could not be read.
tests/test-brief-propose.js (60 assertions) runs with no key and no network:
the deterministic backend answers and gh is a stub. It pins that only the
declared state is built (the seam is the judge, refusing the same caller with
one extra field), that an action keeps the number of its line so ticking one
never renumbers the rest, that the block is idempotent and ticks nothing, that
an abstention is written rather than swallowed, and that nothing is written
without --apply.
Area pages (--area, #2820, #3101)
Section titled “Area pages (--area, #2820, #3101)”--area is BoB’s first generated producer: one page per area:* label, saying
what is open in the area, what is stuck, and what the next move is. The union of
those pages is the portfolio’s next moves.
An area page is a view, not a brief. It is a report, and a report belongs on a
page. On 2026-09-21 --area all filed one issue per area label: 48 of them, 84% of
the briefings pile, each restating the tracker’s counts and asking for no
decision. #3098 closed all 48. So --area now renders the page and nothing else,
unless the area carries a decision (below).
bob brief --area area:fleet # report only — nothing writtenbob brief --area all --apply # every area's page; a brief issue only where a decision isbob brief --area area:fleet --json # the area-brief/1 evidence packet alonebob brief --scheduled --area all # the same as --apply, inside the declared hoursThe terminal says one line and a URL per area, then a totals line:
area:briefings 12 open · page only — no decision — https://…/area-briefings.htmlarea:fleet 9 open · filed #3140 for 2 decisions — https://…/area-fleet.htmlarea:ci 4 open · area:ci no longer needs a decision — reconcile will close it (#3001) — https://…/area-ci.html3 areas · 0 empty · 3 pages · 1 filed · 0 updated · 1 no longer need a decisionDecision items
Section titled “Decision items”An area gets a brief issue only when its packet holds at least one decision
item: an item the tracker cannot resolve by itself. decisionItems() in
scripts/brief/area.js is the one place this is defined. An item is listed once,
under the first rule it matches:
| Kind | Fires when |
|---|---|
needs-clarification |
an open item carries needs-clarification |
blocked |
an open item carries blocked |
todo |
an open todo without completed |
untyped |
an open issue with no cap / req / bug / task / todo (or business-case / use-case) label, excluding the brief itself |
cap-no-approved-children |
a cap open longer than the staleness window (DECISION_STALE_DAYS, 30 days) with no approved req, bug or task in the area naming it Part of #N |
When an area files, its issue’s ## Next action list names exactly those
items, and each line starts with the item’s number (#14 is a human todo not yet completed — …), so bob brief --reconcile (#3100) can close the brief once they
resolve. The issue takes the highest priority among its decisions. The area page
lists the same items in its Decisions section. If there are none, the page
says that is why the area has no issue.
No re-filing. An area with an open brief issue gets that issue updated: the evidence block is rewritten and the decisions are merged in (see below). A second issue is never filed. A closed brief does not stand in: if an area needs a decision again after its brief closed, a new brief is filed.
A brief whose decision is gone is reported, never closed. When an area still
has an open brief issue but no decision item, the run writes nothing and prints
area:<slug> no longer needs a decision — reconcile will close it. Closing is
--reconcile’s job. This command never closes an issue.
Candidate next moves
Section titled “Candidate next moves”The collection is mechanical; the judgement is derived. Every number on an area page is a count of issues the forge returned, and every candidate next move names the evidence that put it there. Nothing is narrated. If a candidate is wrong, it is a rule you can point at, which is the only kind of wrong worth having on a planning surface. The candidates are on the page; they are not tick items. The rules, in the order they are considered:
| Rule | Fires when | The move |
|---|---|---|
| unreadable source | any gh call failed |
Re-run with a working forge — the counts are lower bounds |
| human todo | an open todo carries the label |
Clear it; nothing automated moves past a todo |
| hot bug | an open bug at p1/p2 |
Fix it |
| drain | req/bug/task + approved |
Drain them — what /warp-drive picks up |
| decompose | cap + approved |
Break it into requirements — warp-drive never picks up a capability |
| land | an open PR carries the label | Land it — finished work not yet counted |
| groom | req/bug/task without approved |
Groom it — invisible to /warp-drive until then |
| approve | cap without approved |
Approve or reject it — nothing beneath it can be groomed |
| triage | an open item with no work-type label | Give it one — no tool picks it up and no report counts it |
| stuck | open ≥ 90 days | Re-decide it: three months untouched is a decision never made |
| no rule matched | open work, nothing above fired | Say so — an empty list would read as “nothing to do” |
| empty | nothing open, nothing closed, no gap | Decide whether the area is finished or dormant |
The area’s priority is the highest priority of the work open in it. It is derived, never a default. An area with nothing open carries none.
An empty area is stated, never skipped. --area all prints each one, and it
gets no page and no issue.
Regenerating never destroys a human’s work. On an area brief issue, the
evidence block is rewritten in place and the ## Next action list is merged:
every existing line keeps its tick, new decisions are appended, and a line the
evidence no longer supports is reported rather than deleted out from under
whoever was about to act on it.
An area brief issue carries brief + brief:new + its area label + the priority
of its decisions, and one <!-- brief:area area:x --> marker so it can never be
matched by guessing at a title. Its page is area-<slug>.html, and #2818’s
proposer runs over it unchanged. In the index,
area pages are listed apart from the briefs, in their own Areas section: a
link per area with its generated date (the page’s <meta name="generated">
clock), and no state or priority. An area page never counts toward the open-brief
count; an area brief issue is a brief row like any other.
Exit codes: 0 collected · 2 usage error · 3 the forge could not be read.
tests/test-brief-area.js (67 assertions) runs against a stubbed gh: the packet
and its counts, an unreadable source recorded as a gap rather than a silent zero,
each candidate rule against evidence built to trigger it, the page’s purity and its
handling of a hostile title and a javascript: URL, the merge that keeps a tick
and reports what is unsupported, and report-only until --apply.
tests/test-brief-area-views.js (52 assertions) covers the view model: page-only
rendering for a no-decision area, filing for each decision-item kind with exactly
those items as the Next action list, no re-filing over an open area brief, a stale
brief reported and never closed, the index’s Areas section, and a scheduled area
pass against a fake gh on PATH.
A brief per project (--project, #3040)
Section titled “A brief per project (--project, #3040)”The portfolio briefing spans every project in one document; an area brief spans
one label in the BoB repo. A project brief is the per-project projection:
one brief per registered project, over that project’s own repo, on exactly the
area brief’s model — the same collection, candidate rules, page, merge and
brief-issue model, because it is the same generator (scripts/brief/area.js)
over a project scope (scripts/brief/project.js).
bob brief --project flows # report onlybob brief --project all # every registered projectbob brief --project all --apply # file or update each project's brief issuebob brief --project flows --json # the project-brief/1 run documentWhich projects. The registered projects on this machine — projects.json,
each resolved to its GitHub origin. Names match case-insensitively; an unknown
name exits 2 and lists every known one. A project with no GitHub origin is
reported as having nothing to brief from, never guessed at.
What is read. Every gh call names the project’s repo (--repo owner/name).
Open and closed issues are kept only when they are work — cap, req, bug,
task, todo, business-case, use-case — because a whole repo’s untyped
issues are its chunk reports, session summaries and briefs, not its work. On top
of the area sources a project brief reads its open flight plans, its
releases in the window and this machine’s warp run on it; an unreadable
source is a named gap, a project not checked out here says not measured, and
a run at blocked or budget_exceeded becomes the first candidate move.
Where it lives. The brief issue is filed in the project’s repo: brief +
brief:new + the derived priority, no area label (the repo is its scope),
titled Project: <name>, claiming its project with
<!-- brief:project <name> --> and its page with
<!-- brief:page project-<name>.html -->. The page is written beside the other
briefs as www/bob/briefings/project-<name>.html — flat, like area-<slug>.html,
so the index lists it and --publish carries it without a second path. Each
project repo needs the brief and brief-state labels; scripts/bootstrap-labels.sh
already provisions them, so bob init / bob ready do.
A quiet project is reported, never skipped silently. A project with nothing opened, closed, released or in review in the window is printed as quiet and no brief is written — the same rule as an empty area, with “nothing new” as the test because a project always has open work.
On the schedule. bob brief --send --scheduled --project all, or a declared
bob_workload.brief.projects: "all" (see Schedule and
retention), runs the project briefs after the
portfolio briefing is delivered: the portfolio’s stdout, delivery and exit code
are exactly what they were, and the project report goes to stderr.
In the index. bob brief --index gives every row a Project: portfolio,
area:<slug> or the project’s name — read from the issue’s brief:area /
brief:project marker, or, for a page with no issue, from its generated name
(area-<slug>.html, project-<name>.html). The column sorts client-side like
Date, Pri and State, and a Project filter above the table (shown when more
than one project has a brief) narrows it to one project’s briefs, counting what
it shows; the choice rides in the fragment (#project=flows) so a link carries
it. tests/test-brief-index.js runs the page’s own script against a stub DOM
for both.
In bob viz. The Briefings pane lists project briefs beside portfolio
briefings and area briefs, each with its repo#number, state, needs-you count
(the brief’s todos) and links; it sorts by project, and the selected project’s
own brief lists first — see bob viz → Briefings.
Exit codes: 0 ran · 2 unknown project or no projects.json · 3 the
forge could not be read.
tests/test-brief-project.js runs against a stubbed gh: resolution and the
unknown-name list, --repo on every read, reports excluded from the counts, the
plans / releases / warp sources and their gaps, the issue filed in the project
repo with the right labels and markers, an existing brief found and merged
without losing a tick, quiet and origin-less projects reported, and the
projects declaration.
The index is portfolio-wide (#3102)
Section titled “The index is portfolio-wide (#3102)”bob brief --index reads the brief issues of every registered, active
project that has a GitHub origin, not only the repo it runs in. Each brief
stays in its own project’s repo, and the index reads it there.
Which projects. The registry that bob brief --project uses (#3040):
projects.json in the operator repo (#3432), derived from the operator’s manifests by
scripts/fleet/projects-derive.js (#2066) and read through
registeredProjects() in scripts/brief/brief.js, never from a list in the
code. A project marked active: false is skipped. The slug is the checkout’s
origin, and two registrations of one repo are read once. An active project
that is not checked out on this machine, or has no GitHub origin, cannot be
read. It is named in a note under the table, not dropped.
How it reads. One gh issue list --repo <slug> --label brief --state all
per project. Six run at a time, each with the gh timeout (20 s), so the whole
registry (about 40 repos) reads in a few seconds, well inside a bob tick step.
A repo with no brief issues answers [] and contributes nothing. Without a
projects.json, the index falls back to reading the current repo alone.
Which project a row belongs to. A brief’s brief:area / brief:project
marker wins. A brief without one belongs to the project whose repo it was read
from, except in the repo the index is generated in: its unmarked briefs are
portfolio. A brief from another repo shows the repo in its Issue link
(bodmail#592), since #592 alone is ambiguous.
Area pages are views (#3101). Every area-<slug>.html is listed in its own
Areas section below the briefs: a link per area with its generated date, and
no state or priority. An area page no issue claims is not a brief row, so it
never counts as open and never as a page with no issue. --index --json reports
them as areas. An area brief issue, filed for a decision, stays a brief row.
Open first. Open briefs are those whose state is not marked terminal in
scripts/brief/states.json (brief:new, brief:acting, or a page with no issue
yet). They form the main table. Done and parked briefs are folded into one
collapsed group headed with their count (55 closed briefs). Each table sorts
on its own, so a sort cannot pull a closed brief into the open list. The Project
filter narrows both tables and updates the fold’s count, but it never opens the
fold: only the reader does.
Gaps are rows. A project whose issues could not be read is a row at the top
of the open table, in the form example-awards: not read — forge <state>. The state
is the classified gh outcome: unavailable, timeout, rate-limited,
unauthenticated, gh-missing or invalid. The byline and a stat tile report
N of M projects read. --json carries the same read as
projects: { total, read, gaps[], no_origin[] }, and each gap is also printed
to stderr.
tests/test-brief-index-portfolio.js covers this against a stubbed gh: the
multi-repo read with --repo on every call, the project label on each row,
open-first ordering with the closed fold and its count, the gap row for an
unreadable repo, the empty repo, the inactive and origin-less projects, and
--index end to end over a registry fixture.
Publishing the pages (--publish, #2872)
Section titled “Publishing the pages (--publish, #2872)”The brief pages have two homes. They are generated into www/bob/briefings/
in the operator repo (#4263), found through operatorPath('brief-pages') in
scripts/lib/operator-resolve.js like every other piece of operator data, by
--html, --index, --area, --project and --completions, whatever
directory the command runs from (#3154, #3278). --publish publishes that same
directory. The pages name the operator’s projects and clients, so the BoB source
checkout is never their home: run from any project, BoB’s own included, none of
them writes a www/ into it. With no operator repo resolved, nothing is
written: --html, --index and --publish exit 2 naming the fix
(bob operator init <dir> --link), and --area, --project, --completions
and --reconcile write no page and say so. --html-dir <dir> still names any
other directory explicitly.
No dead links (#3568). The index is built from the forge and the bundle
from one machine’s working tree, so the two can disagree. A brief issue whose
brief:page names a file that is not in the directory is still listed, with
its title unlinked, a page missing tag and a stat tile that counts it.
--publish checks every relative link in index.html against the bundle and
names each missing one (missing: <page>). A report-only run exits 3;
--apply publishes nothing and exits 3. They are served from the
operator’s site. bob brief --publish is a producer for the one site
publisher (#3103): it builds a site-publish/1 bundle and hands it to
bob site publish. The bundle holds the generated pages (top level, dotfiles
excluded), the brief section, the producer bob brief and this checkout’s
commit. The publisher owns the target, the Access gate on every served host
(#2895), the Cloudflare account (#2984), the commit, the push and the deploy.
The whole contract is in the Site Publish reference.
The brief section is the one the operator’s config/site-publish.json
declares with "producer": "bob brief", else its only section, else
bob/briefings. For this operator it is paulirv/iwpi-static →
public/bob/briefings, behind Access. Until 2026-12-31 the older
config/brief-publish.json is read as a fallback and mapped onto that one
section, with a warning.
bob brief --publish reports without writing. --apply publishes and
--apply --deploy redeploys an unchanged set. The document is the site
publisher’s site-publish/1, and so are the exit codes: 0 reported /
published / unchanged · 2 usage · 3 undeclared, invalid, refused or a
git failure · 4 the deploy failed after the push.
--html, --index and --area end their one-line output with
published (<repo>@<branch>) or generated, not published — <why>. The check is
offline: it compares against the checkout’s last fetch.
Reconcile (--reconcile, #3100)
Section titled “Reconcile (--reconcile, #3100)”Nothing used to close a brief when its work was done. When #3098 groomed the
pile, six of its seven narrative briefs were already resolved, but nobody had
moved their labels. A brief’s next actions name issues, so whether an action is
resolved can be read from the tracker. bob brief --reconcile does that for
every open brief issue across the portfolio, reading each project’s repo the
way the index does.
Turning it off (#3978). _workflow.brief_reconcile: false turns off
bob brief --reconcile and bob tick step 9b for the project. Each then
reports verdict off and reads nothing. Reconcile and the completions run as
background callers of the forge, so at the gh shim’s
per-minute ceiling they wait and
interactive work goes first.
What counts as resolved. Reconcile reads the ## Next action checklist of
each open brief. An unticked item that names one or more issues is resolved
when every issue it names is closed. A merged or closed PR counts as
closed. Three forms are recognised:
#NN, an issue in the brief’s own repoowner/repo#NN, an issue anywhere- a
github.com/<owner>/<repo>/issues/NNor/pull/NNURL
A todo it names counts only when it carries completed, which is the
TODO lifecycle rule. A todo closed without completed was
cancelled, not done. An item that names no issue is never ticked; it is
reported as needs judgment. A checkbox under any other heading is not an
action. Every named issue is resolved in batched gh api graphql calls. An
issue the forge does not know is not found and never resolved.
Report, then apply. Without --apply it prints one line per open brief and
changes nothing:
#3098: 3/5 actions resolved → would move brief:new → brief:acting; 2 waiting on open issuesexample-co/example-cms#1428: 0/6 actions resolved → 6 waiting on open issues5 open briefs; 1 would change — report only, pass --apply to write--apply does four things:
- Ticks. It re-reads the body, ticks each resolved line and writes the
body back with
--body-file, never with an inline sed. A line is ticked only if it is still[ ]and its text is unchanged, so an edit made since the read is never mis-ticked. - Comments. It adds one comment per brief naming each tick and the closed issue that justified it.
- Moves the state. Every move goes through the transition table in
scripts/brief/states.json, so there is no second definition of the states:- When every action is resolved (ticked by hand or now), the brief moves to
brief:doneand is closed with a comment giving the evidence. An open brief already labelledbrief:doneis only closed. - When some actions are resolved, a
brief:newbrief moves tobrief:acting. - A move the table does not allow is reported (
stays brief:parked (states.json has no brief:parked → brief:done)) and not made. Reconcile never moves a terminal brief (parked, done) back to acting. A parked brief still gets its ticks. - An unlabelled brief is read as
brief:new. A brief with no next actions never moves. - If a tick fails to land, the brief is not moved or closed.
- When every action is resolved (ticked by hand or now), the brief moves to
- Rebuilds and publishes the index. After any
--applythat changed a brief, the index is regenerated (the samerebuildIndexas--index) and published through the declared publish path. When nothing changed, nothing is published, and it printsindex: nothing changed — index not regenerated, nothing published. The index is written towww/bob/briefings/in the operator repo (#4263) whatever directory the command runs from, because it lists every project’s briefs (#3154);--indexresolves it the same way. With no operator repo and no--html-dir, it regenerates and publishes nothing and says so.
A dead forge changes nothing. If the brief lists cannot be read, or the
issues they name cannot be resolved, it prints
reconcile: skipped (forge <state>), exits 3 and writes nothing, because a half-read tracker must
never close a brief. A single project repo that cannot be read is named in the
closing line and its briefs are left alone.
--json emits brief-reconcile/1:
verdict:okorskippedapplied,forge,changedprojects:{total, read, gaps}briefs[], one per open brief:issue,repo,ref,state,total,resolved,would_tick,needs_judgmentactions[], each with itsrefs[](ref,state,todo,completed,resolved,why) andresolution(ticked·resolved·open·needs-judgment)transition:{from, to, close, allowed, reason}or nullapplied:{ticked, commented, labelled, closed}error
index:{regenerated, file, published, note}under--applyerror:{code, state, message}when skipped
Who calls it. /groom no longer runs it (#4395): grooming never touches
briefs.
bob tick runs bob brief --reconcile --apply --json once per cycle, before
the briefing. The result lands in the digest as brief_reconcile and as a
“Briefs reconciled” row. It is never a pipeline step: a skip (exit 3) never
fails or strikes a tick. A dry-run tick runs it report-only.
tests/test-brief-reconcile.js covers the parser (single and multiple refs,
cross-repo refs and URLs, a todo with and without completed, an unreferenced
item), the report and apply paths against a gh stub, the all-resolved close,
the parked brief the table will not move, a failed tick blocking the close, the
index being rebuilt and published only after a change, and the forge-down exit 3.
Area-completion briefings (#3169)
Section titled “Area-completion briefings (#3169)”An area page is a view of an area at any moment. An area-completion briefing is
written once, when the area’s work runs out. It answers one question from one
page: is this ready to ship to the laptops or to clients? Nobody has to ask for
it. scripts/brief/completion.js detects and collects;
scripts/brief/completion-render.js writes the narrative, the page and the
issue body.
One run reads the merged PRs and the CI runs once per project, however many
areas it briefs: the PR list from the earliest area’s start date, filtered per
area (#3978). _workflow.area_completion_briefs: false turns the completions
off, whether they come from a warp run, bob brief --completions or
bob brief --reconcile.
When an area is complete. Decided in one place, detect():
- no open
req,bugortaskin the area carriesapproved; and - at least one
req,bugortaskin it closed after its last completion briefing. An area that never had one counts only closures in the last 30 days, so the first run does not brief every area that finished long ago.
proposed, deferred and unapproved items never hold an area open; the
briefing lists them as what is left. A closure labelled wontfix,
duplicate, invalid or not-planned is listed as dropped, not shipped. A
todo is neither work nor remainder.
One briefing per completion. The issue claims its completion with
<!-- brief:area-complete area:<slug> v<N> through=<ISO> -->, where through
is the newest closure it covers. The next detection counts only closures after
it, so re-running files nothing. An area that reopens and drains again gets
v<N+1>. The marker is re-read just before the write, so two runs racing each
other (a tick and a warp boundary) still file one. When the brief issues, the
open issues or the closed issues of a project cannot be read, no area in it is
decided.
Where detection runs.
| Trigger | What runs |
|---|---|
A warp run’s item boundary (promoted / promote_skipped) and its entry to session_ending |
bob brief --completions --project . --apply, started detached by the engine; the run never waits for it. Its output is .claude/.area-completions.log. _workflow.area_completion_briefs: false turns it off (Workflow configuration); BOB_AREA_COMPLETIONS=0 turns it off for one process. |
bob tick |
Through bob brief --reconcile --apply, across every registered project. The tick digest’s briefs line reports how many were filed. |
bob brief --reconcile |
The same detection; the outcome is completions on the brief-reconcile/1 document. It never changes reconcile’s verdict or exit. |
bob brief --completions |
On demand; report only without --apply. --project <name>|.|all (default all), --area area:<slug>. |
Every project. Detection reads each registered project (projects.json)
with a GitHub origin: three gh reads per project whatever the number of
areas, plus, for a complete area, its merged PRs and default-branch CI runs,
for a project with a held area its open flight plans (one read),
and git log / git tag in the project’s checkout when this machine has one.
The issue is filed in the project’s own repo, title
Area complete: <slug> (v<N>) — <project>, with the labels its verdict earns
(News or an ask). It
carries the brief:page marker and never the brief:area or brief:project
view markers, so the area and project generators never adopt it. The page is
www/bob/briefings/project-<name>-area-<slug>.html in the operator repo (#4263).
Content. Built from evidence, in this order on the page:
| Section | From |
|---|---|
| Release readiness | the first nightly and stable tags that contain the newest commit behind the area; a ship / hold verdict by stated rules; upgrade steps from the changed surfaces |
| What shipped | each closed item with the merged PR whose text says Fixes #N, the commits whose subject names #N, and its acceptance criteria ticked out of total |
| What changed | for the operator and for end users (narrative), and the changed files by surface: commands, hooks, schema, config, migrations, skills, docs, tests, scripts |
| Tests and CI | completed runs on the default branch inside the area’s window; a workflow is red only when its newest run failed; quarantined bugs |
| Risks, gaps and rollback | narrative over the evidence, including every source that was not read |
| What is left | open items in the area that are not approved, with why (proposed, deferred, follow-up bug) |
| Decisions needed | for a HOLD: the release decision first, then any red run and each item left — the issue’s tickable ## Next action list. A SHIP has none |
The verdict is hold when CI is red on the default branch, when the work is
not on a stable tag (or no commit names it, so no tag can be shown), or when an
open bug is left in the area; otherwise ship. Upgrade steps come from a
table in upgradeSteps(): for BoB, bob update, a session restart when hooks
changed, bob ready when templates or manifests changed, and make check in
the operator repo when a schema changed; for any other project, its promotion
path; for either, a migration step after a backup when a migration shipped.
News or an ask (#3804)
Section titled “News or an ask (#3804)”The verdict decides what the briefing is.
| Verdict | Filed with | What it is |
|---|---|---|
| SHIP | brief, brief:done, the area. No priority. |
News. It is listed under Done in the portfolio briefing for the window it was filed in (kind area-shipped). bob asks does not list it, it has no ## Next action tick list, and no bob notify line is sent for it. |
| HOLD | brief, brief:new, the area, and a priority |
An ask. The first line of the issue body is **Held: <what is in the way>.**, and the portfolio briefing lists it under Needs you as Area <slug> is held: <what is in the way>. |
A HOLD’s priority is p1-critical when an open flight-plan issue in the
project lists an item (ticked or not) that carries the area’s label: a plan is
waiting on the held area. Otherwise it is p3-medium. The packet records the
answer as release.priority and the plans as release.waiting_plans. When the
flight plans cannot be read, the priority is p3-medium and the read is a
named gap.
Briefings filed before this rule. Every run of bob brief --completions
also looks for open SHIP briefings still filed as asks: open, carrying a
completion marker and **Release: SHIP**, and labelled brief:new or with no
state label. With --apply each one is moved to brief:done and loses its
priority label, with one gh issue edit. Each is reported: a line
<project> <area> v<N>: SHIP already filed (#N) — re-labelled brief:new + p2-high → brief:done
(would re-label without --apply), a relabelled row per project on the
area-completion-run/1 document, and counts.relabelled / counts.to_relabel.
A HOLD, and a briefing a human moved to brief:acting or brief:parked, is
left alone. A second run changes nothing.
The narrative. A model writes it over the evidence packet and nothing else:
headless claude -p with no tools, handed the packet, the
writing styleguide rules and the verdict it must not
contradict. It answers one JSON object; an answer that leaves out a section, or
whose shipped prose links no item, is refused. BOB_BRIEF_NARRATOR names a
different command, and BOB_BRIEF_NARRATE=0 turns it off. A narrator that
cannot run is a named narrator gap: the page says No narrative where the
prose would be, and still carries every piece of evidence.
Delivery. After filing, the run rebuilds the index and publishes the pages
once through bob site publish, whose
Access gate decides; a refusal is recorded on the result, never thrown. Each
HOLD is one bob notify line that names what is in the way, with its page
link (or the issue link when the page did not publish). A briefing appears on
the /briefings index (it is a brief issue claiming a page) and on the bob
viz Briefings pane (kind completion, with the verdict and brief state as its
status). A HOLD is under Needs you in the next portfolio briefing until
its state is brief:done or brief:parked; a SHIP is under Done.
Unattended. Nothing here prompts. A source it cannot read is a named gap and the briefing is still produced; a failure to file or publish is recorded per completion. From a warp run it is detached, so it never blocks the run.
Documents. completion.js --json prints area-completion-scan/1 (each
project’s areas with their state, plus an area-completion/1 evidence packet
per new completion); --html-dir DIR previews each page without filing.
bob brief --completions --json prints area-completion-run/1: per project,
what each completion did (filed, issue, url, verdict, narrated,
gaps, error), plus published, index and notified.
The brief vocabulary (#2790)
Section titled “The brief vocabulary (#2790)”A brief is one GitHub Issue with the brief label, exactly one state label
(brief:new, brief:acting, brief:done or brief:parked) and exactly one
priority label (p1-critical..p4-low). scripts/brief/states.json declares
that vocabulary and its transition table once, and scripts/bootstrap-labels.sh
copies it into every repo. make check-brief-labels
(scripts/checks/check-brief-labels.js) checks three things:
- The copy. A label that is missing from the bootstrap script, an extra one, a colour or description that differs, a transition to an undeclared state, or a vocabulary with no terminal state.
- The phases. A brief is an ask, so each state names the
phaseof the ask lifecycle it is:brief:newisqueued,brief:actingispresented,brief:doneisdoneandbrief:parkedisdeclined. A state with no phase, or onescripts/asks/machine.jsondoesn’t declare, isno-phase. A brief transition whose two phases the machine joins by no event isunmapped-transition. - The issues. Every
briefissue must carry exactly one state and one priority. Zero or two of either isstate-count/priority-countdrift, and abrief:*label the vocabulary doesn’t declare isundeclared-state.
The issue check reads the tracker, not the checkout. With $CI set, no gh, or
a forge that can’t be read, it is reported as skipped with the reason and never
fails the run. --issues-file <json> reads a recorded issue list instead, and
--no-issues turns the check off. --states-file <json> checks another
vocabulary file. Exit codes: 0 clean · 1 drift ·
2 usage error.
Determinism and tests
Section titled “Determinism and tests”buildBriefing(inputs) in scripts/brief/brief.js is pure — sources and a
clock in, briefing/1 out — and collect(opts) does the reading with every
reader injectable (gh, subprocess, projects, registry, machine).
make test-brief runs tests/test-brief.js: the schema both ways; every
source landing in its section linked; the window exclusions; the order under
shuffled source rows; the CLI under a temp BOB_HOME (offline gaps, the three
window bases, exit 2 on usage); one named gap per failed source with the other
sections intact; and the collector on recorded fixtures under
tests/fixtures/brief/ — the six gh search answers and the releases GraphQL
answer as gh printed them, a fleet snapshot, and the rituals-due and
rotation-due documents as the engines printed them — validated against the
schema and identical on a second run. A briefing runs to hundreds of KB, so the
CLI sets process.exitCode and lets stdout drain rather than exiting after the
write (which truncates a piped document); the test pins that by source. The
Telegram message (#2245) is proved on fixture documents — the committed sample
composes to its five exact lines, the counts are the document’s, a long title
is clipped, nothing-needs-you drops the Top lines, a quiet document is one line
or nothing under skip, an unresolvable link is its reason — and --send under
a temp BOB_HOME with a planted blocked-run snapshot: no bridge exits 3 with
the message printed, a curl stub on PATH captures exactly the composed text
at severity warning with the secret never printed, quiet sends one line at
info or nothing under --on-quiet skip / BOB_BRIEF_ON_QUIET=skip with the
briefing still stored, and a bad --url or --on-quiet is exit 2. No test
sends a real message.
make test-brief-completion runs tests/test-brief-completion.js (#3169)
end to end against a scratch project with tags and a fake gh and narrator
on PATH: every detection state (open, complete, briefed, reopened as v2,
quiet before the baseline, a proposed-only remainder), a source that cannot be
read, the evidence packet, every page section, the narrator’s gap and quality
bar, filing and idempotence through bob brief --completions --apply, a second
project in its own repo, --reconcile, the warp engine’s detached start, the
Briefings pane and the portfolio item.
make test-briefings-page runs tests/test-briefings-page.js (#2244): the
loader’s order by clock, the inline latest, the named unreadable files, the
by-name read that refuses a path; the renderer against the committed sample
with a committed snapshot (tests/fixtures/bob-viz/briefings.html); the page
stylesheet against the design checklist at phone width and in every theme;
the tailnet posture (one listener, no address or port in any new file); and a
real dashboard server serving every route.
Related
Section titled “Related”- Get Your Briefing — the task, step by step: read, store, send
- bob viz — the console the page belongs to; its regions table carries the page’s row.
- Project Monitor Exposure Posture — why the page is tailnet-only.
- Fleet snapshot timer — where the
snapshotssource comes from. - Rituals (#2222) and Secrets inventory (#2223) — the two due engines.
- Farm tick (#2054) — the tick digests.
- The decide() seam (#2817) — the contract
--proposeasks its question through. - Forge health — the gh shim and its cache.
- bob notify (#2048) — the one sender
--senddelivers through, and its exit codes. - Farm tick — the
bob_workloaddeclaration the briefing’s schedule sits beside, and the tick that briefs at its end (#2246).