Unblock
A live map of your project, kept by your agents. The plan is a dependency graph in your repo (.github/unblock.json). Your coding agents keep it current as they work: they pick the task that unblocks the most, claim it, and mark it done. You look, and see where things stand, what's next, what's in the way, and what a feature still depends on.
It isn't a ticket system. There are no tickets to groom, no sprints, no workflows to configure and no seats. Every task shows how much of the work upstream of it is already done, groups fold into one card, and every pull request shows what it does to the plan. It's the map, not the dispatcher: it works alongside whichever agent (Claude Code, Cursor, Codex) and tracker you use.
index.html: the app. One self-contained file, no build, no server. Open it in a browser.board.json: an example board (the same one the app starts with).unblock.py: command-line tool for scripts and agents (Python 3.8+, standard library only).schema.json: JSON Schema for the board format.skill.md: instructions for coding agents (setup, the work loop, the format). Served at https://unblock.esh.sh/skill.md, with a shortllms.txtnext to it..github/unblock.json: this project's own roadmap, kept as an Unblock board.SECURITY.md: how to report a security problem, and what the sign-in and the CLI can and can't do.LICENSE: MIT.tests/: the test suites (python3 -m unittest discover -s tests) andmcp_host.py, a small MCP Apps host for trying the board in a chat by hand.snap/: the service that draws pull request pictures (snap.py) and itsdeploy.sh.plugin/: Unblock as a Claude Code plugin with a mod (early), and.claude-plugin/marketplace.json, which makes this repository its marketplace.relay/: the sign-in relay for the hosted copy (relay.py, one file, standard library only) anddeploy.sh, which runs it as a container next to the site.landing/: the landing page at https://unblock.esh.sh (with its launch video, made with /brag)..caddy: routing for the hosted copy:/is the landing page,/appand every/gh/...address serve the app.
Track any repository's plan
Commit a board to a repository as .github/unblock.json, then open:
https://unblock.esh.sh/gh/<owner>/<repo>
The page reads the board straight from GitHub. There's no sign-up, no server storage, and nothing to install. It's a read-only view that checks for new commits every few minutes, so progress an agent pushes shows up on its own. Edit a copy turns it into your own editable board.
- Which file: the first of
.github/unblock.json,unblock.jsonorboard.jsonon the default branch. To point at something else, use a GitHub-style address:/gh/<owner>/<repo>/tree/<branch>or/gh/<owner>/<repo>/blob/<branch>/<path>. You can also paste any GitHub link into the box at https://unblock.esh.sh/gh. - Plans kept by other tools: with no Unblock board, the page shows the plan another tool keeps in the repository, turned into a board: Task Master (
.taskmaster/tasks/tasks.json), Backlog.md (backlog/tasks), Spec Kit (specs/*/tasks.md) or Kiro (.kiro/specs/*/tasks.md). Failing those, it shows the repository's GitHub issues, with sub-issues as groups and "blocked by" links as dependencies. Add?source=issues(ortaskmaster,backlog,speckit,kiro) to choose. Nothing in the repository is changed;unblock importkeeps the result as a board. - No board yet: the page offers to create
.github/unblock.jsonon GitHub with a starter board already filled in. - Private repositories: choose Sign in with GitHub. You pick which repositories Unblock may use, approve, and you're back on the board. The app can read those repositories, and write to them only when you ask: saving your edits as a commit or pull request, or starting an agent (an issue for
@claude, a Copilot agent task). It has no webhooks and nothing runs in the background. The page never holds the token: a small relay at/authkeeps it in cookies that page scripts can't read and makes the GitHub calls for the page, only the ones Unblock makes. It reads board and plan files (never other files), and writes only a board file that is already a board,unblock/branches and their pull requests, issues, comments and agent tasks. So even a script injected into the page couldn't read your code or push to it. Sign out in the side panel revokes it. If sign-in isn't available, the page falls back to a pasted fine-grained token with Contents: Read-only, kept in your browser. - Request limits: without a token, GitHub allows 60 API requests an hour per network. A token raises that to 5,000. The viewer uses about three requests when it opens a board and one per check for new commits.
- Edit and save back. Edit and save to GitHub in the board panel makes the board editable in place. When you're done, Open a pull request (a new branch with your edits) or Commit to the branch directly. Signing in is enough when you've accepted the app's current permissions; a pasted fine-grained token needs Contents: Read and write (and Pull requests: Read and write for pull requests). If the file changed on GitHub meanwhile, it won't write over that change. Unsaved edits survive a reload.
- Your view settings (folded groups, focused group, layout) are remembered per repository in your browser. They never touch the file.
- From a local copy of
index.html: useindex.html?gh=<owner>/<repo>instead of the/gh/path.
unblock.py looks for the same files, so inside a repository python3 unblock.py next finds .github/unblock.json without -f.
Using the app
Open index.html in Chrome, Edge, Safari or Firefox, or use the hosted copy at https://unblock.esh.sh/app.
| Do this | How |
|---|---|
| Add a task | + Task or N. With a task selected, A adds a next step, B a prerequisite, S a subtask. |
| Connect two tasks | Drag from the dot on a card's right edge onto the card that should wait. Or use Waits on / Unblocks in the inspector. |
| Change status | Click the circle on a card, or press Space. To do → Doing → Done. |
| Fold a group | Click its arrow, or E. Links in and out of the group are kept on the folded card. |
| See why something is blocked | Select it. Everything it waits on lights up orange, everything it unblocks lights up blue, and the inspector lists Do next to unblock. |
| Switch layout | The two buttons in the toolbar, or V. Layered is a left-to-right flowchart of cards; Force shows every task as a circle (see below). |
| Focus on a group | The ↘ button on a group (or D) shows only what is inside it; you can keep diving into subgroups. Press U or Esc, or use the breadcrumb bar, to go back up. |
| Critical path | C or the route button. Highlights the longest chain of open work by estimate. |
| Move around | Drag the background, scroll to pan, pinch or ⌘/Ctrl-scroll to zoom, F to fit. Arrow keys move the selection. |
| Undo | ⌘Z / ⇧⌘Z. |
Remaining work only
Remaining in the top bar (or H) hides the clutter of finished work. Finished groups disappear, with a note at the bottom of the board ("✓ 8 finished groups hidden · Show"); finished tasks inside open groups become a "✓ 4 done" chip in the group's header; and links from finished tasks go, since they no longer hold anything up. Progress, readiness and the critical path don't change; only the drawing does. It's on by default once a board is two thirds done. It's remembered per board in your browser and never written to the file, and the compare view always shows everything. A task you mark done stays in sight until you switch views. Going to a finished task, from the list or a search, shows everything again.
Focusing on a group
Focusing on a group turns the canvas into that group alone: the rest of the board goes away, the task list shows only tasks inside it, and with nothing selected the inspector shows the group itself. Nothing is lost at the edges:
- tasks elsewhere that something inside waits on appear as dashed cards in the first column, and tasks elsewhere that wait on something inside appear in the last column. Double-click one to go to where it lives;
- links that apply to the whole group (its own
dependsOn, or other tasks that wait on the whole group) are listed in the breadcrumb bar at the top.
Both layouts support this, and the app remembers where you were. Creating a task while focused puts it inside the group.
Force view
Every task is a circle, sized by its estimate. The fill and outline show the state (orange ring for ready, blue half-disc for doing, green check for done, dashed grey for blocked), and the orange arc around a blocked circle is its % unblocked. An expanded group is an outline around its tasks, with a tab showing its progress; click the tab's − to fold the group into a single circle with a progress pie, and double-click that circle to unfold it again. Its tasks spill out from where it was.
Drag circles to untangle the graph; they settle back into their group when you let go. Drag from the dot on a selected circle's edge to link it. Free lets the forces decide; Flow pulls prerequisites to the left and the work they unblock to the right. The shake button re-runs the layout. Selection, the inspector, the critical path and the keyboard work the same in both views.
Editing
- Right-click anything (a task, group, ghost card, link, the empty canvas, or a multi-selection) for everything you can do with it. The menu has a search field, so you can type to find an action. Long-press does the same on touch screens.
- ⌘K / Ctrl+K searches tasks and actions from anywhere.
- Rename in place: double-click a card, or select it and press Enter. While renaming, Tab saves and adds the next step, Shift+Tab adds a prerequisite, ⌘Enter adds a task beside it, and Esc cancels (Esc on a brand-new, still untitled task removes it). New tasks open straight into rename, so N, type, Tab, type, Tab… lays out a chain.
- Quick-add handles: on a hovered or selected card, click the dot on the right to add a next step (drag it to link instead), or the + on the left to add a prerequisite.
- Select several: Shift-click or ⌘-click cards, Shift-drag on empty canvas, or ⌘A. A toolbar appears with status, group, link in sequence (each waits on the one to its left), move and delete.
- Move into a group: drag a card onto a group (in the force view, drop a circle onto a group outline), or press M to pick one. Dropping on empty canvas in the layered view moves it to the top level.
- Clipboard: ⌘C / ⌘X / ⌘V copy, cut and paste tasks, including everything inside groups and the links between them. ⌘D duplicates, ⌘G groups the selection, ⇧⌘G ungroups.
- Inspector: properties (status, estimate, owner, tags, group) edit inline, like a Notion page. The ⋯ button opens the same menu as a right-click, and ⇔ widens the panel for writing.
Descriptions
The description is a rich-text editor, but it is saved as plain Markdown in notes, so agents and scripts read and write it as usual.
- Type Markdown and it formats as you go:
######headings,-bullets,1.numbers,[]checklists,>quotes,```code,---dividers, and**bold**,*italic*,`code`,~~strike~~inline. - Type
/for a menu of block types. Select text for a formatting bar (bold, italic, strike, code, link, headings, lists). - ⌘B, ⌘I, ⌘E (code), ⌘K (link), ⇧⌘S (strike). Tab and Shift+Tab indent list items.
- Pasting from web pages, docs or Markdown keeps structure but drops styling.
- Checklist items (
- [ ],- [x]) show as progress on the card (for example 1/2), andchecklist: {done, total}appears in the computed report.
The board is saved in the browser automatically. Open JSON to copy or edit it, import a file, or link a file on disk.
Linking a file (Chrome and Edge)
JSON → Open & link file… (or Save to file…) connects the app to a real file such as board.json:
- every change in the app is written to the file;
- when something else changes the file (you, a script, an agent), the app reloads it within about two seconds.
That gives you a live view while an agent works through the board with unblock.py or by editing the JSON directly. Other browsers can import, download, copy and paste instead. You can also drop a .json file onto the window, or paste board JSON anywhere outside a text field.
How the numbers work
- Tasks and groups. A task with children is a group. Only tasks without children (leaves) have a status; a group's progress comes from its leaves.
- Dependencies.
dependsOnlists ids that must be done first. Depending on a group means depending on every task inside it. A group's owndependsOnapplies to everything inside it. - Ready vs Blocked are computed, never stored. A
todotask is ready when all of its prerequisites are done; otherwise it is blocked. - % unblocked for a task is the share of all the work upstream of it (the full transitive set of prerequisites, not just direct ones) that is done, weighted by
estimate(default 1). For a group, it counts the upstream work outside the group. 100% means nothing upstream is left. - Do next to unblock lists the open upstream tasks that can be worked on right now (ready or doing), ordered by how much downstream work each one unblocks.
- Unblocks N is the number of open tasks somewhere downstream.
- Critical path is the longest chain of open work, summed by estimate. The board can't finish sooner than this chain does.
Board format
{
"format": "unblock/v1",
"title": "Public beta launch",
"view": { "collapsed": ["research"] },
"tasks": [
{ "id": "backend", "title": "Backend", "dependsOn": ["scope"] },
{ "id": "auth-api", "title": "Auth & sessions API", "status": "doing", "parent": "backend",
"dependsOn": ["db-schema"], "estimate": 5, "assignee": "sam", "tags": ["api"], "notes": "…" }
]
}
| Field | Meaning |
|---|---|
id |
Unique, stable string. Links and parents refer to it. |
title |
What the task is. |
status |
todo, doing or done. Leaves only. |
parent |
Id of the group it sits in. |
dependsOn |
Ids that must be done first. A group id means every task inside it. |
estimate |
Optional number ≥ 0, default 1. Weights every percentage. |
assignee, tags, notes |
Optional. |
ref |
Optional: another board this task stands for, or one task in it: owner/repo, owner/repo#task, owner/repo:path/board.json#task, or a board on disk, ../api/.github/unblock.json#task. The task follows it: done when that is done, doing once work there started (unblock sync, or the workflow after each merge). Cards show its live state, so work that spans repositories shows up in one plan. |
touches |
Optional: paths the task will likely edit, as patterns (["src/auth/**"]). Starting several agents warns when two picked tasks overlap. |
issue |
Optional: the GitHub issue this task is, owner/repo#123 or #123 in the board's repository. Cards show it with its live state, and the task is marked done when the issue closes: unblock sync, or the workflow after each merge (it needs issues: read). Set it with unblock edit <id> --issue owner/repo#123, or let unblock export-issues <ids> create the issues: a group becomes a parent issue with its open tasks as sub-issues, links become "blocked by", and each task gets its issue. Then GitHub's coding agents can be assigned from the issue. Only what you name is exported, and nothing syncs back except closed issues. |
view.collapsed |
Group ids shown folded. Display only. |
Any other fields, on tasks or at the top level, are kept as they are, so you can attach your own data (links, ticket numbers, due dates).
A board is rejected if an id is missing or duplicated, a parent or dependsOn points at an id that doesn't exist, parents form a loop, a task waits on its own group (or a group on its own subtask), or the dependencies form a loop. The error names the ids involved.
For agents and scripts
Point your agent at https://unblock.esh.sh/skill.md. It explains how to install the CLI, start a board for a repository, work through it, and show you progress. It's written as a Claude Code skill, so you can also save it as .claude/skills/unblock/SKILL.md. For any other agent, paste the link.
To get the CLI on its own:
curl -fsSL https://unblock.esh.sh/install.sh | sh
On Windows, in PowerShell: irm https://unblock.esh.sh/install.ps1 | iex. Either one takes the newest release, checks it against the sha256 the site lists, needs only Python 3.8 or newer, and puts unblock on your PATH (~/.local/bin, adding it to your shell's startup file when it's missing; sh -s -- --no-modify-path leaves that file alone). It has to be on the PATH: agents, Claude Code's hooks, the status line and the side pane all run unblock by name.
It's also on PyPI as unblock-cli: uv tool install unblock-cli or pipx install unblock-cli, or run it without installing with uvx --from unblock-cli unblock.
Prefer unblock.py over editing the JSON by hand: it validates every write, refuses loops, and keeps the file formatted the way the app writes it.
python3 unblock.py init # create .github/unblock.json (--example adds a few sample tasks)
python3 unblock.py init --draft # or draft it from what's here: README/TODO checklists, TODO comments, another tool's plan
python3 unblock.py init --agents # the Unblock lines in AGENTS.md (and CLAUDE.md), so any agent follows the loop
python3 unblock.py prime # for an agent starting a session: progress, in flight, what to pick up
python3 unblock.py view # open the board in the browser; edits save to the file
python3 unblock.py # summary: progress, in flight, what to start next
python3 unblock.py next --json # ready tasks, most unblocking first
python3 unblock.py next --waves # all open work in sets that can run in parallel: how many agents are worth running
python3 unblock.py now # one answer to "what should I do now?", and why
python3 unblock.py digest --since 7d # what changed in the plan: finished, now ready, new, blocked (or --since <ref>)
python3 unblock.py show docs # readiness, what to do next to unblock it, links
python3 unblock.py tree # the whole board
python3 unblock.py next --in backend # only what is ready inside one group (also works for tree)
python3 unblock.py report # everything computed, as JSON
python3 unblock.py set auth-api done # prints which tasks just became ready (--note "what you decided" adds a dated line)
python3 unblock.py claim auth-api # before starting: doing, and other agents skip it for 2h (--for 30m)
python3 unblock.py edit auth-api --assignee sam --notes "- [ ] rate limits" # any field; '' clears it
python3 unblock.py point auth-api --note "Starting this" # highlight tasks in the open viewer
python3 unblock.py diff main...feature # what a branch does to the plan (also: diff, diff HEAD~3, diff a.json b.json)
python3 unblock.py init --ci # comment that diff on every pull request (GitHub Actions)
python3 unblock.py init --git # merge the board task by task, so branches don't conflict on it
python3 unblock.py sync # tasks follow what they link to: closed issues, other boards (ref)
python3 unblock.py export-issues launch --dry-run # GitHub issues for chosen tasks (sub-issues, blocked-by), to assign agents
python3 unblock.py start --dry-run # the play button: Claude Code agents on the next ready tasks, at most 3 (then: unblock stop)
python3 unblock.py statusline # one line for Claude Code's status bar (init --agents sets it up)
python3 unblock.py heartbeat --state needs-you # tell the board your agent waits on a person (Claude Code's hooks do this)
python3 unblock.py prompt docs # what to tell an agent to work on a task (the app's Start menu uses the same text)
python3 unblock.py log # what each commit did to the plan: progress over time, what finished when
python3 unblock.py card # a 1200x630 picture of the board's progress, to share
python3 unblock.py graph # the board as it stands, as text: waiting on you, in progress, ready, and what they unblock
python3 unblock.py svg --out now.svg # the same as a picture (light and dark in one SVG); the Claude Code pane shows it
python3 unblock.py svg --png now.png # a PNG for chat apps that show no SVG (needs resvg: https://github.com/linebender/resvg/releases)
python3 unblock.py readme --add # a badge and a Mermaid graph of the plan in README.md, refreshed after merges
python3 unblock.py import # start from the plan Task Master, Backlog.md, Spec Kit or Kiro keeps here
python3 unblock.py import github --repo o/r # or from a repository's issues: sub-issues are groups, "blocked by" are links
python3 unblock.py add "Write release notes" --parent launch --depends-on docs --estimate 2
python3 unblock.py link docs auth-api # docs now waits on auth-api
python3 unblock.py unlink docs auth-api
python3 unblock.py rm site --ungroup # remove a group but keep its tasks
python3 unblock.py undo # put the board back as it was before unblock's last save (--list shows what can be undone)
python3 unblock.py validate
python3 unblock.py lint # worth a look: done while blocked, stuck in doing for a week
python3 unblock.py new garden # another plan in this folder: garden.unblock.json (and use it)
python3 unblock.py list # the plans here, with progress; ▸ marks the one in use
python3 unblock.py use launch # work on another plan here (remembered for this folder)
python3 unblock.py next --all # ready tasks across every plan here, each with its plan (svg --all: a card per plan)
python3 unblock.py where # which plan unblock uses here, and why
A suggested loop for an agent: run next --json, pick the first task, claim <id>, do the work, set <id> done, repeat.
Plans anywhere: any file, several per folder, git optional
A plan is any JSON file in the format above, wherever it is: unblock -f path/to/plan.json … works on it, in a git repository or not. A folder can keep several plans: name them <name>.unblock.json (unblock new garden makes garden.unblock.json) and switch with unblock use <name>. Which plan a command works on, in this order:
- the file you name:
-f, or the MCP tools'boardargument (a file, a folder, or a plan's name); UNBLOCK_FILE;- the folder's choice (
unblock use), kept in.unblock/settings.jsonand looked up to the top of the repository; .github/unblock.json,unblock.jsonorboard.json, in the nearest folder (inside a repository: walking up to its top) that has one;- the only
*.unblock.jsonin the folder.
Several plans and no choice is a question, never a guess: the command lists them and says how to pick. Outside a git repository unblock never looks in a parent folder, so nothing lands in someone else's plan. .unblock/ is each person's own state: unblock use keeps it out of git (.git/info/exclude), and unblock init --agents adds it to .gitignore (and, in Perforce, to your P4IGNORE file).
Everything works without git except what needs GitHub: claims, points, undo, the viewer and the MCP server work the same, and unblock log and unblock diff read unblock's own last saves instead of commits. A task can stand for a task in a plan beside it (--ref garden.unblock.json#plant). unblock view lists the folder's plans in its file chip and switches between them, /gh/owner/repo does the same for a repository's *.unblock.json files, and the pull request comment gets a section for each plan a change touches; there, done: garden:buy-seeds names the plan when two plans share an id.
Claims keep parallel agents apart. unblock claim <id> marks the task doing and holds a lease on it (2 hours unless you pass --for; claiming again renews it). next skips tasks other agents hold, and finishing or putting a task back releases it. Claims live in the repository's shared git folder, so agents in different worktrees on the same machine see each other's claims before their branches merge. Across machines, the board's doing status is the signal. The holder is $UNBLOCK_AGENT if set, else user@host:worktree; unblock claims lists them and unblock release <id> gives one up.
In Claude Code: the plugin (early)
plugin/ is Unblock as a Claude Code plugin: one install brings the unblock command (on Claude's PATH while the plugin is enabled, so nothing else to install), the MCP server, the skill, the heartbeat hooks, and a mod (Claude Code v2.1.286 or later). The agent starts each conversation knowing the plan, and an approved plan-mode plan with several steps goes on the board. In the terminal and the desktop app's Code tab it shows the plan in the band above the prompt (▸ Unblock ▰▰▰▰▰▰▰▱▱▱ 72% · 3 ready · 1 needs you · on auth-api), writes a line in the transcript when a task finishes (✓ Auth API done → now ready: Billing · 72% → 76%) or an agent waits on you, and adds /unblock: where things stand, with the board in a pane (a picture in the desktop app). When the agent points at tasks, the pane opens on them with its note; while it works through a checklist (its task list), the pane shows that too, under the board, without writing it to the plan. /unblock next hands Claude the next ready task, /unblock plan <feature> has it plan a feature on the board after a few questions, and /unblock standup [days] says what moved. Unblock's own tool calls are drawn as the moment they are (✓ Finished: Auth API, with what became ready), and calls that do anything else are left alone. It reads the board and the claims file, draws the picture with the copy of unblock.py it ships, and hands Claude work only when you run one of those commands: no network, and it never writes. This repository is its marketplace:
claude plugin marketplace add sqvist-ab/unblock
claude plugin install unblock@unblock
From a clone, claude plugin marketplace add ./ works the same, and claude --plugin-dir ./plugin loads it for one session. Claude Code keeps an installed plugin as a cached copy and replaces it only when the version changes, so a change to plugin/ goes out with packaging/update-plugin.sh <version> (it also refreshes the plugin's copies of unblock.py and skill.md), and a running session keeps its version until Claude Code restarts. tests/test_mod.py checks its numbers against unblock.py on every version of this repository's roadmap, and claude plugin test plugin runs its hooks in Claude Code's own engine (plugin/hooks/register.test.ts).
Watching a board while an agent works
unblock view serves the app at http://127.0.0.1:7070 against the repository's board file and opens it in your browser:
- Edits you make in the browser are saved to the file.
- Changes made on disk by an agent, an editor or
git pullshow up within a second. - If both change at once, nothing is overwritten: the page reloads the file and says so.
- It needs no GitHub account or server, and only answers on your own machine. Folding and focusing are remembered in the browser and never change the file.
It uses the index.html next to unblock.py if there is one. Otherwise it downloads the app from unblock.esh.sh once a day. Use --port to pick another port, and --no-open to skip opening a browser.
Pointing at tasks
An agent can show you exactly what it means. A point rings one or more tasks or groups, brings them into view (unfolding groups and leaving a focused group if needed) and puts a short note beside them. It stays until you dismiss it (Escape or ×) or the next point replaces it.
- In the open viewer:
unblock point auth-api db-schema --note "Starting with these two".unblock viewfinds the running viewer for the board and shows it there. If none is running,unblock view --point auth-api --note "…"opens one on those tasks. - As a link: add
#point=<id>,<id>¬e=<text>to any board address, for examplehttps://unblock.esh.sh/gh/<owner>/<repo>#point=auth-api¬e=Blocked%20on%20this.unblock pointprints this link when the board is in a GitHub repository. - From MCP: the
pointtool below, which also shows the board in the chat. - From page scripts:
window.unblock.point(['auth-api'], {note: '…'}).
Starting an agent on a task
Every open task has Start with an agent in its panel and its right-click menu. Claude Code and Terminal open Claude Code (the desktop app, or a new terminal) in the project folder with the task written in; Codex does the same in the ChatGPT desktop app's Codex mode; Claude on the web opens a cloud session on the repository. You press Enter there, so the agent runs in your own app on your own login. Unblock never runs agents itself. The prompt tells the agent to unblock claim the task, work on a branch, and open a pull request that says done: <id>, so the task is marked done when it merges. Copy prompt (or unblock prompt <id>) gives the same text for any other agent.
Start agents (the play button at the top of the task list, in the local viewer) starts a wave. It offers the ready tasks nobody holds, most unblocking first, with three picked and a limit of three at once unless you raise it. It warns about tasks with no description, picked tasks likely to conflict (a task's optional touches paths overlap, or, without those, both sit in the same group), and pull requests already waiting for your review. On Start, each task is claimed for its agent and gets a background agent on your login, in its own worktree: a Claude Code session (claude --bg --name unblock/<id>), or Codex (codex exec --json --worktree, with its sandbox and approvals left on). For Codex, the board reads its event stream for what it's doing and what it last said, and gives the command to resume its thread. Nothing starts the wave after that; it waits for you. Stop stops the sessions Unblock started and puts their tasks back to ready. From the command line: unblock start [ids] [--agent claude|codex|copilot] [--max 3] [--dry-run] and unblock stop [ids].
Cloud agents from the /gh viewer. On a GitHub board, Start also offers Copilot (cloud), which starts Copilot's cloud agent through GitHub's Agent tasks API on your Copilot plan and opens a pull request. Where the repository runs anthropics/claude-code-action, it also offers Claude on GitHub: an @claude comment on the task's issue, or a new issue if the task has none, run by that workflow on the repository's own Anthropic key. Both ask first. Signing in is enough when you've accepted the app's current permissions; a pasted fine-grained token needs Agent tasks: Read and write for Copilot, or Issues: Read and write for Claude. The card follows Copilot's task state (checked every 30 seconds, including needs you) and either agent's pull request; what you started is remembered in your browser. From the command line or the local viewer, unblock start <id> --agent copilot does the same with GITHUB_TOKEN or gh auth login.
Beside the chat. unblock init --agents also adds an unblock entry to .claude/launch.json. In Claude Code's desktop app, the browser pane can show it: the live board next to the conversation, where point highlights tasks with the agent's note and Start agents sits. Each project gets a port of its own, and the viewer refuses to move off it (--exact-port), so a pane never shows another project's board. Agents in hosts that don't draw the board in the chat are told it's there.
In the chat: moments, not the board. Finishing or changing a task's status says what became ready and what it did to the plan (▸ Unblock ▰▰▰▰▰▰▰▰▰▱ 88% → 90% · 58 of 66 tasks done, Unblock's one line for where the plan stands, the same in the status line and the plugin's band), two lines an agent can pass on. Where the chat draws MCP Apps, point shows a short strip of the board around those tasks with the agent's note, always in the Remaining view (the tasks pointed at stay drawn even when finished), and Open the full board expands it with your usual view. Agents are told to point at three moments: starting a task, finishing one, and being stuck on you.
In your status bar (the terminal). unblock init --agents also sets Claude Code's status line to unblock statusline, which the terminal CLI shows below the prompt. The desktop app doesn't show custom status lines; there, the side pane does this job. The line reads: ▸ Unblock ▰▰▰▰▰▰▰▰▰▱ 91% · 5 ready · 1 needs you · on auth-api. That's progress, what's ready, how many agents are waiting on you, and the task this session holds. It's always in view and costs the agent nothing. If the project already has a status line, it's left alone.
What each agent is doing shows on its card: the holder while it's working, a pulsing needs you when it waits on a person (a permission prompt, a question, or a finished turn), quiet after 45 minutes without a word, and PR #12 once an open pull request says done: <id>. Tasks that need you also head the rail. unblock init --agents adds Claude Code hooks to .claude/settings.json that call unblock heartbeat --hook on session start, each prompt, notifications, the end of a turn and the end of the session. Each heartbeat records the state and session on the claims that session holds, and renews their lease. Other agents can report the same way with unblock heartbeat --state needs-you. The local viewer also reads Claude Code's background sessions (claude agents --json) and the repository's open pull requests (with gh, or the GitHub API and a token); the /gh viewer reads the pull requests from GitHub. The task panel says who holds the task, when it was last heard from, and gives the command to resume or attach to its session.
Where the board is decides what's offered: the local viewer opens the project folder, the /gh viewer offers the cloud session and the terminal (in your clone of the repository), and the board inside a chat asks the agent you're talking to. Tasks someone has claimed show the holder on the card, and the panel says until when.
History
unblock log lists what each commit did to the plan: the date, progress and its change, and what it finished, reopened, added or removed (--json for the series). In the app, Show history in the board panel draws progress over time and the same list, from the local viewer's git history or, for /gh/ boards, from GitHub (one API request, then the file at each commit). Compare with now on any commit opens the compare view against that version.
A picture of progress
Share a picture of progress in the board panel (or in the command menu) downloads a 1200×630 card: the title, a ring with how much is done and in progress, the four counts, and what's in progress or next. It's the size link previews and slides expect, as PNG or SVG. unblock card writes the same picture from the command line (--dark for the dark version, --out for the file name).
Since you last looked
Nobody writes status updates. unblock digest --since 7d (or 12h, 2w, a git ref) says what finished, what became ready, what's new and what's blocked, from the board file's git history; --markdown for posting it, and the MCP server has it as a digest tool. The app remembers the board as you last saw it, per file or repo, in your browser: when you come back and it has changed, a card says what moved, and See the changes opens the compare view against what you saw.
What a change does to the plan
unblock diff compares two versions of the board and reports what the change means, not just which lines moved:
- Effects: tasks completed, now ready to start, now blocked or reopened, how each task's % unblocked moves, progress before and after, and the critical path.
- Edits: tasks added, removed, renamed or moved, links added and removed, status, estimate, assignee, tags and checklist changes.
In the app: https://unblock.esh.sh/gh/<owner>/<repo>/compare/<base>...<head> (the same shape as GitHub's compare pages; .. compares with the base branch's tip) shows the head board with the changes marked: new tasks outlined, chips for done, now ready, now blocked, reopened and edited, rings showing the % before and after, and a bar with counts you can click to point at those tasks. The list of every change replaces the task list. Locally, unblock view --diff main (or --diff main... for since the branch split off) does the same against the file on disk, and keeps updating as it changes.
With no arguments it compares HEAD with the file on disk, which is handy before committing. diff main feature compares two refs, and diff main...feature compares a branch with where it split off (what a pull request contains). Two .json files work too. Add --json for agents, or --markdown for the pull request comment.
Merging the board
Branches that change the board merge like code, and unblock init --git makes that dependable: it sets git to merge .github/unblock.json task by task (a line in .gitattributes, plus this clone's git config). Two branches that add tasks, or change different tasks or different fields of one task, then always merge; links merge as sets. Only real conflicts stop the merge: the same field changed differently on both sides, a task deleted on one side and changed on the other, or a result with a dependency loop. Then git falls back to its usual conflict markers, and the driver lists exactly what clashed. Other clones run unblock init --git once; until then git merges the file line by line, as before. GitHub's own merge button always merges line by line.
Plan changes on pull requests
unblock init --ci adds .github/workflows/unblock.yml. On every pull request it comments with what merging it does to the plan: a one-line summary (for example "44% → 51% done · completes 2 · unblocks 1 · adds 4"), a picture of the affected tasks drawn like the app (cards, rings, what's new, done or now ready), the lists of what changed, and a link that opens the board at the branch with those tasks highlighted.
- It compares the merge result GitHub prepares with the base branch, so only this pull request's changes count.
- It keeps one comment up to date as you push, and adds an Unblock plan check beside it (neutral when something is worth a look, failed only for an invalid board), stays quiet on pull requests that don't touch the board, and fails the check if the pull request would leave the board invalid.
- Done on merge. Write
done: <id>(orcloses:,finishes:,completes:, several ids with commas) in a commit message or the pull request's description. The comment already shows those tasks done, and when the change reaches the default branch, a second job marks them done in the board and commits that. It's the only job with write access to the repository. Anything that isn't an open task on the board, such ascloses: #12, is left alone. - Under Worth a look it flags what the pull request causes: a task marked done while something it waits on isn't, and new tasks with no description (an agent starting them has only the title). These are notes, never a failed check.
unblock lintchecks the whole board locally, including tasks stuck in doing for a week or more (from the file's git history), and the compare view shows the same notes. - It needs nothing but the built-in token (
pull-requests: write). Pull requests from forks get a read-only token, so for those the diff is only in the run's summary. - The picture is an SVG served from
unblock.esh.sh/d/<id>.svg, with a dark version for dark themes. The run sends a small snapshot (only the changed tasks and their neighbours: titles, states and percentages) and proves which repository it comes from with GitHub's OIDC token (id-token: write), so there's nothing to configure. Public repositories get it by default. Private repositories get a Mermaid graph instead, unless you setUNBLOCK_IMAGES: "on", since the snapshot's titles are then viewable by anyone with the link."off"never sends anything.unblock diff main...feature --svg plan.svgdraws the same picture locally. - It runs one release of unblock, checked against its sha256. The workflow downloads
unblock.esh.sh/v/<version>/unblock.pyand refuses to run it if its sha256 isn't the one written into the workflow, so a change on unblock.esh.sh can't change what runs in your repository.unblock init --cipins your CLI's own version when it's a release (installed from PyPI, say), and otherwise the newest release; run it again to move to the newest, and it changes only the download steps, keeping your other edits. Before the first release exists it downloads the latestunblock.pyon each run, and says so. You can also commitunblock.pyto the repository and runpython3 unblock.py ciinstead, as this repository does.
The plan in your README
unblock readme --add puts a Plan section at the end of README.md: a progress badge (.github/unblock-badge.svg, "plan | 54% done · 3 ready") linking to the live board, a line of counts, and a Mermaid graph of the plan, which GitHub draws in place. Big plans show their groups folded so the graph stays readable, with links between tasks in different groups drawn between the groups. The section sits between <!-- unblock:plan --> and <!-- /unblock:plan -->, each on a line of its own, so you can move it anywhere.
With unblock init --ci, the job that runs after a merge refreshes that section and the badge whenever the plan changed, in the same commit that marks done: tasks done. Nothing is sent anywhere: the badge is a file in the repository. unblock readme refreshes it by hand (--check exits 1 when it was stale), and unblock mermaid prints the graph on its own.
MCP server
unblock mcp runs the CLI as an MCP server over stdio, so any MCP client can read and change the board with tools instead of shell commands.
claude mcp add unblock -- python3 ~/.local/bin/unblock mcp # Claude Code, this project
claude mcp add --scope user unblock -- python3 ~/.local/bin/unblock mcp # every project
With uv, nothing needs installing first: uvx fetches unblock-cli from PyPI and runs it.
claude mcp add unblock -- uvx --from unblock-cli unblock mcp
For apps configured with JSON (Claude Desktop and others):
{ "mcpServers": { "unblock": { "command": "uvx", "args": ["--from", "unblock-cli", "unblock", "mcp"] } } }
or, with the CLI installed, "command": "python3", "args": ["/Users/you/.local/bin/unblock", "mcp"].
Tools: show_board, point, say (a line on a task's card: what the agent is doing right now), next_tasks (with all: across every plan in the folder), claim_task, get_task, get_board, diff, digest, update_task, add_task, link_tasks, unlink_tasks, remove_task, list_boards (a folder's plans), create_board (with name: another plan beside the others) and open_viewer (starts unblock view in the background). unblock mcp --read-only offers only the tools that read and show: nothing can change the board, and the board in the chat is view-only. Every tool takes an optional board: the project folder or the board file. The server looks in the folders the client shares (MCP roots) and in its working directory; once a board is used it stays the default. Add -f path/to/.github/unblock.json before mcp to pin one board.
The board in the chat. show_board and point come with an MCP App: in clients that support MCP Apps (Claude Desktop, VS Code GitHub Copilot, Goose and others), the interactive board appears right in the conversation, with the pointed tasks ringed and the agent's note beside them. It's the full app, so you can fold groups, switch to the force view, go full screen and edit. Edits save to the file through the server, and changes the agent makes show up within a few seconds. Clicking a task tells the agent which one you mean ("do this one"), and Work on this in the chat in a task's menu starts on it. Clients without MCP Apps get the same information as text.
report returns, for every task: state (ready, doing, blocked, done), unblockedPct, progressPct, upstream counts, doNext, waitingOn and unblocksOpen, plus the board summary, the ordered next list and the criticalPath. The app shows the same report under JSON → Computed report; both implementations compute identical numbers.
Boards start with "$schema": "https://unblock.esh.sh/schema.json", so editors such as VS Code check and autocomplete them. Editing board.json by hand or with other tools is fine too. Keep ids stable, and check your change with python3 unblock.py validate.
Inside the page, window.unblock offers getBoard(), getReport(), load(board), setStatus(id, status), addTask({...}), link(task, prereq), select(id) and point(ids, {note}) for browser automation.
Tests
python3 -m unittest discover -s tests
Standard library only, a little over a minute, and nothing talks to GitHub: the suites use scratch git repositories, a fake GitHub API and a fake OIDC issuer (which needs the openssl command). They cover the CLI and the model, unblock diff and the pictures, unblock ci, the picture service, the MCP server, the local viewer and the sign-in relay. test_app_diff.py also runs the app's own diff code (cut out of index.html, in JavaScriptCore on macOS or Node elsewhere) and checks it matches unblock.py's exactly, on every pair of versions of this repository's roadmap and on randomly edited boards. .github/workflows/tests.yml runs them on every push and pull request.
python3 tests/mcp_host.py [project] is a small MCP Apps host for trying the board-in-the-chat by hand: it runs unblock mcp, shows the app in a sandboxed iframe with a strict CSP, and logs every message the app sends.
Packaging
python3 packaging/build.py builds dist/unblock_cli-<version>-py3-none-any.whl and an npm package in dist/npm/, with the standard library only. Both ship unblock.py and index.html (so unblock view and the MCP App work offline) and install an unblock command; the npm one runs it with the machine's python3. The package name is unblock-cli, since unblock is taken on PyPI and npm, so once published it's uvx --from unblock-cli unblock or npx unblock-cli. Nothing is published yet. Publishing goes through .github/workflows/publish.yml with PyPI trusted publishing (no stored token): it runs when a v<version> tag is pushed, refuses without a LICENSE file or when the tag doesn't match __version__, builds and installs the wheel, then uploads it. The wheel and the npm package carry the license and its SPDX id (MIT). tests/test_packaging.py installs the wheel into a fresh virtual environment and runs it.
A release is a tag. Pushing v<version> publishes the wheel, and python3 packaging/site.py <dir> builds the site's files with every tagged release at v/<version>/unblock.py and their sha256s in v/releases.json, which is what unblock init --ci pins to. Deploying the site replaces it whole, so the releases are rebuilt from the tags each time.
Hosting the picture service
snap/snap.py (standard library only) stores pull request snapshots and draws them as SVG with unblock.py's renderer, so it never serves SVG that someone else wrote. Uploads must carry a GitHub Actions OIDC token for https://unblock.esh.sh, checked against GitHub's published keys, and are limited per repository per day. Pictures never change, so they're cached forever by GitHub's image proxy and the CDN. snap/deploy.sh runs it as a container next to the site; Traefik sends /d/* and /api/snapshots to it.
Hosting the sign-in relay
GitHub doesn't let a web page complete "Sign in with GitHub" on its own: the code exchange needs the app's client secret. relay/relay.py does that part and nothing else. It keeps no user data; it stores only the GitHub App's own credentials and a signing key.
relay/deploy.shcopies the relay to the server and starts it. Traefik sends/auth/*to it; everything else is still the static site.- The first time, it prints a one-time setup link. Opening it creates the GitHub App from a manifest: it can read repository contents and, when a person asks, write contents, pull requests, issues and agent tasks (for saving edits and starting agents). No webhooks. You click Create GitHub App on GitHub, and GitHub sends the credentials straight to the relay.
- Install the app on the repositories you want to view, or let the viewer send you there when you first open a private board.
License
MIT: see LICENSE. Use it, change it, ship it, host your own copy.
The name "Unblock" and its logo belong to Sqvist AB and aren't covered by the license. A fork or a hosted copy is welcome, but it should go by a name of its own, so people can tell it apart from the hosted service at unblock.esh.sh.
© 2026 Sqvist AB · org.nr 559603-2812
Plan
106 of 120 tasks done, 5 ready to start.
flowchart LR
n0[["Core board<br/>100% done"]]
n1[["GitHub viewer<br/>100% done"]]
n2[["Agents first<br/>90% done"]]
n3[["Merge-friendly format<br/>100% done"]]
n4[["Shows up on GitHub<br/>100% done"]]
n5[["Momentum<br/>100% done"]]
n6[["Launch<br/>40% done"]]
n7[["See plans kept elsewhere<br/>100% done"]]
n8[["Agents from the board<br/>100% done"]]
n9[["Where the board lives<br/>100% done"]]
n10[["Before going public<br/>100% done"]]
n11[["In the conversation<br/>100% done"]]
n12[["Quality of life<br/>100% done"]]
n13[["Jira and Perforce: Unblock at work<br/>39% done"]]
n14("Windows: the CLI crashes printing ▸ and o…<br/>ready")
n15[["Plans anywhere: any file, many per folder…<br/>100% done"]]
n4 --> n1
n6 --> n2
n1 --> n3
n0 --> n4
n1 --> n4
n4 --> n5
n2 --> n6
n3 --> n6
n1 --> n7
n6 --> n13
classDef done fill:#DAF1E7,stroke:#0F8A62,color:#0A6A4B
classDef ready fill:#FDE8DE,stroke:#E84E12,color:#B03A0A
classDef doing fill:#E1E9FD,stroke:#2563EB,color:#1C4DBD
classDef blocked fill:#E7EBEF,stroke:#96A0AD,color:#586473
class n0 done
class n1 done
class n2 doing
class n3 done
class n4 done
class n5 done
class n6 doing
class n7 done
class n8 done
class n9 done
class n10 done
class n11 done
class n12 done
class n13 doing
class n14 ready
class n15 done
Kept up to date by Unblock. Open the live board.
Metadata
Release files for unblock-cli 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unblock_cli-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / unblock_cli-0.2.0-py3-none-any.whl
| Download URL | unblock_cli-0.2.0-py3-none-any.whl |
|---|---|
| Size | 281.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
372cbe6a8c3d10bce6277d8d7d915218533c1349ff57e62c85c0202aa4f4e195
|
|
BLAKE2b-256 checksum How to use checksums |
08cf45cf95419323ff0fa672c27dfa527f6db4583aeff869b8d1ae2746fc763c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logSecurity for unblock-cli 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 1 file in this release has been uploaded with provenance metadata.
Attestation for unblock_cli-0.2.0-py3-none-any.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.