# AI Kanban — agents.md

You are an agent operating an AI Kanban board through its **Agent API**.
Read this file completely before acting.

## 1. Get a token

The board owner generates a token for a bound member in
**Board settings → Members → 🔑**, or the member copies it from their own
**Agent access** dialog. A token is scoped to **one member on one board**
(least privilege). Send it on every request:

```
Authorization: Bearer <token>
```

## 2. Base URL

```
/api/team/{uid}/boards/{board_id}
```

Replace `{uid}` (owner id) and `{board_id}` with the values you were given.

## 3. Endpoints

### 3.1 Board Metadata Fetch
```http
GET /api/team/{uid}/boards/{board_id}
Authorization: Bearer <token>
```
Returns `{ board, member }`.
- `board`: Contains board structure and configurations: `id`, `title`, `description`, `columns`, `statuses`, `tags`, `agent`, `flow`, `intake`, `ai_prompt`, `review_target_column_id`.
  - `statuses`: `[{ id, name, color?, is_default?, is_start?, is_terminal? }]`. Cards bind statuses by the stable `id`.
  - `tags`: `[{ id, name, group?, color?, allow_multiple? }]`. Cards bind tags by the stable `id`.
  *(Note: Cards/tasks are separated into the task fetch endpoint below).*
- `member`: The caller's bound member profile and granted permissions:
  `{ id, name, title, role_id, role_name, permissions: [...] }`.

### 3.2 Task (Card) Fetch
```http
GET /api/team/{uid}/boards/{board_id}/tasks
Authorization: Bearer <token>
```
*(Also available via alias `GET /api/team/{uid}/boards/{board_id}/cards`)*

Returns `{ tasks, total, fetch_column_id, start_column_id, finish_column_id, member }`. Each task contains `id, title, description, column_id, status, tags, subtasks, assignee, ...`.
- Only tasks **assigned to the calling member** are returned.
- `start_column_id` / `finish_column_id`: move tasks here to start / finish (may be `null`).
- `status`: Holds the status `id` (e.g. `"todo"`, `"doing"`, `"st_3"`).
- `tags`: Array of tag `id` strings (e.g. `["tag_ab12", "tag_cd34"]`).

- **Automatic Fetch Column Filtering**:
  - If the board owner has configured an Agent Fetch column (`board.agent.fetch_column_id`), this endpoint **automatically returns only tasks belonging to that column**.
  - If `fetch_column_id` is not configured, it returns all active tasks assigned to you across the board.
- **Query Parameters**:
  - `?column_id=<id>`: Fetch tasks from a specific column ID.
  - `?all=true`: Ignore `fetch_column_id` and consider tasks across the entire board (still only yours).
  - `?status=<status_id>`: Filter tasks matching a specific status id.

### 3.3 Single Task Detail
```http
GET /api/team/{uid}/boards/{board_id}/tasks/{task_id}
Authorization: Bearer <token>
```
*(Also available via `GET /api/team/{uid}/boards/{board_id}/cards/{card_id}`)*

Returns `{ task }` with full card details and checklists.

### 3.4 List Releases
```http
GET /api/team/{uid}/boards/{board_id}/releases
```

### 3.5 Release Detail
```http
GET /api/team/{uid}/boards/{board_id}/releases/{release_id}
```

### 3.6 Change Stream (SSE)
```http
GET /api/team/{uid}/boards/{board_id}/events
```
Server-Sent Events. Emits:
- `change` — whenever anyone mutates the board (use it to stay in sync instead of polling).
- `subscription` — the owner's current subscription state, sent once on connect
  and again whenever it changes (e.g. the owner cancels/renews). Shape:
  ```json
  { "subscription": { "plan_key": "free", "plan_status": "canceled",
                      "expires_at": 1792548217, "expired": false,
                      "limits": { "boards": 1, ... } } }
  ```
- `ready` / `ping` — connection lifecycle / keepalive.

### 3.7 Mutations
```http
POST /api/team/{uid}/boards/{board_id}/mutate
Authorization: Bearer <token>
Content-Type: application/json

{ "action": "<action>", ... }
```

## 4. Mutation Actions

Every mutation requires the matching permission on the token's role.

| action | payload | permission |
|---|---|---|
| `card.create` | `column_id, title, description?, tags?, status?, position?` | card.create |
| `card.update` | `card_id, updates:{ title?, description?, status?, tags?, subtasks?, assignee?, estimated_minutes?, column_id? }` | card.edit |
| `card.move` | `card_id, column_id, position?` | card.move |
| `card.delete` | `card_id` | card.delete |
| `column.create` | `title, color?, status_on_move?, position?` | column.create |
| `column.update` | `column_id, title?, color?, status_on_move?` | column.edit |
| `column.delete` | `column_id` | column.delete |
| `intake.accept` | `card_id, column_id?` | intake.edit |
| `intake.reject` | `card_id` | intake.edit |
| `intake.restore` | `card_id` | intake.edit |
| `release.create` | `name, note_md?, card_ids[]?, estimate_unit?` | release.create |
| `release.revoke` | `release_id` | release.revoke |

*(Direct REST endpoints `POST /tasks`, `PATCH /tasks/{id}`, `DELETE /tasks/{id}` are also supported).*

> **IMPORTANT: Status and Tags Value Conventions**:
> - **`status`**: Always pass the status **`id`** from `board.statuses` (e.g. `"doing"`, `"st_3"`). A card's status **may be empty (`null`)** — pass `"status": null` in `card.update` to clear it (no status). Moving a card into a column with `status_on_move` applies that status automatically (it wins over an explicit clear).
> - **`tags`**: Always pass an array of tag **`id`** strings from `board.tags` (e.g. `["tag_ab12"]`).
> - **`status_on_move`**: In `column.create` / `column.update`, pass the status **`id`**.

## 5. Agent Flow & Examples

### REQUIRED workflow (do these in order, per task)

For **every task you pick up you MUST move it to the start column BEFORE
starting work** — this is the whole point of the start column, so the owner can
see "in progress" in real time. Do not batch the moves: move-to-start when you
begin, move-to-finish when you are done.

```
fetch tasks  ->  MOVE to start_column_id (immediately)  ->  do the work  ->  MOVE to finish_column_id
```

Step-by-step:

1. **Read board config** (once)
   ```bash
   curl -X GET "$BASE" -H "Authorization: Bearer $TOKEN"
   ```
   Read `board.agent` = `{ fetch_column_id, start_column_id, finish_column_id }`
   (also mirrored at `board.flow`, and returned by `GET $BASE/tasks` as
   `start_column_id` / `finish_column_id`). These are the routing targets.

2. **Fetch your tasks**
   ```bash
   curl -X GET "$BASE/tasks" -H "Authorization: Bearer $TOKEN"
   ```
   Returns tasks assigned to you plus `fetch_column_id`, `start_column_id`,
   `finish_column_id`.

3. **START — move to the start column FIRST (mandatory)**
   Before doing any work on a task, if `start_column_id` is set, move it there
   **immediately**:
   ```bash
   curl -X POST "$BASE/mutate" -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"action":"card.move","card_id":"<card_id>","column_id":"<start_column_id>"}'
   ```
   - The destination column's `status_on_move` is applied automatically (usually
     "in progress") and `started_at` is recorded.
   - If `start_column_id` is `null`/unset, skip this move (do NOT error).
   - Only after this move should you begin implementing.

4. **FINISH — move to the finish column when done**
   - When the work is complete, **do not mark the task terminal done directly**.
   - Move it to `finish_column_id` (fallback `board.review_target_column_id`):
   ```bash
   curl -X POST "$BASE/mutate" -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"action":"card.move","card_id":"<card_id>","column_id":"<finish_column_id>"}'
   ```
   - The destination column's `status_on_move` (usually the review status) is
     applied automatically and `ended_at` is recorded.

> Anti-pattern (do NOT do this): fetching a task, working on it silently, then
> calling `card.move` only once at the end. The start move must happen up front.

## 6. Data Models

### Statuses (`board.statuses`)
- Defined as `[{ id, name, color?, is_default?, is_start?, is_terminal? }]`.
- `id`: Stable system identifier (e.g. `"todo"`, `"doing"`, `"done"`). Returned for reference.
- `name`: Literal user-facing status name (display only).
- **CRITICAL**: `card.status` stores the status **`id`** (e.g. `"doing"`).
- `is_default`: Initial status for new cards.
- `is_start`: The "start" state; entering it records the task start time (optional, single).
- `is_terminal`: Completed state (ready for release).

### Columns (`board.columns`)
- Defined as `[{ id, title, color?, status_on_move?, position }]`.
- `status_on_move`: Status **`id`** automatically applied when a card moves into this column.

### Tags (`board.tags`)
- Defined as `[{ id, name, group?, color?, allow_multiple? }]`.
- `id`: Stable system identifier. Returned for reference.
- `name`: Literal tag display name (e.g. `"bug"`, `"frontend"`).
- **CRITICAL**: `card.tags` stores an array of tag **`id`** strings (`["tag_ab12"]`).

## 7. Rules

- Always read `GET /api/team/{uid}/boards/{board_id}` to understand columns, statuses, tags, and agent routing.
- Use `GET /api/team/{uid}/boards/{board_id}/tasks` to fetch tasks.
- **Card `status` ALWAYS uses the status `id` (e.g. `"doing"`), NEVER the display name.**
- **Card `tags` ALWAYS uses tag `id` strings (e.g. `["tag_ab12"]`), NEVER tag names.**
- Deliver finished tasks to `board.agent.finish_column_id`.
- Only perform actions permitted by your role.
- If a mutation returns `403 SUBSCRIPTION_RESTRICTED`, the owner is over their
  plan's board limit. Stop writing and report it — writes resume only after the
  owner resubscribes or removes extra boards.

## 8. Errors

| status | body | meaning |
|---|---|---|
| 401 | `{"error":"Unauthorized"}` | missing/invalid token |
| 403 | `{"error":"NOT_A_MEMBER"}` | token not valid for this board |
| 403 | `{"error":"FORBIDDEN","permission":"..."}` | role lacks permission |
| 404 | `{"error":"Card not found"}` / `{"error":"Task not found"}` | unknown entity |
| 409 | `{"error":"CONFLICT","card":{...}}` | optimistic-lock mismatch |
| 403 | `{"error":"SUBSCRIPTION_RESTRICTED","reason":"BOARD_LIMIT_EXCEEDED",...}` | owner is over their plan's board limit; ALL writes blocked until they resubscribe or delete boards |
| 429 | `{"error":"LIMIT_REACHED",...}` | plan limit reached |
