diff --git a/.planning/phases/23-brainstormer-flow/23-RESEARCH.md b/.planning/phases/23-brainstormer-flow/23-RESEARCH.md new file mode 100644 index 00000000..d1e6754e --- /dev/null +++ b/.planning/phases/23-brainstormer-flow/23-RESEARCH.md @@ -0,0 +1,553 @@ +# 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 (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. + + +--- + + +## 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 | + + +--- + +## 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 ; + if (messageType === "handoff") return ; + if (messageType === "task_created") return ; + if (messageType === "status_update") return ; + } + // ... 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 ( +
+ + {content} + +
+ ); +} +``` + +### ChatTaskCreatedBadge Layout (from UI-SPEC) +```tsx +// Source: 23-UI-SPEC.md +export function ChatTaskCreatedBadge({ taskId, taskTitle, taskUrl }: Props) { + if (!taskId) { + return ( +
+ Creating task... +
+ ); + } + return ( +
+ {taskId} + {taskTitle} + + View task + +
+ ); +} +``` + +### 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`; 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 `
` 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)