Skip to content

Agent Architecture

How a local AI agent picks up tasks from Taskbase, works them, and reports results back. This document covers the task lifecycle, authentication model, API contract, and runtime setup.

See ADR 0003 for the decision record that established the local MCP server approach.


Task Lifecycle

A task moves through the following states, modelled using done (boolean) and group_id (kanban column):

┌─────────┐   agent claims    ┌─────────────┐   agent works    ┌──────────┐
│ Backlog  │ ────────────────► │ In Progress │ ───────────────► │   Done   │
│ (queued) │                   │  (running)  │                  │          │
└──────────┘                   └──────┬──────┘                  └──────────┘
                                      │ context full / blocked
                                  ┌─────────────┐
                                  │   Blocked   │
                                  │ (paused w/  │
                                  │  checkpoint)│
                                  └─────────────┘
Kanban Column Meaning for agent
Backlog Task is queued and ready for pickup
In Progress Agent has claimed the task and is actively working it
Done Agent has completed and closed the task (done = true)
Blocked Agent has checkpointed; context was exhausted or work is blocked
Review Agent has delivered work; waiting for human review

State transitions

Claiming a task (Backlog → In Progress)

The agent polls GET /organizations/{id}/tasks filtered to group Backlog, picks the highest-priority task, and atomically transitions it:

PUT /tasks/{id}
{
  "group_id": <in-progress group id>,
  "assignee_type": 2,        // agent
  "assignee": "<agent-id>",
  ...all other fields preserved...
}

To prevent two agent instances from claiming the same task, the agent reads the full task body first, then immediately PUTs with assignee_type=2. In the current single-agent setup this is sufficient; a future version can add optimistic locking via a version column.

Completing a task (In Progress → Done)

PUT /tasks/{id}
{
  "done": true,
  "group_id": <done group id>,
  ...description updated with completion summary...
}

Checkpointing (In Progress → Blocked)

When the context budget is running low or the task is externally blocked:

PUT /tasks/{id}
{
  "group_id": <blocked group id>,
  "description": "<original description>\n\n## Checkpoint\n<state summary>"
}

Intermediate progress

The agent appends progress notes to the task description using the Markdown ## Progress section convention. There is no separate log table in v1; the description field is the audit trail.


Authentication Model

Agent ↔ Taskbase API

The agent authenticates to the Taskbase REST API using a long-lived API key:

  • Key is generated once via POST /api/api-keys (or the Taskbase UI)
  • Stored at ~/.config/taskbase-agent/config.yaml on the agent machine (or as a Cowork plugin config)
  • Sent as X-API-Key: <key> header on every request
  • Never exposed in agent context — the MCP server holds the key; Claude never sees it

Agent ↔ External services

When the agent needs credentials for external services (Gitea, Kubernetes, etc.) while working a task:

Service How credentials are stored How agent accesses them
Gitea GITEA_TOKEN env var in task description (injected by operator) OR OpenBao secret Agent reads from task description or calls OpenBao via Taskbase proxy
Kubernetes kubeconfig at ~/.kube/config on agent machine kubectl / client-go reads automatically
Anthropic API ANTHROPIC_API_KEY env var on agent machine Set in Cowork session environment

Principle: credentials for external services are never stored in Taskbase tasks. If a task requires credentials, the operator either injects them as environment variables on the agent machine or references a named OpenBao secret that the agent can retrieve via the platform secrets API.

Gitea PAT for Taskbase sync

The Taskbase API itself uses a Gitea PAT for the server-side Gitea sync feature (not the agent). This PAT is:

  • Stored in OpenBao at secret/tools/taskbase/gitea-token
  • Injected into the API deployment as GITEA_TOKEN env var via External Secrets Operator
  • Used by the Taskbase API server (not the agent) to create/update Gitea issues

API Contract (Agent ↔ Taskbase)

These are the endpoints the agent (via the local MCP server) calls to manage tasks:

Operation Endpoint Notes
List queued tasks GET /organizations/{orgId}/tasks Filter by group (Backlog) client-side
Get task detail GET /tasks/{id} Full description, metadata
Claim task PUT /tasks/{id} Set group_id (In Progress) + assignee_type=2
Update progress PUT /tasks/{id} Append to description
Complete task PUT /tasks/{id} Set done=true + group_id (Done)
Checkpoint task PUT /tasks/{id} Set group_id (Blocked), add checkpoint to description
List orgs GET /organizations To discover org IDs at startup
List projects GET /organizations/{orgId}/projects To enumerate work queues
List groups GET /projects/{id}/groups To resolve group name → ID

All requests use X-API-Key: <key> header. Base URL: https://taskbase.skatzi.com/api.


Runtime (Mac Mini)

The local MCP server runs on the Mac Mini alongside Cowork as a persistent background process. Decision: launchd service (native macOS service manager).

Why launchd

  • Native to macOS — no additional tooling needed
  • Starts automatically on boot
  • Restarts on crash
  • Logs to the system journal (viewable with log show)
  • Consistent with how other background services run on the Mac Mini

Alternative considered: manual process / tmux session

Rejected because it requires the operator to start the server manually after every reboot, which breaks the "zero manual steps" requirement from ADR 0003.

Config location

~/.config/taskbase-agent/
├── config.yaml          # API base URL, key, token budget settings
└── taskbase-agent.plist # launchd service definition (symlinked to ~/Library/LaunchAgents/)

config.yaml schema:

api_base_url: https://taskbase.skatzi.com/api
api_key: <generated API key>
default_token_budget: 100000
budget_warning_threshold: 0.15   # pause when 15% budget remains
agent_id: mac-mini-agent

MCP server registration

The server is registered in ~/.config/claude/claude_desktop_config.json (or the Cowork equivalent) so it is available as a plugin in every Cowork session:

{
  "mcpServers": {
    "taskbase": {
      "command": "/usr/local/bin/taskbase-agent-mcp",
      "args": ["--config", "/Users/<user>/.config/taskbase-agent/config.yaml"]
    }
  }
}

Failure Modes and Observability

Failure Detection Recovery
Agent crashes mid-task Task stays In Progress with no updates for > 30 min Operator manually moves to Blocked or re-queues
Taskbase API unreachable MCP get_next_task returns error Agent session ends; launchd restarts MCP server; operator retries
Context exhausted without checkpoint Task stays In Progress Operator inspects description for partial work, manually checkpoints
Duplicate task claim (future multi-agent) Two agents PUT same task v1: not a concern (single agent). v2: add optimistic locking via version column
Gitea sync failure Task update succeeds but Gitea issue not created Taskbase API logs error; issue can be manually created; async retry not in v1

Logging

  • Taskbase API logs to stdout → captured by the Kubernetes pod logs → visible in Grafana (Loki)
  • MCP server logs to ~/Library/Logs/taskbase-agent.log
  • Agent session transcript available in the Cowork UI

Alerts (v1 — manual)

In v1 there are no automated alerts. The operator checks the Taskbase UI periodically. Automated alerting (e.g. "task stuck in In Progress for > N minutes") is a future addition.