---
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>
