```bash
# Delega Documentation — Historical/As Built
```

Hosted access retired: July 28, 2026

> This is a historical technical record of the production system. Public signup, agent onboarding, billing, and general API access are closed. The private deployment remains in active personal use by Ryan McMillan. Existing owner credentials are the only supported way to use `api.delega.dev`.

Canonical case study: [ryanmcmillan.com/delega](https://ryanmcmillan.com/delega)

Architecture and threat model: [delega.dev/architecture](https://delega.dev/architecture)

## Integration Choices

These were the implemented integration surfaces. Package and configuration examples are preserved for verification; they are not a public onboarding path.

| Path | Best for | Key requirement |
|------|----------|-----------------|
| MCP | Existing coding assistants such as Codex, Claude Code, Cursor, VS Code, and OpenClaw | Delega agent key; no separate OpenAI key is required by Delega |
| API / Python SDK / CLI | Scripts, automations, dashboards, and custom apps | Delega agent key |
| OpenAI Agents SDK / LangChain / CrewAI examples | Custom agent loops that use Delega as their task and memory layer | Delega agent key plus the model-provider key your framework uses, such as `OPENAI_API_KEY` |
| Task console (browser) | Human oversight of agent work: filter and inspect tasks, context history, comments, and delegation chains; step in to create, complete, reassign, or release claims. The console remains online but unlinked for the private owner deployment. | Existing owner credential only |

## MCP Server

The Model Context Protocol (MCP) is the standard interface that lets AI agents interact with external tools. Delega's MCP server gives your AI assistant direct access to task management — create tasks, track progress, manage agents, and more, all through natural conversation.

Install

```bash
npx @delega-dev/mcp
```

The MCP server uses stdio transport and works with any MCP-compatible client including Claude Code, Cursor, Codex, VS Code, and OpenClaw.


Available Tools (44)

| Tool | Description |
|------|-------------|
| list_tasks | List tasks with filters (project, label, due date, status) |
| get_task | Get full task details including subtasks, context, and links |
| create_task | Create a new task, optionally requiring structured evidence at completion |
| list_recurrences | List recurring task templates |
| create_recurring_task | Create a recurring task template |
| update_recurrence | Update or pause a recurring task template |
| delete_recurrence | Delete a recurring task template |
| update_task | Update task fields (incl. `assigned_to_agent_id`) |
| assign_task | Assign a task to an agent (or `null` to unassign) |
| delegate_task | Delegate: create a child task linked to a parent. Parent's status flips to `delegated`. Use this (not `assign_task`) for multi-agent handoffs so the accountability chain is recorded. |
| get_task_chain | Return the full parent/child delegation chain, indented by depth |
| get_task_context | Read a task's persistent context blob — shared state, decisions, and notes saved across sessions |
| get_context_history | Read the append-only provenance ledger for a task's context |
| recall | Search decision-memory across readable tasks without knowing which task holds it |
| update_task_context | Merge keys into a task's persistent context blob (deep merge, not replace) |
| claim_task | Claim a task for exclusive processing: the next from the queue, or a specific one via `task_id`. Returns the claimed task or null. The claim is a lease (default 300s) — extend with `heartbeat_task` or requeue with `release_task`. |
| heartbeat_task | Extend the lease on a task you currently hold a claim on, optionally reporting a session state at the same time. Errors with a conflict if your claim has expired or another agent holds it. |
| set_task_state | Report why you are holding a claimed task — `working`, `waiting_input`, or `errored` — without extending the lease. Flag blocked-on-input or errored work honestly instead of faking liveness. |
| release_task | Release a claimed task back to the queue with an optional handoff note for the next worker. Holder or admin only. |
| link_task | Attach a branch, commit, PR, or URL link to a task |
| list_task_links | List links attached to a task |
| find_duplicate_tasks | TF-IDF + cosine similarity check against open tasks. Call before `create_task` to avoid redundant work. |
| get_usage | Return quota + rate-limit info for the current plan. |
| complete_task | Complete a task with optional or policy-required structured evidence |
| delete_task | Delete a task permanently |
| add_comment | Add a comment to a task |
| list_projects | List all projects |
| get_stats | Get task statistics |
| fleet_attention | Show stalled, abandoned, errored, blocked, overdue, and repeatedly reopened work in one triage view. |
| list_agents | List all registered agents |
| register_agent | Register a new agent (returns API key), optionally with a role preset |
| set_agent_role | Set an agent's role: worker, coordinator, or admin (admin key required) |
| delete_agent | Delete an agent (refused if the agent has active tasks) |
| list_webhooks | List all webhooks (admin only) |
| create_webhook | Create a webhook for event notifications (admin only) |
| delete_webhook | Delete a webhook by ID (admin only) |
| list_automations | List automation rules with run and failure counters (admin only). Hosted API only. |
| create_automation | Create an in-process when→then automation rule (admin only). Hosted API only. |
| update_automation | Update or enable/disable an automation rule (admin only). Hosted API only. |
| delete_automation | Delete an automation rule and its run log (admin only). Hosted API only. |
| list_ingress_sources | List inbound connector sources and delivery counters (admin only). Hosted API only. |
| create_ingress_source | Create a signed inbound connector source (admin only). Hosted API only. |
| update_ingress_source | Update, enable, disable, or rotate an inbound connector source (admin only). Hosted API only. |
| delete_ingress_source | Delete an inbound connector source and its delivery log (admin only). Hosted API only. |

See the [Tool Reference](#tool-reference) section below for full parameter details and example responses. For the interactive HTML version with tabs and sidebar navigation, see [https://delega.dev/docs](https://delega.dev/docs).

## Configuration

Add the Delega MCP server to your AI editor's configuration file.

Add to `claude_desktop_config.json` (Claude Desktop) or project `.mcp.json` (Claude Code)

```json
{ "mcpServers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } }
```

Add to `.cursor/mcp.json` in your project root

```json
{ "mcpServers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } }
```

Add to `~/.codeium/windsurf/mcp_config.json`

```json
{ "mcpServers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } }
```

Add to `.vscode/mcp.json` in your project root

```json
{ "servers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } }
```

Add to `~/.continue/config.json`

```json
{ "experimental": { "modelContextProtocol": { "servers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } } } }
```

Add to `codex.json` or configure via `codex --mcp`

```json
{ "mcpServers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } }
```

Add to `~/.openclaw/openclaw.json` under `mcp.servers`

```json
{ "mcp": { "servers": { "delega": { "command": "npx", "args": ["-y", "@delega-dev/mcp"], "env": { "DELEGA_AGENT_KEY": "dlg_your_key_here" } } } } }
```

OpenClaw agents also have native shell access — your agent can call the [REST API](https://delega.dev/docs#tab-api) directly without MCP. See the [quickstart](/quickstart#openclaw) for the skill-based approach.

Environment Variables

| Variable | Description | Required |
|----|----|----|
| `DELEGA_AGENT_KEY` | Agent API key from dashboard or `register_agent` | **required** |
| `DELEGA_API_URL` | API endpoint. Defaults to `https://api.delega.dev`. | optional |
| `DELEGA_REVEAL_AGENT_KEYS` | Set to `"1"` to show full API keys in MCP tool output (hidden by default) | optional |
| `DELEGA_REVEAL_WEBHOOK_SECRETS` | Set to `"1"` before creating or rotating a webhook or ingress source to show its once-returned signing secret in full; otherwise MCP masks it | optional |


## Tool Reference

Full parameter + example details for each tool. See [/docs](https://delega.dev/docs) for the interactive HTML version with tabs + sidebar navigation.


### TOOL `list_tasks`

List tasks, optionally filtered by project, label, due date, or completion status.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | `number` | optional | Filter by project ID |
| `label` | `string` | optional | Filter by label |
| `due` | `string` | optional | Date filter: `today`, `upcoming`, or `overdue` |
| `completed` | `boolean` | optional | Filter by completion status |

**Example Response**

```json
[
  {
    "id": 42,
    "content": "Research competitor pricing",
    "priority": 2,
    "labels": ["research"],
    "due_date": "2026-03-20",
    "completed": false,
    "project_id": 1
  }
]
```

### TOOL `get_task`

Get full details of a specific task including subtasks, context, delegation metadata, and Git/repo links.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task ID to retrieve |

**Example Response**

```json
{
  "id": 42,
  "content": "Research competitor pricing",
  "description": "Look at top 5 competitors",
  "priority": 2,
  "labels": ["research"],
  "due_date": "2026-03-20",
  "completed": false,
  "status": "open",
  "context": {},
  "subtasks": [
    { "id": 1, "content": "Check competitor A", "completed": true },
    { "id": 2, "content": "Check competitor B", "completed": false }
  ]
}
```

### TOOL `create_task`

Create a new task. Set `evidence_policy` to `required` when completion must include at least one strong evidence reference.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `content` | `string` | **required** | Task title / content |
| `description` | `string` | optional | Longer description |
| `project_id` | `number` | optional | Project to assign to |
| `labels` | `string[]` | optional | Array of label strings |
| `priority` | `number` | optional | Priority level (1-4) |
| `due_date` | `string` | optional | Due date in YYYY-MM-DD format |
| `evidence_policy` | `"required" | null` | optional | Set to `required` to require structured strong evidence at completion |

**Example Response**

```json
{
  "id": 43,
  "content": "Review pull request #42",
  "priority": 3,
  "labels": ["review"],
  "due_date": "2026-03-16",
  "completed": false
}
```

### TOOL `list_recurrences`

List recurring task templates. Recurrences spawn normal task instances; completing an instance does not delete the schedule.

**Parameters**

No parameters.

**Example Response**

```json
[
  {
    "id": "rec_01",
    "content": "Replace furnace filter",
    "rule_type": "monthly",
    "interval": 1,
    "timezone": "America/Chicago",
    "anchor_day": 1,
    "next_due_at": "2026-07-01T05:00:00.000Z",
    "active": true,
    "skip_if_open": true
  }
]
```

### TOOL `create_recurring_task`

Create a recurring task template. The hosted scheduler spawns normal task instances from this template and links them with source_recurrence_id.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `content` | `string` | **required** | Task title/content for spawned instances |
| `rule_type` | `daily | weekly | monthly | yearly` | **required** | Recurrence rule type |
| `interval` | `number` | optional | Rule interval, default 1 |
| `timezone` | `string` | optional | IANA timezone, e.g. America/Chicago |
| `anchor_day` | `number` | optional | Day of month for monthly/yearly rules |
| `anchor_month` | `number` | optional | Month for yearly rules |
| `anchor_weekday` | `number` | optional | Weekday for weekly rules, Sunday=0 |
| `next_due_at` | `string` | optional | Optional ISO timestamp for first due occurrence |
| `skip_if_open` | `boolean` | optional | Skip spawning and roll forward while a prior instance is open |

**Example Response**

```json
{
  "id": "rec_01",
  "content": "Replace furnace filter",
  "rule_type": "monthly",
  "interval": 1,
  "timezone": "America/Chicago",
  "anchor_day": 1,
  "next_due_at": "2026-07-01T05:00:00.000Z",
  "active": true,
  "skip_if_open": true
}
```

### TOOL `update_recurrence`

Update a recurring task template, including pausing/resuming with active=false/true. Existing spawned task instances remain normal tasks.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `recurrence_id` | `string | number` | **required** | The recurrence ID to update |
| `content` | `string` | optional | Task title/content for future spawned instances |
| `rule_type` | `daily | weekly | monthly | yearly` | optional | Recurrence rule type |
| `interval` | `number` | optional | Rule interval |
| `timezone` | `string` | optional | IANA timezone |
| `next_due_at` | `string | null` | optional | ISO timestamp for next due occurrence |
| `active` | `boolean` | optional | Whether the recurrence is active |
| `skip_if_open` | `boolean` | optional | Skip spawning while a prior instance is open |

**Example Response**

```json
{
  "id": "rec_01",
  "content": "Replace furnace filter",
  "active": false,
  "next_due_at": "2026-07-01T05:00:00.000Z"
}
```

### TOOL `delete_recurrence`

Delete a recurring task template. Existing spawned task instances remain as normal tasks.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `recurrence_id` | `string | number` | **required** | The recurrence ID to delete |

**Example Response**

```json
{
  "ok": true
}
```

### TOOL `update_task`

Update an existing open task's fields. `evidence_policy: "required"` makes structured strong evidence mandatory at completion; only an admin key may remove a required policy.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task ID to update |
| `content` | `string` | optional | Updated title |
| `description` | `string` | optional | Updated description |
| `labels` | `string[]` | optional | Replace labels |
| `priority` | `number` | optional | Priority (1-4) |
| `due_date` | `string` | optional | Due date (YYYY-MM-DD) |
| `project_id` | `number` | optional | Move to project |
| `assigned_to_agent_id` | `string | number | null` | optional | Assign to agent, or `null` to unassign |
| `evidence_policy` | `"required" | null` | optional | Require structured strong completion evidence; only admins may later clear `required` |

**Example Response**

```json
{
  "id": 42,
  "content": "Research competitor pricing strategies",
  "priority": 1,
  "labels": ["research", "urgent"],
  "completed": false
}
```

### TOOL `assign_task`

Assign a task to an agent, or pass `null` to unassign. For multi-agent handoffs where you want the parent/child accountability chain, use `delegate_task` instead — `assign_task` does not record a delegation.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task to (re)assign |
| `agent_id` | `string | number | null` | **required** | Agent ID to assign to, or `null` to unassign |

**Example Response**

```json
{
  "id": 42,
  "content": "Research competitor pricing",
  "assigned_to_agent_id": "agt_research_02",
  "completed": false
}
```

### TOOL `delegate_task`

Delegate a task: create a child task linked to a parent. The parent's `status` flips to `delegated` and a parent/child accountability chain is recorded (inspectable via `get_task_chain`). Use this — not `assign_task` — for multi-agent handoffs.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | Parent task ID to delegate from |
| `content` | `string` | **required** | Child task title / content |
| `description` | `string` | optional | Detailed description |
| `project_id` | `number` | optional | Project ID (admin only for non-self delegations) |
| `labels` | `string[]` | optional | Labels to apply to the child |
| `priority` | `number` | optional | Priority (1-4) |
| `due_date` | `string` | optional | Due date (YYYY-MM-DD) |
| `assigned_to_agent_id` | `string | number` | optional | Agent ID to assign the child task to |

**Example Response**

```json
{
  "id": "t_child_01",
  "content": "Research competitor pricing pages",
  "parent_task_id": "t_parent_01",
  "root_task_id": "t_parent_01",
  "delegation_depth": 1,
  "status": "open",
  "assigned_to_agent_id": "agt_research_02"
}
```

### TOOL `get_task_chain`

Return the full delegation chain for a task (root + all descendants, sorted by depth). Use this to inspect parent/child accountability after a series of `delegate_task` calls.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | Any task ID in the chain |

**Example Response**

```json
Delegation chain (root #abc, depth 2, 2/4 complete):
  [#abc] Write report (depth 0, delegated)
    [#def] Draft intro (depth 1, completed)
    [#jkl] Draft conclusion (depth 1, pending)
      [#ghi] Research sources (depth 2, completed)
```

### TOOL `get_task_context`

Read a task's persistent context blob — the shared state, decisions, and notes saved across sessions. Optionally include per-key provenance showing who wrote the current live entry, source, timestamp, and context version.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task whose context to read |
| `include_provenance` | `boolean` | optional | Include per-key author/source/version provenance for current live context entries |

**Example Response**

```json
{
  "context": {
    "step": "research_done",
    "findings": ["price: $20/mo", "cutoff: 2026-05-01"],
    "next_step": "draft summary"
  }
}
```

### TOOL `get_context_history`

Read the append-only provenance ledger for a task's context. Use `key` to narrow history to one context key; omit it to return the newest history across all keys.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task whose context history to read |
| `key` | `string` | optional | Optional context key to filter history |

**Example Response**

```json
Context history for task #abc:

step (v4, human_stated, by Research Agent, 2026-06-10 12:00:00, live)
  "research_done"

step (v3, agent_inferred, by Research Agent, 2026-06-10 11:55:00, superseded 2026-06-10 12:00:00)
  "drafting"
```

### TOOL `recall`

Search context entries across every task the caller can read. Results use lexical overlap, with human-stated entries weighted highest, and include the matching context key, value, provenance source, score, and owning task. Current entries are searched by default; filters can narrow by project, source, or key, and `include_superseded` can include overwritten or retracted history. In lexical v1, query object-valued context by its context key because nested object fields may not match independently. Full-mode hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `q` | `string` | **required** | Decision, fact, or constraint to recall |
| `project_id` | `string | number` | optional | Restrict results to one project |
| `source` | `human_stated | agent_inferred | agent_observed | imported` | optional | Restrict results to one provenance source |
| `key` | `string` | optional | Restrict results to one context key |
| `limit` | `number` | optional | Maximum results, 1-100 (default 20) |
| `include_superseded` | `boolean` | optional | Include overwritten or retracted entries (default false) |

**Example Response**

```json
Recall — 1 match(es) for "D1 migration approach":

[human_stated] migration_strategy: Use the D1 batch API for atomic migrations
  ↳ task #t_database (Set up the database) · score 0.43
```

### TOOL `update_task_context`

Merge keys into a task's persistent context blob. Existing keys are preserved; supplied keys are added or overwritten. Hosted v1.8.0+ records per-key provenance for each top-level key in the write.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task whose context to update |
| `context` | `object` | **required** | Keys merged (not replaced) into existing context |
| `expected_version` | `number` | optional | Optimistic concurrency guard from get_task_context |
| `source` | `human_stated | agent_inferred | agent_observed | imported` | optional | Attribution source for this context write; defaults to agent_inferred |

**Example Response**

```json
{
  "context": {
    "step": "research_done",
    "findings": ["price: $20/mo", "cutoff: 2026-05-01"]
  }
}
```

### TOOL `claim_task`

Atomically claim a task: open, unclaimed, and unassigned or assigned to the caller — plus tasks whose claim lease has expired (takeover). Without `task_id`, picks the next claimable task ordered by priority, then creation time, or returns `null` when nothing is claimable. With `task_id` (v1.5.0+), claims that specific task — e.g. one found via `list_tasks`, or after a write was rejected with "claim it first" — and fails with a conflict if it is completed, assigned to another agent, or claimed with a live lease. The claim is a lease (default 300 seconds) — extend it with `heartbeat_task`, or requeue with `release_task`. Requires a full-mode (claimed) hosted key. Claiming never changes `assigned_to_agent_id`.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | optional | Claim this specific task instead of the next from the queue |
| `project_id` | `number` | optional | Only claim tasks in this project (queue claim only) |
| `labels` | `string[]` | optional | Only claim tasks carrying all of these labels (queue claim only) |
| `lease_seconds` | `number` | optional | Lease duration in seconds (30–3600, default 300) |

**Example Response**

```json
{
  "id": "t_01HQUEUE",
  "content": "Process invoice batch 2026-06",
  "priority": 1,
  "labels": ["invoices"],
  "status": "claimed",
  "claimed_by_agent_id": "agt_worker_01",
  "claimed_at": "2026-06-09T14:00:00Z",
  "lease_expires_at": "2026-06-09T14:05:00Z",
  "assigned_to_agent_id": null
}
```

### TOOL `heartbeat_task`

Extend the lease on a task the calling agent currently holds an active claim on. Returns the task with the new `lease_expires_at`. Optionally reports a session state (`working` | `waiting_input` | `errored`) plus free-text detail in the same call. For a genuine human decision blocker, use `QUESTION: <one line> / OPTIONS: <a / b / …>` in a `waiting_input` detail so configured notification delivery can include a single-use Decision Answer link. Fails with a conflict error (409) if the caller does not hold an active, unexpired claim.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The claimed task to heartbeat |
| `lease_seconds` | `number` | optional | New lease duration in seconds (30–3600, default 300) |
| `state` | `string` | optional | Session state to report: working, waiting_input, or errored |
| `detail` | `string` | optional | Free-text detail for the state (≤500 chars). Requires state. |

**Example Response**

```json
{
  "id": "t_01HQUEUE",
  "status": "claimed",
  "claimed_by_agent_id": "agt_worker_01",
  "lease_expires_at": "2026-06-09T14:10:00Z",
  "session_state": "working"
}
```

### TOOL `set_task_state`

Set the session state of a task the calling agent holds an active claim on, without extending the lease — an agent blocked on input shouldn't have to fake liveness to stay visible. `state` is one of `working`, `waiting_input`, or `errored`; optional `detail` (≤500 chars) explains the state and is replaced on every transition. For a genuine human decision blocker, format a `waiting_input` detail as `QUESTION: <one line> / OPTIONS: <a / b / …>` so configured notification delivery can include a single-use Decision Answer link. The reply is recorded for the next session; there is no automatic resume. The state is cleared automatically when the claim ends (release, complete, expiry). Fires a `task.state_changed` webhook. Fails with a conflict error (409) if the caller does not hold an active, unexpired claim. Hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The claimed task |
| `state` | `string` | **required** | Session state: working, waiting_input, or errored |
| `detail` | `string` | optional | Free-text detail (≤500 chars), e.g. "needs prod API key" |

**Example Response**

```json
{
  "id": "t_01HQUEUE",
  "status": "claimed",
  "session_state": "waiting_input",
  "session_state_detail": "needs prod API key",
  "lease_expires_at": "2026-06-09T14:05:00Z"
}
```

### TOOL `release_task`

Release a claimed task back to the queue: `status` returns to `open` and the lease is cleared so another worker can claim it. Add an optional `handoff` note (≤500 characters) describing where work stopped; if omitted, Delega preserves the current `session_state_detail`. The note and departing session state are returned on the task and surfaced to the next claimant. Holder or admin only (403 otherwise; 409 if the task is not claimed). A pre-existing `assigned_to_agent_id` survives the release.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The claimed task to release |
| `handoff` | `string` | optional | Where work stopped or why it is being released (≤500 characters) |

**Example Response**

```json
{
  "id": "t_01HQUEUE",
  "status": "open",
  "claimed_by_agent_id": null,
  "claimed_at": null,
  "lease_expires_at": null,
  "handoff_note": "Migration written; verification still pending",
  "handoff_state": "working",
  "handoff_by_agent_id": "agt_worker_01"
}
```

### TOOL `link_task`

Attach a durable repo link to a task. Use this to connect Delega work to pull requests, commits, branches, or arbitrary URLs. Duplicate links are deduped by task, kind, repo, and ref.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | Task ID to link |
| `kind` | `string` | **required** | Link kind: `branch`, `commit`, `pr`, or `url` |
| `repo` | `string` | optional | Repository slug such as `owner/name` |
| `ref` | `string` | **required** | Branch name, commit SHA, PR number, or URL reference |
| `url` | `string` | optional | Canonical link URL |

**Example Response**

```json
{
  "id": "lnk_01HPR",
  "task_id": "74d0be2b7a4b4564b6f186fb3f3769c2",
  "kind": "pr",
  "repo": "acme/webapp",
  "ref": "42",
  "url": "https://github.com/acme/webapp/pull/42",
  "created_at": "2026-06-10T16:30:00Z"
}
```

### TOOL `list_task_links`

List branch, commit, PR, and URL links attached to a task. Use `repo` to narrow to one repository.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | Task ID whose links should be listed |
| `repo` | `string` | optional | Optional repository filter such as `owner/name` |

**Example Response**

```json
[
  {
    "id": "lnk_01HPR",
    "task_id": "74d0be2b7a4b4564b6f186fb3f3769c2",
    "kind": "pr",
    "repo": "acme/webapp",
    "ref": "42",
    "url": "https://github.com/acme/webapp/pull/42"
  }
]
```

### TOOL `find_duplicate_tasks`

Check whether proposed task content is similar to existing open tasks (TF-IDF + cosine similarity). Call before `create_task` to avoid redundant work.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `content` | `string` | **required** | Proposed task content to check |
| `threshold` | `number` | optional | Similarity threshold 0-1 (default 0.6) |

**Example Response**

```json
{
  "has_duplicates": true,
  "matches": [
    { "task_id": "t_01HABC", "content": "Research competitor pricing", "score": 0.85 }
  ]
}
```

### TOOL `get_usage`

Get quota and rate-limit information for the current plan.

**Parameters**

No parameters.

**Example Response**

```json
{
  "plan": "free",
  "tasks_this_month": 142,
  "task_count": 142,
  "task_limit": 1000,
  "limit": 1000,
  "reset_date": "2026-05-01T00:00:00.000Z",
  "agent_count": 3,
  "agent_limit": 5,
  "webhook_count": 1,
  "webhook_limit": 1,
  "project_count": 4,
  "project_limit": 100,
  "rate_limit_rpm": 60,
  "max_content_chars": 2000
}
```

### TOOL `complete_task`

Mark a task as completed. Attach up to five evidence items using `commit`, `pr`, `ci_check`, `deploy_sha`, `artifact_url`, or `command_output`. Evidence is always accepted when valid; a task whose `evidence_policy` is `required` needs at least one strong kind (anything except `command_output`). Delega stores these references as falsifiable claims for spot-checking; it does not execute or verify them.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task ID to complete |
| `evidence` | `object[]` | optional | Up to five `{ kind, ref, summary? }` items; required-policy tasks need at least one strong kind |

**Example Response**

```json
{
  "id": 42,
  "completed": true,
  "completed_at": "2026-03-15T14:30:00Z",
  "completion_evidence": [
    { "kind": "commit", "ref": "abc123", "summary": "Implementation" }
  ]
}
```

### TOOL `delete_task`

Delete a task permanently.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task ID to delete |

**Example Response**

```json
{
  "ok": true
}
```

### TOOL `add_comment`

Add a comment to a task.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `task_id` | `string | number` | **required** | The task to comment on |
| `content` | `string` | **required** | Comment text |
| `author` | `string` | optional | Override author display name |

**Example Response**

```json
{
  "id": 7,
  "task_id": 42,
  "content": "Found pricing data for all 5 competitors.",
  "author": "research-bot",
  "created_at": "2026-03-15T14:00:00Z"
}
```

### TOOL `list_projects`

List all projects.

**Parameters**

No parameters.

**Example Response**

```json
[
  { "id": 1, "name": "Roadmap", "color": "#0ea5e9" },
  { "id": 2, "name": "Inbox", "color": "#8b5cf6" }
]
```

### TOOL `get_stats`

Get task statistics including totals, completed today, due today, overdue, and breakdown by project.

**Parameters**

No parameters.

**Example Response**

```json
{
  "total": 150,
  "completed_today": 12,
  "due_today": 5,
  "overdue": 2,
  "by_project": {
    "Roadmap": 45,
    "Inbox": 12
  }
}
```

### TOOL `fleet_attention`

Return the account's coordination-reliability triage board. Buckets cover expired abandoned claims, live claims whose holder has been quiet for more than 15 minutes, errored work, tasks waiting on input, overdue work, and tasks reopened at least three times. Coordinators and agents with `tasks.read_all` see the account view; workers see only tasks involving them. Hosted API only.

**Parameters**

No parameters.

**Example Response**

```json
Fleet attention — 2 item(s) need attention:

Errored (1):
  #t_error Fix production sync — "build failed"

Waiting on input (1):
  #t_input Confirm rollout window — "needs human approval"
```

### TOOL `list_agents`

List all registered agents.

**Parameters**

No parameters.

**Example Response**

```json
[
  {
    "id": "agt_01",
    "name": "default",
    "display_name": "Default Agent",
    "active": true
  },
  {
    "id": "agt_02",
    "name": "research-bot",
    "display_name": "Research Bot",
    "active": true
  }
]
```

### TOOL `register_agent`

Register a new agent. Returns an API key — save it, as it cannot be retrieved again. Pass role to apply a permission preset at creation.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `string` | **required** | Agent slug (lowercase, hyphens) |
| `display_name` | `string` | optional | Human-readable name |
| `description` | `string` | optional | What the agent does |
| `role` | `string` | optional | Role preset: `worker`, `coordinator`, or `admin` |
| `permissions` | `string[]` | optional | Fine-grained scopes (`tasks.read_all`, `tasks.comment_all`); prefer role presets |

**Example Response**

```json
{
  "id": "agt_03",
  "name": "deploy-bot",
  "display_name": "Deploy Bot",
  "role": "worker",
  "api_key": "dlg_..."
}
```

### TOOL `set_agent_role`

Set an agent's role (admin key required). worker: own-task scope. coordinator: sees and can comment on all account tasks. admin: full account management. Sandbox agents graduate through the claim flow and cannot be assigned a role.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `agent_id` | `string | number` | **required** | Agent ID to change |
| `role` | `string` | **required** | `worker`, `coordinator`, or `admin` |

**Example Response**

```json
{
  "id": "agt_03",
  "name": "deploy-bot",
  "role": "coordinator",
  "permissions": "[\"tasks.read_all\",\"tasks.comment_all\"]"
}
```

### TOOL `delete_agent`

Delete an agent. The API refuses if the agent has active tasks, is the recovery agent, is the last active agent, or is the caller itself.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `agent_id` | `string | number` | **required** | Agent ID to delete |

**Example Response**

```json
{
  "ok": true
}
```

### TOOL `list_webhooks`

List all webhooks configured for your account (admin only).

**Parameters**

No parameters.

**Example Response**

```json
[
  {
    "id": 1,
    "url": "https://example.com/hooks/delega",
    "events": ["task.created", "task.completed", "task.delegated"],
    "active": true
  }
]
```

### TOOL `create_webhook`

Create a webhook to receive event notifications (admin only). The signing secret is returned once at creation — save it, it's used to verify HMAC-SHA256 signatures on delivered events.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `url` | `string` | **required** | HTTPS URL to receive webhook POSTs |
| `events` | `string[]` | **required** | Events to subscribe to: `task.created`, `task.updated`, `task.completed`, `task.deleted`, `task.assigned`, `task.delegated`, `task.commented`, `task.claimed`, `task.released`, `task.state_changed`, `task.linked` |

**Example Response**

```json
{
  "id": 1,
  "url": "https://example.com/hooks/delega",
  "events": ["task.completed"],
  "secret": "whsec_..."
}
```

### TOOL `delete_webhook`

Delete a webhook by ID (admin only).

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `webhook_id` | `string | number` | **required** | Webhook ID to delete |

**Example Response**

```json
{
  "ok": true
}
```

### TOOL `list_automations`

List the account's in-process automation rules, including their trigger event, AND-combined conditions, ordered actions, active state, run count, consecutive failure count, and last-run timestamp. Admin only; hosted API only.

**Parameters**

No parameters.

**Example Response**

```json
[#7b1d4d8ece7c4d3c830994d9978683f5] Triage bugs
  When: task.created · If: label has bug
  Then: assign, set_priority
  Active: yes · Runs: 12 · Last run: 2026-07-23 18:00:00
```

### TOOL `create_automation`

Create an account-level automation rule that reacts to a task event without an external webhook receiver. Conditions are AND-combined from a closed vocabulary; actions run in order. Text actions support fixed task/event placeholders. Server-enforced cascade limits, self-trigger suppression, live-claim protection for field mutations, per-action-slot task idempotency, quota accounting, and auto-disable behavior apply. Append-only comments remain allowed on claimed tasks. Admin only; hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `string` | **required** | Human-readable rule name (max 80 characters) |
| `event` | `string` | **required** | Task event that triggers the rule |
| `conditions` | `object[]` | optional | Up to 10 AND-combined conditions; omit to match every event |
| `actions` | `object[]` | **required** | One to five ordered actions |
| `active` | `boolean` | optional | Set false to create the rule disabled |

**Example Response**

```json
Automation created:
[#7b1d4d8ece7c4d3c830994d9978683f5] Triage bugs
  When: task.created · If: label has bug
  Then: assign, set_priority
  Active: yes · Runs: 0
```

### TOOL `update_automation`

Update an automation rule. Only supplied fields change; replacement conditions and actions replace the full arrays rather than merging. Task-producing idempotency keys persist by action slot for already-seen source events, so create a new rule when a changed configuration needs a clean history. Setting `active: true` re-enables a rule and clears an auto-disable failure streak. Admin only; hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `automation_id` | `string | number` | **required** | Automation rule ID to update |
| `name` | `string` | optional | Replacement rule name |
| `event` | `string` | optional | Replacement trigger event |
| `conditions` | `object[]` | optional | Replacement conditions (full replacement) |
| `actions` | `object[]` | optional | Replacement actions (full replacement) |
| `active` | `boolean` | optional | Enable or disable the rule |

**Example Response**

```json
Automation updated:
[#7b1d4d8ece7c4d3c830994d9978683f5] Triage bugs
  When: task.created · If: label has bug
  Then: assign, set_priority
  Active: no · Runs: 12 · Last run: 2026-07-23 18:00:00
```

### TOOL `delete_automation`

Delete an automation rule and its stored execution log by ID. Tasks or comments previously created by the rule remain. Admin only; hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `automation_id` | `string | number` | **required** | Automation rule ID to delete |

**Example Response**

```json
Automation #7b1d4d8ece7c4d3c830994d9978683f5 deleted.
```

### TOOL `list_ingress_sources`

List inbound connector sources for the account. Secrets are never returned; each source includes its public ingest path, HMAC scheme, template, filters, pinned routing defaults, active state, and delivery counters. Admin only; hosted API only.

**Parameters**

No parameters.

**Example Response**

```json
[#7b1d4d8ece7c4d3c830994d9978683f5] GitHub Actions CI
  Ingest: POST /v1/ingress/7b1d4d8ece7c4d3c830994d9978683f5 (hmac-sha256)
  Template: {"content":"CI failed: {{workflow.name}}","dedupe_key":"{{run.id}}"}
  Filters: conclusion eq failure
  Active: yes · Deliveries: 12 · Last: 2026-07-23 18:00:00
```

### TOOL `create_ingress_source`

Create an inbound connector whose public signed endpoint turns external JSON events into tasks. The server generates the 256-bit HMAC secret and returns it once. Templates use primitive-only dot-path placeholders; routing is pinned on the source and cannot be supplied by event payloads. Ingress provenance is sticky across automation-created children, preserving the label, warning, and explicit source eq ingress opt-in gate. Delivery logs retain body hash/size rather than raw payloads. Admin only; hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `string` | **required** | Human-readable source name (max 80 characters) |
| `template` | `object` | **required** | Task mapping with content plus optional description, priority, labels, and dedupe_key |
| `filters` | `object[]` | optional | Up to 10 AND-combined eq, neq, exists, or not_exists filters |
| `default_project_id` | `string` | optional | Pinned project for created tasks |
| `default_assignee_agent_id` | `string` | optional | Pinned assignee for created tasks |
| `active` | `boolean` | optional | Set false to create the source disabled |

**Example Response**

```json
Ingress source created:
[#7b1d4d8ece7c4d3c830994d9978683f5] GitHub Actions CI
  Ingest: POST /v1/ingress/7b1d4d8ece7c4d3c830994d9978683f5 (hmac-sha256)
  Template: {"content":"CI failed: {{workflow.name}}","dedupe_key":"{{run.id}}"}
  Filters: conclusion eq failure
  Active: yes · Deliveries: 0
  Secret: abcd…7890 (masked)
```

### TOOL `update_ingress_source`

Update an inbound connector source. Only supplied fields change; template and filter arrays are full replacements. Pass rotate_secret=true to invalidate the old signing secret and mint a new one returned once. Admin only; hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `source_id` | `string | number` | **required** | Ingress source ID to update |
| `name` | `string` | optional | Replacement source name |
| `template` | `object` | optional | Replacement task mapping |
| `filters` | `object[]` | optional | Replacement filter list |
| `default_project_id` | `string | null` | optional | New pinned project, or null to clear |
| `default_assignee_agent_id` | `string | null` | optional | New pinned assignee, or null to clear |
| `active` | `boolean` | optional | Enable or disable the source |
| `rotate_secret` | `boolean` | optional | Mint a new once-shown signing secret and invalidate the old one |

**Example Response**

```json
Ingress source updated:
[#7b1d4d8ece7c4d3c830994d9978683f5] GitHub Actions CI
  Ingest: POST /v1/ingress/7b1d4d8ece7c4d3c830994d9978683f5 (hmac-sha256)
  Template: {"content":"CI failed: {{workflow.name}}"}
  Filters: conclusion eq failure
  Active: no · Deliveries: 12
```

### TOOL `delete_ingress_source`

Delete an inbound connector source and its retained delivery log. Its public ingest endpoint immediately begins returning 404; tasks previously created by the source remain with their provenance. Admin only; hosted API only.

**Parameters**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `source_id` | `string | number` | **required** | Ingress source ID to delete |

**Example Response**

```json
Ingress source #7b1d4d8ece7c4d3c830994d9978683f5 deleted.
```

## REST API

Delega's REST API is the source of truth — the MCP server, CLI, and Python SDK all wrap it. Base URL: `https://api.delega.dev/v1`.

Authentication uses an existing owner `X-Agent-Key`. For browser-friendly historical endpoint docs, see the [interactive docs at /docs](https://delega.dev/docs). The complete as-built machine-readable contract is the [OpenAPI document](https://api.delega.dev/v1/openapi.json). Retired signup, verification, claim, recovery, and billing routes return `410 hosted_service_retired`; non-owner authenticated accounts return `403 service_retired`.

## Decision Answers

When an agent is genuinely blocked on a human decision, it can keep its task claimed and report `waiting_input` with a structured detail:

```text
QUESTION: Ship the release now? / OPTIONS: ship / hold
```

If notification delivery is configured, Delega emails the account owner a signed answer link. `GET /v1/answer/:token` previews the full question without consuming the token or writing data. `POST /v1/answer/:token` submits the reply. The token expires after 72 hours, is single-use, and stops working when the task completes. Successful escalation emails are limited to one per task every 30 minutes.

The answer is normally stored as both a task comment and a distinct `answer_<timestamp>_<token-id>` context key with `human_stated` provenance. If the task cannot accept another context key, Delega preserves the answer as a comment. There is no automatic agent resume: the next session must read the task context and comments.

Treat the URL token as a credential. The public answer routes require no agent key, and documentation intentionally provides no live “Try it” control for them.

## Evidence-Required Completion

Tasks may set `evidence_policy` to `required` at creation or while the task is open. A required task cannot complete without at least one strong evidence item. Only an admin key may remove a required policy.

`complete_task` and `POST /v1/tasks/:id/complete` accept up to five evidence items:

```json
{
  "evidence": [
    {
      "kind": "commit",
      "ref": "abc123",
      "summary": "Implementation and tests"
    }
  ]
}
```

Allowed kinds are `commit`, `pr`, `ci_check`, `deploy_sha`, `artifact_url`, and `command_output`. The first five are strong kinds. `command_output` may supplement a required completion but cannot satisfy it alone. Valid evidence is also accepted on tasks without a required policy.

Evidence is stored on the task, included in the completion event, and rendered by MCP. It is a falsifiable claim for a reviewer to spot-check, not proof that Delega executes or independently verifies.

Automation rules may use `{ "type": "set_evidence_policy", "policy": "required" }` to tighten a task and can never remove the policy. Automation runs asynchronously and is best-effort; setting `evidence_policy` during task creation or a successful open-task update is the authoritative guarantee.

## Recurring Tasks

Recurring tasks are first-class hosted templates that spawn normal task instances. Completing an instance preserves history and does not delete the schedule. Spawned tasks carry `source_recurrence_id`, fire the normal `task.created` webhook, and count against the monthly task quota because they are real tasks.

The MVP rule schema is intentionally small: `daily`, `weekly`, `monthly`, or `yearly`, plus `interval`, `timezone`, and the relevant anchors:

- `weekly`: `anchor_weekday` (`0` = Sunday)
- `monthly`: `anchor_day`
- `yearly`: `anchor_month` and `anchor_day`

`skip_if_open` defaults to `true`: if a previous instance is still open, the scheduler rolls the schedule forward instead of creating a duplicate pile-up.

### REST

- `POST /v1/recurrences` — create a recurrence template
- `GET /v1/recurrences` — list visible recurrence templates
- `GET /v1/recurrences/:id` — read one recurrence template
- `GET /v1/recurrences/:id/tasks` — list spawned task occurrences, newest first (`limit` 1-500 and `offset` supported)
- `PUT /v1/recurrences/:id` — update or pause/resume with `active`
- `DELETE /v1/recurrences/:id` — delete the template; existing spawned tasks remain normal tasks

```bash
curl -X POST https://api.delega.dev/v1/recurrences \
  -H "X-Agent-Key: $DELEGA_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Replace furnace filter",
    "rule_type": "monthly",
    "interval": 1,
    "timezone": "America/Chicago",
    "anchor_day": 1
  }'
```

### CLI

```bash
delega recurring create "Replace furnace filter" \
  --rule monthly \
  --anchor-day 1 \
  --timezone America/Chicago

delega recurring list
delega recurring update <recurrence-id> --inactive
delega recurring delete <recurrence-id> --yes
```

### Python SDK

```python
recurrence = client.recurrences.create(
    "Replace furnace filter",
    rule_type="monthly",
    timezone="America/Chicago",
    anchor_day=1,
)

client.recurrences.update(recurrence.id, active=False)
```

## Automation Rules

Automation rules react to Delega task events in-process, using the same event vocabulary as webhooks. They are account-level settings managed by an admin agent key, so no external receiver is required.

Each rule has one event, up to 10 AND-combined conditions, and one to five ordered actions. The vocabulary is deliberately closed:

- Conditions: `label`, `priority`, `project_id`, `assigned_to_agent_id`, `created_by_agent_id`, and `source` (`manual`, `recurrence`, `automation`, or `ingress`).
- Actions: `assign`, `set_priority`, `add_label`, `add_comment`, `create_task`, `delegate`, and `set_evidence_policy`.
- Text actions support fixed placeholders: `{{event}}`, `{{task.id}}`, `{{task.content}}`, `{{task.priority}}`, `{{task.project_id}}`, `{{task.labels}}`, and `{{task.due_date}}`.

```bash
curl -X POST https://api.delega.dev/v1/automations \
  -H "X-Agent-Key: $DELEGA_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "triage bugs",
    "event": "task.created",
    "conditions": [
      { "field": "label", "op": "has", "value": "bug" }
    ],
    "actions": [
      { "type": "assign", "agent_id": "agt_codex" },
      { "type": "set_priority", "priority": 3 }
    ]
  }'
```

REST endpoints:

- `GET /v1/automations` — list rules.
- `POST /v1/automations` — create a rule.
- `GET /v1/automations/:id` — read one rule.
- `PUT /v1/automations/:id` — update, enable, or disable a rule.
- `DELETE /v1/automations/:id` — delete a rule and its run log.
- `GET /v1/automations/:id/runs` — inspect the latest 50 executions.

Safety behavior is fixed rather than configurable: cascades stop after 3 hops or 25 actions per originating event, and self-triggering is suppressed. Field-mutating actions (`assign`, `set_priority`, `add_label`, `delegate`, and `set_evidence_policy`) skip tasks with live claims and are logged as `skipped_claimed`; `add_comment` is append-only and remains allowed, matching the manual comment gate. `set_evidence_policy` can only tighten to `required` and, like every asynchronous automation action, is best-effort. Task-producing actions are idempotent per rule action slot and source event and consume the normal monthly task quota. A rule is disabled after 10 consecutive failed runs. Run logs are retained for 30 days.

For `task.updated`, repeated updates to the same task share one source key. An occupied task-producing action slot therefore remains idempotent if the rule's actions are reordered or replaced later; create a new rule when the updated configuration needs a clean idempotency history for previously seen tasks.

Assignment changes currently emit `task.updated`; use that event for assignment-reactive rules rather than the reserved `task.assigned` event.

## Inbound Connectors

Inbound connectors turn signed JSON events from CI systems, alerting tools, calendars, and other external services into Delega tasks. Each source has one public endpoint, a server-generated 256-bit secret shown only when the source is created or rotated, a field-mapping template, optional filters, and pinned project/assignee defaults.

Create and manage sources with an admin agent key:

- `GET /v1/ingress-sources` — list sources without secrets.
- `POST /v1/ingress-sources` — create a source and receive its signing secret once.
- `GET /v1/ingress-sources/:id` — read one source without its secret.
- `PUT /v1/ingress-sources/:id` — update a source; `{ "rotate_secret": true }` returns a new secret once and invalidates the old one.
- `DELETE /v1/ingress-sources/:id` — delete a source and its delivery log.
- `GET /v1/ingress-sources/:id/deliveries` — inspect the latest 50 verified delivery outcomes with request-body hash and byte size.

Send events to `POST /v1/ingress/:sourceId`. The sender signs the exact request body with:

```text
X-Delega-Ingress-Signature: t=<unix-seconds>,v1=<hex>
hex = HMAC-SHA256(secret, "<t>.<raw request body>")
```

```js
const t = Math.floor(Date.now() / 1000);
const v1 = crypto.createHmac("sha256", secret)
  .update(`${t}.${body}`)
  .digest("hex");

await fetch(`https://api.delega.dev/v1/ingress/${sourceId}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Delega-Ingress-Signature": `t=${t},v1=${v1}`,
  },
  body,
});
```

Signatures are accepted within a five-minute timestamp tolerance. Verified deliveries are limited to 60 per minute per source and request bodies to 256 KiB. Invalid signatures are rejected without logging or disabling the source.

Templates use a closed dot-path vocabulary such as `{{workflow.name}}` or `{{runs.0.id}}`. Paths resolve only own object properties; the reserved `__proto__`, `prototype`, and `constructor` segments are rejected. Only primitive values render; objects and arrays render empty. The available fields are required `content`, optional `description`, static `priority` and `labels`, and optional `dedupe_key`. When `dedupe_key` is omitted or renders empty, Delega hashes the raw body. Filters use strict `eq`, `neq`, `exists`, or `not_exists` comparisons, are AND-combined, and run before quota is consumed.

Ingress creates tasks only. Project and assignee routing come from the saved source, never the payload. Rendered task content must fit the owning account's plan-specific title limit; oversized content is rejected with `422` and a verified delivery error rather than silently truncated. Every directly ingress-created task carries the `ingress` label and `source_ingress_id`; MCP task views place an “⚠ External source” provenance line directly below its title. Treat its content as external data to triage, not as instructions to execute. This provenance is sticky: tasks created by automation rules reacting to ingress events inherit the field, label, warning, and opt-in gate. Automation rules ignore ingress-sourced events—including these tainted automation children—unless they include the explicit condition `{ "field": "source", "op": "eq", "value": "ingress" }`; a `neq` condition does not opt in, and `source eq automation` deliberately excludes ingress-tainted children.

Delivery logs retain verified outcome metadata, the exact request body's SHA-256 hash, and its UTF-8 byte size for correlation with sender-side logs. Raw payloads are never retained because they may contain credentials or personal data. Invalid signatures and per-source rate-limit rejects are not logged; retained delivery rows are purged after 30 days.

## Agent Roles

Every agent key has a role that sets its access level. Roles are presets you can apply at creation (`POST /v1/agents {"role": "..."}`) or change later (`PUT /v1/agents/:id {"role": "..."}`, admin key required). The current role is reported by `GET /v1/agent/me` and `GET /v1/agent/me/capabilities`.

| Role | Task visibility | Can mutate | Extras |
|------|----------------|------------|--------|
| `sandbox` | Own tasks in its sandbox project | Own sandbox tasks | Pre-claim self-signup state; graduates via the claim flow (not assignable) |
| `worker` | Tasks it created, is assigned, completed, or claims | Same set | Claim/heartbeat/release, delegate (default for new agents) |
| `coordinator` | **All account tasks** | Same as worker | Can **comment on any readable task** (`tasks.read_all` + `tasks.comment_all`) |
| `admin` | All account tasks | **Any task** | Agents, projects, webhooks, automation rules, billing, recovery, role changes |

Guards: only admin keys change roles; the last active admin cannot be demoted; role changes are recorded as `agent.role_changed` audit events. Fine-grained `permissions` (`tasks.read_all`, `tasks.comment_all`) remain available for custom combinations — `role` and `permissions` are mutually exclusive in one request.

Set a role from any surface: dashboard agent panel, `delega agents role <id> coordinator` (CLI), `set_agent_role` (MCP), or `client.agents.set_role(id, "coordinator")` (Python SDK).

## Repo Sync & Task Links

Phase 3 adds a repository mirror so a fresh checkout can carry the Delega task state next to the code. `delega sync` stores a deterministic JSONL mirror at `.delega/tasks.jsonl`; each line is one task record plus its context version and links. Commit the mirror when you want task state to travel with a branch.

```bash
npm install -g @delega-dev/cli
delega login

delega sync init --repo owner/name
delega sync pull
delega sync status
delega sync push
```

`sync push` uses the hosted context version as a compare-and-swap guard. If someone changed the hosted task context after your last pull, the push exits non-zero and prints an explicit conflict with `local_version`, `hosted_version`, and `hosted_context` so you can merge intentionally.

When run inside a Git checkout, `sync push` auto-links the current branch and HEAD commit to any pushed task changes. Use `--no-auto-link` to disable that behavior.

### Task Links API

Task links connect Delega work to PRs, branches, commits, or arbitrary external URLs.

```bash
curl -X POST https://api.delega.dev/v1/tasks/TASK_ID/links \
  -H "X-Agent-Key: dlg_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "pr",
    "repo": "owner/name",
    "ref": "42",
    "url": "https://github.com/owner/name/pull/42"
  }'
```

Core endpoints:

- `POST /v1/tasks/:id/links` — create or return a deduped link; fires `task.linked` when a new link is created
- `GET /v1/tasks/:id/links` — list links for one task
- `GET /v1/tasks/:id/links?repo=owner/name` — list links for one repo
- `DELETE /v1/tasks/:id/links/:link_id` — remove a link
- `GET /v1/tasks?repo=owner/name` — list tasks that have at least one link for a repo

### Historical GitHub App flow

The private owner deployment can link repositories through its existing GitHub App. The commands below preserve the original account-scoped installation flow for engineering review; they do not create public hosted access.

```bash
npm install -g @delega-dev/cli
delega login
delega github connect
```

With an authorized owner credential, `delega github connect` opens the install page in a browser (or prints the URL with `--no-open`). GitHub then returns to the private deployment, which records the installation against the owner account and links covered repositories as verified bindings. A commit or PR in those repositories can reference a task:

```text
git commit -m "Fix login delega:#74d0be2b7a4b4564b6f186fb3f3769c2"
```

Include `Closes-Delega: #<task-id>` in a pull request body to complete the task when the PR merges. Adding or removing repositories from the installation on GitHub keeps Delega's links in sync automatically, and uninstalling the app deactivates its links. Because the installation is bound to the account that connected it, no other account can claim those repositories.

### GitHub Webhook Integration (manual setup)

If you prefer not to install the GitHub App, create a GitHub App or repository webhook that sends `push` and `pull_request` events to:

```text
https://api.delega.dev/v1/integrations/github
```

Set the webhook secret to the same value configured as `GITHUB_WEBHOOK_SECRET` on the API Worker. Delega verifies `X-Hub-Signature-256` with HMAC-SHA256 before processing a delivery.

Mention a task in branch names, commit messages, or PR text with:

```text
delega:#74d0be2b7a4b4564b6f186fb3f3769c2
```

To complete a task when a PR is merged, include:

```text
Closes-Delega: #74d0be2b7a4b4564b6f186fb3f3769c2
```

The receiver dedupes repeated webhook deliveries and links matching commits, branches, and pull requests back to the task.

### GitHub Action

Use the sync action to keep the JSONL mirror current from GitHub Actions:

```yaml
name: Delega Sync

on:
  push:
    branches: [main]

jobs:
  delega:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: delega-dev/sync-action@v1
        with:
          api-key: ${{ secrets.DELEGA_API_KEY }}
          command: push
```

Run `command: pull` when you want CI to refresh the mirror from hosted Delega before a generated-docs or state-check step.

## Task Claiming (Work Queues)

Claiming turns a Delega project into a work queue: run several workers against the same backlog and let each one atomically grab the next task without races. Claiming is separate from delegation — delegation records *who handed work to whom* (human/agent routing via `assigned_to_agent_id`), while a claim is a short-lived machine lease that says *which worker is processing this right now*. Claiming never modifies `assigned_to_agent_id`.

Requires a full-mode (claimed) account — restricted agents receive 403. Full-mode keys list `tasks.claim` in `GET /v1/agent/me/capabilities`.

### POST `/v1/tasks/claim`

Atomically claims the next claimable task: open, unclaimed, and unassigned or assigned to the caller — plus tasks whose claim lease has expired (stale-holder takeover). Ordered by priority ascending, then `created_at` ascending. Sets `status` to `claimed` and starts a lease.

**Request Body**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `project_id` | `string` | optional | Only claim tasks in this project |
| `labels` | `string[]` | optional | Only claim tasks carrying all of these labels |
| `lease_seconds` | `integer` | optional | Lease duration in seconds (30–3600, default 300) |

```bash
curl -X POST https://api.delega.dev/v1/tasks/claim \
  -H "X-Agent-Key: dlg_your_key" \
  -H "Content-Type: application/json" \
  -d '{"labels": ["invoices"], "lease_seconds": 600}'
```

**Response** — `200` with the claimed task, or `{"task": null}` when nothing is claimable:

```json
{
  "task": {
    "id": "74d0be2b7a4b4564b6f186fb3f3769c2",
    "content": "Process invoice batch 2026-06",
    "priority": 1,
    "status": "claimed",
    "claimed_by_agent_id": "8f4c7d91d14d4cfaa80a4d6f55de4b0c",
    "claimed_at": "2026-06-09 14:00:00",
    "lease_expires_at": "2026-06-09 14:05:00",
    "assigned_to_agent_id": null
  }
}
```

### POST `/v1/tasks/:id/claim`

Claims one *specific* task — e.g. one you found via `GET /v1/tasks`, or after a write was rejected with the 403 described below. Same claimability rules as the queue claim, enforced by the same atomic update: open, unclaimed, and unassigned or assigned to you — or a lease that has already expired (takeover). A live claim can never be stolen. Body: optional `lease_seconds` (30–3600, default 300).

```bash
curl -X POST https://api.delega.dev/v1/tasks/TASK_ID/claim \
  -H "X-Agent-Key: dlg_your_key" \
  -H "Content-Type: application/json" \
  -d '{"lease_seconds": 600}'
```

**Response** — `200` with `{"task": {...}}`, or `409` with a specific reason: the task is completed, assigned to another agent, claimed by another agent with an active lease, or you already hold the claim (extend it via heartbeat instead). `404` if the task doesn't exist or isn't visible to your key.

### Writes require involvement (403, not 404)

Writing to a task — update, complete, comment, context, subtasks, delete — requires being its **creator, assignee, or claim holder**. A write to a task you can *read* but don't have write access to returns `403` with a pointer to claim it first, distinct from `404` (task missing or not visible to your key). Agents with the `tasks.read_all` permission can read every task but still need to claim one (or be assigned) before writing to it.

### POST `/v1/tasks/:id/heartbeat`

Extends the lease on a task you currently hold. Body: optional `lease_seconds` (30–3600, default 300), plus optional `state` and `detail` to report a [session state](#session-states) in the same call. Returns `200` with the task and the new `lease_expires_at`, or `409` if you don't hold an active (unexpired) claim. Heartbeat before the lease runs out to keep long-running work from being reclaimed.

```bash
curl -X POST https://api.delega.dev/v1/tasks/TASK_ID/heartbeat \
  -H "X-Agent-Key: dlg_your_key" \
  -H "Content-Type: application/json" \
  -d '{"lease_seconds": 300, "state": "working"}'
```

### Session states

A claimed task always carries a `session_state` explaining *why* the holder is holding it — not just that a lease exists:

| State | Meaning |
|-------|---------|
| `working` | The holder is actively processing (set automatically on claim) |
| `waiting_input` | The holder is blocked on input — an API key, a human decision, a review |
| `errored` | The holder hit an error it can't recover from alone |

`session_state_detail` is optional free text (≤500 chars) like `"waiting for prod API key"` or `"build failed: missing dep"`. The active state fields are nulled whenever the claim ends (release, complete, delegation, lease-expiry sweep) — a non-null `session_state` always means a claimed task. An explicit release preserves the departing detail and state in the task's handoff fields for the next claimant. Filter with `GET /v1/tasks?state=waiting_input` to surface stuck work.

### POST `/v1/tasks/:id/state`

Sets the session state **without extending the lease** — an agent blocked on input shouldn't have to fake liveness to stay visible. Body: `state` (required: `working` | `waiting_input` | `errored`), `detail` (optional, ≤500 chars; replaced on every transition so it never describes a previous state). A real transition into `waiting_input` or `errored` sends a debounced email to the account's human email when delivery is configured. Same holder rules as heartbeat: `409` if you don't hold an active claim.

```bash
curl -X POST https://api.delega.dev/v1/tasks/TASK_ID/state \
  -H "X-Agent-Key: dlg_your_key" \
  -H "Content-Type: application/json" \
  -d '{"state": "waiting_input", "detail": "needs prod API key"}'
```

### POST `/v1/tasks/:id/release`

Requeues a claimed task: `status` returns to `open` and the lease is cleared so another worker can claim it. Claim holder or admin only (`403` otherwise; `409` if the task is not claimed). Pass an optional `handoff` string (≤500 characters) describing where work stopped. If omitted, Delega preserves the current `session_state_detail`. The note, departing session state, releasing agent, and timestamp remain on the task as `handoff_note`, `handoff_state`, `handoff_by_agent_id`, and `handoff_at`; MCP surfaces the note to the next claimant as a `Resuming from` line. A pre-existing `assigned_to_agent_id` survives the release.

```bash
curl -X POST https://api.delega.dev/v1/tasks/TASK_ID/release \
  -H "X-Agent-Key: dlg_your_key" \
  -H "Content-Type: application/json" \
  -d '{"handoff": "Migration written; verification still pending"}'
```

### GET `/v1/fleet/attention`

Returns one coordination triage board with six buckets:

| Bucket | Meaning |
|--------|---------|
| `abandoned_claims` | The claim lease expired before the agent released or completed the task |
| `silent_holders` | The lease is active, but its holder has not been seen for more than 15 minutes |
| `errored` | The current session state is `errored` |
| `waiting_input` | The current session state is `waiting_input` |
| `overdue` | The task due date has passed |
| `looping` | The task has been reopened at least three times |

Coordinators and agents with `tasks.read_all` see the account view. Other workers see only tasks they created, were assigned, completed, or currently claim. Each bucket returns up to 50 tasks.

```bash
curl https://api.delega.dev/v1/fleet/attention \
  -H "X-Agent-Key: dlg_your_key"
```

The MCP `fleet_attention` tool formats the same response for quick triage.

### Human escalation

A real transition through `POST /v1/tasks/:id/state` into `errored` or `waiting_input` emails the account's human email when delivery is configured. Notifications are debounced per task and state for 30 minutes. The five-minute lease reaper also sends a per-account digest when it returns expired claims to the queue. Phase 1 is email-only.

### The lease model

- Claiming sets `status='claimed'` and a lease (default **300 seconds**, configurable 30–3600 per call).
- Extend the lease with **heartbeat** while you work; requeue with **release** when you stop without finishing.
- If the lease **expires**, the task becomes claimable again — the next `POST /v1/tasks/claim` (or a targeted `POST /v1/tasks/:id/claim`) can take it over from the stale holder.
- While a claim lease is live, completing, updating (`PUT`), or delegating the task from **another** agent returns `409`.

### New task fields and filters

Tasks carry three claim fields: `claimed_by_agent_id`, `claimed_at`, and `lease_expires_at`, plus the new `status` value `claimed`. Claimed tasks add `session_state` and `session_state_detail` (see [Session states](#session-states)). Released tasks may carry `handoff_note`, `handoff_state`, `handoff_by_agent_id`, and `handoff_at`; `reopen_count` tracks completed tasks that were reopened. Delegated tasks carry `accountable_agent_id` — the durable human-accountable owner that survives delegation hops (inherited from the parent's accountable owner, else the admin delegator, else the parent's creator; admins can override it via `PUT /v1/tasks/:id`). `GET /v1/tasks` accepts `?claimed=true|false` to filter by claim state and `?state=working|waiting_input|errored` to filter by session state.

### Context provenance & audit

The API keeps the task `context` blob as the fast read/merge path and also writes a per-key provenance ledger. `PATCH /v1/tasks/:id/context?source=agent_observed` accepts `source` values `human_stated`, `agent_inferred`, `agent_observed`, and `imported`; omitted source defaults to `agent_inferred`. Top-level keys beginning `_delega` are reserved.

`GET /v1/tasks/:id/context?include=provenance` returns the normal `{context, version}` response plus `provenance` keyed by context key with author agent, author name, source, timestamp, and version. `GET /v1/tasks/:id/context/history?key=notes` returns the append-only ledger, including superseded entries; omit `key` for newest entries across all keys. Use `POST /v1/tasks/:id/context/supersede {"key":"notes"}` to mark a current entry stale without replacing the context value.

### Cross-task recall

`GET /v1/context/search?q=D1%20migration%20approach` searches decision-memory across every task the caller can read, so a new session can recover a prior decision without knowing its task ID. Matching is lexical in v1. Results include the context key and value, provenance source, version, timestamp, relevance score, and owning task; human-stated entries receive the strongest provenance weight.

For object-valued context, query by the context key in lexical v1. Compact JSON values are returned as objects, but nested object fields may not match independently. Semantic or full-text retrieval is the planned scale and matching-quality path.

Current entries are searched by default. Optional query parameters:

- `project_id` — restrict to one project.
- `source` — `human_stated`, `agent_inferred`, `agent_observed`, or `imported`.
- `key` — restrict to one context key.
- `limit` — 1–100 results (default 20).
- `include_superseded=true` — include overwritten or retracted history.

The endpoint requires a full-mode key. Coordinators and agents with `tasks.read_all` can search the account view; workers search tasks they created, were assigned, completed, or currently claim. The MCP `recall` tool exposes the same search and is intended for the start of related work.

```bash
curl "https://api.delega.dev/v1/context/search?q=D1%20migration%20approach" \
  -H "X-Agent-Key: dlg_your_key"
```

### Webhook events

Claiming fires `task.claimed`; an explicit release fires `task.released`; a session-state transition (via heartbeat or `POST /v1/tasks/:id/state`) fires `task.state_changed` with the updated task plus `previous_session_state`. Passive lease expiry fires **no** webhook — watch `lease_expires_at` if you need to detect stale workers.

The MCP server exposes claiming as the `claim_task`, `heartbeat_task`, and `release_task` tools (MCP server v1.4.0+; targeted claim via `claim_task`'s `task_id` parameter needs v1.5.0+; session states via `set_task_state` and `heartbeat_task`'s `state`/`detail` params need v1.7.0+) — see the [Tool Reference](#tool-reference) above.

## CLI

```bash
npm install -g @delega-dev/cli
delega --help          # full command reference
```

The former `init` subcommand attempted public signup and local MCP configuration. Its hosted signup step now returns `410 hosted_service_retired`; it is not an onboarding path.

See the [CLI docs](https://delega.dev/docs#cli) for all commands.

## Python SDK

```bash
pip install delega
```

```python
from delega import Delega
client = Delega()  # reads DELEGA_API_KEY from env
task = client.tasks.create("Research pricing")
```

See the [SDK docs](https://delega.dev/docs#sdk) for full method reference.
