Skip to main content

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 short llms.txt next 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) and mcp_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 its deploy.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) and deploy.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, /app and 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.json or board.json on 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 (or taskmaster, backlog, speckit, kiro) to choose. Nothing in the repository is changed; unblock import keeps the result as a board.
  • No board yet: the page offers to create .github/unblock.json on 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 /auth keeps 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: use index.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), and checklist: {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. dependsOn lists ids that must be done first. Depending on a group means depending on every task inside it. A group's own dependsOn applies to everything inside it.
  • Ready vs Blocked are computed, never stored. A todo task 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:

  1. the file you name: -f, or the MCP tools' board argument (a file, a folder, or a plan's name);
  2. UNBLOCK_FILE;
  3. the folder's choice (unblock use), kept in .unblock/settings.json and looked up to the top of the repository;
  4. .github/unblock.json, unblock.json or board.json, in the nearest folder (inside a repository: walking up to its top) that has one;
  5. the only *.unblock.json in 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 pull show 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 view finds 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>&note=<text> to any board address, for example https://unblock.esh.sh/gh/<owner>/<repo>#point=auth-api&note=Blocked%20on%20this. unblock point prints this link when the board is in a GitHub repository.
  • From MCP: the point tool 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> (or closes:, 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 as closes: #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 lint checks 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 set UNBLOCK_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.svg draws the same picture locally.
  • It runs one release of unblock, checked against its sha256. The workflow downloads unblock.esh.sh/v/<version>/unblock.py and 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 --ci pins 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 latest unblock.py on each run, and says so. You can also commit unblock.py to the repository and run python3 unblock.py ci instead, 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.

  1. relay/deploy.sh copies the relay to the server and starts it. Traefik sends /auth/* to it; everything else is still the static site.
  2. 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.
  3. 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.

Sqvist AB
© 2026 Sqvist AB · org.nr 559603-2812

Plan

Plan: 91% done

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)

Table of built distributions (wheels) for unblock-cli 0.2.0
File Interpreter ABI Platform
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 log

Security 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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page