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:
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¶
- The brain is a priority dispatcher, not a state tracker. It does not maintain
in_progress/ready_for_review/donelifecycle 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. - 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. - Workers are first-class API clients of the brain. Any agent worker can
PATCH /tasks/{id}(update its own task) orPOST /tasks(create follow-up work assigned to anotheragent_type— e.g., "review PR #N"). Multi-agent coordination flows through the queue, not through bespoke channels. - Runtime-agnostic at the brain layer. The brain stores no Claude-Code-specific concepts. The current Claude Code executor (
claude -psubprocess 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 theagent_typestable 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>-agentsin Gitea to add some" - Personas removed from the repo become
is_active=falserather 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
*-agentsrepos - 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
nameanddescriptionkeys) - 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>-agentsrepos in Gitea - Agent instances + persistent
session_idresumption: 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
queuedvsdispatchedonly;in_progress/ready_for_review/doneare 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.yamlcarried 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.
Links¶
- ADR-0002 — Task Management App with Agent Integration
- ADR-0004 — Taskbase Agent Module (superseded by this ADR)
- Taskbase API architecture
- Taskbase deployment guide
- Taskbase agent architecture — needs follow-up to reflect this v1 scope
- Taskbase ↔ Gitea integration — agent-types sync extends this surface