Skip to content
commission

JSON output

You will be able to parse commission’s --json output without guessing: the shape of a task, a context bundle, a board, an event, and a refusal, and the rules that say which of those fields you may depend on.

Every command that emits structured data supports --json. Under --json, stdout carries exactly one pretty-printed JSON value and nothing else; warnings, notices, and refusals go to stderr, also as JSON. A pipeline can read stdout without filtering.

The compatibility promise for these shapes — what may be added, what may not be removed — is in compatibility.md. Field addition is not breaking; parse defensively.

The task

The unit everything else embeds. It is the database row, serialised as-is, which is why a few fields are typed the way SQLite stores them rather than the way you would design them fresh.

{
  "id": "myapp-2",
  "projectId": 1,
  "parentId": null,
  "title": "Webhook ingest endpoint",
  "body": "",
  "status": "open",
  "priority": 2,
  "severity": null,
  "dueAt": null,
  "rank": null,
  "sprint": null,
  "sprintState": null,
  "gate": "shipped",
  "assignee": null,
  "autonomy": null,
  "preferredActor": null,
  "mayExecute": null,
  "version": 1,
  "touches": "[\"src/http\",\"src/db\"]",
  "planKey": "ingest",
  "externalSource": null,
  "externalKey": null,
  "createdAt": "2026-07-27T14:49:17.039Z",
  "updatedAt": "2026-07-27T14:49:17.039Z",
  "closedAt": null,
  "claimedBy": null,
  "claimExpiresAt": null,
  "claimExpired": false
}

Three things that surprise people:

  • touches and mayExecute are JSON-encoded strings, not arrays. They are stored as text and handed back unparsed. JSON.parse(task.touches) gives you the list; null and "[]" both mean “no labels”.
  • status is one of five valuesopen, in_progress, in_review, done, cancelled. There is no blocked: blocked is derived from unmet dependencies at read time, and appears as a section of the board rather than as a value here.
  • version is an optimistic-concurrency counter, incremented on every mutation. Pass it back as --if-version (CLI) or expectedVersion (HTTP) to refuse a write that would clobber a change you did not see.

claimedBy, claimExpiresAt, and claimExpired are joined onto the row by list views; they are properties of the claim, not of the task.

The context bundle

What commission show --json and commission next --json return — everything a fresh session needs in one read.

keytypewhat it is
projectobjectslug, name, vision
taskobjectthe task, as above
parentobject | nullthe parent task, when this one has one
criteriaarray{ id, taskId, text, done, position }done is 0/1
depsarraytasks this one waits on, with their status
dependentsarraytasks waiting on this one
childrenarraysubtasks
linksarrayattached documents and URLs to read
evidencearraywhat has been attached so far
bindingsarrayexternal objects this task is (Jira issue, GitHub PRs)
gateRequiresstring[]evidence kinds the gate still needs
approvalsobject{ required, given, outstanding, stale }
openQuestionsarrayescalations waiting on a human
decisionsarrayanswered questions — the durable reasoning
notesarray{ ts, actor, text }, oldest first
recentEventsarray{ ts, actor, type, payload }
contextobject | nullguidance fingerprint and drift, or null if nothing is tracked
nextCommandsstring[]the commands that make sense from here

nextCommands is worth using rather than reimplementing. It already accounts for the task’s status, its claim, and what its gate is still missing.

Under a budget

--budget <tokens> trims the bundle and says what it dropped:

{
  "truncated": true,
  "omitted": { "events": 12, "olderNotes": 5, "links": 2 },
  "estimatedTokens": 3820,
  "budget": 4000
}

Sections are given up in one fixed order — events, olderNotes, links, dependents, children, bindings, evidence, parentCriteria, deps, criteria, body, vision — so the same bundle at the same budget always degrades identically. Task identity and state are never dropped. The token count is a deliberate estimate (four characters to a token), not a tokenizer: a guardrail against unbounded growth, not an accounting system.

The board

{
  "project": { "...": "..." },
  "inProgress": [],
  "inReview": [],
  "ready": [],
  "blocked": [],
  "recentlyDone": [],
  "stale": [],
  "counts": { "...": "..." }
}

Every array holds tasks. blocked entries carry the blockers that put them there. Nothing in this object is stored — it is one query, computed on read, which is why it cannot disagree with the graph.

The ready frontier

commission ready --json returns { entries, batches }.

Each entry is { task, claimed, claimExpired, touchesOverlap, eligible, routing }:

  • touchesOverlap — ids of other ready tasks sharing a touches label.
  • eligible{ eligible, reason, rule }; rule is autonomy, mayExecute, or null.
  • routing — why this actor would or would not get it, including why (one sentence) and wouldChange (levers, each with the command that pulls it).

batches is a list of id lists with no touches overlap inside a batch — a suggested fan-out. It is a heuristic over labels you wrote, not an analysis of your code.

Events

commission log --json returns { events, nextCursor }:

{
  "events": [
    {
      "id": 4,
      "ts": "2026-07-27T14:55:22.303Z",
      "occurredAt": null,
      "actor": "blaze",
      "projectId": 1,
      "taskId": "myapp-1",
      "type": "transition",
      "payload": { "from": "open", "to": "in_progress" }
    }
  ],
  "nextCursor": 4
}

payload is already parsed. Store nextCursor and pass it back as --after; do not compare timestamps. Ids are a monotonic integer and the log is append-only, so a cursor is exact where a timestamp is a race.

Event type values you will see: project, created, edit, criteria, dep, note, evidence, claim, release, transition, approval, question, answer, link, bind, unbind, sync, workspace.

The inbox

commission inbox --json — what needs a human.

{
  "project": { "slug": "myapp", "name": "myapp" },
  "actor": "blaze",
  "scope": "actor",
  "summary": "1 thing needs your judgment in myapp — 1 escalated decision",
  "items": [],
  "total": 0,
  "counts": {
    "approval": 0, "verification": 0, "decision": 0, "review": 0,
    "blocked_on_human": 0, "divergence": 0, "stale_claim": 0,
    "drift": 0, "delegation": 0
  },
  "inFlight": [],
  "ready": 0
}

counts enumerates every item kind, always, including the zeroes — a consumer never has to distinguish absent from empty. Each item carries a rendered line alongside its structured fields, so text and JSON cannot describe the same situation differently.

Refusals

Refusals are on stderr, in one shape, on every surface. They are documented once in exit codes and the refusal shape.

Rules that hold everywhere

  • stdout is the answer; stderr is everything else. Always.
  • Empty is emitted, not omitted. A list with nothing in it is [], not a missing key. envKeys, warnings, chain, and counts follow this deliberately.
  • Names are never values. The workspace record reports envKeys (names) and envFiles (paths); a secret value cannot reach --json, the event log, or the persisted record, because it never leaves the module that collected it.
  • Text output may change between minor versions. JSON shapes may not — see compatibility.md.

Part of Reference.

This page is docs/reference/json.md in the Commission repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.