Warp-drive internals
Warp-drive internals
Section titled “Warp-drive internals”The Warp-Drive Guide is how to run the loop. This page is how the loop works: the phase machine and its overlays, the hooks and circuit breakers around it, how scope and discovery decide what a run may touch, what the preflight gate grades, what the bob warp status / bob warp viz monitor shows and why, how integration-branch streams and session segmentation keep long runs honest, and the _workflow configuration that tunes it. Every state-machine subcommand and flag is in the generated flag reference; the state file, CLI actions and transition table are in the state reference; why the phase table is frozen is the warp-drive contract.
Where the pieces live
Section titled “Where the pieces live”Warp Drive is part of BoB (Big ol’ Brain) at ~/.claude/. The key files:
~/.claude/├── bin/warp # alias shim → bob warp <verb> (#2060)├── scripts/cli/warp # the CLI engine├── commands/warp-drive.md # /warp-drive slash command├── commands/dev-up.md # /dev-up slash command├── skills/stop-warp-drive/SKILL.md # /stop-warp-drive skill├── scripts/warp-drive/state-machine.js # State machine engine├── scripts/dev-lifecycle/ # Dev environment IaC scripts│ ├── dev-up.sh # Full lifecycle orchestrator│ ├── health-check.sh # Lightweight health probe│ ├── provision-users.sh # Test user provisioning│ └── check-seed-coverage.sh # Seed data coverage gate├── hooks/warp-drive-gate.sh # Blocks commits outside committing phase├── hooks/warp-drive-inject.sh # Resumes state on session start├── hooks/warp-drive-edit-tracker.sh # Doom loop detection├── hooks/warp-drive-pre-exit.sh # Self-verification before phase advance├── hooks/warp-drive-stop.sh # Blocks exit when report is pending├── templates/dev.json # Dev manifest template├── templates/seed/ # Seed data templates└── docs/how-to/warp-drive.md # The operator guideThe state machine
Section titled “The state machine”Warp-drive is a phase-based state machine backed by a persistent JSON file at .claude/.warp-drive-state.json in your project. Hooks enforce phase ordering — commits are blocked outside the committing phase, session exit is blocked without a report.
Phase Flow
Section titled “Phase Flow”prerequisites → discovering → planning → chunking ↓ coding → updating_docs → testing → committing → reporting ↑ ↓ └──────────── next chunk ←──── chunk_complete ─────┘ ↓ (all done) requirement_complete → qa → merging │ ↑ ↓ bugs found ───┘ └─ pass/skip (file + fix in-session, loop back to coding) awaiting_continue ↙ ↘ discovering session_ending (next task) (done)
─── Circuit-breaker checkpoint ────────────────────────────── any phase ──(phase_timeout | ┌─ budget_continue → interrupted phase retry_exceeded | │ coding_cycles_exceeded | │ (resume work where it halted; no_progress | │ falls back to coding) qa_cycles_exceeded | │ total_chunks_exceeded | │ session_timeout)──→ budget_exceeded ─┤ │ └─ budget_abort → aborted (give up & clean up) ─────────────────────────────────────────────────────────────The state machine routes any phase into budget_exceeded when an enforced budget trips (see Configuration for the trigger reasons). From budget_exceeded, a budget_continue event resumes at the exact phase the breaker interrupted (captured in budgets.exceeded_from_phase — #1183; falls back to coding for older state files), and each approval extends the chunk cap by 10 (#1149); budget_abort moves to aborted.
The total_chunks_exceeded breaker gates starting new chunk work only (next_chunk, chunks_defined, and the like): wrap-up transitions that land or close work already in flight (merge_ready, qa_passed/qa_skipped, merged and its retries, promoted/promote_skipped/promote_failed, continue_no) are exempt (#1183), mirroring the qa-cap rule — so a session at the cap can always merge, promote, and end cleanly. The cost/token and session-time ceilings are not exempted; they halt everything. The aborted phase has its own cleanup events: abort_resolved (cleanup OK → completed), abort_cleanup_failed (cleanup failed → completed anyway), and restart (back to idle).
Phase Details
Section titled “Phase Details”| Phase | What Happens | Advances When |
|---|---|---|
| idle | No run in flight — a state file exists but nothing is being worked | start (kickoff) |
| prerequisites | Checks automation level (2+), branch (not main), test command; runs the baseline-docs check (greenfield README/runbook/CLAUDE.md gap → unapproved req, non-blocking) |
All checks pass |
| preflight | The engine grades this machine (bob doctor, #1921) and this project (bob ready --dry-run, #1922) via state-machine.js preflight — non-mutating; --converge applies the idempotent bob ready steps first, then re-checks. A failure files one todo per cause — machine + failing check, a host cause in bigbrain, a project cause in the project repo — or comments on the one already there; a later pass closes it (#3161). preflight_ok / preflight_failed are engine-fired only — a hand-fired transition is refused (see Preflight gate) |
Engine decides: pass → discovering, fail → blocked |
| blocked | Held on a failed preflight (at kickoff, or — on BOB_SOURCE only, #3160 — a mid-session host re-check at next_chunk / continue_yes). The coding loop is unreachable; nothing is compensated by hand — the todo carries the remediation |
preflight_retry re-runs the gate (a pass resumes the interrupted phase); blocked_end ends the run cleanly |
| discovering | Finds work via the fingerprint-cached queue (discovery-cache.js queue → discover-queue.sh); an unchanged actionable set reuses the cached order (see Discovery caching) |
Issue selected |
| planning | Reads issue, drafts approach if complex (>3 ACs or architecture decisions); a blocked item (no agent-side work) is skipped state-only via work_skipped → discovering (see Skipping blocked items). Entered across a cap/area seam it runs as the revalidating sub-mode first (see Per-Area Re-Validation) |
Plan ready (or item skipped) |
| chunking | Splits work into commit-sized pieces — at most _workflow.max_acs_per_commit ACs each (default 3), resolved by the engine and substituted into the phase instruction (#2740); a tracer item gets the vertical-slice tiebreaker (#2736) |
Chunks defined |
| coding | Implements the current chunk’s acceptance criteria | Code complete |
| updating_docs | Checks off completed ACs in the GitHub Issue | Hook auto-detects the AC tick or a docs/ edit; with nothing to tick (no unchecked box on the issue) the agent fires docs_updated itself and the engine accepts it without recording a hook miss (#1856); the hook records what it evaluated on last_bash_eval with a verdict, and a fire the engine cannot verify (forge unreachable) is accepted without a defect record unless that verdict says the hook decided to advance (#2169) |
| testing | Runs the project’s declared checks scoped to the change (#1979): tier chunk, path-matched against the changed files, under testing_budget_minutes; every skipped check is recorded with its reason and surfaces as the commit’s Tests-Skipped: trailer and a “Checks” section in the chunk report. A failing check is re-run once on the unmodified base: the same failure there is quarantined (skipped: quarantined, a fingerprinted bug filed/bumped on the project repo, never blocks) — re-runs capped at 2 per chunk. No checks block → the detected test command, as before. Retries up to 3 alternatives on failure |
Every ran check passes |
| committing | Creates conventional commit; hook auto-detects | Commit created |
| doc_drift_check (opt-in) | Runs /doc-sync (mechanical fixes) and /doc-audit --fail-on-drift after the commit. Active when _workflow.doc_drift_gate=true. |
drift_clean → reporting / drift_blocked → coding |
| reporting | Files chunk report as GitHub Issue; adds an “Iteration trail” section when the chunk took >1 verify attempt (#589) | Report filed |
| chunk_complete | Checks if more chunks remain | Next chunk or done |
| requirement_complete | Final test, close issue via PR | Merge ready |
| qa (#868) | Runs Playwright E2E (webapp-testing) against the dev-up app; records a passing run as “fully functioning” evidence before promotion. Proportionate by default (qa_scope, #1979): on entry the engine classifies the item’s changed files and a presentation-only change (templates, styles, config, copy) skips with reason presentation-only unless an AC names browser verification; never regenerates visual baselines. Runs the declared requirement-tier checks first (run-checks.js --tier requirement) — release-tier checks never run inside warp-drive. Bugs found are filed as bug issues and fixed in-session (reproduce → regression test → fix), then QA re-runs. Clean no-op skip when there is no dev/E2E surface. Bounded by max_qa_cycles (default 3) → budget_exceeded. |
qa_passed/qa_skipped → merging; qa_bugs_found → coding |
| merging | L2: push + PR / L3: rebase + push master — per the promotion matrix (promotion.js decide). Unattended (_workflow.unattended, #2053): push + PR at every level, then merge-on-green.sh fast-forwards master only when CI is green; red/timeout leaves the PR open and is recorded on state.merge_on_green. Forge degraded (#1751): entering merging in degraded mode, or merge_failed with forge_park, parks the run here (display parked, recorded on state.forge.parked); merged / merge_failed / push_failed are refused while parked, and state-machine.js forge-check releases the park once the forge is operational |
Merged |
| promoting | Deploys to the project’s promotion ceiling through the shared gate (promotion.js decide → deploy.sh); an external/pr ceiling is a clean no-op |
promoted/promote_skipped → awaiting_continue; promote_failed retries |
| awaiting_continue | Asks: “Continue to next task?” | User responds |
| session_ending | Files session summary, notifies, cleans up state | Done |
| budget_exceeded | Mandatory human checkpoint reached by a tripped breaker (cost, tokens, chunk cap, QA cycles, no-progress stall) — see Cost Budget | budget_continue resumes at the interrupted phase; budget_abort → aborted |
| aborted | Run aborted; cleanup runs from here. Reachable from any phase | abort_resolved/abort_cleanup_failed → completed; restart → idle |
| completed | Terminal — the run is over and the state file no longer drives a loop | Nothing; a new kickoff starts a fresh run |
Where phase instructions come from (#2740)
Section titled “Where phase instructions come from (#2740)”The text above is a summary. The instructions an agent actually receives on entering a phase are a single string per phase, and there is exactly one declaration of them:
| Source | Role |
|---|---|
PHASE_INSTRUCTIONS (scripts/warp-drive/state-machine.js) |
Canonical. Injected into agent context on every transition — it is what drives behaviour. Rendered through one function, renderPhaseInstruction, which substitutes every placeholder ($PROJECT_ROOT, $GITHUB_USER, $ISSUE_NUM, $MAX_ACS) so a placeholder cannot reach the agent resolved at one call site and raw at another. |
commands/warp-drive.md — the ### <phase> sections |
Operative derived copy. The command text the agent has loaded: the same loop with the rationale, worked examples and cross-links the injected string has no room for. |
| The Phase Details table above | Reference derived copy. One row per phase, for a reader orienting in the machine. |
A derived copy may elaborate freely and may not contradict. make check enforces that (scripts/checks/check-phase-instructions.js, decision #2757) across all 22 phases, comparing only the claims a reader acts on — which phases exist, the exits each phase tells you to fire, and who fires them ([explicit] / [hook-detected] / [engine-fired]). Wording, depth and ordering are never compared; another phase’s event named as context is not a claim. A deliberate difference is recorded with its reason in scripts/checks/phase-instruction-accepted.json, and an entry that stops diverging fails as stale-acceptance so the record cannot become an exemption list.
Before that gate existed, an edit to the command text alone looked done and changed nothing at runtime: chunking hardcoded max 3 ACs while the command file called the number configurable, and four phases of the loop — doc_drift_check, budget_exceeded, aborted, completed — had no section in the operative copy at all.
Phase overlays
Section titled “Phase overlays”Overlays change a phase in place rather than adding one, so the topology stays one graph. Three relabel a node, and one only adds to its instruction:
- Bug work — when the selected item carries the
buglabel the loop reuses the same phases, relabelled:codingshows asbug_fixing,testingasregression_gate,requirement_completeasbug_verified. The semantics that come with the relabel are the bug contract — reproduce the failure first, root-cause it, and land a regression test that failed pre-fix and passes post-fix. The tooltip names both the overlay label and the underlying phase. - Task work (#1959) — when the selected item carries the
tasklabel (maintenance: a dependency bump, CVE update, upgrade, migration or cleanup) the same phases read astask_executing(coding),verifying(testing) andtask_complete(requirement_complete). A task has no acceptance criteria; its definition of done is the issue’s## Verificationchecklist. The loop performs the change, runs every declared step (a failing step is a failed verification, not a pass), andrequirement_doneis refused while any step is unchecked.bob warp statusand the tooltip show the task labels. - Vertical slice (#2736) — when the selected item carries the
tracerlabel (greenfield or cross-layer work, #2738),chunkinggets a tiebreaker addendum: prefer a vertical slice over logical relatedness, keep the issue’s AC order, and never regroup by layer. Chunk 1 is the tracer slice. Themax_acs_per_commitcap is resolved in the text: a tracer AC-01 covers the whole path as one AC and fits the cap; an item with no tracer AC builds chunk 1 from the fewest ACs that run end to end, and that chunk alone is exempt. It is a preference, never a gate. Unlike the bug and task overlays it relabels nothing — the phase keeps its name and only the instruction grows._workflow.vertical_slice_first: falseswitches it off and leaves the instruction byte-identical.check-phase-instructionsreads every overlay (PHASE_OVERLAYS: bug, task, slice), so an exit an addendum names must be documented like one the base instruction names. revalidating—planningentered across a cap/area seam runs the freshness re-check first (see Per-Area Re-Validation) and shows asrevalidatinguntil the pass is recorded.
Automation Levels in Practice
Section titled “Automation Levels in Practice”| Behavior | Level 2 | Level 3 |
|---|---|---|
| Branch creation | You create via /start-work |
Auto-creates feature/issue-NN-title |
| Merge strategy | Creates PR for review | Direct merge to master |
| Decisions | Asks for architecture, scope, ambiguity | Same |
| Continue prompt | “Continue to next task?” | Same |
| File operations | Auto-approved | Auto-approved |
| Destructive git | Blocked (force push, reset –hard) | Blocked (force push, reset –hard) |
The shared machine engine (#2975)
Section titled “The shared machine engine (#2975)”warp-drive is not the only state machine a checkout can carry. bob decompose (#2973) and bob do (#2974) are sibling machines with their own phases, built on scripts/warp-drive/machine-engine.js rather than on a second copy of warp-drive’s engine. The module holds only the mechanics every machine needs:
- The state-file discipline — atomic tmp-and-rename writes under the
<state>.lockflock the hooks share, and a fail-open read of a corrupt file. warp-drive’s ownreadState/writeStatedelegate to it, as does its liveness rule (isStale, #1363). - Transition validation against the machine’s own
(phases, transitions)table, through the sameinit/transition/statusverbs. - Evidence-fired advancement — the declared-behaviours pattern (#1834) generalised. A machine declares
evidence: [{phase, event, source}]; a baretransitionof such an event is refused, and only the observer that saw the artifact (exportingMACHINE_EVIDENCE_SOURCE) fires it. The source is written to the machine’s history. - The concurrency rule — declared once, on each definition, as
concurrent_with_warp: true | false(there is no default). It is enforced atinit, before anything is written:
| Machine | concurrent_with_warp |
Why |
|---|---|---|
bob decompose |
true |
It writes issues, not files — it cannot collide with a coding chunk. It starts beside an active run and records that run (co_running_warp: issue, phase, pid) on its own state. |
bob do |
false |
It writes files in the working tree. It refuses to start while .claude/.warp-drive-state.json describes an active run (not completed, not stale), and the refusal names that run’s issue and phase. The rule holds the other way too (#2979): warp-drive’s init exits 3 and bob warp start exits 5 while a do run is active, naming its question and phase. See The bob do machine. |
warp-drive’s phase list and transition table are not part of the module: they stay in state-machine.js under the frozen phase-contract/1 (#2042), and the extraction left that contract byte-identical. A sibling machine cannot claim warp-drive’s state file. Its state files are declared as session state beside warp-drive’s (see Session state files). Coverage: tests/test-machine-engine.js, with a fixture machine that shares no phase name with warp-drive.
Hooks and safety rails
Section titled “Hooks and safety rails”Hook Registration
Section titled “Hook Registration”The warp-drive hooks are registered in ~/.claude/settings.json automatically. If you’re setting up from scratch, the hooks need entries in:
| Hook Type | File | Purpose |
|---|---|---|
| SessionStart | warp-drive-inject.sh |
Resume from last phase, for the session that owns the run (#2275) |
| PreToolUse (Bash) | warp-drive-gate.sh |
Block commits outside committing phase, in the repo the commit runs in, for the owning session only (#2275) |
| PreToolUse (Bash) | warp-drive-pre-exit.sh |
Self-verification before phase advance |
| PostToolUse (Edit|Write) | warp-drive-edit-tracker.sh |
Track edits, detect doom loops |
| PostToolUse (Edit|Write|Bash) | warp-drive-docs-detector.sh |
Auto-detect the AC tick (gh issue edit <req> --body…) or a doc edit → advance state (#1852) |
| PostToolUse (Bash) | warp-drive-commit-detector.sh |
Auto-detect git commit → advance state |
| Stop | warp-drive-stop.sh |
Block exit when report is pending (owning session only, #2275) |
The state file sits in a shared checkout, so every Claude Code session whose cwd is that checkout can see a run. Ownership (#2275) is what keeps the guards on the run’s own session. session.session_id records the owner, and each hook passes its payload’s session_id. A second session (a groom, a hotfix, a commit from its own worktree) is not gated, is not blocked from stopping, and cannot take the run over on SessionStart. It gets a one-line advisory naming the live run instead. A run whose owner has died is re-claimed by whichever session drives it next. That happens at SessionStart, or at that session’s first transition when it started in another checkout, for example a worktree run resumed from the main checkout (#3305). See session.session_id.
Every auto-advance and gate in this table is a declared behaviour with a stable fingerprint — see Declared automatic behaviours; state-machine.js declared "$(pwd)" reports whether each one is present and wired on this machine. The phase table itself is frozen behind a fixture until 2026-10-11 — see Warp-drive contract (#2042).
If hooks aren’t registered, warp-drive still works — you just lose the safety rails (commits could happen at the wrong time, doom loops won’t be caught, auto-detection won’t fire, etc.).
If the phase stays at committing after a successful commit, the hook did not fire. Hooks run from the deployed copy in ~/.claude/hooks/, so a fix merged to BOB_SOURCE is inert until it is deployed. On a self-hosted run the redeploy route does that at the next merged boundary, handing off to a segment that deploys after this session exits (#2472). If the redeploy is refused (another live session, a dirty tree), the fix stays inert until scripts/deploy.sh runs from a plain terminal; the detector also refuses to advance while HEAD still equals the sha recorded at tests_passed (a commit a commit-msg hook rejected), and on a fresh repo with no commits — where nothing could be recorded — it refuses until HEAD exists at all, so a commit that failed with “nothing added to commit” behind a masked exit status (| tail) no longer advances the phase (#1955). The gate judges by the landed commit, never by the whole Bash call’s exit status: git commit … && git push with a rejected push exits non-zero, yet the commit landed, so the hook advances (#1897). Check git log -1 first, then state-machine.js status | jq .last_bash_eval.verdict — the hook records why it decided on every evaluated call: advanced, head-unmoved (nothing landed), call-failed (the call failed and HEAD moved by something other than one new commit on the recorded sha), or not-a-commit (prose, data, or a non-commit command). Firing transition … committed by hand is a recovery step, not the normal path; tests/test-commit-gate-detect.sh covers the detector against the real payload shape.
Self-Verification
Section titled “Self-Verification”Before advancing past key phases (code_complete, tests_passed, requirement_done), a hook injects a verification prompt forcing Claude to re-read the original acceptance criteria and confirm each one is genuinely satisfied — not just that code exists.
Doom Loop Detection
Section titled “Doom Loop Detection”A PostToolUse hook tracks how many times each file is edited during a phase. If any file exceeds the threshold (default: 5 edits), a warning is injected. This catches infinite edit cycles where Claude keeps tweaking the same file without making progress.
No-Progress / Stall Detection
Section titled “No-Progress / Stall Detection”Distinct from the raw retry caps, the state machine watches for stagnation on the coding ↔ testing and coding ↔ doc_drift_check loops (#267). On every tests_failed / drift_blocked it computes two signatures:
- a diff signature — a hash of
git diff HEADplus the untracked-file list (captures whether any code changed since the last attempt), and - a failure signature — a hash of the short failure string passed via
--data '{"failure":"…"}'(the failing test name or first error line).
A per-chunk stall counter increments only when both are unchanged since the previous attempt — i.e. the same failure with no new diff. Any change to either (different error, or the diff moved) is treated as legitimate slow progress and resets the counter to zero. This is the key distinction the raw coding_cycles count can’t make: it halts on stagnation, not on attempts.
When the counter reaches max_no_progress (default 3), an enforced no_progress budget issue trips the circuit breaker into budget_exceeded. At Level 3 this is a mandatory human checkpoint — the loop will not spin on an unsolvable chunk burning budget; it stops and asks. budget_continue clears the guard and resumes; budget_abort cleans up. The counter also resets whenever a new chunk begins or the loop makes real headway (tests_passed, drift_clean).
Always pass the failure signature on tests_failed/drift_blocked; without it the detector falls back to the diff signature alone (still catches “no code changed”, just less precisely).
Cost Budget
Section titled “Cost Budget”The circuit breaker also enforces a spend ceiling (#587). Token usage is tracked per chunk and per session; checkBudgets() compares the cumulative session total against max_session_tokens and, when crossed, emits an enforced cost_budget_exceeded issue that routes to budget_exceeded — the same mandatory Level-3 checkpoint as the other hard limits. This closes the gap where a runaway loop could burn budget while staying under every count-based cap.
- Token count is the primary signal. The check reads
token_usage.session_totalwhen captured, else the running sum ofchunk_snapshots, so it trips mid-session on accumulated spend rather than only at session end. max_session_usdis optional/secondary — an estimated-dollar ceiling via token-report’s shared cost model, which prices each record at its model’s rate from a dated table (PRICING_AS_OF, #1805). Prefer the token ceiling; the dollar estimate is best-effort, and it is enforced only when qualified — a record with no priced model is an upper bound that surfaces an advisorycost_budget_unqualifiedwarning and never halts the run.- Disabled by default. Both ceilings default to
0(off), so existing sessions are unaffected unless you configure them under_workflowinsettings.local.json. - The
budget_exceededcheckpoint reports actual spend vs ceiling (e.g.cost_budget_exceeded: 1500 tokens exceeded 1000 tokens) and offers the usualbudget_continue(extend) /budget_abortchoice.
Kickoff scope
Section titled “Kickoff scope”The kickoff argument defines the run’s scope, not just its first work item (#992):
/warp-drive 42(or--issue 42,43) authorizes only the listed issue(s). When they are done, the run ends cleanly — it does not discover the next approved issue, and it does not segment into a fresh self-spawned session. Completed issues are recorded insession.completed_issues, so even a resumed segment of a multi-issue scoped run works only what remains./warp-drive --area billingauthorizes the workstream’s approved queue./warp-drive(unscoped) authorizes the full approved queue — the run continues until the queue is empty or a breaker trips./warp-drive --flightplan <N>authorizes the ordered items of a persisted flight-plan issue — it consumes them in order, never widens, and ticks each off as its requirement completes. This is the most explicit scope and wins over--issueand--area. A plan may contain gating human-action todos (#1088):flightplan.js remainingreturns only runnable work, so a req gated by a pending todo is skipped, not failed — the run proceeds to non-gated items and the gated req re-enters once the todo carries the canonicalcompletedlabel (a todo closed withoutcompletednever releases; the gated work is moot). Released todos are reconcile-ticked in the plan issue automatically. Ordered plans stop at the gate instead of skipping it (#1415): when the gated item is labelledserial-only, it is a barrier — every later item iswithheldtoo, so a run can never execute downstream of a human checkpoint it has not cleared. Withheld items name the barrier holding them rather than a todo of their own, and clearing that one gate frees the chain down to the next barrier in a single step. A plan with noserial-onlyitems keeps skip semantics; a<!-- flightplan:gate-mode barrier|skip -->marker in the plan body forces either mode. Ifremainingcomes back empty whilestatusstill showsgated,withheld, ormootitems, the run ends cleanly — report gated work as waiting on its named todos, withheld work as blocked behind its barrier, andmootitems as cancelled, in the session summary rather than as failures.
Run-boundary plan maintenance (#1667). A flightplan-scoped run maintains its plan’s lifecycle at both natural boundaries, using the #1666 engine — never a re-implementation. Kickoff: init runs flightplan.js lifecycle <N> before consuming the plan. stale (scope drifted since build) is refused with the exact build refresh command at L1/L2, and auto-refreshed via the same idempotent build path at L3+ (logged as plan_lifecycle in the init response); drained is refused with its gate(s) named — parity with a fully-gated remaining result; exhausted is refused as already-complete and closed via the guarded reconcile path. The gate fails open if the engine or gh is unreachable. Session end: the run converges the plan it consumed with flightplan.js reconcile --apply --plan <N> — an exhausted plan is closed with the engine’s dated final summary (a plan with an unchecked open item is never closed), a drained plan is stamped with its gates named and left open, a stale plan is reported with its refresh command. Both boundaries are conditional on session.kickoff.flightplan — unscoped, --issue, and --area runs are untouched.
Flight-plan provision overlay (#1133). A flightplan-scoped run can carry the plan’s registry items for the duration of the session without editing the project’s base manifest. At prerequisites the run applies a plan-scoped overlay — bob provision overlay apply --plan <N> derives the items from the plan’s issues via scripts/orchestrator/plan-scope.js (labels, area:* labels, stack signals in title/body) and links them, recording state at .claude/.provision-overlay.json. While the overlay is active, bob provision refresh keeps its links (union with the manifest) and bob provision status surfaces it first. At session_ending the run restores the base manifest with bob provision overlay restore (overlay-only links removed; items also in the manifest stay). Crash safety: an interrupted run leaves the state file behind — the next session’s prerequisites (or any bob provision status) detects it, and bob provision overlay restore recovers it; restore is idempotent. Both transitions post a comment on the plan issue, so the lifecycle is visible from GitHub. Role default kit (#2228). The same step passes --default-kit, on every run, not only flightplan-scoped ones. It adds the kit the machine’s global manifest declares as kits.default (farm workers: bugfix) to the overlay. It is a no-op when the machine has no default or a kit is already applied, and a kit the machine forbids is never applied — see Session Kits.
A flight plan wins over an issue list, which wins over --area, when more than one is given. The scope rule lives in one place (kickoffScope() in scripts/warp-drive/kickoff.js) and is enforced at every point that could otherwise widen a run: the segmentation decision computes “more work” within scope, the discovering phase refuses to fall back to the queue on a scoped run, and the resume context handed to a fresh segment names the scope explicitly. bob warp status shows the reach on its Scope: line. See the Kickoff Flags reference.
Discovery caching
Section titled “Discovery caching”Discovery is fingerprint-cached (#1069): each cycle, scripts/warp-drive/discovery-cache.js queue makes one cheap gh issue list call and hashes the actionable set — the approved open req/bug issue numbers plus each one’s priority label and serial-only flag (the attributes that determine queue order). That 12-char hash is the invalidation signal:
- Unchanged fingerprint → the cached ordered queue (stored under
discovery_cachein.claude/.warp-drive-state.json) is reused as-is; no re-derivation, no extra discovery passes. On a multi-chunk run where nothing material changes, discovery after the first cycle is ~free. - Changed fingerprint — an issue was closed, opened, re-prioritized, newly
approved, or hadserial-onlyflipped — → the queue is re-derived throughdiscover-queue.sh(still the single ordering source, #517) and the cache refreshes. A newly-approved p1 that appears mid-run therefore invalidates immediately and is picked up exactly as before; the cache never trades freshness for tokens. Title edits and other order-irrelevant changes deliberately do not invalidate.
The cache is in-run session state only: it lives inside the warp-drive state file and is deleted with it at session end — never a persisted PM file. It applies to --area and full-queue runs; --issue and --flightplan runs have a fixed scope and never consult the live queue (the cache composes with a flight plan but is not required by it). Inspect or drop it with discovery-cache.js status|invalidate --cwd <project-root>.
Skipping blocked items (#1270)
Section titled “Skipping blocked items (#1270)”An approved item can turn out to have zero agent-side work remaining — every open AC is human-only, it carries the blocked label, or it is gated by an open todo (Blocked by #NN). Two mechanisms keep such items from wedging a run:
Queue exclusion (before selection). Blocked work never surfaces as runnable in the first place:
discover-queue.shenforces the whole membership rule —approved, thereq/bugtype filter, and theblockedexclusion — in one shared jq expression, so every consumer inherits it identically regardless of how the rows were fetched (#1794). Passing--label approvedtoghis a server-side narrowing optimisation on top of that, never the gate itself.discover-queue.sh(and everything built on it — warp-drive discovery,/what-next, the discovery cache) excludesblocked-labelled issues from the runnable queue. Removing the label restores the issue; a label flip also changes the discovery-cache fingerprint, so a cached queue can never serve a newly-blocked item.flightplan.js remainingexcludes plan items gated by any open todo — in-plan todo rows and off-plan todos whose body names the item in aBlocked by #NNline. The standardcompleteddone-signal releases the exclusion (an open todo labelledcompletedimposes no gate; one closed withcompletedleaves the open list). A gatedserial-onlyitem additionally acts as a barrier: the run order is truncated there and every later item is withheld (#1415) — see flightplan.md.
The skip path (after selection). If a blocked item still reaches planning (e.g. it was blocked after the queue was fetched, or the blockage is only visible in the issue body), the loop fires work_skipped instead of aborting: the item is recorded to state.skipped_issues as {issue, reason} and the run returns straight to discovering for the next item — state-only, with no commit and no prerequisites re-run. Skipped issues are excluded from re-discovery for the remainder of the run, so the loop can never re-select the same blocked item in a loop.
Skips are surfaced everywhere the run reports: bob warp status shows a Skipped: line (issue + reason, omitted when none), and the session summary includes a “Skipped (blocked)” section. The skip exit exists only at planning — blockage discovered mid-coding (after edits exist) stays on the error-escalation/todo path, which defines cleanup.
Preflight gate (#1923)
Section titled “Preflight gate (#1923)”The gate is a real phase — prerequisites → preflight → discovering | blocked — and the decision is made in the state machine, never in the prompt.
What it runs. state-machine.js preflight <root> [--converge] calls the engine scripts/warp-drive/host-preflight.js (also bob preflight), which reuses two existing verdicts verbatim:
| Check | Engine | Asks |
|---|---|---|
host |
bob doctor (scripts/checks/host-drift.js, #1921) |
Is this machine’s BOB_HOME what deploy.sh would produce — surfaces, symlinks, toolchain, settings hook wiring? |
project |
bob ready --dry-run (scripts/ready/ready.js, #1922) |
Is this project warp-drive-ready — .claude/ initialised, manifest links materialised, labels, port band, dev.json/checks.json valid? |
Non-mutating by default. --converge runs the real bob ready (every step is idempotent) first, then re-runs both checks — the fresh re-check decides, never the convergence run’s own optimism. --converge is kickoff-only (#1997): the engine refuses it once the run has committed a chunk or handed off a segment, the mid-session host re-checks never pass it, and neither it nor bob ready ever runs deploy.sh or settings-hooks-reconcile apply — a settings.json rewrite silences every hook in the running session, so warp-drive never calls those during a run (a grep-verified test pins it). The result is a warp-preflight/1 document: per-check verdict, every failure with its remediation command, a fingerprint over the failing set, and the todo it filed.
Pass. The engine fires preflight_ok → discovering. The verdict — machine id, per-check verdicts, timestamps — is recorded at state.preflight, shown by bob warp status (Preflight: line) and state-machine.js status (.preflight), and reported in the session summary.
Fail. The engine fires preflight_failed → blocked, after filing one todo (todo,warp-drive,p2-high, assigned to the caller) in the project repo whose body conforms to the todo format contract: a summary, one numbered step per failed check with its exact fix command, a closing re-check step, and a Blocked by #NN link to the in-scope issue. An open todo carrying the same fingerprint marker (<!-- warp-preflight: … -->) is bumped with a comment, never refiled — repeat runs of the same drift converge on one issue. RDB, when enabled, gets the [warp-drive] BLOCKED notification.
From blocked the coding loop is unreachable: the only exits are preflight_retry (back to preflight — run the gate again after the human acted; a pass resumes exactly where the run was) and blocked_end (→ session_ending, the summary reports the verdict). preflight_ok / preflight_failed are engine-fired only: a hand-fired transition … preflight_ok is refused with the command to run the gate. An engine that cannot run — a missing script, an unparseable answer — is verdict error and fails closed, never a pass.
Mid-session re-check. Drift does not only happen before a run. At next_chunk (every chunk boundary) and continue_yes (every requirement boundary, which is also where a resumed segment re-enters) the state machine re-runs the cheap host section (~150 ms). Outside BOB_SOURCE the re-check is report-only (#3160): the machine is not the project’s to fix, so a finding is recorded (blocking: false), named in the response as PREFLIGHT RE-CHECK — ADVISORY, files no todo and never refuses the transition. On BOB_SOURCE — the one run that executes what it is changing — a hard finding blocks the next chunk exactly the way a dev-health failure does: the transition is refused, the run moves to blocked, the todo is filed/bumped, and preflight_retry → preflight_ok returns it to the interrupted phase. Host findings are graded by the engine: BOB_HOME is measured against the commit it was deployed from, only a hard finding fails the check, and everything else — host:behind included — is an advisory on the record. Each re-check is recorded under state.preflight.rechecks. _workflow.preflight_recheck: false opts out of the re-check (the kickoff gate itself has no opt-out).
Self-hosted runs (#1982). When the project being worked is BOB_SOURCE (a warp-drive run on bigbrain itself), every chunk that commits to a deploy-managed surface makes BOB_HOME “behind source” until deploy.sh runs — which is the run doing its job, not the machine falling behind. Since #3160 the host engine measures BOB_HOME against the commit recorded in .bob-version, so the run’s commits and working tree are not compared at all and nothing needs excluding. The in-flight exclusion below applies on the fallback baseline only — a machine with no deploy recorded, where the working tree is the comparison and every finding is advisory. There the re-check passes the run’s kickoff HEAD (session.initial_head) to the host engine (host-drift.js --in-flight-base <ref>), which — only when the audited project resolves to BOB_SOURCE — excludes every path the run has changed since it started (git diff --name-only <ref>...HEAD plus the working tree) from the surfaces delta. A state that predates the kickoff-HEAD baseline falls back to the merge target. Excluded items stay visible in the document under surfaces.in_flight; drift that pre-dates the branch still fails; a base git cannot answer means no exclusion (the gate stays strict). The same base governs settings_hooks (#2203): the run’s own rewire of settings.json .hooks (or hooks/dispatch.tsv) since kickoff cannot be deployed mid-session (#2040), so when either is in flight the section grades the deployed settings.json against the source at the base (git show <ref>:settings.json) instead of the working tree — a match passes with settings_hooks.in_flight = {self_hosting: true, reason: "self-hosting: hooks changed on this branch (not deployed until merge)", changed, head_delta}, carried to state.preflight.rechecks[].notes by the re-check; a mismatch is the machine’s own drift. A base at which settings.json did not exist, or a git that cannot answer, means no exclusion. The kickoff gate never passes the flag — its semantics are unchanged. A directory-level item (missing scripts/rdb for a directory the run added, extra scripts/old for one it deleted) names no file the diff lists, so it is in flight when any in-flight path lies under it or the untracked prefix is the directory — matched on the /-terminated path, so scripts/rdb never claims scripts/rdb-x.
bob fork fork --from-issues runs the same gate once, before fanning out, so N worktree sessions never start on a machine one session would refuse.
warp status
Section titled “warp status”What each row of bob warp status means and how it is resolved.
The Phase row (#1960). The engine’s operator-facing name for the phase —
display_phase from state-machine.js status — so bug and task work read as
what they are: verifying (run the declared Verification steps) [testing] for a
task, regression_gate (…) [testing] for a bug, revalidating [planning] during
re-validation. The raw phase follows in brackets whenever the two differ; a plain
requirement shows the raw phase alone. See Phase overlays.
The Host row (#1742). An active run always says where it lives, so a blank
spot can no longer mean either “not tmux-hosted” or “the lookup failed”. The
resolution order is: the tmux session recorded at kickoff → tmux resolved from
the run’s PID by walking its process ancestry (covers sessions predating the
recording) → the iTerm window recorded at kickoff (iTerm · <profile>) →
detached when none of those is known. Where the host can be re-entered, the
row shows the command that does it (tmux attach -t <session>).
The Preflight row (#1923). The preflight gate’s last
verdict for this run: PASS / FAIL / ERROR, the machine it graded, each
check’s verdict, the advisories it named (advisories: host:behind (not blocking), #3160), the todo it filed or bumped when blocked (todo #77 (bumped)),
the number of mid-session host re-checks ((last FAILED) when the latest one
blocked, (last: host findings reported, not blocking) when it reported without
stopping the run) and, after a mid-session block, the phase a retry resumes at. It reads
not yet run until the gate has decided — the same .preflight object that
state-machine.js status emits.
The Forge row (#1751). The run’s forge condition, read from state.forge
(forge in state-machine.js status). Every transition folds the forge health
verdict into it, and forge-check probes it. While the run is in degraded
mode the row is red: DEGRADED MODE, the verdict and its reason, when the
episode began, the writes spooled to the #1750 queue, and the reads served from
the #1749 cache. While merging is parked it leads with
PARKED at merging — <reason> · resume: state-machine.js forge-check, and the
Phase row reads parked [merging]. After recovery the row is dim and counts
the episodes the run rode out. The row is omitted until the engine has read the
signal. The bob warp viz card shows the same condition as a red page banner,
and labels the merging node parked. Parking is a sub-mode of merging: no
phase or transition is added, and the phase contract
is unchanged. See Forge degraded mode.
The same resolution backs the bob warp viz Run region — both read
scripts/warp-drive/host-resolve.js, so the CLI and the page cannot disagree
about where a run is. An idle project shows no host at all, and an active run
whose resolver cannot be reached reads unknown (host resolver unavailable)
rather than going silent.
The Area: line appears only when the session is area-scoped (/warp-drive --area <slug>); an unscoped run omits it (#613). The Scope: line shows the run’s authorized reach (#992): issues #42, #43 (1 done, 1 remaining) for an issue-scoped run, flight-plan #N for a flightplan-scoped one, full approved queue for an unscoped one (an area-scoped run’s reach is the Area: line). A scope inherited from a stream binding (#1177) shows the same way. The bob warp viz Run region carries the same reach (#1644, #1825): a flight-plan run’s plan is its Plan #N tab and its Requirement j of m, and an issue-scoped (issues #42, #43 (1 done, 1 remaining)) or unscoped (full approved queue) run’s reach is worded exactly as the Scope: line. Nothing is shown when there is nothing to say, so an area-scoped run shows no reach line and a flight-plan run is never described twice.
The Tmux: line names the tmux session hosting the run, so you can find where it lives — invaluable across a cdfork fan-out where several sessions run at once (#990). It is recorded at kickoff when warp-drive starts inside tmux (the declarative source of truth); for sessions that predate this it is best-effort resolved by matching the recorded PID against tmux pane ancestry. It is omitted entirely when the run is not tmux-hosted (same omit-when-empty convention as Area:). The bob warp viz Run region shows the same host, resolved in the same order (recorded kickoff context first, PID-ancestry fallback) via the page’s state.json/SSE payload (#1628). When more than one active warp-drive session is detected on the machine (e.g. cdfork worktrees), bob warp status lists every session — project, branch, phase, and tmux name — so a single bob warp status surfaces the whole fan-out, not just the current project’s run.
Live/watch mode (#612). Pass --watch (aliases: --live, -w) to re-render the status on a fixed interval instead of printing once — handy parked in a tmux/iTerm split:
bob warp status --watch # refresh every 5s (default)bob warp status --watch --interval 2 # refresh every 2sbob warp status -w # short aliasbob warp status --watch --log # also show the phase-history log below--interval <seconds> tunes the cadence (default 5s; non-numeric input falls back to the default, sub-1s clamps to the 1s floor). The view redraws in place (no scroll spam) and exits cleanly when the session ends, goes stale, or is aborted — leaving a one-line summary on screen. With no active session it prints the usual “No active session” message and exits. Ctrl-C exits cleanly and restores the cursor.
--log (#1003). Pass --log to append the phase-history log (the same table bob warp log prints) beneath the standard session info. It works on both the one-shot bob warp status and the live --watch view — in watch mode the log re-renders each tick, so new transitions appear in place as they happen. When the session has no recorded history the log section is omitted entirely (no empty header).
warp viz
Section titled “warp viz”bob warp viz opens the project’s run monitor (/warp on the project’s dashboard server), streaming over SSE. The page answers the operator’s questions top to bottom — does it need me, what is it doing and is that normal, how far along is it, what just happened, then the details — in large type, with the engine’s full phase graph one click down. Its build spec is warp-viz-spec.md and its reference design warp-viz-mock.html.
Watching a farm-hosted run from a laptop (--tailnet, #1736). A run on the farm needs a monitor you can open from wherever you are. bob warp viz --tailnet (equivalently bob dash serve --tailnet, or DASH_BIND=tailnet in a service definition) serves the monitor on that machine’s Tailscale address instead of loopback and prints the resulting URL — which is the whole interface when you are on the far end of an SSH session:
ssh farm-01cd ~/projects/bodfeed && bob warp viz --tailnet# http://farm-01.tail3bfead.ts.net:6152/warp <- open this from the laptopNo port forwarding, no public DNS, no reverse proxy: the tailnet is the transport, exactly as it already is for the cross-machine audit (#415). Per-project ports are unchanged — the URL’s port is still the project’s +2 band slot from the ledger, so several projects can be served at once on the same box.
The bind is deliberately narrow. Tailnet mode binds only the tailnet address (loopback is not bound in that mode, and neither is the LAN or public interface — on a farm box with a routable public IP, that address answers nothing). The resolved address must fall inside Tailscale’s CGNAT range or the server refuses to start, and Tailscale being down is likewise a refusal naming its reason — never a fallback to a wider bind. The monitor is unauthenticated and shells out to git/gh on the host, which is why it is never exposed further than the tailnet; the full reasoning, and what would have to change to revisit it, is in Project Monitor Exposure Posture.
Deploy-fresh server reuse (#1603). A running dashboard server is reused only when it is running the currently deployed code. The server exposes a deploy stamp at /warp/server.json (the mtime of the script the process loaded); before reuse, bob warp viz runs scripts/dashboard/server-fresh.sh <port> [host] against it and restarts the server when the deployed file is newer than the stamp — or when the stamp endpoint 404s on an otherwise-alive server (a pre-stamp process is stale by definition).
Live self-update (#1734). Both halves watch continuously. The server stats the on-disk script it loaded (15s tick, DASH_SELF_WATCH_MS override) and, when a deploy rewrites it, marks its stamp stale, waits for a second identical reading of its whole import tree — every module reached from server.js by a relative import, each file’s mtime and size (#2987) — so a mid-write deploy or a checkout in progress never launches a half-copied script or module; an import that has gone missing since the process started holds the restart and is named on the stamp as stale_waiting_on. Then it restarts in place — SSE clients are ended so their retry: hint reconnects them to the successor spawned on the same port. The handoff is checked, never assumed (#2987): the successor’s stdout and stderr go to the dashboard log (${XDG_STATE_HOME:-~/.local/state}/bob/dashboard-<project>.log, the one bob warp viz starts the server with, opened for append), and the old process stays alive until the successor answers /warp/server.json on the bind with its own pid. A successor that exits or stays silent for 10s (DASH_HANDOFF_WAIT_MS) is retried once after 2s (DASH_HANDOFF_RETRY_MS); a second failure is logged as self-update failed: nothing is serving <url> naming bob warp viz / bob viz as the recovery, so an empty port always has a readable cause. The page captures the deploy stamp at load and re-probes it every 15s plus on every SSE reconnect; a changed stamp reloads the page (?root= rides the URL; theme, view, and active tab persist in localStorage). Anti-thrash: the page holds off while the stamp reports stale (reloading then would refetch the old code) and caps auto-reloads at one per 30s, so a flapping stamp can never loop. The context tabs are generated from a single PANES registry — each row declares its endpoint, renderer, and (optionally) a relevant(st) predicate evaluated on every state push — so a pane that becomes relevant mid-session (a flight-plan kickoff) appears without a reload, and the next pane is one registry row rather than edits scattered per feature.
The page, top to bottom (docs/design/warp-viz-spec.md). The operator reads /warp from across the room and from a phone, so it uses the large type scale from design-tokens.js (body --text-base, the current phase --text-3xl), never the dense one /bob uses. Every size and space is a token multiplied by --warp-scale, which the header’s A− / A+ buttons step from 0.75 to 1.5 in steps of 0.125 (decision #4350), so one control scales type, spacing, touch targets and bars together; every control is at least --space-2xl tall. Both themes come from the shared tokens, as on /bob. The regions are drawn by scripts/dashboard/warp-regions.js — pure renderers, documents in and HTML out, tested in node by tests/test-warp-regions.js — and the page owns the state.
- Header — the project · a live badge (
● live,● stale, oridle, always a word) ·updated Ns ago·A−A+·Copy attach(the run’s attach command, hidden when there is none) ·Notify me·Theme. - Needs you — shown when something waits: the run’s own pending decision first (the read-only
attentionblock — thebudget_exceededcheckpoint, the Level-2 continue prompt), then the project’s open asks from/warp/asks.json, Control’sbob-control-asks/1document narrowed to this project and produced by the same code as/control/asks.json(one origin, no cross-server fetch). The summary is the count in words (3 need you) with the current ask’s kind and line and a cycle (‹ 1/3 ›); the body is the question, its detail, and Answer in Control (/control?ask=<owner/repo#N>) — the page stays read-only. When the asks could not be read, the region says so and the page never claims nothing waits. - Run — the now block. The summary carries the state word (
running,paused,stale), the phase in bold, the session andchunk i of n, an all sessions link and a cycle over the sessions on/warp/sessions.json(on a cdfork fan-out, stepping it navigates?root=<worktree>; with one session it reads1/1, both buttons disabled; when the viewed session ends, the page falls back to the primary). The body is the phase label large, time in phase,usually a–b(the 25th–75th percentile of this phase’s earlier visits in the run, left out under three visits — never invented), the stale line with its likely cause, the issue reference and title (the reference alone, never a work type, #1825), the chunk segments,Chunk i of n · Requirement j of m · Running <age>, where the run lives (the same host resolver as warp status, #1742), its branch and its reach (issues #… (n done, m remaining)orfull approved queue, worded asbob warp statuswords it), and four vitals: Commits, Tokens used (percent of the budget when a policy applies, through the samebudget-policy.jshelper the cost breaker enforces), CI (the current branch’s newest run,✓ pass/✗ fail/not measuredwith the reason) and Retries (loop-backs taken). With no run, it says so, links the last session report and givesbob warp start. - Where it is — the happy path as three groups: Set up (preflight · discover · plan · chunk), Chunk loop (code · docs · test · commit · drift · report,
i of n) and Finish (req done · qa · merge · promote), each step with its time this run:✓done in ink,●current, to come dimmed. Membership comes fromPHASE_META.groupand the short names fromPHASE_META.label, served on/warp/graph.json— the page keeps no list of its own. A side trip (blocked,budget_exceeded,awaiting_continue,aborted,prerequisites) is drawn only while current, after the step it left, in the bad state with its word. Full state graph opens the phase graph below the tabs (see below). - Acceptance · Time by phase · Activity — the requirement’s checkboxes from
/warp/acs.jsonwithdone / total(hidden with no req or no checkboxes, #1584); the six longest phases this run, the current bar marked; and one newest-first list merging phase transitions, chunk starts, reports filed, retries and commits (/warp/gitlog.json), eight shown and the rest folded with the shared fold. - Context tabs —
Plan #N·Commits·Tree·CI·Issues, a real tablist (arrow keys move between tabs), described below. - Sources —
Every source read.orNot read: <source> — <reason>for each of asks, acs, ci, flightplan, gitlog, issues and activity that failed, plus the #1968 notice when the server can no longer run gh.
Working now (#4394). The warp state file only knows the warp phase. A run can sit in awaiting_continue while a Claude Code workflow of two dozen agents builds its items, and the page used to show that as an idle run. So the Run region also shows what is actually running in the project, from /warp/activity.json (bob-warp-activity/1, built by scripts/dashboard/project-activity.js). The page polls it every 15 s. The server reads the files Claude Code writes for the project’s sessions under $CLAUDE_CONFIG_DIR/projects/<slug>/ (default ~/.claude/projects/<slug>/, only sessions touched in the last 24 h):
- Workflows. Each
<session>/subagents/workflows/wf_*/journal.jsonlgives the agents:startedrecords carry each agent’s label and phase, and aresultrecord marks it done. The name and declared phases come from the workflow script’sexport const metain<session>/workflows/scripts/<name>-<runId>.js. Each agent’s start is the first timestamp in itsagent-<id>.jsonl. The end record<session>/workflows/<runId>.jsonappears only when the workflow ends, and it is the one place afailedagent is written. A workflow is running while that end record is absent and a file in its folder was touched in the last 30 minutes; a session that was killed never writes the end record, so its workflow drops out after 30 quiet minutes. An ended workflow stays in view for 60 s, then leaves. - Background subagents. Each
<session>/subagents/agent-<id>.meta.jsonwithrequestShape: "background"(Agent-tool subagents) is shown by its description and type. It counts as finished when the parent transcript<session>.jsonlhas a<task-notification>for that task id at or after the agent’s last record. The server reads each parent transcript whole once, then only the bytes added since its last read, so a notification is found however much the session writes after it. A resumed agent writes again and shows as running again.
The region draws a Working now block: each workflow’s name, its state word and elapsed time, its phases with the current one marked, every running or failed agent with its label, state and elapsed time, and the done agents as one line. Background agents follow as label, type and state. While a workflow runs and the run is in awaiting_continue or discovering, the phase reads Building #4201, #4202 in a workflow (24 agents) in the active tone, not as a side trip: the Run header says building instead of paused, and the tab drops its (!) marker (the desktop notification is unchanged). The items are the #N references in the running agents’ labels, or the workflow names when there are none. When nothing is running or just ended and more than 3 minutes have passed since the latest of the run entering those phases, the newest workflow’s end and the newest background agent’s stop, the region says Idle between items since 15:52, timed from that latest moment. A shorter pause shows nothing. If the session folder cannot be read, the document says sources.read: false with its reason, and the Sources line names activity. An unread folder never claims the run is idle. This is display only: no prompt, todo, push or stop point.


At 390 wide the page keeps its order:

The screenshots are rendered from a fixed run by node scripts/dashboard/bob-viz-shots.js.
Region open state, the selected tab, the text scale and the Activity fold persist per viewer under warp-viz-view/1, every storage call in try/catch. The title’s (!) prefix and the favicon swap while the run waits on a human (#1586) are unchanged; Notify me opts in to a desktop notification on the transition into waiting, and the Notification permission is requested only when it is clicked. At 390 wide the page keeps its order: the phase drops to --text-2xl, Where it is shows only the chunk loop with done steps folded to ✓ n done, and nothing scrolls sideways. A machine’s session screen (?kind=decompose, ?kind=do, #3815) stands in for regions 3–5; the session-kind switch sits beside the Sources line. The forge riding out an outage (#1751) keeps its page-wide line under the header.
Full state graph. The engine’s phase topology as an SVG, the traversed path highlighted and the current phase marked, in two views switched by its view: iso/flat button (persisted in localStorage, per browser):
- flat (default) — the vertical spine layout, strictly top-to-bottom reading order; the graph scrolls inside its section when the spine is wider than it.
- iso — the layered phase DAG projected onto a 2.5D isometric staircase, using both screen axes so the whole graph fits a typical viewport with no scrolling. Loop-back edges arc through the open area above the staircase; traversal counts, visited/current highlighting, the side lane, and the stale grayscale treatment all behave exactly as in flat view. The view scales to fit down to a legibility floor (0.6×); a graph too large even for that keeps the floor and scrolls instead of shrinking further or clipping.
Both layouts are pure functions of /warp/graph.json — live state colors the picture but never moves nodes, so the shape is stable across sessions. The graph is drawn only while its section is open.
Phase tooltips (#1740). Node labels are the engine’s raw phase names, so
hovering a node explains it: what happens in that phase, what advances it, how
long the session has spent there, and a link into that phase’s row in
Phase Details (new tab). The same nodes are reachable by
keyboard — Tab to focus, Escape to dismiss — and the tooltip flips and clamps to
stay inside the viewport in both views. In bug mode it names the overlay label
and the underlying engine phase, and revalidating is explained rather than
silently substituted (see Phase overlays). The copy is served
with the topology on /warp/graph.json (phase_meta, sourced from PHASE_META
beside the engine’s PHASES), never duplicated into the page: a phase the engine
adds without metadata falls back to the plain name-only tooltip rather than
showing something wrong.
The page header is titled per project — the project name alone, since the page is the project’s monitor (a ?root=<worktree> view is titled by that worktree’s project; #1733) — and issue references in the Run region, commit subjects and issue entries are hyperlinks to the GitHub issue; commit hashes link to the commit on GitHub. Links resolve from the repo’s origin remote — a repo with no remote shows plain text instead (#1583). The fleet links that once sat on this page (#1744) are gone: bob viz and BoB Control own the fleet, and /warp/fleet.json stays for their use.
Context tabs (#1583, #1734). The PANES registry rows, in the order Plan #N · Commits · Tree · CI · Issues; the selected tab persists with the view, and a relevance-gated tab (Plan, CI) that hides while selected falls back to the first shown. The retired utility pane — its docked 75% column, its drag-resize and its warp tab — is gone; the warp tab’s sections are regions 2–4 now. Each tab is backed by a read-only server endpoint that honors ?root= and is polled lightly while it is selected:
-
Commits (
/warp/gitlog.json) — recent commits: short hash, subject, relative age. -
Tree (
/warp/difftree.json) — the uncommitted working set (staged + unstaged + untracked) as directory groups with per-file+adds/-delscounts (additions green, deletions red); when the tree is clean it shows the HEAD commit’s files, labelled as such. When the repo has linked git worktrees (a cdfork fan-out), the tab adds a section per worktree — branch + path, then that worktree’s changed files via the same per-root diff — so the whole fan-out’s working state is visible from one page; a worktree whose diff can’t be read (pruned/locked) degrades to an inline unavailable note for its own section only, and a repo with no linked worktrees renders exactly as before (#1628). The base checkout is a section too (#1741): its files sit under the same header treatment, showing its branch, a· currentmarker naming it as the page’s own root, and the root path when there are other sections to tell it apart from — so a fan-out reads as one list of labelled sections rather than anonymous files followed by named worktrees. The branch is resolved server-side from the resolved root, so a?root=-scoped page names that worktree’s branch; a detached checkout reads as its short SHA rather than the literalHEAD. A clean tree with no linked worktrees still renders the plainno changesempty state. -
Issues (
/warp/issues.json, formerlyfeed) — recently updated GitHub issues (number, title, labels, relative age), newest first. The feed degrades to a quiet empty state whenghor the network is unavailable — the endpoints never write viagh. -
CI (
/warp/ci.json, #1735) — the repo’s delivery pipeline: recent workflow runs with status glyph, workflow · branch · event, duration, start age, and a runner-locus badge (runner name such asfarm-01, orhostedderived from the labels), so the CI-locus rollout is observable from the monitor. The current branch’s run is highlighted; a failing run names its failed job/step inline and links out to the run; a queued run whose requested self-hosted labels no online runner carries is flagged loud (⚠ queued — no online runner) — the silent-stall condition. Strictly read-only. API budget: onegh run listper 30s (the server’s cache; the page polls it for the Run region’s CI vital too), jobs enrichment capped per cycle and memoized permanently for completed runs, runner roster every 120s — steady-state ≈ 2 requests/min per project, so a monitor left open all day stays far under the REST limit. Degraded states are distinct and readable (gh missing / unauthenticated / rate-limited); the tab hides only for a project with no CI workflows, via the #1734 pane registry. If CD lands (#1725 option D), deploy rows join this pane as a second section. -
Plan #N (
/warp/flightplan.json, #1644) — visible only when the session is flightplan-scoped (session.kickoff.flightplan; the tab hides itself otherwise, falling back to the first shown tab if it was selected), and named for its plan. Renders the plan’s ordered item checklist in the same style as the Acceptance region — ☑/☐, item number + title, each row linked to its issue, headed byflight-plan #N — done/total. The server mergesflightplan.js itemswith theremaining --alllifecycle classification (#1666): held items (gated/withheld/waiting/moot, plus humantodogate rows) render dimmed with a state tag, so a plan stalled on a human gate is visibly different from one with plain unworked items. Plan data that can’t be derived (gh/network down) degrades to an inline notice, same convention as the feed — the payload answers{plan: N, unavailable: true}, never a well-formed0/0checklist, so “the plan is empty” and “we could not read the plan” stay distinguishable (#1780). That guard lives in the derivation itself (scripts/dashboard/flightplan-view.js), which treats any non-arrayitemsas unreadable, so every failure path reports one degraded shape.Done-ness comes from live state, not the checkbox (#1757). An item that has landed and closed on GitHub but whose plan checkbox is not ticked yet appears in no
remainingbucket — the gate resolver drops closed items by design — so the payload also carriesflightplan.js lifecycle’suntickedClosed, and such a row renderslanded: ticked, counted indone/total, and tagged, with the header naming the lag (4 not yet ticked in the plan).landedis kept distinct fromdoneon purpose — the plan body genuinely is stale at that moment, and the tag disappearing is how you see reconcile catch up.Ticking itself no longer depends on the agent remembering: the transition into
awaiting_continueruns the #1068 check-off for the finished requirement (_workflow.flightplan_tick_on_item_boundary, default on). It is idempotent, and best-effort — aghfailure is reported in the transition output rather than blocking the loop, and thesession_endingreconcile still sweeps whatever it missed. It never ticks an unfinished item (#3694): an item whose issue is still open and labelledblockedor gated by an open todo is held unticked, and the transition output says why.
If a tab’s endpoint fails before it has rendered anything (404 from a stale server, network error), the pane replaces the loading… placeholder with an explicit “data unavailable” note naming the failure and the likely fix (restart via bob warp viz); once a tab has rendered real data, a later failure keeps the last good content instead (#1604).
A server that can no longer run gh (#1968). A long-lived dashboard server can lose the ability to execute gh at all — every call a fast exit 1 — while git in the same process and gh in every other process on the machine keep working (observed on servers orphaned for days; the process-level cause is unconfirmed, so the server now records the failing call’s exit code and stderr tail for the next occurrence). The server keeps one process-wide gh liveness view: consecutive gh failures across every gh-backed backer (feed, CI, AC checklist; the flight-plan pane borrows the verdict since its gh runs in a child), corroborated by a git success in-process and a gh success from another process — read from the forge observations log (#1748): the streak is unbroken, so any ok line after the first failure cannot be the server’s own. Three or more failures with both corroborations is verdict server-cannot-run-gh; the same streak without them stays failing. Every degraded gh-backed payload now carries a typed reason (gh-missing / unauthenticated / rate-limited / server-cannot-run-gh / gh-failing / unavailable) plus the evidence block, and the panes render the cause and its fix — “this dashboard server can no longer run gh (git still works; gh works from other processes) — restart it: warp viz” — instead of gh/network; an uncorroborated streak shows its count, start time and last stderr line inline. Because the AC checklist hides itself when its payload is unavailable and a utility tab only speaks for itself, the page also carries a tab-independent notice under the header, fed by the existing 15s stamp probe: red with the fix on the corroborated verdict, amber with the streak evidence before corroboration, cleared on the first gh success. The evidence also rides /warp/server.json (gh.verdict, fail_streak, first_fail_at, git_ok_at, other_process_ok_at, last_error), where server-fresh.sh reads it: a deploy-fresh but gh-dead server answers exit 5, and bob warp viz restarts it from the launching shell, naming that reason. The server deliberately never restarts itself on this verdict — a successor spawned by the broken process would inherit whatever broke it; the restart from a fresh shell is the one recovery that has been verified. DASH_GH_TTL_MS overrides the gh-backed cache TTLs for tests (make test-warp-gh-liveness).
Integration-branch streams
Section titled “Integration-branch streams”The operator workflow — bob warp session start, bob warp finalize — is in the guide. What follows is how a stream is kept fresh, guarded, merged and shipped.
Start-of-work freshness (#1178)
Section titled “Start-of-work freshness (#1178)”A stream is freshened from main at every start-of-work seam — warp-drive kickoff (prerequisites), each segment resume, and bob warp session start on an existing branch — via the shared scripts/warp-drive/stream-freshness.sh. The sync merges main into the stream (never rebases — the stream is pushed; direction fixed by the #1179 policy): a single fetch + ancestry check makes the up-to-date case a fast no-op, a behind stream absorbs main routinely instead of all at once at finalize, and clean syncs are recorded in the run’s phase history (bob warp status --log shows stream_sync(synced)). A conflicting sync is a blockable event: the merge is aborted with the stream untouched, and the loop files a todo naming the conflicted files and stops — the standard completed-label contract resumes the stream once a human resolves. From the bob warp session start CLI the same conflict refuses the start with the files named.
Wrong-branch guard (#1180)
Section titled “Wrong-branch guard (#1180)”The freshness pass also ends on the stream branch: if a binding is active but HEAD is elsewhere (main, a stale child branch, detached HEAD), stream-freshness.sh checks the stream out ("checked_out": true in its JSON) so a kickoff never silently works off-stream. A dirty working tree that makes git refuse that checkout is a distinct blockable signal — status blocked, exit 4, HEAD and local changes untouched — and the loop files a todo / prompts per level rather than continuing on the wrong branch. On the enforcement side, hooks/session-branch-guard.sh (active whenever _branch.session is set) additionally refuses git commit on main or on any branch that is neither the stream nor a child of the stream; per-requirement child branches — which merge back into the stream via the LOCAL accumulate path — remain allowed, and the refusal message names the bound stream. check-branch.sh (the edit-on-master guard) is unchanged.
Stream↔scope binding (#1177)
Section titled “Stream↔scope binding (#1177)”bob warp session start <branch> --area <slug> / --flightplan <N> records the scope binding as _branch.scope alongside the branch pin (per-checkout, in settings.local.json) — see the branch-config reference for the schema and enforcement rules. In short: an unscoped /warp-drive on a bound stream defaults to the bound scope (so unrelated queue work can never land on the stream branch), a conflicting explicit scope is refused with the override paths named, and the binding survives session end, segmentation, and restarts until bob warp session end / bob warp finalize clear it with the pin. Because it is per-checkout, cdfork worktrees each carry their own binding — parallel streams on different areas coexist.
How merging changes
Section titled “How merging changes”When a session branch is set, merge_target != main, and _workflow.session_merge is local (the default), the merging phase accumulates locally: it merges each requirement’s child branch into the session branch per merge_strategy, pushes the session branch, deletes the child branch, and never touches main / opens no PR. This applies at both Level 2 and Level 3.
Set _workflow.session_merge to pr to keep the legacy behavior (a PR or direct-to-main merge per requirement) even with a session branch. With no session branch set, behavior is unchanged regardless of the value.
session_merge |
session branch set | merging behavior |
|---|---|---|
local (default) |
yes | merge child → session, push session, delete child; main untouched |
pr |
yes | per-requirement PR (L2) / direct-to-main (L3) |
| any | no | unchanged legacy behavior |
finalize safety
Section titled “finalize safety”bob warp finalize refuses to run when there is no integration branch or while a warp-drive session is mid-stream (it only proceeds with no active state or at awaiting_continue/completed, so it never ships a half-done requirement). On a conflict, push, or auth failure it aborts cleanly and leaves _branch.session (and any _branch.scope binding) intact so you can fix and retry. --dry-run prints the exact commands without executing them.
Unified freshness/finalize policy (#1179)
Section titled “Unified freshness/finalize policy (#1179)”Finalize and start-of-work freshness are one coherent policy: merge-in, never rebase. The L3 finalize path freshens the stream through the same stream-freshness.sh engine (a rebase would replay the stream’s absorbed sync-merge commits into mangled history), then ships to main with the configured _branch.merge_strategy flag — ff-only (default) works because a freshened stream strictly descends from main, and preserves the sync merges in history; configure merge-commit for an explicit --no-ff ship commit. Rebase applies nowhere in warp tooling — it is acceptable only manually on a never-pushed branch. Full policy: branch-config reference. On a main branch the manifest protects for admins (_meta.branch_protection), the same decision the per-item merge reads sends finalize through one pull request and merge-on-green instead of a push; a red check leaves the PR open and the stream intact (branch-protection reference, #4184).
Session segmentation (long runs)
Section titled “Session segmentation (long runs)”When segmentation fires and what the operator sees is in the guide.
It reuses the substrate that already exists — the persisted state file, the warp-drive-inject.sh resume hook, and the cdfork-style fresh-process spawn. There is no new resume engine.
How it works
Section titled “How it works”At awaiting_continue (a requirement is done, merged onto the integration branch, git clean) a Level 3+ run consults the segmentation decision before continuing:
- Threshold — if the tokens or chunks accrued since the last checkpoint cross
_workflow.segment_after_tokens/segment_after_chunks, the run segments; otherwise it continues in-process exactly as before. Seams lower the bar (#960): when the next queued item belongs to a different cap (Part of #NN) orarea:label than the one just finished, the lowersegment_at_seam_tokensbar (default 40000) applies instead — warm context is worth less across a semantic boundary. A dimension only counts when known on both sides; missing metadata never seams, and an--area-scoped run has no area seams by construction (cap seams within the area still can). Seam-triggered segments are markedkind: "seam"in the decision and checkpoint reason. - Checkpoint —
state-machine.js checkpointrecords the boundary on the state file (appendssession.segments[], anchors the per-segment counters) without changing phase or deleting state. It requires a boundary that is clean of work — see What a checkpoint counts as dirty. - Fresh spawn —
segment-spawn.shlaunches a freshclaude -a<level> "/warp-drive"in its own per-segmentwarp-seg-<stem>-<N>tmux session (mirroring cdfork). The current session stops; the prior segment’s now-idle session is reaped at the next handoff. - Resume — the fresh session’s SessionStart inject hook reads the still-present state file, sees the segment lineage, and continues the same queue on the same branch. Nothing is re-passed on the command line — all kickoff options live in the persisted state, so no state is lost.
Segmentation only fires when more work remains within the run’s kickoff scope (#992): an issue-scoped run whose issue(s) are done reports moreWork: false and routes to a clean session end instead of handing the approved queue to a fresh self-spawned session — the incident that motivated the scope rule.
The trigger measures per-segment usage (delta since the last checkpoint), never whole-run cumulative — otherwise the persisted state would re-trip the threshold immediately in the fresh session and loop forever. bob warp status surfaces the thresholds, the per-segment usage so far, and the checkpoint lineage under segmentation. See the state reference for the fields and CLI actions.
What a checkpoint counts as dirty (#1756)
Section titled “What a checkpoint counts as dirty (#1756)”A --flightplan run’s plan-scoped provision overlay rewrites the cdprov-managed ignore block in the tracked .gitignore for the whole window in which segmentation can fire, so the checkpoint cannot simply ask “is the tree clean?”.
The checkpoint classifies each dirty path instead:
| What it found | What happens |
|---|---|
| bob provision’s own ignore-block edit — the overlay’s, or the provisioning preflight’s with no overlay (#3066) | Tolerated — this is bookkeeping, not work, and the checkpoint proceeds. |
A BoB-derived path (#1969, #1970) — a session-state file declared in scripts/lib/derived-paths.json (overlay state, last hook payload, warp-drive state, project-ready verdict, forge cache, settings.local.json), or a .claude/* symlink that resolves into BOB_HOME / the source checkout (a universal command bob init linked after the project last committed .claude/) |
Tolerated — the loop or bob init wrote it; it is never a requirement’s work. Tolerance is by ownership, not location: a symlink targeting anywhere else, or a real file under .claude/commands/, is still work. Holds even where the managed ignore block is stale or opted out (_meta.track_provisioned). |
| Changes that pre-date the session and that the run has not touched (#1995) | Carried — the kickoff already recorded these paths in session.initial_dirt as yours, not the run’s. The checkpoint proceeds, leaves them in the tree untouched, and records them as carried_preexisting on state.checkpoint and the segment record so the next segment can see what it inherited. |
| A pre-existing path the run did touch — its content differs from the kickoff snapshot, it was deleted, or a commit since kickoff changed it | Refused (kind: "inflight") — once the run has rewritten or committed an inherited path it is the run’s work, however it started out. |
| Anything else | Refused (kind: "inflight") — the requirement is not committed yet. Unchanged from before. |
Only in-flight work refuses. When carried dirt sits next to in-flight work the refusal lists the in-flight paths under paths and the carried ones under also_preexisting for context — they are what you will see in the tree, not why the checkpoint stopped.
The rule above is written once, as a contract, in Checkpoint dirt rules — classes, outcomes, payload shapes and why — and pinned by tests/fixtures/checkpoint-dirt.json; make check fails if that page and the fixture disagree (#2037). Every refusal names the page under see.
“Touched” is evidenced by what init recorded beside the path list: a fingerprint per inherited path (session.initial_dirt_fingerprints) and HEAD at kickoff (session.initial_head). For a tracked path the fingerprint is the inherited diff — the changed lines of git diff HEAD -U0, no line numbers — so the judgement is diff-level (#2260): the inherited hunk is carried as long as the working copy’s diff against HEAD is still exactly that hunk, even after the run commits a different hunk of the same file, and a commit counts as touching it only when it absorbed that diff. For everything else (untracked, binary, symlink, directory, or a state recorded before #2260) the fingerprint is the bytes, symlink target, or absent, and a fingerprint that no longer matches — or a commit between the kickoff HEAD and HEAD that changed the path — makes it in-flight. A state written before the fingerprint field existed carries on the path baseline alone.
Overlay tolerance is deliberately narrow. It applies only when the working .gitignore matches HEAD exactly once the managed block is stripped from both, so a hand edit elsewhere in .gitignore still refuses. It does not require an applied overlay (#3066): only cdprov writes inside the fence, and the L3 provisioning preflight’s bob provision add … --now rebuilds it at prerequisites with no overlay — a change the run was told to make and cannot commit at awaiting_continue. The derived-path tolerance reads the same declaration the managed ignore block is built from, so the block and the checkpoint can never disagree about what counts as work; on a freshly provisioned project those paths are ignored and never reach the classifier at all.
You no longer need to stash your own uncommitted changes around a segment boundary, nor the older workaround of restoring and re-applying the overlay around the checkpoint. If a checkpoint refuses as inflight on a path you left dirty at kickoff, the run has modified or committed that path since — treat it as the run’s work.
Spawn guard (#1995). segment-spawn.sh refuses (exit 3, nothing created) unless a checkpoint has landed for the current handoff: state-machine.js checkpoint writes a fresh checkpoint object each time it succeeds, and a successful spawn stamps that object with spawned_at, consuming it. A state file with no checkpoint, or one whose checkpoint was already consumed by the spawn that launched the current segment, means this segment’s checkpoint was refused or never run — spawning anyway would put a second live session on the same state file, both resuming at awaiting_continue and racing on the same refusal. The refusal names the reason and the checkpoint command to run; --print (a pure formatter that spawns nothing) is not gated.
segment_after_tokens defaults to 120000 (enabled, #959); segment_after_chunks defaults to 0. The default is resolved in one place (the state-machine config layer), and an explicit _workflow value — including 0 to disable — always wins over it.
Auto-surfacing the continuation on macOS (#996, #1022, #1023)
Section titled “Auto-surfacing the continuation on macOS (#996, #1022, #1023)”Every re-segment hands the queue to a fresh claude process, and it always surfaces as a new window — never a split of the pane you’re watching (#1022):
- A fresh per-segment session, one window each — segment-spawn creates the segment’s own
warp-seg-<stem>-<N>session (N= the segment it hosts) as a single-window session, the same path whether or not warp-drive is running inside tmux. It never splits the spawning pane. Attached via iTerm2-CC, the auto-attach below pops this as a fresh native iTerm2 window near your work. - A new window every segment (#1023) — because each segment gets its own session, and a brand-new session is always unattached, the auto-attach fires on every handoff — not just the first. The
warp-seg-prefix is unchanged, sobob warp reap/bob warp status/_reap_seg_sessionkeep matching.
The fresh segment is then focused (select-pane + select-window by unique id) so an attached client lands on the running segment, never a stale same-named window from an earlier segment.
Because a per-segment session is always unattached, on macOS with iTerm2 running segment-spawn opens an iTerm2 window in control mode (tmux -CC attach -t <session>) so the continuation actually pops on screen — every time. Control mode (not a plain attach) is used deliberately — a plain attach would contend for terminal size against any other client. The first use triggers a one-time macOS Automation permission prompt (iTerm2 controlling itself via osascript); approve it once. iTerm2 is detected via TERM_PROGRAM/AppleScript is running (#1016).
-CC tab semantics: each tmux window shows as a native iTerm2 tab within its control-mode window. Closing a tab kills that tmux window — to leave the run alive, detach the control-mode client instead (tmux detach), don’t close individual tabs. See iTerm2 tmux -CC.
The surfacing is strictly best-effort and safe by construction:
- Fresh session, always unattached: each per-segment
warp-seg-<stem>-<N>session is brand-new, so a new iTerm2 control-mode window is opened on every handoff — the intended behavior (a distinct window per segment). Thealready-attachedgate (which the shared session tripped for later continuations) no longer applies to continuations, only to a redundant re-spawn onto the same session. - One owner of the attach (#3942): a live
bob warp attachfollower records its pid on the tmux server as@bob_attach_follower_<repo>before it attaches anything. The spawner reads that record and skips its window (auto-attach skipped: follower), because the follower moves its own terminal to the new segment. Two-CCclients on one session made iTerm2 show one of them as raw control-mode protocol. The record exists before the segment does, so which attach would have been first never matters. A dead pid is ignored, and the follower clears the record when it ends. - Prior segment reaped at handoff: once the fresh segment is live, its resume-time inject reaps the previous segment’s pane by id — which drops that segment’s one-window session, so idle
warp-seg-*windows don’t accumulate across a long run. The reap is guarded towarp-seg-*sessions only, so your own launch window (the first handoff’s “previous” pane) is never closed out from under you. - Swept at run end (#3163): when the run reaches
completedoraborted, the engine kills its ownwarp-seg-*sessions (except the one it runs in), then runsreap.js’ssweep(), which isbob warp reap --forcewithout--yes. It removes every finished segment session on the machine, and spares live runs, attached sessions and the caller’s own pane. That pane is the one matchingTMUX_PANE, or any pane whose shell is an ancestor of the process. The response carriesseg_reap: {reaped, attached, spared, windows_closed}when it removed or left something.WARP_RUN_END_REAP=0opts out. - Non-GUI / headless: silently skipped on non-macOS, over SSH, or when iTerm2 isn’t running. The new tmux session still exists (it’s just tmux); farm/headless machines are otherwise unaffected.
- Opt-out: set
WARP_SEG_AUTOATTACH=offto disable the iTerm2 window (the tmux session is still created).
If the osascript path fails for any reason, it’s non-fatal — the printed attach: tmux attach -t <session> hint line still stands and the spawn still succeeds.
Per-Area Re-Validation (#1008)
Section titled “Per-Area Re-Validation (#1008)”When _workflow.revalidate_on_area_entry is on (the default), the loop runs a lightweight freshness re-check on area entry instead of trusting the approved stamp blindly: /groom stamps approval against a snapshot of the repo, and a long multi-area run lands commits before later areas are reached.
When it fires. At work_selected, the state machine compares the just-finished item’s cap/area metadata against the newly selected item’s — the same seamBetween signal seam-aware segmentation uses (#960). Only a crossing (different cap or different area: label, known on both sides) arms the sub-mode; the first item of a run never re-validates (it is freshly groomed), missing metadata never seams, and a same-area selection proceeds untouched. While armed, planning displays as revalidating in bob warp status.
The movement gate. The pass starts deterministic and cheap — scripts/warp-drive/revalidate.js gate <issue> reads the <!-- approved-at: <short-sha> --> marker /groom stamps into the issue body and compares it against HEAD:
- No commits since the marker — the approval is fresh: the pass records a logged near-no-op (
record --no-drift) and planning proceeds with zero further overhead. - Commits landed since (or the marker is missing/unverifiable — issues approved before the marker convention are treated as movement-unknown, failure-safe): the gate reports the commit count and a changed-file triage list, and the lightweight re-check runs.
The re-check. The loop triages the changed files first — movement that doesn’t touch the item’s area is still a no-drift outcome. Each genuinely drifted AC is classified by the reversibility engine (#878, decision-engine.js classify):
- Two-way (reversible) — e.g. an AC already satisfied by a landed commit, or one made redundant: auto-adjusted (checkbox flip plus a one-line note comment naming the satisfying commit) and recorded.
- One-way (irreversible) — a scope change, an AC that now contradicts landed code, a new cross-area dependency: surfaced, never auto-walked — a decision checkpoint at Level 2/3; under Level 4 the decision-timeout policy (#875) applies (conforming human-only TODO, defer the item, or stop cleanly if it blocks everything).
This is a freshness re-check, not a second grooming authority: it never rewrites issue bodies wholesale, never re-derives the cluster, and /groom remains the single upstream source of truth for cluster coherence (Prime Directive). It also composes with kickoff scope (#992): it re-validates only the already-selected item — it never adds, reorders, or widens work.
Approval is bound to the author and the body (#3628). Issue text is data, never authority: an issue filed by an outsider, or text edited into an approved issue afterwards, must not become approved work. /groom therefore approves only through revalidate.js approve <issue>, which refuses (exit 3) when the issue’s author_association is not OWNER, MEMBER or COLLABORATOR (an attended operator may pass --human after reading it), and stamps a hash of the body — <!-- approved-body: sha256:<16 hex> --> — beside the approved-at commit. The hash ignores ticked checkboxes and the markers themselves, so warp-drive ticking ACs never revokes an approval; any other edit does. revalidate.js approval <issue> is checked at the start of every item’s planning, and gate checks it before the movement test: a changed body loses approved with a comment saying why, the item is skipped (work_skipped), and gate exits 4. An approval older than the convention has no hash and is reported unbound, never revoked.
Observability. record writes each pass into session state (state.revalidation — armed boundary, entries, last verdict) and pushes a phase-history line, so bob warp status shows the last pass and bob warp log / bob warp status --log render the summary, e.g. revalidating -- area billing: 1 auto-adjusted, 0 blocker(s). The state dies with the session — no PM files.
Configuration
Section titled “Configuration”All settings live in .claude/settings.local.json under _workflow (one of four layers — see Workflow config). Edit directly or view with bob warp config.
Ownership (#2740). WORKFLOW_DEFAULTS in scripts/warp-drive/state-machine.js is the only declaration of a _workflow key; Workflow config is generated from it and states, per key, whether the engine honours it (a code path branches on it) or the agent does (an instruction file names it and nothing enforces it). The table below is a narrative subset — it explains the keys that need more than a comment, and it cannot add one: a key that is not on the generated page does not exist, however plausibly it reads here. make check (check-workflow-keys) fails on a key no runtime path honours, which is how five settings this table once listed — test_before_commit, auto_merge, heartbeat_on_commit, dev_health_check, dev_seed_check — were found to document behaviour the engine never had.
| Setting | Default | Purpose |
|---|---|---|
max_acs_per_commit |
3 | Max acceptance criteria per chunk |
max_phase_minutes |
30 | Timeout per phase before budget warning |
max_coding_cycles |
3 | Max code/test retries per chunk |
max_retries_per_chunk |
5 | Max total retries before escalation |
max_no_progress |
3 | No-progress stall halt: consecutive identical failed attempts (same failure, no new diff) before routing to budget_exceeded. See No-Progress / Stall Detection |
qa_scope |
proportionate |
Proportionate qa (#1979): browser-driven verification only when the item changed behaviour (code) or an AC names browser verification; a presentation-only change (templates, styles, config, copy, reorders) advances with qa_skipped and reason presentation-only. The engine classifies the changed files on entry (QA SCOPE block; state.qa.scope_decision). always = run E2E whenever there is a browser-testable surface |
quarantine_bug_filing |
true | At tests_passed / tests_failed, file (or bump by fingerprint) a bug on the project repo for each check the runner quarantined as flaky-on-base (#1979): same failure on HEAD and on the unmodified base. Best-effort — reported on the transition as quarantine_filing, never blocks. See Flaky-on-base quarantine |
todo_stale_after_days |
7 | Age floor for the groom stale-todo sweep (todo-consume.js stale, #2034): a todo younger than this many days is never flagged stale, and links that closed after the todo was filed read as likely-completed (surfaced with a hint, never labelled). 0 disables the floor. See Age floor and likely-completed |
testing_budget_minutes |
10 | Wall-clock budget for the whole scoped check pass in testing (#1979), enforced by run-checks.js — once spent it stops launching checks (skipped: budget), cuts a check still running, and the phase advances on what ran. The skipped list is surfaced in the chunk report and the commit’s Tests-Skipped: trailer, never silently. Distinct from max_phase_minutes (a warn-only phase timer). 0 = unlimited. See Scoping checks to the change |
max_auto_decisions |
10 | L4 decision budget (#876): max auto-decisions per session before a mandatory human digest checkpoint at budget_exceeded (never auto-continued past). 0 disables it. Only bites at Level 4 — below L4 the engine never auto-decides. Surfaced in bob warp status / bob warp config. See the reversibility decision engine. |
max_edits_per_file |
5 | Doom loop detection threshold |
pre_exit_verification |
true | Self-check before phase advance |
reconcile_per_chunk |
true | Auto-correct progress counters |
flightplan_tick_on_item_boundary |
true | Tick the finished requirement off the run’s flight plan on entering awaiting_continue, rather than trusting the prose check-off (#1757); idempotent, best-effort |
harness_defect_filing |
true | On entering chunk_complete and session_ending, file unreported harness_defects[] outward as de-duplicated harness-defect bug reports (#1836); best-effort, reported in the transition output as harness_defect_filing, never blocks |
harness_defect_escalate_after |
3 | Recurrence threshold (#1838): after filing, any bigbrain harness-defect issue whose Recurrences: count reaches this is bumped to p2-high with a comment saying why (idempotent — marker comment + existing p1/p2 skip). 0 disables. Project-owned defects are never escalated |
max_total_chunks |
20 | Circuit breaker: max chunks per session (hard limit) |
max_session_minutes |
480 | Circuit breaker: max session duration in minutes (hard limit) |
max_session_tokens |
0 (disabled) | Circuit breaker: max cumulative session tokens before routing to budget_exceeded. 0 disables the check, so existing sessions are unaffected unless configured. Token count is the reliable spend signal; trips mid-session on the accumulated chunk total. See Cost Budget. |
max_session_usd |
0 (disabled) | Circuit breaker: optional/secondary estimated-dollar ceiling (via token-report’s cost model). 0 disables it. Use max_session_tokens as the primary signal. |
segment_after_tokens |
120000 (enabled) | Session segmentation (L3+): checkpoint + resume in a fresh context once this many tokens accrue since the last checkpoint (per-segment, not cumulative). Default ON since #959 — 120k keeps segments comfortably below the ~150k high-cost context regime. Set an explicit 0 to disable. Soft threshold — hands work to a fresh session, does not halt. See Session segmentation. |
segment_after_chunks |
0 (disabled) | Session segmentation (L3+): same, triggered by chunks completed since the last checkpoint. 0 disables it. |
segment_at_seam_tokens |
40000 (enabled) | Seam-aware segmentation (L3+, #960): when the next queued item crosses a cap/area boundary from the just-finished one, segment at this lower per-segment token bar instead of waiting for segment_after_tokens. Hybrid — a seam lowers the bar, never forces a segment. 0 disables the seam trigger; the full segmentation opt-out keeps it inert. |
revalidate_on_area_entry |
true |
Per-area approval re-validation (#1008): selecting an item across a cap/area seam arms a revalidating sub-mode of planning — a movement gate compares HEAD against the item’s <!-- approved-at: sha --> marker, and only on movement does a lightweight AC re-check run (reversible drift auto-adjusted, irreversible surfaced). false restores trust-approval-blindly semantics. See Per-Area Re-Validation. |
phase_timeout_enforcement |
warn |
How max_phase_minutes breaches are handled: warn (advisory only), block (reject the offending transition until elapsed time recovers), or abort (route through budget_exceeded and auto-abort) |
doc_drift_gate |
false |
When true, the slash command fires commit_with_doc_gate after each commit, routing through the doc_drift_check phase before reporting. Mechanical drift is auto-fixed via /doc-sync; high-severity audit findings route the loop back to coding. See Doc Drift Gate. |
baseline_docs_check |
true |
At prerequisites, detect missing scaffolding docs on greenfield projects and file an unapproved req per gap (documentation,baseline-doc). Never authors docs (CAP-15 non-goal); idempotent; non-blocking. See Baseline Docs Check. |
baseline_docs |
["readme","runbook","claude"] |
Which baseline docs are required. |
baseline_docs_max_commits |
25 | Greenfield gate: skip the check on repos with more commits than this. 0 = always run. |
baseline_docs_auto_approve |
false |
When true, filed tracking issues also get the approved label, so warp-drive writes the doc itself in a later cycle (crosses the human-authored-narrative boundary). |
The decision-response timeout is not a _workflow key: it is the kickoff flag --timeout <minutes>, persisted on session.kickoff (default 10; 0 = never wait — work around or stop). At Level 4 it governs the decision-timeout policy one-way-door wait. The behaviour the five removed rows described still exists, but is not configuration: tests run in the testing phase, the merge route comes from the promotion matrix (promotion.js decide), RDB notifications are the per-phase table above, and the dev-health gate runs at chunk_complete.
phase_timeout_enforcement is the only budget threshold whose enforcement mode is configurable — retry_exceeded, coding_cycles_exceeded, no_progress, total_chunks_exceeded, and session_timeout are always enforced (they always route through budget_exceeded). Set phase_timeout_enforcement to warn (the default) for ordinary use and to block or abort only when you actively want phase-level timeouts to halt the loop.
For the full budget system, circuit breaker behavior, token capture pipeline, cost estimation, and reporting tools, see Token Monitoring & Budget System.
Every _workflow key the engine declares — default, comment and every file that reads it — is generated at Workflow config (#2041).
Verification Gates
Section titled “Verification Gates”You can configure custom shell commands that must pass before phase transitions:
{ "_workflow": { "verification_gates": { "code_complete": ["make lint"], "tests_passed": ["make type-check"] } }}If a gate command fails, the transition is blocked and Claude must fix the issue before advancing.
Doc Drift Gate
Section titled “Doc Drift Gate”When _workflow.doc_drift_gate=true, the loop routes through a new doc_drift_check phase between committing and reporting:
committing → (commit_with_doc_gate) → doc_drift_check ↙ ↘ drift_clean drift_blocked ↓ ↓ reporting coding (proceed normally) (fix high-severity drift)Inside the gate:
~/.claude/scripts/doc-keeper/sync.js --root . --applyruns and applies everyfix_kind: mechanicalfinding (label naming, etc.) and splices the generated blocks (the CLI table and the component count markers).~/.claude/scripts/doc-keeper/audit.js --root . --fail-on-driftre-audits.- If audit exits zero (no high-severity drift), the loop emits
drift_cleanand proceeds toreporting. Any sync edits are staged into a follow-up commit (or amended into the chunk commit, depending on policy). - If audit exits non-zero, the loop emits
drift_blockedand returns tocodingso Claude can address the high-severity findings before retrying the chunk.
The gate is opt-in (doc_drift_gate=false by default) for backward compatibility. When it’s off, the standard committed → reporting transition still runs.
The hook hooks/doc-drift-warning.sh runs after every successful git commit — even when the doc_drift_gate phase above is disabled — and fires when a commit touches tooling source paths (skills/, commands/, agents/, hooks/, bin/, scripts/, Makefile, registry/) without touching any *.md. It has two policy modes (#757), read from .claude/settings.local.json:
- Advisory (default) — warns and lets the commit stand (exit 0).
- Blocking (
_workflow.doc_drift_blocking=true) — exits 2 so Claude Code surfaces a blocking error the agent must resolve (amend the commit with the doc update) before continuing. - Suppressed (
_workflow.suppress_doc_drift_warning=true) — silent no-op.
The gate is PR-independent: the hook fires at commit time on any branch, and the CI backstop runs make docs-check on push: branches:[master] (docs-deploy.yml) — neither depends on a PR event, so direct-merge (L3) work is gated identically to PR-routed work. Blocking mode is the local teeth; the push-stage CI gate is the non-bypassable backstop.
Baseline Docs Check
Section titled “Baseline Docs Check”The Doc Drift Gate above keeps existing docs accurate. The baseline-docs check addresses the complementary gap: a greenfield project that never gets baseline scaffolding docs in the first place (a real miss observed on a fresh MVP scaffold — an operations runbook and project CLAUDE.md were never produced).
At prerequisites, scripts/warp-drive/baseline-docs-check.sh checks for a README, an operations runbook (RUNBOOK.md, runbooks/*.md, or docs/runbook*.md), and a CLAUDE.md. For each missing doc it files a GitHub issue labelled req,documentation,baseline-doc,p3-medium — without approved, so a human gates the writing.
Design boundaries (why it only surfaces the gap):
- Never authors narrative. Consistent with the CAP-15 non-goal —
README/CLAUDE.md/runbook prose stays human-authored. The loop files a tracking issue; a human (or, withbaseline_docs_auto_approve=true, a later cycle) writes it. - Idempotent. An open
baseline-docissue for a given doc is never refiled, so re-running prerequisites across sessions never duplicates. - Non-blocking. The check emits JSON and the loop continues regardless — it is advisory, never a gate.
- Greenfield-scoped. The
baseline_docs_max_commitsgate (default 25) skips mature repos, which already have these docs or have deliberately chosen otherwise. Set it to0to always run.
Configure via the baseline_docs* keys in Configuration. Disable entirely with baseline_docs_check=false.
Reasoning Budget
Section titled “Reasoning Budget”Control Claude’s reasoning effort per phase (the “reasoning sandwich” pattern — high reasoning for planning/verification, standard for implementation):
{ "_workflow": { "reasoning_budget": { "planning": "high", "coding": "standard", "testing": "high" } }}See Token Monitoring & Budget System for the full per-phase default table and how reasoning levels are applied.
What Gets Created
Section titled “What Gets Created”| Artifact | Where | Labels |
|---|---|---|
| Commits | Feature branch (conventional commit format) | — |
| Chunk reports | GitHub Issues | chunk-report, warp-drive |
| Session summary | GitHub Issue | session-report, session-summary, warp-drive |
| Lessons learned | GitHub Issues | lesson, warp-drive |
| Decisions made | GitHub Issues | decision, warp-drive |
| Deferred bugs | GitHub Issues | bug-deferred, warp-drive |
| TODO (timeouts) | GitHub Issues | todo, warp-drive |
| Abort reports | GitHub Issues | session-report, session-summary, warp-drive |
| PR (Level 2) | GitHub PR via gh pr create |
— |
GitHub labels are created automatically on first run (warp-drive, chunk-report, session-report, lesson, decision, risk, bug-deferred); the full set is shipped by scripts/bootstrap-labels.sh.
A report never carries lesson or decision (#2062). The first two rows are
execution artifacts — a record that a commit happened, that a session ran. The
next two are knowledge records, filed deliberately and read later by search.
A chunk report’s body narrates decisions and lessons, which is exactly why it
must not wear their labels. When a chunk produces a genuine lesson or
decision, file it as its own issue via /journal and link it from the
report — one record, one issue. make check enforces this
(check-report-labels.js), and scripts/warp-drive/relabel-reports.js corrects
history. The full glossary is Tracker labels.
See also
Section titled “See also”- Warp-Drive Guide — install, run, stop, remote mode, troubleshooting.
- Warp-drive state-machine flags — every subcommand and flag, generated.
- Kickoff Flags — what the kickoff flags do to a run.
- Warp-Drive State Reference — state file, CLI, transition table.
- Warp-drive contract — why the phase table is frozen.