Plan 00 (Wave 0): DB migration for message_type, shared types/validators, test stubs Plan 01 (Wave 1): Server — addSystemMessage, handoff route, status-update route Plan 02 (Wave 1): UI — ChatSpecCard, ChatHandoffIndicator, ChatTaskCreatedBadge, ChatStatusUpdateBadge, useBrainstormerDefault Plan 03 (Wave 2): Wiring — ChatMessage dispatch, ChatMessageList propagation, ChatPanel brainstormer default, chatApi handoff Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23-brainstormer-flow | 01 | execute | 1 |
|
|
true |
|
|
Purpose: The handoff route is the backbone of the Brainstormer-to-PM flow. The status-update route enables agent completion notifications in chat. Both insert typed system messages. Output: Extended chat service, two new routes on chatRoutes.
<execution_context> @.claude/get-shit-done/workflows/execute-plan.md @.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/23-brainstormer-flow/23-RESEARCH.md @.planning/phases/23-brainstormer-flow/23-00-SUMMARY.mdFrom server/src/services/chat.ts:
export function chatService(db: Db) {
return {
async addMessage(conversationId: string, data: { role: string; content: string; agentId?: string }) { ... },
async getConversation(id: string) { ... }, // returns full row including companyId
async editMessage(messageId: string, content: string) { ... },
// ... other methods
};
}
From server/src/routes/chat.ts:
export function chatRoutes(db: Db): Router {
const router = Router();
const svc = chatService(db);
// Routes: GET/POST conversations, GET/PATCH/DELETE conversations/:id,
// GET/POST messages, POST stream, PATCH messages/:msgId, DELETE messages/after/:msgId
}
From packages/shared/src/validators/chat.ts (after Plan 00):
export const handoffSchema = z.object({
spec: z.object({ what: z.string().min(1), why: z.string().min(1), constraints: z.string().optional().default(""), success: z.string().optional().default("") }),
targetRole: z.enum(["pm", "engineer", "general"]),
});
From packages/shared/src/validators/issue.ts:
export const createIssueSchema = z.object({
title: z.string().min(1),
description: z.string().optional().nullable(),
status: z.enum(ISSUE_STATUSES).optional().default("backlog"),
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
// ... many optional fields
});
Issue creation pattern: issueService(db).create(companyId, { title, description, ... }) returns { id, identifier, title, ... }.
From server/src/routes/issues.ts (line 964):
router.post("/companies/:companyId/issues", validate(createIssueSchema), async (req, res) => {
const issue = await svc.create(companyId, { ...req.body, createdByAgentId: actor.agentId, createdByUserId: ... });
res.status(201).json(issue);
});
The chatRoutes function currently receives only db: Db. To call issueService, either:
(a) Import and instantiate issueService inside chatRoutes, or
(b) Add issueService as a parameter to chatRoutes.
Option (a) is simplest and matches how heartbeat.ts instantiates issueService locally.
Task 1: Extend chat service with messageType support and addSystemMessage - server/src/services/chat.ts - packages/db/src/schema/chat_messages.ts server/src/services/chat.ts 1. Extend `addMessage` to accept optional `messageType?: string` in its data parameter. Pass `messageType: data.messageType ?? null` in the `.values()` call to `chatMessages`. This requires the schema from Plan 00 to include the messageType column.-
Add
addSystemMessagehelper method to the returned service object:async addSystemMessage( conversationId: string, data: { content: string; messageType: string; agentId?: string }, ) { const [message] = await db .insert(chatMessages) .values({ conversationId, role: "system", content: data.content, agentId: data.agentId ?? null, messageType: data.messageType, }) .returning(); // Bump conversation updatedAt (same pattern as addMessage Pitfall 3) await db .update(chatConversations) .set({ updatedAt: new Date() }) .where(eq(chatConversations.id, conversationId)); return message!; }, -
Also extend
listMessagesreturn — themessageTypefield will automatically be included inselect()results since the Drizzle schema now defines it. No change needed in listMessages itself, but verify the return rows will includemessageType. cd /opt/nexus && pnpm exec tsc --noEmit -p server/tsconfig.json 2>&1 | head -20 <acceptance_criteria>- grep -q "addSystemMessage" server/src/services/chat.ts
- grep -q "messageType" server/src/services/chat.ts </acceptance_criteria> addMessage accepts messageType; addSystemMessage creates system messages with typed messageType; both bump conversation updatedAt
-
Import
issueServicefrom../services/issues.jsat the top. -
Inside
chatRoutes(db), instantiate:const issueSvc = issueService(db); -
Add
POST /conversations/:id/handoffroute (before thereturn routerline):router.post("/conversations/:id/handoff", async (req, res) => { assertBoard(req); const data = handoffSchema.parse(req.body); // Resolve companyId from conversation (Pitfall 4) 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] }); }); -
Add
POST /conversations/:id/status-updateroute: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); });
IMPORTANT: The issueService import path uses .js extension (ESM convention in this codebase). Check the existing imports in chat.ts and issues.ts for the exact pattern. The server uses "../services/issues.js" style imports.
cd /opt/nexus && pnpm exec tsc --noEmit -p server/tsconfig.json 2>&1 | head -20
<acceptance_criteria>
- grep -q "handoff" server/src/routes/chat.ts
- grep -q "status-update" server/src/routes/chat.ts
- grep -q "issueService" server/src/routes/chat.ts
- grep -q "handoffSchema" server/src/routes/chat.ts
- grep -q "addSystemMessage" server/src/routes/chat.ts
</acceptance_criteria>
POST /conversations/:id/handoff creates handoff message + issue + task_created message; POST /conversations/:id/status-update creates status_update message; both routes use assertBoard for auth
<success_criteria>
- addSystemMessage helper exists in chat service
- addMessage accepts optional messageType
- Handoff route creates handoff + task_created system messages and an issue
- Status-update route creates status_update system message
- TypeScript compilation clean </success_criteria>