AW-02 — Task board · Task breakdown
Ordered, executable tasks derived from
plan.md. Each carries explicit file paths and a definition of done. Every task ships with its tests (Constitution VI). Work top to bottom; tasks marked(parallel)may run alongside the task immediately above them.
Feature ID: aw-02-task-board
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-06
How to use
- All paths are repo-relative to the monorepo root.
- Run everything with
pnpmfrom the root unless a task says otherwise. - No task in this breakdown adds an entity, a table, a column or a migration. If
a task appears to need one, stop and re-read
plan.md§3 — the signal it wants already exists. The single exception is deferred to T39 and gated on a product decision. - Add new tasks at the bottom; never renumber.
- Commit style:
feat(tasks): …,test(tasks): …,chore(i18n): ….
The three standing constraints
Re-read these before every task. They are what makes this epic additive.
TasksKanbanView.tsxkeeps its export and its props. Every existing caller —TasksList,TasksScopedSection, and through them/tasks,/missions/[id]/tasks,/works/[id]/tasks,/ideas/[id]/tasks— must keep compiling and keep working with no change on their side.GET /api/tasksdoes not change. Its existing controller specs must pass untouched. Any diff in them means an additive promise was broken.- Every new default that changes what a user sees has a one-action toggle back. Board-as-landing-view, top-level-only, templates-out-of-columns.
PHASE 1 — Make the board true
P1.A — Pure domain logic (no I/O, no framework)
-
T1. Column tables and drop resolution. - Create
packages/agent/src/tasks-domain/task-board-columns.tsexporting: -export type BoardLayout = 'status' | 'focus';-export interface BoardColumnDef { key: string; statuses: readonly TaskStatus[]; terminal: boolean; }-STATUS_COLUMNS: readonly BoardColumnDef[]— seven entries, one status each, in the orderbacklog, todo, in_progress, in_review, blocked, done, cancelled. -FOCUS_COLUMNS: readonly BoardColumnDef[]—backlog(backlog,todo),in_flight(in_progress),needs_you(in_review,blocked),done(done), plus acancelledentry marked as toggle-only. -columnsFor(layout, opts: { includeCancelled: boolean }): BoardColumnDef[]-columnForStatus(layout, status): string-resolveDrop(from: TaskStatus, column: BoardColumnDef, allowed: Record<TaskStatus, TaskStatus[]>): { kind: 'apply'; to: TaskStatus } | { kind: 'ask'; options: TaskStatus[] } | { kind: 'refuse' }- No imports from TypeORM, NestJS, or any service.TaskStatusis imported as a type only. - Add a file-header comment stating the invariant: everyTaskStatusvalue appears in exactly one column of each layout; a column is never derived from anything but status. - Test:packages/agent/src/tasks-domain/__tests__/task-board-columns.spec.ts- Both layouts cover all seven statuses exactly once (assert by set equality againstObject.values(TaskStatus), so adding a status to the enum without adding a column fails the suite). -resolveDropover the full 7 × 5 matrix, asserting the verdict for every pair. -in_progress → needs_youis the onlyaskin the whole matrix. - Every drop out ofcancelledisrefuse. - Done when:pnpm --filter @ever-works/agent test task-board-columnsis green and the file imports nothing outsideentities/task.entity. -
T2. The stall predicate. (parallel with T1)
- Create
packages/agent/src/tasks-domain/task-board-stall.tsexporting:export const DEFAULT_STALL_AFTER_DAYS = 2;clampStallAfterDays(value: number | null | undefined): number— 1..30, default 2.stallCutoff(now: Date, days: number): DateisStalled(input: { status; latestRunStatus; updatedAt; now; stallAfterDays }): booleanexactly asplan.md§2.4.
- Add the header comment stating what the predicate costs: it derives "no
progress" from
updatedAt, so an unrelated edit clears the flag. It under-reports and never over-reports, and that asymmetry is why no new column is stored. - Test:
.../__tests__/task-board-stall.spec.ts— 47 h vs 49 h at the default threshold;latestRunStatusofqueuedandrunning(not stalled) vs each terminal value andnull(stalled); every non-in_progressstatus (never stalled); the clamp at0,1,30,31,null,undefined,NaN. - Done when: green, and the file has no framework import.
- Create
-
T3. Provenance ordering. (parallel with T1)
- Create
packages/agent/src/tasks-domain/task-board-provenance.tsexporting:export type ProvenanceKind = 'trigger' | 'recurringTemplate' | 'scheduled' | 'mission' | 'idea' | 'work' | 'team' | 'goal' | 'agent' | 'raisedByAgent' | 'delegated' | 'creator';PROVENANCE_PRECEDENCE: readonly ProvenanceKind[]in spec FR-28's order.orderProvenance(entries: ProvenanceEntry[]): ProvenanceEntry[]— sorted by precedence, with entries whosenameisnulldropped (spec FR-29).
- Test:
.../__tests__/task-board-provenance.spec.ts— all twelve present returns all twelve in precedence order; an unresolvable name is dropped, not rendered as an id; an empty input returns empty. - Done when: green.
- Create
-
T4. Export the new module surface.
- Modify
packages/agent/src/tasks-domain/index.ts: re-export T1, T2 and T3. - Done when:
apps/apiandapps/webcan both import them, andpnpm --filter @ever-works/agent buildis clean.
- Modify
P1.B — Filter options (additive, no schema)
- T5. Three optional fields on
ListTasksFilter.- Modify
packages/agent/src/database/repositories/task.repository.ts:- Widen
parentTaskId?: stringtoparentTaskId?: string | 'none'. - Add
isRecurring?: boolean. - Add
orderBy?: 'updatedAt' | 'priorityThenUpdated' | 'stalledThenPriority'. - Add
stallCutoff?: Date(only read whenorderByis'stalledThenPriority').
- Widen
- In
list():parentTaskId === 'none'→andWhere('task.parentTaskId IS NULL'); a uuid keeps today's meaning;undefinedadds no predicate.isRecurringdefined →andWhere('task.isRecurring = :isRecurring', …);undefinedadds no predicate.- Replace the single
qb.orderBy('task.updatedAt', 'DESC')with a switch whoseundefinedand'updatedAt'branches emit the identical clause. 'stalledThenPriority'emits the three-keyORDER BYofplan.md§3.2.
- Add the comment explaining why
task.priority ASCis correct: the column is avarchar(4)holdingp0–p4, so lexicographic order is priority order. Without that note it reads as an accident and someone will "fix" it. - Test:
.../__tests__/task-repository-board-filters.spec.ts— assert the generated SQL for each option, and one regression case asserting that omitting all four new fields produces the exact SQL the repository produces today. - Done when: green, and every existing
task.repositorytest passes untouched.
- Modify