Skip to content

Rituals

What a ritual is, the fixed vocabulary, where a project’s schedule is declared, and how the declaration is checked (#2222, #2230). (Back to reference index.)

A ritual is a recurring, attended activity held on a declared cadence — a comprehensive review every quarter, a dependency audit every month, a security review before each stable release. Declaring the schedule makes it version-controlled instead of remembered; the farm can gather a ritual’s evidence ahead of the session, and the session applies the ritual’s kit.

Per the Prime Directive each fact has one owner: the vocabulary and its defaults live in one file, a project’s schedule lives in its manifest, and run history is never declared — it is derived from ritual-run issues (#2231).

What Path
Vocabulary (names, drill kinds, cadence grammar) schemas/rituals.schema.json → definitions
Each ritual’s kit, farm-eligibility and default cadences registry/rituals.json (BOB_SOURCE), synced to ~/.claude/registry/rituals.json by deploy.sh
A project’s schedule provisions/<project>.json → _meta.rituals
BoB’s own drills (restore, lost-laptop, farm-compromise) and their runbooks registry/rituals.json → the drill entry’s drills; registry/runbooks/drill-*.md (Drills, #2240)
A ritual’s checklist — the security review’s attack-surface list, the docs review’s list provisions/<project>.json → the declaration’s checklist; registry/runbooks/bob-attack-surface-review.md, registry/runbooks/docs-review-checklist.md (Checklists, #2241, #2625)
Gate scripts/checks/check-rituals.js, run by make check-rituals (part of make check)

Why the manifest and not a sibling file. The provision manifest is already the single per-project declaration (stack, phase, group, active), there is one per managed project, and every fleet reader already walks provisions/*.json. A sibling provisions/<project>.rituals.json would be mis-validated as a manifest by check-schemas and split one project’s declaration across two files. Recorded as decision #2368.

The names are fixed — adding a ritual is a schema change.

Ritual Kit Evidence farm-eligible Evidence collectors Default (active) Default (dormant)
comprehensive-review review yes the eight general ones (not the docs-review five) P3M —
vision-planning vision-planning yes status-report, aged-issues, usage-gaps, cost-churn P6M —
retrospective incident yes status-report, cost-churn, aged-issues, memory-audit P3M —
dependency-audit security yes dependency-audit P1M P6M
security-review security yes dependency-audit, fleet-readiness P6M —
docs-review review yes doc-staleness, doc-coverage, doc-changes, docs-tutorial, docs-site-live, doc-audit P1M P3M
portfolio-review vision-planning yes portfolio P3M on bigbrain, for every project — see The portfolio review —
drill incident no — — (a project declares its own; BoB’s three drills are held on bigbrain at P3M — see Drills) —
guide-walkthrough review no — — (owed by a guide’s requirement, never by a clock — see Guide walkthroughs) —

“Farm-eligible” means the ritual’s evidence step may be gathered unattended by bob tick (#2233); the session itself is always attended. The collectors come from a fixed set — see Evidence packets. A drill carries a drill_kind — restore, failover, credential-rotation or incident-response — and no other ritual may. A project’s own drill is not scheduled by default; the project declares it. BoB’s own recovery is rehearsed by three drills declared at the BoB level on the drill entry’s drills list and held on bigbrain quarterly (Drills, #2240). portfolio-review is never declared by a project: it is held once for the whole portfolio (#2235). The dependency-audit entry also carries an advisories block — the severity at or above which its audit files advisories as security issues on the owning repo (file_at: high; see Advisories, #2242).

registry/rituals.json also declares the liveness window that separates active from dormant: liveness.active_within (P3M).

Per-project reviews don’t answer “which projects deserve attention this quarter”. portfolio-review does, once, across every managed project (#2235). It is declared at the BoB level — a portfolio block on its vocabulary entry — never in a project manifest:

{
"name": "portfolio-review",
"kit": "vision-planning",
"farm_eligible": true,
"evidence": ["portfolio"],
"portfolio": { "home": "bigbrain", "cadence": "P3M", "why": "…" },
"defaults": { "active": null, "dormant": null }
}
Field Rule
portfolio.home required; the project (_meta.project) whose repo holds the runs — bigbrain
portfolio.cadence required; a cadence
portfolio.why optional one line
defaults both null — the ritual is not scheduled per project by liveness

What the block changes:

  • Scheduling. The ritual is on the home project’s schedule at portfolio.cadence, whatever the home declares in _meta.rituals or its liveness, and on no other project’s. ritual.js due marks it scope: "portfolio"; every other ritual is scope: "project".
  • Filing. ritual.js open, bob ritual run and prepare --due file the run on the home project, in its repo, with no --project. --project naming any other project is a config error.
  • Declaring. A project that lists it in _meta.rituals fails check-rituals (portfolio-declared).
  • Evidence. Its one collector, portfolio, reads every managed project at once.

One row per project manifest in provisions/ (the _-prefixed templates and global manifests excepted). An archived project (_meta.active: false) is listed, never measured. For every other project:

Field Read from
liveness ritual.js’s liveness reader — the fleet snapshots, else the checkout here (Defaults by liveness)
readiness the newest fleet snapshot row’s grades, else readiness.js on the checkout here (source says which)
rituals one ritual.js due --all report: overdue, due, unknown counts and overdue_keys
issues two gh issue list reads per repo — open p1-critical and p2-high
cost_churn one series.js --json read: usd_window (spend over the series window), usd_per_merged_req, fix_to_feat, tokens_window
harness_defects the newest snapshot row, else the series’ harness_defect_recurrences

The repo is the snapshot row’s repo, else the checkout’s github.com origin. A field that cannot be measured for one project is null and named in that row’s unmeasured[]. That is not a gap, since most projects are not checked out on any one machine. A source that fails for the whole portfolio (due --all, series.js) is a named gap, and every other field is still gathered. No project manifest at all is a gap. The newest snapshot row counts across machines; a row whose checkout is absent on its machine (exists: false) is skipped.

The packet gets a portfolio section before the raw data. It holds one table row per project, active first, then unknown, dormant and archived:

Project Liveness Readiness (dev / test / deploy) Rituals overdue Open p1 / p2 Cost & churn Harness defects
widget active (2026-09-01) working / working / partial 1: dependency-audit 2 / 1 $12.50 spent · $2.25/merged req · fix:feat 0.4 1 open, 1 recurrence(s)

Each project’s status is declared in its manifest as _meta.portfolio:

Status Means Cadence it declares
invest give it attention this period its own _meta.rituals, or the liveness defaults
maintain keep it healthy, nothing more its own _meta.rituals, or the liveness defaults
park stop scheduling work; keep it patched exactly the dormant defaults (a dependency-audit every P6M, a docs-review every P3M)
archive no longer managed the dormant defaults, with _meta.active: false

An absent _meta.portfolio reads as maintain, or as archive when _meta.active is false. check-rituals holds the status and the cadence together, so a status change and its cadence land in the same PR (portfolio-status):

  • a park or archive project must declare the dormant defaults. A schedule written before a ritual joined the vocabulary may lack that ritual’s entry and still passes, so a new ritual never fails every parked manifest at once; bob ritual decide writes the full defaults the next time the project is decided (#2625);
  • an archive project must be active: false;
  • an inactive project cannot declare any other status.

A portfolio run’s body ends with a Portfolio decisions section, after the delta. It has one row per project manifest, including archived ones, and each row records the project’s status when the run was opened:

## Portfolio decisions
| Project | Status now | Decision | Journal |
|---|---|---|---|
| bigbrain | maintain | invest | — |
| oldsite | maintain | park | #2400 |

In the session, set each Decision to invest, maintain, park or archive. A decision that differs from Status now changes the project’s status, and it needs two more things before the run closes:

  1. a /journal decision issue (labelled decision), linked in that row’s Journal cell;
  2. the manifest change, written with bob ritual decide and merged with the run.

bob ritual close refuses a portfolio run (portfolio-decision) while any of these hold:

  • a managed project has no row, or a row names no project;
  • a row has no decision;
  • a changed row links no decision issue;
  • the manifest does not yet declare the new status, or declares it without the cadence check-rituals requires.

Decision issues linked in the table are journal entries, so they are checked like --journal links and listed under the run’s Journal section. The closed marker’s closed_with gains decisions: {total, changed}.

Terminal window
bob ritual decide oldsite park # report what would change; writes nothing
bob ritual decide oldsite park --apply # write provisions/oldsite.json
bob ritual decide oldsite archive --apply # …regenerate projects.json and archive the GitHub repo
Decision Writes
park _meta.portfolio: "park"; _meta.rituals set to the dormant defaults, replacing any other declared schedule (named in rituals_replaced); drops active: false
archive as park, plus _meta.active: false; the project’s GitHub repo is archived
invest, maintain _meta.portfolio; a _meta.rituals that is exactly the dormant defaults is removed, so the liveness defaults apply again; drops active: false

An archive is carried out, not only recorded (#3609). With --apply, archive archives the project’s GitHub repo. The repo is never deleted, and gh repo unarchive reverses it. Deciding an archived project anything else un-archives the repo. The repo is --repo, else the manifest’s _meta.remote, else the github.com origin of the project’s checkout on this machine. Running archive --apply again on a project whose manifest already says so finishes an archive whose repo is still live, and changes nothing once it is archived. A checkout with no github.com origin has nothing to archive. A project with no checkout here and no remote named is exit 5, never a silent skip.

An archived project keeps active: false in projects.json, so rituals, the tick, mirrors and the CI rollout skip it. A checkout of it that is still on a machine is carried in that machine’s fleet snapshot as summary.archived ({checkouts, projects: [{name, path, dirty}]}), and audit.js view lists it under Archived projects with the machines that hold it.

Only the manifest’s _meta is rewritten. It is written in the style that reproduces the original _meta exactly: JSON.stringify indentation, one object per line inside arrays, or scalar arrays inline. Every other byte of the file is kept, and the result is checked to parse back to the intended manifest before anything is written. When active changes, projects.json is regenerated (projects-derive.js --write), since it carries active. The manifest is the one the operator resolver finds, in the operator repo since #2270 (#3458). --apply refuses when the repo holding it is not a git checkout, because the change must be committed there.

Flag Meaning
<project> required; by _meta.project or manifest file name
<decision> required; invest, maintain, park or archive
--apply write, and archive or un-archive the GitHub repo; without it, report what would change
--repo owner/name the GitHub repo, for a project with no checkout on this machine
--keep-repo record the decision and leave GitHub alone (a repo someone else owns)
--json emit bob-ritual-decide/1
--root DIR read and write another checkout’s provisions/

Exit codes: 0 applied, unchanged, or a report · 2 usage or config error (unknown project or decision, the manifest’s repo is not a git checkout) · 4 the manifest was written but projects.json could not be regenerated (its output is carried) · 5 the manifest was written but the GitHub repo was not archived (its error is carried).

bob-ritual-decide/1: verdict (applied | dry-run | unchanged), project, file, from and to (statuses), meta_before, meta_after, rituals_replaced (the schedule a park or archive replaced, or null), rituals_removed, active_changed, style (expanded | rows | compact), projects_json (regenerated, exit, output; null unless active changed on --apply), repo_archive (repo, action archive or unarchive, verdict, error; null when the decision touches no repo) and error. The repo_archive verdicts are archived, unarchived, already, no-repo (the checkout has no github.com origin), unknown (no checkout here and no repo named), failed and dry-run.

The portfolio review decides the projects that have a manifest. The directories on disk and the manifests drift apart: a checkout with no manifest, a manifest whose path is on no machine, a backup beside its original. The sweep (#3609) lists every project directory on the fleet and records one decision for each.

Terminal window
bob ritual sweep inventory # scan the fleet and print the list; writes nothing
bob ritual sweep inventory --write # save it as portfolio.json in the operator repo
bob ritual sweep decide Sites/old-thing scrap --with Sites/old-thing-bak
bob ritual sweep status # the rows still undecided
bob ritual sweep apply # what carrying the decisions out would do
bob ritual sweep apply --apply # write the manifests, archive the repos
bob ritual sweep scrap # the deletion list, per machine; deletes nothing
bob ritual sweep scrap --approve <digest>
bob ritual sweep scrap --apply # delete what this machine removes
bob ritual sweep audit --write # the dated audit, in the operator repo
bob ritual sweep record <run-issue> --journal <N> --apply

inventory scans each machine in machines.json: this one directly, the others over Tailscale SSH. Under ~/projects and ~/Sites it finds:

  • every git checkout down to five levels, so a folder of repos such as Sites/acme gives one row per repo (node_modules and vendor are not searched);
  • every plain directory that sits directly in a root, or beside nested repos;
  • a directory whose .git is not a repository of its own (a vendored copy), reported as that.

A manifest whose _meta.path is outside the two roots is scanned at that path. A manifest whose path is on no inventoried machine is a row of its own. A machine that cannot be reached is named as not inventoried, with the reason, and the command exits 3.

Each row carries:

Field Meaning
id the path relative to the home directory, the same on every machine
kind repo, dir (a plain directory) or manifest (on no machine)
project the manifest that declares this path, or null
machines per machine: git, dirty (uncommitted files), unpushed (commits on no remote), stashes, branch, last_commit
remote, remote_url, remotes, default_branch where it clones from. remote_url is origin’s URL whatever its host. remote is the first remote that names a GitHub repo, origin first. remotes lists every remote by name, so an origin on a host such as Pantheon, with a GitHub remote beside it, shows both (#3685). A row reads no remote only when it has none.
last_commit, subject the newest commit across machines
nested_in the checkout this one sits inside
same_remote_as other rows that are checkouts of the same repo
audit_class its class in the newest audits/portfolio-audit-*.md
portfolio the manifest’s _meta.portfolio, when it declares one
notes what needs a look: a manifest path that is not a checkout, or differs in case
decision {outcome, decided_on, note, with}, or null

--write saves the document to portfolio.json in the operator repo. A decision already recorded is kept. A decided row that is no longer on disk stays in the record, marked gone.

decide records one outcome for a row, named by its id or by its project. Nothing is carried out by this command.

Outcome For Means
invest, maintain, park any row a project we work on; a row with no manifest gets one
archive a row with a manifest the project is finished; see bob ritual decide
scrap a row with no manifest its local copies are to be deleted; --discard accepts the loss of what exists only there
not-a-project a row with no manifest kept on disk and excluded: a third-party clone, a documents folder

--with names another row decided as part of the same decision, such as a backup beside its original or the same repo at a second path. --note records one line of why. The row that is decided is the project’s one path: when its manifest sits on a row decided --with it, apply moves the manifest’s _meta.path to the decided row. A managed project is never scrapped. It ends with archive.

status counts the decided rows per outcome and lists the rows still undecided. It exits 0 only when every row has a decision.

apply carries the recorded decisions out, one decision at a time. It reports each step without --apply and changes nothing. For invest, maintain, park and archive the steps are:

Step What it does
archive archive only, and it runs first. If any inventoried machine holds unpushed commits or stashes in the project’s checkouts, the decision is blocked. The step names each machine, branch and count, and nothing is written. An archived repo refuses pushes, so that work would be stranded. There is no flag to accept the loss: push it, re-inventory, and apply again (#3687).
manifest a directory with no manifest gets one through bob ready. The directory must be a git checkout on this machine. Otherwise the step is blocked and names the machines that hold it.
meta _meta.path is corrected to the decided row. _meta.remote is recorded from the checkout. _meta.default_branch comes from GitHub when the repo is on it (a checkout’s origin/HEAD is set at clone and goes stale), else from the local ref. The step names which one it used (#3687).
decide bob ritual decide <project> <outcome> --apply, which writes _meta.portfolio and, for archive, archives the GitHub repo.

scrap and not-a-project rows change no manifest. Their record in portfolio.json is their outcome. projects.json is regenerated when a manifest changed. Running apply again changes nothing. Each step’s verdict is done, would, unchanged, blocked or failed, and a blocked or failed step names its fix.

scrap is the deletion list for the rows decided scrap. With no flag it deletes nothing. It measures every path again, on the machine that holds it, and prints one line per path, grouped by machine:

Verdict Means
DELETE nothing exists only there: no uncommitted file, no unpushed commit, no stash, and a remote; or an empty directory. With an archive drive declared, only once the path is archived
DELETE … archived as … the path’s archive on the drive was checked against it; the line names the archive and what existed only there
DELETE … LOSES … the row was decided with --discard; the line says what is lost
BLOCKED something exists only there. The line names it and says what to do. A blocked path is never touched.
gone already removed
UNKNOWN its machine could not be reached

Blockers are: uncommitted files, unpushed commits, stashes, a repo with commits and no remote, a plain directory that is not empty, and a directory that holds a row not decided scrap. With an archive drive declared, a path is also blocked when it is not archived yet, when its archive failed the check, or when it changed since it was archived.

--archive copies every path on the list to a drive before anything is deleted (#3706). The drive is declared once, in the operator repo’s config/sweep-archive.json:

{
"mount": "/Volumes/rome",
"folder": "bob-sweep-archive",
"reach": "open smb://fubaz/rome in Finder, then run the command again",
"machines": { "farm-01": { "mount": "/mnt/rome" } }
}
Key Meaning
mount the mounted volume. It counts as there only when something is mounted on it (its device differs from its parent’s), so an empty mount point is never written to
folder the folder on the volume that holds the archives; created when missing
reach how to mount it, printed when it is not there
machines.<id> a machine’s own mount, folder or reach
mountpoint: false the location is a plain directory, not a mounted volume

When the drive is declared but not there, --archive and --apply exit 2, name it and say how to reach it, and archive or delete nothing.

On the drive, under <mount>/<folder>/:

File Holds
<machine>/<path>.tar.gz the directory, .git included, as tar -C $HOME <path>
SHA256SUMS one line per archive; shasum -a 256 -c SHA256SUMS checks them
README.md what the folder holds and how to restore one path: tar -xzf <machine>/<path>.tar.gz -C "$HOME"

Each archive is checked by its content: it is read back from the drive, unpacked into a scratch directory and compared, directory by directory, link by link and file by file, with the source. A difference is named and the path stays blocked. A path on another machine is packed and measured on that machine over the fleet’s SSH transport and streamed to the drive here. SHA256SUMS and README.md are rebuilt from the records in portfolio.json (archives, keyed <machine>:<path>) and written in one write each.

An archived path is listed as DELETE … archived as …, and the approval covers it. Just before --apply deletes it, the path is measured again and compared with what was archived. A path that changed is refused and the command exits 1. --discard still deletes a row without an archive.

Deleting needs one approval of that exact list. The dry run prints a digest of the paths it would delete. --approve <digest> records it in portfolio.json (scrap_approval, with each approved entry). --apply refuses, exit 2, when no approval is recorded or the list has changed since. Paths the approved list deleted and that are now gone do not count as a change, so after one machine has deleted its share, the next machine runs --apply on the same approval. A new path, or an approved path that now loses more, still refuses (#3705).

--apply deletes on the machine it runs on, and only there:

Path Removed by
a plain directory bob ritual sweep scrap --apply on that machine
a checkout on a machine that is not a laptop (farm-01) the same, run on that machine
a checkout on a laptop (role dev-laptop) Fleet: the prune step of fleet repos sync (paulirv/fleet#77). The list names it and this command leaves it alone.

After a row’s copy here is gone, its GitHub repo is archived, never deleted, when the operator owns it (operator.json forge.owner) and no kept row is a checkout of it. A repo someone else owns is left alone.

audit renders portfolio.json as one markdown page: the machines inventoried, the count per outcome, and one row per directory with its decision. --write saves it as audits/portfolio-audit-<date>.md in the operator repo. The newest dated audit is the current evidence, and the next inventory reads each row’s audit_class from it.

record <run-issue> writes the sweep into the portfolio-review run issue, so that bob ritual close finds what it checks:

  • the Portfolio decisions table gets one row per manifest, with Decision set from the manifest’s _meta.portfolio. A project the sweep made managed is added with new under Status now;
  • --journal <N> is linked on every row whose status changed and that has no link yet;
  • a Swept directories section lists the rows with no manifest, with their outcome.

It reports without --apply. It names the manifests that still declare no decision, and the changed rows with no decision issue. The repo is --repo, else the origin of the bigbrain checkout. Exit 4 when the forge could not be read or refused the write.

projects.json answers “which projects do we work on, and where does each clone from” in one read (#3609). It is derived from the manifests by projects-derive.js and deployed to BOB_HOME:

Field From
name, path, group, active _meta.project, _meta.path, _meta.group or the path default, _meta.active
remote, default_branch _meta.remote, _meta.default_branch, recorded by sweep apply; absent until recorded
portfolio _meta.portfolio; absent until decided
excluded[] top level: {path, outcome, remote} for every directory the sweep decided scrap or not-a-project

Fleet checks its declared repos against this file: a project decided invest or maintain belongs on a laptop, one decided archive and a directory decided scrap do not.

Flag Meaning
--write inventory: save portfolio.json
--machines id,id inventory: scan only these machines; the rest are named as not inventoried
--self <id> inventory: which machine this is, when its hostname is not in machines.json
--audit FILE inventory: the audit to read classes from
--with <row> decide: repeatable
--note "<why>" decide
--discard decide <row> scrap: accept the loss of what exists only there
--apply apply: write; scrap: delete; without it, report
--only <row> apply: carry out this one decision
--approve <digest> scrap: approve the exact list the dry run printed
--archive scrap: archive every path on the list to the declared drive and check each archive; deletes nothing
--write audit: save the dated audit in the operator repo
--journal <N> record: the decision issue linked on each status change
--repo owner/name record: the repo that holds the run issue
--json emit bob-portfolio/1, or the -decide/1, -status/1, -apply/1, -scrap/1, -audit/1, -record/1 document of the subcommand
--root DIR another checkout’s operator data

Exit codes: 0 done · 1 status: a row is undecided; apply: a decision is blocked or failed; scrap --apply: a path could not be removed; scrap --archive: an archive failed its check · 2 usage or config error (an unknown row, an outcome the row cannot take, no operator repo, a scrap list that is not approved, an archive drive not declared or not there) · 3 inventory: a machine was not inventoried · 4 record: the forge could not be read or refused the write.

A manifest carries what apply records in two _meta keys: remote (the origin URL of the checkout) and default_branch.

Recovery procedures that are never rehearsed fail when needed. Three drills are declared once, at the BoB level, on the drill entry’s drills list in registry/rituals.json (#2240) — like the portfolio review, each is held on its home (bigbrain) at its own cadence whatever that project’s liveness, keyed drill:<name>, with its runs filed in the home’s repo, and no other project may declare it:

Drill Kind Rehearses Runbook Default
restore restore rebuilding BOB_HOME and one project on a machine from cdr and declared state alone, timed — what is copied back from the backup by hand is undeclared state drill-restore P3M
lost-laptop credential-rotation revoking every credential whose consumers include one machine, from the revocation list secrets-inventory.json generates — the inventory is only complete enough if the list can be acted on (requires_machine: a run names the machine) drill-lost-laptop P3M
farm-compromise incident-response a tabletop: farm-01 is assumed compromised — blast radius from the inventory, containment order, revocation, rebuild, detection; nothing is executed drill-farm-compromise P3M

Each drill entry carries name, kind (a drill_kind), purpose, home, cadence (the default), runbook (a registry/runbooks/ basename the gate checks exists), requires_machine, tabletop and why. Every runbook has the same spine: a timed checklist (target minutes per step, ticked in the run issue with the clock time), the steps, an outcome template — machine, started, recovered, time to recover, steps that failed, undeclared state discovered, steps over target — and an escalation rule. The incident kit (the drill kit) carries the three runbooks, so bob ritual run drill links them into .claude/runbooks/.

The home may restate a drill’s cadence — and only that — with a declaration naming the drill:

{ "name": "drill", "drill": "restore", "cadence": "P6M", "why": "restores are rehearsed on every new machine anyway" }

drill_kind may accompany it only if it matches. Any other project declaring drill: <name> is refused (drill-declared), as is the home declaring a plain drill of the kind that is a BoB-level drill’s key. A drill is never farm-eligible: bob tick prepares no packet for it, and the rehearsal itself is the evidence. bob ritual due --project bigbrain lists the three as drill:restore, drill:lost-laptop and drill:farm-compromise with scope: drill; a run’s marker carries drill (the name) beside drill_kind, and machine when one was named. Gaps a drill finds are filed as issues from the run exactly as any ritual’s findings are (Closing a run).

A guide that needs a real person to follow it once is not a to-do. It is an attended session, and the agent leads it (#3809). The ask gate refuses the to-do and names this ritual.

The guide-walkthrough entry in registry/rituals.json lists the guides that are owed a walkthrough:

Field Meaning
name the guide’s name; the run key is guide-walkthrough:<name>
title the guide’s title
doc its file, relative to the home project’s checkout
home the project whose repo holds the guide, its requirement and the runs
requirement the issue whose acceptance criterion asks for the walkthrough
criterion which checkbox of that issue asks for it, counted from 1

A walkthrough has no cadence. bob ritual due lists one for each guide whose requirement is open with that criterion unticked, as due. Ticking the criterion, once the findings are folded back into the guide, takes it off the list. A requirement that cannot be read grades its walkthrough unknown. No project declares the ritual (check-rituals: walkthrough-declared).

Terminal window
bob ritual open guide-walkthrough --guide first-product --dry-run # a listed guide, by name
bob ritual open guide-walkthrough --guide docs/tutorials/first-product.md
bob ritual open guide-walkthrough --guide runbooks/restore.md --project <project> # any other .md file
bob ritual run guide-walkthrough --guide first-product # open or resume, apply the kit, print the brief

A listed guide is walked on its home project. Any other .md file is walked on the project given, keyed by its file name. The run body carries a three-item checklist, and close refuses a run with none of them ticked:

  1. Open the guide.
  2. Do each step with the operator, in order and exactly as written.
  3. Record each place it was wrong as a finding.

Replacing a to-do. A to-do that asked the operator to follow the guide is converted with --replaces <todo> (repeatable):

Terminal window
bob ritual open guide-walkthrough --guide first-product --replaces 2575 --dry-run
bob ritual open guide-walkthrough --guide first-product --replaces 2575

Once the run exists, filed now or already open, each to-do gets a comment linking the run and is closed as not planned. It never gets completed, so nothing resumes from it; the walkthrough stays on bob ritual due instead. Only an open todo without completed is replaced: a closed one, a completed one or an issue that is not a to-do is refused (exit 2) before a run is filed. A to-do the forge would not close is named and the command exits 4; run it again.

The session brief for a walkthrough replaces “gather the evidence yourself” with leading the guide one step at a time: the agent says the step, the operator does it as written, and each difference between the page and what happened is a finding.

A review that walks a list nobody wrote down covers whatever came to mind that day. A declaration may therefore name a checklist — a registry runbook the run embeds as tickable items — so every run of the ritual walks the same list at a known version, and what it skipped is recorded rather than forgotten (#2241):

{ "name": "security-review", "cadence": "before-stable-release", "gates": ["stable-release"], "checklist": "bob-attack-surface-review" }

The checklist runbook carries two things ritual.js reads:

In the runbook Read as
**Checklist version:** \1`` the version — bumped in the same PR as any change to the list
a | # | Item | Surface | How to assess | table one tickable item per row; the last column links the runbook section that says how

What it changes:

  • Opening. The run’s body gains a Checklist section between Kit and Evidence: the runbook, its version, and each row as - [ ] N. <item> (`<surface>`). The marker stamps checklist: {name, version}, so the run knows which version it was opened against even after the list changes. A runbook with no version line or no rows is embedded as unversioned with a visible gap line, never silently empty; a declared checklist whose runbook does not exist is refused (unknown-checklist).
  • Holding. bob ritual run prints the checklist line beside the packet, and the session brief has the agent walk the items. Each item is assessed as its runbook section says and ticked; an item left unticked is skipped on purpose — — not assessed: <why> after it records the reason.
  • Closing. bob ritual close refuses a run with nothing ticked (it was not held). Otherwise the marker’s closed_with gains checklist: {name, version, items, assessed, not_assessed: [n…]}, the close prints the record, and the delta gains a Checklist line — this run’s version, count assessed and items not assessed, then the previous run’s, with a note when the versions differ (items are only comparable at the same version).
  • Gate. checklist names a registry/runbooks/ file (unknown-checklist) and is refused on a drill, whose checklist is its runbook’s timed one (checklist-on-drill).

The docs review. docs-review (#2625) is the human pass over the documentation that make docs-check cannot make: its checklist, docs-review-checklist, carried by the review kit, re-verifies stale and aging pages, walks one tutorial end to end on a clean project, checks that new commands and skills are reachable from a how-to, that no page presents a retired item as live, that the live site matches the latest build, the “Start here” nav, the generated references, the troubleshooting page against the period’s errors, and the orphan and archive candidates. comprehensive-review keeps doc-audit as one of its inputs; docs-review is the focused pass, so the two do not duplicate each other. It is farm-eligible — the farm prepares the packet, a person holds the run — and its default cadence is monthly while a project is active and quarterly while it is dormant. A project that declares its own _meta.rituals adds it with the checklist:

{ "name": "docs-review", "cadence": "P1M", "checklist": "docs-review-checklist" }

Its durable outputs are the ones every run has — bob ritual close refuses a finding that links no req, bug or todo and gives no no-action reason — plus one PR of the pages the run re-verified, each with a restamped last_reviewed, whose body says Refs #<run> so it is listed on the run issue. The packet names the tutorial the run walks (Tutorial rotation). bigbrain’s declaration waits for the vocabulary to reach master, because the operator repo is shared with the master checkout (#3447). Holding one: Hold a Ritual → Docs review.

BoB’s attack-surface review. bigbrain’s security-review walks bob-attack-surface-review, carried by the security kit: hooks on every tool call, the automation profiles and the full-auto floor, the farm tick’s unattended merge, the RDB bridge and Telegram channel, gh token scopes per machine, Tailscale ACLs and SSH, BOB_CHECKOUT_TOKEN and the CI credentials, registry items that run shell, .mcp.json servers, the self-hosted runner, and the credential inventory against the stores. Each item names the commands whose output is its evidence and what a clean result looks like; the session runs the security-auditor agent once per surface and files each finding as a security issue. It is held on a laptop — the farm-worker role forbids the security kit — before each stable release, and the release gate holds bob release promote until the run is closed. Its first run is #2422, opened 2026-09-15 with checklist v1 embedded; the gate is on for farm-01 (the machine that promotes), so the first stable release after #2241 waits for that run to close — todo #2423 is the human’s handle for holding it.

Form Meaning
P<n>D, P<n>W, P<n>M, P<n>Y Due that long after the ritual’s last run — P14D, P2W, P3M, P1Y. n ≥ 1; hours and combined durations are refused.
before-stable-release Due ahead of each stable release rather than on a clock. With gates: ["stable-release"] it holds the stable promotion until it has been held since the last release — see The release gate.
{
"_meta": {
"project": "bigbrain",
"rituals": [
{ "name": "comprehensive-review", "cadence": "P3M" },
{ "name": "security-review", "cadence": "before-stable-release", "gates": ["stable-release"], "why": "security review keeps pace with releases" },
{ "name": "drill", "cadence": "P6M", "drill_kind": "restore" }
]
}
}
Field Rule
name required; one of the vocabulary, except portfolio-review
cadence required; a cadence as above
kit optional; a kit in registry/kits.json — defaults to the vocabulary entry’s
farm_eligible optional boolean — defaults to the vocabulary entry’s
drill_kind required on a drill, refused on anything else; two drills of different kinds may coexist
drill a BoB-level drill by name — only on its home project, to restate its cadence (Drills); a drill_kind alongside must match
why optional one line — why this project departs from the default
gates optional; a non-empty list of what the ritual holds while overdue — today only stable-release (The release gate)
checklist optional; a registry/runbooks/ basename the run embeds as tickable items at its declared version (Checklists, #2241); refused on a drill
advisories_at optional, dependency-audit only; critical | high | moderate | low — the severity at or above which this project’s audit files advisories as issues, over the vocabulary’s high (Advisories, #2242)

Declaring _meta.rituals — even as an empty list — replaces the defaults: the project’s schedule is exactly what it lists. A project with no _meta.rituals gets the default cadences for its liveness.

A project that declares nothing is classified, and scheduled with that column of the vocabulary table:

Liveness When Schedule
active its last commit falls inside liveness.active_within (P3M), or a warp run is live the active defaults
dormant its last commit is older than the window the dormant defaults — a dependency audit every six months, nothing else
unknown nothing measured it the active defaults, so an unmeasured project is shown its rituals rather than let lapse
archived _meta.active: false no rituals

Liveness is read, never declared:

  1. The fleet snapshots ($BOB_FLEET_SNAPSHOTS, default ~/.claude/fleet/snapshots/*.json) — each project row’s last_commit.at and warp.session. The newest measurement across machines wins; a row whose has neither a last commit nor a live run did not measure the project.
  2. The checkout on this machine, when no snapshot measured it and _meta.path exists — read through scripts/fleet/project-live.js, the same reader the snapshot uses, so the two can never disagree about a project.

The report names which one it used (snapshot:<machine> or live).

Every held ritual is a GitHub issue in the project’s repo, so run history is shared across machines with no local ledger (#2231):

Label Meaning
ritual-run this issue is one ritual run — an execution artifact, never lesson or decision
ritual:<name> which ritual — one label per vocabulary entry; a drill’s kind is in the body

Both are part of the standard label set scripts/bootstrap-labels.sh ensure ships. The run’s body carries a one-line marker:

<!-- bob-ritual-run/1 {"project":"bigbrain","ritual":"comprehensive-review","held_on":"2026-09-10"} -->

drill_kind is added for a drill; a BoB-level drill adds drill (its name, which keys the run as drill:<name>) and machine when the run named one (Drills). Last run per (project, ritual) is derived, never recorded:

  • only closed runs count; a run closed as not planned was cancelled and does not;
  • the day is the marker’s held_on, else the date the issue closed;
  • a marker naming another project is skipped; a run with no marker counts for every project in the repo;
  • the newest day wins, ties to the higher issue number, which is reported as last_run_issue.

bigbrain’s first recorded run is the 2026-09-10 architecture review, backfilled as comprehensive-review run #2372 (held_on 2026-09-10, closed as held) with the issues it filed as its findings.

The repo is --repo owner/name, else the github.com origin of the project’s checkout on this machine. Reads go through gh (and the bin/gh shim’s forge cache), one call per repo per report. No repo, or a forge that cannot be read, makes every dated ritual unknown — never “never run”, so a degraded forge cannot flip a whole fleet to due.

Terminal window
node scripts/rituals/ritual.js open comprehensive-review --project bigbrain --dry-run
node scripts/rituals/ritual.js open drill --drill-kind restore --project bigbrain
node scripts/rituals/ritual.js open --ritual guide-walkthrough --guide first-product --dry-run

The ritual is the positional name or --ritual <name>.

open reads the repo’s ritual-run issues first and refuses a duplicate: if a run of the same ritual (a drill: the same kind) is already open for the project, nothing is filed and the open run is named. Otherwise it files one issue labelled ritual-run and ritual:<name>, titled ritual: <name> — <project> (<held_on>), whose body is the standard shape:

Section Holds
marker bob-ritual-run/1 — project, ritual, drill_kind, checklist ({name, version}, #2241), held_on
header table project and manifest, held date, kit, cadence (declared, default, or not on the schedule)
Scope the ritual’s purpose from registry/rituals.json, to narrow before the session
How the session runs the session brief as a numbered list, so a session that starts from the issue alone is led the same way (#3611)
Kit claude --kit <kit>
Checklist a run whose declaration names a checklist (#2241): the runbook, its version and each of its rows as a tickable item — see Checklists
Drill a drill run only (#2240): the drill, kind, machine and runbook, then the runbook’s timed checklist as tickable steps with their target minutes and a done at: __:__ slot; a tabletop says nothing is executed
Revocation list a credential-rotation drill with --machine only: the machine’s revocation list generated from secrets-inventory.json by cred-check.js revocation (secrets-inventory.md) — its gaps are already findings
Evidence a placeholder — the packet is posted as a comment (farm-prepared when the ritual is farm-eligible, #2233); a drill says the rehearsal itself is the evidence
Outcome a drill run only: the outcome template — machine, started, recovered, time to recover, steps that failed, undeclared state discovered, steps over target — filled in the session; close refuses while it is unfilled
Findings a checklist; each finding is its own bug, req, task, security or todo issue, linked, or a no-action: <why> line
Journal lessons and decisions from the run, each its own /journal issue, linked — never a label on the run (#2062)
Delta vs previous run the previous closed run for this project and ritual, or “first recorded run”; filled in at close
Portfolio decisions a portfolio run only: one row per project with its status now, to decide in the session (Portfolio decisions)

Close the run with bob ritual close when the ritual is held — that close is what makes it the last run. Close it as not planned to cancel.

Flag Meaning
<ritual> required; a vocabulary name
--project <p> the project (default: the one holding the current directory)
--drill <name> a BoB-level drill — restore, lost-laptop, farm-compromise — filed on its home (--project may only name the home); its kind and runbook come from the vocabulary (Drills)
--drill-kind <kind> a project’s own drill: required for drill without --drill, refused on any other ritual; with --drill it must match the drill’s kind
--machine <id> a machines.json id the drill rehearses losing or rebuilding — required by lost-laptop, allowed on any drill, refused elsewhere; recorded in the marker and the title
--repo owner/name file in this repo instead of the checkout’s origin
--held-on YYYY-MM-DD the held_on stamped in the marker (default today, UTC) — a backfill passes the day it was held
--dry-run print the title, labels and body; file nothing
--json emit bob-ritual-open/1
--root DIR read registry/, schemas/ and provisions/ from another checkout

Exit codes: 0 filed, queued, or --dry-run · 2 usage or config error (unknown ritual or project, a kindless drill, a bad --held-on, no resolvable repo) · 3 a run of this ritual is already open · 4 nothing filed — the forge could not be read (a duplicate cannot be ruled out) or refused the create (a missing label: scripts/bootstrap-labels.sh ensure --repo <repo>).

bob-ritual-open/1: verdict (filed | queued | dry-run | duplicate | forge-unreadable | not-filed), project, repo, ritual, drill_kind, key, held_on, kit, checklist ({name, version, items} — the checklist embedded, or null), title, labels, issue, url, duplicate, previous ({date, issue} or null), body and error. A create the bin/gh write queue spooled during an outage is queued with its BOB-Q<n> handle as issue — never reported as filed.

Gathering evidence is mechanical and cheap next to the judgement the session spends, so it happens before the session: prepare runs the ritual’s collectors report-only and posts one packet comment to the open run issue (#2233).

Terminal window
node scripts/rituals/ritual.js prepare 2372 --dry-run # print the packet, post nothing
node scripts/rituals/ritual.js prepare 2372 --json # gather and post (or replace) it

Each ritual names its collectors in registry/rituals.json → evidence, from this fixed set (schemas/rituals.schema.json → definitions.collector, implemented in scripts/rituals/evidence.js):

Collector Runs Needs
cost-churn scripts/dashboard/series.js --json — the project’s bob-series/1 row (#2072) the project registered on this machine
status-report the /what-next --report data through gh: issues closed in the last 14 days, open capability progress, the approved queue, open todos the repo
aged-issues scripts/warp-drive/cap-status.js audit --json — divergent cap labels, long-in-progress and parked issues (#2064) the checkout
usage-gaps scripts/orchestrator/gap-detect.js divergence --json and bob tour --no-backfill --json, plus the continuing-education queues: bob u status --json and loop-notes.js retire --dry-run --json / review --json the checkout
doc-audit scripts/doc-keeper/audit.js --root <checkout> --json the checkout
memory-audit scripts/memory-audit/audit.js --root <checkout> --json the checkout
dependency-audit npm audit --json / pnpm audit --json / composer audit --format=json --locked for each lockfile found — n-a when there is none; on a dependency-audit run the advisories are then filed, commented on and closed (Advisories) the checkout
fleet-readiness scripts/fleet/readiness.js projectReadiness(<checkout>) (#446) the checkout
doc-staleness scripts/doc-keeper/staleness.js --json (#2552) — every stale and aging page by path, grade and age the checkout
doc-coverage check-howto-coverage.js --json (#2553) and check-troubleshooting.js --json (#2562) — commands no how-to runs, entries that no longer match their message the checkout
doc-changes git from the previous docs-review run’s close (or the last 30 days) to now — bob commands, skills and retired items added that no page under docs/, runbooks/ or README.md names; generated references do not count — n-a when the checkout has none of the three the checkout; the run history
docs-tutorial the tutorial this run walks: the least recently walked across the closed runs’ packets, never-walked first (Tutorial rotation) — n-a without docs/tutorials/ the checkout; the run history
docs-site-live the live site’s /version.json (#2620) against the default branch’s HEAD and the newest release, and the last docs-deploy.yml run — a gap until the site serves the stamp; n-a without docs-site.json the checkout; the site; the repo
portfolio every managed project at once — liveness, readiness, overdue rituals, open p1/p2, cost/churn, harness defects (details, #2235) provisions/; each project’s snapshot row, checkout or repo where it has one

The checkout is the project’s _meta.path on this machine. Nothing a collector runs writes to the tracker, a manifest or the checkout — bob tour runs with --no-backfill for that reason, and a non-zero “found something” exit (cap-status 3, gap-detect 3, npm audit 1) is an outcome, not a failure. check-rituals refuses a farm-eligible ritual that declares no collectors.

A collector that cannot run is a gap, never a failure. No checkout on this machine, a tool that is not installed, a non-zero exit, output that is not JSON, a timeout or a spent budget each become a named gap with its reason; the other collectors still run and the packet is still posted. A collector that reads half of what it needs (usage-gaps with gap-detect but not the tour) keeps the half and names the rest.

Each docs-review run walks one tutorial end to end. Which one is derived, never stored: docs-tutorial reads the packets of the closed docs-review runs (the newest 24; a run closed as not planned walked nothing), where each packet names its tutorial in a <!-- bob-docs-review-tutorial/1 {"tutorial": …} --> line, and picks a tutorial no run has walked yet — in file order — or else the one walked least recently. The packet shows the whole rotation as a table. doc-changes reads the same history for its window, so prepare reads the run list once for both.

Continuing education — the usage-gaps queues (#2905)

Section titled “Continuing education — the usage-gaps queues (#2905)”

Continuing education is not a separate ritual (decision #2906). It is evidence on the two rituals that already hold usage-gaps: comprehensive-review and vision-planning, and no others. It adds no ritual name and no collector enum value. usage-gaps reports two queues, each as its own section of the packet and as a clause of its summary line:

Queue Read from Lists
Disposition backlog bob u status --json (Dispositions) Every item with no disposition, with its applicability bucket and fleet usage, and every certification flagged re-examine, naming the model it was certified on and the model it runs on now (Re-examination, #2904).
Loop notes due for review loop-notes.js retire --dry-run --json and loop-notes.js review --json, in the checkout (Loop memory) Notes whose defect: has closed but that are still in the store, and untagged notes recorded at least _workflow.loop_note_review_after_days (default 30) ago that have not recurred inside that window.

Both are report-only. The packet shows what retire would drop, and the attended session applies it with loop-notes.js retire --cwd <checkout>. For a due untagged note, keep it by tagging its defect or recording it again, or remove it from .claude/loop-notes.json. A long list is cut at 25 rows with the rest counted; the raw data keeps every row. When one queue cannot be read, its clause says so and usage-gaps still reports the rest.

The packet opens with a marker, then a summary table (collector, status ok | n-a | gap, one-line summary), a Gaps section naming each gap, a section for any collector with more to show than one line (the portfolio table, the usage-gaps continuing-education queues), and each collector’s raw JSON in a collapsed <details> block, truncated so the comment stays under GitHub’s size limit:

<!-- bob-ritual-packet/1 {"run":2372,"project":"bigbrain","ritual":"comprehensive-review","prepared_at":"2026-09-14T23:41:43.681Z"} -->

Re-running replaces, never duplicates. prepare reads the run’s comments first; a comment carrying the marker is edited in place, otherwise a new comment is added. When the comments cannot be read nothing is gathered or posted — an existing packet could not be ruled out.

Flag Meaning
<run-issue> required; the ritual-run issue number (# optional)
--project <p> the project (default: the run marker’s project, else the one holding the current directory); refused when the marker names another
--repo owner/name the repo holding the run (default: the project checkout’s github.com origin)
--budget-seconds N bound the whole gathering pass: a running collector’s timeout is cut to what is left and later collectors become budget gaps (each collector’s own timeout is 180 s)
--dry-run gather and print the packet; read no comments and post nothing
--json emit bob-ritual-prepare/1
--root DIR read registry/, schemas/ and provisions/ from another checkout

Exit codes: 0 posted, updated, queued, or --dry-run · 2 usage or config error (not a ritual-run issue, a project mismatch, no resolvable repo or project) · 3 nothing to prepare — the run is closed, or its ritual declares no collectors · 4 nothing posted — the run or its comments could not be read, or the forge refused the write.

bob-ritual-prepare/1: verdict (posted | updated | queued | dry-run | closed | no-collectors | forge-unreadable | not-posted), project, repo, issue, ritual (the key — drill:<kind> for a drill), kit, checkout (the path, or null when absent here), collectors[] (name, status, summary, reason, ms), gaps[] (name, reason), advisories (a dependency-audit run’s filing outcome — below; null on every other ritual), comment (id, url; both null when queued), body and error. A comment the bin/gh write queue spooled during an outage is queued, never posted.

Advisories — filed from the dependency audit

Section titled “Advisories — filed from the dependency audit”

The collectors are report-only; a dependency-audit run adds one write step after them (#2242). prepare hands the audit output the collector gathered to scripts/security/advisories.js, which reconciles it against the repo’s open advisory issues before the packet is rendered — so bob tick, which prepares farm-eligible rituals due within 48 hours, files advisories on the ritual’s cadence (monthly for an active project, every six months for a dormant one) with no one at the keyboard.

Each advisory is one per advisory ID + package — the GHSA slug where the tool gives one (npm, pnpm), else the CVE, else the tool’s own id (composer’s PKSA-…) — and the issue that tracks it opens with a marker:

<!-- bob-advisory/1 {"id":"GHSA-xvch-5gv4-984h","package":"minimist","tool":"npm"} -->
The audit reports The repo holds prepare does
an advisory at or above the threshold no open issue with its marker files a security, advisory, task issue on the owning repo — p1-critical for critical, p2-high for high, p3-medium for moderate, p4-low for low — titled by package, title and ID, in the task shape below
an advisory (any severity) an open issue with its marker comments “still reported on <date>” — a re-run never files a second issue
an advisory below the threshold no open issue nothing — it is counted as below_threshold and stays in the packet’s raw data for the attended session
nothing for an open issue’s advisory, and its tool ran an open issue with its marker closes it with the fixing commit when git shows one — the newest commit since the issue was filed that touched that tool’s lockfile with the package named on a changed line (a bump rewrites its resolved URL); otherwise “not detected”
nothing, but its tool did not run (no lockfile now, or the audit gapped) an open issue leaves it open as unverified — an audit that did not run resolves nothing

A filed advisory is a task (#3406). It carries the task label and the body /task writes, so warp-drive’s task loop picks it up and its definition-of-done gate judges it (The task work type):

  • ## Description — the advisory table: package (direct or transitive), linked advisory ID, title, vulnerable range, fix and lockfile.
  • ## Verification — three steps the loop runs and ticks: the package resolves to a fixed version in the tool’s lockfile (or, with no fix reported, to a version outside the vulnerable range); npm audit, pnpm audit or composer audit --locked no longer reports the ID; the project’s tests pass on the updated lockfile.
  • ## What to do — triage, patch or accept, with the fix path for its tool.
  • ## Notes — Priority: and a Trigger: line naming the linked advisory ID, the package, the audit tool, the date and the run number.

The marker still opens the body, so dedupe is unchanged: an issue filed before the task shape is found by its marker and commented on, never filed twice. Those older issues are not changed by an audit. advisories.js relabel [<checkout>] [--repo owner/name] [--dry-run] [--json] brings the open ones up to the task shape once: it adds task and appends the Verification checklist and the Trigger: line, read from the issue’s own marker and table, keeping every line the body had. An issue already in the task shape is current and left alone, so a second pass changes nothing. Exit 0 done, 2 usage error, 4 the forge could not be read or refused an edit.

An advisory whose tool reports no severity (older composer output) is never dropped: it is filed at the threshold with p3-medium and its severity shown as unknown. The threshold is the vocabulary’s advisories.file_at (high), which a project restates with advisories_at on its dependency-audit declaration (Declaring a project’s rituals). The three labels come from scripts/bootstrap-labels.sh; a create the repo refuses for a missing label is that advisory’s error, with the ensure command to run.

Nothing is written when the open issues cannot be read — a duplicate could not be ruled out — and --dry-run reads them, says what it would file, comment on and close (fixing commit included) and writes nothing, the state file included. A create the bin/gh write queue spooled is queued, with no issue number yet.

The packet’s summary table keeps the collector’s one-line count; a ### dependency-audit section beneath the gaps reports the outcome — one line of counts, then a row per advisory at the threshold, resolved issue and unverified issue with its action and issue number. The document’s advisories carries verdict (ok | partial | dry-run | forge-unreadable | skipped — no audit ran), threshold, open (at the threshold), by_severity, filed, commented, queued, closed, unverified, below_threshold, errors, issues[], state_file and error.

The state file. Every non-dry reconcile writes <checkout>/.claude/.dependency-advisories.json (bob-dependency-advisories/1): audited_at, run, threshold, tools (ok | gap per lockfile), open[] (id, package, severity, tool, issue, url), counts (by severity and total), below_threshold, unverified[] and actions. It is derived session state (scripts/lib/derived-paths.json, so the managed ignore block covers it) and the reader the fleet snapshot uses for a project’s open-advisory counts — no forge call. Outside a ritual:

Terminal window
node scripts/security/advisories.js run --dry-run # audit the checkout here, say what would be filed
node scripts/security/advisories.js run --severity moderate --json
node scripts/security/advisories.js status # what the last audit left: exit 0 clean · 1 open · 3 never audited

run exits 0 with nothing open at the threshold, 1 while advisories are open, 2 on a usage error, 4 when the forge could not be read or refused a write. It needs a github.com origin or --repo.

In the fleet snapshot. audit.js snapshot reads the state file for every present project — no forge call — into the project row’s advisories (open, critical, high, moderate, low, unknown, threshold, audited_at, run, issues[], unverified; null when the project was never audited on that machine) and rolls it up in summary.advisories (projects audited, with_open, the counts summed, names with something open, oldest_audit, and by_project with each project’s counts, issues and audit time). audit.js view renders a Dependency advisories table whose FLEET row counts each project once, by its newest audit across machines. That snapshot field is what the briefing collector (#2243) reads for open-advisory counts per project; rendering them in the briefing belongs to #2224.

What bob tick runs after its fleet snapshot (#2233):

Terminal window
node scripts/rituals/ritual.js prepare --due --all --within P2D --budget-seconds 17400 --json
node scripts/rituals/ritual.js prepare --due --dry-run # the project holding the current directory

A ritual is a target when it is farm-eligible, declares collectors, and is overdue or due with --within as the due window (default P2D, the next 48 hours — so a never-run ritual is always a target). unknown rituals are not: with no readable run history there is no repo to post to. Rituals are taken most urgent first, project by project. For each target:

  1. the open run of that ritual (for a drill, that kind) is reused; with none, one is opened through ritual.js open — the same duplicate refusal, so a target never gets a second run;
  2. its packet is prepared as above, with the remaining budget passed down.

--budget-seconds bounds the whole pass. A target reached after the budget is spent is listed as not prepared, and no run is opened for it. --dry-run reads the runs but opens nothing, posts nothing and runs no collector. It lists each target as would_prepare with the open run it would use, or opens: true. A run the forge write queue spooled, a refused create, or a packet that could not be posted is listed as not prepared with its reason; none of these is thrown.

Flag Meaning
--due prepare every due target instead of one run issue
--project <p> / --all as for due (neither: the project holding the current directory)
--within <duration> the due window (default P2D)
--budget-seconds N the evidence budget for the whole pass
--dry-run list the targets; write and gather nothing
--now YYYY-MM-DD, --snapshots DIR, --root DIR as for due

--repo belongs to prepare <run-issue> only; each target’s repo comes from its run history.

Exit codes: 0 every target prepared, nothing due, or --dry-run · 1 at least one target not prepared · 2 usage or config error.

bob-ritual-prepare-due/1: now, within, budget_seconds, dry_run, verdict (ok | partial), summary (targets, prepared, would_prepare, failed). Three lists follow, each entry carrying project, ritual, status and next_due:

  • prepared[] adds issue, opened, verdict (posted | updated | queued), gaps (a count), comment and advisories (a dependency-audit packet’s filing outcome, null otherwise) — the tick digest carries its verdict, threshold, open, filed, commented, closed and unverified;
  • would_prepare[] adds issue (null when a run would be opened) and opens;
  • failed[] adds issue and reason.

The attended session is where the judgement happens. run starts it prepared (#2234):

Terminal window
bob ritual run comprehensive-review # the project holding the current directory
bob ritual run drill --drill-kind restore --project bigbrain
bob ritual run dependency-audit --dry-run --json # what it would resume or open; writes nothing

bob ritual <verb> routes to node scripts/rituals/ritual.js <verb> — run, due, open, prepare, close and decide are rows in scripts/bob/commands.json.

  1. The role gate. The ritual’s kit is checked against this machine’s global manifest (kits.forbid, #2228). A forbidden kit is refused, exit 3, naming the manifest — before any run is opened or any overlay applied. A farm worker forbids security and vision-planning, so a dependency audit or vision session is never held there.
  2. Resume or open. The open run of this ritual for the project (for a drill, that kind) is resumed; with none, one is opened through open, with the same duplicate refusal.
  3. Apply the kit. bob provision overlay apply --kit <kit> --ritual-run <owner/repo#N> runs against the project’s checkout on this machine. The overlay is stamped with the run, so bob provision overlay status names it and a session abandoned without a close stays visible (Session Kits). With no checkout here the run is a config error unless --no-kit is passed.
  4. Present. It prints the evidence packet’s summary table. The packet is read back from the run’s bob-ritual-packet/1 comment. A fresh run has none yet, so run names bob ritual prepare <N> instead. It also prints the previous closed run’s Findings checklist, each line with the issue it links, and — for a ritual with a declared checklist — the checklist’s name, version and item count on the run issue.
  5. Brief. A run that started or resumed ends with the session brief. A --dry-run starts no session and prints none.

A ritual is a session the agent leads; the operator decides. Without that instruction an agent assesses everything and hands back a report: a list of findings, one question per finding, and “rotate per the runbook” for work it could have done (#3611). So run ends with a brief, one numbered line per step, in the order the session runs:

# Step What it tells the agent
1 Open in plain words Before any work, say what this ritual is (the vocabulary’s purpose, quoted), how long it will take and what the operator’s part will be. Assume it is the operator’s first time: no jargon, and no runbook or command named without saying what it does
2 Gather the evidence yourself Read the packet and walk the checklist, the drill’s timed checklist or the project. Ask the operator nothing while gathering
3 One recommended plan, one decision Put the findings into a plan, each with a recommendation, and ask once: do everything recommended / do part of it / stop for now, with the recommended answers pre-selected. Never one question per finding
4 Run the follow-up yourself Follow-up a runbook describes (a rotation, a cleanup, a config fix) is the agent’s to run on the operator’s go, saying what will be run and what the operator will see. The operator is handed a step only for a login, a vendor page or a decision, and that step says where to go, what to type and what to expect
5 Open every page with “Your steps” Any page or briefing the session produces opens with a “Your steps” list: the operator’s actions, numbered, written to the todo-writing rules. Findings come after it; when nothing needs the operator, the list says so
6 Record and close File each finding as its own issue, link it on the run, tick what was assessed, then bob ritual close <N>

The brief is the same for every ritual and every drill; only the purpose, the thing walked in step 2 and the run number change. It is built by run from the vocabulary, so a change to it reaches every open run. The same lines are written into the run issue’s How the session runs section when the run is opened, and each checklist runbook’s Holding the run section restates them for its own list.

Flag Meaning
<ritual> required; a vocabulary name
--project <p> the project (default: the one holding the current directory)
--drill <name> a BoB-level drill — restore, lost-laptop, farm-compromise — resumed or opened on its home; the printed line names the drill and machine and points at the runbook (Drills)
--drill-kind <kind> a project’s own drill: required for drill without --drill, refused otherwise
--machine <id> the machines.json id the drill rehearses — required by lost-laptop (its run body embeds that machine’s revocation list)
--repo owner/name the repo holding the runs (default: the checkout’s github.com origin)
--no-kit resume or open the run without applying the overlay
--dry-run open nothing and apply nothing — report the run it would resume, or that it would open one
--json emit bob-ritual-run/1
--root DIR read registry/, schemas/ and provisions/ from another checkout

Exit codes: 0 started, resumed, or --dry-run · 2 usage or config error (unknown ritual or project, a kindless drill, no checkout here without --no-kit) · 3 refused — the machine’s role forbids the kit · 4 not started — the forge could not be read (an open run cannot be ruled out), refused the create, or spooled it to the write queue · 5 the run is open but its kit overlay could not be applied (provision’s output is carried).

bob-ritual-run/1: verdict (started | resumed | dry-run | refused | forge-unreadable | not-started), project, repo, ritual (the key), kit, checklist ({name, version, items} or null, #2241), issue, url, opened, checkout, machine (id, role, manifest, forbid, error), overlay (applied, skipped — --no-kit | dry-run | null, exit, output), packet (prepared_at, comment, collectors[] of {name, status, summary}, gaps; null when there is none), packet_error, previous (date, issue, findings[] of {text, checked, issues, no_action}, error; null for a first run), brief (the session brief as an array of lines; null unless the verdict is started or resumed, #3611) and error.

A review that ends in conversation evaporates. close only closes a run whose outputs are durable (#2234):

Terminal window
bob ritual close 2372 --dry-run # check everything, print the closed body, write nothing
bob ritual close 2372 --finding 2380 --journal 2381 # add a finding and a lesson, then close
bob ritual close 2390 --no-action "lockfile already patched upstream"
bob ritual close 2391 --no-findings # held, found nothing — recorded
  1. Findings. Every line of the run’s Findings checklist, plus each --finding N and --no-action "<why>", must either link an issue in the run’s repo labelled bug, req, task, security or todo, or say no-action. Anything else refuses the close:

    • an unlinked line;
    • a missing issue or a pull request;
    • an issue with none of those labels;
    • an issue labelled lesson or decision — that is a journal entry, so it is linked with --journal.
  2. No findings. A run whose checklist is empty closes only with --no-findings. The Findings section then says No findings with the day it was held, and the marker records closed_with. --no-findings on a run that lists findings is refused.

  3. Journal. Lessons and decisions are filed as their own /journal issues. They are linked under the run’s Journal section, from its existing links plus each --journal N. A linked issue must carry lesson or decision and must not be a ritual run. A run that itself carries lesson or decision is refused, because a run is an execution artifact (#2062).

  4. Delta. The previous closed run of the ritual for the project is compared with this run, by the issues each run’s findings link:

    Bucket Holds
    Resolved since the previous run its findings that are now closed and not found again
    Recurring its findings still open, or found again this run
    New this run this run’s findings the previous run did not have, plus this run’s no-action lines

    A first run says so, and every finding in it is new. A portfolio run is also checked against its Portfolio decisions table here.

  5. Outcome (a drill run, #2240). A run whose marker names a drill or a machine — or whose body carries an Outcome section — closes only with the outcome recorded: Time to recover (or tabletop), Steps that failed and Undeclared state discovered must be filled, not the italic placeholders. A value in either of the last two that is not none is a gap, and a gap must be filed: closing with --no-findings, or with no finding that links an issue, is refused naming the gap. This is how a drill’s gaps become issues from the run.

  6. Checklist (a run with a declared checklist, #2241). A run whose body carries a Checklist section with items closes only when at least one is ticked — nothing ticked means the ritual was not held (checklist). Every unticked item is recorded as not assessed, with the reason given after — not assessed: when there is one; nothing is refiled or re-ticked.

  7. One write. Refusals are collected together and printed, and nothing is written until there are none. The Findings, Journal and Delta sections are rewritten, the marker gains held_on (--held-on restamps it) and closed_with: {findings, no_action, journal} — plus, for a drill, outcome: {time_to_recover, steps_failed, undeclared_state, gaps} while drill, drill_kind and machine are kept, and, for a checklist run, checklist: {name, version, items, assessed, not_assessed: [n…]} while the marker’s opening checklist stamp is kept. The issue is then closed as completed in the same PATCH.

  8. Restore the kit. Once the run is closed, the kit overlay bob ritual run applied is restored in the project’s checkout on this machine (bob provision overlay restore). Only an overlay stamped with this run is restored; the close says why it left one alone:

    Overlay Result
    none, or no checkout here nothing to restore
    a kit stamped with another run, or an unstamped kit applied by hand left as is
    the run’s kit together with a flight-plan source left for that warp run to restore
    unreadable state left as is — restore it by hand

    A refused close restores nothing, since the session is not over. --dry-run reports whether the overlay would be restored.

Flag Meaning
<run-issue> required; the ritual-run issue number (# optional)
--project <p> the project (default: the run marker’s, else the one holding the current directory)
--repo owner/name the repo holding the run (default: the checkout’s github.com origin)
--finding N a finding issue to add to the checklist (repeatable)
--no-action "<why>" a finding that needs nothing, with its reason (repeatable)
--journal N a /journal lesson or decision issue to link (repeatable)
--no-findings close a run with an empty checklist, recording that it found nothing
--held-on YYYY-MM-DD the day the ritual was held (default: the marker’s)
--dry-run run every check and print the closed body; write nothing
--json emit bob-ritual-close/1
--root DIR read registry/, schemas/ and provisions/ from another checkout

Exit codes: 0 closed, queued, or --dry-run · 2 usage or config error (not a ritual-run issue, a project mismatch, a bad --finding/--journal number, an empty --no-action) · 3 refused (each problem named, the run left open) or already closed · 4 the forge could not be read — the run, a linked issue, the run history or the previous run — or it refused the write · 5 closed, but the kit overlay stamped with the run could not be restored (restore’s output is carried).

bob-ritual-close/1: verdict (closed | queued | dry-run | refused | already-closed | forge-unreadable | not-closed), project, repo, issue, url, ritual, held_on, findings[] (text, issues, no_action, status filed | no-action | problem, reason), journal[] (issue, kind, title, status, reason), no_findings, decisions (a portfolio run: [{project, was, decision, changed, journal, status, reason}]; null otherwise), outcome (a drill run: {present, rows, filled, unfilled[], gaps[]} from its Outcome table; null otherwise), checklist (a checklist run: {name, version, items, assessed, not_assessed: [{n, text, reason}]}; null otherwise, #2241), delta (previous {date, issue} or null, resolved, recurring, new, new_no_action, and checklist — this run’s {name, version, items, assessed, not_assessed: [n…]} with the previous run’s record as previous, or null), problems[] (kind finding | journal | knowledge-label | no-findings | portfolio-decision | outcome | checklist, message), overlay (restored, skipped, exit, output, ritual_run, and would_restore on a dry run; null when the close wrote nothing), body and error.

Terminal window
node scripts/rituals/ritual.js due # the project holding the current directory
node scripts/rituals/ritual.js due --project bigbrain
node scripts/rituals/ritual.js due --all --json # every active project manifest
Flag Meaning
--project <p> one project, by _meta.project or manifest file name
--all every provisions/*.json except archived projects and the _-prefixed templates and global manifests
(neither) the project whose _meta.path contains the current directory (deepest match)
--json emit bob-rituals-due/1
--now YYYY-MM-DD grade as of this date (default today, UTC)
--window <duration> how far ahead counts as due (default P14D)
--repo owner/name read ritual-run issues from this repo instead of the checkout’s origin (one project only — refused with --all)
--runs FILE last-run dates, {"<project>": {"<ritual>": "YYYY-MM-DD"}} with a project drill keyed drill:<kind> and a BoB-level drill drill:<name> — replaces the forge read entirely (offline use, fixtures); a project the file omits has never run, and no guide walkthrough is listed
--snapshots DIR read snapshots from here instead
--root DIR read registry/, schemas/ and provisions/ from another checkout
--last-release YYYY-MM-DD the date of the project’s last stable release, for the release gate, instead of the newest v* tag in its checkout (one project only — refused with --all)

Each ritual is graded:

Status When
overdue its next due date (last run + cadence) has passed — days_overdue says by how much
due it has never run, or its next due date falls inside the window; a guide walkthrough whose requirement still has its criterion unticked (trigger names it)
unknown its run history could not be read (no repo, or the forge is unreadable) — never counted as overdue; an event cadence grades unknown too, with trigger set; a guide walkthrough whose requirement could not be read, with reason set
upcoming its next due date is beyond the window, or its cadence is an event (before-stable-release, with trigger set and next_due null)

Rituals are listed most urgent first. A month cadence clamps at the month end (a run on 31 January is next due 28 February).

Exit codes: 0 nothing overdue · 1 at least one ritual overdue · 2 a usage or config error — an unknown project, --project with --all, a bad --now/--window/--runs, an unreadable vocabulary, or a _meta.rituals that check-rituals refuses. With --json an error is {"schema": "bob-rituals-due/1", "verdict": "error", "error": "…"}.

bob-rituals-due/1: now, window, verdict (ok | overdue), summary (overdue, due, unknown, upcoming counts, and held — the projects whose stable promotion is held) and projects[] — each with project, file, archived, source (declared | default:active | default:dormant), liveness (state, source, last_activity, warp_active; null for a declared schedule), run_history (source issues | runs-file, repo, known, reason; null when archived), rituals[] (key, name, drill (a BoB-level drill’s name, else null), drill_kind, cadence, kit, farm_eligible, scope (project | portfolio | drill), gates, last_run, last_run_issue, open_run (the ritual’s open run issue, or null), next_due, trigger, days_overdue, status) and release_gate (below; null when archived).

Security review should keep pace with stable releases rather than drift behind them (#2236). A project can therefore make a ritual hold its stable promotion:

  1. Declare what the ritual holds — "gates": ["stable-release"] on its _meta.rituals entry (any cadence; before-stable-release is the natural one).
  2. Opt the project in — _workflow.ritual_release_gate: true in its .claude/settings.local.json (workflow-config.md). The gate is off by default: with it off, due still reports what would hold, and nothing refuses — except bob release promote --auto, the farm tick’s unattended promotion, which refuses (exit 3) while the gate is off or its setting cannot be read, naming _workflow.ritual_release_gate (#3627). Auto-promotion never runs past an overdue security review because the gate happened to be off; a human can still promote by hand with bob release promote.

ritual.js due grades each project’s release_gate:

Field Meaning
enabled the resolved _workflow.ritual_release_gate (false when the project has no checkout on this machine)
rituals the keys declared with gates: ["stable-release"]
last_release {tag, date} — the newest v* tag in the project’s checkout and its commit date, or --last-release (tag null), or null when neither is known
holding every gating ritual that would hold the promotion: key, status, reason, last_run, last_run_issue, open_run, next_due, days_overdue
held enabled and holding is not empty — the promotion is held

A gating ritual holds when:

Cadence Holds while
before-stable-release it has never been held, or its last run is before last_release.date — not held since the last stable release. With no release known and a run on record it does not hold.
a duration (P1M, …) it is overdue — past its next due date; merely due does not hold
any its run history is unknown — the gate never promotes blind

A held gate is informational here: due still exits 1 only for an overdue ritual. The human line reads release gate: HELD — <ritual> (<reason>; open run #N), release gate: off (_workflow.ritual_release_gate) — would hold: …, or release gate: clear — …, each ending with the last stable release it judged against. bob release promote reads the same document and refuses a held promotion; the farm digest lists it (see versioning.md and farm-tick.md).

A schedule nobody sees lapses silently, so every place that reads ritual.js due reads the same bob-rituals-due/1 grading (#2232):

Surface What it shows
Fleet snapshot (audit.js snapshot) projects[].rituals — {overdue, due, upcoming, unknown, overdue_keys, due_keys} — and summary.rituals; see fleet-snapshot-timer.md
audit.js view a Rituals table per machine plus a FLEET row that counts each project once
bob status --scope project an informational rituals section (this document) and rituals_overdue; see bob-status.md
bob status --scope fleet an informational rituals section (the fleet roll-up) and rituals_overdue
bob tick digest rituals: {prepared, overdue, due, unknown, error, prepare} — the rituals whose evidence packet the tick posted ([{project, ritual, issue, opened, verdict, gaps}], from prepare --due, #2233), what it would prepare or could not, in prepare (ran, reason, budget_seconds, would_prepare, failed), and every ritual still overdue ([{project, ritual, next_due, days_overdue, kit}]); a Rituals row in the markdown and rituals overdue N on the bob notify line; see farm-tick.md
/what-next overdue rituals in a block above the ranked work
/what-next --report a Rituals block: overdue, plus the due rituals of the next 14 days
/groom Step 5h — one rituals: <project> — N overdue: … line per project, report-only
bob release promote reads the project’s release_gate and holds the stable promotion (exit 4) while it is held; the tick digest’s rituals.held lists what was held (The release gate, versioning.md)

Overdue rituals are informational everywhere but one: they never move a health verdict or an exit code, except that a project which opted into the release gate has its stable promotion held (bob release promote exit 4) while a gating ritual is overdue.

node scripts/checks/check-rituals.js [--json] [--root DIR] — exit 0 clean, 1 violations, 2 a file it needs is unreadable or a usage error. --json emits bob-rituals-check/1: verdict, rituals (the vocabulary entries), declaring (the manifests with _meta.rituals) and problems.

Problem Meaning
schema registry/rituals.json does not validate against the schema
duplicate-ritual the vocabulary file describes a ritual twice
missing-ritual a name the schema enumerates has no entry in registry/rituals.json
unknown-kit a vocabulary entry or a declaration names a kit registry/kits.json does not declare
no-evidence a farm-eligible vocabulary entry declares no evidence collectors (an unknown or repeated collector is a schema problem)
declaration-schema a _meta.rituals entry names an unknown ritual, has an invalid cadence, lacks a cadence, carries an unknown key, or has a gates list that is empty, repeats a gate or names one the schema does not enumerate
drill-kind a drill with no drill_kind, or a drill_kind on a ritual that is not a drill
duplicate-declaration a project declares the same ritual (for a drill, the same kind) twice
portfolio-declared a project declares a ritual held once for the portfolio (portfolio-review)
unknown-home a portfolio ritual’s portfolio.home names no project manifest
portfolio-defaults a portfolio ritual carries liveness defaults — its cadence is portfolio.cadence
portfolio-status a park or archive project declares something other than the dormant defaults (a subset written before a ritual joined the vocabulary passes), an archive project is not active: false, or an inactive project declares another status (Portfolio status)
drills-on-non-drill a vocabulary entry other than drill carries a drills list (Drills)
duplicate-drill the drill entry declares the same BoB-level drill name twice
unknown-runbook a BoB-level drill’s runbook is not a file in registry/runbooks/ (skipped when the directory is unreadable)
drill-name a declaration gives a ritual that is not a drill a drill name
unknown-drill a declaration’s drill names no BoB-level drill
drill-declared a project other than the drill’s home declares a BoB-level drill, or the home declares a plain drill whose kind is a BoB-level drill’s key
checklist-on-drill a declaration gives a drill a checklist — a drill’s checklist is its runbook’s timed one (Checklists, #2241)
unknown-checklist a declaration’s checklist is not a file in registry/runbooks/ (skipped when the directory is unreadable)

Tests: tests/test-rituals.js (make test-rituals).

  • Hold a Ritual — the task, step by step: due → run → prepare → close → decide
  • Tracker labels — ritual-run among the execution artifacts
  • Session Kits — the kit each ritual applies; the incident kit carries the drill runbooks, the security kit the attack-surface checklist
  • Credential Inventory — what the lost-laptop drill’s revocation list is generated from
  • Verification System — check-rituals in make check
  • Capability #2222 — recurring rituals