From 0407f053324e9f85cc5439f05abd2693e11feb35 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 10 Feb 2026 10:51:42 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20Rename=20agent=20modes=20breakdown?= =?UTF-8?q?=E2=86=92plan,=20decompose=E2=86=92detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full rename across the codebase for clarity: - breakdown (initiative→phases) is now "plan" - decompose (phase→tasks) is now "detail" Updates schema enums, TypeScript types, events, prompts, output handler, tRPC procedures, CLI commands, frontend components, tests, and docs. Also fixes 0022 migration multi-statement issue (adds statement-breakpoint markers). --- docs/agent.md | 4 +- docs/cli-config.md | 4 +- docs/database.md | 7 +- docs/dispatch-events.md | 2 +- docs/frontend.md | 10 +- docs/server-api.md | 10 +- docs/testing.md | 4 +- drizzle/0022_branch_refactor.sql | 4 + drizzle/0023_rename_breakdown_decompose.sql | 12 ++ drizzle/meta/_journal.json | 14 ++ .../web/src/components/ChangeSetBanner.tsx | 4 +- packages/web/src/components/ExecutionTab.tsx | 26 +-- .../web/src/components/InitiativeCard.tsx | 8 +- .../web/src/components/InitiativeList.tsx | 2 +- .../src/components/SpawnArchitectDropdown.tsx | 12 +- .../src/components/execution/PhaseActions.tsx | 56 ++++-- .../components/execution/PhaseDetailPanel.tsx | 68 +++---- .../src/components/execution/PhasesList.tsx | 4 +- .../{BreakdownSection.tsx => PlanSection.tsx} | 54 ++--- .../web/src/components/execution/index.ts | 2 +- packages/web/src/lib/invalidation.ts | 4 +- packages/web/src/lib/labels.ts | 11 ++ packages/web/src/routes/agents.tsx | 3 +- packages/web/src/routes/initiatives/$id.tsx | 10 +- src/agent/index.ts | 4 +- src/agent/mock-manager.test.ts | 84 ++++---- src/agent/mock-manager.ts | 4 +- src/agent/output-handler.ts | 27 ++- src/agent/process-manager.test.ts | 2 +- src/agent/prompts.ts | 20 +- src/agent/prompts/{decompose.ts => detail.ts} | 19 +- src/agent/prompts/index.ts | 4 +- src/agent/prompts/{breakdown.ts => plan.ts} | 8 +- src/agent/types.ts | 6 +- src/cli/index.ts | 26 +-- src/db/repositories/change-set-repository.ts | 2 +- src/db/repositories/drizzle/cascade.test.ts | 10 +- src/db/schema.ts | 15 +- src/events/types.ts | 4 +- src/test/e2e/architect-workflow.test.ts | 102 +++++----- src/test/e2e/decompose-workflow.test.ts | 186 +++++++++--------- src/test/fixtures.ts | 16 +- src/test/harness.ts | 38 ++-- .../integration/crash-race-condition.test.ts | 2 +- .../integration/real-providers/prompts.ts | 12 +- .../real-providers/schema-retry.test.ts | 16 +- src/trpc/routers/agent.ts | 2 +- src/trpc/routers/architect.ts | 62 +++--- src/trpc/routers/phase-dispatch.ts | 4 +- src/trpc/routers/phase.ts | 14 +- src/trpc/routers/task.ts | 10 +- 51 files changed, 551 insertions(+), 483 deletions(-) create mode 100644 drizzle/0022_branch_refactor.sql create mode 100644 drizzle/0023_rename_breakdown_decompose.sql rename packages/web/src/components/execution/{BreakdownSection.tsx => PlanSection.tsx} (69%) create mode 100644 packages/web/src/lib/labels.ts rename src/agent/prompts/{decompose.ts => detail.ts} (57%) rename src/agent/prompts/{breakdown.ts => plan.ts} (85%) diff --git a/docs/agent.md b/docs/agent.md index c84bf3f..17e8cba 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -9,7 +9,7 @@ | `types.ts` | Core types: `AgentInfo`, `AgentManager` interface, `SpawnOptions`, `StreamEvent` | | `manager.ts` | `MultiProviderAgentManager` — main orchestrator class | | `process-manager.ts` | `AgentProcessManager` — worktree creation, command building, detached spawn | -| `output-handler.ts` | `OutputHandler` — JSONL stream parsing, completion detection, proposal creation | +| `output-handler.ts` | `OutputHandler` — JSONL stream parsing, completion detection, proposal creation, task dedup | | `file-tailer.ts` | `FileTailer` — watches output files, emits line events | | `file-io.ts` | Input/output file I/O: frontmatter writing, signal.json reading, tiptap conversion | | `markdown-to-tiptap.ts` | Markdown to Tiptap JSON conversion using MarkdownManager | @@ -24,7 +24,7 @@ | `accounts/` | Account discovery, config dir setup, credential management, usage API | | `credentials/` | `AccountCredentialManager` — credential injection per account | | `lifecycle/` | `LifecycleController` — retry policy, signal recovery, missing signal instructions | -| `prompts/` | Mode-specific prompt builders (execute, discuss, breakdown, decompose, refine) | +| `prompts/` | Mode-specific prompt builders (execute, discuss, plan, detail, refine) | ## Key Flows diff --git a/docs/cli-config.md b/docs/cli-config.md index 8b0ae6e..50bfbee 100644 --- a/docs/cli-config.md +++ b/docs/cli-config.md @@ -65,8 +65,8 @@ Uses **Commander.js** for command parsing. | Command | Description | |---------|-------------| | `discuss [-c context]` | Start discussion agent | -| `breakdown [-s summary]` | Start breakdown agent | -| `decompose [-t taskName] [-c context]` | Decompose phase into tasks | +| `plan [-s summary]` | Start plan agent | +| `detail [-t taskName] [-c context]` | Detail phase into tasks | ### Phase (`cw phase`) | Command | Description | diff --git a/docs/database.md b/docs/database.md index 04236af..b6493d4 100644 --- a/docs/database.md +++ b/docs/database.md @@ -20,7 +20,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r | name | text NOT NULL | | | status | text enum | 'active' \| 'completed' \| 'archived', default 'active' | | mergeRequiresApproval | integer/boolean | default true | -| mergeTarget | text nullable | target branch for merges | +| branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') | | createdAt, updatedAt | integer/timestamp | | ### phases @@ -46,7 +46,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r | name | text NOT NULL | | | description | text nullable | | | type | text enum | 'auto' \| 'checkpoint:human-verify' \| 'checkpoint:decision' \| 'checkpoint:human-action' | -| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'breakdown' \| 'decompose' \| 'refine' \| 'verify' \| 'merge' \| 'review' | +| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' | | priority | text enum | 'low' \| 'medium' \| 'high' | | status | text enum | 'pending_approval' \| 'pending' \| 'in_progress' \| 'completed' \| 'blocked' | | requiresApproval | integer/boolean nullable | null = inherit from initiative | @@ -68,7 +68,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r | provider | text NOT NULL | default 'claude' | | accountId | text nullable FK → accounts (set null) | | | status | text enum | 'idle' \| 'running' \| 'waiting_for_input' \| 'stopped' \| 'crashed' | -| mode | text enum | 'execute' \| 'discuss' \| 'breakdown' \| 'decompose' \| 'refine' | +| mode | text enum | 'execute' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' | | pid | integer nullable | OS process ID | | exitCode | integer nullable | | | outputFilePath | text nullable | | @@ -122,6 +122,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r | id | text PK | | | name | text NOT NULL UNIQUE | | | url | text NOT NULL UNIQUE | git repo URL | +| defaultBranch | text NOT NULL | default 'main' | | createdAt, updatedAt | integer/timestamp | | ### initiative_projects (junction) diff --git a/docs/dispatch-events.md b/docs/dispatch-events.md index c40abea..a67649f 100644 --- a/docs/dispatch-events.md +++ b/docs/dispatch-events.md @@ -32,7 +32,7 @@ AgentSpawnedEvent { agentId, name, taskId, worktreeId, provider } AgentStoppedEvent { agentId, name, taskId, reason } // reason: 'user_requested'|'task_complete'|'error'|'waiting_for_input'| - // 'context_complete'|'breakdown_complete'|'decompose_complete'|'refine_complete' + // 'context_complete'|'plan_complete'|'detail_complete'|'refine_complete' AgentWaitingEvent { agentId, name, taskId, sessionId, questions[] } AgentOutputEvent { agentId, stream, data } TaskCompletedEvent { taskId, agentId, success, message } diff --git a/docs/frontend.md b/docs/frontend.md index 58e9cbe..74cf24a 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -63,7 +63,7 @@ The initiative detail page has three tabs managed via local state (not URL param |-----------|---------| | `ExecutionTab` | Main execution view container | | `ExecutionContext` | React context for execution state | -| `PhaseDetailPanel` | Phase detail with tasks, dependencies, breakdown | +| `PhaseDetailPanel` | Phase detail with tasks, dependencies, plan | | `PhaseSidebar` | Phase list sidebar | | `TaskDetailPanel` | Task detail with agent status, output | @@ -88,7 +88,7 @@ shadcn/ui components: badge, button, card, dialog, dropdown-menu, input, label, | Hook | Purpose | |------|---------| | `useRefineAgent` | Manages refine agent lifecycle for initiative | -| `useDecomposeAgent` | Manages decompose agent for phase breakdown | +| `useDetailAgent` | Manages detail agent for phase planning | | `useAgentOutput` | Subscribes to live agent output stream | ## tRPC Client @@ -120,9 +120,9 @@ Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query fo 3. Approve phases → queue for dispatch 4. Tasks auto-queued when phase starts -### Decomposing Phases -1. Select phase → "Breakdown" button -2. `spawnArchitectDecompose` mutation → agent creates task proposals +### Detailing Phases +1. Select phase → "Detail" button +2. `spawnArchitectDetail` mutation → agent creates task proposals 3. Accept proposals → tasks created under phase 4. View tasks in phase detail panel diff --git a/docs/server-api.md b/docs/server-api.md index 2e941a0..bab3637 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -87,7 +87,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | listInitiatives | query | Filter by status | | getInitiative | query | With projects array | | updateInitiative | mutation | Name, status | -| updateInitiativeMergeConfig | mutation | mergeRequiresApproval, mergeTarget | +| updateInitiativeConfig | mutation | mergeRequiresApproval, executionMode | ### Phases | Procedure | Type | Description | @@ -98,7 +98,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | updatePhase | mutation | Name, content, status | | approvePhase | mutation | Validate and approve | | deletePhase | mutation | Cascade delete | -| createPhasesFromBreakdown | mutation | Bulk create from agent output | +| createPhasesFromPlan | mutation | Bulk create from agent output | | createPhaseDependency | mutation | Add dependency edge | | removePhaseDependency | mutation | Remove dependency edge | | listInitiativePhaseDependencies | query | All dependency edges | @@ -111,15 +111,15 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | queuePhase | mutation | Queue approved phase | | dispatchNextPhase | mutation | Start next ready phase | | getPhaseQueueState | query | Queue state | -| createChildTasks | mutation | Create tasks from decompose parent | +| createChildTasks | mutation | Create tasks from detail parent | ### Architect (High-Level Agent Spawning) | Procedure | Type | Description | |-----------|------|-------------| | spawnArchitectDiscuss | mutation | Discussion agent | -| spawnArchitectBreakdown | mutation | Breakdown agent (generates phases). Passes full initiative context (existing phases, tasks, pages) | +| spawnArchitectPlan | mutation | Plan agent (generates phases). Passes full initiative context (existing phases, tasks, pages) | | spawnArchitectRefine | mutation | Refine agent (generates proposals) | -| spawnArchitectDecompose | mutation | Decompose agent (generates tasks). Passes full initiative context (sibling phases, tasks, pages) | +| spawnArchitectDetail | mutation | Detail agent (generates tasks). Passes full initiative context (sibling phases, tasks, pages) | ### Dispatch | Procedure | Type | Description | diff --git a/docs/testing.md b/docs/testing.md index 9d484d7..9c646dc 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,8 +22,8 @@ Located alongside source files (`*.test.ts`): | File | Scenarios | |------|-----------| | `happy-path.test.ts` | Single task, parallel, complex flows | -| `architect-workflow.test.ts` | Discussion + breakdown agent workflows | -| `decompose-workflow.test.ts` | Task decomposition with child tasks | +| `architect-workflow.test.ts` | Discussion + plan agent workflows | +| `detail-workflow.test.ts` | Task detail with child tasks | | `phase-dispatch.test.ts` | Phase-level dispatch with dependencies | | `recovery-scenarios.test.ts` | Crash recovery, agent resume | | `edge-cases.test.ts` | Boundary conditions | diff --git a/drizzle/0022_branch_refactor.sql b/drizzle/0022_branch_refactor.sql new file mode 100644 index 0000000..df3c649 --- /dev/null +++ b/drizzle/0022_branch_refactor.sql @@ -0,0 +1,4 @@ +-- Rename merge_target → branch on initiatives, add default_branch to projects +ALTER TABLE `initiatives` RENAME COLUMN `merge_target` TO `branch`; +--> statement-breakpoint +ALTER TABLE `projects` ADD COLUMN `default_branch` TEXT NOT NULL DEFAULT 'main'; diff --git a/drizzle/0023_rename_breakdown_decompose.sql b/drizzle/0023_rename_breakdown_decompose.sql new file mode 100644 index 0000000..44cae31 --- /dev/null +++ b/drizzle/0023_rename_breakdown_decompose.sql @@ -0,0 +1,12 @@ +-- Rename agent modes: breakdown → plan, decompose → detail +UPDATE tasks SET category = 'plan' WHERE category = 'breakdown'; +--> statement-breakpoint +UPDATE tasks SET category = 'detail' WHERE category = 'decompose'; +--> statement-breakpoint +UPDATE agents SET mode = 'plan' WHERE mode = 'breakdown'; +--> statement-breakpoint +UPDATE agents SET mode = 'detail' WHERE mode = 'decompose'; +--> statement-breakpoint +UPDATE change_sets SET mode = 'plan' WHERE mode = 'breakdown'; +--> statement-breakpoint +UPDATE change_sets SET mode = 'detail' WHERE mode = 'decompose'; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2264f0e..aa5c7a8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -155,6 +155,20 @@ "when": 1771372800000, "tag": "0021_drop_proposals", "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1771459200000, + "tag": "0022_branch_refactor", + "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1771545600000, + "tag": "0023_rename_breakdown_decompose", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/web/src/components/ChangeSetBanner.tsx b/packages/web/src/components/ChangeSetBanner.tsx index 7c43e2c..825bfe2 100644 --- a/packages/web/src/components/ChangeSetBanner.tsx +++ b/packages/web/src/components/ChangeSetBanner.tsx @@ -10,8 +10,8 @@ interface ChangeSetBannerProps { } const MODE_LABELS: Record = { - breakdown: "phases", - decompose: "tasks", + plan: "phases", + detail: "tasks", refine: "pages", }; diff --git a/packages/web/src/components/ExecutionTab.tsx b/packages/web/src/components/ExecutionTab.tsx index 9b40ed9..f5352e3 100644 --- a/packages/web/src/components/ExecutionTab.tsx +++ b/packages/web/src/components/ExecutionTab.tsx @@ -5,7 +5,7 @@ import { topologicalSortPhases, type DependencyEdge } from "@codewalk-district/s import { ExecutionProvider, PhaseActions, - BreakdownSection, + PlanSection, TaskModal, type PhaseData, } from "@/components/execution"; @@ -22,7 +22,7 @@ interface ExecutionTabProps { phasesLoading: boolean; phasesLoaded: boolean; dependencyEdges: DependencyEdge[]; - mergeTarget?: string | null; + branch?: string | null; } export function ExecutionTab({ @@ -31,7 +31,7 @@ export function ExecutionTab({ phasesLoading, phasesLoaded, dependencyEdges, - mergeTarget, + branch, }: ExecutionTabProps) { // Topological sort const sortedPhases = useMemo( @@ -53,7 +53,7 @@ export function ExecutionTab({ return map; }, [dependencyEdges, sortedPhases]); - // Decompose agent tracking: map phaseId → most recent active decompose agent + // Detail agent tracking: map phaseId → most recent active detail agent const agentsQuery = trpc.listAgents.useQuery(); const allAgents = agentsQuery.data ?? []; @@ -133,8 +133,8 @@ export function ExecutionTab({ return { taskCountsByPhase: counts, tasksByPhase: grouped }; }, [allTasks]); - // Map phaseId → most recent active decompose agent - const decomposeAgentByPhase = useMemo(() => { + // Map phaseId → most recent active detail agent + const detailAgentByPhase = useMemo(() => { const map = new Map(); // Build taskId → phaseId lookup from allTasks const taskPhaseMap = new Map(); @@ -143,7 +143,7 @@ export function ExecutionTab({ } const candidates = allAgents.filter( (a) => - a.mode === "decompose" && + a.mode === "detail" && a.initiativeId === initiativeId && ["running", "waiting_for_input", "idle"].includes(a.status) && !a.userDismissedAt, @@ -163,7 +163,7 @@ export function ExecutionTab({ return map; }, [allAgents, allTasks, initiativeId]); - // Phase IDs that have zero tasks (eligible for breakdown) + // Phase IDs that have zero tasks (eligible for detailing) const phasesWithoutTasks = useMemo( () => sortedPhases @@ -178,11 +178,11 @@ export function ExecutionTab({ [sortedPhases], ); - // No phases yet and not adding — show breakdown section + // No phases yet and not adding — show plan section if (phasesLoaded && sortedPhases.length === 0 && !isAddingPhase) { return ( - @@ -258,8 +258,8 @@ export function ExecutionTab({ tasks={tasksByPhase[activePhase.id] ?? []} tasksLoading={allTasksQuery.isLoading} onDelete={() => deletePhase.mutate({ id: activePhase.id })} - decomposeAgent={decomposeAgentByPhase.get(activePhase.id) ?? null} - mergeTarget={mergeTarget} + detailAgent={detailAgentByPhase.get(activePhase.id) ?? null} + branch={branch} /> ) : ( diff --git a/packages/web/src/components/InitiativeCard.tsx b/packages/web/src/components/InitiativeCard.tsx index 25e30b1..5940ff4 100644 --- a/packages/web/src/components/InitiativeCard.tsx +++ b/packages/web/src/components/InitiativeCard.tsx @@ -18,7 +18,7 @@ export interface SerializedInitiative { name: string; status: "active" | "completed" | "archived"; mergeRequiresApproval: boolean; - mergeTarget: string | null; + branch: string | null; createdAt: string; updatedAt: string; } @@ -26,7 +26,7 @@ export interface SerializedInitiative { interface InitiativeCardProps { initiative: SerializedInitiative; onView: () => void; - onSpawnArchitect: (mode: "discuss" | "breakdown") => void; + onSpawnArchitect: (mode: "discuss" | "plan") => void; onDelete: () => void; } @@ -94,9 +94,9 @@ export function InitiativeCard({ Discuss onSpawnArchitect("breakdown")} + onClick={() => onSpawnArchitect("plan")} > - Breakdown + Plan diff --git a/packages/web/src/components/InitiativeList.tsx b/packages/web/src/components/InitiativeList.tsx index e458456..31ecd55 100644 --- a/packages/web/src/components/InitiativeList.tsx +++ b/packages/web/src/components/InitiativeList.tsx @@ -11,7 +11,7 @@ interface InitiativeListProps { onViewInitiative: (id: string) => void; onSpawnArchitect: ( initiativeId: string, - mode: "discuss" | "breakdown", + mode: "discuss" | "plan", ) => void; onDeleteInitiative: (id: string) => void; } diff --git a/packages/web/src/components/SpawnArchitectDropdown.tsx b/packages/web/src/components/SpawnArchitectDropdown.tsx index 61115c8..3b87c08 100644 --- a/packages/web/src/components/SpawnArchitectDropdown.tsx +++ b/packages/web/src/components/SpawnArchitectDropdown.tsx @@ -31,18 +31,18 @@ export function SpawnArchitectDropdown({ onSuccess: handleSuccess, }); - const breakdownSpawn = useSpawnMutation(trpc.spawnArchitectBreakdown.useMutation, { + const planSpawn = useSpawnMutation(trpc.spawnArchitectPlan.useMutation, { onSuccess: handleSuccess, }); - const isPending = discussSpawn.isSpawning || breakdownSpawn.isSpawning; + const isPending = discussSpawn.isSpawning || planSpawn.isSpawning; function handleDiscuss() { discussSpawn.spawn({ initiativeId }); } - function handleBreakdown() { - breakdownSpawn.spawn({ initiativeId }); + function handlePlan() { + planSpawn.spawn({ initiativeId }); } return ( @@ -57,8 +57,8 @@ export function SpawnArchitectDropdown({ Discuss - - Breakdown + + Plan diff --git a/packages/web/src/components/execution/PhaseActions.tsx b/packages/web/src/components/execution/PhaseActions.tsx index bbb98ff..e92ad97 100644 --- a/packages/web/src/components/execution/PhaseActions.tsx +++ b/packages/web/src/components/execution/PhaseActions.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Loader2, Plus, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { trpc } from "@/lib/trpc"; @@ -8,45 +8,55 @@ interface PhaseActionsProps { phases: Array<{ id: string; status: string }>; onAddPhase: () => void; phasesWithoutTasks: string[]; - decomposeAgentByPhase: Map; + detailAgentByPhase: Map; } export function PhaseActions({ onAddPhase, phasesWithoutTasks, - decomposeAgentByPhase, + detailAgentByPhase, }: PhaseActionsProps) { - const decomposeMutation = trpc.spawnArchitectDecompose.useMutation(); + const detailMutation = trpc.spawnArchitectDetail.useMutation(); + const [isDetailingAll, setIsDetailingAll] = useState(false); - // Phases eligible for breakdown: no tasks AND no active decompose agent + // Phases eligible for detailing: no tasks AND no active detail agent const eligiblePhaseIds = useMemo( - () => phasesWithoutTasks.filter((id) => !decomposeAgentByPhase.has(id)), - [phasesWithoutTasks, decomposeAgentByPhase], + () => phasesWithoutTasks.filter((id) => !detailAgentByPhase.has(id)), + [phasesWithoutTasks, detailAgentByPhase], ); - // Count of phases currently being decomposed - const activeDecomposeCount = useMemo(() => { + // Count of phases currently being detailed + const activeDetailCount = useMemo(() => { let count = 0; - for (const [, agent] of decomposeAgentByPhase) { + for (const [, agent] of detailAgentByPhase) { if (agent.status === "running" || agent.status === "waiting_for_input") { count++; } } return count; - }, [decomposeAgentByPhase]); + }, [detailAgentByPhase]); - const handleBreakdownAll = useCallback(() => { - for (const phaseId of eligiblePhaseIds) { - decomposeMutation.mutate({ phaseId }); + const handleDetailAll = useCallback(async () => { + setIsDetailingAll(true); + try { + for (const phaseId of eligiblePhaseIds) { + try { + await detailMutation.mutateAsync({ phaseId }); + } catch { + // CONFLICT errors expected if agent already exists — continue + } + } + } finally { + setIsDetailingAll(false); } - }, [eligiblePhaseIds, decomposeMutation]); + }, [eligiblePhaseIds, detailMutation]); return (
- {activeDecomposeCount > 0 && ( + {activeDetailCount > 0 && (
- Decomposing ({activeDecomposeCount}) + Detailing ({activeDetailCount})
)}
); diff --git a/packages/web/src/components/execution/PhaseDetailPanel.tsx b/packages/web/src/components/execution/PhaseDetailPanel.tsx index 632c9ba..f59ac62 100644 --- a/packages/web/src/components/execution/PhaseDetailPanel.tsx +++ b/packages/web/src/components/execution/PhaseDetailPanel.tsx @@ -36,8 +36,8 @@ interface PhaseDetailPanelProps { tasks: SerializedTask[]; tasksLoading: boolean; onDelete?: () => void; - mergeTarget?: string | null; - decomposeAgent: { + branch?: string | null; + detailAgent: { id: string; status: string; createdAt: string | Date; @@ -53,8 +53,8 @@ export function PhaseDetailPanel({ tasks, tasksLoading, onDelete, - mergeTarget, - decomposeAgent, + branch, + detailAgent, }: PhaseDetailPanelProps) { const { setSelectedTaskId, handleTaskCounts, handleRegisterTasks } = useExecutionContext(); @@ -137,46 +137,46 @@ export function PhaseDetailPanel({ handleRegisterTasks(phase.id, entries); }, [tasks, phase.id, displayIndex, phase.name, handleTaskCounts, handleRegisterTasks]); - // --- Change sets for decompose agent --- + // --- Change sets for detail agent --- const changeSetsQuery = trpc.listChangeSets.useQuery( - { agentId: decomposeAgent?.id ?? "" }, - { enabled: !!decomposeAgent && decomposeAgent.status === "idle" }, + { agentId: detailAgent?.id ?? "" }, + { enabled: !!detailAgent && detailAgent.status === "idle" }, ); const latestChangeSet = useMemo( () => (changeSetsQuery.data ?? []).find((cs) => cs.status === "applied") ?? null, [changeSetsQuery.data], ); - // --- Decompose spawn --- - const decomposeMutation = trpc.spawnArchitectDecompose.useMutation(); + // --- Detail spawn --- + const detailMutation = trpc.spawnArchitectDetail.useMutation(); - const handleDecompose = useCallback(() => { - decomposeMutation.mutate({ phaseId: phase.id }); - }, [phase.id, decomposeMutation]); + const handleDetail = useCallback(() => { + detailMutation.mutate({ phaseId: phase.id }); + }, [phase.id, detailMutation]); - // --- Dismiss handler for decompose agent --- + // --- Dismiss handler for detail agent --- const dismissMutation = trpc.dismissAgent.useMutation(); - const handleDismissDecompose = useCallback(() => { - if (!decomposeAgent) return; - dismissMutation.mutate({ id: decomposeAgent.id }); - }, [decomposeAgent, dismissMutation]); + const handleDismissDetail = useCallback(() => { + if (!detailAgent) return; + dismissMutation.mutate({ id: detailAgent.id }); + }, [detailAgent, dismissMutation]); // Compute phase branch name if initiative has a merge target - const phaseBranch = mergeTarget - ? `${mergeTarget}-phase-${phase.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}` + const phaseBranch = branch + ? `${branch}-phase-${phase.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}` : null; const isPendingReview = phase.status === "pending_review"; const sortedTasks = sortByPriorityAndQueueTime(tasks); const hasTasks = tasks.length > 0; - const isDecomposeRunning = - decomposeAgent?.status === "running" || - decomposeAgent?.status === "waiting_for_input"; - const showBreakdownButton = - !decomposeAgent && !hasTasks; + const isDetailRunning = + detailAgent?.status === "running" || + detailAgent?.status === "waiting_for_input"; + const showDetailButton = + !detailAgent && !hasTasks; const showChangeSet = - decomposeAgent?.status === "idle" && !!latestChangeSet; + detailAgent?.status === "idle" && !!latestChangeSet; return (
@@ -214,25 +214,25 @@ export function PhaseDetailPanel({ )} - {/* Breakdown button in header */} - {showBreakdownButton && ( + {/* Detail button in header */} + {showDetailButton && ( )} {/* Running indicator in header */} - {isDecomposeRunning && ( + {isDetailRunning && (
- Breaking down... + Detailing...
)} @@ -342,11 +342,11 @@ export function PhaseDetailPanel({ )}
- {/* Decompose change set */} + {/* Detail change set */} {showChangeSet && ( )} diff --git a/packages/web/src/components/execution/PhasesList.tsx b/packages/web/src/components/execution/PhasesList.tsx index 5076e0c..1a49c64 100644 --- a/packages/web/src/components/execution/PhasesList.tsx +++ b/packages/web/src/components/execution/PhasesList.tsx @@ -1,7 +1,7 @@ import { Skeleton } from "@/components/Skeleton"; import { useExecutionContext, type PhaseData } from "./ExecutionContext"; import { PhaseWithTasks } from "./PhaseWithTasks"; -import { BreakdownSection } from "./BreakdownSection"; +import { PlanSection } from "./PlanSection"; interface PhasesListProps { initiativeId: string; @@ -35,7 +35,7 @@ export function PhasesList({ if (phasesLoaded && phases.length === 0) { return ( - ; onAddPhase?: () => void; } -export function BreakdownSection({ +export function PlanSection({ initiativeId, phasesLoaded, phases, onAddPhase, -}: BreakdownSectionProps) { - // Breakdown agent tracking +}: PlanSectionProps) { + // Plan agent tracking const agentsQuery = trpc.listAgents.useQuery(); const allAgents = agentsQuery.data ?? []; - const breakdownAgent = useMemo(() => { + const planAgent = useMemo(() => { const candidates = allAgents .filter( (a) => - a.mode === "breakdown" && + a.mode === "plan" && a.initiativeId === initiativeId && ["running", "waiting_for_input", "idle"].includes(a.status), ) @@ -36,12 +36,12 @@ export function BreakdownSection({ return candidates[0] ?? null; }, [allAgents, initiativeId]); - const isBreakdownRunning = breakdownAgent?.status === "running"; + const isPlanRunning = planAgent?.status === "running"; - // Query change sets when we have a completed breakdown agent + // Query change sets when we have a completed plan agent const changeSetsQuery = trpc.listChangeSets.useQuery( - { agentId: breakdownAgent?.id ?? "" }, - { enabled: !!breakdownAgent && breakdownAgent.status === "idle" }, + { agentId: planAgent?.id ?? "" }, + { enabled: !!planAgent && planAgent.status === "idle" }, ); const latestChangeSet = useMemo( () => (changeSetsQuery.data ?? []).find((cs) => cs.status === "applied") ?? null, @@ -50,18 +50,18 @@ export function BreakdownSection({ const dismissMutation = trpc.dismissAgent.useMutation(); - const breakdownSpawn = useSpawnMutation(trpc.spawnArchitectBreakdown.useMutation, { + const planSpawn = useSpawnMutation(trpc.spawnArchitectPlan.useMutation, { showToast: false, }); - const handleBreakdown = useCallback(() => { - breakdownSpawn.spawn({ initiativeId }); - }, [initiativeId, breakdownSpawn]); + const handlePlan = useCallback(() => { + planSpawn.spawn({ initiativeId }); + }, [initiativeId, planSpawn]); const handleDismiss = useCallback(() => { - if (!breakdownAgent) return; - dismissMutation.mutate({ id: breakdownAgent.id }); - }, [breakdownAgent, dismissMutation]); + if (!planAgent) return; + dismissMutation.mutate({ id: planAgent.id }); + }, [planAgent, dismissMutation]); // Don't render during loading if (!phasesLoaded) { @@ -73,8 +73,8 @@ export function BreakdownSection({ return null; } - // Show change set banner when breakdown agent completed - if (breakdownAgent?.status === "idle" && latestChangeSet) { + // Show change set banner when plan agent completed + if (planAgent?.status === "idle" && latestChangeSet) { return (

No phases yet

- {isBreakdownRunning ? ( + {isPlanRunning ? (
- Breaking down initiative... + Planning phases...
) : (
{onAddPhase && ( <> @@ -123,9 +123,9 @@ export function BreakdownSection({ )}
)} - {breakdownSpawn.isError && ( + {planSpawn.isError && (

- {breakdownSpawn.error} + {planSpawn.error}

)}
diff --git a/packages/web/src/components/execution/index.ts b/packages/web/src/components/execution/index.ts index bbc4d07..ecfc58c 100644 --- a/packages/web/src/components/execution/index.ts +++ b/packages/web/src/components/execution/index.ts @@ -1,5 +1,5 @@ export { ExecutionProvider, useExecutionContext } from "./ExecutionContext"; -export { BreakdownSection } from "./BreakdownSection"; +export { PlanSection } from "./PlanSection"; export { PhaseActions } from "./PhaseActions"; export { PhaseSidebarItem } from "./PhaseSidebarItem"; export { PhaseDetailPanel, PhaseDetailEmpty } from "./PhaseDetailPanel"; diff --git a/packages/web/src/lib/invalidation.ts b/packages/web/src/lib/invalidation.ts index 39851ab..2363197 100644 --- a/packages/web/src/lib/invalidation.ts +++ b/packages/web/src/lib/invalidation.ts @@ -42,8 +42,8 @@ const INVALIDATION_MAP: Partial> = { // --- Architect spawns --- spawnArchitectRefine: ["listAgents"], spawnArchitectDiscuss: ["listAgents"], - spawnArchitectBreakdown: ["listAgents"], - spawnArchitectDecompose: ["listAgents", "listInitiativeTasks"], + spawnArchitectPlan: ["listAgents"], + spawnArchitectDetail: ["listAgents", "listInitiativeTasks"], // --- Initiatives --- createInitiative: ["listInitiatives"], diff --git a/packages/web/src/lib/labels.ts b/packages/web/src/lib/labels.ts new file mode 100644 index 0000000..7e86e7c --- /dev/null +++ b/packages/web/src/lib/labels.ts @@ -0,0 +1,11 @@ +const MODE_LABELS: Record = { + plan: 'Plan', + detail: 'Detail', + discuss: 'Discuss', + refine: 'Refine', + execute: 'Execute', +}; + +export function modeLabel(mode: string): string { + return MODE_LABELS[mode] ?? mode; +} diff --git a/packages/web/src/routes/agents.tsx b/packages/web/src/routes/agents.tsx index f1c4681..9912126 100644 --- a/packages/web/src/routes/agents.tsx +++ b/packages/web/src/routes/agents.tsx @@ -11,6 +11,7 @@ import { AgentOutputViewer } from "@/components/AgentOutputViewer"; import { AgentActions } from "@/components/AgentActions"; import { formatRelativeTime } from "@/lib/utils"; import { cn } from "@/lib/utils"; +import { modeLabel } from "@/lib/labels"; import { StatusDot } from "@/components/StatusDot"; import { useLiveUpdates } from "@/hooks"; @@ -247,7 +248,7 @@ function AgentsPage() { {agent.provider} - {agent.mode} + {modeLabel(agent.mode)} {/* Action dropdown */}
e.stopPropagation()}> diff --git a/packages/web/src/routes/initiatives/$id.tsx b/packages/web/src/routes/initiatives/$id.tsx index 5753914..0728c8c 100644 --- a/packages/web/src/routes/initiatives/$id.tsx +++ b/packages/web/src/routes/initiatives/$id.tsx @@ -10,8 +10,8 @@ import { ReviewTab } from "@/components/review"; import { PipelineTab } from "@/components/pipeline"; import { useLiveUpdates } from "@/hooks"; -type Tab = "content" | "breakdown" | "execution" | "review"; -const TABS: Tab[] = ["content", "breakdown", "execution", "review"]; +type Tab = "content" | "plan" | "execution" | "review"; +const TABS: Tab[] = ["content", "plan", "execution", "review"]; export const Route = createFileRoute("/initiatives/$id")({ component: InitiativeDetailPage, @@ -90,7 +90,7 @@ function InitiativeDetailPage() { name: initiative.name, status: initiative.status, executionMode: (initiative as any).executionMode as string | undefined, - mergeTarget: (initiative as any).mergeTarget as string | null | undefined, + branch: (initiative as any).branch as string | null | undefined, }; const projects = (initiative as { projects?: Array<{ id: string; name: string; url: string }> }).projects; @@ -130,14 +130,14 @@ function InitiativeDetailPage() { {/* Tab content */} {activeTab === "content" && } - {activeTab === "breakdown" && ( + {activeTab === "plan" && ( )} {activeTab === "execution" && ( diff --git a/src/agent/index.ts b/src/agent/index.ts index 34aaffe..02ec653 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -34,10 +34,10 @@ export type { AgentProviderConfig } from './providers/index.js'; // Agent prompts export { buildDiscussPrompt, - buildBreakdownPrompt, + buildPlanPrompt, buildExecutePrompt, buildRefinePrompt, - buildDecomposePrompt, + buildDetailPrompt, } from './prompts/index.js'; // Schema diff --git a/src/agent/mock-manager.test.ts b/src/agent/mock-manager.test.ts index c09e8a9..19c3eae 100644 --- a/src/agent/mock-manager.test.ts +++ b/src/agent/mock-manager.test.ts @@ -596,7 +596,7 @@ describe('MockAgentManager', () => { }); // =========================================================================== - // Agent modes (execute, discuss, breakdown) + // Agent modes (execute, discuss, plan) // =========================================================================== describe('agent modes', () => { @@ -626,21 +626,21 @@ describe('MockAgentManager', () => { expect(agent.mode).toBe('discuss'); }); - it('should spawn agent in breakdown mode', async () => { - manager.setScenario('breakdown-agent', { + it('should spawn agent in plan mode', async () => { + manager.setScenario('plan-agent', { status: 'done', delay: 0, - result: 'Breakdown complete', + result: 'Plan complete', }); const agent = await manager.spawn({ - name: 'breakdown-agent', + name: 'plan-agent', taskId: 't1', - prompt: 'breakdown work', - mode: 'breakdown', + prompt: 'plan work', + mode: 'plan', }); - expect(agent.mode).toBe('breakdown'); + expect(agent.mode).toBe('plan'); }); it('should emit stopped event with context_complete reason for discuss mode', async () => { @@ -662,63 +662,63 @@ describe('MockAgentManager', () => { expect(stopped?.payload.reason).toBe('context_complete'); }); - it('should emit stopped event with breakdown_complete reason for breakdown mode', async () => { - manager.setScenario('breakdown-done', { + it('should emit stopped event with plan_complete reason for plan mode', async () => { + manager.setScenario('plan-done', { status: 'done', delay: 0, - result: 'Breakdown complete', + result: 'Plan complete', }); await manager.spawn({ - name: 'breakdown-done', + name: 'plan-done', taskId: 't1', prompt: 'test', - mode: 'breakdown', + mode: 'plan', }); await vi.runAllTimersAsync(); const stopped = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped') as AgentStoppedEvent | undefined; - expect(stopped?.payload.reason).toBe('breakdown_complete'); + expect(stopped?.payload.reason).toBe('plan_complete'); }); }); // =========================================================================== - // Decompose mode (plan to tasks) + // Detail mode (phase to tasks) // =========================================================================== - describe('decompose mode', () => { - it('should spawn agent in decompose mode', async () => { + describe('detail mode', () => { + it('should spawn agent in detail mode', async () => { const agent = await manager.spawn({ - name: 'decomposer', + name: 'detailer', taskId: 'plan-1', - prompt: 'Decompose this plan', - mode: 'decompose', + prompt: 'Detail this phase', + mode: 'detail', }); - expect(agent.mode).toBe('decompose'); + expect(agent.mode).toBe('detail'); }); - it('should complete with decompose_complete reason in decompose mode', async () => { - manager.setScenario('decomposer', { + it('should complete with detail_complete reason in detail mode', async () => { + manager.setScenario('detailer', { status: 'done', - result: 'Decompose complete', + result: 'Detail complete', }); - await manager.spawn({ name: 'decomposer', taskId: 'plan-1', prompt: 'test', mode: 'decompose' }); + await manager.spawn({ name: 'detailer', taskId: 'plan-1', prompt: 'test', mode: 'detail' }); await vi.advanceTimersByTimeAsync(100); - // Verify agent:stopped event with decompose_complete reason (derived from mode) + // Verify agent:stopped event with detail_complete reason (derived from mode) const stoppedEvent = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped') as AgentStoppedEvent | undefined; expect(stoppedEvent).toBeDefined(); - expect(stoppedEvent?.payload.reason).toBe('decompose_complete'); + expect(stoppedEvent?.payload.reason).toBe('detail_complete'); }); - it('should pause on questions in decompose mode', async () => { - manager.setScenario('decomposer', { + it('should pause on questions in detail mode', async () => { + manager.setScenario('detailer', { status: 'questions', questions: [{ id: 'q1', question: 'How many tasks?' }], }); - await manager.spawn({ name: 'decomposer', taskId: 'plan-1', prompt: 'test', mode: 'decompose' }); + await manager.spawn({ name: 'detailer', taskId: 'plan-1', prompt: 'test', mode: 'detail' }); await vi.advanceTimersByTimeAsync(100); // Verify agent pauses for questions @@ -726,41 +726,41 @@ describe('MockAgentManager', () => { expect(stoppedEvent).toBeDefined(); // Check agent status - const agent = await manager.getByName('decomposer'); + const agent = await manager.getByName('detailer'); expect(agent?.status).toBe('waiting_for_input'); }); - it('should emit stopped event with decompose_complete reason (second test)', async () => { - manager.setScenario('decompose-done', { + it('should emit stopped event with detail_complete reason (second test)', async () => { + manager.setScenario('detail-done', { status: 'done', delay: 0, - result: 'Decompose complete', + result: 'Detail complete', }); await manager.spawn({ - name: 'decompose-done', + name: 'detail-done', taskId: 'plan-1', prompt: 'test', - mode: 'decompose', + mode: 'detail', }); await vi.runAllTimersAsync(); const stopped = eventBus.emittedEvents.find((e) => e.type === 'agent:stopped') as AgentStoppedEvent | undefined; - expect(stopped?.payload.reason).toBe('decompose_complete'); + expect(stopped?.payload.reason).toBe('detail_complete'); }); - it('should set result message for decompose mode', async () => { - manager.setScenario('decomposer', { + it('should set result message for detail mode', async () => { + manager.setScenario('detailer', { status: 'done', - result: 'Decompose complete', + result: 'Detail complete', }); - const agent = await manager.spawn({ name: 'decomposer', taskId: 'plan-1', prompt: 'test', mode: 'decompose' }); + const agent = await manager.spawn({ name: 'detailer', taskId: 'plan-1', prompt: 'test', mode: 'detail' }); await vi.runAllTimersAsync(); const result = await manager.getResult(agent.id); expect(result?.success).toBe(true); - expect(result?.message).toBe('Decompose complete'); + expect(result?.message).toBe('Detail complete'); }); }); diff --git a/src/agent/mock-manager.ts b/src/agent/mock-manager.ts index 821f730..f213ce1 100644 --- a/src/agent/mock-manager.ts +++ b/src/agent/mock-manager.ts @@ -195,8 +195,8 @@ export class MockAgentManager implements AgentManager { private getStoppedReason(mode: AgentMode): AgentStoppedEvent['payload']['reason'] { switch (mode) { case 'discuss': return 'context_complete'; - case 'breakdown': return 'breakdown_complete'; - case 'decompose': return 'decompose_complete'; + case 'plan': return 'plan_complete'; + case 'detail': return 'detail_complete'; case 'refine': return 'refine_complete'; default: return 'task_complete'; } diff --git a/src/agent/output-handler.ts b/src/agent/output-handler.ts index cde6d53..d6444d9 100644 --- a/src/agent/output-handler.ts +++ b/src/agent/output-handler.ts @@ -426,7 +426,7 @@ export class OutputHandler { let resultMessage = summary?.body ?? 'Task completed'; switch (mode) { - case 'breakdown': { + case 'plan': { const phases = readPhaseFiles(agentWorkdir); if (canWriteChangeSets && this.phaseRepository && phases.length > 0) { const entries: CreateChangeSetEntryData[] = []; @@ -485,13 +485,13 @@ export class OutputHandler { agentId, agentName: agent.name, initiativeId, - mode: 'breakdown', + mode: 'plan', summary: summary?.body ?? `Created ${phases.length} phases`, }, entries); this.eventBus?.emit({ type: 'changeset:created' as const, timestamp: new Date(), - payload: { changeSetId: cs.id, initiativeId, agentId, mode: 'breakdown', entryCount: entries.length }, + payload: { changeSetId: cs.id, initiativeId, agentId, mode: 'plan', entryCount: entries.length }, }); } catch (err) { log.warn({ agentId, err: err instanceof Error ? err.message : String(err) }, 'failed to record change set after successful writes'); @@ -503,14 +503,22 @@ export class OutputHandler { } break; } - case 'decompose': { + case 'detail': { const tasks = readTaskFiles(agentWorkdir); if (canWriteChangeSets && this.taskRepository && tasks.length > 0) { const phaseInput = readFrontmatterFile(join(agentWorkdir, '.cw', 'input', 'phase.md')); const phaseId = (phaseInput?.data?.id as string) ?? null; const entries: CreateChangeSetEntryData[] = []; + // Load existing tasks for dedup — prevents duplicates when multiple agents finish concurrently + const existingTasks = phaseId ? await this.taskRepository.findByPhaseId(phaseId) : []; + const existingNames = new Set(existingTasks.map(t => t.name)); + for (const [i, t] of tasks.entries()) { + if (existingNames.has(t.title)) { + log.info({ agentId, task: t.title, phaseId }, 'skipped duplicate task'); + continue; + } try { const created = await this.taskRepository.create({ initiativeId, @@ -521,6 +529,7 @@ export class OutputHandler { category: (t.category as any) ?? 'execute', type: (t.type as any) ?? 'auto', }); + existingNames.add(t.title); // prevent dupes within same agent output entries.push({ entityType: 'task', entityId: created.id, @@ -531,7 +540,7 @@ export class OutputHandler { this.eventBus?.emit({ type: 'task:completed' as const, timestamp: new Date(), - payload: { taskId: created.id, agentId, success: true, message: 'Task created by decompose' }, + payload: { taskId: created.id, agentId, success: true, message: 'Task created by detail' }, }); } catch (err) { log.warn({ agentId, task: t.title, err: err instanceof Error ? err.message : String(err) }, 'failed to create task'); @@ -544,13 +553,13 @@ export class OutputHandler { agentId, agentName: agent.name, initiativeId, - mode: 'decompose', + mode: 'detail', summary: summary?.body ?? `Created ${tasks.length} tasks`, }, entries); this.eventBus?.emit({ type: 'changeset:created' as const, timestamp: new Date(), - payload: { changeSetId: cs.id, initiativeId, agentId, mode: 'decompose', entryCount: entries.length }, + payload: { changeSetId: cs.id, initiativeId, agentId, mode: 'detail', entryCount: entries.length }, }); } catch (err) { log.warn({ agentId, err: err instanceof Error ? err.message : String(err) }, 'failed to record change set after successful writes'); @@ -709,8 +718,8 @@ export class OutputHandler { getStoppedReason(mode: AgentMode): AgentStoppedEvent['payload']['reason'] { switch (mode) { case 'discuss': return 'context_complete'; - case 'breakdown': return 'breakdown_complete'; - case 'decompose': return 'decompose_complete'; + case 'plan': return 'plan_complete'; + case 'detail': return 'detail_complete'; case 'refine': return 'refine_complete'; default: return 'task_complete'; } diff --git a/src/agent/process-manager.test.ts b/src/agent/process-manager.test.ts index ca7b5f6..ba8e826 100644 --- a/src/agent/process-manager.test.ts +++ b/src/agent/process-manager.test.ts @@ -138,7 +138,7 @@ describe('ProcessManager', () => { // Mock project repository vi.mocked(mockProjectRepository.findProjectsByInitiativeId).mockResolvedValue([ - { id: '1', name: 'project1', url: 'https://github.com/user/project1.git', createdAt: new Date(), updatedAt: new Date() } + { id: '1', name: 'project1', url: 'https://github.com/user/project1.git', defaultBranch: 'main', createdAt: new Date(), updatedAt: new Date() } ]); // Mock existsSync to return true for worktree paths diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index a54463c..bea3ef8 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -115,14 +115,14 @@ ${ID_GENERATION} } /** - * Build prompt for breakdown mode. - * Agent decomposes initiative into executable phases. + * Build prompt for plan mode. + * Agent plans initiative into executable phases. */ -export function buildBreakdownPrompt(): string { - return `You are an Architect agent in the Codewalk multi-agent system operating in BREAKDOWN mode. +export function buildPlanPrompt(): string { + return `You are an Architect agent in the Codewalk multi-agent system operating in PLAN mode. ## Your Role -Decompose the initiative into executable phases. You do NOT write code — you plan it. +Plan the initiative into executable phases. You do NOT write code — you plan it. ${INPUT_FILES} ${SIGNAL_FORMAT} @@ -149,14 +149,14 @@ ${ID_GENERATION} } /** - * Build prompt for decompose mode. + * Build prompt for detail mode. * Agent breaks a phase into executable tasks. */ -export function buildDecomposePrompt(): string { - return `You are an Architect agent in the Codewalk multi-agent system operating in DECOMPOSE mode. +export function buildDetailPrompt(): string { + return `You are an Architect agent in the Codewalk multi-agent system operating in DETAIL mode. ## Your Role -Decompose the phase into individual executable tasks. You do NOT write code — you define work items. +Detail the phase into individual executable tasks. You do NOT write code — you define work items. ${INPUT_FILES} ${SIGNAL_FORMAT} @@ -165,7 +165,7 @@ ${SIGNAL_FORMAT} Write one file per task to \`.cw/output/tasks/{id}.md\`: - Frontmatter: - \`title\`: Clear task name - - \`category\`: One of: execute, research, discuss, breakdown, decompose, refine, verify, merge, review + - \`category\`: One of: execute, research, discuss, plan, detail, refine, verify, merge, review - \`type\`: One of: auto, checkpoint:human-verify, checkpoint:decision, checkpoint:human-action - \`dependencies\`: List of other task IDs this depends on - Body: Detailed description of what the task requires diff --git a/src/agent/prompts/decompose.ts b/src/agent/prompts/detail.ts similarity index 57% rename from src/agent/prompts/decompose.ts rename to src/agent/prompts/detail.ts index dedf9a0..b6a7e98 100644 --- a/src/agent/prompts/decompose.ts +++ b/src/agent/prompts/detail.ts @@ -1,14 +1,14 @@ /** - * Decompose mode prompt — break a phase into executable tasks. + * Detail mode prompt — break a phase into executable tasks. */ import { ID_GENERATION, INPUT_FILES, SIGNAL_FORMAT } from './shared.js'; -export function buildDecomposePrompt(): string { - return `You are an Architect agent in the Codewalk multi-agent system operating in DECOMPOSE mode. +export function buildDetailPrompt(): string { + return `You are an Architect agent in the Codewalk multi-agent system operating in DETAIL mode. ## Your Role -Decompose the phase into individual executable tasks. You do NOT write code — you define work items. +Detail the phase into individual executable tasks. You do NOT write code — you define work items. ${INPUT_FILES} ${SIGNAL_FORMAT} @@ -17,7 +17,7 @@ ${SIGNAL_FORMAT} Write one file per task to \`.cw/output/tasks/{id}.md\`: - Frontmatter: - \`title\`: Clear task name - - \`category\`: One of: execute, research, discuss, breakdown, decompose, refine, verify, merge, review + - \`category\`: One of: execute, research, discuss, plan, detail, refine, verify, merge, review - \`type\`: One of: auto, checkpoint:human-verify, checkpoint:decision, checkpoint:human-action - \`dependencies\`: List of other task IDs this depends on - Body: Detailed description of what the task requires @@ -31,10 +31,11 @@ ${ID_GENERATION} - Dependencies should be minimal and explicit ## Existing Context -- Read context files to see sibling phases and their tasks -- Your target is \`phase.md\` — only create tasks for THIS phase -- Pages contain requirements and specifications — reference them for task descriptions -- Avoid duplicating work that is already covered by other phases or their tasks +- FIRST: Read ALL files in \`context/tasks/\` before generating any output +- Your target phase is \`phase.md\` — only create tasks for THIS phase +- If a task in context/tasks/ already covers the same work (even under a different name), do NOT create a duplicate +- Pages contain requirements — reference them for detailed task descriptions +- DO NOT create tasks that overlap with existing tasks in other phases ## Rules - Break work into 3-8 tasks per phase diff --git a/src/agent/prompts/index.ts b/src/agent/prompts/index.ts index e464643..455b319 100644 --- a/src/agent/prompts/index.ts +++ b/src/agent/prompts/index.ts @@ -8,7 +8,7 @@ export { SIGNAL_FORMAT, INPUT_FILES, ID_GENERATION } from './shared.js'; export { buildExecutePrompt } from './execute.js'; export { buildDiscussPrompt } from './discuss.js'; -export { buildBreakdownPrompt } from './breakdown.js'; -export { buildDecomposePrompt } from './decompose.js'; +export { buildPlanPrompt } from './plan.js'; +export { buildDetailPrompt } from './detail.js'; export { buildRefinePrompt } from './refine.js'; export { buildWorkspaceLayout } from './workspace.js'; diff --git a/src/agent/prompts/breakdown.ts b/src/agent/prompts/plan.ts similarity index 85% rename from src/agent/prompts/breakdown.ts rename to src/agent/prompts/plan.ts index f753d4f..3e3907e 100644 --- a/src/agent/prompts/breakdown.ts +++ b/src/agent/prompts/plan.ts @@ -1,14 +1,14 @@ /** - * Breakdown mode prompt — decompose initiative into phases. + * Plan mode prompt — plan initiative into phases. */ import { ID_GENERATION, INPUT_FILES, SIGNAL_FORMAT } from './shared.js'; -export function buildBreakdownPrompt(): string { - return `You are an Architect agent in the Codewalk multi-agent system operating in BREAKDOWN mode. +export function buildPlanPrompt(): string { + return `You are an Architect agent in the Codewalk multi-agent system operating in PLAN mode. ## Your Role -Decompose the initiative into executable phases. You do NOT write code — you plan it. +Plan the initiative into executable phases. You do NOT write code — you plan it. ${INPUT_FILES} ${SIGNAL_FORMAT} diff --git a/src/agent/types.ts b/src/agent/types.ts index b91c15b..70aeb07 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -12,10 +12,10 @@ export type AgentStatus = 'idle' | 'running' | 'waiting_for_input' | 'stopped' | * * - execute: Standard task execution (default) * - discuss: Gather context through questions, output decisions - * - breakdown: Decompose initiative into phases - * - decompose: Decompose phase into individual tasks + * - plan: Plan initiative into phases + * - detail: Detail phase into individual tasks */ -export type AgentMode = 'execute' | 'discuss' | 'breakdown' | 'decompose' | 'refine'; +export type AgentMode = 'execute' | 'discuss' | 'plan' | 'detail' | 'refine'; /** * Context data written as input files in agent workdir before spawn. diff --git a/src/cli/index.ts b/src/cli/index.ts index bca12a1..3f7b476 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -847,52 +847,52 @@ export function createCli(serverHandler?: (port?: number) => Promise): Com } }); - // cw architect breakdown + // cw architect plan architectCommand - .command('breakdown ') - .description('Start breakdown phase for an initiative') + .command('plan ') + .description('Plan phases for an initiative') .option('--name ', 'Agent name (auto-generated if omitted)') .option('-s, --summary ', 'Context summary from discuss phase') .action(async (initiativeId: string, options: { name?: string; summary?: string }) => { try { const client = createDefaultTrpcClient(); - const agent = await client.spawnArchitectBreakdown.mutate({ + const agent = await client.spawnArchitectPlan.mutate({ name: options.name, initiativeId, contextSummary: options.summary, }); - console.log(`Started architect agent in breakdown mode`); + console.log(`Started architect agent in plan mode`); console.log(` Agent: ${agent.name} (${agent.id})`); console.log(` Mode: ${agent.mode}`); console.log(` Initiative: ${initiativeId}`); } catch (error) { - console.error('Failed to start breakdown:', (error as Error).message); + console.error('Failed to start plan:', (error as Error).message); process.exit(1); } }); - // cw architect decompose + // cw architect detail architectCommand - .command('decompose ') - .description('Decompose a phase into tasks') + .command('detail ') + .description('Detail a phase into tasks') .option('--name ', 'Agent name (auto-generated if omitted)') - .option('-t, --task-name ', 'Name for the decompose task') + .option('-t, --task-name ', 'Name for the detail task') .option('-c, --context ', 'Additional context') .action(async (phaseId: string, options: { name?: string; taskName?: string; context?: string }) => { try { const client = createDefaultTrpcClient(); - const agent = await client.spawnArchitectDecompose.mutate({ + const agent = await client.spawnArchitectDetail.mutate({ name: options.name, phaseId, taskName: options.taskName, context: options.context, }); - console.log(`Started architect agent in decompose mode`); + console.log(`Started architect agent in detail mode`); console.log(` Agent: ${agent.name} (${agent.id})`); console.log(` Mode: ${agent.mode}`); console.log(` Phase: ${phaseId}`); } catch (error) { - console.error('Failed to start decompose:', (error as Error).message); + console.error('Failed to start detail:', (error as Error).message); process.exit(1); } }); diff --git a/src/db/repositories/change-set-repository.ts b/src/db/repositories/change-set-repository.ts index f7f89a9..18e72d3 100644 --- a/src/db/repositories/change-set-repository.ts +++ b/src/db/repositories/change-set-repository.ts @@ -11,7 +11,7 @@ export type CreateChangeSetData = { agentId: string | null; agentName: string; initiativeId: string; - mode: 'breakdown' | 'decompose' | 'refine'; + mode: 'plan' | 'detail' | 'refine'; summary?: string | null; }; diff --git a/src/db/repositories/drizzle/cascade.test.ts b/src/db/repositories/drizzle/cascade.test.ts index 34a44aa..90318ce 100644 --- a/src/db/repositories/drizzle/cascade.test.ts +++ b/src/db/repositories/drizzle/cascade.test.ts @@ -27,7 +27,7 @@ describe('Cascade Deletes', () => { /** * Helper to create a full hierarchy for testing. - * Uses parent tasks (decompose category) to group child tasks. + * Uses parent tasks (detail category) to group child tasks. */ async function createFullHierarchy() { const initiative = await initiativeRepo.create({ @@ -44,12 +44,12 @@ describe('Cascade Deletes', () => { name: 'Phase 2', }); - // Create parent (decompose) tasks that group child tasks + // Create parent (detail) tasks that group child tasks const parentTask1 = await taskRepo.create({ phaseId: phase1.id, initiativeId: initiative.id, name: 'Parent Task 1-1', - category: 'decompose', + category: 'detail', order: 1, }); @@ -57,7 +57,7 @@ describe('Cascade Deletes', () => { phaseId: phase1.id, initiativeId: initiative.id, name: 'Parent Task 1-2', - category: 'decompose', + category: 'detail', order: 2, }); @@ -65,7 +65,7 @@ describe('Cascade Deletes', () => { phaseId: phase2.id, initiativeId: initiative.id, name: 'Parent Task 2-1', - category: 'decompose', + category: 'detail', order: 1, }); diff --git a/src/db/schema.ts b/src/db/schema.ts index f9b85cb..bd9044f 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -25,7 +25,7 @@ export const initiatives = sqliteTable('initiatives', { mergeRequiresApproval: integer('merge_requires_approval', { mode: 'boolean' }) .notNull() .default(true), - mergeTarget: text('merge_target'), // Target branch for merges (e.g., 'feature/xyz') + branch: text('branch'), // Auto-generated initiative branch (e.g., 'cw/user-auth') executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] }) .notNull() .default('review_per_phase'), @@ -120,8 +120,8 @@ export const TASK_CATEGORIES = [ 'execute', // Standard execution task 'research', // Research/exploration task 'discuss', // Discussion/context gathering - 'breakdown', // Break initiative into phases - 'decompose', // Decompose plan into tasks + 'plan', // Plan initiative into phases + 'detail', // Detail phase into tasks 'refine', // Refine/edit content 'verify', // Verification task 'merge', // Merge task @@ -135,7 +135,7 @@ export const tasks = sqliteTable('tasks', { // Parent context - at least one should be set phaseId: text('phase_id').references(() => phases.id, { onDelete: 'cascade' }), initiativeId: text('initiative_id').references(() => initiatives.id, { onDelete: 'cascade' }), - // Parent task for decomposition hierarchy (child tasks link to parent decompose task) + // Parent task for detail hierarchy (child tasks link to parent detail task) parentTaskId: text('parent_task_id').references((): ReturnType => tasks.id, { onDelete: 'cascade' }), name: text('name').notNull(), description: text('description'), @@ -172,7 +172,7 @@ export const tasksRelations = relations(tasks, ({ one, many }) => ({ fields: [tasks.initiativeId], references: [initiatives.id], }), - // Parent task (for decomposition hierarchy - child links to parent decompose task) + // Parent task (for detail hierarchy - child links to parent detail task) parentTask: one(tasks, { fields: [tasks.parentTaskId], references: [tasks.id], @@ -263,7 +263,7 @@ export const agents = sqliteTable('agents', { }) .notNull() .default('idle'), - mode: text('mode', { enum: ['execute', 'discuss', 'breakdown', 'decompose', 'refine'] }) + mode: text('mode', { enum: ['execute', 'discuss', 'plan', 'detail', 'refine'] }) .notNull() .default('execute'), pid: integer('pid'), @@ -307,7 +307,7 @@ export const changeSets = sqliteTable('change_sets', { initiativeId: text('initiative_id') .notNull() .references(() => initiatives.id, { onDelete: 'cascade' }), - mode: text('mode', { enum: ['breakdown', 'decompose', 'refine'] }).notNull(), + mode: text('mode', { enum: ['plan', 'detail', 'refine'] }).notNull(), summary: text('summary'), status: text('status', { enum: ['applied', 'reverted'] }) .notNull() @@ -451,6 +451,7 @@ export const projects = sqliteTable('projects', { id: text('id').primaryKey(), name: text('name').notNull().unique(), url: text('url').notNull().unique(), + defaultBranch: text('default_branch').notNull().default('main'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), }); diff --git a/src/events/types.ts b/src/events/types.ts index 4c84fe3..b23333a 100644 --- a/src/events/types.ts +++ b/src/events/types.ts @@ -160,8 +160,8 @@ export interface AgentStoppedEvent extends DomainEvent { | 'error' | 'waiting_for_input' | 'context_complete' - | 'breakdown_complete' - | 'decompose_complete' + | 'plan_complete' + | 'detail_complete' | 'refine_complete'; }; } diff --git a/src/test/e2e/architect-workflow.test.ts b/src/test/e2e/architect-workflow.test.ts index 2c01596..fadae20 100644 --- a/src/test/e2e/architect-workflow.test.ts +++ b/src/test/e2e/architect-workflow.test.ts @@ -3,8 +3,8 @@ * * Tests the complete architect workflow from discussion through phase creation: * - Discuss mode: Gather context, answer questions, capture decisions - * - Breakdown mode: Decompose initiative into phases - * - Full workflow: Discuss -> Breakdown -> Phase persistence + * - Plan mode: Break initiative into phases + * - Full workflow: Discuss -> Plan -> Phase persistence * * Uses TestHarness from src/test/ for full system wiring. */ @@ -100,35 +100,35 @@ describe('Architect Workflow E2E', () => { }); }); - describe('breakdown mode', () => { - it('should spawn architect in breakdown mode and create phases', async () => { + describe('plan mode', () => { + it('should spawn architect in plan mode and create phases', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); - // Set up breakdown completion - harness.setArchitectBreakdownComplete('auth-breakdown', [ + // Set up plan completion + harness.setArchitectPlanComplete('auth-plan', [ { number: 1, name: 'Database Setup', description: 'User table and auth schema', dependencies: [] }, { number: 2, name: 'JWT Implementation', description: 'Token generation and validation', dependencies: [1] }, { number: 3, name: 'Protected Routes', description: 'Middleware and route guards', dependencies: [2] }, ]); - const agent = await harness.caller.spawnArchitectBreakdown({ - name: 'auth-breakdown', + const agent = await harness.caller.spawnArchitectPlan({ + name: 'auth-plan', initiativeId: initiative.id, }); - expect(agent.mode).toBe('breakdown'); + expect(agent.mode).toBe('plan'); await harness.advanceTimers(); - // Verify stopped with breakdown_complete + // Verify stopped with plan_complete const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[]; expect(events).toHaveLength(1); - expect(events[0].payload.reason).toBe('breakdown_complete'); + expect(events[0].payload.reason).toBe('plan_complete'); }); - it('should persist phases from breakdown output', async () => { + it('should persist phases from plan output', async () => { const initiative = await harness.createInitiative('Auth System'); const phasesData = [ @@ -136,8 +136,8 @@ describe('Architect Workflow E2E', () => { { name: 'Features' }, ]; - // Persist phases (simulating what would happen after breakdown) - const created = await harness.createPhasesFromBreakdown(initiative.id, phasesData); + // Persist phases (simulating what would happen after plan) + const created = await harness.createPhasesFromPlan(initiative.id, phasesData); expect(created).toHaveLength(2); @@ -149,95 +149,95 @@ describe('Architect Workflow E2E', () => { }); }); - describe('breakdown conflict detection', () => { - it('should reject if a breakdown agent is already running', async () => { + describe('plan conflict detection', () => { + it('should reject if a plan agent is already running', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); - // Set up a long-running breakdown agent (never completes during this test) - harness.setArchitectBreakdownComplete('first-breakdown', [ + // Set up a long-running plan agent (never completes during this test) + harness.setArchitectPlanComplete('first-plan', [ { number: 1, name: 'Phase 1', description: 'First', dependencies: [] }, ]); // Use a delay so it stays running - harness.setAgentScenario('first-breakdown', { status: 'done', delay: 999999 }); + harness.setAgentScenario('first-plan', { status: 'done', delay: 999999 }); - await harness.caller.spawnArchitectBreakdown({ - name: 'first-breakdown', + await harness.caller.spawnArchitectPlan({ + name: 'first-plan', initiativeId: initiative.id, }); // Agent should be running const agents = await harness.caller.listAgents(); - expect(agents.find(a => a.name === 'first-breakdown')?.status).toBe('running'); + expect(agents.find(a => a.name === 'first-plan')?.status).toBe('running'); - // Second breakdown should be rejected + // Second plan should be rejected await expect( - harness.caller.spawnArchitectBreakdown({ - name: 'second-breakdown', + harness.caller.spawnArchitectPlan({ + name: 'second-plan', initiativeId: initiative.id, }), ).rejects.toThrow(/already running/); }); - it('should auto-dismiss stale breakdown agents before checking', async () => { + it('should auto-dismiss stale plan agents before checking', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Auth System'); - // Set up a breakdown agent that crashes immediately - harness.setAgentScenario('stale-breakdown', { status: 'error', error: 'crashed' }); + // Set up a plan agent that crashes immediately + harness.setAgentScenario('stale-plan', { status: 'error', error: 'crashed' }); - await harness.caller.spawnArchitectBreakdown({ - name: 'stale-breakdown', + await harness.caller.spawnArchitectPlan({ + name: 'stale-plan', initiativeId: initiative.id, }); await harness.advanceTimers(); // Should be crashed const agents = await harness.caller.listAgents(); - expect(agents.find(a => a.name === 'stale-breakdown')?.status).toBe('crashed'); + expect(agents.find(a => a.name === 'stale-plan')?.status).toBe('crashed'); - // New breakdown should succeed (stale one gets auto-dismissed) - harness.setArchitectBreakdownComplete('new-breakdown', [ + // New plan should succeed (stale one gets auto-dismissed) + harness.setArchitectPlanComplete('new-plan', [ { number: 1, name: 'Phase 1', description: 'First', dependencies: [] }, ]); - const agent = await harness.caller.spawnArchitectBreakdown({ - name: 'new-breakdown', + const agent = await harness.caller.spawnArchitectPlan({ + name: 'new-plan', initiativeId: initiative.id, }); - expect(agent.mode).toBe('breakdown'); + expect(agent.mode).toBe('plan'); }); - it('should allow breakdown for different initiatives', async () => { + it('should allow plan for different initiatives', async () => { vi.useFakeTimers(); const init1 = await harness.createInitiative('Initiative 1'); const init2 = await harness.createInitiative('Initiative 2'); // Long-running agent on initiative 1 - harness.setAgentScenario('breakdown-1', { status: 'done', delay: 999999 }); - await harness.caller.spawnArchitectBreakdown({ - name: 'breakdown-1', + harness.setAgentScenario('plan-1', { status: 'done', delay: 999999 }); + await harness.caller.spawnArchitectPlan({ + name: 'plan-1', initiativeId: init1.id, }); - // Breakdown on initiative 2 should succeed - harness.setArchitectBreakdownComplete('breakdown-2', [ + // Plan on initiative 2 should succeed + harness.setArchitectPlanComplete('plan-2', [ { number: 1, name: 'Phase 1', description: 'First', dependencies: [] }, ]); - const agent = await harness.caller.spawnArchitectBreakdown({ - name: 'breakdown-2', + const agent = await harness.caller.spawnArchitectPlan({ + name: 'plan-2', initiativeId: init2.id, }); - expect(agent.mode).toBe('breakdown'); + expect(agent.mode).toBe('plan'); }); }); describe('full workflow', () => { - it('should complete discuss -> breakdown -> phases workflow', async () => { + it('should complete discuss -> plan -> phases workflow', async () => { vi.useFakeTimers(); // 1. Create initiative @@ -254,21 +254,21 @@ describe('Architect Workflow E2E', () => { }); await harness.advanceTimers(); - // 3. Breakdown phase - harness.setArchitectBreakdownComplete('breakdown-agent', [ + // 3. Plan phase + harness.setArchitectPlanComplete('plan-agent', [ { number: 1, name: 'Core', description: 'Core functionality', dependencies: [] }, { number: 2, name: 'Polish', description: 'UI and UX', dependencies: [1] }, ]); - await harness.caller.spawnArchitectBreakdown({ - name: 'breakdown-agent', + await harness.caller.spawnArchitectPlan({ + name: 'plan-agent', initiativeId: initiative.id, contextSummary: 'MVP scope defined', }); await harness.advanceTimers(); // 4. Persist phases - await harness.createPhasesFromBreakdown(initiative.id, [ + await harness.createPhasesFromPlan(initiative.id, [ { name: 'Core' }, { name: 'Polish' }, ]); diff --git a/src/test/e2e/decompose-workflow.test.ts b/src/test/e2e/decompose-workflow.test.ts index 84df8a2..8598945 100644 --- a/src/test/e2e/decompose-workflow.test.ts +++ b/src/test/e2e/decompose-workflow.test.ts @@ -1,10 +1,10 @@ /** - * E2E Tests for Decompose Workflow + * E2E Tests for Detail Workflow * - * Tests the complete decomposition workflow from phase through task creation: - * - Decompose mode: Break phase into executable tasks - * - Q&A flow: Handle clarifying questions during decomposition - * - Task persistence: Save child tasks from decomposition output + * Tests the complete detail workflow from phase through task creation: + * - Detail mode: Break phase into executable tasks + * - Q&A flow: Handle clarifying questions during detailing + * - Task persistence: Save child tasks from detail output * * Uses TestHarness from src/test/ for full system wiring. */ @@ -13,7 +13,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createTestHarness, type TestHarness } from '../index.js'; import type { AgentStoppedEvent, AgentWaitingEvent } from '../../events/types.js'; -describe('Decompose Workflow E2E', () => { +describe('Detail Workflow E2E', () => { let harness: TestHarness; beforeEach(() => { @@ -25,30 +25,30 @@ describe('Decompose Workflow E2E', () => { vi.useRealTimers(); }); - describe('spawn decompose agent', () => { - it('should spawn agent in decompose mode and complete with tasks', async () => { + describe('spawn detail agent', () => { + it('should spawn agent in detail mode and complete with tasks', async () => { vi.useFakeTimers(); // Setup: Create initiative -> phase -> plan const initiative = await harness.createInitiative('Test Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Auth Plan', 'Implement authentication'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Auth Plan', 'Implement authentication'); - // Set decompose scenario - harness.setArchitectDecomposeComplete('decomposer', [ + // Set detail scenario + harness.setArchitectDetailComplete('detailer', [ { number: 1, name: 'Create schema', content: 'User table', type: 'auto', dependencies: [] }, { number: 2, name: 'Create endpoint', content: 'Login API', type: 'auto', dependencies: [1] }, ]); - // Spawn decompose agent - const agent = await harness.caller.spawnArchitectDecompose({ - name: 'decomposer', + // Spawn detail agent + const agent = await harness.caller.spawnArchitectDetail({ + name: 'detailer', phaseId: phases[0].id, }); - expect(agent.mode).toBe('decompose'); + expect(agent.mode).toBe('detail'); // Advance timers for async completion await harness.advanceTimers(); @@ -56,33 +56,33 @@ describe('Decompose Workflow E2E', () => { // Verify agent completed const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[]; expect(events).toHaveLength(1); - expect(events[0].payload.name).toBe('decomposer'); - expect(events[0].payload.reason).toBe('decompose_complete'); + expect(events[0].payload.name).toBe('detailer'); + expect(events[0].payload.reason).toBe('detail_complete'); }); it('should pause on questions and resume', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Test Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Complex Plan'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Complex Plan'); // Set questions scenario - harness.setArchitectDecomposeQuestions('decomposer', [ + harness.setArchitectDetailQuestions('detailer', [ { id: 'q1', question: 'How granular should tasks be?' }, ]); - const agent = await harness.caller.spawnArchitectDecompose({ - name: 'decomposer', + const agent = await harness.caller.spawnArchitectDetail({ + name: 'detailer', phaseId: phases[0].id, }); await harness.advanceTimers(); // Verify agent is waiting for input - const waitingAgent = await harness.caller.getAgent({ name: 'decomposer' }); + const waitingAgent = await harness.caller.getAgent({ name: 'detailer' }); expect(waitingAgent?.status).toBe('waiting_for_input'); // Verify paused on questions (emits agent:waiting, not agent:stopped) @@ -96,19 +96,19 @@ describe('Decompose Workflow E2E', () => { expect(pending?.questions[0].question).toBe('How granular should tasks be?'); // Set completion scenario for resume - harness.setArchitectDecomposeComplete('decomposer', [ + harness.setArchitectDetailComplete('detailer', [ { number: 1, name: 'Task 1', content: 'Single task', type: 'auto', dependencies: [] }, ]); // Resume with answer await harness.caller.resumeAgent({ - name: 'decomposer', + name: 'detailer', answers: { q1: 'Very granular' }, }); await harness.advanceTimers(); // Verify completed after resume - const finalAgent = await harness.caller.getAgent({ name: 'decomposer' }); + const finalAgent = await harness.caller.getAgent({ name: 'detailer' }); expect(finalAgent?.status).toBe('idle'); }); @@ -116,20 +116,20 @@ describe('Decompose Workflow E2E', () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Multi-Q Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Complex Plan'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Complex Plan'); // Set multiple questions scenario - harness.setArchitectDecomposeQuestions('decomposer', [ + harness.setArchitectDetailQuestions('detailer', [ { id: 'q1', question: 'What task granularity?', options: [{ label: 'Fine' }, { label: 'Coarse' }] }, { id: 'q2', question: 'Include checkpoints?' }, { id: 'q3', question: 'Any blocking dependencies?' }, ]); - const agent = await harness.caller.spawnArchitectDecompose({ - name: 'decomposer', + const agent = await harness.caller.spawnArchitectDetail({ + name: 'detailer', phaseId: phases[0].id, }); @@ -140,7 +140,7 @@ describe('Decompose Workflow E2E', () => { expect(pending?.questions).toHaveLength(3); // Set completion scenario for resume - harness.setArchitectDecomposeComplete('decomposer', [ + harness.setArchitectDetailComplete('detailer', [ { number: 1, name: 'Task 1', content: 'First task', type: 'auto', dependencies: [] }, { number: 2, name: 'Task 2', content: 'Second task', type: 'auto', dependencies: [1] }, { number: 3, name: 'Verify', content: 'Verify all', type: 'checkpoint:human-verify', dependencies: [2] }, @@ -148,7 +148,7 @@ describe('Decompose Workflow E2E', () => { // Resume with all answers await harness.caller.resumeAgent({ - name: 'decomposer', + name: 'detailer', answers: { q1: 'Fine', q2: 'Yes, add human verification', @@ -158,106 +158,106 @@ describe('Decompose Workflow E2E', () => { await harness.advanceTimers(); // Verify completed - const finalAgent = await harness.caller.getAgent({ name: 'decomposer' }); + const finalAgent = await harness.caller.getAgent({ name: 'detailer' }); expect(finalAgent?.status).toBe('idle'); }); }); - describe('decompose conflict detection', () => { - it('should reject if a decompose agent is already running for the same phase', async () => { + describe('detail conflict detection', () => { + it('should reject if a detail agent is already running for the same phase', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Test Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - // Long-running decompose agent - harness.setAgentScenario('decomposer-1', { status: 'done', delay: 999999 }); + // Long-running detail agent + harness.setAgentScenario('detailer-1', { status: 'done', delay: 999999 }); - await harness.caller.spawnArchitectDecompose({ - name: 'decomposer-1', + await harness.caller.spawnArchitectDetail({ + name: 'detailer-1', phaseId: phases[0].id, }); - // Second decompose for same phase should be rejected + // Second detail for same phase should be rejected await expect( - harness.caller.spawnArchitectDecompose({ - name: 'decomposer-2', + harness.caller.spawnArchitectDetail({ + name: 'detailer-2', phaseId: phases[0].id, }), ).rejects.toThrow(/already running/); }); - it('should auto-dismiss stale decompose agents before checking', async () => { + it('should auto-dismiss stale detail agents before checking', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Test Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - // Decompose agent that crashes immediately - harness.setAgentScenario('stale-decomposer', { status: 'error', error: 'crashed' }); + // Detail agent that crashes immediately + harness.setAgentScenario('stale-detailer', { status: 'error', error: 'crashed' }); - await harness.caller.spawnArchitectDecompose({ - name: 'stale-decomposer', + await harness.caller.spawnArchitectDetail({ + name: 'stale-detailer', phaseId: phases[0].id, }); await harness.advanceTimers(); - // New decompose should succeed - harness.setArchitectDecomposeComplete('new-decomposer', [ + // New detail should succeed + harness.setArchitectDetailComplete('new-detailer', [ { number: 1, name: 'Task 1', content: 'Do it', type: 'auto', dependencies: [] }, ]); - const agent = await harness.caller.spawnArchitectDecompose({ - name: 'new-decomposer', + const agent = await harness.caller.spawnArchitectDetail({ + name: 'new-detailer', phaseId: phases[0].id, }); - expect(agent.mode).toBe('decompose'); + expect(agent.mode).toBe('detail'); }); - it('should allow decompose for different phases simultaneously', async () => { + it('should allow detail for different phases simultaneously', async () => { vi.useFakeTimers(); const initiative = await harness.createInitiative('Test Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, { name: 'Phase 2' }, ]); // Long-running agent on phase 1 - harness.setAgentScenario('decomposer-p1', { status: 'done', delay: 999999 }); - await harness.caller.spawnArchitectDecompose({ - name: 'decomposer-p1', + harness.setAgentScenario('detailer-p1', { status: 'done', delay: 999999 }); + await harness.caller.spawnArchitectDetail({ + name: 'detailer-p1', phaseId: phases[0].id, }); - // Decompose on phase 2 should succeed - harness.setArchitectDecomposeComplete('decomposer-p2', [ + // Detail on phase 2 should succeed + harness.setArchitectDetailComplete('detailer-p2', [ { number: 1, name: 'Task 1', content: 'Do it', type: 'auto', dependencies: [] }, ]); - const agent = await harness.caller.spawnArchitectDecompose({ - name: 'decomposer-p2', + const agent = await harness.caller.spawnArchitectDetail({ + name: 'detailer-p2', phaseId: phases[1].id, }); - expect(agent.mode).toBe('decompose'); + expect(agent.mode).toBe('detail'); }); }); describe('task persistence', () => { - it('should create tasks from decomposition output', async () => { + it('should create tasks from detail output', async () => { const initiative = await harness.createInitiative('Test Project'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Auth Plan'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Auth Plan'); - // Create tasks from decomposition + // Create tasks from detail output await harness.caller.createChildTasks({ - parentTaskId: decomposeTask.id, + parentTaskId: detailTask.id, tasks: [ { number: 1, name: 'Schema', description: 'Create tables', type: 'auto', dependencies: [] }, { number: 2, name: 'API', description: 'Create endpoints', type: 'auto', dependencies: [1] }, @@ -266,7 +266,7 @@ describe('Decompose Workflow E2E', () => { }); // Verify tasks created - const tasks = await harness.getChildTasks(decomposeTask.id); + const tasks = await harness.getChildTasks(detailTask.id); expect(tasks).toHaveLength(3); expect(tasks[0].name).toBe('Schema'); expect(tasks[1].name).toBe('API'); @@ -276,14 +276,14 @@ describe('Decompose Workflow E2E', () => { it('should handle all task types', async () => { const initiative = await harness.createInitiative('Task Types Test'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Mixed Tasks'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Mixed Tasks'); // Create tasks with all types await harness.caller.createChildTasks({ - parentTaskId: decomposeTask.id, + parentTaskId: detailTask.id, tasks: [ { number: 1, name: 'Auto Task', description: 'Automated work', type: 'auto' }, { number: 2, name: 'Human Verify', description: 'Visual check', type: 'checkpoint:human-verify', dependencies: [1] }, @@ -292,7 +292,7 @@ describe('Decompose Workflow E2E', () => { ], }); - const tasks = await harness.getChildTasks(decomposeTask.id); + const tasks = await harness.getChildTasks(detailTask.id); expect(tasks).toHaveLength(4); expect(tasks[0].type).toBe('auto'); expect(tasks[1].type).toBe('checkpoint:human-verify'); @@ -302,14 +302,14 @@ describe('Decompose Workflow E2E', () => { it('should create task dependencies', async () => { const initiative = await harness.createInitiative('Dependencies Test'); - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Phase 1' }, ]); - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Dependent Tasks'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Dependent Tasks'); // Create tasks with complex dependencies await harness.caller.createChildTasks({ - parentTaskId: decomposeTask.id, + parentTaskId: detailTask.id, tasks: [ { number: 1, name: 'Task A', description: 'No deps', type: 'auto' }, { number: 2, name: 'Task B', description: 'Depends on A', type: 'auto', dependencies: [1] }, @@ -318,7 +318,7 @@ describe('Decompose Workflow E2E', () => { ], }); - const tasks = await harness.getChildTasks(decomposeTask.id); + const tasks = await harness.getChildTasks(detailTask.id); expect(tasks).toHaveLength(4); // All tasks should be created with correct names @@ -326,31 +326,31 @@ describe('Decompose Workflow E2E', () => { }); }); - describe('full decompose workflow', () => { - it('should complete initiative -> phase -> plan -> decompose -> tasks workflow', async () => { + describe('full detail workflow', () => { + it('should complete initiative -> phase -> plan -> detail -> tasks workflow', async () => { vi.useFakeTimers(); // 1. Create initiative const initiative = await harness.createInitiative('Full Workflow Test'); // 2. Create phase - const phases = await harness.createPhasesFromBreakdown(initiative.id, [ + const phases = await harness.createPhasesFromPlan(initiative.id, [ { name: 'Auth Phase' }, ]); // 3. Create plan - const decomposeTask = await harness.createDecomposeTask(phases[0].id, 'Auth Plan', 'Implement JWT auth'); + const detailTask = await harness.createDetailTask(phases[0].id, 'Auth Plan', 'Implement JWT auth'); - // 4. Spawn decompose agent - harness.setArchitectDecomposeComplete('decomposer', [ + // 4. Spawn detail agent + harness.setArchitectDetailComplete('detailer', [ { number: 1, name: 'Create user schema', content: 'Define User model', type: 'auto', dependencies: [] }, { number: 2, name: 'Implement JWT', content: 'Token generation', type: 'auto', dependencies: [1] }, { number: 3, name: 'Protected routes', content: 'Middleware', type: 'auto', dependencies: [2] }, { number: 4, name: 'Verify auth', content: 'Test login flow', type: 'checkpoint:human-verify', dependencies: [3] }, ]); - await harness.caller.spawnArchitectDecompose({ - name: 'decomposer', + await harness.caller.spawnArchitectDetail({ + name: 'detailer', phaseId: phases[0].id, }); await harness.advanceTimers(); @@ -358,11 +358,11 @@ describe('Decompose Workflow E2E', () => { // 5. Verify agent completed const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[]; expect(events).toHaveLength(1); - expect(events[0].payload.reason).toBe('decompose_complete'); + expect(events[0].payload.reason).toBe('detail_complete'); - // 6. Persist tasks (simulating what orchestrator would do after decompose) + // 6. Persist tasks (simulating what orchestrator would do after detail) await harness.caller.createChildTasks({ - parentTaskId: decomposeTask.id, + parentTaskId: detailTask.id, tasks: [ { number: 1, name: 'Create user schema', description: 'Define User model', type: 'auto', dependencies: [] }, { number: 2, name: 'Implement JWT', description: 'Token generation', type: 'auto', dependencies: [1] }, @@ -372,13 +372,13 @@ describe('Decompose Workflow E2E', () => { }); // 7. Verify final state - const tasks = await harness.getChildTasks(decomposeTask.id); + const tasks = await harness.getChildTasks(detailTask.id); expect(tasks).toHaveLength(4); expect(tasks[0].name).toBe('Create user schema'); expect(tasks[3].type).toBe('checkpoint:human-verify'); // Agent should be idle - const finalAgent = await harness.caller.getAgent({ name: 'decomposer' }); + const finalAgent = await harness.caller.getAgent({ name: 'detailer' }); expect(finalAgent?.status).toBe('idle'); }); }); diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts index 42eebc9..e9c5ae3 100644 --- a/src/test/fixtures.ts +++ b/src/test/fixtures.ts @@ -29,7 +29,7 @@ export interface TaskFixture { /** Task priority */ priority?: 'low' | 'medium' | 'high'; /** Task category */ - category?: 'execute' | 'research' | 'discuss' | 'breakdown' | 'decompose' | 'refine' | 'verify' | 'merge' | 'review'; + category?: 'execute' | 'research' | 'discuss' | 'plan' | 'detail' | 'refine' | 'verify' | 'merge' | 'review'; /** Names of other tasks in same fixture this task depends on */ dependsOn?: string[]; } @@ -39,7 +39,7 @@ export interface TaskFixture { * Tasks are grouped by parent task in the new model. */ export interface TaskGroupFixture { - /** Group name (becomes a decompose task) */ + /** Group name (becomes a detail task) */ name: string; /** Tasks in this group */ tasks: TaskFixture[]; @@ -51,7 +51,7 @@ export interface TaskGroupFixture { export interface PhaseFixture { /** Phase name */ name: string; - /** Task groups in this phase (each group becomes a parent decompose task) */ + /** Task groups in this phase (each group becomes a parent detail task) */ taskGroups: TaskGroupFixture[]; } @@ -87,7 +87,7 @@ export interface SeededFixture { /** * Seed a complete task hierarchy from a fixture definition. * - * Creates initiative, phases, decompose tasks (as parent), and child tasks. + * Creates initiative, phases, detail tasks (as parent), and child tasks. * Resolves task dependencies by name to actual task IDs. * * @param db - Drizzle database instance @@ -126,19 +126,19 @@ export async function seedFixture( }); phasesMap.set(phaseFixture.name, phase.id); - // Create task groups as parent decompose tasks + // Create task groups as parent detail tasks let taskOrder = 0; for (const groupFixture of phaseFixture.taskGroups) { - // Create parent decompose task + // Create parent detail task const parentTask = await taskRepo.create({ phaseId: phase.id, initiativeId: initiative.id, name: groupFixture.name, description: `Test task group: ${groupFixture.name}`, - category: 'decompose', + category: 'detail', type: 'auto', priority: 'medium', - status: 'completed', // Decompose tasks are completed once child tasks are created + status: 'completed', // Detail tasks are completed once child tasks are created order: taskOrder++, }); taskGroupsMap.set(groupFixture.name, parentTask.id); diff --git a/src/test/harness.ts b/src/test/harness.ts index 33c88bc..15befc2 100644 --- a/src/test/harness.ts +++ b/src/test/harness.ts @@ -301,25 +301,25 @@ export interface TestHarness { ): void; /** - * Set up scenario where architect completes breakdown. + * Set up scenario where architect completes plan. */ - setArchitectBreakdownComplete( + setArchitectPlanComplete( agentName: string, _phases: unknown[] ): void; /** - * Set up scenario where architect completes decomposition. + * Set up scenario where architect completes detail. */ - setArchitectDecomposeComplete( + setArchitectDetailComplete( agentName: string, _tasks: unknown[] ): void; /** - * Set up scenario where architect needs questions in decompose mode. + * Set up scenario where architect needs questions in detail mode. */ - setArchitectDecomposeQuestions( + setArchitectDetailQuestions( agentName: string, questions: QuestionItem[] ): void; @@ -344,17 +344,17 @@ export interface TestHarness { createInitiative(name: string): Promise; /** - * Create phases from breakdown output through tRPC. + * Create phases from plan output through tRPC. */ - createPhasesFromBreakdown( + createPhasesFromPlan( initiativeId: string, phases: Array<{ name: string }> ): Promise; /** - * Create a decompose task through tRPC (replaces createPlan). + * Create a detail task through tRPC (replaces createPlan). */ - createDecomposeTask( + createDetailTask( phaseId: string, name: string, description?: string @@ -543,29 +543,29 @@ export function createTestHarness(): TestHarness { }); }, - setArchitectBreakdownComplete: ( + setArchitectPlanComplete: ( agentName: string, _phases: unknown[] ) => { agentManager.setScenario(agentName, { status: 'done', - result: 'Breakdown complete', + result: 'Plan complete', delay: 0, }); }, - setArchitectDecomposeComplete: ( + setArchitectDetailComplete: ( agentName: string, _tasks: unknown[] ) => { agentManager.setScenario(agentName, { status: 'done', - result: 'Decompose complete', + result: 'Detail complete', delay: 0, }); }, - setArchitectDecomposeQuestions: ( + setArchitectDetailQuestions: ( agentName: string, questions: QuestionItem[] ) => { @@ -596,19 +596,19 @@ export function createTestHarness(): TestHarness { return caller.createInitiative({ name }); }, - createPhasesFromBreakdown: ( + createPhasesFromPlan: ( initiativeId: string, phases: Array<{ name: string }> ) => { - return caller.createPhasesFromBreakdown({ initiativeId, phases }); + return caller.createPhasesFromPlan({ initiativeId, phases }); }, - createDecomposeTask: async (phaseId: string, name: string, description?: string) => { + createDetailTask: async (phaseId: string, name: string, description?: string) => { return caller.createPhaseTask({ phaseId, name, description, - category: 'decompose', + category: 'detail', type: 'auto', requiresApproval: true, }); diff --git a/src/test/integration/crash-race-condition.test.ts b/src/test/integration/crash-race-condition.test.ts index 440d13a..39e82d8 100644 --- a/src/test/integration/crash-race-condition.test.ts +++ b/src/test/integration/crash-race-condition.test.ts @@ -17,7 +17,7 @@ interface TestAgent { id: string; name: string; status: 'idle' | 'running' | 'waiting_for_input' | 'stopped' | 'crashed'; - mode: 'execute' | 'discuss' | 'breakdown' | 'decompose' | 'refine'; + mode: 'execute' | 'discuss' | 'plan' | 'detail' | 'refine'; taskId: string | null; sessionId: string | null; worktreeId: string; diff --git a/src/test/integration/real-providers/prompts.ts b/src/test/integration/real-providers/prompts.ts index d408477..e7fddf1 100644 --- a/src/test/integration/real-providers/prompts.ts +++ b/src/test/integration/real-providers/prompts.ts @@ -82,17 +82,17 @@ Now complete the task by outputting: {"status":"done"}`, /** - * ~$0.02 - Breakdown complete - * Tests: breakdown mode output handling (now uses universal done signal) + * ~$0.02 - Plan complete + * Tests: plan mode output handling (now uses universal done signal) */ - breakdownComplete: `Output exactly this JSON with no other text: + planComplete: `Output exactly this JSON with no other text: {"status":"done"}`, /** - * ~$0.02 - Decompose complete - * Tests: decompose mode output handling (now uses universal done signal) + * ~$0.02 - Detail complete + * Tests: detail mode output handling (now uses universal done signal) */ - decomposeComplete: `Output exactly this JSON with no other text: + detailComplete: `Output exactly this JSON with no other text: {"status":"done"}`, } as const; diff --git a/src/test/integration/real-providers/schema-retry.test.ts b/src/test/integration/real-providers/schema-retry.test.ts index 838a5dc..3b244c9 100644 --- a/src/test/integration/real-providers/schema-retry.test.ts +++ b/src/test/integration/real-providers/schema-retry.test.ts @@ -262,12 +262,12 @@ describeRealClaude('Schema Validation & Retry', () => { ); it( - 'validates breakdown mode output', + 'validates plan mode output', async () => { const agent = await harness.agentManager.spawn({ taskId: null, - prompt: MINIMAL_PROMPTS.breakdownComplete, - mode: 'breakdown', + prompt: MINIMAL_PROMPTS.planComplete, + mode: 'plan', provider: 'claude', }); @@ -277,18 +277,18 @@ describeRealClaude('Schema Validation & Retry', () => { expect(dbAgent?.status).toBe('idle'); expect(result?.success).toBe(true); - console.log(' Breakdown mode result:', result?.message); + console.log(' Plan mode result:', result?.message); }, REAL_TEST_TIMEOUT ); it( - 'validates decompose mode output', + 'validates detail mode output', async () => { const agent = await harness.agentManager.spawn({ taskId: null, - prompt: MINIMAL_PROMPTS.decomposeComplete, - mode: 'decompose', + prompt: MINIMAL_PROMPTS.detailComplete, + mode: 'detail', provider: 'claude', }); @@ -298,7 +298,7 @@ describeRealClaude('Schema Validation & Retry', () => { expect(dbAgent?.status).toBe('idle'); expect(result?.success).toBe(true); - console.log(' Decompose mode result:', result?.message); + console.log(' Detail mode result:', result?.message); }, REAL_TEST_TIMEOUT ); diff --git a/src/trpc/routers/agent.ts b/src/trpc/routers/agent.ts index 712f634..6de93b1 100644 --- a/src/trpc/routers/agent.ts +++ b/src/trpc/routers/agent.ts @@ -18,7 +18,7 @@ export const spawnAgentInputSchema = z.object({ taskId: z.string().min(1), prompt: z.string().min(1), cwd: z.string().optional(), - mode: z.enum(['execute', 'discuss', 'breakdown', 'decompose', 'refine']).optional(), + mode: z.enum(['execute', 'discuss', 'plan', 'detail', 'refine']).optional(), provider: z.string().optional(), initiativeId: z.string().min(1).optional(), }); diff --git a/src/trpc/routers/architect.ts b/src/trpc/routers/architect.ts index d3e09f1..4441920 100644 --- a/src/trpc/routers/architect.ts +++ b/src/trpc/routers/architect.ts @@ -1,5 +1,5 @@ /** - * Architect Router — discuss, breakdown, refine, decompose spawn procedures + * Architect Router — discuss, plan, refine, detail spawn procedures */ import { TRPCError } from '@trpc/server'; @@ -14,9 +14,9 @@ import { } from './_helpers.js'; import { buildDiscussPrompt, - buildBreakdownPrompt, + buildPlanPrompt, buildRefinePrompt, - buildDecomposePrompt, + buildDetailPrompt, } from '../../agent/prompts/index.js'; import type { PhaseRepository } from '../../db/repositories/phase-repository.js'; import type { TaskRepository } from '../../db/repositories/task-repository.js'; @@ -114,7 +114,7 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { }); }), - spawnArchitectBreakdown: publicProcedure + spawnArchitectPlan: publicProcedure .input(z.object({ name: z.string().min(1).optional(), initiativeId: z.string().min(1), @@ -134,11 +134,11 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { }); } - // Auto-dismiss stale breakdown agents + // Auto-dismiss stale plan agents const allAgents = await agentManager.list(); const staleAgents = allAgents.filter( (a) => - a.mode === 'breakdown' && + a.mode === 'plan' && a.initiativeId === input.initiativeId && ['crashed', 'idle'].includes(a.status) && !a.userDismissedAt, @@ -147,37 +147,37 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { await agentManager.dismiss(stale.id); } - // Reject if a breakdown agent is already active for this initiative - const activeBreakdownAgents = allAgents.filter( + // Reject if a plan agent is already active for this initiative + const activePlanAgents = allAgents.filter( (a) => - a.mode === 'breakdown' && + a.mode === 'plan' && a.initiativeId === input.initiativeId && ['running', 'waiting_for_input'].includes(a.status), ); - if (activeBreakdownAgents.length > 0) { + if (activePlanAgents.length > 0) { throw new TRPCError({ code: 'CONFLICT', - message: 'A breakdown agent is already running for this initiative', + message: 'A plan agent is already running for this initiative', }); } const task = await taskRepo.create({ initiativeId: input.initiativeId, - name: `Breakdown: ${initiative.name}`, - description: 'Break initiative into phases', - category: 'breakdown', + name: `Plan: ${initiative.name}`, + description: 'Plan initiative into phases', + category: 'plan', status: 'in_progress', }); const context = await gatherInitiativeContext(ctx.phaseRepository, ctx.taskRepository, ctx.pageRepository, input.initiativeId); - const prompt = buildBreakdownPrompt(); + const prompt = buildPlanPrompt(); return agentManager.spawn({ name: input.name, taskId: task.id, prompt, - mode: 'breakdown', + mode: 'plan', provider: input.provider, initiativeId: input.initiativeId, inputContext: { @@ -267,7 +267,7 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { }); }), - spawnArchitectDecompose: publicProcedure + spawnArchitectDetail: publicProcedure .input(z.object({ name: z.string().min(1).optional(), phaseId: z.string().min(1), @@ -296,16 +296,16 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { }); } - // Auto-dismiss stale decompose agents for this phase + // Auto-dismiss stale detail agents for this phase const allAgents = await agentManager.list(); - const decomposeAgents = allAgents.filter( - (a) => a.mode === 'decompose' && !a.userDismissedAt, + const detailAgents = allAgents.filter( + (a) => a.mode === 'detail' && !a.userDismissedAt, ); - // Look up tasks to find which phase each decompose agent targets - const activeForPhase: typeof decomposeAgents = []; - const staleForPhase: typeof decomposeAgents = []; - for (const agent of decomposeAgents) { + // Look up tasks to find which phase each detail agent targets + const activeForPhase: typeof detailAgents = []; + const staleForPhase: typeof detailAgents = []; + for (const agent of detailAgents) { if (!agent.taskId) continue; const agentTask = await taskRepo.findById(agent.taskId); if (agentTask?.phaseId !== input.phaseId) continue; @@ -322,29 +322,29 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { if (activeForPhase.length > 0) { throw new TRPCError({ code: 'CONFLICT', - message: `A decompose agent is already running for phase "${phase.name}"`, + message: `A detail agent is already running for phase "${phase.name}"`, }); } - const decomposeTaskName = input.taskName ?? `Decompose: ${phase.name}`; + const detailTaskName = input.taskName ?? `Detail: ${phase.name}`; const task = await taskRepo.create({ phaseId: phase.id, initiativeId: phase.initiativeId, - name: decomposeTaskName, - description: input.context ?? `Break phase "${phase.name}" into executable tasks`, - category: 'decompose', + name: detailTaskName, + description: input.context ?? `Detail phase "${phase.name}" into executable tasks`, + category: 'detail', status: 'in_progress', }); const context = await gatherInitiativeContext(ctx.phaseRepository, ctx.taskRepository, ctx.pageRepository, phase.initiativeId); - const prompt = buildDecomposePrompt(); + const prompt = buildDetailPrompt(); return agentManager.spawn({ name: input.name, taskId: task.id, prompt, - mode: 'decompose', + mode: 'detail', provider: input.provider, initiativeId: phase.initiativeId, inputContext: { diff --git a/src/trpc/routers/phase-dispatch.ts b/src/trpc/routers/phase-dispatch.ts index f8b2302..8cf95f0 100644 --- a/src/trpc/routers/phase-dispatch.ts +++ b/src/trpc/routers/phase-dispatch.ts @@ -51,10 +51,10 @@ export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { message: `Parent task '${input.parentTaskId}' not found`, }); } - if (parentTask.category !== 'decompose') { + if (parentTask.category !== 'detail') { throw new TRPCError({ code: 'BAD_REQUEST', - message: `Parent task must have category 'decompose', got '${parentTask.category}'`, + message: `Parent task must have category 'detail', got '${parentTask.category}'`, }); } diff --git a/src/trpc/routers/phase.ts b/src/trpc/routers/phase.ts index 4c4ce99..e505837 100644 --- a/src/trpc/routers/phase.ts +++ b/src/trpc/routers/phase.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; import type { Phase } from '../../db/schema.js'; import type { ProcedureBuilder } from '../trpc.js'; import { requirePhaseRepository, requireTaskRepository, requireBranchManager, requireInitiativeRepository, requireProjectRepository, requireExecutionOrchestrator } from './_helpers.js'; -import { initiativeBranchName, phaseBranchName } from '../../git/branch-naming.js'; +import { phaseBranchName } from '../../git/branch-naming.js'; import { ensureProjectClone } from '../../git/project-clones.js'; export function phaseProcedures(publicProcedure: ProcedureBuilder) { @@ -80,9 +80,9 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) { }); } - // Validate phase has work tasks (filter out decompose tasks) + // Validate phase has work tasks (filter out detail tasks) const phaseTasks = await taskRepo.findByPhaseId(input.phaseId); - const workTasks = phaseTasks.filter((t) => t.category !== 'decompose'); + const workTasks = phaseTasks.filter((t) => t.category !== 'detail'); if (workTasks.length === 0) { throw new TRPCError({ code: 'BAD_REQUEST', @@ -101,7 +101,7 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) { return { success: true }; }), - createPhasesFromBreakdown: publicProcedure + createPhasesFromPlan: publicProcedure .input(z.object({ initiativeId: z.string().min(1), phases: z.array(z.object({ @@ -201,11 +201,11 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) { } const initiative = await initiativeRepo.findById(phase.initiativeId); - if (!initiative?.mergeTarget) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Initiative has no merge target' }); + if (!initiative?.branch) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Initiative has no branch configured' }); } - const initBranch = initiativeBranchName(initiative.mergeTarget); + const initBranch = initiative.branch; const phBranch = phaseBranchName(initBranch, phase.name); const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId); diff --git a/src/trpc/routers/task.ts b/src/trpc/routers/task.ts index a52eeb8..a817cf9 100644 --- a/src/trpc/routers/task.ts +++ b/src/trpc/routers/task.ts @@ -57,7 +57,7 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { initiativeId: z.string().min(1), name: z.string().min(1), description: z.string().optional(), - category: z.enum(['execute', 'research', 'discuss', 'breakdown', 'decompose', 'refine', 'verify', 'merge', 'review']).optional(), + category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(), requiresApproval: z.boolean().nullable().optional(), })) @@ -89,7 +89,7 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { phaseId: z.string().min(1), name: z.string().min(1), description: z.string().optional(), - category: z.enum(['execute', 'research', 'discuss', 'breakdown', 'decompose', 'refine', 'verify', 'merge', 'review']).optional(), + category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).optional(), requiresApproval: z.boolean().nullable().optional(), })) @@ -120,7 +120,7 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { .input(z.object({ initiativeId: z.string().optional(), phaseId: z.string().optional(), - category: z.enum(['execute', 'research', 'discuss', 'breakdown', 'decompose', 'refine', 'verify', 'merge', 'review']).optional(), + category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), }).optional()) .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); @@ -132,7 +132,7 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const tasks = await taskRepository.findByInitiativeId(input.initiativeId); - return tasks.filter((t) => t.category !== 'decompose'); + return tasks.filter((t) => t.category !== 'detail'); }), listPhaseTasks: publicProcedure @@ -140,7 +140,7 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { .query(async ({ ctx, input }) => { const taskRepository = requireTaskRepository(ctx); const tasks = await taskRepository.findByPhaseId(input.phaseId); - return tasks.filter((t) => t.category !== 'decompose'); + return tasks.filter((t) => t.category !== 'detail'); }), approveTask: publicProcedure