docs(23): research brainstormer-flow phase domain
This commit is contained in:
parent
b9719a8ca5
commit
9f7ab07752
1 changed files with 553 additions and 0 deletions
553
.planning/phases/23-brainstormer-flow/23-RESEARCH.md
Normal file
553
.planning/phases/23-brainstormer-flow/23-RESEARCH.md
Normal file
|
|
@ -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>
|
||||
## 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)
|
||||
Loading…
Add table
Reference in a new issue