tb_update
Update a task's status or append timestamped notes. Transition to 'done' requires prior status of 'in_progress' or 'in_review'.
Instructions
Update a task's status and/or append notes. Moving to 'done' requires 'in_progress' or 'in_review'. Notes are stored as timestamped entries.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| task_id | Yes | Task ID | |
| status | No | New status (omit to keep current status and only add notes) | |
| notes | No | Notes to append (timestamped). Works with or without status change. |
Implementation Reference
- src/tools/task-board.ts:255-363 (handler)The handler function for the 'tb_update' tool. Updates a task's status and/or appends timestamped notes. Validates transitions to 'done' (requires 'in_progress' or 'in_review'), auto-unblocks dependent tasks when a task is completed.
server.tool( "tb_update", "Update a task's status and/or append notes. Moving to 'done' requires 'in_progress' or 'in_review'. Notes are stored as timestamped entries.", { task_id: z.string().max(256).describe("Task ID"), status: z.enum(TASK_STATUSES).optional().describe("New status (omit to keep current status and only add notes)"), notes: z.string().max(65536).optional().describe("Notes to append (timestamped). Works with or without status change."), }, async ({ task_id, status, notes }) => { const db = getDb(); const task = db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(task_id) as Record<string, unknown> | undefined; if (!task) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Task not found" }) }] }; } const now = new Date().toISOString(); const effectiveStatus = status ?? task.status as string; // Validate done transition if (status === "done") { if (task.status !== "in_progress" && task.status !== "in_review") { return { content: [ { type: "text" as const, text: JSON.stringify({ error: `Cannot move to 'done' from '${task.status}'. Must be 'in_progress' or 'in_review' first.` }), }, ], }; } } // Append notes if provided let notesCount: number | undefined; if (notes) { const existing = JSON.parse((task.notes as string) || "[]") as Array<{ text: string; timestamp: string }>; existing.push({ text: notes, timestamp: now }); db.prepare(`UPDATE tasks SET notes = ?, updated_at = ? WHERE id = ?`).run( JSON.stringify(existing), now, task_id ); notesCount = existing.length; } // Update status if provided if (status) { const updates: Record<string, unknown> = { status, updated_at: now }; if (status === "done") { updates.completed_at = now; } const setClauses = Object.keys(updates).map((k) => `${k} = ?`).join(", "); db.prepare(`UPDATE tasks SET ${setClauses} WHERE id = ?`).run( ...Object.values(updates), task_id ); // Auto-unblock dependent tasks if (status === "done") { const dependents = db .prepare(`SELECT id, dependencies FROM tasks WHERE board_id = ? AND status = 'backlog'`) .all(task.board_id as string) as Record<string, unknown>[]; const unblocked: string[] = []; for (const dep of dependents) { const depIds = JSON.parse(dep.dependencies as string) as string[]; if (depIds.includes(task_id)) { const remaining = depIds.filter((d) => d !== task_id); if (remaining.length === 0) { db.prepare(`UPDATE tasks SET status = 'ready', updated_at = ? WHERE id = ?`).run(now, dep.id); unblocked.push(dep.id as string); } } } return { content: [ { type: "text" as const, text: JSON.stringify({ updated: true, task_id, status: effectiveStatus, unblocked_tasks: unblocked, notes_count: notesCount, }), }, ], }; } } return { content: [ { type: "text" as const, text: JSON.stringify({ updated: true, task_id, status: effectiveStatus, notes_count: notesCount, }), }, ], }; } ); - src/tools/task-board.ts:258-262 (schema)Input schema for tb_update: task_id (string, required), status (enum from TASK_STATUSES, optional), notes (string, optional).
{ task_id: z.string().max(256).describe("Task ID"), status: z.enum(TASK_STATUSES).optional().describe("New status (omit to keep current status and only add notes)"), notes: z.string().max(65536).optional().describe("Notes to append (timestamped). Works with or without status change."), }, - src/tools/task-board.ts:255-363 (registration)Registration of the 'tb_update' tool via server.tool() within registerTaskBoardTools(), called from src/server.ts line 19.
server.tool( "tb_update", "Update a task's status and/or append notes. Moving to 'done' requires 'in_progress' or 'in_review'. Notes are stored as timestamped entries.", { task_id: z.string().max(256).describe("Task ID"), status: z.enum(TASK_STATUSES).optional().describe("New status (omit to keep current status and only add notes)"), notes: z.string().max(65536).optional().describe("Notes to append (timestamped). Works with or without status change."), }, async ({ task_id, status, notes }) => { const db = getDb(); const task = db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(task_id) as Record<string, unknown> | undefined; if (!task) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Task not found" }) }] }; } const now = new Date().toISOString(); const effectiveStatus = status ?? task.status as string; // Validate done transition if (status === "done") { if (task.status !== "in_progress" && task.status !== "in_review") { return { content: [ { type: "text" as const, text: JSON.stringify({ error: `Cannot move to 'done' from '${task.status}'. Must be 'in_progress' or 'in_review' first.` }), }, ], }; } } // Append notes if provided let notesCount: number | undefined; if (notes) { const existing = JSON.parse((task.notes as string) || "[]") as Array<{ text: string; timestamp: string }>; existing.push({ text: notes, timestamp: now }); db.prepare(`UPDATE tasks SET notes = ?, updated_at = ? WHERE id = ?`).run( JSON.stringify(existing), now, task_id ); notesCount = existing.length; } // Update status if provided if (status) { const updates: Record<string, unknown> = { status, updated_at: now }; if (status === "done") { updates.completed_at = now; } const setClauses = Object.keys(updates).map((k) => `${k} = ?`).join(", "); db.prepare(`UPDATE tasks SET ${setClauses} WHERE id = ?`).run( ...Object.values(updates), task_id ); // Auto-unblock dependent tasks if (status === "done") { const dependents = db .prepare(`SELECT id, dependencies FROM tasks WHERE board_id = ? AND status = 'backlog'`) .all(task.board_id as string) as Record<string, unknown>[]; const unblocked: string[] = []; for (const dep of dependents) { const depIds = JSON.parse(dep.dependencies as string) as string[]; if (depIds.includes(task_id)) { const remaining = depIds.filter((d) => d !== task_id); if (remaining.length === 0) { db.prepare(`UPDATE tasks SET status = 'ready', updated_at = ? WHERE id = ?`).run(now, dep.id); unblocked.push(dep.id as string); } } } return { content: [ { type: "text" as const, text: JSON.stringify({ updated: true, task_id, status: effectiveStatus, unblocked_tasks: unblocked, notes_count: notesCount, }), }, ], }; } } return { content: [ { type: "text" as const, text: JSON.stringify({ updated: true, task_id, status: effectiveStatus, notes_count: notesCount, }), }, ], }; } ); - src/types/index.ts:71-83 (helper)TASK_STATUSES constant used as zod enum for the 'status' parameter in tb_update's schema.
export const TASK_STATUSES = [ "backlog", "ready", "in_progress", "in_review", "done", "blocked", ] as const; export type TaskStatus = (typeof TASK_STATUSES)[number]; export const TASK_PRIORITIES = ["p0", "p1", "p2", "p3"] as const; export type TaskPriority = (typeof TASK_PRIORITIES)[number];