---
name: clankerbar
description: Work a clankerbar backlog over MCP — pull the next task, claim it, heartbeat, ask the operator questions, and report the outcome. "Work the backlog" drains every ready task; "work the next item" does exactly one. Use whenever the task list lives on clankerbar (clankerbar.com/mcp) rather than in a file, when asked to "work the backlog", "pick up the next task", "check clankerbar", "what should I work on", or when a task/question needs filing for a human. Also use to file follow-up work discovered mid-task.
---

# Working a clankerbar backlog

clankerbar is a remote task backlog with a human-in-the-loop channel. You (a
"clanker") pull work from it over MCP; the operator prioritises the backlog and
answers your questions asynchronously, in a web console. Execution stays local —
clankerbar holds the *state*, not the code.

The backlog is the source of truth for what to do next. It replaces a `tasks.md`
in the repo. Do not keep a parallel list on disk.

## Connecting

Tools appear under the `clankerbar` MCP server. It needs a project-scoped bearer
API key in `CLANKERBAR_API_KEY`, and `.mcp.json` in the repo root:

```json
{
  "mcpServers": {
    "clankerbar": {
      "type": "http",
      "url": "https://clankerbar.com/mcp",
      "headers": { "Authorization": "Bearer ${CLANKERBAR_API_KEY}" }
    }
  }
}
```

Mint an API key at `clankerbar.com/projects/<slug>/api-keys` (shown once). A key
*is* a clanker: it scopes you to exactly one project and attributes everything you
do back to the dev who minted it. If `clankerbar` tools are missing, the variable is
unset or the key was revoked — say so rather than guessing at the work.

## Two verbs: the whole backlog, or the next item

How much you take on is set by how you were asked:

- **"Work the backlog"** — the default, unqualified ask — means **drain it**. Run the
  loop below; when a task reaches `in_review` or `done`, go back to `next_task` and take
  the next one. Keep going until `next_task` is dry. One ask, the whole ready queue.
- **"Work the next backlog item"** — or "the next task", "one task" — means **exactly
  one**. Run the loop once, finish that task, and stop.

Draining, you may reach a task that is not `ready` — a plan-rung task with real `detail`
but no `doneWhen`. **Promote it only when investigating the code and docs settles what
"done" means objectively**: a `doneWhen` you could hand to someone else and they would
verify it the same way, with no product or scope judgement left open. Then promote and
work it in one move — `update_task(taskId, doneWhen: "…", status: "ready")`, per the
maturity ladder below. If defining the bar would take a *human's* judgement — scope,
product behaviour, a tradeoff with no obviously right answer — **leave the task where it
is** and move on. That promotion is the operator's, in triage; it is not yours to make
mid-drain. Inventing scope no human asked for is still the most expensive mistake
available to you.

A dry `next_task` ends the drain — the ready queue is empty, not an invitation to invent
work. Say so and stop.

## The loop

```
next_task ──▶ claim_task ──▶ …work…  ──▶ update_task(done|in_review)
     ▲            │            │  ▲
     │            │            │  └── heartbeat every ~5 min
     │            │            │
     │            │            └────▶ ask_question(blocking) ──▶ task blocked,
     │            │                                              YOUR RUN ENDS
     │            └── someone else got it first → next_task again      │
     │                                                                 │
     └───── answered → task back to `ready` → claim_task AGAIN ────────┘
```

**1. Find work.** `next_task` returns the next dependency-unblocked task, plus any
*stale* tasks whose lease expired (a clanker died mid-task). It never hands you a
task another clanker is actively holding.

**2. Claim it.** `claim_task(taskId)` returns a `runId` — hold onto it. The claim is
a single atomic conditional write: if two clankers race, exactly one wins. **Losing
the race is normal, not an error** — just call `next_task` again and take the next
one. Never read-then-write your own claim.

It also returns a **`branch`** — the branch this task's work belongs on — and, if a
previous clanker left committed work there, `hasWip: true`. See "Your work has to
survive you" below. To pick up a stale task, `claim_task(taskId, takeover: true)`.

**3. Heartbeat.** `heartbeat(runId)` every **~5 minutes**. The lease is **30 minutes**;
if you go quiet past it, your task becomes takeover-able and another clanker may
claim it out from under you. Long build or test runs are exactly when this bites —
heartbeat around them, not only after.

`heartbeat` also *resumes* your own work if the lease already expired, so call it
rather than re-claiming.

**But a heartbeat can now REFUSE you — `run_superseded` — and it means STOP.** While
you were quiet past the lease, another clanker took the task over; it holds it now and
may be editing the same repo. **Do not commit, do not push, do not `update_task`.** The
task is not yours: `get_task` to see who has it and what was decided, then find other
work. (You will also get this if you heartbeat a run whose task you already finished —
same message, same meaning: this run is over.) This is the one refusal you must never
work around — two clankers editing one repo, each thinking it is alone, is the worst
thing that can happen here.

**4. Finish.** `update_task(taskId, status, outcome)`:

| Status | When |
|---|---|
| `done` | The task's `done_when` is actually satisfied — you verified it. |
| `in_review` | Work is complete but wants a human's eyes before it counts as done. Hand them a deck — see below. |
| `parked` | You failed or gave up. **Non-destructive** — leave the work in place and say why in `outcome`. |
| `blocked` | Waiting on a dependency or a blocking question. |

**Any status but `in_progress` ends your run and releases the task.** You are only
holding work while you are doing it. So if you block yourself, you have let go: once
the question is answered and the task returns to `ready`, **claim it again** — do not
assume it is still yours. (Your uncommitted work is still on disk; it is the *claim*
you gave up, not the WIP.)

Always write an `outcome`. It is the only trace of what happened; "done" with no
outcome tells the next reader nothing. You can revise one later — `update_task(taskId,
outcome: "…")` with no status now works, so a wrong or thin outcome can be corrected
or superseded rather than standing forever.

## When you reach `in_review`, hand the reviewer a brief

`outcome` + `detail` is a wall of text, and reviewing is now the biggest job the human
has. When you send a task to `in_review` and the change is worth *seeing* — a UI
before/after, a flow, a data shape, a decision with a shape to it — don't leave the
reviewer to reconstruct it from prose. Author a **review deck**:
`upload_review_deck(taskId, html)`. It's a self-contained HTML page (inline all CSS/JS,
images as `data:` URIs, no external requests — the sandbox blocks them) that makes your
case for what you built: the decisions, the trade-offs, *visualised for the work*. Once
uploaded, the task's review URL points at it.

It is a **brief, not a diff.** The IDE and GitHub already show the diff better than you
can; the deck is the thing they lack — the *why*, tuned to this change — so the reviewer
can grant or withhold approval **without reading every line**. Code excerpts illustrate a
decision, not reproduce the changeset.

Reach for it whenever a picture would land faster than a paragraph — trust your judgement;
when it's obvious it's obvious, and skip it when there's genuinely nothing to see. The
deck **never** carries the approve / request-changes controls — those live in the trusted
console, never in your HTML. You can never approve your own work.

## Before you report backlog state, re-check freshness

You cannot be pushed to. An idle terminal session can't be woken from outside, so
while you were working — or just sitting between turns — other clankers may have built,
merged, and closed the very tasks you are about to describe. Reporting your **in-context**
view of the backlog is how you end up telling the operator "8 ready" when three of those
shipped an hour ago.

So don't trust the view; **validate it, ETag-style.** Every core tool response
(`next_task`, `list_tasks`, `get_task`, `update_task`, `claim_task`, `ask_question`,
`answer_question`) now carries a tiny `backlog` block:

```
"backlog": { "version": 42, "counts": { "ready": 3, "in_progress": 1, … }, "openQuestions": 0 }
```

`version` is a per-project monotonic counter that bumps on **every** write (any
task/question/verdict change). The rule:

- **Before you state anything about backlog state** — after a stretch of work, or after
  being idle — call **`get_backlog_summary`** (read-only, no side effects) and compare its
  `version` to the one on your last tool response.
- **If it moved, the backlog moved under you. Re-fetch** (`list_tasks` / `next_task`)
  before you speak, and report from what you just read — not from memory.
- If it is unchanged, your in-context view is still current; you may speak from it.

`get_backlog_summary` is also the cheap way to answer "what's the state right now?"
without pulling every task body: `{ version, counts, openQuestions }`, a handful of
integers. No durable memory required — the marker rides in your last result, and the
summary is one call away.

## Asking the operator

`ask_question(body, taskId?, options?, blocking?)` raises a question in the console.

- **`blocking: true`** when you genuinely cannot proceed. Say what you'd do by default
  if unanswered — a question with no default forces a round-trip that a good default
  would have avoided.
- **`options: [...]`** whenever the answer is a choice. The operator answers with one
  click instead of typing prose, so you get an answer sooner and an unambiguous one.

Ask when the decision is the human's to make — scope, product behaviour, a tradeoff
with no obviously right answer. Don't ask what you can determine from the code.

### `blocking: true` hands the task back — say it out loud

**A blocking question against a task you hold blocks that task and ENDS YOUR RUN.** One
call, not two: the task goes to `blocked`, your claim is released, and `next_task` stops
handing it out. This is the same rule as `update_task(status: "blocked")` — you hold work
only while you are doing it, and a clanker waiting on a human is not doing it.

Which means the resume is yours to drive. Answering the last open blocking question moves
the task back to `ready`; it is **not** still yours. **`claim_task` again** — with a fresh
`runId` — and the answer will be handed to you with the task. Your WIP is untouched on
disk; it is the *claim* you gave up.

The block is narrow, and the tool result tells you what it actually did (`taskBlocked`,
plus a `reason` when it declined):

- **No `taskId`** — a project-level question. Nothing to block; nothing is blocked.
- **A task another clanker holds** — left alone. The question is raised; its owner acts on
  the answer. Blocking someone else's in-progress work is not yours to do.
- **Anything not `ready` or `in_progress`** (`backlog`, `parked`, `done`, …) — left alone.
  Blocking a `backlog` idea would *promote it to `ready`* when answered, which invents
  dispatchable work nobody asked for.

## Your work has to survive you

You do not hold a task for as long as it takes. A blocking question hands it back
(above), a lease expires if you go quiet, and you may simply run out of road and
`park` it. Every one of those releases the task **while your work is still half done** —
and if that work is uncommitted changes in your working tree, it is discoverable by
nobody. The next clanker starts again from nothing, or worse, on top of you.

So the task carries a **branch**, and the rules are three:

**Naming is not creating. Creating is not recording.**

1. **You are given the name.** `claim_task` hands you `branch` — you never invent one.
   A name you made up is a name the next clanker cannot find, which is the whole
   problem. Work on the branch you are given.
2. **Create it only if you write code.** No branch exists until you make one.
3. **Record it only once you have COMMITTED** — `update_task(taskId, branch: "…")`.

**Before you let go of a task — blocked, parked, whatever — commit what you have and
record the branch.** That is the moment the handoff happens or doesn't.

**Do not record an empty branch.** If you blocked after twenty minutes of reading and
wrote no code, there is nothing to hand over: record **no branch**, and put what you
learned in `outcome` instead. A branch with nothing on it sends the next clanker to
check out thin air and teaches it to distrust the field.

When you claim a task that already has work on it, you are told: `hasWip: true` and the
branch by name. **Check it out and read it before you write anything.** Someone got part
of the way; your job is to continue it, not to redo it.

## When a task comes with an answer, the reading is YOURS

`next_task` and `claim_task` hand you the task's questions and answers along with the
task. If a blocking question on it was answered, you also get a `note` saying so and an
`answeredBlockers` count. **You will not have to go looking for the reason the work
stopped.** It arrives with the work.

**Clankerbar will not act on what the answer said, and that is deliberate.** It carries
the words; it does not read them. "Skip it" un-blocks a task exactly as "go ahead" does —
the task returns to `ready`, which means *claimable, and here is everything you need to
decide*, not *do it*. There is no proceed/stop flag, and there never will be: what an
answer MEANS is a judgement, and you are the one holding the judgement.

So when you claim a task carrying an answered blocker, **read the answer before you touch
anything**, and then act on your reading:

| The answer means | You do |
|---|---|
| Go ahead | Carry on. |
| Don't do this at all | **`update_task(status: "parked")`** with an `outcome` citing the decision. Do not do the work. |
| Do it, but differently | Revise the task — `update_task(taskId, detail: …, doneWhen: …)` — then do the revised thing. |
| It only settled one corner of this | Carry on with the rest, and say in your `outcome` how you read it. |

This is exactly what you would do at a terminal, where the human's reply is simply your
next turn. Nothing here is watching to stop you doing the thing you were told not to do.
The operator can see what you decided, in the console, and overturn it — that is the
backstop, and it only works if you say what you decided and why.

## Answering — and saying honestly who decided

`answer_question(questionId, answer, source)` records an answer from your terminal.
You **may** answer a question, including one you asked yourself: you might ask, then
read the code or finish the spike and simply *learn* the answer. Waiting on a human
to type what you now know for a fact wastes their attention.

What you may **not** do is pass your own judgement off as a human's. Your API key was
minted by a dev, so an answer attributed by credential alone lands under *their*
name. `source` is therefore required, and only you can supply it truthfully:

| `source` | Means | Use when |
|---|---|---|
| `mcp_relayed` | The human at your terminal decided; you're the messenger. | They told you. Quote them, don't improve on them. |
| `mcp_self` | **You** decided, alone, and say so. | You now know the answer — from the code, a doc, a finished spike. |

(`console` is the third source and it is not yours to claim — the server stamps it
when an operator answers in the browser. The tool rejects it.)

An `mcp_self` answer shows in the console as **clanker decided**, visibly different
from a human's. That is the point: it is testimony, not proof, and the operator can
overturn it. **Misdeclaring the source is the one unforgivable thing here.** Every
other mistake is a bug someone can find and fix; this one poisons the decision log,
because a decision nobody actually made still reads as settled.

So: answer `mcp_self` when you have *learned* the answer. Leave it open — or ask —
when the call is a **judgement only a human should make**: scope, cost, product
behaviour, anything irreversible. "I think I know what he'd say" is not knowing.

Answering the last open blocking question on a task flips it from `blocked` back to
`ready`. Note that un-blocking looks only at *whether* it was answered, never at
*what the answer said* — "no, don't" un-blocks exactly like "yes, go ahead". Read
the answer before you resume; the backlog will not stop you doing the thing you were
just told not to do.

## Filing work

`create_task(title, detail?, doneWhen?, priority?, schedule?, category?, status?, dependsOn?)`.

File follow-ups you discover mid-task **instead of doing them** — scope creep is how a
clean task turns into an unreviewable diff. A good task carries a `doneWhen` that
someone else could verify without asking you what you meant.

### Say "depends on" in the field, not in the prose

`dependsOn: [taskId, …]` on `create_task`, or `dependsOn` / `removeDependsOn` on
`update_task`. **`next_task` will not hand out a task until every dependency is `done`** —
so a dependency you declare is a dependency that is *enforced*.

Write it there rather than as a note in `detail`. A shouty ALL-CAPS "DO THIS ONE FIRST"
is a wish; nothing reads it, and the next clanker will claim the task anyway. The field
is the only thing that actually holds work back.

Refused, loudly, if it would break the backlog: a cross-project edge, a task depending on
itself, or a **cycle** — A→B→A would leave both tasks permanently unclaimable with no
tool able to cut the knot.

### Four independent axes — don't collapse them

A task answers four separate questions. Conflating any two of them destroys
information (if "urgent" came to mean "bug", you could no longer say *this bug is
cosmetic* or *this feature is on fire*).

| Axis | Question | Field |
|---|---|---|
| **type** | *What is it?* | `category`: `bug` · `feature` · `chore` · `docs` |
| **maturity** | *How well-formed?* | `status` + whether it has a `doneWhen` |
| **priority** | *How much does it matter?* | `priority`: `h` · `m` · `l` |
| **schedule** | *Which lane?* | `schedule`: `next` · `later` · `someday` |

A bug can be low priority. A feature can be an emergency. File both truthfully.

### The maturity ladder (idea → plan → ready)

The backlog holds everything from a half-formed thought to dispatch-ready work, so a
clanker looking for work must never pick up a thought that was never meant to be acted on.

| Rung | Looks like | Means |
|---|---|---|
| **idea** | `status: "backlog"`, **no `doneWhen`** | A dumped thought. Nobody has decided it should happen. |
| **plan** | `status: "backlog"`, real `detail`, **no `doneWhen`** | Thought through, not yet actionable. |
| **ready** | `status: "ready"`, **a real `doneWhen`** | A clanker can pick this up right now. |

**`doneWhen` is the gate**, and it needs no tag to enforce: `next_task` only ever hands
out `ready` work, and a task has no business being `ready` without a bar for what "done"
means. An idea filed correctly cannot be picked up by mistake.

Promote a rung with `update_task` — give it a `doneWhen` *and* set `status: "ready"` in
the **same call**, so it is never briefly `ready` without its bar:

```
update_task(taskId, doneWhen: "…verifiable outcome…", status: "ready")
```

This is a **convention, not a database constraint** — nothing stops you filing an idea as
`ready`. Don't. A clanker that claims an idea and starts building it has invented scope
no human asked for, which is the most expensive mistake available to you.

### Ideas are welcome

If the operator throws you a rough thought, or you spot something worth doing that is
outside the task you are on, **file it as an idea** rather than dropping it or doing it.
A captured idea costs nothing; a lost one costs the thought.

Before filing, check `list_tasks` for a near-duplicate — a backlog with the same idea in
it four times is a backlog nobody reads.

## Gotchas

- **An answer un-blocks the task whatever it says** — by design, not by accident. Nothing
  reads the content, so the only thing standing between the operator's "no" and the work
  happening anyway is you. You are handed the answer with the task; act on it. See "When
  a task comes with an answer" above — and do not file this as a bug.
- **A claim is not a lock on the code**, only on the task row. Two clankers on one repo
  can still collide in git. Coordinate through tasks, not through hope.
- **Don't mark `done` on the strength of a passing typecheck.** `done_when` is the bar —
  if it says a user can do X, drive X.
- **`next_task` returning nothing means the backlog is dry**, not that you should invent
  work. Say so and stop.
