6.2 — Kanban tracker API reference
On this page
6.2 — Kanban tracker API reference
If you've gone through the user guide, you already know the kanban tracker as the Development board. Under the hood it's also a fully-scriptable API — every move on the board is one HTTP call, and every MCP tool your IDE agent uses for kanban work calls these endpoints.
This page is the reference for those endpoints: the seven-state lifecycle, the MCP tool signatures, the HTTP API, the audit log shape, the real-time subscription, and the error codes.
If you just want your IDE agent to work through criteria, you don't need any of this — the MCP server handles it. Read this when you're:
- Building a custom UI on top of the kanban data
- Wiring up CI to resolve reviews (the ready-made recipe is 6.3 — CI review driver)
- Writing automations that watch the audit log
- Debugging why a transition got rejected
The lifecycle
Every acceptance criterion moves through these seven states:
┌──────────┐ you approve ┌──────────┐ claim (atomic) ┌──────────────┐
│ draft │ ──── (web only) ──▶ │ ready │ ─── agent or you ▶│ in_progress │
└──────────┘ └──────────┘ └──────┬───────┘
▲ │
│ │ agent submits
system auto-reverts after 24h idle │ (with git SHA + diff URL)
│ ▼
│ ┌─────────────┐
└──────────────────────────│ in_review │
└──────┬──────┘
│
ci runner or web user resolves │
┌──────────────────────┐ │
│ passed (terminal) │ ◀──────┤
└──────────────────────┘ │
┌──────────────────────┐ │
│ failed │ ◀──────┤
└──────────┬───────────┘
│ rework — the ASSIGNED
│ agent, or you
▼
┌──────────┐
│ ready │
└──────────┘
any active state → blocked (agent or user) ↔ unblock restores prior state
| State | What it means | Who can move out of it | Move it where |
|---|---|---|---|
draft | AI just generated the AC; not yet vetted by you | You (web) | → ready |
ready | You approved the spec; claimable | Agent (claim) · You (claim — drag into In Progress) · Auto-revert lands here from in_progress | → in_progress · → blocked |
in_progress | Someone is actively implementing it | Agent (submit / block) · You (block) · System (auto-revert) | → in_review · → blocked · → ready (system) |
in_review | Implementation done, awaiting verification | CI runner or you (web) | → passed · → failed · → blocked |
passed | Verified — terminal state | — | — |
failed | Verification failed; needs rework | You (web) · The assigned agent (rework) | → ready |
blocked | Agent (or human) flagged something preventing progress | Agent or you (unblock) | → prior state (whatever it was when blocked) |
Two of these changed with the build-loop work and are worth calling out, because older material (including earlier versions of this page) says otherwise:
ready → in_progressis no longer agent-only. A web session can claim by dragging a card into In Progress. Claiming binds the caller as the assignee, so who claimed it is recorded either way.failed → readyis no longer human-only. The criterion's assigned agent can re-queue its own rejected work via the rework endpoint. An agent that is not the assignee still gets403 not_assigned_agent— you cannot re-queue someone else's rejected work.
Key principle: the agent owns the work-in-progress states (claim, submit_for_review, block, rework) but cannot resolve its own review. Final acceptance is always gated by either a CI result or a human click. Even if an agent calls the resolve endpoint with the right token shape, it gets 403 agents_cannot_self_resolve.
Roles & permissions
| Action | You (web session) | Agent token (agent scope) | CI token (ci_review scope) | System |
|---|---|---|---|---|
approve (draft → ready) | ✅ | ❌ | ❌ | ❌ |
claim (ready → in_progress, atomic) | ✅ | ✅ | ❌ | ❌ |
report_progress (heartbeat) | ❌ | ✅ | ❌ | ❌ |
submit_for_review (in_progress → in_review) | ❌ | ✅ | ❌ | ❌ |
resolve_review (in_review → passed/failed) | ✅ | ❌ | ✅ | ❌ |
ci_result (in_review → passed/failed) | ❌ | ❌ | ✅ | ❌ |
rework (failed → ready) | ✅ | ✅ assigned agent only | ❌ | ❌ |
block (* → blocked) | ✅ | ✅ | ❌ | ❌ |
unblock (blocked → prior) | ✅ | ✅ | ❌ | ❌ |
auto_revert (in_progress → ready) | ❌ | ❌ | ❌ | ✅ |
list_events (read audit log) | ✅ | ✅ | ✅ | — |
execution_plan (read whole plan) | ✅ | ✅ | ❌ | — |
A token carries exactly one scope — agent or ci_review, never both. That split is the whole reason an agent can't approve itself: the two capabilities can't live on the same credential.
MCP tool reference
Each tool below is exposed by @vibemap.ai/mcp-server and callable from your IDE.
vibemap_get_next_ready_criterion
Find the highest-priority AC in ready state. Use this at session start or after finishing the previous AC.
Input: { projectId: string }
Output: the criterion record (or null if nothing is available to you).
{
"criterion": {
"id": "abc-...",
"project_id": "...",
"story_id": "...",
"title": "User can reset password via email",
"description": "...",
"status": "ready",
"created_at": "..."
}
}
Reservation — and the footgun
The selector applies a reservation filter: it will only return a criterion whose assignee_id is null or equals the caller's own id. Anything assigned to somebody else is skipped, so assigning a card is a real "leave this one alone" signal rather than a decorative label.
The footgun is who the caller is. A personal access token's identity is the agent — the token itself — not the human who owns it. So:
If you assign a criterion to yourself, your own agent will skip it.
That is the intended behaviour (it's how you hold work back for yourself), but it surprises people, because the token is "yours" in every other sense. To hand work to an agent, assign it to that agent by name — your tokens appear in the board's assignee picker under Your agents. See Assigning work.
VibeMap's own first-party tokens — the ones whose actor id reads env_token:* — pass no caller identity and keep unrestricted behaviour. Their actor id isn't a UUID, so they could never be an assignee in the first place. Your personal access token is not one of these.
Because reservation makes an empty result ambiguous — "nothing is ready" and "everything ready belongs to someone else" look identical to a polling loop — the response adds a diagnostic when the second case applies:
{
"criterion": null,
"reason": "reserved",
"reservedElsewhere": 3,
"detail": "No criterion is available to you. 3 ready criteria are assigned to someone else."
}
Treat reason: "reserved" as "back off and tell the human", not as "the project is finished".
vibemap_claim_criterion
Atomically transition an AC from ready → in_progress. Race-safe: two parallel callers will see one succeed and the other receive 409 race.
Input: { criterionId: string }
Output: the new event row.
Errors: 409 race if another agent already claimed it; 422 illegal_transition if the AC isn't in ready.
vibemap_report_progress
Append a progress note to the AC's timeline without changing status.
Input: { criterionId: string, summary: string } (summary 1–2000 chars)
Output: the new event row (from_status === to_status).
vibemap_submit_for_review
Mark the implementation done and request review. Transitions in_progress → in_review. "Done" means a named test: the HTTP endpoint requires the commit, the files that changed and the tests that prove the criterion (see the body below).
Request body (POST /api/mcp/kanban/criterion/[id]/submit-for-review):
| Field | Type | Required | Description |
|---|---|---|---|
git_sha | string (7–64 chars) | ✅ | Commit where the work landed |
changed_files | array (1–1000) of { path, status, additions?, deletions? } | ✅ | status is added, modified, deleted or renamed. Each non-deleted path is linked to the criterion |
tests | array (1–200) of { file, name, ac_codes[] } | ✅ | The tests that prove it. Name each one [AC-042] … (6.3) |
base_sha | string | ❌ | Commit the work branched from |
diff_url | string (URL) | ❌ | PR link or compare URL |
spec_seq | integer | ❌ | The spec version the work was built against |
notes | string | ❌ | Max 2000 chars |
The body is strict: unknown keys are a 422.
MCP tool input:
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
criterionId | string | ✅ | |
gitSha | string | ✅ | Commit SHA (≥7 chars) where the work landed |
diffUrl | string (URL) | ✅ | PR link or compare URL the reviewer can open |
notes | string | ❌ | Free-text notes (max 2000 chars) |
vibemap_resolve_review (CI / human only)
Resolve a criterion in review — in_review → passed or in_review → failed.
A pass needs evidence: a test_run_url, or (for a person accepting the work by hand) a manual_note of at least 10 characters. Without either the endpoint answers 422. Only a CI-token pass is recorded as CI-verified (verification_kind: ci); a pass from a person is recorded as manual, even when it links a run.
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
criterionId | string | ✅ | |
outcome | 'passed' | 'failed' | ✅ | |
testRunUrl | string (URL) | ❌ | CI run / report URL. A pass needs this or manualNote |
manualNote | string (10–2000 chars) | ❌ | Why a person accepts it without a CI run |
notes | string | ❌ | Reviewer notes |
Errors: 403 agents_cannot_self_resolve — agents can't self-pass their own work. Use a ci_review-scoped token (CI runners) or have a human click "Pass" on the web.
vibemap_block_criterion
Flag a criterion as blocked. Transitions any active state → blocked and captures the prior status so unblock can restore it.
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
criterionId | string | ✅ | |
category | enum | ✅ | One of spec_unclear, missing_dep, external_blocker, other |
reason | string | ✅ | Human-readable explanation (1-2000 chars) |
vibemap_unblock_criterion
Restore a blocked criterion to its prior state.
Input: { criterionId: string, resolution: string }
vibemap_list_kanban_events
List the project's transition history, newest first. Use since for incremental fetches.
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | ✅ | |
since | ISO timestamp | ❌ | Only events strictly after this |
limit | number | ❌ | Max events to return (default 200, max 1000) |
HTTP API reference
Every MCP tool is a thin wrapper around an HTTP endpoint. You can call these directly from CI scripts, custom integrations, or curl. All endpoints require Authorization: Bearer <vm_token>.
| Method | Path | Auth scope | What it does |
|---|---|---|---|
| POST | /api/mcp/kanban/criterion/[id]/approve | session-only | draft → ready |
| GET | /api/mcp/kanban/projects/[projectId]/next-ready | agent or ci_review | Next claimable AC, reservation-filtered |
| GET | /api/mcp/kanban/projects/[projectId]/execution-plan | agent | Whole plan, topo-sorted and sprint-grouped |
| POST | /api/mcp/kanban/criterion/[id]/claim | agent or session | Atomic ready → in_progress; binds the caller as assignee |
| POST | /api/mcp/kanban/criterion/[id]/progress | agent | Heartbeat event, no status change |
| POST | /api/mcp/kanban/criterion/[id]/submit-for-review | agent | in_progress → in_review |
| POST | /api/mcp/kanban/criterion/[id]/resolve-review | ci_review or session | in_review → passed/failed |
| POST | /api/mcp/kanban/criterion/[id]/ci-result | ci_review only | in_review → passed/failed, plus the build-evidence join. With per_test, resolves every in-review criterion the run reports |
| POST | /api/mcp/kanban/projects/[projectId]/ci-result | ci_review only | Per-test results for a whole project: each in-review criterion is decided by its own tests |
| GET | /api/mcp/kanban/criterion/[id]/evidence | agent, ci_review or session | Submission history: files, tests, verdict, per attempt |
| POST | /api/mcp/kanban/criterion/[id]/rework | agent (assignee) or session | failed → ready, carrying the review feedback |
| POST | /api/mcp/kanban/criterion/[id]/block | agent or session | * → blocked |
| POST | /api/mcp/kanban/criterion/[id]/unblock | agent or session | blocked → prior |
| GET | /api/mcp/kanban/projects/[projectId]/events | agent or ci_review | Audit log with since/limit |
The routes below are the newest, and are not in the MCP tools reference — call them over HTTP.
POST /api/mcp/kanban/criterion/[id]/ci-result
Your CI pipeline's outcome for a criterion sitting in in_review. Resolved as actor kind ci.
This is a deliberately separate surface from resolve-review, not a synonym: it requires the ci_review scope and rejects agent tokens outright, so the CI driver can never become a back door for an agent approving its own work. A browser session is refused too (403 ci_token_required) — humans resolve from the board.
Body:
| Field | Type | Required | Description |
|---|---|---|---|
outcome | 'passed' | 'failed' | ✅ | |
test_run_url | string (URL) | ✅ | The CI run. Required here, unlike on resolve-review — a CI verdict with no run behind it isn't evidence. |
git_sha | string (≥7 chars) | ❌ | The commit that was tested |
per_test | array (max 5000) of { name, ac_codes[], outcome } | ❌ | Per-test results. outcome is passed, failed or skipped; ac_codes are the [AC-042] tags parsed from the name. With it, each criterion is decided by its own tests (below) |
notes | string | ❌ | Max 2000 chars |
Per-test mode. When per_test is present, outcome is the run's overall result and is not applied to any criterion by itself. Each AC code reported is looked up in the criterion's project: if it is in_review, all its tests passing resolves it passed, any failing resolves it failed (the failing test names go in the event's notes, which become the rework feedback), and all-skipped leaves it alone. Codes that are unknown, or whose criterion is not in review, are ignored and listed. When git_sha is sent and differs from the commit the agent submitted, the criterion is ignored as sha_mismatch; without git_sha it resolves and the event carries sha_unverified: true. per_test must be non-empty and name at most 500 distinct codes (422). Response: { resolved: [...], ignored: [...], event }, where event is the addressed criterion's own event when the run resolved it. POST /api/mcp/kanban/projects/[projectId]/ci-result takes the same body (with per_test required) and needs no criterion id.
On success it also fires the build-evidence join (kanban/build.evidence.record) for each resolved criterion. It records the evidence on the criterion and links it to every file the submission reported. The join runs out of band — if it fails, the resolve still stands. The resolve itself sets verification_kind to ci.
Errors: 403 ci_token_required (session caller) · 403 agents_cannot_self_resolve (agent token) · 403 insufficient_scope (token lacks ci_review) · 422 illegal_transition (not in in_review; single-criterion form) · 422 invalid_payload · 404 unknown criterion or project (the project route answers 404 for a project the token cannot reach, too) · 409 race.
GET /api/mcp/kanban/criterion/[id]/evidence
The criterion's submission history, newest first, read from the event log. ?limit= is 1 to 50 (default 20). Each attempt: { sha, base_sha, files[], tests[], outcome, test_run_url, verification_kind, resolved_by_kind, failing_tests[], manual_note, submitted_at, at }; outcome is passed, failed, pending or superseded. The response also carries criterion_id, project_id, ac_code, status and the criterion's current verification_kind. Shape and an example: 6.3.
Full walkthrough, including the GitHub Actions workflow: 6.3 — CI review driver.
POST /api/mcp/kanban/criterion/[id]/rework
Re-queue a rejected criterion: failed → ready.
The caller must be the criterion's assigned agent — assignee_kind = 'agent' and assignee_id equal to the calling token's own id. Any other agent gets 403 not_assigned_agent, and so does a first-party env token, which has no agent identity and was never bindable as an assignee. A browser session can always do this; that path is the Send back to Ready drag on the board.
Body:
| Field | Type | Required | Description |
|---|---|---|---|
plan | string | ❌ | Max 2000 chars — what you intend to do differently |
review_feedback is injected server-side from the most recent failed event, so a retry always carries the reason it failed (outcome, test_run_url, notes, resolved_at). Don't send it yourself — the payload schema is strict and an extra key is a 422.
The resulting kanban_events row is also the human-visible notification that rework started: it's what surfaces in the events feed and the card's timeline.
Errors: 403 not_assigned_agent · 403 insufficient_scope · 422 illegal_transition (not in failed) · 409 race.
GET /api/mcp/kanban/projects/[projectId]/execution-plan
The whole plan in one read, instead of discovering it one next-ready call at a time. Read-only; requires the agent scope.
Criteria are topologically sorted by hard dependency, then grouped by sprint in sprint position order, with an Unscheduled group last for anything without a sprint.
{
"projectId": "...",
"total": 47,
"cycle": null,
"groups": [
{
"sprintId": "...",
"name": "Auth & Sign-up",
"position": 1,
"criteria": [
{
"id": "...",
"title": "User can sign in with email",
"status": "ready",
"storyId": "...",
"sprintId": "...",
"assignee": { "kind": "agent", "id": "..." },
"order": 0,
"ready": true,
"blockedOn": []
}
]
}
]
}
orderis the global topo position, not the position within the group.readyistrueonly when the criterion is inreadyand every hard prerequisite haspassed.blockedOnlists the prerequisite ids that haven't.assigneeisnullwhen unassigned — combine it with the reservation rule to see which items your agent will actually be served.
Dependency cycles. A surviving hard-dependency cycle makes a strict order impossible. Rather than fail, the plan falls back to created-at order and names the offenders:
{
"cycle": {
"memberIds": ["...", "..."],
"message": "Hard dependency cycle detected — strict ordering impossible; plan falls back to created-at order. Resolve the dependency-cycle finding first."
}
}
A non-null cycle means the ordering you're reading is not dependency-respecting. Fix the cycle before trusting it.
Request/response shape
POST endpoints accept either:
- Bare object:
{ "git_sha": "...", "diff_url": "..." } - Wrapped:
{ "payload": { "git_sha": "...", "diff_url": "..." } }
Success response (status-changing routes):
{
"event": {
"id": "uuid",
"project_id": "...",
"entity_type": "criterion",
"entity_id": "...",
"from_status": "in_progress",
"to_status": "in_review",
"actor_kind": "mcp",
"actor_id": "env_token:agent",
"payload": { "git_sha": "...", "diff_url": "..." },
"created_at": "..."
}
}
Curl examples
# Find the next ready AC
curl https://vibemap.ai/api/mcp/kanban/projects/$PROJECT_ID/next-ready \
-H "Authorization: Bearer $VIBEMAP_API_KEY"
# Claim it
curl -X POST https://vibemap.ai/api/mcp/kanban/criterion/$AC_ID/claim \
-H "Authorization: Bearer $VIBEMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# Submit for review with evidence
curl -X POST https://vibemap.ai/api/mcp/kanban/criterion/$AC_ID/submit-for-review \
-H "Authorization: Bearer $VIBEMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"git_sha":"a1b2c3d","diff_url":"https://github.com/me/proj/pull/42","changed_files":[{"path":"lib/title.ts","status":"added"}],"tests":[{"file":"tests/title.test.ts","name":"[AC-042] rejects an empty title","ac_codes":["AC-042"]}]}'
# Block it
curl -X POST https://vibemap.ai/api/mcp/kanban/criterion/$AC_ID/block \
-H "Authorization: Bearer $VIBEMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"category":"spec_unclear","reason":"AC says \"fast\" — what latency target?"}'
# Replay history (incremental from last seen)
curl "https://vibemap.ai/api/mcp/kanban/projects/$PROJECT_ID/events?since=2026-05-09T00:00:00Z&limit=50" \
-H "Authorization: Bearer $VIBEMAP_API_KEY"
CI integration
Wire your test runner to the ci-result endpoint with a ci_review-scoped token, most simply through npx @vibemap.ai/mcp-server ci-report. That route is purpose-built for this: it resolves criteria from their own tagged tests, records build evidence and links each criterion to the files it touched, which resolve-review does not.
The full recipe — minting the token, the workflow YAML, and how to get the criterion id into the job — is 6.3 — CI review driver.
Convention for linking commits to ACs
CI needs to know which criteria a run proves. The convention is in the test names: start each test name with the criterion's code in square brackets, [AC-042] rejects an empty title. ci-report reads those tags, so the workflow never needs a criterion id. Commits carry the same code as a Spec-Ref: AC-042 trailer so the history is searchable.
The API enforces the tags only in the sense that an untagged test proves nothing: it never moves a card. See 6.3.
Audit & event log
Every transition produces an event you can query via the events endpoint above. Event shape:
| Field | Type | What it captures |
|---|---|---|
id | uuid | Primary key |
project_id | uuid | Scope |
entity_type | 'feature' | 'story' | 'criterion' | What kind of thing changed |
entity_id | uuid | The AC (or feature/story) ID |
from_status | text | null | Status before; null only on creation events |
to_status | text | Status after |
actor_kind | 'user' | 'mcp' | 'ci' | 'system' | Who did it |
actor_id | text | User UUID, env_token:agent, env_token:ci, or system:auto-revert |
payload | jsonb | Type-specific evidence (see below) |
created_at | timestamptz | When |
Payload shapes by transition
| Event | Payload contents |
|---|---|
approve | {} |
claim | {} |
progress (no status change) | { summary } |
submit_for_review | { git_sha, changed_files[], tests[], base_sha?, diff_url?, spec_seq?, notes? } |
resolve_review | { outcome, test_run_url?, manual_note?, notes? } (a pass needs test_run_url or manual_note) |
ci_result | { outcome, test_run_url, git_sha?, per_test?, notes? } (in per-test mode per_test holds only that criterion's tests, and a failed event's notes lists the failing test names) |
rework | { plan?, review_feedback? } — review_feedback is server-injected |
block | { category, reason, prior_status } |
unblock | { resolution } |
auto_revert (system) | { idle_hours: 24 } |
Real-time subscription (for custom UIs)
The events feed is published in real time. You can subscribe from a browser client to stream transitions as they happen — useful for building your own kanban UI or a status dashboard. Your client only sees events for projects you can access.
Operational behavior
| Background job | Schedule | Effect |
|---|---|---|
| Stuck-claim detector | Hourly | Finds ACs in in_progress with last event >24h old; auto-reverts to ready |
| Long-blocked metric | Daily at 09:00 UTC | Emits an analytics event for ACs in blocked >7 days |
If you'd like the long-blocked metric to also notify Slack/email, wire your own alerting against the events feed — VibeMap doesn't push notifications natively in v1.
Error reference
| Error | When it fires | What to do |
|---|---|---|
agents_cannot_self_resolve | Agent token tried to call resolve_review or ci-result. | Resolve with a ci_review-scoped token (CI) or a session cookie (human). |
ci_token_required | A browser session called ci-result. | Sessions resolve from the board, or via resolve-review. |
insufficient_scope | The token's single scope isn't the one this route needs. | Check whether you need agent or ci_review — a token has exactly one. |
not_assigned_agent | An agent called rework on a criterion assigned to someone else (or to nobody). | Only the assigned agent may re-queue. Assign it first, or send it back from the board. |
race | CAS claim lost — another caller already moved the AC. | Call get_next_ready_criterion again to find a different one. |
illegal_transition | Target status not legal from current state for this actor. | The response includes a legalNextStates array showing valid moves. |
entity_not_found | Criterion ID doesn't exist (or you don't have access). | Verify the ID; check project membership. |
not_blocked | Tried to unblock an AC not in the blocked state. | Read the current state via list_kanban_events first. |
invalid_payload | Validation failed. | The response's issues array shows which fields failed and why. |
forbidden | Session user not a member of the project. | Add the user to the project, or use a different account. |
unauthenticated / invalid_api_key | Bearer token missing or wrong. | Recheck VIBEMAP_API_KEY env in your IDE config. |
Notes on terminal states
passedis terminal by design. To "reopen", edit the AC's content and approve a new draft, or open a follow-up AC. This intentional friction keeps the audit log trustworthy.submit_for_reviewrequiresgit_sha,changed_filesandtestsat the schema level. Local-only experiments can pass a placeholder sha (e.g.local-plus the first 7 chars of a hash). See the local-only scenario in the user guide.
Related docs
- Kanban tracker: step-by-step user guide — the agent loop, assignment, and the scenarios
- CI review driver — wiring your test pipeline to resolve reviews automatically
- 5.1 — MCP server overview — the broader MCP integration
- 5.2 — Full tools reference — every MCP tool, including non-kanban ones
- 4.1 — API overview — general API conventions