nexus/.planning/phases/23-brainstormer-flow/23-RESEARCH.md

32 KiB
Raw Blame History

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

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

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

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:

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

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

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

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

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

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

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

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

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

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

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

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