# Phase 23: Brainstormer Flow - Research

**Researched:** 2026-04-01
**Domain:** Chat agent orchestration, structured messaging, SSE streaming, DB migration, React component extension
**Confidence:** HIGH

---

## Summary

Phase 23 builds the Brainstormer's end-to-end flow on top of the chat infrastructure delivered in Phases 21–22. The core work is: (1) a DB migration adding `message_type` to `chat_messages`, (2) new server routes for handoff and spec-to-task promotion, (3) four new UI components that render structured `system` role messages, and (4) wiring the default agent selector to the `general` role.

The Phase 22 echo stub (`streamEcho`) is explicitly designed to be replaced in Phase 23 with a real LLM adapter. However, the UI-SPEC.md clarifies that the Brainstormer's structured questioning flow is **entirely server-side** (system prompt + LLM). The UI simply renders whatever message type the server returns. This means Phase 23 does not require a real LLM to function — the spec card, handoff, and task creation flows are all triggered by explicit API calls from the UI, not by the LLM output type changing.

The entire flow works inside the existing `ChatPanel` without new pages or navigation. The success criteria are fully achievable by extending existing patterns established in Phases 21–22.

**Primary recommendation:** Extend existing patterns exactly — no new routing, no new state machines. Add `message_type` to DB, extend `ChatMessage` with a type-dispatch branch, add a `/handoff` route that creates a system message + triggers PM agent, and add a `/handoff/complete` route that creates tasks via the existing `issuesApi`.

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.

### Claude's Discretion
All implementation choices are at Claude's discretion.

### Deferred Ideas (OUT OF SCOPE)
None — discuss phase skipped.
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| AGENT-01 | Default agent is the Brainstormer (Generalist with a Superpowers-style system prompt, or a dedicated 4th Brainstormer agent) | `useBrainstormerDefault` hook queries workspace agents and selects the first `role === "general"` agent; wired into `ChatPanel` when `activeAgentId === null` and no messages |
| AGENT-02 | Brainstormer follows a structured questioning flow: asks clarifying questions, produces a spec template, and hands off to PM | Server-side: system prompt drives the LLM behavior. UI side: spec card triggers via `message_type: "spec_card"` — `ChatSpecCard` component renders inside `ChatMessage`. Edit mode enabled. "Send to PM" posts to new `/handoff` route |
| AGENT-03 | PM agent can receive specs from chat and create Nexus tasks/issues from them | New `POST /api/conversations/:id/handoff` route receives spec content + `targetRole: "pm"`, creates system handoff message, inserts tasks via existing `issueService.createIssue()`, returns task IDs |
| AGENT-05 | Handoff indicators visible in chat: "Brainstormer → PM: Here's the spec for approval" | `ChatHandoffIndicator` component — separator-style, rendered when `messageType === "handoff"` |
| AGENT-06 | Task creation from chat: user or agent can say "create a task for this" and it becomes a Nexus issue | Route creates issues via `issuesApi.create()` pattern (same as existing UI); returns issue `id`, `identifier`, `title` for `ChatTaskCreatedBadge` |
| AGENT-07 | Status updates from agents appear in chat: "Engineer completed task X" notification in the relevant conversation | `ChatStatusUpdateBadge` component renders `message_type: "status_update"` messages. Server-side: agent completion events insert `system` messages into the relevant conversation |
| CHAT-09 | System message indicator: when the Brainstormer hands off to PM, or PM delegates to Engineer, the handoff is visible in chat | `ChatHandoffIndicator` is the implementation. DB column `message_type: "handoff"` identifies these messages across sessions |
</phase_requirements>

---

## Standard Stack

### Core — Everything Already Installed
| Library | Version | Purpose | Phase 23 Usage |
|---------|---------|---------|----------------|
| drizzle-orm | existing | ORM + migrations | `ALTER TABLE chat_messages ADD COLUMN message_type text` |
| @tanstack/react-query | existing | Server state, cache invalidation | `useQuery` in `useBrainstormerDefault`, mutations for handoff |
| lucide-react | ^0.574.0 | Icon set | `CheckCircle2` (status badge), `Brain` (brainstormer icon) |
| shadcn/ui | new-york preset | UI primitives | `button`, `card`, `textarea` already installed — no new installs |
| express | existing | Server routing | New `/handoff` route on existing `chatRoutes` |
| zod | existing | Schema validation | New `handoffSchema` in `@paperclipai/shared` validators |

**No new npm packages required for Phase 23.** All dependencies exist from Phases 21–22.

### New Components to Build
| Component | File Path | Renders When |
|-----------|-----------|--------------|
| `ChatSpecCard` | `ui/src/components/ChatSpecCard.tsx` | `messageType === "spec_card"` |
| `ChatHandoffIndicator` | `ui/src/components/ChatHandoffIndicator.tsx` | `messageType === "handoff"` |
| `ChatTaskCreatedBadge` | `ui/src/components/ChatTaskCreatedBadge.tsx` | `messageType === "task_created"` |
| `ChatStatusUpdateBadge` | `ui/src/components/ChatStatusUpdateBadge.tsx` | `messageType === "status_update"` |
| `useBrainstormerDefault` | `ui/src/hooks/useBrainstormerDefault.ts` | Used in `ChatPanel` on mount |

### Existing Files to Modify
| File | Change Summary |
|------|---------------|
| `packages/db/src/schema/chat_messages.ts` | Add `messageType: text("message_type")` nullable column |
| `packages/db/src/migrations/NNNN_add_message_type.sql` | Raw SQL: `ALTER TABLE "chat_messages" ADD COLUMN "message_type" text;` |
| `packages/shared/src/types/chat.ts` | Add `messageType: string \| null` to `ChatMessage` interface |
| `packages/shared/src/validators/chat.ts` | Add `messageType` to `createMessageSchema`; add `handoffSchema` |
| `server/src/routes/chat.ts` | Add `POST /conversations/:id/handoff` route |
| `server/src/services/chat.ts` | Add `addSystemMessage()` helper; extend `addMessage()` to accept `messageType` |
| `ui/src/components/ChatMessage.tsx` | Add `messageType` prop; dispatch to specialized components |
| `ui/src/components/ChatMessageList.tsx` | Pass `messageType` from stored message to `ChatMessage` |
| `ui/src/components/ChatPanel.tsx` | Wire `useBrainstormerDefault` for auto-selection |
| `ui/src/api/chat.ts` | Add `handoffSpec()` method, `patchMessage()` for spec edits |

---

## Architecture Patterns

### Recommended Project Structure (Phase 23 additions only)

```
packages/
├── db/src/
│   ├── schema/chat_messages.ts          ← add messageType column
│   └── migrations/NNNN_add_message_type.sql
├── shared/src/
│   ├── types/chat.ts                    ← add messageType to ChatMessage
│   └── validators/chat.ts              ← add messageType, handoffSchema

server/src/
├── routes/chat.ts                       ← add POST /conversations/:id/handoff
└── services/chat.ts                    ← extend addMessage, add addSystemMessage

ui/src/
├── components/
│   ├── ChatMessage.tsx                  ← extend with messageType dispatch
│   ├── ChatMessageList.tsx             ← pass messageType prop
│   ├── ChatPanel.tsx                   ← wire useBrainstormerDefault
│   ├── ChatSpecCard.tsx                ← NEW
│   ├── ChatHandoffIndicator.tsx        ← NEW
│   ├── ChatTaskCreatedBadge.tsx        ← NEW
│   └── ChatStatusUpdateBadge.tsx       ← NEW
└── hooks/
    └── useBrainstormerDefault.ts       ← NEW
```

### Pattern 1: DB Migration (SQL File, Not TypeScript)

The project uses raw SQL migration files, not TypeScript migrations. Look at the existing pattern:

```sql
-- packages/db/src/migrations/NNNN_add_message_type.sql
ALTER TABLE "chat_messages" ADD COLUMN "message_type" text;
```

After adding the SQL file, update the Drizzle schema in `packages/db/src/schema/chat_messages.ts`:

```typescript
// Source: existing packages/db/src/schema/chat_messages.ts
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
import { chatConversations } from "./chat_conversations.js";

export const chatMessages = pgTable(
  "chat_messages",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    conversationId: uuid("conversation_id").notNull()
      .references(() => chatConversations.id, { onDelete: "cascade" }),
    role: text("role").notNull(),
    content: text("content").notNull(),
    agentId: uuid("agent_id"),
    messageType: text("message_type"),    // NEW: null | "handoff" | "spec_card" | "task_created" | "status_update"
    createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
  },
  (table) => ({
    conversationCreatedIdx: index("chat_messages_conversation_created_idx").on(table.conversationId, table.createdAt),
  }),
);
```

**CRITICAL: The migration numbering.** The journal shows entries up to idx 46 (tag: `0046_smooth_sentinels`), but files on disk go up to `0048_add_chat_messages_updated_at.sql`. The next migration must be named `0049_*.sql` with idx 49 added to `_journal.json`.

**Migration journal update required:** `_journal.json` must be manually updated with a new entry. The `generate` script uses drizzle-kit which reads compiled schema from `dist/` — but since we are adding raw SQL manually (matching the existing pattern for Phase 21/22 chat migrations), we append both the SQL file and a journal entry.

### Pattern 2: ChatMessage Type Dispatch

Extend `ChatMessage` with a `messageType` prop and a dispatch block before the existing `role === "user"` check:

```typescript
// Source: existing ui/src/components/ChatMessage.tsx — extend this pattern
interface ChatMessageProps {
  // ... existing props ...
  messageType?: string | null;
}

export function ChatMessage({ role, content, messageType, ...rest }) {
  // Dispatch to specialized system message components
  if (role === "system" || messageType) {
    if (messageType === "spec_card") return <ChatSpecCard content={content} ... />;
    if (messageType === "handoff") return <ChatHandoffIndicator content={content} />;
    if (messageType === "task_created") return <ChatTaskCreatedBadge content={content} />;
    if (messageType === "status_update") return <ChatStatusUpdateBadge content={content} />;
  }
  // ... existing user / assistant rendering ...
}
```

The `content` field for structured messages stores JSON. `ChatSpecCard` parses `JSON.parse(content)` to extract `{ what, why, constraints, success }`. Parse errors fall back to `"Could not render spec."` in `text-destructive text-[13px]`.

### Pattern 3: Handoff Route

New route on the existing `chatRoutes` router:

```typescript
// POST /api/conversations/:id/handoff
router.post("/conversations/:id/handoff", async (req, res) => {
  assertBoard(req);
  const data = handoffSchema.parse(req.body);
  // 1. Insert system message with messageType: "handoff"
  const handoffMsg = await svc.addSystemMessage(req.params.id!, {
    content: "Brainstormer → PM: spec handed off",
    messageType: "handoff",
  });
  // 2. Create Nexus issues from spec via issueService
  const issues = await issueSvc.createFromSpec(data.spec, req.params.companyId!);
  // 3. Insert system message with messageType: "task_created" for each issue
  for (const issue of issues) {
    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 });
});
```

**The `companyId` must be resolved from the conversation.** The `chatService.getConversation()` method returns the full row including `companyId`. Use that to call `issueService`.

### Pattern 4: useBrainstormerDefault Hook

```typescript
// ui/src/hooks/useBrainstormerDefault.ts
import { useQuery } from "@tanstack/react-query";
import { agentsApi } from "../api/agents";
import { useCompany } from "../context/CompanyContext";

export function useBrainstormerDefault() {
  const { selectedCompanyId } = useCompany();

  const { data: agents = [] } = useQuery({
    queryKey: ["agents", selectedCompanyId],
    queryFn: () => agentsApi.list(selectedCompanyId!),
    enabled: !!selectedCompanyId,
  });

  // ChatPanel already runs this same query — React Query deduplicates the fetch
  const generalAgent = agents
    .filter((a) => a.role === "general")
    .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0];

  return generalAgent?.id ?? null;
}
```

**Cache deduplication:** `ChatPanel` already queries `["agents", selectedCompanyId]`. The hook reuses the same query key, so no extra network request fires.

### Pattern 5: Spec Card Edit Mode

`ChatSpecCard` manages its own edit state locally. It does NOT use `useStreamingChat` or `useChatMessages`. Edit → "Save changes" calls `chatApi.patchMessage(conversationId, messageId, newContent)` which maps to the existing `PATCH /api/conversations/:id/messages/:msgId` route. No new server endpoints needed for edit.

```typescript
// Spec card content schema
interface SpecContent {
  what: string;
  why: string;
  constraints: string;
  success: string;
}
// Stored in chat_messages.content as JSON.stringify(SpecContent)
```

### Pattern 6: Status Update Insertion (AGENT-07)

AGENT-07 says "Status updates from agents appear in chat." In Phase 23, the trigger for status updates is not implemented end-to-end (that requires the LLM adapter replacement from Phase 22's note: "Phase 23 replaces with real LLM adapter"). The **UI side** (rendering `ChatStatusUpdateBadge`) is fully implemented. The **server-side trigger** can be a new `POST /api/conversations/:id/status-update` route that any system component can call to insert a `system` message with `messageType: "status_update"`.

For Phase 23 scope: implement the UI component and the insertion route. The actual agent-to-chat notification plumbing can be minimal (the route exists; agent adapter wiring is Phase 24+).

### Anti-Patterns to Avoid

- **Storing spec fields as separate DB columns**: The spec content (`what`, `why`, `constraints`, `success`) goes into `chat_messages.content` as JSON. No new columns. This is consistent with how structured data is handled elsewhere.
- **New React Router pages for the handoff flow**: The UI-SPEC is explicit — everything stays in `ChatPanel`. No new routes.
- **Re-fetching all messages after handoff**: Use optimistic insertion + targeted invalidation. Append the handoff + task badge messages locally first (optimistic), then invalidate `["chat", "messages", conversationId]` on success.
- **Modifying `useStreamingChat`**: The spec card and handoff flow are triggered by explicit button clicks, not by streaming. Do not add streaming state to spec card actions.
- **Spec card inside streaming entry**: Spec cards are stored messages (from DB), not streaming content. They always appear as real `ChatMessage` entries with `role === "system"`.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Issue creation | Custom issue schema | `issuesApi.create(companyId, data)` via existing route `POST /companies/:companyId/issues` | Existing endpoint handles all business logic, status defaults, identifier generation |
| Optimistic UI updates | Manual cache state | `queryClient.setQueryData` + `queryClient.invalidateQueries` (established Phase 21/22 pattern) | React Query cache is already wired for chat messages |
| Toast notifications | Custom toast component | shadcn `toast` (already wired via Sonner in the app) | Existing toast infrastructure from Phase 21 |
| JSON content parsing error boundary | Custom error component | Inline try/catch with fallback render in `ChatSpecCard` | Single component, single location |
| Agent lookup | New API call | `agentMap` already built in `ChatPanel` and passed to `ChatMessageList` | Zero additional fetches |

**Key insight:** The entire Phase 23 feature surface is achievable by extending existing patterns. The only net-new infrastructure is the DB migration, the `/handoff` route, and the four UI components.

---

## Common Pitfalls

### Pitfall 1: Migration File Numbering Mismatch
**What goes wrong:** The `_journal.json` lists entries up to idx 46, but disk has files 0047 and 0048. If the new migration is numbered incorrectly, `drizzle-kit` will fail to apply or generate a gap.
**Why it happens:** The journal and the files can drift when migrations are added manually without running `drizzle-kit generate`.
**How to avoid:** Check the last file number on disk (`ls src/migrations/*.sql | tail -1`) and use the next sequential number. Also add the corresponding journal entry with matching `idx`, `version: "7"`, `when: Date.now()`, `tag`, and `breakpoints: true`.
**Warning signs:** `drizzle-kit migrate` complains about missing or duplicate journal entries.

### Pitfall 2: `messageType` Not Propagated Through Message List
**What goes wrong:** `ChatMessageList` renders `ChatMessage` but only passes fields that are currently in `ChatMessage` — adding `messageType` to the DB schema and shared type does not automatically flow to the component.
**Why it happens:** The `ChatMessage` component props interface must be extended, and `ChatMessageList` must read `msg.messageType` from the message object and pass it down.
**How to avoid:** Update the props interface in `ChatMessage.tsx`, update `ChatMessageList.tsx` to pass `messageType={msg.messageType}`, and update the synthetic streaming entry object to include `messageType: null`.
**Warning signs:** Spec cards appear as raw JSON text in the chat.

### Pitfall 3: Spec Card "Send to PM" Race Condition (Optimistic + Failure)
**What goes wrong:** The optimistic `ChatHandoffIndicator` is appended before the API call. If the call fails, the indicator must be removed — but if the component is purely display-driven by the messages array, removing it requires either a local state flag or removing the optimistically inserted message.
**Why it happens:** Optimistic UI requires rollback on failure.
**How to avoid:** Track the handoff submission state in `ChatSpecCard` with a local `submitting` flag. The optimistic indicator should be a **local state** element (not a persisted message) until the API succeeds. On success, the server-inserted `handoff` message causes React Query to re-fetch and display the persisted version. On failure, the local state is cleared and buttons are re-enabled with a toast.
**Warning signs:** Ghost handoff indicators appearing after failed API calls, or spec cards being stuck in "submitting" state.

### Pitfall 4: companyId Not Available in Chat Route for Issue Creation
**What goes wrong:** `POST /api/conversations/:id/handoff` needs `companyId` to call `issueService.createFromSpec()`, but the route parameter is only `:id` (conversation ID).
**Why it happens:** The chat routes are conversation-scoped, not company-scoped.
**How to avoid:** Call `svc.getConversation(req.params.id!)` at the start of the handoff route to resolve `companyId` from the conversation row. This is a single extra DB read and follows the established pattern.
**Warning signs:** 500 errors on handoff with "companyId undefined".

### Pitfall 5: Virtualizer Height with System Messages
**What goes wrong:** System message components (spec cards, task badges) have different heights than standard prose messages. The virtualizer uses an estimated height of 80px (from Phase 22). Spec cards will be taller; task badges will be shorter.
**Why it happens:** `estimateSize: () => 80` is a fixed estimate; actual heights vary.
**How to avoid:** The virtualizer already uses `measureElement` and `virtualizer.measure()` — the dynamic measurement from Phase 22 handles this. No additional work needed as long as the system message components are inside the virtualizer's `ref={virtualizer.measureElement}` wrapper.
**Warning signs:** Messages overlapping or large gaps between messages after a spec card renders.

### Pitfall 6: Spec Content JSON Parse in Streaming Context
**What goes wrong:** If the streaming echo stub produces content that happens to have `messageType: "spec_card"` set (e.g., during testing), the content will be raw text, not JSON, and `JSON.parse` will throw.
**Why it happens:** The spec card branch in `ChatMessage` fires before content is verified as valid JSON.
**How to avoid:** Always wrap the content parse in try/catch in `ChatSpecCard`. The fallback render is `"Could not render spec."` in `text-destructive text-[13px]` per the UI-SPEC.
**Warning signs:** White screen or unhandled error in `ChatMessage` during development.

---

## Code Examples

### DB Schema Extension Pattern (verified from existing codebase)
```typescript
// Source: packages/db/src/schema/chat_messages.ts — existing file
// Add messageType column using the same text() pattern as role/content
messageType: text("message_type"),
```

### addSystemMessage Service Helper
```typescript
// Source: server/src/services/chat.ts — extend existing addMessage pattern
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();

  await db
    .update(chatConversations)
    .set({ updatedAt: new Date() })
    .where(eq(chatConversations.id, conversationId));

  return message!;
},
```

### ChatHandoffIndicator Layout (from UI-SPEC)
```tsx
// Source: 23-UI-SPEC.md
export function ChatHandoffIndicator({ content }: { content: string }) {
  return (
    <div
      className="flex items-center gap-3 py-2 text-[13px] text-muted-foreground"
      aria-label="Agent handoff from Brainstormer to PM"
    >
      <hr className="flex-1 border-border" aria-hidden="true" />
      <span className="whitespace-nowrap">{content}</span>
      <hr className="flex-1 border-border" aria-hidden="true" />
    </div>
  );
}
```

### ChatTaskCreatedBadge Layout (from UI-SPEC)
```tsx
// Source: 23-UI-SPEC.md
export function ChatTaskCreatedBadge({ taskId, taskTitle, taskUrl }: Props) {
  if (!taskId) {
    return (
      <div className="inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1 text-[13px] text-muted-foreground">
        Creating task...
      </div>
    );
  }
  return (
    <div className="inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1 text-[13px]" role="status">
      <span className="text-[11px] font-semibold text-muted-foreground">{taskId}</span>
      <span className="text-foreground">{taskTitle}</span>
      <Link to={taskUrl} className="text-primary underline-offset-2 hover:underline" aria-label={`View task ${taskId}`}>
        View task
      </Link>
    </div>
  );
}
```

### useBrainstormerDefault Hook (cache-sharing pattern)
```typescript
// Source: research — matches React Query pattern from ChatPanel.tsx
// ChatPanel already runs: useQuery({ queryKey: ["agents", selectedCompanyId], ... })
// This hook reuses the SAME queryKey — no duplicate network request

export function useBrainstormerDefault(): string | null {
  const { selectedCompanyId } = useCompany();
  const { data: agents = [] } = useQuery({
    queryKey: ["agents", selectedCompanyId],
    queryFn: () => agentsApi.list(selectedCompanyId!),
    enabled: !!selectedCompanyId,
  });
  const generalAgent = agents
    .filter((a) => a.role === "general")
    .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0];
  return generalAgent?.id ?? null;
}
```

### ChatPanel auto-selection wiring
```typescript
// Source: ui/src/components/ChatPanel.tsx — extend existing pattern
// Add effect after existing state declarations:
const brainstormerDefaultId = useBrainstormerDefault();

useEffect(() => {
  // Only auto-select when no agent chosen AND no messages yet (new conversation)
  if (activeAgentId === null && brainstormerDefaultId !== null) {
    const hasMessages = messages && messages.length > 0;
    if (!hasMessages) {
      setActiveAgentId(brainstormerDefaultId);
    }
  }
}, [activeAgentId, brainstormerDefaultId, messages]);
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `streamEcho` stub (word-by-word) | Real LLM adapter (Phase 23) | Phase 22 note: "Phase 23 replaces with real LLM adapter" | The streaming endpoint in `chat.ts` currently calls `svc.streamEcho()` — Phase 23 must replace this with actual LLM invocation. This is a significant addition, separate from the structured message flow. |
| No structured messages | `message_type` column + type dispatch | Phase 23 (this phase) | System messages can now carry typed content for specialized rendering |

**Important clarification on LLM integration:** The STATE.md note says "streamEcho stub yields word-by-word with 50ms delay; Phase 23 replaces with real LLM adapter." This means Phase 23 should connect the streaming endpoint to an actual agent adapter. The `agents` table has `adapterType` and `adapterConfig` fields. The server already has adapter loading via `findServerAdapter()` in `routes/agents.ts`. The streaming route in `chat.ts` needs to call the agent's adapter instead of `streamEcho`. This is distinct from the spec card / handoff flow but is part of Phase 23's scope (AGENT-02 depends on the LLM actually producing the questioning flow).

**Adapter invocation pattern:** Look at how `issueService` or agent task sessions invoke adapters. The `adapterConfig` from the agent row contains the LLM connection details. For Phase 23, the simplest approach is to call the adapter's streaming generation method with the conversation history as context.

---

## Open Questions

1. **LLM Adapter Invocation in `streamEcho` Replacement**
   - What we know: `findServerAdapter()` exists in `server/src/adapters/index.ts`; agents have `adapterType` and `adapterConfig`; existing adapters (claude_local, etc.) have streaming capability
   - What's unclear: The exact interface for calling an adapter's streaming generation from `chat.ts` — the adapters are designed for task execution, not conversational streaming
   - Recommendation: Read `server/src/adapters/index.ts` and one adapter's implementation before writing the streaming replacement. The planner should allocate a dedicated plan wave for this.

2. **Issue Creation Required Fields for PM Agent**
   - What we know: `issuesApi.create(companyId, data)` takes `Record<string, unknown>`; the issues table has `title`, `description`, `status`, `priority`, `projectId` (nullable)
   - What's unclear: Which fields are required by the server route's validation; whether a `projectId` is needed for chat-originated tasks
   - Recommendation: The STATE.md Blockers section flags this exact issue for Phase 4 — "POST /api/companies/:companyId/issues required fields not fully documented — read server/src/routes/companies.ts before implementing." Read `server/src/routes/issues.ts` in the plan's Wave 0 before implementing task creation.

3. **Spec Card Content Storage Format**
   - What we know: Content is stored as JSON in `chat_messages.content`; the existing `content` column is `text` with `min(1) max(100_000)` validation
   - What's unclear: Whether the `createMessageSchema` max of 100,000 characters is sufficient for typical spec cards (yes — spec cards will be well under 1KB)
   - Recommendation: HIGH confidence this is fine. No action needed.

---

## Environment Availability

Step 2.6: SKIPPED — Phase 23 is purely code/config changes. All runtime dependencies (PostgreSQL, Node.js, existing adapters) are already verified operational from Phase 22.

---

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest 3.x |
| Config file | `vitest.config.ts` (root) — includes `ui` and `server` projects |
| UI config | `ui/vitest.config.ts` — environment: node |
| Quick run command | `pnpm test:run --project=ui` |
| Full suite command | `pnpm test:run` |

### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| AGENT-01 | `useBrainstormerDefault` returns `general` role agent ID | unit | `pnpm test:run --project=ui -- useBrainstormerDefault` | ❌ Wave 0 |
| AGENT-01 | Falls back to first alphabetical if no `general` agent | unit | same | ❌ Wave 0 |
| AGENT-02 | `ChatMessage` renders `ChatSpecCard` when `messageType === "spec_card"` | unit | `pnpm test:run --project=ui -- ChatMessage` | ✅ (extend existing) |
| AGENT-02 | `ChatSpecCard` parses JSON content and renders four sections | unit | `pnpm test:run --project=ui -- ChatSpecCard` | ❌ Wave 0 |
| AGENT-02 | `ChatSpecCard` shows error fallback on JSON parse failure | unit | same | ❌ Wave 0 |
| AGENT-02 | Edit mode: textareas appear; "Save changes" disabled when all empty | unit | same | ❌ Wave 0 |
| AGENT-03 | `POST /conversations/:id/handoff` inserts handoff message + task messages | unit | `pnpm test:run --project=server -- chat-routes` | ✅ (extend existing) |
| AGENT-05 | `ChatHandoffIndicator` renders with flanking `<hr>` elements | unit | `pnpm test:run --project=ui -- ChatHandoffIndicator` | ❌ Wave 0 |
| AGENT-06 | Task badge renders "Creating task..." before taskId | unit | `pnpm test:run --project=ui -- ChatTaskCreatedBadge` | ❌ Wave 0 |
| AGENT-06 | Task badge renders taskId + "View task" link after resolve | unit | same | ❌ Wave 0 |
| AGENT-07 | `ChatStatusUpdateBadge` renders agent name + task reference | unit | `pnpm test:run --project=ui -- ChatStatusUpdateBadge` | ❌ Wave 0 |
| CHAT-09 | Handoff message stored with `messageType: "handoff"` | unit | `pnpm test:run --project=server -- chat-service` | ✅ (extend existing) |

### Sampling Rate
- **Per task commit:** `pnpm test:run --project=ui` + `pnpm test:run --project=server`
- **Per wave merge:** `pnpm test:run` (full suite)
- **Phase gate:** Full suite green before `/gsd:verify-work`

### Wave 0 Gaps
- [ ] `ui/src/hooks/useBrainstormerDefault.test.ts` — covers AGENT-01
- [ ] `ui/src/components/ChatSpecCard.test.tsx` — covers AGENT-02 (spec render, edit mode, JSON error)
- [ ] `ui/src/components/ChatHandoffIndicator.test.tsx` — covers AGENT-05
- [ ] `ui/src/components/ChatTaskCreatedBadge.test.tsx` — covers AGENT-06
- [ ] `ui/src/components/ChatStatusUpdateBadge.test.tsx` — covers AGENT-07

---

## Sources

### Primary (HIGH confidence)
- Direct codebase inspection — `packages/db/src/schema/chat_messages.ts`, `chat_conversations.ts`, `agents.ts`
- Direct codebase inspection — `server/src/routes/chat.ts`, `server/src/services/chat.ts`
- Direct codebase inspection — `ui/src/components/ChatMessage.tsx`, `ChatMessageList.tsx`, `ChatPanel.tsx`
- Direct codebase inspection — `ui/src/hooks/useStreamingChat.ts`, `useChatMessages.ts`
- Direct codebase inspection — `ui/src/api/chat.ts`, `ui/src/api/issues.ts`
- Direct codebase inspection — `packages/shared/src/types/chat.ts`, `validators/chat.ts`
- Direct codebase inspection — `packages/shared/src/constants.ts` (AgentRole, AGENT_ICON_NAMES)
- Direct codebase inspection — `.planning/phases/23-brainstormer-flow/23-UI-SPEC.md`
- Direct codebase inspection — `.planning/STATE.md` (Phase 22 decisions and notes)
- Direct codebase inspection — `packages/db/src/migrations/` (migration naming convention)

### Secondary (MEDIUM confidence)
- Migration journal structure inferred from `_journal.json` + existing SQL files

### Tertiary (LOW confidence)
- LLM adapter invocation interface — not inspected; flagged as Open Question 1

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — all libraries verified present in codebase
- Architecture patterns: HIGH — directly observed from Phase 21/22 code
- DB migration: HIGH — verified file naming convention and journal format
- Pitfalls: HIGH — derived from actual code paths and Phase 22 decisions in STATE.md
- LLM adapter replacement: LOW — not inspected; flagged as open question

**Research date:** 2026-04-01
**Valid until:** 2026-05-01 (stable codebase; no fast-moving external dependencies)
