Agent-first issue tracker — MCP-first. A local-first, dependency-aware issue store built for
agent swarms, offline-capable in local mode. Every domain feature is a Model Context Protocol (MCP)
tool, resource, or prompt over stdio; the unblock command-line binary is lifecycle/ops only.
A ground-up, idiomatic, multi-crate Rust tool. GA 1.0.0 — semver stability applies from GA (D35): the MCP contract, CLI surface, and 0–8 exit codes are stable; a breaking change bumps to 2.0.0.
unblock is the issue store an agent swarm coordinates through. Issues, dependencies, and atomic multi-agent claims live in one database; agents drive everything over MCP. unblock has two modes. In local mode the database is one SQLite file on one machine, with no server, no account and no internet — this is the default build and the only mode any shipped binary has today. In remote mode the database is one shared instance every developer on a team connects to, over a private server the team runs or over Turso Cloud.
unblock is the only dependency-aware issue store that runs entirely on one local file — no account, no server, no internet — with atomic multi-agent claim, a versioned dependency-aware scheduler, and contention-safe swarm coordination at 250k+ issues, and a team can move that same workspace onto one shared database without changing a single tool call (D51 remote mode, which is online-only).
Persistence sits behind a Storage trait with one implementation per mode. Local mode is a
libsql (Turso's SQLite fork) database file, embedded and
network-free. Remote mode speaks SQL over HTTP to the shared database and keeps no local copy at all,
so reads and writes both cross the network and there is no offline fallback — when the network is
down, remote mode does not work. A line-oriented JSONL export/import is an optional, git-diffable
portability/audit feature in both modes, not a sync mechanism.
- Swarm-scale correctness. Contention-safe coordination and integrity at 250k+ issues, proven by a dedicated contention lab and a 250k-issue scale gate in CI (NFR-1/2/3 — see ci-cd-and-distribution.md §5).
- Atomic multi-agent claim. The
claimtool assigns an issue and flips it toin_progressin one atomic step, so two agents never grab the same work. - Offline by default. Local mode runs no git operations, links no git library, and touches no
network on any normal command path — the only network access in a default build is the explicit
unblock update. Remote mode is an explicit opt-in and needs the network for every operation. - Dependency-aware scheduler. A real dependency graph (
petgraph) drives ready/blocked ordering, cycle detection, and a versioned scheduler, so agents pick genuinely unblocked work.
Compared to alternatives: a GitHub MCP server needs internet and an account even for one developer,
and has no dependency graph or atomic claim; saga-mcp has no atomic claim and is single-agent; a
raw SQLite MCP has no domain model and no exit-code contract. unblock's remote mode also needs a
server and a token, so the honest claim is narrower than "no internet" — unblock is the one that
does not FORCE it.
Prebuilt installers are produced by dist for five target
triples (x86_64/aarch64 on linux-gnu and macOS, x86_64 on windows-msvc) — a single self-contained binary
with no runtime system dependencies.
Shell (Linux / macOS):
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/websublime/unblock/releases/latest/download/unblock-cli-installer.sh | shPowerShell (Windows):
powershell -ExecutionPolicy Bypass -c "irm https://github.com/websublime/unblock/releases/latest/download/unblock-cli-installer.ps1 | iex"The installer artifacts are named
unblock-cli-*(dist derives the release App name from theunblock-clipackage) even though the installed binary isunblock. Thereleases/latest/download/links resolve once the maintainer cuts the first published release; a version-pinned form such as.../releases/download/v1.0.0/unblock-cli-installer.shalso works.The installer adds
unblockto yourPATHby editing your shell profile — open a new terminal (orsourcethe env file it prints) before running theunblockcommands below, or you'll hit "command not found".
Build from source (alternative):
cargo build --release # stable Rust 1.96.0, edition 2024 → target/release/unblock
cargo install unblockis not available: every workspace crate — including the binary crateunblock-cli— ispublish = false, and there is nounblockcrate on crates.io. Only the dist installers or a from-source build produce the binary.
unblock's product surface is MCP, so the goal is to get an MCP client spawning the stdio server.
-
Scaffold a workspace. From your project root, create a
.unblock/directory holdingconfig.toml(with the issue-id prefix seeded — defaultub) and a migrated, emptyunblock.db. Idempotent and clobber-guarded.unblock init # or: unblock init --prefix myprojFrom v1.1,
unblock init --agentsalso writes the step 2 block in the same run, and a bareunblock initprints a one-line hint on stderr naming theunblock agents --dir …command to run next (-qsilences it). Release 1.0.x has neither, and it rejects--agentsas a usage error. -
Write or refresh the agent wiring block. Writes/refreshes a managed block in
AGENTS.md(delimited by<!-- BEGIN unblock -->/<!-- END unblock -->) documenting the full MCP surface — tool, resource, and prompt tables, the per-action parameter surface, the error-code/exit table, and the contract id. This is the machine-facing wiring doc for agents in the workspace.unblock agents
Run it after a bare
init, and again after upgradingunblock, because the block carries the binary's contract id. From v1.1,init --agentsruns it once for you.unblock agentsis the only non-destructive way to refresh the block on an existing workspace, because a secondinit --agentsis refused by the clobber guard andinit --forcereplacesconfig.tomlwholesale (the prefix resets toubunless--prefixis passed, and every other key, a customdb_filenameincluded, is dropped). -
Register the server with your MCP client. Point the client at the
unblockbinary over stdio. How you point at the workspace depends on where the config lives — a config committed to the repo (shared with your team) must NOT carry a machine-specific absolute path, while a per-user config that lives outside any repo does need one. Pick the world that matches your client.A) Project-scoped, committed config (the default for a team). These configs live inside the repo and are committed, so every teammate on every machine gets the same wiring. They pass no absolute path:
unblockresolves the workspace fromCLAUDE_PROJECT_DIR(which Claude Code sets in the server's environment andunblockreads on startup), or from the working directory (VS Code / Cursor set it to the workspace), falling back to a walk-up.Claude Code —
.mcp.jsonat the repo root:{ "mcpServers": { "unblock": { "command": "unblock", "args": ["mcp"] } } }Do not write
"args": ["mcp", "--dir", "${CLAUDE_PROJECT_DIR}"]: the${…}form is not expanded inside.mcp.json(the variable lives in the spawned child's env, not a token Claude Code substitutes intoargs), so it would reachunblockverbatim and fail. Omitting--diris the correct, committable form.VS Code —
.vscode/mcp.json, pinning the working directory to the workspace folder:{ "servers": { "unblock": { "type": "stdio", "command": "unblock", "args": ["mcp"], "cwd": "${workspaceFolder}" } } }Cursor —
.cursor/mcp.json; the path-free form is preferred (Cursor sets the working directory to the project, and the walk-up finds.unblock/):{ "mcpServers": { "unblock": { "command": "unblock", "args": ["mcp"] } } }If your Cursor version does not set the working directory to the project, use
"args": ["mcp", "--dir", "${workspaceFolder}/.unblock"]— but only if your Cursor expands${workspaceFolder}before spawning (it resolves to each developer's own checkout, so the file stays portable); if it does not expand, drop--dirand let the walk-up resolve it. Never commit a literal machine path here.B) User-global config, not committed (Claude Desktop). Claude Desktop's config is per-user, lives outside any repo (
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS), sets noCLAUDE_PROJECT_DIR, and is often spawned from$HOME— so an absolute--diris the correct and expected form here. Use the absolute path to the.unblockdirectory created in step 1 (the output ofpwdin your project root, followed by/.unblock):{ "mcpServers": { "unblock": { "command": "unblock", "args": ["mcp", "--dir", "/absolute/path/to/project/.unblock"] } } }If the client can't find
unblock: GUI-launched clients (e.g. Claude Desktop) do not inherit your interactive shellPATH, and the installer placesunblockin a per-user directory. If the server fails to start (spawnENOENT), replace"command": "unblock"with the absolute path fromwhich unblock(macOS/Linux) orwhere.exe unblock(Windows). -
Let the client spawn the server. On startup the client launches
unblock mcpas a stdio child and speaks MCP to it.unblockresolves the workspace in this order: an explicit--dir/--db(orUNBLOCK_DIR), thenCLAUDE_PROJECT_DIRfrom its environment, then a bounded walk-up from the working directory to the nearest.unblock/. Prefer one of the project-scoped forms above (or an absolute--dirfor a user-global client) rather than relying on the walk-up alone. On startupunblockreports the workspace directory it bound to stderr (diagnostics only, NFR-14) — check that line to confirm it opened the workspace you expected.
The contract id is unblock.mcp.v1.10. For machine-readable discovery, agents read the resources
unblock://capabilities (the descriptor tables) and unblock://schema (the full JsonSchema bundle
for every tool I/O). The topology is child-per-client (D31): each MCP client spawns its own
unblock mcp child, and a cross-process advisory .unblock/.write.lock serializes writers across
clients.
The contract (unblock.mcp.v1.10) exposes 8 tools, 5 resources, and 3 prompts.
| Tool | Actions |
|---|---|
issue |
create · show · update · close · reopen · delete · restore (plus create_bulk from markdown, quick-create) |
claim |
atomic assignee + flip to in_progress |
defer |
defer · undefer |
query |
list · ready · blocked · search · count · stale (with filters) |
dep |
add · remove · list · tree · cycles · graph |
sync |
export · import · import_bd (one-shot bd import) |
diagnostics |
stats · info · where · version · lint · changelog · orphans · dangling (dependency edges whose target does not exist) |
comment |
add · list · update · delete (soft-redact: the row is kept, the body masked) |
Resources: unblock://issues/{id} · unblock://issues/ready · unblock://issues/blocked ·
unblock://capabilities · unblock://schema.
Prompts: triage · plan_next_work · close_with_suggestions.
The full per-action parameter surface (required and optional fields for every action) lives in the
generated AGENTS.md block and in the unblock://capabilities / unblock://schema resources — read
those rather than duplicating the ~40-row table here.
The unblock binary is lifecycle/ops only — domain features are MCP tools. Seven commands:
| Command | Description |
|---|---|
unblock mcp |
Run the MCP stdio server (the primary product surface, FR-20) |
unblock migrate |
Bring the workspace database schema to the version this build expects and report the from→to delta; a database written by an older release is migrated forward, a database newer than this build is refused rather than opened (FR-16, D46) |
unblock doctor |
Run read-only health diagnostics on the workspace, including the schema version observed on disk against the one this build expects (doctor-lite, FR-16, D46) |
unblock version |
Print version / build metadata (no workspace, no network) |
unblock init |
Scaffold a new .unblock/ workspace (config + migrated empty database, FR-14) |
unblock agents |
Write / refresh the managed AGENTS.md MCP-wiring block (FR-14) |
unblock update |
Self-update the unblock binary (checksum-verified before swap, FR-25/D17) |
Command-specific flags: init takes --prefix <PREFIX>, --force and, from v1.1, --agents (after
scaffolding, it also writes the AGENTS.md block through the same merge unblock agents runs);
update takes --dry-run; version takes --short.
Global options (present on every subcommand):
--dir <DIR>[envUNBLOCK_DIR] — the explicit workspace.unblock/directory (no walk-up;--dir>UNBLOCK_DIR). When neither is set,unblockreadsCLAUDE_PROJECT_DIRfrom its environment (injected by editors such as Claude Code, probed as a project root) and, failing that, walks up from the working directory to the nearest.unblock/. Precedence:--db>--dir/UNBLOCK_DIR>CLAUDE_PROJECT_DIR> bounded cwd walk-up.--actor <ACTOR>[envUNBLOCK_ACTOR] — the actor override.-o, --output <FORMAT>— one ofjson|robot|plain|csv|markdown.-v/-vv/-vvv— increase verbosity (INFO/DEBUG/TRACE; logs go to stderr only, NFR-14);-qquiets all but errors.-h, --help·-V, --version.
Exit codes — a stable 0–8 contract: 0 ok · 1 internal · 2 workspace/db · 3 not-found/id ·
4 validation · 5 dependency/cycle · 6 sync/path · 7 config · 8 io/json. The full
code → exit → retryable table is in the generated AGENTS.md and in unblock://capabilities.
Structured output goes to stdout; diagnostics go to stderr (NFR-14). One carve-out: unblock mcp owns
stdout as the MCP JSON-RPC framing channel, so once the server starts it writes nothing there but frames —
its structured errors go to stderr too, whole and machine-readable (the exit code is the same either way).
unblock mcp --help never starts the server, so it keeps its usage text on stdout like every other subcommand.
unblock update # download, verify, and swap the binary
unblock update --dry-run # report an available update without swappingunblock update runs the dist installer via axoupdater,
which verifies each artifact's SHA256 checksum against dist-manifest.json before self_replace
swaps the binary — a mismatched or tampered download is refused and nothing is swapped (NFR-17/D17).
The command lives behind the default-on self-update Cargo feature; --no-default-features drops
it (and with it the only network surface). No network is touched on any normal command path — only on
explicit unblock update.
AXOUPDATER_GITHUB_TOKEN is optional. Set it to a GitHub token and unblock update sends it with the
release query. The query then counts against the authenticated limit of the account behind the token
(5,000 requests per hour for a user, or 1,000 per hour per repository for a GitHub Actions
GITHUB_TOKEN) instead of GitHub's anonymous limit of 60 per hour per IP address. If GitHub refuses
the query, the error is RATE_LIMITED (exit 2, retry later) for a 403 or 429, and CONFIG_ERROR
(exit 7) for a 401, which means GitHub rejected the token (D55, from v1.0.2). The token is never
printed.
unblock is an acyclic, multi-crate Rust workspace: layers L0 → L7 (model/error → policy →
storage → sync/health → config → engine → render → mcp/cli), with edges pointing
downward only (enforced by cargo xtask check-layering, NFR-15). A single binary — unblock (from
unblock-cli) — ships; every unblock-* library crate is workspace-internal (publish = false). See
crates/README.md for the per-crate layering table.
Releases are cut by pushing a vX.Y.Z tag, which fires the dist-generated release workflow. The
maintainer runbook — including the guarded cargo xtask release helper and the first GA cut — is in
RELEASING.md. The authoritative pipeline detail is
docs/plans/ci-cd-and-distribution.md §3.
- Product truth & decisions:
docs/PRD.md - Plans, task DAG, and live status:
docs/plans/— notablySTATUS.mdandimplementation-plan.md - Workspace contract:
CLAUDE.md
Licensed under either of
- MIT license (LICENSE-MIT), or
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
