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

10 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
23-brainstormer-flow 01 execute 1
23-00
server/src/services/chat.ts
server/src/routes/chat.ts
true
AGENT-03
AGENT-06
AGENT-07
CHAT-09
truths artifacts key_links
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
path provides contains
server/src/services/chat.ts addSystemMessage helper and messageType support in addMessage addSystemMessage
path provides contains
server/src/routes/chat.ts handoff and status-update routes handoff
from to via pattern
server/src/routes/chat.ts server/src/services/chat.ts svc.addSystemMessage call svc.addSystemMessage
from to via pattern
server/src/routes/chat.ts server/src/routes/issues.ts issueService for task creation from handoff issueSvc.create
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.

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

From 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.
  1. Add addSystemMessage helper 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!;
    },
    
  2. 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. 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
Task 2: Add handoff and status-update routes - 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 server/src/routes/chat.ts 1. Import `handoffSchema` from `@paperclipai/shared` at the top of chat.ts.
  1. Import issueService from ../services/issues.js at the top.

  2. Inside chatRoutes(db), instantiate: const issueSvc = issueService(db);

  3. Add POST /conversations/:id/handoff route (before the return router line):

    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] });
    });
    
  4. Add POST /conversations/:id/status-update route:

    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

- `pnpm exec tsc --noEmit -p server/tsconfig.json` passes - `pnpm vitest run --project=server` passes (existing tests not broken)

<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>
After completion, create `.planning/phases/23-brainstormer-flow/23-01-SUMMARY.md`