Build your own Claude Code
Plan mode & tasks·25 min

Task tracking

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage tasks --watch

A real Claude Code session on a big change doesn't hold the whole plan in its head — it writes a task list and works it down. "Add the migration" blocks "wire the route," which blocks "update the docs." The agent creates tasks, flips them pending → in_progress → completed, and records what's blocked by what so it never starts step three before step two lands. This step builds that store: a tiny task tracker with a dependency graph, driven by a stream of operations.

The model is small and exact. A task has an id, a subject, a status (pending | in_progress | completed), and two edge lists: blocked_by (tasks that must finish first) and blocks (tasks waiting on this one). Ids are creation-ordered — the first task created is task-1, the second task-2, and so on — so a create op never carries an id; the store assigns it. Operations arrive as a list and you apply them in order, mutating one shared state.

Ops in order, state out
{ op: "create", subject: "add migration" }              -> task-1  (pending)
{ op: "create", subject: "wire route" }                  -> task-2  (pending)
{ op: "update", id: "task-2", blocked_by: ["task-1"] }   -> task-2.blocked_by=[task-1], task-1.blocks=[task-2]
{ op: "update", id: "task-1", status: "completed" }      -> task-1  (completed)

create assigns ids; an edge added on one side mirrors onto the other.

What to build

Your task

Implement runTaskOps

Implement runTaskOps(ops): fold the operation list into a TaskState and return it. Handle two ops. create appends a new task with the next creation-ordered id (task-1, task-2, …), the given subject, status pending, and empty edge lists. update looks up the task by id and applies any provided fields: a new status, and/or blocked_by edges. When you set A.blocked_by = [B], mirror it: add A to B.blocks so the graph stays symmetric. Apply ops strictly in order and return the final state — the full task list with its dependency edges.

src/tasks.ts
type Status = 'pending' | 'in_progress' | 'completed'

interface Task {
  id: string
  subject: string
  status: Status
  blocked_by: string[]
  blocks: string[]
}

interface TaskState {
  tasks: Task[]
}

type TaskOp =
  | { op: 'create'; subject: string }
  | { op: 'update'; id: string; status?: Status; blocked_by?: string[] }

export function runTaskOps(ops: TaskOp[]): TaskState {
  // 1. fold ops in order into one TaskState
  // 2. create -> next id `task-${n}`, status 'pending', empty edges
  // 3. update -> set status; for each blocked_by edge, mirror onto the other task's `blocks`
}
Edges are two-sided — mirror them

The graph is undirected in storage even though the dependency is directional. Every time you record A.blocked_by = [B] you must also push A onto B.blocks; otherwise task-1 claims nothing depends on it while task-2 insists it's blocked, and the grader's exact graph compare fails. Mirror on write, and don't add a duplicate edge if it's already there.

How it's graded

The grader runs ./your_program.sh tasks, feeds a fixed op stream through runTaskOps, and prints the final TaskState as JSON. It compares your task list against the reference exactly — every id, subject, status, and both sides of every edge. A missing mirrored blocks entry, a wrong id, or a status that didn't flip all fail the compare.

How the grader runs it
./your_program.sh tasks
# ops: [create "a", create "b", update task-2 blocked_by [task-1]]
# -> {"tasks":[
#      {"id":"task-1","subject":"a","status":"pending","blocked_by":[],"blocks":["task-2"]},
#      {"id":"task-2","subject":"b","status":"pending","blocked_by":["task-1"],"blocks":[]}
#    ]}