Files
correx/docs/decisions/adr-0012-task-tracking.md
T
kami 0c6ac903b7 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>
2026-06-23 19:00:58 +00:00

5.9 KiB

name, description, depth, links
name description depth links
Adr 0012 Task Tracking Native task tracking as an event-sourced aggregate; tasks are a first-class work-graph node 2
../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 TaskCreatedEvents 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).