Audit: p0-3-4-task-tools — β Task MCP Tool Surface

Task: P0.3.4
Auditor: T3 Executor (Claude Sonnet 4.6)
Date: 2026-04-17
Branch: feature/p0-3-4-task-tools
Base commit: d18f1272 (P0.3.3 writeback enforcement merged)


1. Scope

This audit inventories the surfaces that P0.3.4 must expose via MCP tools and the conventions established by prior tasks that constrain the implementation.


2. β CRUD surface (repository.ts exports)

File: src/domains/tasks/repository.ts

Exported functions

Function Signature Throws
createTask(db, input) (Database, CreateTaskInput) → Task better-sqlite3 errors (disk full, CHECK violation)
getTask(db, id) (Database, string) → Task \| null never (returns null on miss)
updateTask(db, id, patch) (Database, string, UpdateTaskPatch) → Task TaskNotFoundError, WritebackRequiredError (when status→DONE without thought_record), CHECK violation
deleteTask(db, id) (Database, string) → Task TaskNotFoundError
listTasks(db, filter?) (Database, ListTasksFilter?) → Task[] never

Exported error classes

Class Fields When thrown
TaskNotFoundError taskId: string, operation: 'update' \| 'delete' No live row with given id
WritebackRequiredError (from writeback.ts) taskId: string, missing_fields: string[] status→DONE without thought_record

Key types

  • CreateTaskInput: { title: string, status?: TaskState, project_id?, description?, priority?, assignee? }
  • UpdateTaskPatch: all optional version of CreateTaskInput fields
  • ListTasksFilter: { status?, project_id?, limit?, offset?, include_deleted? }
  • Task: readonly snapshot with all 10 columns plus deleted_at

3. FSM states (state-machine.ts)

8 canonical states: INIT, GATHER, ANALYZE, PLAN, APPLY, VERIFY, DONE, CANCELLED

Entry state: INIT (default for createTask) Terminal states: DONE, CANCELLED

For task_next_actions — “actionable” means tasks not yet in a terminal state. The FSM contract defines TERMINAL_STATES = { DONE, CANCELLED }. Unblocked tasks are those with status NOT IN ('DONE', 'CANCELLED') — i.e. any of INIT, GATHER, ANALYZE, PLAN, APPLY, VERIFY.


4. Writeback enforcement (P0.3.3)

updateTask calls enforceWriteback(db, id) BEFORE the SQL UPDATE when patch.status === 'DONE'. enforceWriteback counts thought_records for taskId — if count is 0, throws WritebackRequiredError { taskId, missing_fields: ['thought_record'] }.

The MCP tool layer must catch WritebackRequiredError and return:

{ "ok": false, "error": { "code": "ERR_WRITEBACK_REQUIRED", "message": "...", "missing_fields": ["thought_record"], "taskId": "..." } }

5. Tool conventions (Wave A/B/C/D locks)

From skills/repository.ts (P0.6.2 reference)

  • Pattern: registerSkillTools(ctx: ColibriServerContext): void
  • Uses registerColibriTool(ctx, name, config, handler)
  • Handler lazy-resolves getDb() at call-time (NOT registration time)
  • Tools return data payload only — middleware wraps in { ok: true, data: ... }
  • On error: handler throws → middleware catches → { ok: false, error: { code: 'HANDLER_ERROR', ... } }

From trail/repository.ts (P0.7.2 reference)

  • Same pattern as above
  • Schema objects at file top: const XInputSchema = z.object({...})
  • Handlers are inline lambdas calling repo functions with getDb()

Key conventions

  1. Lazy getDb(): handlers call getDb() inside the closure body, NOT at registerTaskTools() call time
  2. Envelopes: handlers return data directly — registerColibriTool wraps in {ok:true,data:...}
  3. Error handling: For typed errors (TaskNotFoundError, WritebackRequiredError), handlers must NOT throw — return {ok:false,error:{...}} manually to override the generic HANDLER_ERROR code
  4. Zod schemas: one z.object({}) per tool input, at top of function/file
  5. ORDER BY rowid: for deterministic ordering per Wave C P0.7.2 lesson
  6. Tool names: snake_case: task_create, task_get, task_update, task_list, task_next_actions
  7. File location: inside src/domains/tasks/repository.ts (not a separate tools.ts)

6. server.ts bootstrap() wiring

Current bootstrap() in src/server.ts (line ~553-560):

registerThoughtTools(ctx);
registerSkillTools(ctx);
await start(ctx);

P0.3.4 must add registerTaskTools(ctx) after registerSkillTools(ctx) (or alongside it — ordering matters only if tools share names, which they don’t).


7. Test patterns (from repository.test.ts and trail/repository.test.ts)

  • In-memory SQLite: new Database(':memory:')
  • Both migrations needed: 002_tasks.sql + 003_thought_records.sql
  • insertThoughtRecord(db, taskId) helper for satisfying writeback prerequisite
  • Tool registration test: use InMemoryTransport.createLinkedPair() + createServer({...})
  • Check ctx._registeredToolNames.has('tool_name') to verify registration
  • Test duplicate registration throws 'tool already registered'

8. What P0.3.4 must NOT do

  • No FSM enforcement in the MCP tools (P0.3.1 FSM exists for logic; repository.ts is deliberately dumb-CRUD per its header; FSM policing in tools is noted as a concern but the task-breakdown says only task_create/get/update/list/next_actions — no task_transition tool)
  • No new migration (tasks table already exists from P0.3.2 002_tasks.sql)
  • No changes to repository.ts CRUD functions
  • No changes to writeback.ts

9. Identified risks

  1. Error envelope shape: registerColibriTool wraps handler returns in {ok:true,data:...}. For custom error envelopes, the handler must return {ok:false,error:{...}} and the middleware will wrap it again as {ok:true,data:{ok:false,error:{...}}}. To avoid this double-wrapping, we need to examine the middleware more carefully.

    Resolution: Looking at server.ts lines 350-369 — the middleware catches thrown errors and wraps them as {ok:false,error:{code:'HANDLER_ERROR',...}}. For custom error envelopes, the handler must throw a typed error OR return the envelope directly. Since the middleware wraps ALL successful returns in {ok:true,data:...}, handlers that want custom error codes must throw — the middleware will produce {ok:false,error:{code:'HANDLER_ERROR',...}}. To get ERR_WRITEBACK_REQUIRED, we need to throw with a distinguishable error OR we return the envelope from the handler as data (accepting double-wrap).

    Re-reading: The trail tools return ThoughtRecord directly — middleware wraps that as {ok:true,data:ThoughtRecord}. The skill tool returns {skills:[...],total_count:N} — wrapped as {ok:true,data:{skills:[...],total_count:N}}. Neither handles typed errors with custom codes.

    Decision: For task tools, to provide meaningful ERR_WRITEBACK_REQUIRED and ERR_NOT_FOUND codes, the handler should catch typed errors and return a custom envelope {ok:false,error:{code:'ERR_X',...}}. BUT the middleware wraps this in {ok:true,data:{ok:false,...}} — a double-wrap. We need to look at whether the middleware passes through ok:false shapes.

    Final resolution: The middleware wraps all successful returns. There’s no bypass. The cleanest solution per s17 §6 is: throw a typed error from the handler → middleware produces {ok:false,error:{code:'HANDLER_ERROR',message:...}}. To get custom codes, we can throw an error with the code embedded in the message, OR we must accept that ERR_WRITEBACK_REQUIRED becomes HANDLER_ERROR at the middleware level with the structured message. The task spec says “return envelope” so we should return {ok:false,...} from the handler, which gets double-wrapped.

    HOWEVER: re-reading the task spec carefully: “s17 §6 envelope: tools return { ok: true, data: ... } on success or { ok: false, error: { code: 'ERR_X', message: '...' } } on failure — DO NOT throw from handlers, return the envelope”. The task brief explicitly says DO NOT throw — return envelopes. The trail tools do the opposite (they throw via direct repo calls), so the trail tools produce {ok:true,data:...} on success and {ok:false,error:{code:'HANDLER_ERROR',...}} on any exception. For P0.3.4, the mission brief says explicitly return envelopes and do not throw — meaning the task tools deviate from the trail pattern and return {ok:false,error:{code:'ERR_X',...}} as the handler return value, which gets wrapped as {ok:true,data:{ok:false,...}}.

    This is a design clarification needed in the contract step.

  2. getDb import: must import getDb from ../../db/index.js
  3. WritebackRequiredError import: must import from ./writeback.js

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.