fix: Switch auto-spawn from discuss to refine agent, surface in UI
The auto-spawned agent on initiative creation was using discuss mode (Q&A) when it should use refine mode (expand content). Now: - Description seeds root page as tiptap content (split on double newlines) - Spawns refine agent with the populated page in inputContext - getActiveRefineAgent broadened to also surface discuss agents (for CLI-spawned discuss agents) - RefineAgentPanel shows mode-appropriate label for discuss vs refine
This commit is contained in:
@@ -167,10 +167,12 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
.query(async ({ ctx, input }): Promise<AgentInfo | null> => {
|
.query(async ({ ctx, input }): Promise<AgentInfo | null> => {
|
||||||
const agentManager = requireAgentManager(ctx);
|
const agentManager = requireAgentManager(ctx);
|
||||||
const allAgents = await agentManager.list();
|
const allAgents = await agentManager.list();
|
||||||
|
// Surface discuss and refine agents — both work on initiative content
|
||||||
|
const CONTENT_MODES = ['discuss', 'refine'];
|
||||||
const candidates = allAgents
|
const candidates = allAgents
|
||||||
.filter(
|
.filter(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.mode === 'refine' &&
|
CONTENT_MODES.includes(a.mode) &&
|
||||||
a.initiativeId === input.initiativeId &&
|
a.initiativeId === input.initiativeId &&
|
||||||
['running', 'waiting_for_input', 'idle', 'crashed'].includes(a.status) &&
|
['running', 'waiting_for_input', 'idle', 'crashed'].includes(a.status) &&
|
||||||
!a.userDismissedAt,
|
!a.userDismissedAt,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function deriveInitiativeActivity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for active architect agents BEFORE zero-phases check
|
// Check for active architect agents BEFORE zero-phases check
|
||||||
// so auto-spawned discuss/plan/refine agents surface activity
|
// so architect agents (discuss/plan/detail/refine) surface activity
|
||||||
const activeAgent = activeArchitectAgents?.find(
|
const activeAgent = activeArchitectAgents?.find(
|
||||||
a => a.initiativeId === initiative.id
|
a => a.initiativeId === initiative.id
|
||||||
&& (a.status === 'running' || a.status === 'waiting_for_input'),
|
&& (a.status === 'running' || a.status === 'waiting_for_input'),
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { z } from 'zod';
|
|||||||
import type { ProcedureBuilder } from '../trpc.js';
|
import type { ProcedureBuilder } from '../trpc.js';
|
||||||
import { requireAgentManager, requireInitiativeRepository, requireProjectRepository, requireTaskRepository } from './_helpers.js';
|
import { requireAgentManager, requireInitiativeRepository, requireProjectRepository, requireTaskRepository } from './_helpers.js';
|
||||||
import { deriveInitiativeActivity } from './initiative-activity.js';
|
import { deriveInitiativeActivity } from './initiative-activity.js';
|
||||||
import { buildDiscussPrompt } from '../../agent/prompts/index.js';
|
import { buildRefinePrompt } from '../../agent/prompts/index.js';
|
||||||
|
import type { PageForSerialization } from '../../agent/content-serializer.js';
|
||||||
|
|
||||||
export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
||||||
return {
|
return {
|
||||||
@@ -47,38 +48,57 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
await projectRepo.setInitiativeProjects(initiative.id, input.projectIds);
|
await projectRepo.setInitiativeProjects(initiative.id, input.projectIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create root page — seed with description as tiptap content if provided
|
||||||
|
const descriptionText = input.description?.trim();
|
||||||
|
let rootPage: { id: string; parentPageId: string | null; title: string; content: string | null; sortOrder: number } | null = null;
|
||||||
if (ctx.pageRepository) {
|
if (ctx.pageRepository) {
|
||||||
await ctx.pageRepository.create({
|
const tiptapContent = descriptionText
|
||||||
|
? JSON.stringify({
|
||||||
|
type: 'doc',
|
||||||
|
content: descriptionText.split(/\n{2,}/).map(para => ({
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: para.trim() }],
|
||||||
|
})).filter(p => p.content[0].text),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
rootPage = await ctx.pageRepository.create({
|
||||||
initiativeId: initiative.id,
|
initiativeId: initiative.id,
|
||||||
parentPageId: null,
|
parentPageId: null,
|
||||||
title: input.name,
|
title: input.name,
|
||||||
content: null,
|
content: tiptapContent,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-spawn discuss agent when description is provided
|
// Auto-spawn refine agent when description is provided
|
||||||
if (input.description?.trim() && ctx.agentManager && ctx.taskRepository) {
|
if (descriptionText && rootPage && ctx.agentManager && ctx.taskRepository) {
|
||||||
try {
|
try {
|
||||||
const taskRepo = requireTaskRepository(ctx);
|
const taskRepo = requireTaskRepository(ctx);
|
||||||
const agentManager = requireAgentManager(ctx);
|
const agentManager = requireAgentManager(ctx);
|
||||||
|
|
||||||
const task = await taskRepo.create({
|
const task = await taskRepo.create({
|
||||||
initiativeId: initiative.id,
|
initiativeId: initiative.id,
|
||||||
name: `Discuss: ${initiative.name}`,
|
name: `Refine: ${initiative.name}`,
|
||||||
description: input.description.trim(),
|
description: descriptionText,
|
||||||
category: 'discuss',
|
category: 'refine',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
});
|
});
|
||||||
|
|
||||||
const prompt = buildDiscussPrompt();
|
const pages: PageForSerialization[] = [{
|
||||||
|
id: rootPage.id,
|
||||||
|
parentPageId: null,
|
||||||
|
title: rootPage.title,
|
||||||
|
content: rootPage.content,
|
||||||
|
sortOrder: 0,
|
||||||
|
}];
|
||||||
|
|
||||||
agentManager.spawn({
|
agentManager.spawn({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
prompt,
|
prompt: buildRefinePrompt(),
|
||||||
mode: 'discuss',
|
mode: 'refine',
|
||||||
initiativeId: initiative.id,
|
initiativeId: initiative.id,
|
||||||
inputContext: { initiative, task },
|
inputContext: { initiative, pages },
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Fire-and-forget — don't fail initiative creation if agent spawn fails
|
// Fire-and-forget — don't fail initiative creation if agent spawn fails
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function CreateInitiativeDialog({
|
|||||||
<Label htmlFor="initiative-description">
|
<Label htmlFor="initiative-description">
|
||||||
Description{" "}
|
Description{" "}
|
||||||
<span className="text-muted-foreground font-normal">
|
<span className="text-muted-foreground font-normal">
|
||||||
(optional — spawns a discuss agent)
|
(optional — seeds root page and spawns refine agent)
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ export function RefineAgentPanel({ initiativeId }: RefineAgentPanelProps) {
|
|||||||
|
|
||||||
// Running
|
// Running
|
||||||
if (state === "running") {
|
if (state === "running") {
|
||||||
|
const runningLabel = agent?.mode === 'discuss' ? 'Architect is discussing...' : 'Architect is refining...';
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2">
|
<div className="mb-3 flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2">
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Architect is refining...
|
{runningLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query fo
|
|||||||
|
|
||||||
### Creating an Initiative
|
### Creating an Initiative
|
||||||
1. Dashboard → "New Initiative" → enter name, optional description, select projects
|
1. Dashboard → "New Initiative" → enter name, optional description, select projects
|
||||||
2. `createInitiative` mutation → auto-creates root page; if description provided, auto-spawns discuss agent
|
2. `createInitiative` mutation → auto-creates root page (seeded with description as tiptap content); if description provided, auto-spawns refine agent
|
||||||
3. Navigate to initiative detail page on success
|
3. Navigate to initiative detail page on success
|
||||||
|
|
||||||
### Managing Content (Pages)
|
### Managing Content (Pages)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
|||||||
### Initiatives
|
### Initiatives
|
||||||
| Procedure | Type | Description |
|
| Procedure | Type | Description |
|
||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
| createInitiative | mutation | Create with optional branch/projectIds/description, auto-creates root page; if description provided, auto-spawns discuss agent |
|
| createInitiative | mutation | Create with optional branch/projectIds/description, auto-creates root page (seeded with description); if description provided, auto-spawns refine agent |
|
||||||
| listInitiatives | query | Filter by status; returns `activity` (state, activePhase, phase counts) computed from phases |
|
| listInitiatives | query | Filter by status; returns `activity` (state, activePhase, phase counts) computed from phases |
|
||||||
| getInitiative | query | With projects array |
|
| getInitiative | query | With projects array |
|
||||||
| updateInitiative | mutation | Name, status |
|
| updateInitiative | mutation | Name, status |
|
||||||
|
|||||||
Reference in New Issue
Block a user