diff --git a/server/src/routes/chat.ts b/server/src/routes/chat.ts index 1dbf27e7..12cdf114 100644 --- a/server/src/routes/chat.ts +++ b/server/src/routes/chat.ts @@ -2,15 +2,18 @@ import { Router } from "express"; import type { Db } from "@paperclipai/db"; import { assertBoard, assertCompanyAccess } from "./authz.js"; import { chatService } from "../services/chat.js"; +import { issueService } from "../services/issues.js"; import { createConversationSchema, updateConversationSchema, createMessageSchema, + handoffSchema, } from "@paperclipai/shared"; export function chatRoutes(db: Db): Router { const router = Router(); const svc = chatService(db); + const issueSvc = issueService(db); // GET /api/companies/:companyId/conversations router.get("/companies/:companyId/conversations", async (req, res) => { @@ -144,5 +147,65 @@ export function chatRoutes(db: Db): Router { res.status(204).end(); }); + // POST /api/conversations/:id/handoff -- Brainstormer handoff to PM: inserts handoff + task_created messages and creates an issue + router.post("/conversations/:id/handoff", async (req, res) => { + assertBoard(req); + const data = handoffSchema.parse(req.body); + + // Resolve companyId from conversation + const conversation = await svc.getConversation(req.params.id!); + const companyId = conversation.companyId; + + // 1. Insert handoff system message + const handoffMsg = await svc.addSystemMessage(req.params.id!, { + content: `Brainstormer \u2192 PM: spec handed off`, + messageType: "handoff", + }); + + // 2. Create issue from spec + const specDescription = [ + `**What:** ${data.spec.what}`, + `**Why:** ${data.spec.why}`, + data.spec.constraints ? `**Constraints:** ${data.spec.constraints}` : "", + data.spec.success ? `**Success:** ${data.spec.success}` : "", + ].filter(Boolean).join("\n\n"); + + const issue = await issueSvc.create(companyId, { + title: data.spec.what.slice(0, 100), + description: specDescription, + status: "backlog", + priority: "medium", + }); + + // 3. Insert task_created system message + await svc.addSystemMessage(req.params.id!, { + content: JSON.stringify({ + taskId: issue.identifier, + taskTitle: issue.title, + taskUrl: `/issues/${issue.id}`, + }), + messageType: "task_created", + }); + + res.json({ handoffMessageId: handoffMsg.id, issues: [issue] }); + }); + + // POST /api/conversations/:id/status-update -- Agent completion notification in chat + router.post("/conversations/:id/status-update", async (req, res) => { + assertBoard(req); + const { agentName, taskId, taskTitle, taskUrl } = req.body; + if (!agentName || !taskId) { + res.status(400).json({ error: "agentName and taskId are required" }); + return; + } + + const message = await svc.addSystemMessage(req.params.id!, { + content: JSON.stringify({ agentName, taskId, taskTitle, taskUrl }), + messageType: "status_update", + }); + + res.status(201).json(message); + }); + return router; }