Skip to content

Build master @ bc50cd5bunreleased — 31 commits past v1.282.0Last reviewed Last changed Grade fresh (aging >90d, stale >180d)Source / History

bob asks ​

bob asks lists every open ask of the operator across the portfolio. It is a projection: the GitHub issues are the store, and the only thing the command writes is the ask-waiting label, under --reconcile --apply. bob asks recheck --apply is the other write: it closes or comments on a to-do nobody has touched (Re-checking). bob asks done and bob asks snooze answer one ask (Answering an ask). Engine: scripts/asks/asks.js. Task guide: See Everything Waiting on You.

bob asks [--json] [--all] [--reconcile [--apply]]
bob asks recheck [--json] [--apply]
bob asks done <owner/repo#N> [--json]
bob asks snooze <owner/repo#N> --until <YYYY-MM-DD> [--json]
bob asks verify [--apply] [--json]

What an ask is ​

KindListed whenNever listed when
todothe issue is open, carries todo and does not carry completedit carries completed, or it is closed
briefthe issue is open, carries brief and is in brief:new or brief:actingit is in brief:done or brief:parked, or it is closed

The open brief states are the non-terminal states in scripts/brief/states.json.

Which projects are read ​

Every project in the operator repo's projects.json (#3432) that is not "active": false, is checked out on this machine and has a GitHub origin. Two registrations of one repo are read once. An active project that is not checked out here, or has no GitHub origin, cannot be read; --json names it under projects.not_here.

Each repo costs two gh issue list calls, one per kind. Six repos are read at a time. Every call waits for GitHub quota instead of failing on a limit. A repo with an ask that names what it gates costs two more (What an ask unblocks).

What an ask unblocks ​

The operator's rule for what comes first is an ask that holds up work BoB could be doing (#3915). Every ask carries unblocks, and the list is ordered by it. Engine: scripts/asks/unblocks.js.

TierThe ask gates
runningan unticked item of an open flight plan that has a live run
readyan unticked item of an open flight plan with no live run
approvedan open approved issue that is on no flight plan
unknownnot known: the forge could not be read. reason carries what it said. It is never reported as none
nonenothing that is open
RuleValue
The gate linksthe Blocked by #NN and Gates #NN lines of the ask's body, and the gates an open flight plan seats beside the ask's to-do row
A live runa warp-drive run whose kickoff names the plan: in this machine's checkout of the project, or in another machine's fleet snapshot less than 24 hours old
itemshow many open items the ask gates: unticked plan items and approved issues on no plan. null when the tier is unknown
planthe flight plan's number for running and ready; the plan holding the most gated items when there are several. Otherwise null
summaryone sentence, for example Unblocks 1 item in the running alpha plan.
The forge readsthe repo's open flight plans and its open approved issues, read only when one of the repo's asks carries a gate link. When either read fails, every ask of that repo with a gate link is unknown; the project is still listed

The asks are ordered by tier (running, ready, approved, unknown, none), then items descending, then priority (p1-critical first, no priority last), then oldest first.

The attention budget ​

The list is capped, so BoB has to choose what is worth the operator's attention (#3805).

LeverDefaultCapsSet it in
_workflow.ask_budget5the asks shown for one projectthe project's manifest or its .claude/settings.local.json
_workflow.ask_budget_portfolio15the asks shown across every projectprovisions/_default.json; $BOB_HOME/.claude/settings.local.json overrides it on one machine

The asks are put in most-blocking order and taken from the top. An ask is inside the budget while its project has room and the portfolio has room. Every other ask waits behind it: it is counted, not listed. --all lists the waiting asks under the ones inside. A budget of 0 is no cap. bob warp config shows both levers and which layer set each (Workflow configuration).

Sittings ​

A sitting is every ask the operator can clear in one place (#3806). The asks inside the budget are grouped by the place each one states on its Place: line.

RuleValue
One sitting perplace, compared in lower case
A sitting's minutesthe sum of its asks' Minutes:
Order inside a sittingdecide, then do, then check; within one kind, the list's order
Order of the sittingsby each sitting's first-listed ask, so the first sitting holds the most-blocking ask
An ask with no minutes or no placein no sitting; listed under unestimated, never dropped

An ask filed before the admission gate (the ask fields) states neither, and a brief states neither, so both are unestimated. An ask waiting behind the budget is in no sitting.

Asks that wait: ask-waiting ​

An ask beyond the budget carries the label ask-waiting. While it carries it, BoB does not interrupt the operator for it:

SurfaceA waiting ask
Phone pushNot sent. Its handoff record is stored waiting: true (the handoff record).
bob viz and warp viz bannersNot shown.
bob asksCounted as waiting; listed only with --all.

Two things set the label.

  • bob asks --reconcile compares the label with the budget across the portfolio. It reports each ask beyond the budget that lacks the label and each ask inside it that still carries it. With --apply it adds or removes the label, one gh issue edit per ask. For each to-do it releases, it sweeps that project's handoff records, which sends the push that was held back. bob tick runs it on every cycle.
  • ask-gate.js file decides at filing. The new ask is the newest of its priority. If the project's budget is already full of asks that rank above it, it is filed with ask-waiting. Only the project budget is applied here, because one project's read cannot apply the portfolio budget; the next reconcile does.

--json gains reconcile: applied, held[] and released[] (each project, repo, number, kind, url, changed, error) and swept[] (project and the handoff keys it released). Each row of asks[] and waiting[] carries ask_waiting: whether the issue has the label now.

Re-checking: bob asks recheck ​

bob asks recheck finds the to-dos nobody has touched for a long time and says whether each still matters (#3808). Engine: scripts/asks/recheck.js.

RuleValue
What is re-checkedan open todo without completed, in a project bob asks reads. A brief is not: its states belong to bob brief --reconcile
Untoucheddays since the issue's updatedAt. A comment, a label change or an edit touches it
The window_workflow.ask_recheck_days, default 30, set per project. A to-do is re-checked when it has gone untouched for longer than the window. 0 never re-checks that project
The issues it namesevery #NN and owner/repo#NN in its body, except its own number
VerdictWhen
mootit names at least one issue, and every issue it names is closed (a pull request: closed or merged)
neededan issue it names is open or cannot be found; or it names no issue; or its body carries the operator's <!-- not-stale: … --> marker
uncheckedthe issues it names could not be read. It is never treated as moot

The named issues of every to-do are read together, in as few gh api graphql calls as they allow.

bob asks recheck: 3 to-dos untouched for longer than 30 days — 1 moot, 2 still needed
moot    45d  alpha  #11 TODO: approve the old DNS change  https://github.com/acme/alpha/issues/11 — every issue it names is closed: #5, acme/beta#6
needed  45d  alpha  #12 TODO: rotate the deploy token  https://github.com/acme/alpha/issues/12 — open: #7
needed  40d  alpha  #13 TODO: pick a name  https://github.com/acme/alpha/issues/13 — it names no issue, so nothing says it is moot
report only — pass --apply to close the moot to-dos and record the re-check on the rest

One line per to-do: verdict, days untouched, project, number and title, link, and the reason for the verdict.

Without --apply nothing is written. With it:

VerdictWhat --apply writes
mootThe to-do is closed as not planned, with a comment naming the closed issues that made it moot. The completed label is not added, so nothing is resumed (the to-do lifecycle). Its line ends — closed
neededOne comment: the re-check date and why it is still needed. The comment touches the issue, so the to-do is not re-checked again until it has gone a full window untouched. Its line ends — re-check recorded
uncheckedNothing

A write the forge refuses is reported on the to-do's line as ERROR with the reason, and in asks[].error. That to-do is re-checked again on the next run.

--json prints one bob-asks-recheck/1 document.

KeyValue
schemabob-asks-recheck/1
generated_atISO 8601 time the days are measured from
verdictok · partial a project or the named issues could not be read · unread no project could be read
appliedwhether --apply was passed
window_days{ "<project>": <days or null> } for every project that was read
count, moot, needed, uncheckedthe number of rows in asks, and of each verdict
asks[]project, repo, number, title, priority, url, untouched_days, window_days, verdict, why, closed[] and open[] (the named issues in each state), action (close, comment or null), changed, error
projectsas in bob-asks/1
errornull, or { code: "forge", state, message }

Exit codes are those of bob asks.

bob tick runs bob asks recheck --json on every cycle, without --apply, and prints one line:

bob tick: asks recheck → 3 untouched past the window: 1 moot, 2 still needed, 0 unchecked — bob asks recheck --apply withdraws the moot

Sources: bob asks sources ​

bob asks sources says which filing path files the asks that turn out not to matter (#3810). Engine: scripts/asks/sources.js.

Every ask the admission gate files carries a marker naming its filing path: <!-- ask:source <name> --> (the todo format contract). The command reads every todo filed in the window, open and closed, in each project bob asks reads, and counts them per source. Nothing is written.

RuleValue
The windowto-dos created in the last 90 days. --days <n> sets another
The sourcethe name in the to-do's marker. A to-do with no marker is counted under unrecorded: it was filed before the marker, or not through the gate
Donethe to-do carries completed
Mootthe to-do is closed without completed: it did not need the operator
Openthe to-do is open and does not carry completed
Median to donethe median days from filed to closed, over the done to-dos. - when none is done
bob asks sources: 10 asks filed in the last 90 days by 3 sources (2 projects read)
SOURCE          FILED  DONE  MOOT  OPEN  MOOT%  MEDIAN TO DONE
host-preflight  5      1     3     1     60% !  2d
session         4      3     0     1     0%     6d
unrecorded      1      0     1     0     100%   -
moot: closed without `completed` — the ask did not need the operator
above the 30% moot threshold (!): host-preflight — bob asks sources --file-bugs lists the bug for each

The moot threshold ​

A source is above the threshold when its moot share (closed without completed ÷ filed) is greater than _workflow.ask_moot_threshold: default 0.3, a share from 0 to 1, resolved at the fleet layer. A source at the threshold is not above it, and 1 marks none. unrecorded names no filing path, so it is never marked.

A source above the threshold is reported in three places:

WhereWhat
bob asks sources! beside its share, and a line naming every such source
bob briefone needs_you item of kind ask-source per source, naming its counts (briefing.md)
bob asks sources --file-bugsthe bug for each source

Filing the bugs: --file-bugs ​

bob asks sources --file-bugs lists the bug each source above the threshold should have. Without --apply nothing is written. With --apply it files one bug per source in the BoB tracker, the repo of the BoB source checkout.

LineMeaning
bug: <source> — would file in <repo>no open bug carries the source's marker; --apply files it
bug: <source> — filed: <url>--apply filed it
bug: <source> — already open: <url>an open bug carries <!-- ask-source-bug: <source> -->; none is filed
bugs not filed: …the open bugs could not be read, or the BoB tracker could not be resolved. Nothing is filed

The bug's title is Ask source <source> files asks that turn out not to matter. Its body names the counts: filed, done, closed without completed, open, the median days to done, the share and the threshold.

--json prints one bob-asks-sources/1 document.

KeyValue
schemabob-asks-sources/1
generated_atISO 8601 time the window is measured back from
verdictok · partial a project could not be read · unread no project could be read
window{ days, since }
thresholdthe moot threshold, or null when none is set
above[]the names of the sources above it
totalsfiled, done, closed_without_completed, open over every source
sources[]source, filed, done, closed_without_completed, open, median_days_to_done (null when none is done), moot_rate (closed_without_completed ÷ filed), above, projects[]. Most filed first
bugswith --file-bugs only: applied, repo, rows[] (source, action file or exists, number, url, changed, error) and error
projectsas in bob-asks/1
errornull, or { code: "forge", state, message }

Exit codes are those of bob asks.

The lifecycle: bob asks draft, transition, status ​

An ask moves through one declared machine, scripts/asks/machine.json (#3811). It runs on the shared machine engine (scripts/warp-drive/machine-engine.js, #2975), so a change of state is a checked event and not a label edit.

PhaseWhat it meansEvents it allows
draftedA draft nobody has checked yet.admitted
admittedThe admission gate passed the draft.queued, presented
queuedFiled, and waiting behind the attention budget.presented, done, declined
presentedIn front of the operator: listed by bob asks.queued, done, declined, snoozed
snoozedPut off by the operator. It comes back.presented, declined
doneThe operator did it.consumed, presented
declinedClosed without being done.presented
consumedDone, and the work it gated has resumed.none

An event is named for the phase it reaches. Each ask has one state file, .claude/asks/<id>.json, in the project checkout. The managed ignore block lists the directory.

bash
bob asks draft renew-token --title "TODO: renew the deploy token" --label todo,p2-high --body-file draft.md
bob asks transition renew-token admitted
bob asks status renew-token --json
  • draft <id> records a draft at drafted. --body-file - reads the body from stdin. --kind is todo (the default) or brief. An id is lower-case letters, digits and dashes. A second draft under the same id is refused.
  • transition <id> <event> fires one event. An event the ask's phase does not allow exits 1 and names the events it does allow. The state file is not written. --number <n> records the issue the ask was filed as.
  • status <id> prints the phase and the events it allows.

admitted is the admission gate's to give. The gate reads the draft first. When it refuses, the command exits 3, the phase stays drafted, and the refusal is recorded on the ask:

$ bob asks transition renew-token admitted
bob asks transition: refused by the ask gate (#3803): minutes: …; tried: …
$ bob asks status renew-token
ask renew-token: drafted — TODO: renew the deploy token
  refused by the ask-gate: missing minutes, tried
    minutes: …
    tried: …
  next: admitted

done on a to-do is the completed done-signal. The command adds completed to the ask's issue first, and the phase moves only when the forge took the label. A forge that refuses it exits 4 and the phase stays where it was. An ask with no recorded issue number is refused: record it with --number.

consumed is fired on evidence. A bare transition <id> consumed is refused. todo-consume.js consume --apply fires it, and only after it has removed blocked from every open issue the to-do named and closed the to-do. When one unblock fails, the ask stays at done.

An ask filed before the machine existed has no state file. When the id is an issue number, transition adopts it first: it reads the issue and starts the record at the phase the labels say.

The issuePhase
open to-dopresented
open to-do with ask-waitingqueued
open to-do with completeddone
closed to-do with completedconsumed
closed to-do without completeddeclined
briefthe phase of its state label in scripts/brief/states.json

todo-consume.js adopts a completed to-do the same way, at done, before it fires consumed. An issue that cannot be read exits 4 and records nothing.

The brief states map onto the same phases: brief:new is queued, brief:acting is presented, brief:done is done and brief:parked is declined. make check-brief-labels fails when a state names no declared phase, or a brief transition has no event in the machine.

--json on any of the three prints one bob-ask/1 document: id, kind, phase, terminal, allowed_events, evidence_events, title, repo, number, refusal ({gate, missing, problems, at}, or null) and history.

make check-ask-machine (part of make check) fails when the definition lacks one of the eight phases, or names a transition to a phase it does not declare.

Answering an ask: bob asks done and snooze ​

These two answer one ask from a shell (#3807). The asks page gives the same two answers through the brief-tick Worker (apps/brief-tick), in every repo the Worker lists. Engine: scripts/asks/answer.js.

CommandWhat it writes
bob asks done acme/beta#7A to-do gets the completed label: the done-signal todo-consume.js acts on. A brief moves to brief:done, from a state scripts/brief/states.json lets reach it. An issue that is neither a to-do nor a brief, a closed one, or a brief:parked brief is refused with exit 1
bob asks snooze acme/beta#7 --until 2026-10-20One <!-- ask:snooze-until 2026-10-20 --> marker in the issue body. A second snooze replaces the date. The date must be after today

The same answer twice writes nothing. An ask whose marker is dated after today is not listed: the text output ends with snoozed: 1 ask hidden until their date — the soonest back 2026-10-20, and --json names each one under snoozed. From that date on, the ask is listed again. The marker the Worker writes and the one this command writes are the same, so a snooze from the page and one from a shell look the same to bob asks.

--json prints one bob-ask-answer/1 document: verdict (answered, refused, forge or error), answer (done / snooze), repo, number, kind (todo / brief), changed, until, from and to (the label states), and error.

Done-checks: bob asks verify ​

A to-do may declare a machine-runnable Done-check: (the field). bob asks runs it before listing the to-do (#3986). When the check passes, the to-do gets completed and is closed with a comment naming the evidence, so it is never listed, on BoB Control's Now page either, since Control reads bob asks --json. When it fails with a reason the body does not already record, the reason is written into the body's ## Latest check section first. --json counts the results under done_checks. The briefing runs the same check on the to-dos it lists.

bob asks verify runs every open to-do's done-check across the portfolio. Without --apply it only reports. With --apply it closes the to-dos that pass, records each new failure reason, and re-runs a failed CI run once per run id. That is what bob tick runs. --json prints one bob-asks-verify/1 document with these keys:

  • verdict, applied, checked
  • closed[], passing[], pending[], unknown[], recorded[]: owner/repo#N keys
  • failing[], errors[]: each {key, reason}
  • gaps[]
bob asks verify: 3 to-dos with a done-check — 1 closed, 1 failing, 1 pending, 0 unknown
closed: acme/site#63
failing: acme/api#12 — deploy.yml failure: Error: Missing permission Workers Scripts: Edit (https://github.com/acme/api/actions/runs/9)

Results are cached in $BOB_HOME/.ask-done-checks.json for _workflow.ask_done_check_ttl_minutes.

Text output ​

bob asks: 5 open asks in 2 projects (2 read)
sitting: phone — 17 min, 2 asks
p3-medium    9d   beta   todo   do      15m  #8 TODO: run the migration  https://github.com/acme/beta/issues/8  — Unblocks 1 item in the running beta plan.
p1-critical  2d   beta   todo   decide  2m   #7 TODO: approve the DNS change  https://github.com/acme/beta/issues/7
sitting: farm-01 — 10 min, 1 ask
p2-high      12d  alpha  todo   do      10m  #11 TODO: rotate the deploy token  https://github.com/acme/alpha/issues/11
not yet estimated: 2 asks with no minutes or place stated
p2-high      20d  beta   brief  #31 Brief: mobile area  https://github.com/acme/beta/issues/31
-            3d   alpha  todo   #14 TODO: pick a name  https://github.com/acme/alpha/issues/14
gap: gamma (acme/gamma) not read — forge rate-limited: API rate limit exceeded
LineShape
sitting:the place, the sitting's total minutes and its number of asks; one per sitting, in sitting order
An ask in a sittingpriority, age in days, project, kind, what it asks for (decide / do / check), its minutes, number and title, link. When its tier is not none the line ends with — and its summary
not yet estimated:heads the last group: the asks that state no minutes or no place; printed only when there is one
An ask not yet estimatedpriority, age in days, project, kind, number and title, link, and the same ending; in the list's order
gap:one per project that could not be read

When asks wait behind the budget, the first line ends — 2 more waiting behind the budget, and a line under the list reports them:

waiting: 2 more asks behind the budget (5 per project, 15 for the portfolio) — bob asks --all lists them

The bob-asks/1 document ​

--json prints one document.

KeyValue
schemabob-asks/1
generated_atISO 8601 time the ages are measured from
verdictok every project was read · partial at least one gap · unread no project could be read
countnumber of rows in asks: the asks inside the budget
openevery open ask that was read, inside the budget or waiting
by_project{ "<project>": <count> }, the open asks of every project that was read, zero included; an unread project has no entry
asks[]kind (todo / brief), project, repo (owner/repo), number, title, priority (a p1–p4 label or null), state (the brief state, null for a todo), created_at, age_days, ask_kind (the body's Kind: line: decide, do or check, else null), minutes (the whole number on the body's Minutes: line, else null), place (the body's Place: line in lower case: a fleet machine id or phone, else null), url, unblocks (tier, items, plan, summary, reason: see What an ask unblocks)
sittings[]one per place, over the rows of asks: place (string), minutes (integer, the sum of its asks' minutes), asks (an array of owner/repo#N keys). See Sittings
unestimated[]the owner/repo#N keys of the rows of asks that state no minutes or no place; they are in no sitting
waiting[]the asks behind the budget, in the same order and with the same keys as asks[]
done_checksthe to-dos whose done-check ran before listing: checked, closed[] (closed and left out of every list), passing[], failing[] ({key, reason}), pending[], unknown[], recorded[], errors[]
snoozed[]the open asks snoozed past today, soonest back first: kind, project, repo, number, until, url. They are in no other list
budgetportfolio (the cap, or null for none), per_project { "<project>": <cap or null> }, waiting (the number of rows in waiting)
projects.totalnumber of repos tried
projects.read[]names of the projects that were read
projects.gaps[]project, repo, state, reason for each project that could not be read
projects.not_here[]active projects not checked out on this machine, or with no GitHub origin
errornull, or { code: "forge", state, message } when the verdict is unread

A gap's state is the forge state of the failed call: rate-limited, unauthenticated, timeout, gh-missing, unavailable, or invalid when gh answered with something that is not a JSON list. If either of a repo's two calls fails, the whole project is a gap and contributes no rows.

Exit codes ​

CodeMeaning
0listed; a project that could not be read is named as a gap
1transition: the event is not one the ask's phase allows. draft: the ask already exists. done / snooze: the issue is neither a to-do nor a brief, is closed, or its brief state cannot reach brief:done. Nothing changed
2usage error, or no readable projects.json. transition and status: no such ask
3no project could be read: every forge read failed. transition <id> admitted: the admission gate refused the draft. done / snooze: the issue could not be read, or the forge did not take the write
4transition: the forge did not take the completed label, or the issue to adopt could not be read. The phase did not move

Tests ​

tests/test-bob-asks.js drives bin/bob asks against a scratch BOB_HOME, scratch git repos and a stub gh on PATH. tests/test-bob-asks-unblocks.js does the same over to-dos that gate a running plan, a ready plan, an approved issue and nothing, and one whose plans cannot be read. tests/test-asks-recheck.js drives bin/bob asks recheck the same way, and tests/test-asks-sources.js drives bin/bob asks sources. tests/test-asks-lifecycle.js drives bin/bob asks draft, transition and status in a scratch checkout, and check-ask-machine.js over definitions that lack a phase. It also runs todo-consume.js consume --apply and check-brief-labels.js there. tests/test-bob-asks-answer.js drives bin/bob asks done and snooze against a stub gh that keeps its issues in one file, then lists them with bin/bob asks. It also checks that the Worker's marker hides an ask. tests/test-brief-tick.js covers the Worker's repo list, Done and Snooze.