refactor: Rename agent modes breakdown→plan, decompose→detail

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).
This commit is contained in:
Lukas May
2026-02-10 10:51:42 +01:00
parent f9f8b4c185
commit 0407f05332
51 changed files with 551 additions and 483 deletions

View File

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

View File

@@ -65,8 +65,8 @@ Uses **Commander.js** for command parsing.
| Command | Description |
|---------|-------------|
| `discuss <initiativeId> [-c context]` | Start discussion agent |
| `breakdown <initiativeId> [-s summary]` | Start breakdown agent |
| `decompose <phaseId> [-t taskName] [-c context]` | Decompose phase into tasks |
| `plan <initiativeId> [-s summary]` | Start plan agent |
| `detail <phaseId> [-t taskName] [-c context]` | Detail phase into tasks |
### Phase (`cw phase`)
| Command | Description |

View File

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

View File

@@ -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 }

View File

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

View File

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

View File

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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
}
]
}

View File

@@ -10,8 +10,8 @@ interface ChangeSetBannerProps {
}
const MODE_LABELS: Record<string, string> = {
breakdown: "phases",
decompose: "tasks",
plan: "phases",
detail: "tasks",
refine: "pages",
};

View File

@@ -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<string, (typeof allAgents)[number]>();
// Build taskId → phaseId lookup from allTasks
const taskPhaseMap = new Map<string, string>();
@@ -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 (
<ExecutionProvider>
<BreakdownSection
<PlanSection
initiativeId={initiativeId}
phasesLoaded={phasesLoaded}
phases={sortedPhases}
@@ -209,7 +209,7 @@ export function ExecutionTab({
phases={sortedPhases}
onAddPhase={handleStartAdd}
phasesWithoutTasks={phasesWithoutTasks}
decomposeAgentByPhase={decomposeAgentByPhase}
detailAgentByPhase={detailAgentByPhase}
/>
</div>
@@ -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}
/>
) : (
<PhaseDetailEmpty />

View File

@@ -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
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onSpawnArchitect("breakdown")}
onClick={() => onSpawnArchitect("plan")}
>
Breakdown
Plan
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -11,7 +11,7 @@ interface InitiativeListProps {
onViewInitiative: (id: string) => void;
onSpawnArchitect: (
initiativeId: string,
mode: "discuss" | "breakdown",
mode: "discuss" | "plan",
) => void;
onDeleteInitiative: (id: string) => void;
}

View File

@@ -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({
<DropdownMenuItem onClick={handleDiscuss} disabled={isPending}>
Discuss
</DropdownMenuItem>
<DropdownMenuItem onClick={handleBreakdown} disabled={isPending}>
Breakdown
<DropdownMenuItem onClick={handlePlan} disabled={isPending}>
Plan
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -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<string, { id: string; status: string }>;
detailAgentByPhase: Map<string, { id: string; status: string }>;
}
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 (
<div className="flex items-center gap-2">
{activeDecomposeCount > 0 && (
{activeDetailCount > 0 && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
Decomposing ({activeDecomposeCount})
Detailing ({activeDetailCount})
</div>
)}
<Button
@@ -61,12 +71,16 @@ export function PhaseActions({
<Button
variant="outline"
size="sm"
disabled={eligiblePhaseIds.length === 0}
onClick={handleBreakdownAll}
disabled={eligiblePhaseIds.length === 0 || isDetailingAll}
onClick={handleDetailAll}
className="gap-1.5"
>
<Sparkles className="h-3.5 w-3.5" />
Breakdown All
{isDetailingAll ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Sparkles className="h-3.5 w-3.5" />
)}
Detail All
</Button>
</div>
);

View File

@@ -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 (
<div className="space-y-6">
@@ -214,25 +214,25 @@ export function PhaseDetailPanel({
</span>
)}
{/* Breakdown button in header */}
{showBreakdownButton && (
{/* Detail button in header */}
{showDetailButton && (
<Button
variant="outline"
size="sm"
onClick={handleDecompose}
disabled={decomposeMutation.isPending}
onClick={handleDetail}
disabled={detailMutation.isPending}
className="gap-1.5"
>
<Sparkles className="h-3.5 w-3.5" />
{decomposeMutation.isPending ? "Starting..." : "Breakdown"}
{detailMutation.isPending ? "Starting..." : "Detail Tasks"}
</Button>
)}
{/* Running indicator in header */}
{isDecomposeRunning && (
{isDetailRunning && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
Breaking down...
Detailing...
</div>
)}
@@ -342,11 +342,11 @@ export function PhaseDetailPanel({
)}
</div>
{/* Decompose change set */}
{/* Detail change set */}
{showChangeSet && (
<ChangeSetBanner
changeSet={latestChangeSet!}
onDismiss={handleDismissDecompose}
onDismiss={handleDismissDetail}
/>
)}

View File

@@ -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 (
<BreakdownSection
<PlanSection
initiativeId={initiativeId}
phasesLoaded={phasesLoaded}
phases={phases}

View File

@@ -5,27 +5,27 @@ import { trpc } from "@/lib/trpc";
import { useSpawnMutation } from "@/hooks/useSpawnMutation";
import { ChangeSetBanner } from "@/components/ChangeSetBanner";
interface BreakdownSectionProps {
interface PlanSectionProps {
initiativeId: string;
phasesLoaded: boolean;
phases: Array<{ status: string }>;
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 (
<div className="py-4">
<ChangeSetBanner
@@ -88,24 +88,24 @@ export function BreakdownSection({
return (
<div className="py-8 text-center space-y-3">
<p className="text-muted-foreground">No phases yet</p>
{isBreakdownRunning ? (
{isPlanRunning ? (
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Breaking down initiative...
Planning phases...
</div>
) : (
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleBreakdown}
disabled={breakdownSpawn.isSpawning}
onClick={handlePlan}
disabled={planSpawn.isSpawning}
className="gap-1.5"
>
<Sparkles className="h-3.5 w-3.5" />
{breakdownSpawn.isSpawning
{planSpawn.isSpawning
? "Starting..."
: "Break Down Initiative"}
: "Plan Phases"}
</Button>
{onAddPhase && (
<>
@@ -123,9 +123,9 @@ export function BreakdownSection({
)}
</div>
)}
{breakdownSpawn.isError && (
{planSpawn.isError && (
<p className="text-xs text-destructive">
{breakdownSpawn.error}
{planSpawn.error}
</p>
)}
</div>

View File

@@ -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";

View File

@@ -42,8 +42,8 @@ const INVALIDATION_MAP: Partial<Record<MutationName, QueryName[]>> = {
// --- Architect spawns ---
spawnArchitectRefine: ["listAgents"],
spawnArchitectDiscuss: ["listAgents"],
spawnArchitectBreakdown: ["listAgents"],
spawnArchitectDecompose: ["listAgents", "listInitiativeTasks"],
spawnArchitectPlan: ["listAgents"],
spawnArchitectDetail: ["listAgents", "listInitiativeTasks"],
// --- Initiatives ---
createInitiative: ["listInitiatives"],

View File

@@ -0,0 +1,11 @@
const MODE_LABELS: Record<string, string> = {
plan: 'Plan',
detail: 'Detail',
discuss: 'Discuss',
refine: 'Refine',
execute: 'Execute',
};
export function modeLabel(mode: string): string {
return MODE_LABELS[mode] ?? mode;
}

View File

@@ -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}
</Badge>
<Badge variant="secondary" className="text-xs">
{agent.mode}
{modeLabel(agent.mode)}
</Badge>
{/* Action dropdown */}
<div onClick={(e) => e.stopPropagation()}>

View File

@@ -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" && <ContentTab initiativeId={id} initiativeName={initiative.name} />}
{activeTab === "breakdown" && (
{activeTab === "plan" && (
<ExecutionTab
initiativeId={id}
phases={phases}
phasesLoading={phasesQuery.isLoading}
phasesLoaded={phasesQuery.isSuccess}
dependencyEdges={depsQuery.data ?? []}
mergeTarget={serializedInitiative.mergeTarget}
branch={serializedInitiative.branch}
/>
)}
{activeTab === "execution" && (

View File

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

View File

@@ -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');
});
});

View File

@@ -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';
}

View File

@@ -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';
}

View File

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

View File

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

View File

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

View File

@@ -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';

View File

@@ -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}

View File

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

View File

@@ -847,52 +847,52 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
}
});
// cw architect breakdown <initiative-id>
// cw architect plan <initiative-id>
architectCommand
.command('breakdown <initiativeId>')
.description('Start breakdown phase for an initiative')
.command('plan <initiativeId>')
.description('Plan phases for an initiative')
.option('--name <name>', 'Agent name (auto-generated if omitted)')
.option('-s, --summary <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 <phase-id>
// cw architect detail <phase-id>
architectCommand
.command('decompose <phaseId>')
.description('Decompose a phase into tasks')
.command('detail <phaseId>')
.description('Detail a phase into tasks')
.option('--name <name>', 'Agent name (auto-generated if omitted)')
.option('-t, --task-name <taskName>', 'Name for the decompose task')
.option('-t, --task-name <taskName>', 'Name for the detail task')
.option('-c, --context <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);
}
});

View File

@@ -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;
};

View File

@@ -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,
});

View File

@@ -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<typeof text> => 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(),
});

View File

@@ -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';
};
}

View File

@@ -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' },
]);

View File

@@ -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');
});
});

View File

@@ -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);

View File

@@ -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<Initiative>;
/**
* Create phases from breakdown output through tRPC.
* Create phases from plan output through tRPC.
*/
createPhasesFromBreakdown(
createPhasesFromPlan(
initiativeId: string,
phases: Array<{ name: string }>
): Promise<Phase[]>;
/**
* 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,
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
);

View File

@@ -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(),
});

View File

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

View File

@@ -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}'`,
});
}

View File

@@ -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);

View File

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