0c6ac903b7
Adds task tracking as a first-class event-sourced aggregate (ADR-0012). The
event log stays the only source of truth; the board is a projection, and all of
a project's task events live in one stream (tasks:<projectId>).
core:
- new core:tasks module mirroring core:sessions: TaskStatus/State/Reducer/
BoardProjector/CounterProjection/Repository + TaskService write path that
appends events via the existing EventStore (no new storage)
- task events in core:events (sealed marker TaskEvent), registered in
Serialization.kt; status is derived by the reducer from lifecycle facts
- work-graph edges: TaskLinkType + typed TaskTargetKind on the link, so bundle
resolution dispatches on the recorded kind instead of guessing from the id
- delete is a soft tombstone (TaskDeletedEvent); cancel stays a visible outcome
surfaces:
- agent tools (Tier T2 mutators, T1 read): task_create / task_update /
task_delete / task_context, registered in both the main and per-workspace
tool registries
- REST GET /tasks/{id}/context for external agents
context bundle (TaskContextAssembler):
- task fields + acceptance criteria + relevant files + resolved dependency
tasks (live status) + raw related links + notes, with a compact render()
- semantic enrichment via a TaskKnowledgeRetriever port bridged to the existing
L3 retriever (late-bound holder for composition-root ordering)
- ADR/doc resolution via a TaskDocumentResolver port + filesystem adapter
(docs/decisions/adr-*.md, padding-agnostic; *.md paths)
Unit-verified across core:tasks / infrastructure:tools / apps:server; full
gradlew check green. Not yet live-QA'd end-to-end against a running server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
5.9 KiB
Markdown
113 lines
5.9 KiB
Markdown
---
|
|
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:<projectId>`
|
|
(`TaskStreams.forProject`). One read rebuilds the whole board (`TaskBoard = Map<TaskId,
|
|
TaskState>`). 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 (`<prefix>-<n>`, 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:<projectId>` 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).
|