feat(tasks): native task tracking — aggregate, agent tools, context bundle

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>
This commit is contained in:
2026-06-23 19:00:58 +00:00
parent 7909357747
commit 0c6ac903b7
51 changed files with 2431 additions and 2 deletions
+112
View File
@@ -0,0 +1,112 @@
---
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).