tatara-operator¶
The central component of the tatara platform. A controller-runtime Kubernetes operator that reconciles the platform CRDs, drives the agentic development lifecycle, receives SCM and Grafana webhooks, provisions per-project memory stacks, gates spend, supervises deploys, and enforces the security model.
Repository: github.com/szymonrychu/tatara-operator
What it does¶
- Reconciles four CRDs with a controller-runtime manager:
Project,Repository,Task, andQueuedEvent. A fifth CRD,Subtask, is a data-only object written and read over the REST API (the agent self-planning ledger); it has no reconciler.WorkItemis not a CRD - it is a plain Go struct that backs theTask.Status.WorkItemsproject-level work-item ledger. - Receives HMAC-verified GitHub and GitLab webhooks and bearer-verified Grafana alert webhooks on a shared HTTP listener
- Provisions per-project memory stacks (CNPG Postgres + Neo4j + LightRAG + tatara-memory service)
- Schedules repo-ingest jobs (
tatara-memory-repo-ingester) on push and on cron - Admits queued work against per-project concurrency and token-budget gates, then spawns
tatara-claude-code-wrapperpods for agent turns - Drives the turn loop: submits prompts, receives callbacks, transitions task states
- Writes results back to the SCM: opens PRs, posts comments, applies labels, merges on approval
- Supervises the post-merge push-CD cascade to a
tatara-helmfileapply - Reaps orphaned pods and GCs terminal Tasks and stale conversation transcripts
- Exposes an OIDC-gated REST API (used by tatara-cli and agent pods)
Listener ports¶
The manager binds four separate addresses. Only the public HTTP listener is routed through the ingress.
| Bind | Env / default | Serves |
|---|---|---|
| Public HTTP | HTTP_ADDR :8080 | SCM + Grafana webhooks and the OIDC-gated REST API (tatara-cli, agent pods) |
| Metrics | METRICS_ADDR :9090 | Prometheus /metrics |
| Health | HEALTH_ADDR :8081 | /healthz, /readyz |
| Internal callback | INTERNAL_ADDR :8082 | Agent turn-complete callbacks (in-cluster only) |
Layout¶
cmd/manager/ # controller-runtime entrypoint + wiring
api/v1alpha1/ # CRD types: Project/Repository/Task/QueuedEvent/Subtask (+ WorkItem struct)
internal/controller/ # reconcilers + turn loop + writeback + queue dispatcher + reaper
internal/agent/ # agent Pod/Service builder + turn session/callback
internal/ingest/ # repo-ingest Job builder
internal/memory/ # per-project memory stack builders
internal/budget/ # token-budget admission gate
internal/scm/ # GitHub/GitLab clients + provider registry
internal/restapi/ # OIDC-gated CRUD REST API
internal/webhook/ # HMAC-verified SCM + bearer-verified Grafana webhook server
internal/auth/ # OIDC verifier + client-credentials token source
internal/config/ # env-scalar config
internal/obs/ # JSON slog + Prometheus metrics
charts/tatara-operator/ # cluster-agnostic Helm chart + CRDs
Reconciliation model¶
Each controller reconciles its resource type independently. The most complex is the Task reconciler, which drives a state machine across the full lifecycle of an agent session. Two orthogonal state fields are in play and it is important not to conflate them (see Dual Phase / LifecycleState below).
Admission happens first, on the QueuedEvent:
stateDiagram-v2
[*] --> Queued: QueuedEvent created
Queued --> Admitted: pool slot free + budget gates pass
Admitted --> [*]: Task created A single-shot task kind (implement, review, brainstorm, incident, triageIssue, selfImprove, refine) then runs a pod-backed Status.Phase machine:
stateDiagram-v2
[*] --> Planning
Planning --> Running
Running --> Succeeded
Running --> Failed
Running --> Deploying: implement PR merged
Deploying --> Succeeded: helmfile applied
Deploying --> Failed: deploy-timeout (parks recoverable) issueLifecycle tasks are the exception: one durable long-lived Task spans the whole issue-to-deploy arc (Triage, Conversation, Implement, MRCI, Merge, Deploy). These Tasks leave Status.Phase empty for their whole life and signal progress and completion through Status.LifecycleState.
Dual Phase / LifecycleState¶
Task.Status.Phase and Task.Status.LifecycleState are two distinct fields with different owners:
Status.Phase(bare stringsPlanning/Running/Succeeded/Failed/Deploying) is the pod-backed execution phase used by single-shot kinds.Deployingis the pod-less post-merge deploy-supervision phase and is deliberately not terminal.Status.LifecycleState(Triage/Conversation/Implement/MRCI/Merge/Deploying/Done/Stopped/Parked) is the issue-level state carried byissueLifecycleTasks, which keepStatus.Phaseempty by design.
Terminality is therefore a function of both fields: a Task is terminal when Phase is Succeeded/Failed or LifecycleState is Done/Stopped/Parked. Auditing issueLifecycle Tasks by Phase alone reports every one of them as non-terminal churn, which is wrong. The TaskTerminal helper is the source of truth; controller code and any external audit must use it rather than testing Phase directly.
Queue admission and concurrency¶
Agent work is not spawned directly from a webhook. Producers stash a QueuedEvent (class normal or alert) and an in-operator dispatcher admits events against per-project pool capacity, so a burst of issues cannot fan out into unbounded concurrent agent pods.
| Pool | Class | Capacity source | Default |
|---|---|---|---|
| Normal | normal | spec.queue.capacity, else spec.maxConcurrentTasks, else 3 | 3 |
| Alert | alert | spec.queue.alertCapacity | 1 |
Over-capacity events wait in Queued and are admitted when a slot frees; the alert pool has reserved slots so an incident is never starved by a backlog of normal work.
maxConcurrentTasks: 0 fully pauses a Project
A zero value (the field's zero value, whether unset or set to 0) is a hard pause: the dispatcher admits no work of either class, so no agent pod, brainstorm, or incident Task is created while the Project sits at 0. A positive value sets normal-pool concurrency. This is the operational kill switch for a runaway or a maintenance window.
Token conservation and budget gates¶
The operator has three independent spend controls, from coarsest to finest:
- Concurrency cap -
spec.maxConcurrentTasks(above). Bounds how many agent pods can run at once. - Per-Task token ceiling -
spec.agent.maxTaskTokens. A cumulative output-token ceiling per Task; when a Task crosses it the operator terminates the pod and marks the Task failed with reasonTokenBudgetExceeded. This is the runaway backstop, independent of the admission gate. - Per-Project token-budget admission gate -
spec.tokenBudgetplus operator env. When enabled, the dispatcher pauses admission before spawning new work once the Project crosses a usage threshold within a window. Two thresholds:proactivePercentpauses the normal pool (brainstorm/implement/review/...),emergencyPercentpauses the alert pool (incidents) so incidents keep running longer than routine work.
The gate has two modes, selected by the TOKEN_BUDGET_MODE operator env (default customWindow):
| Mode | Meters against |
|---|---|
customWindow | The operator's own per-turn token accounting, reset on TOKEN_BUDGET_RESET_SCHEDULE (cron) |
claudeSubscription | The Claude Code 5-hour and weekly usage windows |
What is actually enforced today
The per-Task maxTaskTokens ceiling and the concurrency cap are the always-on controls. The tokenBudget admission gate is present and functional but ships inert unless a spec.tokenBudget block and a token limit are configured; it is off across the reference fleet at the time of writing. See operations/tuning.md for the current fleet posture and the honest scope of what is deployed.
Deploy supervision (push-CD)¶
An implement Task does not go terminal when its PR merges. It enters Deploying and the operator drives the post-merge push-CD cascade: it tracks the artifact through CascadeStage (tagged -> parent-pr-open -> parent-merged -> helmfile-applied) toward a terminal tatara-helmfile apply, then resolves the Task Succeeded (or Done for issueLifecycle) and closes the originating issue. A wall-clock deadline of now + spec.deployBudgetSeconds (with a spec.deploySingleHopBudgetSeconds per-artifact override) bounds the wait; on exceed the Task parks recoverable with reason deploy-timeout. This is how a merged change is followed all the way to running in the cluster rather than being assumed deployed at merge.
Reaper and GC¶
A background sweep keeps state bounded: it reaps orphaned agent pods (pods whose owning Task is gone or terminal, after a grace period), GCs terminal Tasks, and GCs stale conversation transcripts. Each path is metered (operator_orphan_reaped_total, operator_tasks_gc_total, operator_conversation_gc_total) so leaks are visible.
Leader election and metrics¶
The operator runs multi-replica with leader election. Metrics that can only be observed on the leader (reconcile state, queue depth) are exported with sum by() / max by() aggregates so Prometheus correctly handles the non-leader replicas reporting zero.
Helm chart¶
The chart at charts/tatara-operator/ is cluster-agnostic. Cluster-specific configuration (ingress host, storage class, imagePullSecrets, OIDC URLs) comes from the tatara-helmfile values files.
The chart packages both the operator itself and charts/tatara-project/ as a sibling chart. The tatara-project chart templates Project and Repository CRs declaratively from helmfile values (replacing raw YAML presync manifests).
CRD ownership¶
CRDs are templated in the chart and applied via helm upgrade. On initial install or first upgrade, pre-existing CRDs need a one-time ownership annotation (helm.sh/resource-policy: keep + managed-by Helm annotations) before the chart can adopt them.
Key configuration¶
Operator configuration is env scalars. The webhook signing secrets are not operator env: they are read per-Project from Kubernetes Secrets referenced by the Project CR (see the note below).
| Env / Value | Description |
|---|---|
OIDC_ISSUER | Keycloak issuer URL |
OIDC_AUDIENCE | Expected audience in bearer tokens from agent pods |
S3_BUCKET | Conversation persistence bucket (empty disables the feature; off by default) |
TOKEN_BUDGET_MODE | customWindow (default) or claudeSubscription |
TOKEN_BUDGET_RESET_SCHEDULE | Cron for the customWindow reset |
LOG_LEVEL | debug/info/warn/error |
Webhook secrets are per-Project, not operator env
There is no global WEBHOOK_SECRET or GRAFANA_WEBHOOK_SECRET. The SCM HMAC secret is read from the Secret named by Project.spec.scmSecretRef (key webhookSecret); the Grafana alert-webhook bearer secret is read from Project.spec.grafana.secretRef (key webhookSecret). Each Project supplies its own.
Metrics¶
The operator exposes roughly sixty Prometheus series on :9090/metrics. A representative subset:
| Metric | Type | Labels | Description |
|---|---|---|---|
operator_reconcile_total | counter | controller, result | Reconcile counts by controller and result |
operator_task_turns_total | counter | project, repo, kind, issue | Agent turns completed |
operator_task_tokens_total | counter | project, repo, kind, issue, model, type | Agent token usage (input/output/cache) |
operator_task_terminal_total | counter | kind, phase, reason | Tasks reaching a terminal phase |
operator_queue_depth | gauge | project, class | Queued (not-yet-admitted) events per pool |
operator_queue_inflight | gauge | project, class | Admitted in-flight events per pool |
operator_admission_blocked_total | counter | project, class, reason | Events the gate declined to admit (token_budget, project_paused) |
operator_token_budget_used_ratio | gauge | project, scope | Budget usage vs the window limit |
operator_webhook_events_total | counter | provider, kind, action, result | Webhook events |
operator_ingest_job_total | counter | result, mode | Finished ingest Jobs by result and mode |
operator_scm_writes_total | counter | - | SCM writeback operations |
operator_turn_timeout_total | counter | - | Turns aborted on inactivity timeout |
operator_orphan_reaped_total | counter | - | Orphaned pods reaped |
operator_review_outcome_total | counter | project, repo, model, verdict | Review verdicts, keyed by review model |
operator_implement_ci_total | counter | - | Implement-PR CI outcomes (quality feedback) |