import { FastifyInstance, FastifyReply } from 'fastify'; import { createTask, listTasks, getTask, claimTask, doneTask } from '../core/services/taskService.js'; import { createHandoff, listHandoffs, getHandoff } from '../core/services/handoffService.js'; import { createDecision, listDecisions } from '../core/services/decisionService.js'; import { addMemory, searchMemory, listMemory } from '../core/services/memoryService.js'; import { getStatus, updateStatus } from '../core/services/statusService.js'; import { autoDelegate, suggestDelegation } from '../core/services/delegateService.js'; import { loadConfig } from '../core/config.js'; import { renderBoardHtml } from './board.js'; import type { Task, Handoff, Decision, Memory } from '../core/schema.js'; function notFound(reply: FastifyReply, resource: string) { return reply.status(404).send({ error: `${resource} not found` }); } function badRequest(reply: FastifyReply, message: string) { return reply.status(400).send({ error: message }); } export async function registerRoutes(app: FastifyInstance, cwd: string): Promise { // Static, self-contained Trello-like board. Polls /tasks, /handoffs and // /decisions on the same origin; no build step, no deps. Cached once — the // markup is constant, only the data it fetches changes. const boardHtml = renderBoardHtml(); app.get('/board', async (_request, reply) => reply.type('text/html; charset=utf-8').send(boardHtml)); app.get('/status', async () => ({ body: getStatus(cwd) })); app.post('/status/update', async () => ({ body: updateStatus(cwd) })); app.get('/tasks', async (request) => { const { status, role } = request.query as { status?: string; role?: string }; return listTasks(cwd, { status, role }); }); app.post('/tasks', async (request, reply) => { try { return createTask(cwd, request.body as Partial); } catch (err) { return badRequest(reply, err instanceof Error ? err.message : 'Invalid task'); } }); app.get('/tasks/:id', async (request, reply) => { const { id } = request.params as { id: string }; try { const { task, body } = getTask(cwd, id); return { task, body }; } catch { return notFound(reply, 'Task'); } }); app.patch('/tasks/:id', async (request, reply) => { const { id } = request.params as { id: string }; const patch = request.body as Partial; if (patch.status === 'in_progress' && patch.assignedTo) { return claimTask(cwd, id, patch.assignedTo); } if (patch.status === 'done') { return doneTask(cwd, id); } return badRequest(reply, 'Unsupported patch'); }); app.get('/handoffs', async () => listHandoffs(cwd)); app.post('/handoffs', async (request, reply) => { try { return createHandoff(cwd, request.body as Partial); } catch (err) { return badRequest(reply, err instanceof Error ? err.message : 'Invalid handoff'); } }); app.get('/handoffs/:id', async (request, reply) => { const { id } = request.params as { id: string }; try { const { handoff, body } = getHandoff(cwd, id); return { handoff, body }; } catch { return notFound(reply, 'Handoff'); } }); app.get('/decisions', async () => listDecisions(cwd)); app.post('/decisions', async (request, reply) => { try { return createDecision(cwd, request.body as Partial); } catch (err) { return badRequest(reply, err instanceof Error ? err.message : 'Invalid decision'); } }); app.get('/memory', async () => listMemory(cwd)); app.post('/memory', async (request, reply) => { try { return addMemory(cwd, request.body as Partial); } catch (err) { return badRequest(reply, err instanceof Error ? err.message : 'Invalid memory'); } }); app.get('/memory/search', async (request) => { const { q } = request.query as { q: string }; return searchMemory(cwd, q ?? ''); }); app.post('/delegate', async (request) => { const { auto } = request.query as { auto?: string }; const config = loadConfig(cwd); const suggestion = suggestDelegation(cwd); if (!suggestion) return { suggestion: null }; const shouldAuto = auto === 'true' || config.delegationMode === 'auto'; if (shouldAuto) { const handoff = autoDelegate(cwd); return { suggestion, handoff }; } return { suggestion }; }); }