TODO Format Contract
Agent-created todo Format Contract
Section titled “Agent-created todo Format Contract”A todo issue is a human action item an agent hands back when it hits a
human-only blocker (a decision timeout, an escalation, a missing credential).
For the Human Operator to act on it without reading any session log, every
agent-emitted todo must be self-contained. This is the contract that makes
that guarantee — and the basis for the groom compliance check (#777) and the
downstream completed lifecycle (#775, see Work Tracking → TODO lifecycle).
Validator (single source of truth): ~/.claude/scripts/warp-drive/todo-contract.js
— check one issue (it also warns on WS-4, WS-5, WS-6) or audit every open todo.
groom and warp-drive both call it so the rule lives in exactly one place. Prose: WS-4 to WS-10.
The contract
Section titled “The contract”Every agent-emitted todo body MUST contain:
| # | Element | Requirement |
|---|---|---|
| 1 | Summary | A one-sentence statement of what is blocked and why, as the first line of the body, ≤ 25 words (WS-4). |
| 2 | Instructions | Numbered, self-contained steps the human can follow without external context (1. / 2. / …), each starting with a verb (WS-5). |
| 3 | Issue link | At least one explicit #NN reference. Use Blocked by #NN when the todo gates a specific issue, so downstream tooling knows what to resume. |
| 4 | Why a human | A Why a human is needed: <reason> — <detail> line naming why the agent could not act: login, decision, vendor UI, operator data (data only the operator has) or permission (e.g. permission denied). If none applies, the todo is not filed: the agent does the work. Required for every todo filed from #3161 on; the todo-writing lint (todo-lint.sh, E3) enforces it. todo-contract.js does not, so older todos are not flagged. |
How links are parsed
Section titled “How links are parsed”A link is either a bare #NN, an issue in the todo’s own repo, or a cross-repo
owner/repo#NN, an issue in the repo it names (#2912).
The contract check (todo-contract.js) accepts either form as the required link.
The stale sweep (todo-consume.js stale) reads each from its own repo — a
cross-repo link with gh issue view NN --repo owner/repo, never as a local #NN —
and grades the todo from the real state of both. A link that names the todo’s own
repo is the same as the bare #NN. The current repo is GH_REPO when set, else
what gh repo view reports. A cross-repo link the forge cannot read is reported
whole, as UNCHECKED #36 (… could not be read: owner/repo#NN). In the linked
issue of another repo, a bare #NN means that repo’s issue, so it counts as a
reference to the todo only in the full owner/repo#NN form. Blocked by, which
the consume path acts on, still takes bare #NN only: the blocked label it
removes belongs to an issue in the same repo.
And the issue MUST carry:
| Element | Requirement |
|---|---|
| Assignee | --assignee paulirv (the Human Operator) so it surfaces in todo + assignee:@me discovery (#778). |
| Priority | One of p1-critical / p2-high / p3-medium / p4-low. |
| Label | todo (plus warp-drive when filed by the loop). |
The admission gate (#3803)
Section titled “The admission gate (#3803)”Before an ask is filed, the filing path runs one check:
~/.claude/scripts/warp-drive/ask-gate.js check --body-file <draft>. The draft
is admitted only when its body states all five fields. Each of the four named
fields is one line, Name: value; the name may be bold or a list item.
| Field | Line | Admitted when |
|---|---|---|
kind |
Kind: <kind> |
one of decide, do, check |
minutes |
Minutes: <n> |
a whole number above zero: how long it takes the operator |
place |
Place: <where> |
a fleet machine id from machines.json, or phone |
steps |
1. … |
at least one numbered step |
tried |
Tried: <what> |
says what BoB attempted before asking |
A field that is absent, empty or not a valid value is missing. The refusal names every missing field, not just the first:
ask-gate: refused — missing minutes, tried minutes: no `Minutes:` line — state how many minutes it takes the operator tried: no `Tried:` line — say what BoB attempted before asking--json prints one ask-gate/1 document: verdict (admitted /
refused), missing, problems (field, message), duplicate,
duplicate_check, priority_check, guide and parsed.
| Exit | Meaning |
|---|---|
| 0 | admitted |
| 2 | usage error, or the draft could not be read |
| 3 | refused: a field is missing, the draft duplicates an open ask, p1-critical is not earned, or it is a guide to follow |
When no operator repo resolves, the fleet cannot be read. The gate then checks only that the place is a single word.
Done-check (#3986)
Section titled “Done-check (#3986)”A todo may also say how a machine can tell it is done, on one or more
Done-check: lines. These are separate from the steps the human performs.
The gate does not require the field. If the field is present, every line
must parse, or the draft is refused naming the line (missing then includes
done-check).
| Line | Passes when |
|---|---|
Done-check: ci <owner/repo> <workflow> [<branch>] |
the latest run of that workflow (on that branch) succeeded |
Done-check: secret <owner/repo> <NAME> |
the repo has a secret with that name |
Done-check: label <owner/repo#N> <label> |
the issue carries the label |
Done-check: closed <owner/repo#N> |
the issue is closed |
Done-check: file ~/<path> |
the file exists on the machine that runs the check |
The probes are a closed set and are parsed into arguments. Issue text names what to look at, never what to run (#3628). Every line must pass.
scripts/asks/done-check.js runs the check before a todo is listed by
bob asks, BoB Control’s Now page and the briefing, and on every bob tick
(bob asks verify):
| Result | What happens |
|---|---|
| pass | the todo gets completed and is closed with a comment naming the evidence, without ever being shown |
| fail | the reason is written into the body as a ## Latest check section (placed before ## Instructions), when it differs from the reason already recorded there |
| pending | a CI run is in progress or was just re-run; the todo is shown as it is |
| unknown | the forge could not say; the todo is shown as it is, never closed |
When bob tick finds a failed CI run, it re-runs it once per run id
(gh run rerun --failed) before reporting the failure. Results are cached in
$BOB_HOME/.ask-done-checks.json for _workflow.ask_done_check_ttl_minutes
(default 30). Only a failing or unknown result is reused; a pass, and a CI run that is pending or was just re-run, is probed again every time.
No open duplicate
Section titled “No open duplicate”Pass the draft’s title and the gate also refuses a duplicate:
node ~/.claude/scripts/warp-drive/ask-gate.js check --body-file draft.md \ --title "TODO: log in to Cloudflare so the test Worker deploys" --repo owner/repoask-gate: refused — duplicate of #41 duplicate: duplicates open ask #41 (same title) — https://github.com/owner/repo/issues/41A draft is a duplicate when its title or its first numbered step matches an
open ask in the same repo. An open ask is an open todo without completed.
The comparison ignores case, punctuation, markup and a leading TODO:.
--repo names the repo the ask is for; without it gh uses the current one.
When an open ask already covers the cause, comment on it instead of filing
(one cause, one todo).
If the forge cannot be read, the duplicate check is not made and the draft is
judged on its five fields. The output says so: a note: line in the text,
and duplicate_check: { made: false, reason } in the document. A forge
failure never refuses an ask. --no-duplicate-check skips the forge call.
p1-critical is earned (#3805)
Section titled “p1-critical is earned (#3805)”p1-critical is for an ask that holds up a flight plan. The gate admits it
only when the body’s Blocked by #NN line names an issue that is an unticked
item (- [ ] #NN) of an open flight-plan issue in the same repo: a plan
that is running, or approved to run, is waiting on the operator. Pass the
labels the ask will carry with --label (ask-gate.js file already has
them):
node ~/.claude/scripts/warp-drive/ask-gate.js check --body-file draft.md \ --title "TODO: rotate the deploy token" --label todo,p1-criticalask-gate: refused — p1-critical not earned priority: p1-critical is for an ask that blocks a flight plan: #123 is not an unticked item of an open flight plan — file it at p2-high or lowerA draft with no Blocked by #NN line is refused the same way and told to add
one. An ask at any other priority is not checked, and the flight plans are not
read for it. The document records the outcome as priority_check:
{ made: true, ok, blocks, plan }, or { made: false, reason }. When the
flight plans cannot be read the check is not made, the output says so, and the
ask is admitted.
A guide to follow is a led session (#3809)
Section titled “A guide to follow is a led session (#3809)”A Kind: check whose steps have the operator follow a named guide is not a
to-do. It is an attended session, and the agent leads those. The gate refuses
it and names the command that opens the session instead:
ask-gate: refused — a guide to follow is a led session route: step 2 has the operator follow a guide (docs/tutorials/first-product.md): that is a session the agent leads, not a to-do — open it with `bob ritual open guide-walkthrough --guide docs/tutorials/first-product.md`A step follows a guide when it says to follow, walk through, work through, go
through or run through one. The guide is named by its file anywhere in the
body (under docs/tutorials/, docs/how-to/ or runbooks/), or by the step
calling it a tutorial, guide, runbook, how-to or walkthrough. When the file is
named, a step that has the operator do its stops, steps, parts or sections
follows it too. The document
records it as guide: { step, guide, route }, or null. A do or a
decide is not checked: a runbook the operator has to run in person stays a
to-do.
An admitted ask that lands beyond its project’s
attention budget is filed by
ask-gate.js file with the ask-waiting label
(Asks that wait).
What groom reports
Section titled “What groom reports”todo-contract.js validateIssue runs the same field check on an existing
todo. Its missing list carries kind, minutes, place and tried
beside the older names, and admission.missing is the gate’s own list. The
gate’s steps is this contract’s instructions, reported once. So
todo-contract.js audit flags a todo that would not be admitted today:
NON-CONFORMING #212 (missing kind, minutes, place, tried) — TODO: rotate the deploy tokenAdmission is a filing rule. A todo that lacks the fields can still be
completed: the completed done-signal does not depend on them.
Filing through the gate
Section titled “Filing through the gate”Every path that files an ask runs the gate before gh issue create. A
refused draft files nothing.
| Path | How it calls the gate | What a refusal looks like |
|---|---|---|
| A session filing by hand: a warp-drive decision timeout, a ritual finding | ask-gate.js file |
exit 3, nothing filed, each missing field named |
warp-drive preflight gate (host-preflight.js) |
askGate.admit |
the todo record is action: error with the refusal |
warp-drive dry-run blockers (preflight.js emit) |
askGate.admit |
the blocker is listed under refused, and stderr names the reason |
PM front-end (pm-frontend.js emit-todo) |
askGate.admit |
exit 3; state the fields with --kind, --minutes, --place, --tried |
Rotation schedule (cred-check.js due) |
askGate.admit |
the row’s todo is action: error with the refusal |
Promotion gate (setup-promotion/human-gate.js emit --apply) |
askGate.admit |
filed: false with the refusal in error |
ask-gate.js file takes the flags of gh issue create that an ask needs:
node ~/.claude/scripts/warp-drive/ask-gate.js file \ --source warp-drive \ --title "TODO: {what's needed}" --label todo,warp-drive,p2-high \ --assignee paulirv --body-file draft.md--body-file - reads the draft from stdin. Exit 0: filed, the issue URL is
printed. Exit 3: refused, nothing filed. Exit 4: admitted, but
gh issue create failed.
What filed it
Section titled “What filed it”Every ask the gate files carries one marker naming the filing path (#3810):
<!-- ask:source host-preflight -->The gate adds it as the last line of the body. ask-gate.js file writes
session unless --source <name> names the path: warp-drive for a decision
timeout, ritual for a ritual finding. A script passes source to
askGate.admit and files the body the gate returns. A script that names no
source is refused. A draft that already carries a marker keeps it.
| Source | Filing path |
|---|---|
session |
ask-gate.js file with no --source |
warp-drive |
a warp-drive decision timeout |
ritual |
a ritual finding |
host-preflight |
the warp-drive preflight gate |
warp-preflight |
warp-drive dry-run blockers |
pm-frontend |
pm-frontend.js emit-todo |
promotion-human-gate |
the promotion gate |
bob asks sources counts the asks of each source. See
bob-asks.md.
A script that de-duplicates by its own marker (the preflight gate, the rotation
schedule, the promotion roll-up) has already read the tracker, so it calls
askGate.admit with duplicates: false and no second read is made.
Fields in a body a script builds
Section titled “Fields in a body a script builds”A script that files asks builds the four lines with askGate.fieldLines({ kind, minutes, place, tried }). For an ask the operator does at the machine that
filed it, askGate.selfPlace() gives the place: $BOB_ASK_PLACE when set,
else this machine’s id in machines.json, else its hostname.
Canonical shape
Section titled “Canonical shape”node ~/.claude/scripts/warp-drive/ask-gate.js file \ --title "TODO: {what's needed}" \ --label todo,warp-drive,p2-high \ --assignee paulirv \ --body-file - <<'EOF'{One sentence: what is blocked and why.}
Kind: {decide | do | check}Minutes: {how long it takes the operator}Place: {a fleet machine id | phone}Tried: {what BoB attempted before asking}
Why a human is needed: {login | decision | vendor UI | operator data | permission} — {detail}.
## Instructions1. {Self-contained step.}2. {Self-contained step.}
Blocked by #NNEOFThe label list carries the priority: one of p1-critical, p2-high, p3-medium, p4-low.
Non-conformance → needs-clarification
Section titled “Non-conformance → needs-clarification”A todo that is missing instructions, an issue link or an admission field is not actionable. The
groom reconciliation pass adds the needs-clarification label to such todos
rather than treating them as ready work.
A needs-clarification todo is:
- excluded from “ready to complete” — it is not a candidate for the
completeddone-signal until it conforms, and downstream tooling (#779) must skip it; and - re-entered into a clarification loop — warp-drive’s next run surfaces
needs-clarificationtodos so the gap (missing steps or link) is filled, after which the label is removed and the todo rejoins the normal flow.
This keeps an under-specified handback from masquerading as an actionable item or as a resolvable blocker.
Exempting a todo from the stale sweep
Section titled “Exempting a todo from the stale sweep”The groom stale sweep (todo-consume.js stale, groom §5d)
flags a todo whose linked issues are all closed, on the reasoning that the work
it gated is gone. That is a proxy, and it is wrong whenever an issue closes for a
reason unrelated to the human action still pending — the todo is then a live action
item flagged as dead work.
Mark such a todo with a not-stale marker anywhere in its body:
<!-- not-stale: 2026-08-17 outage is still live; #466 closed for unrelated reasons -->planStale() tests for the marker before the linked-issue-state test and before
the stale label check, and reports the todo as pinned-not-stale. The sweep prints
an EXEMPT #NN line naming the date and reason, so an exemption is visible in the
sweep output rather than a silent skip.
Shape: <!-- not-stale: YYYY-MM-DD reason --> — an ISO date, then a
one-line reason. Both are for the human reading the body later; the parser is
deliberately permissive, so a malformed or undated marker still exempts the todo. A
safety valve must not be disarmed by a typo.
When to use it. This is a deliberate, dated operator override, not a convenience — it suppresses a signal designed to stop dead todos accumulating. Use it when the human action is genuinely still required and the link-state heuristic cannot see that. Record why, and date it, so a later reader can judge whether the override still holds.
Why a marker and not a label. It is body-resident, so it survives label churn:
removing the stale label by hand does not restore re-flag eligibility, which was
the defect in #1663. It also
matches the convention the warp-drive scripts already use — <!-- approved-at … -->,
<!-- gated: … -->, <!-- flightplan … -->.
The sweep remains surface-only either way: it flags for a human to close, and closes nothing itself.
Age floor and likely-completed (#2034)
Section titled “Age floor and likely-completed (#2034)”“Every link is closed” describes two opposite situations, and the sweep must tell them apart:
- The gated work was cancelled or became moot — truly stale.
- The human performed the action, the gated work then finished, and only
the
completedlabel was never applied.
#1999 was the second case:
a p1 todo filed at 00:45Z, its three links closed between 01:00Z and 05:00Z
because the restore was done, and the sweep labelled it stale at 05:22Z —
4.5 hours old, mid-action. That label contradicted the lifecycle contract above
(only completed is a resume signal) by pushing a done todo toward a
close-without-completed. Two guards now sit after the link-state test:
- Age floor. A todo younger than
_workflow.todo_stale_after_days(default 7, declared once inWORKFLOW_DEFAULTS;0disables) is never flagged — reported astoo-young. The sweep’s real target is age, which it did not measure: the 2026-09-10 review found 10 of 14 open todos older than 30 days. - Ordering. When every link closed after the todo was created and at
least one of them references the todo (body or comment), the verdict is
likely-completed: the sweep printsLIKELY-COMPLETED #NNwith the hint addcompletedif you did this and labels nothing — only the human may saycompleted. A reference check that was not performed counts as a reference (the safe direction).staleis kept for links that were already closed when the todo was filed (all-links-closed-at-creation) and for links that closed afterwards without any reference to the todo (links-closed-without-reference).
The summary line names likely-completed todos separately (N todo(s) look completed) and the sweep exits 3 for them, as it does for anything else a
human should look at. The sweep needs createdAt on the todo and closedAt on
each link to judge ordering; without them the pre-#2034 all-closed verdict stands.
What the sweep could not check
Section titled “What the sweep could not check”A ref the sweep failed to read is not a ref it read and found open. Conflating the two is #1724: during the 2026-08-17 API wobble nine lookups returned HTTP 503, each collapsed silently into “not stale”, and the sweep signed off with “no stale todos — every open todo has at least one open linked issue”. It had not read most of the refs it was speaking for.
The sweep is forge health’s first consumer, and now reports what it could not establish:
- A todo whose refs could not all be read prints as
UNCHECKED #NN, naming the unreadable refs, distinct from a todo that was read and found to have open links. - Such a todo is never flagged stale — including when the readable subset is all closed. The old code flagged that case, which is a false positive, not merely a silent one: the unread ref could be wide open.
- The closing line states how many todos could not be fully checked instead of
asserting every link is open, and names the forge condition when it is not
operational. - An incomplete sweep exits
3— “there is something for a human to look at” — just as a flagged todo does.
An empty result may only be reported as an empty result when the lookups behind it succeeded.
Relationship to the completed lifecycle
Section titled “Relationship to the completed lifecycle”The format contract governs a todo’s birth; the completed label governs its
death. A conforming todo, once the human performs the action, gets completed
(the canonical done-signal) and downstream work resumes. A todo closed without
completed is cancelled/moot. See CLAUDE.md → Work Tracking → TODO lifecycle.
Roll-ups and why a human (#3161)
Section titled “Roll-ups and why a human (#3161)”One cause is one todo, wherever it shows up:
- Search before filing. When an open todo already covers the cause, comment on it. A second todo for the same cause is noise.
- Roll up a fan-out. A cause that spans several projects or steps (the same
gate in every project, one feature split into steps) is ONE checklist todo,
with one checkbox per project or step. It is filed in the repo that owns the
cause: a machine or BoB condition in bigbrain, never a copy in each project.
The promotion mirror gate (
scripts/setup-promotion/human-gate.js) works this way: one open roll-up in bigbrain, a row per project, and ticking a project’s row clears that project’s gate (completedon the roll-up clears them all). - Say why a human is needed. Every todo carries the element-4 line above. The
reasons are closed:
login,decision,vendor UI,operator data,permission. A todo none of them fits is work the agent does itself.
One cause, one todo: the preflight filer (#3161)
Section titled “One cause, one todo: the preflight filer (#3161)”The preflight gate (scripts/warp-drive/host-preflight.js) files one todo per
cause, not per run and not per project.
- Fingerprint. A cause is one failing check on one machine. Its fingerprint
is
preflight:<machine>:<check>, e.g.preflight:farm-01:host:surfaces, carried in the body as<!-- warp-preflight: … -->. The other checks that failed in the same run are not part of it, sohost:surfacesfailing alone and failing alongsideproject:labelsis the same todo. - Where it is filed. A machine condition (
host,host:*) is filed in bigbrain only, never in a project repo, whichever project’s run tripped it. A project condition (project:*) is filed in that project’s repo. The bigbrain repo is read from the BOB_SOURCE checkout’s remote (BOB_BIGBRAIN_REPOoverrides it). - Recurrence. When a todo with the fingerprint is open, the gate comments
on it. When one was closed in the last 14 days, the gate reopens it with a
comment if it was closed without
completed, or comments that the fix did not hold if it was closed withcompleted(a reopenedcompletedtodo would be consumed again at once). In neither case is a second todo created. A cause last closed more than 14 days ago is filed afresh. When the tracker cannot be read, nothing is filed: a missing todo is safer than a duplicate. - Auto-resolve. When a later gate on the same machine finds a cause
passing, it closes that cause’s open todo with a comment (“Resolved
automatically …”) and never adds
completed: nobody acted, the condition went away. Only a scope the gate actually checked is resolved (a host-only re-check never closes a project todo), only this machine’s todos, and only per-cause fingerprints. A todo from before #3161 (a set hash) is left alone. - Why a human. Each preflight todo says why the run could not act itself:
the gate may not repair the machine or project it grades (#1923), and
scripts/deploy.shrefuses while a Claude Code session is live. - The old todos. Todos the earlier filer wrote (one per project and per set
of failing checks) were closed once, without
completed, byscripts/warp-drive/preflight-todo-cleanup.js --apply. It searches the owner’s repos for opentodoissues titled “TODO: warp-drive preflight blocked on …” or carrying a set-hash marker, and leaves per-cause andcompletedtodos alone. It is a dry run without--applyand can be re-run safely.
The human handoff record (#3058)
Section titled “The human handoff record (#3058)”A todo holds the full copy-paste steps. The handoff record is how Paul
finds out a todo exists at the moment a run stops for it. Whenever a run stops
for a human action, the engine writes one record per todo to
.claude/.warp-handoff.json (warp-handoff/1, written by
scripts/warp-drive/handoff.js). The phone push, the bob viz and warp viz
banners and the terminal handoff all read that record. No surface re-derives
or paraphrases it.
| Stop | Raised by |
|---|---|
preflight: the preflight gate blocks the run |
the engine, on preflight_failed and on a failed mid-session re-check |
skipped: an item is skipped as blocked |
the engine, on work_skipped; the gating todo is data.todo, else the first #NN in the reason. When that issue is not a todo (another req, bug or task), no record is written and no push is sent, because another run owns that work. The transition reports the item as waiting on other work, and the session summary lists it with the skipped items (#3209). |
budget: a circuit-breaker checkpoint |
the engine, on entry to budget_exceeded (no todo; the record lasts while the run waits there) |
decision-timeout: a decision timed out |
the agent, after filing the todo: handoff.js record --kind decision-timeout --todo <NN> |
plan-drained: a flight plan is drained on a todo |
the engine, when a kickoff is refused as drained; the agent, when the session-end reconcile reports drained: handoff.js plan <N> |
Each record carries:
what: one plain-English line saying what Paul needs to do. By default it is the todo’s own summary sentence, which is why that sentence must read as a standalone instruction.why: what is waiting on it.command: the command to copy, taken from the preflight fix or the first fenced code block under the todo’s## Instructions. It is never taken from prose, so afile:linereference cannot become a command (#3209).todo: the link to the todo.text:whatwith the todo appended as a trailing link, e.g.Deploy BoB on farm-01 so the forge plan can run ([#3057](/reference/…)).
Beyond the attention budget (#3805). A record whose todo carries the
ask-waiting label is stored with waiting: true. No push is sent for it, and
the bob viz and warp viz banners and the terminal handoff leave it out; the
terminal handoff ends with one count line, (N parked asks not shown: labelled ask-waiting), so a parked ask is never hidden silently (#4202). handoff.js sweep reads the
label again: when it is gone the record is released and its push is sent,
once. See Asks that wait.
Clear English. The text names the task and the project in words. An issue
number may appear only as a trailing link in parentheses. A bare #NN, or a
text that leads with a number, fails clearEnglishViolations(), which
tests/test-handoff.js runs over every stop kind. You can run the same check
from a shell with handoff.js check "<text>".
Phone push. Each record key is pushed once. Inside a Claude Code session
the push goes through the Claude Code app: the engine hands the agent the text
for its PushNotification tool, and the agent confirms with
handoff.js pushed <key>. From a shell (bob tick, bob warp start) the push
goes through bob notify. The push is sent whether or not RDB is
on. A missing channel is recorded as no-channel and shown in the terminal
handoff and the session summary. It is never skipped silently. Test runs never
push: the suite runner exports BOB_TEST=1, and WARP_HANDOFF_PUSH=0 turns
pushes off anywhere.
The push ends with one link, kept on the record as push.link. For a record
with a todo it opens the todo’s
Ask screen on BoB Control, at an
address the phone can reach on the tailnet. When no dashboard server is
reachable from the tailnet, the link is the todo’s issue.
Terminal handoff. handoff.js terminal prints the open records as a short
numbered list of plain-English actions, each with a Why line and a Run line.
When a run blocks or ends for a human, its last message is this list.
Clearing. A record is cleared once its todo is closed or carries
completed. A record with no todo clears once its phase has moved on
(handoff.js sweep). The banners check the todo’s state on every refresh, so
a banner clears within one refresh of the todo getting completed. To remove
one stale record by hand, run handoff.js clear <key> (the key is shown by
handoff.js list --json). It exits 2 when there is no such record (#3343).
An item already satisfied raises nothing (#3343). If an item’s criteria
are already true at planning, the run verifies them, closes the issue with the
evidence, and leaves with work_skipped --data '{"satisfied":true,…}'. The
item is counted as resolved (session.completed_issues, satisfied_issues),
not as skipped, and no handoff is written.