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

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