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:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user