--- name: "Adr 0012 Task Tracking" description: "Native task tracking as an event-sourced aggregate; tasks are a first-class work-graph node" depth: 2 links: ["../index.md", "./adr-0001-event-sourcing.md", "./adr-0005-persistence-choice.md", "./adr-0006-event-store-append-only.md"] --- # ADR 0012: native task tracking as an event-sourced aggregate **status:** accepted **date:** 23.06.2026 (June) **deciders:** correx design team --- ## context Correx is to gain **native task tracking** — a durable "task" (work item / issue) entity, agent-first. Today the system has no concept of a task; it has `sessions` (agent runs) and session/stage-scoped `artifacts`. A task is conceptually different: a durable unit of intent that outlives any single session and *references* the artifacts and sessions that act on it. An originating pitch proposed storing tasks as YAML/markdown files as the **source of truth** with Postgres as a cache, plus a `GET /tasks/next` assignment scheduler. Those technical choices are **rejected** here: they contradict correx's foundational invariants — "the event log is the only source of truth; projections are disposable and rebuilt from events" (`.correx/project.toml`, ADR-0001, ADR-0006). Adopting hand-edited files as truth would create two masters (files vs. log) and forfeit replay/idempotency. We keep the *goal* (native tasks) and realise it with correx's actual architecture. This ADR covers the **foundational slice**: the aggregate, its events, and the work-graph edge model. REST/CLI/agent-context-bundle/search are deliberately later phases. ## decision **A task is a first-class event-sourced aggregate**, modelled on the existing `core:sessions` aggregate (Status / State / Reducer / Projector / Repository / Counter), living in a new `core:tasks` module. It is *not* an `ArtifactKind` — artifacts are session/stage-scoped LLM outputs; a task is durable and cross-session. ### 1. event log is the source of truth Tasks are rebuilt by folding events. Any future markdown/file representation is a *disposable projection* (export), never the master copy. ### 2. per-project task stream The `EventStore` is partitioned by `sessionId`. Since `SessionId` and `TaskId` are both `TypeId`, all task events for a project live in one stream keyed `tasks:` (`TaskStreams.forProject`). One read rebuilds the whole board (`TaskBoard = Map`). The `tasks:` prefix keeps these streams distinguishable from real agent sessions; when task streams are later wired into the live server, session-listing consumers filter the prefix. ### 3. lifecycle as facts; status is derived Mirroring sessions (whose events never carry `SessionStatus`), task events are facts (`TaskClaimed`, `TaskBlocked`, `TaskCompleted`, …) and `DefaultTaskReducer` derives `TaskStatus`. This keeps the `TaskStatus` enum out of the lowest module (`core:events`). A `sealed interface TaskEvent { val taskId }` (a marker, *not* part of the serialized `EventPayload` hierarchy) lets projections route any payload to its task without a giant `when`. ### 4. status workflow `TODO → IN_PROGRESS → IN_REVIEW → DONE`, with `BLOCKED` (reversible; unblock returns to IN_PROGRESS if claimed, else TODO) and `CANCELLED` (terminal, reopenable). Illegal transitions are ignored and counted in `TaskState.invalidTransitions` (as `SessionState` does). ### 5. work-graph edges `TaskLinkType` (sibling to the existing `ArtifactRelationshipType`): `DEPENDS_ON, BLOCKS, IMPLEMENTS, RELATES_TO, PRODUCED, CONTEXT`. A link target is a plain id string — another task, an `ArtifactId`, or a `SessionId` — plus a type. This is the work-graph backbone the agent-context bundle (later phase) will traverse. ### 6. human-friendly ids `TaskCounterProjection(projectId)` counts `TaskCreatedEvent`s in the project stream; the numeric suffix of a key (`-`, e.g. `auth-142`) is the count of *created* tasks, so ids never collide even after cancellation. The creating caller composes the key and stores it in `TaskCreatedEvent.key`. (The creating service is a later phase; the projection and `key` field land now.) ## consequences **positive:** * reuses the entire event-sourcing backbone (sequencing, idempotency, replay, projections); no new persistence model, no second source of truth * the work graph (tasks ↔ tasks/artifacts/sessions) is native, enabling later "everything touching X" queries and the agent-context bundle * deterministic, replayable task state; status derivation is unit-testable in isolation **negative:** * overloading the `sessionId`-partitioned stream key with `tasks:` means session-enumerating consumers must learn to filter the prefix once tasks go live (acceptable; isolated to the live-wiring phase) * per-project (not per-task) stream means rebuilding one task replays the project's task events — fine at expected volumes; revisit with snapshots if a project's task log grows large ## alternatives considered * **Task as an `ArtifactKind`:** rejected — artifacts are session/stage-scoped outputs; tasks are durable, cross-session, and carry their own lifecycle/claiming. * **YAML/markdown files as source of truth + Postgres cache (the pitch):** rejected — violates ADR-0001/0006; creates two masters and loses replay/idempotency. * **`GET /tasks/next` assignment scheduler (the pitch):** dropped — scheduling/prioritisation is a separate hard problem; v1 offers `claim` (pull), not `assign` (push). * **One stream per task:** cleaner per-task sequencing but pollutes session enumeration with one stream per task and makes board/list queries fan out across many streams; the per-project stream is the better default for a tracker that lists/boards constantly. ## status Governs the foundational task-tracking slice. Re-evaluation expected when the REST surface, the `GET /tasks/{id}/context` agent bundle, search, and git-driven auto status updates are designed (those phases build on, and must not contradict, this ADR).