npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pounceai/bob-control

v2.2.0

Published

Bob Control: MCP server + CLI + worker that runs IBM Bob (and any MCP-capable agent) unattended against a SQLite task board.

Readme

Bob Control

CI npm MCP

Bob Control turns an AI coding agent from a one-prompt-at-a-time chat window into an unattended queue worker. Fill a board and each task gets dispatched, auto-approved inside risk guardrails, answered when the agent asks a question, verified (a command check or an LLM judge), retried on transient failures, and written back — in dependency and priority order, with no one watching. Gating and an acceptance judge keep it honest, and IBM Bob and Claude Code can share one board so each does the work it's best at.

Concretely: an MCP server + CLI + auto-dispatch worker over a SQLite task board. The worker pulls each task in dependency and priority order and dispatches it to IBM Bob (IBM's AI coding agent for IBM i), then watches it run to completion. On Bob 2.0 that dispatch is in-process (the companion extension calls Bob's exported startTask API); on Bob 1.x it's live over the node-ipc pipe — auto-detected, one build. Bob is the MCP client; this is the server it connects to — and Claude Code speaks the same MCP, so it can work the same board as foreman (provision / route / triage) or worker.

One build, both Bobs — auto-detected. Still on 1.x and want the last pipe-only release? Grab v1.1.0 from Releases. Full matrix: CHANGELOG → Compatibility.

Where it fits

This is the runtime execution + orchestration layer: it actually drives a live agent, keeps it unattended, and verifies the output. That's a different job from planning / governance CLIs (e.g. MissionForge), which deterministically decompose and scope work but don't run an agent. The two compose — plan upstream, then queue the pieces here with depends_on ordering and let the worker drain them. What this layer owns that a planning CLI doesn't: live agent control (dispatch, auto-approve, verify), LLM-judged acceptance, a multi-agent shared board, and command/risk gating.

Setup

npm install        # not --ignore-scripts: esbuild's postinstall fetches its binary
npm run build      # tsc -> dist/, then bundles claude-plugin/server/server.mjs
npm run smoke      # optional self-test

Or skip the clone and run the published package — the MCP server (bob-control) and CLI (bob-tasks) come straight from npm. Point an MCP client at it instead of an absolute dist/ path:

{
  "mcpServers": {
    "bob-tasks": { "type": "stdio", "command": "npx", "args": ["-y", "@pounceai/bob-control"] }
  }
}

Under Claude Code the board resolves automatically (it sets CLAUDE_PROJECT_DIR); any other MCP client should add "env": { "BOB_TASKS_DB": "…" } pointing at the board the worker drains — a bare npx server with no board env writes to a throwaway path inside the npx cache that nothing can share.

Windows + nvm-for-windows: npx can fail to resolve the bin ('bob-control' is not recognized). Install globally and run it with plain node instead: npm i -g @pounceai/bob-control, then set command to node and args to ["<npm root -g>/@pounceai/bob-control/dist/server.js"].

The unattended worker and the Claude Code plugin still want the repo (see below).

Needs Node 22.5+ (uses the built-in node:sqlite, so there's no native build step). Board path resolution: BOB_TASKS_DB (explicit) › BOB_TASKS_PORTABLE=1 (a shared ~/.bob-tasks/tasks.db) › BOB_TASKS_WORKTREE_SHARED=1 (every linked git worktree resolves the main worktree's data/tasks.db; a no-op for non-worktree dirs) › CLAUDE_PROJECT_DIR/data/tasks.db (Claude Code sets CLAUDE_PROJECT_DIR in every MCP server it spawns — plugin and a plain terminal/project .mcp.json — so both resolve the same project board) › repo-local module-relative data/tasks.db. The plugin points each project at its own board (see Boards are per project). On first open the store also writes a .gitignore beside itself, so the board never lands as untracked state in a consuming repo.

All processes must agree on one board. The plugin MCP, a terminal-configured MCP, the worker, and Bob each resolve tasks.db independently; if they diverge, await_task polls a board nothing writes to and never settles. The server logs the resolved board path on startup — confirm every process prints the same one. The worker is launched outside Claude Code (so it has no CLAUDE_PROJECT_DIR); point it with an explicit BOB_TASKS_DB matching the project board.

Connecting Bob

Bob reads project MCP servers from .bob/mcp.json. Copy the template and point it at your clone:

cp .bob/mcp.json.example .bob/mcp.json
# edit the four /absolute/path/to/bob-control paths

The template pins BOB_TASKS_DB to the repo's own data/tasks.db (boards are per project — see below); for one shared queue across every repo, replace that env entry with "BOB_TASKS_PORTABLE": "1" on both Bob and the plugin.

Bob hot-reloads .bob/mcp.json on save and lists bob-tasks under the MCP Servers panel's Project tab. To make it available in every workspace, put the same mcpServers block in the global file instead:

%APPDATA%\IBM Bob\User\globalStorage\ibm.bob-code\settings\mcp_settings.json

Bob 2.0 keeps its settings under ~/.bob/ rather than the %APPDATA%\IBM Bob path above; the simplest version-independent route is the in-app MCP Servers panel (+ Add Server), which writes to whichever location your Bob build uses. The mcpServers block is identical either way — this wiring is independent of the dispatch transport.

Bob Companion (Claude Code plugin)

Because the connector is an MCP server and Claude Code is an MCP client, Claude Code can work the same board Bob drains — as the foreman (provision / route / triage) and as a worker (claim and execute tasks itself). It ships as a self-contained Claude Code plugin in claude-plugin/ so you can use it from any repo, not just this one.

The plugin bundles the connector into a single server/server.mjs (esbuild; no native deps thanks to node:sqlite), so install is all that's required — no checkout needed at runtime. Build it, add the local marketplace, install:

npm install && npm run build      # build also bundles claude-plugin/server/server.mjs
# in Claude Code, from anywhere:
#   /plugin marketplace add /absolute/path/to/bob-control
#   /plugin install bob-companion@bob-control

| Surface | What it does | | ------- | ------------ | | /bob-new <desc> | Turn a rough ask into one well-formed task (title, criteria, priority, tags, routed mode) | | /bob-board [tag] | The board grouped by status, in pull order | | /bob-next [tag] | What Bob pulls next and the mode it routes to (read-only; never claims) | | /bob-route <id\|text> | Predict the dispatch mode for a task or a hypothetical | | /bob-triage [focus] | Review the board, propose fixes, apply the safe ones on confirm | | /bob-review-diff [range] | Queue a read-only Bob (review-mode) code review of the diff you just made; findings land on the board | | /bob-work [tag] [--max N] | Worker: claim pending tasks Claude can do, execute them, submit results | | /bob-statusline | Install (or --remove) a live one-line board summary in your Claude Code status line | | bob-foreman subagent | Split a large request into several correctly-routed, ordered tasks | | Skills (model-invoked) | Ask for IBM Bob by name to dispatch its specialty modes: bob-review (review), bob-plan (plan), bob-refactor (refactor), bob-security (devsecops) |

The dispatch surfaces — bob-review / bob-plan / bob-refactor / bob-security and /bob-review-diffblock on Bob via await_task and present the result in the same turn, so a review / plan / fix runs end-to-end hands-off while a worker is draining the board (npm run worker); with no worker draining they fall back to "queued as #id".

Boards are per project

The plugin's MCP server points at the open project's own board — ${CLAUDE_PROJECT_DIR}/data/tasks.db — so every repo has its own queue. For Bob to share a project's queue, its .bob/mcp.json must set the same BOB_TASKS_DB for that project; tools/init-project-board.mjs <dir> scaffolds a correct per-project .bob/mcp.json (pointing at the shared connector's dist/server.js, with BOB_TASKS_DB/cwd set, and creates data/). /bob-work claims tasks as claude and leaves IBM-i/RPG work for Bob, so the two never double-work a task.

Prefer one queue across every repo instead? Set BOB_TASKS_PORTABLE=1 on both the plugin and Bob to share a fixed ~/.bob-tasks/tasks.db. Either way the server logs the resolved board path on startup, so you can confirm both tools agree.

From a plain terminal (no plugin)

A plain claude terminal session gets the tools from a project/user .mcp.json but not the plugin's skills (/bob-work, bob-review, …) that script the dispatch loop — so without guidance it may fall back to polling the board by hand instead of blocking. Two things make it work like the plugin:

  1. Point it at the project board. In a hand-written .mcp.json, ${CLAUDE_PROJECT_DIR} is only substituted when you give it a default — ${CLAUDE_PROJECT_DIR:-.} (bare ${CLAUDE_PROJECT_DIR} is a plugin-only direct substitution). The server also reads CLAUDE_PROJECT_DIR from its own environment, so even an unset BOB_TASKS_DB resolves the same project board the plugin uses:

    // <project>/.mcp.json
    {
      "mcpServers": {
        "bob-tasks": {
          "type": "stdio",
          "command": "node",
          "args": ["/absolute/path/to/bob-control/dist/server.js"],
          "env": { "BOB_TASKS_DB": "${CLAUDE_PROJECT_DIR:-.}/data/tasks.db" }
        }
      }
    }
  2. Drive it with await_task, don't poll. After create_task, call await_task to block until the task settles; if it returns needs_input, answer_task_question then await_task again. Don't loop on list_tasks/board_status to watch a task — await_task blocks server-side and needs a worker draining the board (npm run worker against the same BOB_TASKS_DB).

Tools

| Tool | Purpose | | ---- | ------- | | create_task | Add a task (staged:true = created non-pullable, for bulk curation) | | list_tasks | List tasks, filter by status/tag | | get_task | One task plus its notes and any open pending_question | | get_next_task | Highest-priority pending task; optionally claim it (null while disarmed) | | claim_task | Mark in_progress and assign (only pending, only while armed) | | update_task_status | Set status (pending / in_progress / blocked / analysis_done / done / cancelled; staged & needs_input are set by their own tools) | | add_task_note | Append a progress note | | submit_result | Complete a task: read-only run → analysis_done; implementation reaches done only with evidence | | set_task_mode | Set or clear a task's mode slug | | set_task_dependencies | Set/clear the task IDs this one waits on (all must be done/analysis_done); rejects cycles | | record_artifact | Record a file/commit/test a worker produced (delete-safety + done-evidence) | | delete_task | Delete a task; warns/refuses if it recorded artifacts unless force, with optional cleanup | | revert_task | Roll the working tree back to the task's pre-task checkpoint (undo what it changed) | | board_report | Markdown standup/audit of the board, grouped by status | | board_status | Dispatch state: armed?, worker-active heuristic, counts by status | | arm_board / disarm_board | Resume / pause all dispatch (curation gate) | | release_tasks | Move staged tasks → pending (release after curation) | | ask_question | Raise a question for a human; parks the task needs_input (never guess) | | answer_task_question | Answer a worker's question (by question_id); resumes the worker | | await_answer | The worker's blocking wait for an answer (poll of the shared board) | | await_task | Block until a task settles (done / analysis_done / blocked / cancelled / needs_input), then return it with its result — dispatch-and-wait |

A typical Bob loop: get_next_task {claim:true} then add_task_note while working, then submit_result. If it needs a value it can't determine, ask_questionawait_answer rather than guessing.

From the foreman side, a dispatcher that queues work can create_taskawait_task to block until Bob finishes and act on the result in the same turn — no board polling. await_task is a chunked poll of the shared board (each call stays under the MCP timeout; call again if it returns {status:'waiting'}), so the settling write Bob's worker makes "hooks" you straight back into your turn. It resolves on every settled state — including needs_input, where it hands back Bob's open question so you can answer_task_question and await_task again.

CLI

The CLI shares the same store:

node dist/cli.js create "Modernize INVRPT report" --priority high --tags rpg
node dist/cli.js create "Bulk task" --staged             # non-pullable until released
node dist/cli.js list --status pending --tag rpg
node dist/cli.js show 1
node dist/cli.js claim 1 --assignee bob
node dist/cli.js status 1 blocked                        # set status (rejects staged/needs_input)
node dist/cli.js mode 1 refactor                         # set/clear the mode slug
node dist/cli.js deps 3 1,2                               # task 3 waits on 1 and 2 (empty clears)
node dist/cli.js note 1 "Waiting on test data"
node dist/cli.js result 1 "Done; 3 procedures extracted"
node dist/cli.js next                                    # next pending task + its routed mode
node dist/cli.js delete 1 [--force] [--cleanup]          # refuses if it wrote files; cleanup removes created ones
node dist/cli.js revert 1 [--force]                      # roll the tree back to task 1's checkpoint while one exists (--force if HEAD moved)
node dist/cli.js stats
node dist/cli.js report                                  # markdown standup/audit of the board
node dist/cli.js report --status blocked --out report.md

# curation gate (pause dispatch while you triage, then release)
node dist/cli.js disarm "triaging"                        # no worker pulls until armed
node dist/cli.js release --tag batch                      # staged -> pending (all, or by ids/tag)
node dist/cli.js arm                                      # resume dispatch
node dist/cli.js board                                    # armed? + counts by status

# answer a worker's question (see "Asking a human" below)
node dist/cli.js questions                                # open questions awaiting an answer
node dist/cli.js answer 5 <question_id> pool size is 20

report groups tasks by status in pull order, each with age, idle time, the latest note, and a ⚠ stalled flag for in_progress work idle over 30 min — handy for a standup or spotting wedged tasks. It also prints an audit summary tallying unattended automation activity from task notes: classifier approvals/denials, answerer answers/escalations, and human actions (with an estimated classifier cost). Same output is available to agents via the board_report MCP tool.

Modes

Each task carries a mode (or routes to one); on dispatch the connector sends it to Bob along with a matching risk level and auto-approve profile (see src/modes.ts). Eight built-in modes:

| Mode | Risk | What it's for | | ---- | ---- | ------------- | | ask | safe | Read-only — explain / research / document. No writes or commands. | | plan | safe | Read-only planning/design — produces a plan, no code changes. | | review | safe | Read-only code review — returns structured findings (see below). | | code | standard | Normal edit + build + run. The default. | | refactor | standard | Behavior-preserving restructuring/cleanup. | | devsecops | standard | Security-focused remediation — finds and fixes vulnerabilities (write-capable; the LLM judge verifies the fix). | | orchestrator | standard | Coordinate a multi-step epic with sub-tasks. | | advanced | elevated | code plus MCP + browser power. |

Leave a task's mode blank and the keyword router picks one from the title, description, and tags. The router only auto-routes to ask, advanced, orchestrator, or code; the specialty modes (plan / review / refactor / devsecops) are reached by setting the mode explicitly, a tag naming the mode, or the plugin's model-invoked skills.

node dist/cli.js create "Explain the IPC envelope"   # routes to ask
node dist/cli.js create "Fix bug in db.ts"           # routes to code
node dist/cli.js create "Add export" --mode refactor # explicit
node dist/cli.js route 1                             # preview the routed mode

Precedence: explicit mode, then a tag naming a mode, then the keyword router, then code.

Worker

The worker drains the board one task at a time: pull the next eligible task (highest priority whose dependencies are all done), route it, dispatch to Bob in the sidebar, wait for the result, write it back, mark it done (verification and retry below are opt-in).

npm run worker              # drain, then idle-poll
node dist/worker.js --once  # one task then exit
node dist/worker.js --tag rpg
node dist/worker.js --dry-run

Flags: --once --tag <t> --dry-run --pipe --poll --timeout --assignee --new-tab --max-risk <safe|standard|elevated> --retry <N> --detect-plan-stop --no-notify --no-defer --answer-followups --escalate-all --review-plans --allow-commands <prefix,prefix> --deny-commands <prefix,prefix> --no-command-gate --allow-all-commands --no-checkpoint --no-idle-watchdog --idle-timeout <ms> --no-budget --budget-cap <tokens> --max-turns <n> --webhook <url> --webhook-secret <s> (plus the verify-and-continue flags below). Needs Bob running with IPC enabled (see below). Aborted or timed-out tasks are parked as blocked; with --retry <N> the worker first re-dispatches a transient failure (timeout/abort) up to N times before parking it. --detect-plan-stop catches a "completion" that wrote no code (Bob just presented a plan) and continues it instead of marking it done.

The worker is resilient across Bob restarts: if the IPC pipe drops it reconnects on the next dispatch (instead of wedging), and on startup it re-queues any task a previous run left in_progress when it died mid-dispatch, so nothing is stranded.

Each mode has a risk level, and the worker only dispatches tasks at or below --max-risk (default standard); higher-risk ones stay pending for manual dispatch. On finish the worker pops a tray toast (--no-notify to silence; the system sound and terminal bell are off by default).

--webhook <url> POSTs the notable transitions — a task finishing, blocking, needing input, or retrying, and the worker itself stopping or erroring — to a URL as application/json. One payload serves three consumers: it carries text (a Slack incoming webhook renders it), content (a Discord webhook renders it), and the structured { event, seq, data, worker, ts } for a generic receiver (seq monotonic within worker.run, a per-process id, so a restart reads as a new run, not dropped deliveries; concurrent POSTs can be reordered). Delivery is best-effort: a POST never blocks or crashes the drain, bursts past an in-flight cap to a slow endpoint are dropped, and the worker flushes pending POSTs before it exits so the final event still lands. The URL is validated at startup (http/https only) and only its host is logged, since a Slack/Discord webhook URL carries its secret in the path. The payload transmits the repo path (cwd), task titles, and Bob's question text to that URL — point it only at an endpoint you trust; --webhook-secret <s> HMAC-signs the body (X-Bob-Signature: sha256=…) so a generic receiver can verify authenticity.

Run it standing (hands-off loop). Creating a task doesn't start Bob — a worker has to pull it. Keep one always draining and the plugin's dispatch skills become end-to-end hands-off: they create_task, then await_task hooks back with Bob's result in the same turn (no manual dispatch, no "check back later"). launch-worker.cmd starts one; autostart it at logon so it's always up. No admin: drop a shortcut to it in your Startup folder (Win+Rshell:startup). With an elevated terminal you can use a Scheduled Task instead (Task Scheduler needs admin):

schtasks /create /tn "BobWorker" /sc onlogon /tr "\"<repo>\launch-worker.cmd\"" /f   # remove: schtasks /delete /tn "BobWorker" /f

Task dependencies

A task can declare depends_on — other task IDs that must all be done before it becomes eligible. The worker's pull queue skips a task while any dependency is unmet (surfaced as blocked on dependencies), so you can queue an ordered pipeline and let one drain run it end to end. Dependencies are validated when set: a missing ID is rejected, and an edge that would form a cycle is refused (DFS check in src/db.ts).

node dist/cli.js create "Write tests for INVRPT"        # -> #5
node dist/cli.js create "Refactor INVRPT" --depends-on 5  # won't dispatch until #5 is done
node dist/cli.js deps 6 5                                # set/clear deps on an existing task (empty clears)

Over MCP, pass create_task {depends_on:[…]} or call the set_task_dependencies tool.

Unattended execution

Each dispatch sends the mode's auto-approve profile — the autoApprovalEnabled master switch, per-category toggles, and a curated SAFE_COMMANDS allowlist — so Bob runs without stalling on approval prompts. Read-only modes (ask/plan/review) take no writes; the write-capable modes (code/refactor/devsecops/orchestrator/ advanced) add writes; all of them auto-run allowlisted commands (build/test/vcs) and prompt for anything else. This per-dispatch profile is enough for the worker on its own; to apply the same auto-approve to your interactive Bob session, fully quit Bob and launch via launch-bob-ipc.cmd (it runs set-bob-autoapprove.mjs, which writes the same allowlist to Bob's global state).

For the gray zone (a command not on the allowlist), the worker resolves the approval prompt non-interactively by default — a headless dispatch never waits on a human. A deterministic permission gate (command-policy.ts) evaluates the command: an allowlisted one (safe local git subcommands, pytest / uv run pytest / python -m pytest, python, scoped __pycache__ cleanup) is approved; a denied or unrecognised one (git push, network installs, curl/wget/sudo, rm -rf outside the repo) is rejected, recorded as a needs_input question (the exact command + cwd + task), and the dispatch is ended promptly so a blocked command can't burn the wall-clock. Tune it with --allow-commands / --deny-commands (comma-separated), or:

node dist/worker.js --allow-commands "make build,go test"   # extend the auto-run allowlist
node dist/worker.js --no-command-gate                       # disable the gate (idle watchdog stays the backstop)
node dist/worker.js --allow-all-commands                    # SANDBOX ONLY: auto-run every command, gate off

For an unrecognised command the deterministic gate default-denies; to instead have Claude judge the gray zone, add --command-classifier:

node dist/worker.js --command-classifier                             # cli backend: reuses your Claude login, no key
node dist/worker.js --command-classifier --classifier-backend api    # one Anthropic call (needs ANTHROPIC_API_KEY)

The classifier presses approve/reject over IPC, which needs a one-time patch that exposes Bob's buttons — node tools/patch-bob-buttons.mjs, then restart Bob. Fail-safe: only an explicit "approve" runs a command; any error or timeout falls back to the deterministic default-deny. Extra flags: --classifier-backend <cli|api> --classifier-model --classifier-cli.

The other thing that stalls an unattended task is Bob asking a question mid-task (e.g. "which approach should I take?"). With --answer-followups, the worker asks Claude to answer — preferring one of Bob's offered options — and sends the reply back over IPC (a native SendMessage, so no button patch). It applies in any mode and uses the same backend as the classifier. Fail-safe: when the answerer is unsure or the question is consequential (deletes, scope changes), it escalates to you (a desktop toast + a note on the task) instead of guessing.

node dist/worker.js --answer-followups                  # Claude answers Bob's questions; escalates when unsure
node dist/worker.js --answer-followups --escalate-all   # escalate ALL questions to you (including plan approvals)
node dist/worker.js --answer-followups --review-plans   # escalate plan/design questions, auto-answer mechanical ones

With --escalate-all, every followup question is escalated to you for review instead of being auto-answered — ensuring you see and approve plans/designs before Bob proceeds. Off by default (Claude answers confident questions); only applies when --answer-followups is on.

With --review-plans, plan/design-approval questions ("should I proceed with this refactor?", "which approach should I take?") are escalated to you for review, while mechanical clarifications ("which file?", "flag name -x or -y?") are auto-answered. This is a middle ground between --escalate-all (blocks on every question) and the default (auto-answers everything confident). When both --review-plans and --escalate-all are set, --review-plans takes precedence.

Verify-and-continue with LLM judge

When --verify-and-continue is on, the worker runs an acceptance check after Bob completes a task and loops back to Bob to fix issues until it passes or --max-continues is reached. This catches broken builds/tests without human intervention.

By default, with no --verify-command set, the loop blind-passes (no reliable heuristic). Add --verify-judge to use an LLM judge that reviews Bob's completion against the task criteria and actual code changes (git diff). The judge uses the same backend as the command classifier (reuses your Claude login or ANTHROPIC_API_KEY). The diff is scoped to this task's changes — captured against a pre-dispatch baseline so pre-existing edits in a dirty tree aren't attributed to the task, and new (untracked) files the task creates are included.

node dist/worker.js --verify-and-continue --verify-judge                    # judge is sole verifier
node dist/worker.js --verify-and-continue --verify-command "npm test" --verify-judge  # command first, then judge
node dist/worker.js --verify-and-continue --verify-command "npm test"       # command only, no judge

When both --verify-command and --verify-judge are set, the command runs first and must pass; the judge then provides an additional gate (both must pass). When only --verify-judge is set, the judge is the sole acceptance signal. The judge fails safe: any LLM error or timeout is treated as a pass (logged as a task note) so infrastructure failures never block tasks.

Resilience guards: checkpoint-before-death, idle watchdog, token budget

The worker hardens each unattended dispatch against the ways it can die without finishing. These are on by default (in a git repo); tune or disable them with the flags noted.

Checkpoint-before-death → branch. Before each task the worker captures a pre-task git snapshot (pinned, gc-safe). On any terminal failure — an idle / blocked-on-ask trip, a token budget abort, a no-result timeout/abort (after retries), a verify/judge give-up, or a worker error — it preserves the dispatch's partial work to a dedicated bob/task-<id> branch and restores the working tree to its pre-task state, so a dying run never leaves uncommitted WIP on main. Recover the stranded work with git checkout bob/task-<id>; the failure note records the branch and root cause. --no-checkpoint disables it; no-op outside a git repo, or when the task committed its work (HEAD moved — see Verified below).

The same machinery powers the manual bob revert <id> / revert_task rollback, which restores a task's pre-task checkpoint while one still exists. The checkpoint is consumed on completion (both on success and when failed work is preserved to a branch), so it never pins a commit per task forever — which means revert is a no-op once a task has terminated; recover a failed task's work from its bob/task-<id> branch instead.

The manual rollback is destructive, so it's safe by construction (src/checkpoint.ts):

  • Repo-bound — the checkpoint records the repo it came from; a revert run in any other working tree is refused, never applied to the wrong repo.
  • Pinned — the snapshot is held by a real ref (refs/bob/checkpoint/<id>), so git gc can't reclaim it before you revert.
  • Verified — if the snapshot is missing, or HEAD moved since capture (a commit landed), the revert refuses rather than silently half-restoring or orphaning a commit (--force / force:true overrides the HEAD-moved check).
  • Recoverable — the pre-revert state is pinned to a refs/bob/recovery/<sha> ref first, so discarded work is never truly lost (the command prints the ref).
  • HEAD-preserving — tracked files are restored with git read-tree (HEAD/branch untouched); only files the task created are removed (emptied dirs pruned). Pre-task edits and pre-existing untracked files are left intact.

No-op outside a git repo. Gitignored files a task creates aren't auto-removed (they're not the tracked snapshot's concern). Deleting a task also drops its checkpoint ref; refs/bob/recovery/<sha> refs from past reverts are kept as a safety net and can be pruned by hand once you're sure you don't need them.

Idle / blocked-on-ask watchdog. A dispatch that makes no progress for --idle-timeout (180s default) — or wedges on a prompt the headless worker can't answer (e.g. a command-permission ask) — ends as idle well before the wall clock, after a short --blocked-ask-grace (10s). The pending ask is surfaced to the board as a needs_input question rather than swallowed. --no-idle-watchdog disables it (wall-clock --timeout only).

Token / turn budget. Each task carries an estimated token scope (set at creation — see right-sizing); the worker enforces estimate × (1 + --budget-headroom%) (floored), or a flat --budget-cap when there's no estimate, as a hard ceiling that aborts a runaway loop as budget. --max-turns adds an api-request cap; --no-budget disables both. (Best-effort: it reads Bob's api-request token usage, and falls open — never trips — if that usage isn't reported.)

Right-sizing at creation. create_task estimates a task's single-dispatch scope from its description length, the files it names, and its mode (scope.ts) and stamps the estimated_tokens the budget ceiling above is derived from. An oversized task is tagged too-big, and an oversized implementation task with no explicit mode is routed to orchestrator — which decomposes it into subtasks — rather than dispatched doomed to a mid-work timeout.

Review-mode findings

When a review task finishes, the worker captures Bob's findings to the board. Bob's native review writes structured issues (severity, location, category, often a suggested fixed_diff); under headless dispatch it's tool-restricted and returns the review as completion text instead, so the worker parses that text back into structured findings (src/review-findings.ts). Either way the findings land as the task result plus a bob-review note — the board, not Bob's webview panel, is the source of truth. Queue one with /bob-review-diff (or the bob-review skill / --mode review).

Templates

create --template <name> applies a preset mode, priority, tags, and description scaffold:

node dist/cli.js templates
node dist/cli.js create "INVRPT report" --template bug-fix

Built-ins: bug-fix, feature, research, code-review, doc, refactor.

VS Code / Bob extension

extension/ packages the auto-dispatch loop as an IDE extension ("Bob Tasks") for IBM Bob (and stock VS Code). It claims and dispatches queued tasks from inside the editor, with a status-bar item, native completion/failure toasts, a bobTasks.maxRisk gate, and defer-while-chatting so it never aborts a live Bob conversation.

It auto-detects the Bob it's running in and picks the transport:

  • Bob 2.0 (current) — runs the dispatch loop in-process and calls Bob's exported startTask API directly; no child process, no pipe. Completion is read from Bob's task store (~/.bob/db/bob.db). Headless auto-approve is written to Bob's global settings, gated by bobTasks.autoApproveGlobal (default on; a one-time notice fires on the first write — see extension/README.md).
  • Bob 1.x (legacy) — spawns dist/worker.js, which dispatches over the node-ipc pipe; needs Bob launched with ROO_CODE_IPC_SOCKET_PATH (launch-bob-ipc.cmd).

A URI handler (ibm-bob://local.bob-tasks/start) lets a process outside the editor — e.g. Claude Code in WSL — start or stop the loop without a Command Palette click. It's independent of the MCP server and CLI; on Bob 1.x you can skip it and run npm run worker instead. Build/install steps and the full bobTasks.* settings reference live in extension/README.md.

Board safety gates

Guardrails for running unattended at scale — so a bulk-create can't race a live worker, and "done" means something:

  • Staging / arming. Create tasks staged (non-pullable) and release_tasks them once you've reviewed the set, or disarm_board to pause all dispatch while you curate, then arm_board. A worker never pulls a staged task or pulls while disarmed (enforced in the board layer, so both the worker and /bob-work obey it).
  • Done-integrity. A read-only run (ask/plan/review) terminates as analysis_done, not done. An implementation task reaches done only with execution evidence (files changed / commit / test); without it the worker parks it analysis_done rather than show green for unbuilt work. submit_result takes an evidence field; the worker derives it from the task's git diff.
  • Delete-safety. Workers record the files/commits they produce as task artifacts, so delete_task warns and refuses when a task already wrote files (unless force); cleanup removes only files the task created — never source it merely edited.

Asking a human (board round-trip)

When a worker needs a value it can't determine, it asks on the board and waits — it does not guess. ask_question parks the task needs_input and writes the question (with optional multiple-choice options) where any board client can see it: get_task returns a pending_question, and board_report lists an "❓ Awaiting answer" group with how long it's been waiting. The worker then blocks on await_answer (a poll of the shared board).

Anyone answers with one call — answer_task_question(task_id, question_id, answer) over MCP, or bob answer <id> <question_id> <text> from a terminal — and the waiting worker resumes. Answers are matched by a unique question_id, so a stale answer can't apply to a new question. Fail-safe: a question unanswered past its deadline times out and the task parks blocked (a board-activity sweep fires this even if the asking worker died) — never a fabricated answer, never a silent done. The bob-work skill follows this path instead of inventing a value.

Driving Bob over IPC (Bob 1.x — legacy)

Bob 1.x only. IBM Bob 2.0 removed the node-ipc server, so there's no pipe to drive — on 2.0 the companion extension drives Bob in-process (see the extension), and the outside-process commands here (--cancel, --list-pipes, a direct StartNewTask) have no 2.0 analog: 2.0 exposes no external control channel.

Bob starts a node-ipc server when launched with ROO_CODE_IPC_SOCKET_PATH set. bob-control.mjs connects to it, sends StartNewTask, and streams back Bob's events:

node bob-control.mjs "Refactor src/db.ts"
node bob-control.mjs --mode ask "Explain this codebase"
node bob-control.mjs --cancel <taskId>
node bob-control.mjs --list-pipes

To enable it, fully quit Bob, then relaunch from the Start menu or launch-bob-ipc.cmd (which also enables auto-approve for unattended runs — see above).

Worktrees: run N in parallel

Keep main, feat-a, … checked out at once and let Bob work in each at the same time on one shared board, without two windows racing the same checkout. A task is pinned to a worktree with a worktree:<name> tag (routing); each worktree gets its own Bob window draining only its slice.

The shared-board machinery is identical on both Bob versions: BOB_TASKS_WORKTREE_SHARED=1 resolves the main worktree's data/tasks.db from any linked worktree, and the worktree:<name> tag pin — the one exact-match filter shared by the worker's pickEligible, nextTask, and get_next_task — routes each task end-to-end. What differs between Bob versions is only how each window's dispatch is isolated.

Bob 2.0 (in-process — recommended)

2.0 makes this simpler: with no IPC pipe, each window's in-process loop dispatches into its own workspace — the per-instance pipe plumbing (the 1.x section below) is gone. Per worktree:

1. Add the worktree and build once in the main checkout (each window's loop loads its dist/):

git worktree add ..\bob-control-feat-a feat-a
npm run build

2. Open it in its own Bob 2.0 window with a distinct --user-data-dir — the one piece carried over from the 1.x launcher, since Electron's single-instance lock otherwise refuses a 2nd window:

bobide --user-data-dir "%LOCALAPPDATA%\bob-wt\feat-a" ..\bob-control-feat-a

3. Point that window's loop at its slice via the worktree's .vscode/settings.json (workspace scope); with autoStart the in-process loop comes up draining only its tag on the shared board:

{ "bobTasks.tag": "worktree:feat-a", "bobTasks.worktreeShared": true, "bobTasks.autoStart": true }

4. Pin tasks to a worktree and each dispatches only into that window:

node dist/cli.js create "Add export to feat-a" --tags worktree:feat-a

No pipe, no launch-bob.cmd, no print-pipe-name.mjs, no per-window worker.js. The isolation rules below carry over. Not carried over: the worker lease (board_status.worker_leases) — the in-process loop is one-per-window by construction, so there's no cross-worker lease to coordinate, and board_status's worker fields won't reflect the 2.0 loops (they read the 1.x heartbeat). Opening the same worktree in two windows is the one unguarded misconfig; don't.

Bob 1.x (IPC pipe — legacy)

The model: one Bob window and one worker per worktree, sharing the main worktree's data/tasks.db. A task is pinned to a worktree (routing); a worktree is leased by one live worker at a time (exclusivity). Both are required because a Bob 1.x window owns one IPC pipe and edits one folder, so a task only reaches worktree X if a Bob+worker already bound to X pulls it.

1. Add the worktrees (each on its own branch) and build once in the main checkout — the launcher and worker run its dist/:

git worktree add ..\bob-control-feat-a feat-a
git worktree add ..\bob-control-feat-b feat-b
npm run build

2. Launch one Bob per worktree. launch-bob.cmd binds a Bob to one workspace on a distinct pipe + --user-data-dir (so two Electron instances coexist), seeds auto-approve into %LOCALAPPDATA%\bob-instances\<slug>, and scaffolds bobTasks.pipe into that worktree's .vscode/settings.json. The pipe is derived from the workspace path by tools/print-pipe-name.mjs — the single source of truth, shared by the launcher and the worker — so concurrent windows never collide on one global pipe:

.\launch-bob.cmd C:\dev\bob-control            # main window
.\launch-bob.cmd C:\dev\bob-control-feat-a     # feat-a window

3. Start one worker per worktree — cwd = the worktree (so checkpoints / diffs / evidence run against its files and the lease keys on its path), the shared board on, pinned to its tag, and pointed at its Bob's pipe:

cd C:\dev\bob-control-feat-a                                                  # worker cwd = the worktree
$env:BOB_TASKS_WORKTREE_SHARED = "1"                                          # resolve the MAIN worktree's board
$env:ROO_CODE_IPC_SOCKET_PATH  = (node C:\dev\bob-control\tools\print-pipe-name.mjs C:\dev\bob-control-feat-a)
node C:\dev\bob-control\dist\worker.js --tag worktree:feat-a

BOB_TASKS_WORKTREE_SHARED=1 makes defaultDbPath() resolve the main worktree's board from any linked worktree (a no-op for non-worktree dirs — see Setup). The lease refuses a 2nd worker on the same checkout and reclaims a dead one after the heartbeat window; board_status lists worker_leases so a foreman sees who owns what. (In-editor: set bobTasks.tag + bobTasks.worktreeShared; bobTasks.pipe is scaffolded by step 2.)

4. Pin tasks to a worktree and each dispatches only into that window:

node dist/cli.js create "Add export to feat-a" --tags worktree:feat-a
node dist/cli.js create "Fix feat-b regression" --tags worktree:feat-b

Rules that keep it isolated

The pin is a convention the tag filter enforces at discovery, not at claim (both Bob versions), so:

  • Tag every worker and every task; never run an untagged worker beside them — an untagged puller (worker with no --tag, bob next, /bob-work) sees every pinned task.
  • Keep a task's depends_on in its own worktree — a worktree:feat-a task waiting on a worktree:feat-b task deadlocks: feat-a's worker never pulls the prerequisite.
  • One worktree: tag per task — two would make both workers eligible.
  • Workers can share the default assignee bob: startup reclaim is tag-scoped, so a restart re-queues only that worker's own worktree:<name> slice.

Verify: create one task per worktree, confirm each lands in its own window and edits its own checkout, that board_status shows one lease per worktree, and that feat-a's worker never pulls feat-b's task.

Task model

id, title, description, status, priority (low/medium/high/urgent), tags[], mode (or null to auto-route), depends_on[] (task IDs that must be done/analysis_done first), assignee, result, timestamps, plus per-task notes, artifacts (files/commits/tests a worker produced), and questions (the ask/answer round-trip) tables.

Status lifecycle: staged (created non-pullable) → pendingin_progressneeds_input (awaiting a human answer) → back to in_progress, or a terminal analysis_done (read-only / no verified code) · done · blocked · cancelled.

Development

npm run build         # tsc -> dist/ + the plugin bundle
npm test              # node:test suite (board logic, gates, parsers, checkpoint, …)
npm run lint          # ESLint 9 (flat config, typescript-eslint)
npm run format        # Prettier --write   (format:check to verify)

CI (GitHub Actions, windows-latest, Node 22 + 24) runs lint → format-check → build → test-with-coverage on every push/PR; a red check blocks merge. Before opening a PR, run npm run lint && npm run format:check && npm test.

Layout

src/
  types.ts        task types and enums
  db.ts           SQLite store + repository (connection, tasks, deps, artifacts, checkpoints)
  questions.ts    board-native ask/answer/timeout round-trip (extracted from db.ts)
  completion.ts   done-integrity gate: completeTask + Evidence (extracted from db.ts)
  await-task.ts   await_task poll classification (settled / needs_input / unsettled) — testable seam
  modes.ts        mode slugs, router, per-mode risk + auto-approve profiles
  templates.ts    task templates
  bob-ipc.ts      async IPC client (BobClient; approve/reject/sendMessage)
  bob-polls.ts    verify-and-continue poll loop (re-dispatch until it passes)
  llm.ts          shared Claude transport (api / cli backends)
  classify.ts     command-safety classifier (gray-zone command asks) + hard deny-list
  json-extract.ts balanced-brace JSON extraction shared by classify/answer/judge
  command-policy.ts deterministic command allow/deny/escalate policy (allowlist source + gate resolver)
  permission-gate.ts default-on non-interactive permission gate (allow->approve, deny->needs_input + end)
  command-gate.ts gray-zone LLM approve/reject gate (worker -> IPC), the escalation path
  answer.ts       answerer for Bob's followup questions
  followup-gate.ts answer-or-escalate gate for followup asks (worker -> IPC)
  worker-answer.ts handle a human's answer to an escalated followup
  judge.ts        LLM acceptance judge + scoped git-diff capture (verify-and-continue)
  verdict-cache.ts LRU cache for classifier verdicts (skip repeat calls for identical commands)
  review-findings.ts (de)serialize review-mode findings (text <-> structured) for the board
  retry-policy.ts retry/backoff for failed dispatches
  watchdog.ts     idle / blocked-on-ask dispatch watchdog (ends a wedged run early)
  budget.ts       per-dispatch token/turn budget tracker + ceiling
  scope.ts        task scope estimate for right-sizing at creation
  checkpoint.ts   per-task git checkpoint: capture, preserve-to-branch on death, revert
  defer.ts        pause dispatch while you're chatting with Bob
  report.ts       board -> markdown standup/audit (CLI + board_report tool)
  notify.ts       desktop toast and terminal bell
  server.ts       MCP server (stdio)
  cli.ts          CLI
  worker.ts       auto-dispatch loop
  smoke.ts        self-test
tools/
  patch-bob-buttons.mjs   expose Bob's approve/reject buttons + GetReviewFindings over IPC
  measure-completion-latency.mjs  read-only probe: Bob's completion -> board settling (await_task lag)
extension/                VS Code / Bob "Bob Tasks" extension — packages the worker (see extension/README.md)