# Kade v0.2 — Tasks API Contract

**From:** Forge
**To:** Riv
**Date:** 2026-05-24
**Status:** SCHEMA APPLIED — endpoints landing today. You can start bot handlers against this contract now.

---

## TL;DR

- Atlas migration `009_kade_tasks` is applied: `kade_tasks` (queue) + `kade_tasks_log` (audit/inbound DM events).
- Endpoints live at `localhost:3000/api/kade/tasks/*` — same bearer-auth pattern as `/api/kade/meds/*` (header `Authorization: Bearer $ATLAS_INGEST_TOKEN`).
- Response shape mirrors meds: `{ ok: boolean, ... }` on success, `{ ok: false, error, field?, trace_id? }` on failure. All bigint ids serialized as JS numbers.
- The bucket discipline ("Top 3 / Today max 8 / List From Hell") is **enforced server-side** — if you POST a task into `today` when 8 are already there, you get HTTP 409 with a clear `error` field.
- **Vague tasks** (where Kade can't tell what Jimmie meant) carry `clarity = "vague"` and are excluded from today/top3 rollups until clarified.

---

## Schema (FYI — you query through the API, not the DB)

### `kade_tasks`

| column | type | notes |
|---|---|---|
| `id` | bigint PK | |
| `title` | text NOT NULL | short label, what the bot says back |
| `details` | text NULL | longer body, populated after clarification |
| `status` | enum text | `open` (default) \| `done` \| `snoozed` \| `cancelled` |
| `bucket` | enum text | `top3` (cap 3) \| `today` (cap 8) \| `list` (default, uncapped) |
| `clarity` | enum text | `clear` (default) \| `vague` |
| `source` | enum text | `slack-text` (default) \| `slack-voice` \| `slack-slash` \| `web` \| `email` \| `agent` \| `manual` \| `kade-inferred` \| `qordinate-import` |
| `source_agent` | text NULL | when `source='agent'`, e.g. `'echo'`, `'loom'` |
| `external_id` | text NULL | Slack msg ts, voice memo id, etc.; UNIQUE (source, external_id) |
| `original_input` | text NULL | raw user input — preserves "all I've got saved is…" for fuzzy recall |
| `project_id` | bigint NULL → projects(id) | nullable; ON DELETE SET NULL |
| `owner` | text NOT NULL | default `'jimmie'` |
| `due_at` | timestamptz NULL | |
| `bucket_set_at` | timestamptz NULL | when this task entered top3/today (for midnight demotion scheduler later) |
| `snooze_until` | timestamptz NULL | reserved for v0.3 |
| `notes` | text NULL | ongoing context; ledger-style |
| `sass_level` | smallint NOT NULL | reserved for v0.3 nudge tone (0..3) |
| `calendar_event_id` | text NULL | reserved for v0.4 GCal sync |
| `completed_at` | timestamptz NULL | set when status flips to `done` |
| `created_at`, `updated_at` | timestamptz NOT NULL | auto-managed |

### `kade_tasks_log`

| column | type | notes |
|---|---|---|
| `id` | bigint PK | |
| `task_id` | bigint NULL → kade_tasks(id) | ON DELETE CASCADE |
| `event_type` | enum text | `created` \| `clarified` \| `rebucketed` \| `snoozed` \| `completed` \| `cancelled` \| `nudge_sent` \| `reply_received` \| `due_changed` \| `noted` |
| `payload` | jsonb NOT NULL | event-specific data |
| `actor` | text NULL | `'kade'`, `'jimmie'`, `'echo'`, etc. |
| `channel` | text NOT NULL | default `'slack'` |
| `external_id` | text NULL | dedup key per channel; UNIQUE (channel, external_id) |
| `occurred_at` | timestamptz NOT NULL | default `now()` |

---

## Endpoints

All endpoints require `Authorization: Bearer $ATLAS_INGEST_TOKEN`. Bodies are JSON. Errors are `{ ok: false, error, field?, trace_id? }`. Bigint ids are JS numbers in responses.

### `POST /api/kade/tasks/create`

Create a new task. Idempotent on `(source, external_id)` — if the same Slack msg fires twice, you get the existing row back with `deduped: true` and HTTP 200.

**Request**
```json
{
  "title": "call the accountant about Q1",         // required, non-empty
  "details": "...",                                 // optional
  "bucket": "list",                                 // optional, default "list"; one of top3|today|list
  "clarity": "clear",                               // optional, default "clear"; one of clear|vague
  "source": "slack-text",                           // optional, default "slack-text"; see enum above
  "source_agent": null,                             // optional; required only when source="agent"
  "external_id": "T01234.1715098765.000200",        // strongly recommended for dedup
  "original_input": "remind me to call the acct",   // optional, raw user input
  "project_id": 17,                                 // optional; must exist in projects table
  "due_at": "2026-05-26T17:00:00-05:00",            // optional ISO 8601 with offset
  "notes": "..."                                    // optional
}
```

**Response 201 Created** (new) **/ 200 OK** (deduped)
```json
{
  "ok": true,
  "task_id": 42,
  "bucket": "list",
  "clarity": "clear",
  "deduped": false,
  "created_at": "2026-05-24T14:32:10-05:00"
}
```

**Response 409 Conflict** — bucket cap hit
```json
{ "ok": false, "error": "bucket 'today' is full (max 8 open tasks)", "field": "bucket" }
```

**Response 400** — missing/invalid field, e.g. `{ "ok": false, "error": "title is required", "field": "title" }`

### `POST /api/kade/tasks/update`

Update any subset of mutable fields on an existing task. Same call handles rebucket, reprioritize, complete, snooze, clarify, due-date change.

**Request**
```json
{
  "task_id": 42,                                    // required
  "status": "done",                                 // optional; if "done", completed_at is auto-set
  "bucket": "today",                                // optional; cap-enforced
  "clarity": "clear",                               // optional; transitioning vague→clear logs a 'clarified' event
  "title": "...",                                   // optional
  "details": "...",                                 // optional
  "due_at": "2026-05-25T17:00:00-05:00",            // optional; null clears the due date
  "snooze_until": "2026-05-26T09:00:00-05:00",      // optional
  "project_id": 17,                                 // optional; null unlinks
  "notes": "...",                                   // optional
  "sass_level": 1,                                  // optional, 0..3
  "actor": "kade"                                   // optional, for the audit log; defaults to "kade"
}
```

**Response 200**
```json
{
  "ok": true,
  "task_id": 42,
  "status": "done",
  "bucket": "today",
  "clarity": "clear",
  "events_logged": ["completed", "rebucketed"]
}
```

**Response 404** — task not found
**Response 409** — bucket cap hit
**Response 400** — invalid field value

### `GET /api/kade/tasks/list`

List tasks with filters. Default: open tasks, ordered by due_at (nulls last) then created_at.

**Query params** (all optional)
- `status` — `open` (default) \| `done` \| `snoozed` \| `cancelled` \| `all`
- `bucket` — `top3` \| `today` \| `list` \| `all`
- `clarity` — `clear` \| `vague` \| `all`
- `project_id` — bigint
- `due_before` — ISO 8601 (used for scheduler scans)
- `limit` — int, 1..200, default 50

**Response 200**
```json
{
  "ok": true,
  "count": 3,
  "tasks": [
    {
      "id": 42,
      "title": "call the accountant about Q1",
      "details": null,
      "status": "open",
      "bucket": "today",
      "clarity": "clear",
      "source": "slack-text",
      "source_agent": null,
      "external_id": "T01234.1715098765.000200",
      "original_input": "remind me to call the acct",
      "project_id": 17,
      "project_name": "Q1 Tax Filing",
      "owner": "jimmie",
      "due_at": "2026-05-26T17:00:00-05:00",
      "bucket_set_at": "2026-05-24T14:32:10-05:00",
      "snooze_until": null,
      "notes": null,
      "sass_level": 0,
      "completed_at": null,
      "created_at": "2026-05-24T14:32:10-05:00",
      "updated_at": "2026-05-24T14:32:10-05:00"
    }
  ]
}
```

### `GET /api/kade/tasks/today`

The bot's most-called endpoint: today's rollup. Excludes `clarity='vague'`. Includes overdue spillover. Returns three lanes.

**Query params** (optional)
- `tz` — default `America/Chicago`

**Response 200**
```json
{
  "ok": true,
  "date": "2026-05-24",
  "tz": "America/Chicago",
  "top3": [ /* up to 3 task objects */ ],
  "today": [ /* up to 8 task objects */ ],
  "overdue": [ /* any open task with due_at < today's start, bucket = list, not vague */ ],
  "counts": {
    "top3": 3, "today": 6, "overdue": 2,
    "list_total": 24,        // List From Hell open + clear
    "vague_pending": 1        // tasks awaiting clarification
  }
}
```

### `GET /api/kade/tasks/vague`

The clarification inbox — `status='open' AND clarity='vague'` ordered by created_at asc.

**Response 200**
```json
{
  "ok": true,
  "count": 1,
  "tasks": [ /* task objects with clarity=vague */ ]
}
```

### `POST /api/kade/tasks/bulk-complete`

Mark multiple tasks done in one call (for Jimmie's "X, Y, Z done" Slack replies).

**Request**
```json
{
  "task_ids": [42, 43, 44],
  "actor": "jimmie"
}
```

**Response 200**
```json
{
  "ok": true,
  "completed": [42, 43],
  "not_found": [44],
  "already_done": []
}
```

### `POST /api/kade/tasks/log-event`

Riv-side audit logging — write directly to `kade_tasks_log` for inbound DM events the bot wants to record (nudge sent, reply received, etc.) without mutating the task itself.

**Request**
```json
{
  "task_id": 42,                                    // nullable for orphan events
  "event_type": "nudge_sent",                       // required; see enum
  "payload": { "channel_id": "C123", "ts": "..." }, // optional, defaults to {}
  "actor": "kade",
  "channel": "slack",                               // default "slack"
  "external_id": "C123.1715098765.000200"           // optional; (channel, external_id) is unique
}
```

**Response 201 / 200 (deduped)**
```json
{ "ok": true, "log_id": 99, "deduped": false }
```

---

## Behavior Notes

### Bucket caps (server-enforced)
- `top3` — at most 3 `open` tasks at a time
- `today` — at most 8 `open` tasks
- `list` — uncapped
- Hitting a cap returns 409; the bot should surface this and let Jimmie demote something or override (override mechanic is v0.3, not v0.2).
- `done`/`cancelled`/`snoozed` tasks don't count toward caps.

### Vague → clear transition
- Sending an `update` with `clarity: "clear"` (from `vague`) automatically writes a `clarified` audit event.
- Optionally include the clarification text in `details` in the same call.
- Vague tasks always live in `bucket='list'`. Trying to set `bucket='today'` while `clarity='vague'` returns 400.

### Status flips
- `status='done'` auto-sets `completed_at = now()` and logs a `completed` event.
- `status='cancelled'` logs a `cancelled` event; `completed_at` stays null.
- Re-opening a completed task: send `status='open'`; `completed_at` clears.

### Idempotency
- `(source, external_id)` is the dedup key on tasks. Always pass `external_id` on Slack-sourced creates.
- `(channel, external_id)` is the dedup key on the log. Use this for retryable bot ops.

### Timezone
- All responses serialize timestamps as ISO 8601 with America/Chicago offset (`...-05:00` or `...-06:00` depending on DST), matching the meds endpoints.
- The bot should parse them as offset-aware datetimes (Python: `datetime.fromisoformat` works on these).

---

## Answers from Jimmie (2026-05-24)

1. **Snooze in v0.2 — YES.** Wire `status='snoozed' + snooze_until=...` for "ok later" Slack replies. Don't wait for v0.3 DND.
2. **Default bucket — `list`.** Never auto-promote. Promotion to `today`/`top3` is Jimmie's explicit act.
3. **Surface — conversational, not slash commands.** Jimmie wants to talk to Kade the way he talks to the rest of the PKA team (Larry/Forge/Echo/etc.) — natural-language DMs, free-form intent. No `/kade add` / `/kade today` syntax.

### What "conversational" means concretely for your build

The bot has to interpret intent from free-text DMs and route to the right tasks API endpoint. Examples (illustrative, not exhaustive):

| Jimmie says | Bot should |
|---|---|
| "remind me to call the accountant tomorrow at 10" | `create` with title, due_at parsed, bucket=list |
| "what's on top 3" / "show me today" | `list` or `today` rollup, render as Slack message |
| "the accountant call is done" | fuzzy-match against open tasks → `update status=done` (or `bulk-complete` if multiple) |
| "ok later" / "snooze the dashboard one an hour" | `update snooze_until=now+1h, status=snoozed` |
| "promote the accountant call to today" | `update bucket=today` (handles 409 with a sensible reply) |
| "that's too vague, dunno what i meant" | `update clarity=vague` |
| "the vague one — i meant review the prospect dashboard" | `update clarity=clear, title=..., bucket=list` |
| "what's on top 3 for the Q1 filing project" | `list?project_id=...` after fuzzy-matching project name |

This is bigger than the slash-command path. You'll likely want:
- Claude API in the loop (one call per DM) with the open-task list + active projects passed as context so it can disambiguate references like "the accountant one"
- Tool definitions mapping to my 7 endpoints — Claude decides which to call
- A short system prompt giving Kade his voice (sass + Stoic, per `Team/Kade/profile.md`) and the rules (default bucket=list, no auto-promote)
- Fuzzy matching for task references — could be LLM-side (let Claude pick by id from the list it sees) rather than a separate fuzzy lib

The bearer-auth REST API I shipped is already shaped right for tool-calling — each endpoint is one tool. You shouldn't need a wrapper layer.

### Open question I now have for *you*

The conversational interface puts a Claude API cost on every Jimmie DM. Probably negligible (a few cents/day at his volume) but worth confirming the budget before you wire it. If you want to discuss the agent loop design, ping me or pair with Pax — I can sketch the tool-call schema if useful.

---

## Ready Signal

✅ Migration applied to local Postgres
✅ Schema locked
⏳ Endpoints landing today (next commit)
⏳ PM2 reload + smoke test
⏳ `/kade` web UI

You can start the bot handler stubs now against this spec. I'll ping you again when the endpoints respond 200.

— Forge
