# Working with the codexworker.com control plane

A detailed, step-by-step guide for humans and AI coding agents: how to
authenticate, create projects, work with tasks, move statuses, and run the
merge flow through the REST API at `https://www.codexworker.com/api/v1`.

This file is public — agents and models can fetch it directly without any
credentials:

```bash
curl https://www.codexworker.com/agent-guide.md
```

The machine-readable API spec is served at `https://www.codexworker.com/openapi.yaml`.

Human readers: the getting-started page at `https://www.codexworker.com/help`
is the human-side counterpart of this guide — registration, projects, agent
tokens, task descriptions, reading the board and what every status means.

## 1. What the control plane is

- **Projects** are containers of work. A project has members — only members
  (and admins) can see or act on it.
- **Tasks** are units of work inside a project. Each task has a **status**
  (where it is in the workflow) and a **priority** (LOW, NORMAL, HIGH).
- The **API** is plain REST with JSON bodies under `/api/v1`. Agents fetch
  their work queue from the API — not from a chat or the command line.
- The **web UI** mirrors the API: `/dashboard`, `/projects`, `/projects/{id}`
  (project hub with the kanban board), `/projects/{id}/tasks` (task list
  with search), `/projects/{id}/tasks/new`, `/projects/{id}/users`,
  `/tasks/{id}` (task detail with comments), `/tasks/{id}/session` (agent
session stream with live execution), `/account` (password,
  two-factor auth, API tokens).

## 2. Base URL

- Production base URL: `https://www.codexworker.com/api/v1`
- Always call **www.codexworker.com** directly. The apex domain
  (`codexworker.com`) redirects to www **without forwarding the
  Authorization header**, so Bearer calls made against the apex fail with
  401.
- This guide is public at `https://www.codexworker.com/agent-guide.md`
  (plain GET, no authentication).

## 3. Authentication

1. Register an account at `/register` (project access is granted by an admin).
2. Log in at `/login`.
3. Open `/account` → **API tokens** → **Create token**:
   - give it a recognizable name (e.g. `opencode worker`),
   - optionally **scope it to one project** (recommended: least privilege —
     a leaked token cannot touch your other projects).
4. The token value is listed in the token table on `/account`. Send it with
   every API call:

```bash
curl -H "Authorization: Bearer <token>" \
  https://www.codexworker.com/api/v1/projects
```

Notes:

- **Rotate** a token when it leaks — the old value stops working
  immediately.
- Token creation/rotation only happens on the web account page; the API can
  use tokens but cannot manage them.
- Two-factor authentication and password changes affect web login only, not
  the API.

### 3.1 The agent token file (codexworker_token.json)

Agents keep their API credentials and their per-project configuration in a
single JSON file in the repository root, named `codexworker_token.json`:

```json
{
  "api_token": "<the 64-hex token value from /account>",
  "model_id": "<the model name identifying this agent (optional, see below)>",
  "agent_info": "<where this agent runs, e.g. \"PC - home - 8 cores, 32GB RAM\">",
  "project_id": 3
}
```

The guide is the **common** document for every project; the token file is
the **per-project** part: `api_token` and `project_id` are required,
everything else in the file (`model_id`, options and named project
commands) is optional and validated on start.

`model_id` names the model this agent runs as. The `codexworker-agent`
supervisor **always reports the model the OpenCode session actually runs
with** — it reads it from OpenCode and registers the name automatically
when the server does not know it yet — so supervised agents work without
`model_id` at all; the field only pins the identity *before the first
session* (and is what manual/bash pings identify with).

`project_id` is the id of the project this agent works on; it is the number
the commands in this guide put into the `projects/<project id>` part of
their URLs. The public copy of this guide shows a sample number there; the
per-project preview (`/projects/{id}/agent-guide/preview`) and the
downloadable .md from the project page show the project's own number.

How to create it:

1. Create an API token (step 3 above) and copy its value.
2. Register the model name this agent runs on — once per name, the call is
   idempotent, and `model_id` must match a registered name. Model names are
   registered **per user account**: the name is stored in the account's own
   namespace, so another account registering the same name gets its own
   entry — nobody can pre-register (and impersonate) a name your agents
   will auto-register:

   ```bash
   curl -X POST -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{"name":"gpt-5-codex"}' \
     https://www.codexworker.com/api/v1/models
   ```

   `GET /api/v1/models` lists the names your account can use — the names
   you registered plus the legacy unowned ones. The `codexworker-agent`
   supervisor does this automatically for the model it discovers from the
   OpenCode session, so supervised agents do not need this step (and can
   leave `model_id` out of the token file entirely).
3. Write the values into `codexworker_token.json`. `agent_info` is free text
   (up to 255 characters) describing the machine the agent runs on — OS,
   hardware, location, whatever helps the owner see where the agent is
   working; it is shown in the **Agents online** table on the dashboard.
   `project_id` is the id of the project this agent works on. The file is a
   secret — never commit it to a repository.

   Optionally, `probe_commands` lists the command names the agent should
   check on its own machine (e.g. `["git", "go", "npm"]`). The agent reports
   the ones it actually finds — with the first line of their version output —
   on every ping, and they are shown on the dashboard next to `agent_info`.
   Without the field the default list (git, go, node, npm, python3, docker)
   is probed; `"probe_commands": []` disables the probe.

#### Named project commands (`commands` / `run_in`)

Besides identity, the token file describes **how this project builds,
tests and verifies** as named, machine-actionable commands. The agent uses
them three ways: the supervisor runs `test` itself, the OpenCode prompt
gets the exact lines so the worker stops guessing, and later dashboard
buttons (release, merge, ...) map onto the same names.

```json
{
  "api_token": "…", "model_id": "…", "agent_info": "…", "project_id": 3,

  "run_in": {
    "app": { "container": "codexworker_app", "workdir": "/var/www/dev.codexworker.com" }
  },

  "commands": {
    "test":  { "cmd": "go test ./...",       "run_in": "app" },
    "lint":  { "cmd": "govulncheck ./...",   "run_in": "app" },
    "build": { "cmd": "bash build.sh" }
  }
}
```

Rules:

- Command and `run_in` names are lowercase (`[a-z]` then letters, digits,
  `_` or `-`, up to 32 characters). `cmd` is required (max 1000 chars).
- `run_in` entries name a **docker container** the commands execute in via
  `docker exec`; `container` is the running container's name, `workdir`
  (optional) is the working directory inside it, `user` (optional) the
  exec user. A command **without** `run_in` runs on the host in the
  repository directory.
- A command with `run_in` executes in a **running** docker container via
  `docker exec` — if the container is down the command fails with a plain
  error; start the project first. Git always runs on the host (credentials,
  remotes); never route git through `run_in`.
- The name `test` is special: the supervisor runs it after every task
  (falling back to `test_cmd` / auto-detection when unset).
- Errors name the problem exactly (unknown field, bad name, missing
  `run_in` reference, missing `container`); the file is a secret — never
  commit it (the agent also excludes it from git automatically).

Reading the values:

```bash
TOKEN=$(python3 -c 'import json; print(json.load(open("codexworker_token.json"))["api_token"])')
MODEL=$(python3 -c 'import json; print(json.load(open("codexworker_token.json")).get("model_id",""))')
INFO=$(python3 -c 'import json; print(json.load(open("codexworker_token.json")).get("agent_info",""))')
PROJECT=$(python3 -c 'import json; print(json.load(open("codexworker_token.json"))["project_id"])')
```

**Before every task, verify the token file.** `api_token` and `project_id`
must be present and non-empty, and `model_id` must be present and a name
registered to your account (`GET /api/v1/models`) when you ping manually —
the `codexworker-agent` supervisor discovers and registers the model itself
and works without it. A missing or stale file means the agent would work with
the wrong credentials or on the wrong project — stop and fix the file
before calling the API:

```bash
python3 - <<'EOF'
import json, sys
try:
    tf = json.load(open("codexworker_token.json"))
except Exception as e:
    sys.exit(f"codexworker_token.json missing or invalid JSON: {e}")
required = [k for k in ("api_token", "project_id") if k not in tf]
if required:
    sys.exit("codexworker_token.json is missing required fields: " + ", ".join(required))
empty = [k for k in ("api_token", "project_id") if not str(tf[k]).strip()]
if empty:
    sys.exit("codexworker_token.json has empty required fields: " + ", ".join(empty))
optional = [k for k in ("model_id", "agent_info") if k not in tf]
if optional:
    print(f"note: optional fields not set (fine for supervised agents): {', '.join(optional)}")
print(f"token file ok (project {tf['project_id']})")
EOF
```

### 3.2 Agent presence (ping)

While working, an agent pings the control plane so the dashboard can show
which agents are online, with which model, on which machine and on which
task. The ping uses the values from the token file:

```bash
# working on task 12
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"model_id\":\"$MODEL\",\"agent_info\":\"$INFO\",\"task_id\":12,\
\"capabilities\":[{\"name\":\"git\",\"version\":\"git version 2.45.0\"},{\"name\":\"go\",\"version\":\"go version go1.26.0 linux/amd64\"}]}" \
  https://www.codexworker.com/api/v1/agents/ping

# idle (no task)
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"model_id\":\"$MODEL\",\"agent_info\":\"$INFO\"}" \
  https://www.codexworker.com/api/v1/agents/ping
```

The `codexworker-agent` supervisor sends the pings for you, fills
`model_id` with **the model the OpenCode session actually runs with** and
sends `capabilities`: it reads the model from OpenCode's session data,
registers the name on first use when the server does not know it, and
reports that model from then on (before the first session it identifies
with `opencode_model` when set, then the model the last session ran with,
then the token file's `model_id`). It probes the machine for the commands
in `probe_commands` (see the token file section) and reports the ones it
found, re-probing at most once an hour. A manual ping can carry the same
field too — an array of up to 20
`{"name": <command>, "version": <first line of its version output>}`
objects. Omit it (or send `[]`) to clear the stored list.

A ping may also carry the agent's own identity: `agent_version` (the
release label, i.e. what `codexworker-agent -version` prints) and
`agent_hash` (the sha256 fingerprint of its own binary, 64 lowercase hex
characters). Both are optional and an omitted value keeps the previously
reported one. The `codexworker-agent` supervisor sends them on every ping
automatically; the dashboard shows the version on the agent card and
badges the agent as **outdated** when the reported version differs from
the release published in `/downloads/version.json` (source builds show as
"dev build" instead). The comparison is also available to API consumers:
`GET /api/v1/agents` returns `agent_outdated: true` together with
`agent_current_version` for a stale label, and omits both for current or
unknown ones.

Presence is kept fresh automatically: every successful API call the agent
makes refreshes its "last seen", and a call that names a concrete task
(`/api/v1/tasks/{id}` and its comments) rebinds the agent to that task —
the dashboard shows what the agent is doing right now without constant
pings. Still ping to (1) register on first start, (2) update `agent_info`
or the bound task explicitly, and (3) mark yourself idle (omit `task_id`);
keep pinging occasionally if your workflow goes long stretches without API
calls. An agent counts as online for 15 minutes after its last ping or API
call. `task_id` must be a task of a project the token can access; omit it
to mark the agent idle. `agent_info` (up to 255 characters) and
`capabilities` (the toolchain the probe found) are shown on the dashboard
next to the agent. `GET /api/v1/agents` lists the agents online right now.

**One token is one agent.** Presence is keyed by the API token, not by the
model: two agents of one user (one token per machine) — even running the
same model — keep separate rows and both show up on the dashboard and in
`GET /api/v1/agents`. `model_id` names the model the agent currently
identifies with and follows the agent across model switches.

### 3.3 Agent runtime metrics (OpenCode session state)

While working inside an OpenCode session, an agent's supervisor reports the
session runtime state to the control plane so the dashboard can show live
activity (tokens, cost, runtime):

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "task_id": 12,
    "session_id": "ses_abc123",
    "status": "running",
    "model": "provider/model-name",
    "input_tokens": 12543,
    "output_tokens": 4382,
    "reasoning_tokens": 2100,
    "cache_read_tokens": 8000,
    "cache_write_tokens": 1200,
    "cost": 0.42,
    "runtime_seconds": 184,
    "tool_calls": 17
  }' \
  https://www.codexworker.com/api/v1/agents/metrics
```

Rules:

- `session_id` is required (the OpenCode session); every other field is
  optional — send only what is currently available.
- Reports are polled every few seconds; each report **updates** the same
  record (agent + session key). Values you omit keep their last reported
  value; nothing is stored more than once, and no task comments are created.
- The identity (user, model) comes from the token, never from the body.
- `task_id` must be a task of a project the token can access; a report that
  names it counts as agent activity on that task (presence stays fresh).
- The runtime `status` (`starting`, `running`, `waiting`, `completed`,
  `failed`, `interrupted`) describes the **OpenCode session**, not the
  codexworker task — reporting metrics never changes the task status, and
  `DONE` stays human-controlled through the normal task flow.

Read the state back:

```bash
# the latest metrics of a task (404 while none were reported)
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/tasks/12/metrics

# the caller's own latest runtime state; idle object when nothing reported
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/agents/me/metrics
```

The dashboard shows the reported state on the **Agents online** table
(token total, cost, runtime) and refreshes it live; `GET /api/v1/agents`
also carries the same data in each agent's `runtime` object.

### 3.4 Agent runtime events (OpenCode session stream)

Besides the current state (section 3.3), an agent can report what actually
happens inside the OpenCode session as a stream of events: generated text,
reasoning, tool calls and state changes. Events are stored as history per
task (readable and paginable) and at the same time update the session's
current runtime state.

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "task_id": 12,
    "session_id": "ses_abc123",
    "events": [
      {"type": "step",     "event_id": "op_0", "sequence": 0, "content": "start"},
      {"type": "text",     "event_id": "op_1", "sequence": 1, "content": "Working on the failing test."},
      {"type": "reasoning","event_id": "op_2", "sequence": 2, "content": "The test expects 200 but gets 500 because..."},
      {"type": "tool",     "event_id": "op_3", "sequence": 3, "tool": "bash", "status": "running", "description": "Running the test suite"},
      {"type": "tool",     "event_id": "op_4", "sequence": 4, "tool": "bash", "status": "completed", "description": "Running the test suite"},
      {"type": "status",   "event_id": "op_5", "sequence": 5, "status": "running"},
      {"type": "step",     "event_id": "op_6", "sequence": 6, "content": "tool-calls", "input_tokens": 12543, "output_tokens": 4382, "cost": 0.42, "runtime_seconds": 184, "tool_calls": 17}
    ]
  }' \
  https://www.codexworker.com/api/v1/agents/events
```

The answer is `{"accepted": 7, "duplicates": 0}`.

Rules:

- `session_id` is required; `task_id` is optional and shared by the batch.
  Without it the events are still stored and update your own runtime state.
- A batch holds 1 to 200 events. Event types:
  - `text` / `reasoning` — generated message text; `content` is required
    (max 32768 characters).
  - `tool` — a tool call; `tool` (max 100 chars) and `status` are required
    (`pending`, `running`, `completed`, `error`), `description` optional
    (max 2048 chars).
  - `status` — the OpenCode session runtime state; `status` is required
    (same values as in section 3.3).
  - `metrics` — the metric fields from section 3.3 (all optional, same
    validation).
  - `step` — a model round-trip marker of the OpenCode session, so the live
    stream shows a round-trip happening even before its first tool or text
    event. The step start is `{"type":"step","content":"start"}`; the step
    finish is the finish reason (e.g. `"tool-calls"`) in `content`, plus the
    optional metric fields of that step (counters, cost, runtime, tool
    calls — same validation as `metrics`). `content` is up to 64 characters
    and all counters are optional; a step marker never changes the session
    status.
- `event_id` (max 64 chars) is optional but recommended: events that carry
  one are **idempotent** per (agent, session) — retransmitting the same
  batch after a failed request does not create duplicates, and the answer
  tells you how many events were skipped as duplicates. Events without an
  `event_id` are always stored.
- `sequence` is your own ordering number (optional, non-negative).
- The identity (user, model) comes from the token, never from the body;
  `task_id` must be a task of a project the token can access, and a batch
  that names it counts as agent activity (presence stays fresh).
- Events never change the codexworker task status; `DONE` stays
  human-controlled through the normal task flow.

A batch also updates the session's current runtime state from its events:
the latest `status` event becomes the runtime state, the latest `tool`
event the live tool activity, the latest text/reasoning the last message,
and `metrics` events update the counters — exactly like `POST
/api/v1/agents/metrics` does.

Read the stream and the state back:

```bash
# the task's event history, newest first
# (cursor pagination: ?limit=100&before=<event id>)
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.codexworker.com/api/v1/tasks/12/events?limit=50"

# the latest runtime state of a task, now including the live fields
# (current_tool, current_tool_status, current_tool_description,
# last_text, last_reasoning, last_event_id) — 404 while none reported
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/tasks/12/runtime

# the caller's own latest runtime state; idle object when nothing reported
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/agents/me/runtime
```

`GET /api/v1/tasks/{id}/metrics` and `GET /api/v1/agents/me/metrics`
(section 3.3) remain available and return the same data.

### 3.5 Live execution stream of a task (Server-Sent Events)

```text
curl -N -H "Authorization: Bearer $TOKEN" \\
  "https://www.codexworker.com/api/v1/tasks/12/events/stream?after=0"
curl -N -H "Authorization: Bearer $TOKEN" \\
  -H "Last-Event-ID: 4152" \\
  "https://www.codexworker.com/api/v1/tasks/12/events/stream"
```

`GET /api/v1/tasks/{id}/events/stream` is the live variant of the event
history: a **Server-Sent Events** (`text/event-stream`) connection that
pushes every new runtime event the moment it is stored — no polling, no
waiting for the task to finish. Browsers authenticate with their cookie
session (the agent session page `/tasks/{id}/session` uses it for its
**Live execution** section), scripts and agents with the Bearer token.

Frame format — the same AgentEventEntry objects as the history endpoint,
one frame per event, flushed immediately:

```text
id: 4152
data: {"id":4152,"task_id":12,"type":"text","content":"Working on...",...}

data: {"id":4153,...}
```

Behavior rules:

- `?after=<event id>` starts the stream above that id (`after=0` replays
  the whole available history first, then continues live). Without a cursor
  a reconnecting client's `Last-Event-ID` header is used, so an SSE client
  continues exactly where it left off.
- Keepalive comment frames (`: ping`) keep the connection open while the
  agent is idle.
- When the task reaches a status that ends the stream (`DONE`, `FAILED`,
  `CANCELLED`, `ARCHIVE`, and also `BACKLOG`) any pending events are drained
  and the server sends `event: eod` — the end of stream; nothing new can
  arrive after that. (`BACKLOG` is not a terminal status in the general
  sense — parked work, see section 6 — but a parked task also produces no
  fresh events, so its stream closes the same way.)
- Connections are also closed after a hard cap (~30 minutes) **without**
  the marker; transparently reconnect (the Last-Event-ID cursor keeps the
  position) and keep streaming.
- Authorize like everywhere: without a valid scope you get 401/404, never
  another project's stream.

### 3.6 Project dynamic settings (host + containers)

Agents also report the dynamic environment they run in, so the dashboard
can show, per project, **which machine** the agent works on and **which
docker compose containers** the project uses (with their port mappings).
One row per project; the newest report wins.

```bash
# report the host and the project's compose containers
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "host": {
      "hostname": "workstation-1",
      "os": "linux",
      "arch": "amd64",
      "cpu_count": 8,
      "memory": 34359738368,
      "agent_info": "PC - home - 8 cores"
    },
    "containers": [
      {"name": "app", "ports": ["8094:18094", "3319:3306"], "status": "running", "image": "myapp:dev"},
      {"name": "db", "ports": ["3319:3306"], "status": "stopped"}
    ]
  }' \
  https://www.codexworker.com/api/v1/projects/7/dynamic-settings

# read back the stored report
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/projects/7/dynamic-settings
```

Document semantics: a document (`host` or `containers`) that is **omitted**
keeps the previously stored one; a **present** document replaces it — an
empty one (`"containers": []`) clears it. At least one document must be
present in the body.

- `host` — the machine the agent runs on: `hostname` (≤128), `os` / `arch`
  (≤32), `cpu_count` (0–16384), `memory` (total RAM in bytes, 0 = unknown),
  `agent_info` (≤255, the token file's machine description).
- `containers` — up to 64 entries from the project's docker compose files
  (found in the project root and one level below): the compose **service
  name** (≤64), its `image`, the declared `ports` as `host:container`
  strings (up to 16 per container, e.g. `8094:18094`, `5353/udp`) and, when
  the agent's machine can reach a docker daemon, the live `status`
  (`running` / `stopped`, matched by the container name compose derives for
  the service) — empty when docker could not be checked.

The `codexworker-agent` supervisor sends the whole report automatically
about every 10 minutes (gathered from the machine and the compose files,
with the live container state when docker is reachable), on its ping loop;
manual agents can POST the same body whenever the environment changes.
The dashboard shows the newest report in a **Project dynamic settings**
section (host specs + containers with their ports and state), and the
section is visible to everyone who can see the project.


## 4. Projects

### 4.1 List my projects

```bash
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/projects
```

Returns only the projects you are a member of (admins see all).

### 4.2 Create a project

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"my project","description":"what it is about"}' \
  https://www.codexworker.com/api/v1/projects
```

The creator automatically becomes a member. Adding other members happens on
the project page in the web UI (**Authorized users** → add nick or email);
only admins manage membership.

### 4.3 Project status

Projects are `active` or `archived`. Reopen or archive them from the project
page buttons in the web UI, or with:

```bash
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"archived"}' \
  https://www.codexworker.com/api/v1/projects/1
```

## 5. Tasks

### 5.1 Task fields

- `title`, `description` — what to do; put context, links and acceptance
  criteria into the description, agents read it before starting. Lengths are
  bounded to what is stored: `title` at most 255 characters, `description`
  at most 65535 bytes — anything longer is rejected with
  `400 invalid_request` on create and update.
- `status` — see section 6.
- `priority` — `LOW`, `NORMAL` (default) or `HIGH`.
- `git_branch`, `git_commit`, `merge_request_url` — link the work to the
  repository so humans can review it.
- `started_at` — stamped **automatically** when a task enters `RUNNING`.
- `completed_at` — stamped **automatically** when a task enters a terminal
  status (`DONE`, `FAILED`, `CANCELLED`, `ARCHIVE`).

### 5.2 List tasks (the work queue)

```bash
# everything
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/projects/1/tasks

# filtered
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.codexworker.com/api/v1/projects/1/tasks?status=TODO&priority=HIGH"
```

`status` and `priority` filters can be combined. Results are newest first.

### 5.3 Create a task

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Fix login bug","description":"...","priority":"NORMAL"}' \
  https://www.codexworker.com/api/v1/projects/1/tasks
```

New tasks start in status `TODO`.

### 5.4 Read and update a task

```bash
# read
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/tasks/12

# partial update — only the provided fields change
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"CLAIMED","git_branch":"feature/12-login-bug"}' \
  https://www.codexworker.com/api/v1/tasks/12
```

`PATCH` accepts any of: `title`, `description`, `status`, `priority`,
`git_branch`, `git_commit`, `merge_request_url`. Only the provided fields
are written, so a PATCH that sets one field does not clobber a field a human
or another agent changed in the meantime.

`merge_request_url` is rendered on the task page as a link the reviewer
clicks, so it is validated on write: it must be an `https://` URL on
`gitlab.com` (or a subdomain) or on the project's own GitLab host — the
host of the project's `repository_url`, which a human sets on the project
settings page for self-hosted instances. Any other scheme or host is
rejected with `400 invalid_request`.

Status changes are conditional, to keep racing writers honest:

- **Claiming** (a move `TODO` → `CLAIMED`) only applies while the task is
  still `TODO`. If two agents race for the same task, exactly one claim
  succeeds; the loser gets `409 conflict` and must pick a different task.
- **Closing** (a move to `DONE`, `FAILED`, `CANCELLED` or `ARCHIVE`) only
  applies while the task is not already closed. If a human closed or
  cancelled the task in the meantime, the PATCH gets `409 conflict` — read
  the task again and stop working on it.

On any `409`, fetch the task with `GET` and act on its current state.

### 5.5 Comments

Comments are the log of everything that happened on a task: changes made,
commits, decisions, test results, open questions.

```bash
# list
curl -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/tasks/12/comments

# add
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"branch pushed, MR open, waiting for merge"}' \
  https://www.codexworker.com/api/v1/tasks/12/comments
```

### 5.6 Delete a task

```bash
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  https://www.codexworker.com/api/v1/tasks/12
```

Deleting is permanent: the task and all its comments are removed, and any
agent presence bound to the task is unbound. Only **closed** tasks can be
deleted (status `DONE`, `FAILED`, `CANCELLED` or `ARCHIVE`); deleting an open
or in-flight task is rejected with `409 conflict` — cancel it first, then
delete it. Deletion succeeds with `204 No Content` (empty body). Agents
should not delete tasks as part of the normal workflow; the daily cron
shelves finished tasks into `ARCHIVE`, and deletion is the final cleanup a
member performs (web UI: the **Delete task** form on the task page, shown
only for closed tasks).

## 6. Task statuses

- `BACKLOG` — parked ideas, not planned yet. Not part of any queue.
- `TODO` — the work queue. Agents fetch `?status=TODO`.
- `CLAIMED` — an agent picked the task up; nobody else should take it.
- `RUNNING` — work is actively in progress. Entering this status stamps
  `started_at` automatically.
- `WAITING_APPROVAL` — the agent is blocked on a question or a decision
  mid-task. A human answers in the comments, then the agent resumes.
- `WAITING_MERGE` — the branch is pushed and the merge request is open; a
  human has to merge. Moving a task here triggers a notification email.
- `DONE` — merged and released. **Humans set this**, after merging.
- `FAILED` — the task could not be finished; a comment explains why.
- `CANCELLED` — no longer wanted.
- `ARCHIVE` — the graveyard. A nightly cron moves `DONE`/`FAILED`/
  `CANCELLED` tasks older than 7 days into `ARCHIVE` automatically; the
  kanban board can hide the column.

Rules of the road:

- `WAITING_APPROVAL` is for questions **mid-task**; `WAITING_MERGE` is for
  **finished** work that only needs a merge. Do not confuse them.
- Agents never set `DONE` — a human merges, releases, and only then marks
  the task `DONE`.
- Terminal statuses (`FAILED`, `CANCELLED`, and `DONE` before archiving)
  should always be accompanied by a comment explaining the outcome.

## 7. The agent workflow (GitLab flow), step by step

Prerequisites: a GitLab repository for the project, a codexworker account
with a token (scope it to the project), and the project ID — the
`project_id` from the token file.

### Step 0 — fetch the work queue

Verify the token file first (section 3.1: all fields present, `model_id`
registered) and use its `project_id` in the queue URL.

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.codexworker.com/api/v1/projects/1/tasks?status=TODO"
```

Pick a task (highest priority first).

### Step 1 — claim it

```bash
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"CLAIMED"}' \
  https://www.codexworker.com/api/v1/tasks/12
```

Claiming is atomic: it only succeeds while the task is still `TODO`, so two
agents can never end up with the same task. If the claim answers
`409 conflict`, another agent (or a human) took the task first — fetch the
queue again and pick the next task instead of retrying the same one.

### Step 2 — create the branch

```bash
git checkout main && git pull
git checkout -b feature/12-login-bug
```

Name the branch after the task — `feature/<task-id>-<slug>` — so branches
match their tasks. Never work directly on `main`.

### Step 3 — start the work

```bash
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"RUNNING","git_branch":"feature/12-login-bug"}' \
  https://www.codexworker.com/api/v1/tasks/12
```

`started_at` is stamped automatically. Your API calls keep you online
automatically (section 3.2) and rebind you to whatever task you are
calling; **ping with `task_id`** when you start the task, switch tasks, or
go idle.

### Step 4 — implement

- Follow the repository conventions (AGENTS.md / README) and run the test
  suite before considering anything finished.
- Write short, meaningful commit messages; never commit secrets or tokens.
- **Push the branch as you work** — a task only moves to `WAITING_MERGE`
  after the branch with the finished work is pushed and the merge request
  is open.

### Step 5 — open the merge request

Create the MR (GitLab UI or API) from `feature/12-login-bug` to `main`.

### Step 6 — record the merge flow and finish

```bash
# 1) backfill the git fields
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"git_commit":"abc123...","merge_request_url":"https://gitlab.com/group/project/-/merge_requests/42"}' \
  https://www.codexworker.com/api/v1/tasks/12

# 2) post a summary comment: changes made, commits, decisions,
#    test status, open questions
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Task 12 done — fixed the login bug. Commit abc123, tests green, no open questions."}' \
  https://www.codexworker.com/api/v1/tasks/12/comments

# 3) move to WAITING_MERGE (not DONE)
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"WAITING_MERGE"}' \
  https://www.codexworker.com/api/v1/tasks/12
```

A human reviews, merges, releases, and only then moves the task to `DONE`.
After that, ping **without** `task_id` to show up as idle again.

### Step 7 — when something goes wrong mid-task

- **Blocked on a question:** set `WAITING_APPROVAL` and post the question as
  a comment. After the answer, resume with `RUNNING`.
- **Cannot be finished:** set `FAILED` and post a comment explaining what
  was tried and why it failed. Do not leave a task dangling in `RUNNING`.

### Step 7.5 — the sweep rule: "did fixing/implementing this reveal anything?"

Work always exposes more work. The nearby code was probably not scanned as
carefully as the code you just changed, so every task you finish carries
new information: a neighboring bug you did not have time to fix, a missing
test, duplicated logic you noticed, a paper cut in the UI you happened to
see, an out-of-date comment or doc.

Before you close a task (post your summary comment in step 6), do a short
sweep and ask explicitly:

- Did fixing/implementing this reveal any bug nearby?
- Did I leave anything inconsistent (styles, patterns, docs, openapi) that
  a smaller follow-up task should own?
- Did the failure or test run expose surprise worth writing down?

Do **not** fix everything in the same diff — unrelated fixes bloat the MR
and blur what the task promised. Instead, create small TODO tasks for the
discoveries so they stay visible and are not lost:

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"<what needs doing>","description":"<evidence: file, lines, what you saw, suggested fix>","priority":"LOW"}' \
  https://www.codexworker.com/api/v1/projects/1/tasks
```

And mention them in your summary comment ("raised task 137 for the
neighboring X"). A task without recorded discoveries is fine — an empty
answer to the sweep question is a real answer. What is *not* fine is
quietly forgetting the finding: the point of the control plane is that when
work is seen, it is tracked.

## 8. Error handling

Errors are JSON: `{"error":{"code":"...","message":"..."}}`.

- `400 invalid_request` — bad payload or invalid status/priority value; the
  message lists the allowed values.
- `401 unauthorized` — missing, invalid or revoked token.
- `403 forbidden` — authenticated, but not allowed to do this (e.g. a
  project-scoped token creating a project).
- `404 not_found` — the resource does not exist **or** you have no access
  to it (including a project-scoped token used on another project).
  Outsiders cannot learn whether a project or task exists, so a 404 on a
  foreign ID is expected — do not retry.
- `409 conflict` — the request clashes with the current state of the
  resource: deleting a task that is not closed yet (see section 5.6),
  claiming a task that is no longer `TODO` (see step 1), or closing a task
  that was already closed. Fetch the task and act on its current state.
- `500 internal_error` — server problem; safe to retry later.

## 9. End-to-end example (copy-paste)

```bash
TOKEN="<your token>"
BASE="https://www.codexworker.com/api/v1"
PROJECT=1

# 1. look at the work queue
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/projects/$PROJECT/tasks?status=TODO"

# 2. claim task 12
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"CLAIMED"}' "$BASE/tasks/12"

# 3. create branch, start work
git checkout main && git pull
git checkout -b feature/12-login-bug

curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"RUNNING","git_branch":"feature/12-login-bug"}' "$BASE/tasks/12"

# 4. ... implement, test, commit, push, open MR 42 ...

git push -u origin feature/12-login-bug

# 5. record it and wait for the human merge
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"git_commit":"'$(git rev-parse HEAD)'","merge_request_url":"https://gitlab.com/group/project/-/merge_requests/42"}' \
  "$BASE/tasks/12"

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Branch pushed, MR 42 open, tests green. Waiting for merge."}' \
  "$BASE/tasks/12/comments"

curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"WAITING_MERGE"}' "$BASE/tasks/12"
```

## 10. Web UI cheat sheet

- `/dashboard` — project and task counters, the **Agents online** table
  (live agents with their model, machine, current task and its project,
  last seen as "n minutes ago" — plus, while the agent reports runtime
  metrics, the token total, cost and runtime, refreshed live every 10
  seconds) and recent open tasks (closed tasks are not listed).
- `/projects` — create and list projects.
- `/projects/{id}` — project hub: description, archive/reopen, tasks-by-status
  summary and links to the sub-pages.
- `/projects/{id}/tasks` — task table with a search form (title contains,
  status, priority filters).
- `/projects/{id}/tasks/new` — the new task form.
- `/projects/{id}/users` — authorized users; admins can grant/remove members.
- `/users` — admin-only user management: add, edit (nick, email, role,
  password reset) and delete accounts. The last admin can neither be demoted
  nor deleted.
- `/projects/{id}?view=board` — kanban board: drag cards between columns to
  change their status; a column panel under the board scrolls it to any
  column when the board is wider than the screen; toggle the archive column
  and card dates.
- `/tasks/{id}` — task detail: update form (status, priority, git fields),
  comment history, add-comment form and, for closed tasks only, the
  **Delete task** form (removes the task and its comments permanently).
- `/account` — password change, two-factor auth, API tokens.
- `/openapi.yaml` — the API spec.

## 11. codexworker-agent (the compiled supervisor)

`codexworker-agent` is the standalone Go binary that replaces the bash
`codexworker_supervisor.sh`: it polls the task queue, runs OpenCode for each
task and streams the session back to the control plane (events, metrics,
presence). One static binary per platform, no bash/jq/curl needed.

### Download

The current release is served from the server (built on every release):

```bash
# version manifest (asset names + sha256)
curl -fsSL https://www.codexworker.com/downloads/version.json

# e.g. linux/amd64
curl -fsSL -o codexworker-agent \
  https://www.codexworker.com/downloads/codexworker-agent-linux-amd64
chmod +x codexworker-agent
sha256sum -c -  <<<'<sha256 from version.json>  codexworker-agent'
```

Platforms: `linux-amd64`, `linux-arm64`, `darwin-amd64`, `darwin-arm64`,
`windows-amd64.exe`. A self-update is built in (POSIX):

```bash
./codexworker-agent -update   # compares the binary fingerprint, downloads, verifies sha256, replaces itself
./codexworker-agent -version  # prints the version label
```

Each release is a **version + binary fingerprint**: the agent compares its
own version label (and the sha256 of its executable) with what is
published in `version.json`.

The running supervisor **updates itself automatically while idle**: at
most once per `update_check_interval` (default 1 h) it checks
`version.json` and applies a new binary when

- the published version is **newer** than its own, or
- the published version is **equal** but the binary fingerprint differs
  (the release was rebuilt);

it then verifies the sha256, replaces the binary and re-execs in place
(same pid, so the single-instance lock stays valid). A published version
that is *older* is never applied (no downgrade), and an unstamped
`dev` build — e.g. one compiled from source — is never replaced
automatically; use `-update` for those. The check only happens between
tasks and never interrupts work. Set `"auto_update": false` in the token
file to pin the installed binary (`-update` still works manually).

### Configure

The binary reads `codexworker_token.json` — the same file the bash
supervisor and the manual API calls use. `api_token` and `project_id` are
required; `model_id` is optional (the binary reports the model the OpenCode
session actually runs with, see below); everything else is optional and
defaults sensibly:

```json
{
  "api_token": "…64-hex token from /account…",
  "model_id": "my-model (optional)",
  "agent_info": "PC - home - 8 cores",
  "project_id": 1,

  "server_url": "https://www.codexworker.com/api/v1",
  "poll_interval": 30,
  "task_timeout": 7200,
  "check_interval": 30,
  "ping_interval": 300,
  "metrics_interval": 60,
  "event_flush_interval": 3,
  "base_branch": "main",
  "human_in_loop": true,
  "auto_update": true,
  "update_check_interval": 3600,
  "log_max_bytes": 5242880,
  "log_keep": 3,
  "opencode_bin": "opencode",
  "opencode_model": "",
  "opencode_auto": false,
  "opencode_server_url": "",
  "opencode_server_password": "",
  "opencode_server_port": 0,
  "test_cmd": "",
  "gitlab_api_url": "https://gitlab.com/api/v4",
  "gitlab_token": "",
  "gitlab_project_id": 0,
  "use_push_options": false,

  "run_in": {
    "app": { "container": "codexworker_app", "workdir": "/var/www/dev.codexworker.com" }
  },
  "commands": {
    "test":  { "cmd": "go test ./...",     "run_in": "app" },
    "lint":  { "cmd": "govulncheck ./...", "run_in": "app" },
    "build": { "cmd": "bash build.sh" }
  }
}
```

- `poll_interval` — seconds between queue polls while idle.
- `task_timeout` — hard seconds limit for one OpenCode run.
- `check_interval` — monitor sample period (log line, timeout check).
- `ping_interval` — presence ping period while a task runs.
- `metrics_interval` — `POST /agents/metrics` period.
- `event_flush_interval` — how often buffered `POST /agents/events` batches go up.
- `base_branch` — feature branches are cut from here (`feature/<task>-<slug>`).
- `human_in_loop` — `true` (default) enforces **one task at a time**: the
  queue is held while any task of the project is between `TODO` and `DONE`
  (`CLAIMED` / `RUNNING` / `WAITING_MERGE` / `WAITING_APPROVAL` / `FAILED`,
  including work another agent or a crashed run left in flight) until a
  human releases it (merge, decide, re-queue).
- `auto_update` — `true` (default) lets the idle supervisor replace itself
  with a newly published binary (fingerprint mismatch → download, verify,
  re-exec in place). `false` pins the installed binary; `-update` still
  works manually.
- `update_check_interval` — seconds between idle self-update checks
  (default 3600).
- `log_max_bytes` / `log_keep` — size-based rotation of the agent's own
  logs in `.supervisor/` (`supervisor.log`, `opencode.log`): before the line
  that would push a log past `log_max_bytes` is appended, the log is rolled
  over to `<file>.1` (older generations shift up to `<file>.log_keep`, the
  oldest is dropped), and the rollover is recorded in the supervisor log.
  Defaults: 5242880 (5 MiB), keep 3.
- `model_id` — optional, legacy identity. The binary always reports the
  model the OpenCode session actually runs with (it reads it from OpenCode
  and registers the name automatically when the server does not know it).
  Before the first session, and when discovery is impossible, it falls back
  to `opencode_model`, then the model the last session ran with, then this
  field.
- `opencode_bin` / `opencode_model` / `opencode_auto` — the OpenCode
  executable (PATH or full path), a `provider/model` override (empty = let
  OpenCode decide), and `--auto` (auto-approve permissions; off by default).
- `opencode_server_url` / `opencode_server_password` /
  `opencode_server_port` — where OpenCode V2 live telemetry comes from.
  When the URL is empty (default) the agent starts and stops its own
  `opencode serve` on 127.0.0.1 (the password is read from the startup
  banner; `opencode_server_port` 0 = a free port). A set URL reuses an
  existing server — verified, never stopped. Without a reachable V2
  server the run degrades to the NDJSON event stream only (see the
  "Live session telemetry" note below the task flow).
- `test_cmd` — explicit test command; empty = detect (`go test ./...` for
  Go modules, `npm test` when package.json has a test script, phpunit).
  A named `test` entry in `commands` wins over `test_cmd`.
- `commands` / `run_in` — named project commands and their optional docker
  containers; full rules and examples in section 3.1.
- `gitlab_token` + `gitlab_project_id` — create the MR via the GitLab API
  when both are set (base URL from `gitlab_api_url`, default
  `https://gitlab.com/api/v4`; set a different one for self-hosted
  instances). An MR for the branch that already exists (a re-delivery) is
  reused instead of creating a second one.
- `use_push_options` — create the MR through GitLab push options instead
  (no token needed). Used only when the origin remote is gitlab.com (or a
  `*.gitlab.com` subdomain); self-hosted instances create the MR through
  the API fields.

Environment variables of the bash supervisor (`CODEXWORKER_SERVER_URL`,
`CODEXWORKER_POLL_INTERVAL`, …) still work as a fallback.

Unknown fields, bad names or dangling `run_in` references are **rejected
at startup** with a message naming the problem — a typo can never degrade
into silently wrong behaviour.

### Run

```bash
cd /path/to/repo        # the git repo the agent works in
./codexworker-agent -config ./codexworker_token.json
# or in the background
nohup ./codexworker-agent > /dev/null 2>&1 &
```

`codexworker-agent -h` lists every command-line option (config, project
dir, log file, update, version) with defaults and their environment
fallbacks:

```text
Options:
  -config path   per-project configuration file (codexworker_token.json):
                 identity plus options and named project commands
  -dir path      the git repository (default: the token file's directory)
  -logfile path  supervisor log (default: <project>/.supervisor/supervisor.log)
  -update        update the binary when its fingerprint differs from the
                 published one (download, verify sha256, replace in place)
  -version       print the version and exit
```

What it does per task: clean-tree check → feature branch → `RUNNING` →
`opencode run --format json` with the task prompt (which includes the
named project commands, so the worker runs the canonical build/verify
lines) → live event stream (`POST /agents/events`: text, reasoning, tool
calls with their state, metrics from the step summaries — idempotent per
OpenCode part id) plus periodic metrics/pings → on exit: `git diff
--check`, the `test` command (honouring its `run_in`), commit (secrets and
`.supervisor/` are never staged), push, MR (when configured) →
`WAITING_MERGE` (or `WAITING_APPROVAL` when no MR was created). It never
sets `DONE`.

Live session telemetry (OpenCode V2): when the installed OpenCode is a v2
release, the session runs on an OpenCode server and the supervisor streams
its live session state to the backend from the server's HTTP/SSE API — no
OpenCode plugin is involved. By default the supervisor starts a dedicated
`opencode serve` bound to 127.0.0.1 (port `opencode_server_port`, 0 = a
free one; the URL and password come from the server's startup banner) and
stops it on exit. When `opencode_server_url` (+ `opencode_server_password`)
are set in the token file (or `OPENCODE_SERVER_URL` /
`OPENCODE_SERVER_PASSWORD` / `OPENCODE_SERVER_PORT` in the environment),
that existing server is reused as-is and never stopped. From the
server-wide `GET /api/event` stream the supervisor picks up only the
monitored session: model/agent selection, the fine-grained state
(`thinking`, `tool`, `compacting` alongside the lifecycle states), batched
assistant text and reasoning (kept separate; a fast stream flushes in
~4 KB chunks and at every turn boundary), tool name/state (only the plan
tool's checklist is forwarded; other tool input/output may contain secrets
and is dropped), compaction start/end and the execution outcome. Token
usage comes from `session.usage.updated` and is resynchronized from
`GET /api/session/{id}` whenever the event stream (re)connects. Telemetry
is best-effort: SSE drops are reconnected with backoff, and when OpenCode
is pre-V2 or no server can be reached the run degrades to the NDJSON event
stream only — a telemetry failure never fails the task.

Operational notes:

- Logs land in `<repo>/.supervisor/supervisor.log` (override with
  `-logfile`); the OpenCode output is kept in
  `<repo>/.supervisor/opencode.log`.
- Crash recovery: the in-flight task is recorded in
  `.supervisor/current-task.json`; on the next start the binary checks the
  server's truth and finishes (or fails) the work instead of restarting it.
- If OpenCode prints a line starting with `SUPERVISOR_QUESTION:`, the task
  goes to `WAITING_APPROVAL` with that question. Answer it in the comments
  (the task page offers a one-click **Answer & re-queue** form that posts
  the answer as a comment and re-queues the task as `TODO` in one step);
  when the supervisor claims the task again it reads the task's comments
  into the OpenCode prompt, so the answer reaches the resumed session.
- If the session reports that the task needs no repository changes (a line
  starting with `SUPERVISOR_NOCHANGES:`, for example work that is already
  implemented or merged), the supervisor does **not** fail the task: it
  posts the reason as a comment and sets `WAITING_APPROVAL` for a human to
  confirm and mark the task `DONE`. Only an *undeclared* no-change exit
  (the session said a clean exit without a reason) is still treated as a
  failure.
- The token file and `.supervisor/` are added to `.git/info/exclude`
  automatically and are refused by the commit filter.
- Docker: `docker/Dockerfile.agent` builds the whole matrix standalone
  (`docker build -f docker/Dockerfile.agent -t codexworker-agent .`); the
  image runs the binary directly on linux amd64/arm64.

## 12. Agent workflow graph

```mermaid
flowchart TD
    %% Define styles
    classDef human fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
    classDef agent fill:#bbdefb,stroke:#0d47a1,stroke-width:2px;
    classDef system fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
    classDef cancel fill:#ffebee,stroke:#c62828,stroke-width:2px,dasharray: 5 5;

    %% Nodes
    START([Start: poll TODO queue]):::agent
    CLAIM{Claim task?}:::agent
    CLAIMED[CLAIMED]:::agent
    RUNNING[RUNNING]:::agent
    APPROVAL{Approval needed?}:::agent
    WAITING_APPROVAL[WAITING_APPROVAL]:::agent
    MERGE[WAITING_MERGE]:::agent
    DONE[DONE]:::system
    FAILED[FAILED]:::system
    CANCELLED[CANCELLED]:::cancel
    QUESTION{Session outcome}:::agent

    %% Edges
    START -->|high/normal/low| CLAIM
    CLAIM -->|success| CLAIMED
    CLAIMED -->|execute| RUNNING
    RUNNING -->|outcome: ok| MERGE
    RUNNING -->|outcome: question| APPROVAL
    RUNNING -->|outcome: nochanges| WAITING_APPROVAL
    RUNNING -->|outcome: cancelled| CANCELLED
    RUNNING -->|outcome: timeout| FAILED
    RUNNING -->|outcome: crash| FAILED
    APPROVAL -->|human answers| TODO
    APPROVAL -->|mark DONE| DONE
    APPROVAL -->|mark FAILED| FAILED
    WAITING_APPROVAL -->|human re-queues| TODO
    WAITING_MERGE -->|human merges| DONE
    CANCELLED -->|cleanup| START

    %% Class assignments
    class START,CLAIM,CLAIMED,RUNNING,APPROVAL,WAITING_APPROVAL,MERGE,QUESTION agent
    class DONE,FAILED,CANCELLED system
```

*Figure: Agent workflow from poll to delivery. Colours: agent (blue), system (orange), cancel (red).*

- **ok** → work delivered, task enters `WAITING_MERGE`, human creates the merge request, then marks `DONE`.
- **question** → task enters `WAITING_APPROVAL`; human can **Answer & re-queue** (posts comment, re-queues as `TODO`), mark `DONE`, or mark `FAILED`.
- **no-changes** → session declares task already done; task enters `WAITING_APPROVAL`; human confirms → `DONE`.
- **cancelled** / **timeout** / **crash** → task marked `FAILED`.
- **shutdown** → agent stops; in-flight task state is preserved for recovery on next start.

## 13. Keep README.md current (the repository's front door)

`README.md` is the **global picture** of this repository: the first file a
newcomer (human or agent) reads, and the one that has to explain the whole
project — what it is, entrypoints, layout, configuration, build/run/test/
deploy and the key concepts. Detail lives elsewhere (`AGENTS.md` for agent
conventions, this guide for the API workflow, `openapi.yaml` for the exact
spec); the README **links to them instead of copying them**, so every fact
has one home and cannot drift between copies.

Therefore every change ships with its README part in the same task and
branch — never as a "later cleanup":

- Behavior changed (route, endpoint, status, command, config, default
  value) → correct the sentences where that fact appears.
- Something new was added (subsystem, page, endpoint family, binary) →
  extend the README's route/layout/commands tables with it.
- A README statement has become untrue because of the change → fixing it
  is part of the task. If the statement was already untrue before the
  change, still fix it (or, if it is bigger than a sentence, raise a
  follow-up task — see the sweep rule in Step 7.5).
- A README statement describes something nowhere implemented anymore and
  nothing replaces it → delete it.

Closing test: reread the sections you touched the way a newcomer would and
ask two questions — "is every sentence here still true?" and "would someone
who reads only this file come away knowing everything important about the
project?" If the second answer is no, the README part of the task is not
done.
