Skip to content

0005 - Taskbase Agent System

Status

Accepted (supersedes ADR-0004)

Date

2026-05-02

Context

ADR-0004 defined the Taskbase agent side as a FastAPI executor on the Mac Mini paired with first-class agent instances (named workers per type) backed by persistent session_id resumption — types and instances bundled together with cross-task memory baked into the brain's data model.

In practice the bundle was doing two unrelated jobs: defining what an agent is, and accumulating cross-task memory. The first is needed to map an agent to a task at all. The second is an optimisation. This ADR separates them, ships only the first as v1, and re-anchors the design around three principles that ADR-0004 did not enforce:

  • Multi-tenant agent definitions — each Taskbase organization has its own agent fleet, defined and versioned per org rather than bundled in the taskbase repo
  • Runtime-agnostic at the brain layer — the brain stores no concepts that only make sense in one runtime (Claude Code, Anthropic SDK, Ollama, etc.)
  • Workflow ownership in the target repo — branch protection, review requirements, test gates, merge policies belong to the repo being worked on, not to Taskbase. The brain is a pure priority dispatcher; it does not own task lifecycle states

This ADR replaces ADR-0004 in full. Its predecessor remains in the doc tree as the historical record of the 2026-04-18 decision.

Decision

Per-organization agent definitions in Git

For each Taskbase organization with slug <org-slug>, Taskbase looks up agent definitions in the Gitea repo <org-slug>-agents under that org's Gitea owner — i.e. https://gitea.prod.skatzi.com/<org-slug>/<org-slug>-agents. The Skatzi-internal org's repo is skatzi-agents. An agents_repo_url override on the organizations table covers non-conventional placements.

Repository layout:

<org-slug>-agents/
├── README.md
└── personas/
    └── <slug>.md

Each persona is a runtime-agnostic markdown file with name and description frontmatter and a body covering Role / Responsibilities / Out of scope / Workflow / Output. No model name, no tool list, no permission mode — those are runtime-adapter concerns, not part of the persona definition.

Data model

agent_types:
  id, organization_id, slug, name, description, body, is_active, last_synced_at
  unique (organization_id, slug)

tasks:
  ...existing columns...
  assignee_type = "agent"
  assignee      = agent_type_id
  priority      = (existing field)

body holds the persona's full markdown body; whatever runtime executes the task uses it as a system prompt. Each task is a fresh run — no shared state across tasks at the agent layer.

Architecture

graph LR
    REPO["ORGNAME-agents repo on Gitea\npersonas/*.md"]

    subgraph Taskbase Brain
        SYNC["Agents Sync\ncron + Gitea webhook"]
        TYPES[("agent_types\norganization_id, slug, body")]
        TASKS[("tasks priority queue\nassignee = agent_type_id")]
        SYNC --> TYPES
        TYPES -.-> TASKS
    end

    EXEC["Runtime Executor\nany implementation"]

    REPO -->|fetch contents + webhook| SYNC
    TASKS -->|dispatch by priority| EXEC
    EXEC -->|capacity signals + follow-up tasks| TASKS

End-to-end sequence

sequenceDiagram
    actor Author as Persona Author
    actor User
    participant Gitea
    participant Brain as Taskbase Brain
    participant Executor
    participant Worker

    Note over Author,Brain: Phase 1 - Persona sync (recurring)
    Author->>Gitea: git push to ORGNAME-agents/personas/SLUG.md
    Gitea-->>Brain: webhook (push event, HMAC-signed)
    Brain->>Gitea: GET persona files for ORGNAME-agents
    Gitea-->>Brain: persona contents
    Brain->>Brain: upsert agent_types by (organization_id, slug)
    Note over Brain: Cron also pulls every N minutes as a backstop

    Note over User,Brain: Phase 2 - Task creation (user or another agent)
    User->>Brain: POST /tasks (assignee = agent_type_id, prompt, priority)
    Brain->>Brain: enqueue task

    Note over Brain,Worker: Phase 3 - Dispatch (priority-ordered, when capacity is free)
    Brain->>Brain: pick highest-priority queued task for any free slot
    Brain->>Executor: dispatch (task prompt + agent_type.body)
    Executor->>Worker: spawn (system prompt + task prompt)

    Note over Worker,Gitea: Phase 4 - Execution (agent reads the repo and follows its conventions)
    activate Worker
    Worker->>Gitea: read target repo (CLAUDE.md, branch protection rules, etc.)
    Worker->>Worker: do the work
    Worker->>Brain: POST /tasks (create follow-up tasks for other agents)
    Worker->>Gitea: push branch, open PR, or whatever the repo workflow requires
    Worker-->>Executor: finished
    deactivate Worker
    Executor-->>Brain: capacity freed
    Note over Brain: Loops back to Phase 3 with the next prioritized task. Review and merge happen between subsequent agents and Gitea — the brain does not track them as task states.

Key principles

  1. The brain is a priority dispatcher, not a state tracker. It does not maintain in_progress / ready_for_review / done lifecycle states. From its perspective a task is either queued or dispatched. Taskbase's existing follow-Gitea sync continues to mirror issue and PR state into Taskbase as it always has — independent of the agent system.
  2. Workflow lives in the target repo. Branch protection, review requirements, test gates, merge policies — all defined by the target repo's Gitea settings and CLAUDE.md. The agent reads those at runtime and acts accordingly. The brain doesn't replicate that logic, doesn't dispatch to specific reviewer agents, doesn't decide when work is finished.
  3. Workers are first-class API clients of the brain. Any agent worker can PATCH /tasks/{id} (update its own task) or POST /tasks (create follow-up work assigned to another agent_type — e.g., "review PR #N"). Multi-agent coordination flows through the queue, not through bespoke channels.
  4. Runtime-agnostic at the brain layer. The brain stores no Claude-Code-specific concepts. The current Claude Code executor (claude -p subprocess as worker) is one implementation; Anthropic-SDK runners, Ollama runners, or human-in-the-loop responders all satisfy the same contract.

Implementation Notes

  • Agent types are derived from per-organization Gitea repos named <org-slug>-agents. A background sync (default cadence: 5 minutes; also Gitea-webhook-driven) upserts results into the agent_types table keyed by (organization_id, slug). Synced fields are read-only in the UI; edits go through PRs against the agents repo
  • A missing repo is not an error — the org just shows "no agent types configured; create <org-slug>-agents in Gitea to add some"
  • Personas removed from the repo become is_active=false rather than being hard-deleted, so historical task assignments continue to resolve
  • Brain → Gitea auth: a service-level read-only Gitea token, stored in OpenBao and surfaced via External Secrets, with read access to *-agents repos
  • The brain holds tasks in a priority-ordered queue and dispatches by priority when an executor signals capacity. Issue / PR state for any Gitea-linked task is mirrored by Taskbase's existing follow-Gitea sync, independent of the agent system
  • The Claude Code executor's spawn command for v1: claude -p --output-format stream-json --append-system-prompt <agent_type.body> --chrome <task.prompt> — fresh process per task, no --resume, no --allowedTools, no --permission-mode (executor defaults apply)

What we deferred (and what would trigger adding it)

Concept Trigger to add
Agent instances (named workers per type) Users want distinct named workers within a type — e.g. "alice" and "bob" both PMs but with different scopes
Session / state continuity (re-using prior context across tasks via --resume or equivalent) Re-learning per task wastes too many tokens or tasks naturally chain for the same worker
Per-runtime overlay files in the agents repo (e.g. runtimes/claude-code/<slug>.yaml for model, tool whitelist, permission mode) A second runtime is added, or a persona genuinely needs a non-default executor setting
Dispatch selection (scope-match / LRU / etc.) Instances are added and a type-level task needs a policy for which instance gets it
Concurrency rules around shared agent state State tracking is added
Brain-managed task lifecycle states (in_progress, ready_for_review, etc.) You need to query agent-task state without going through Gitea, or workflow logic genuinely needs to be centralized rather than per-repo

Each extension is additive — none break the v1 schema.

Technical feasibility (verified)

  • Gitea API: GET /api/v1/repos/{owner}/{repo}/contents/{path} for file fetches; per-repo / per-org webhooks with HMAC-signed payloads
  • Frontmatter + markdown parsing: standard Go YAML library + a frontmatter splitter; persona format is a strict subset (only name and description keys)
  • Sync idempotency: upsert keyed by (organization_id, slug) handles webhook + cron racing each other
  • Soft-delete via is_active: keeps task assignments referentially valid when a persona is removed from the repo

What changed from ADR-0004

  • Source of truth for agent definitions: moved from taskbase/agents/<slug>/ (inside the taskbase repo) to per-organization <org-slug>-agents repos in Gitea
  • Agent instances + persistent session_id resumption: removed from v1 (deferred — see table above). Each task runs as a fresh process; cross-task continuity is not a v1 concern
  • Brain-managed task lifecycle states: removed. The brain tracks queued vs dispatched only; in_progress / ready_for_review / done are not concepts at the agent layer. Taskbase's existing follow-Gitea sync continues to mirror issue/PR state for any Gitea-linked task, independent of the agent system
  • Dispatch role: the brain is now a pure priority dispatcher. It does not route to reviewer agents, decide when work is finished, or replicate per-repo workflow rules — those live in the target repo's settings and CLAUDE.md, read by the agent at runtime
  • Runtime coupling: Option D's agent.yaml carried Claude-Code-specific fields (allowed_tools, default_permission_mode, MCP allowlist). v1 personas carry only neutral role definition; runtime-specific config is an executor-side concern with safe defaults

skatzi-agents reference repository

The repository at /Users/dunk/repos/skatzi/skatzi-agents/ is the canonical agent fleet for the Skatzi-internal Taskbase organization. Once Taskbase implements the sync, this repo will be pushed to https://gitea.prod.skatzi.com/skatzi/skatzi-agents and become the source agent_types are derived from for that org.