nexus/.planning/phases/23-brainstormer-flow/23-01-PLAN.md
Nexus Dev 9ed6dd16b3 docs(23-brainstormer-flow): create phase plan — 4 plans across 3 waves
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>
2026-04-04 03:55:47 +00:00

285 lines
10 KiB
Markdown

---
phase: 23-brainstormer-flow
plan: 01
type: execute
wave: 1
depends_on: ["23-00"]
files_modified:
- server/src/services/chat.ts
- server/src/routes/chat.ts
autonomous: true
requirements:
- AGENT-03
- AGENT-06
- AGENT-07
- CHAT-09
must_haves:
truths:
- "POST /conversations/:id/handoff inserts a handoff system message and creates issues"
- "POST /conversations/:id/status-update inserts a status_update system message"
- "addMessage accepts optional messageType and persists it"
- "addSystemMessage helper creates system role messages with messageType"
artifacts:
- path: "server/src/services/chat.ts"
provides: "addSystemMessage helper and messageType support in addMessage"
contains: "addSystemMessage"
- path: "server/src/routes/chat.ts"
provides: "handoff and status-update routes"
contains: "handoff"
key_links:
- from: "server/src/routes/chat.ts"
to: "server/src/services/chat.ts"
via: "svc.addSystemMessage call"
pattern: "svc\\.addSystemMessage"
- from: "server/src/routes/chat.ts"
to: "server/src/routes/issues.ts"
via: "issueService for task creation from handoff"
pattern: "issueSvc\\.create"
---
<objective>
Server-side: extend chat service with addSystemMessage helper, messageType support in addMessage, and add handoff + status-update routes.
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.
</objective>
<execution_context>
@.claude/get-shit-done/workflows/execute-plan.md
@.claude/get-shit-done/templates/summary.md
</execution_context>
<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.md
<interfaces>
<!-- Key server interfaces the executor needs -->
From server/src/services/chat.ts:
```typescript
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:
```typescript
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):
```typescript
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:
```typescript
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):
```typescript
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.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend chat service with messageType support and addSystemMessage</name>
<read_first>
- server/src/services/chat.ts
- packages/db/src/schema/chat_messages.ts
</read_first>
<files>server/src/services/chat.ts</files>
<action>
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.
2. Add `addSystemMessage` helper method to the returned service object:
```typescript
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!;
},
```
3. Also extend `listMessages` return — the `messageType` field will automatically be included in `select()` results since the Drizzle schema now defines it. No change needed in listMessages itself, but verify the return rows will include `messageType`.
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p server/tsconfig.json 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "addSystemMessage" server/src/services/chat.ts
- grep -q "messageType" server/src/services/chat.ts
</acceptance_criteria>
<done>addMessage accepts messageType; addSystemMessage creates system messages with typed messageType; both bump conversation updatedAt</done>
</task>
<task type="auto">
<name>Task 2: Add handoff and status-update routes</name>
<read_first>
- server/src/routes/chat.ts
- server/src/services/issues.ts (first 20 lines + the create method signature)
- server/src/routes/issues.ts (lines 964-1001 for create pattern)
- packages/shared/src/validators/chat.ts
</read_first>
<files>server/src/routes/chat.ts</files>
<action>
1. Import `handoffSchema` from `@paperclipai/shared` at the top of chat.ts.
2. Import `issueService` from `../services/issues.js` at the top.
3. Inside `chatRoutes(db)`, instantiate: `const issueSvc = issueService(db);`
4. Add `POST /conversations/:id/handoff` route (before the `return router` line):
```typescript
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] });
});
```
5. Add `POST /conversations/:id/status-update` route:
```typescript
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.
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p server/tsconfig.json 2>&1 | head -20</automated>
</verify>
<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>
<done>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</done>
</task>
</tasks>
<verification>
- `pnpm exec tsc --noEmit -p server/tsconfig.json` passes
- `pnpm vitest run --project=server` passes (existing tests not broken)
</verification>
<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>
<output>
After completion, create `.planning/phases/23-brainstormer-flow/23-01-SUMMARY.md`
</output>