feat(agent): Enrich breakdown/decompose agent input with full initiative context
Breakdown and decompose agents now receive all existing phases, tasks, and pages as read-only context so they can plan with awareness of what already exists instead of operating in a vacuum.
This commit is contained in:
@@ -201,10 +201,65 @@ export function writeInputFiles(options: WriteInputFilesOptions): void {
|
||||
manifestFiles.push('task.md');
|
||||
}
|
||||
|
||||
// Write read-only context directories
|
||||
const contextFiles: string[] = [];
|
||||
|
||||
if (options.phases && options.phases.length > 0) {
|
||||
const phasesDir = join(inputDir, 'context', 'phases');
|
||||
mkdirSync(phasesDir, { recursive: true });
|
||||
|
||||
for (const ph of options.phases) {
|
||||
let bodyMarkdown = '';
|
||||
if (ph.content) {
|
||||
try {
|
||||
bodyMarkdown = tiptapJsonToMarkdown(JSON.parse(ph.content));
|
||||
} catch {
|
||||
// Invalid JSON content — skip
|
||||
}
|
||||
}
|
||||
const content = formatFrontmatter(
|
||||
{
|
||||
id: ph.id,
|
||||
name: ph.name,
|
||||
status: ph.status,
|
||||
dependsOn: ph.dependsOn ?? [],
|
||||
},
|
||||
bodyMarkdown,
|
||||
);
|
||||
const filename = `context/phases/${ph.id}.md`;
|
||||
writeFileSync(join(phasesDir, `${ph.id}.md`), content, 'utf-8');
|
||||
contextFiles.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.tasks && options.tasks.length > 0) {
|
||||
const tasksDir = join(inputDir, 'context', 'tasks');
|
||||
mkdirSync(tasksDir, { recursive: true });
|
||||
|
||||
for (const t of options.tasks) {
|
||||
const content = formatFrontmatter(
|
||||
{
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
phaseId: t.phaseId,
|
||||
parentTaskId: t.parentTaskId,
|
||||
category: t.category,
|
||||
type: t.type,
|
||||
priority: t.priority,
|
||||
status: t.status,
|
||||
},
|
||||
t.description ?? '',
|
||||
);
|
||||
const filename = `context/tasks/${t.id}.md`;
|
||||
writeFileSync(join(tasksDir, `${t.id}.md`), content, 'utf-8');
|
||||
contextFiles.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// Write manifest listing exactly which files were created
|
||||
writeFileSync(
|
||||
join(inputDir, 'manifest.json'),
|
||||
JSON.stringify({ files: manifestFiles }) + '\n',
|
||||
JSON.stringify({ files: manifestFiles, contextFiles }) + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ ${ID_GENERATION}
|
||||
- Size: 2-5 tasks each (not too big, not too small) - if the work is independent enough and the tasks are very similar you can also create more tasks for the phase
|
||||
- Clear, action-oriented names (describe what gets built, not how)
|
||||
|
||||
## Existing Context
|
||||
- Read context files to see what phases and tasks already exist
|
||||
- If phases/tasks already exist, account for them — don't plan work that's already covered
|
||||
- Produce a complete phase plan — do NOT reuse existing phase IDs, always generate new ones
|
||||
- Pages contain requirements and specifications — reference them for phase descriptions
|
||||
|
||||
## Rules
|
||||
- Start with foundation/infrastructure phases
|
||||
- Group related work together
|
||||
|
||||
@@ -30,6 +30,12 @@ ${ID_GENERATION}
|
||||
- Use \`checkpoint:*\` types for tasks requiring human review
|
||||
- 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
|
||||
|
||||
## Rules
|
||||
- Break work into 3-8 tasks per phase
|
||||
- Order tasks logically (foundational work first)
|
||||
|
||||
@@ -20,13 +20,20 @@ export const INPUT_FILES = `
|
||||
## Input Files
|
||||
|
||||
Read \`.cw/input/manifest.json\` first — it lists exactly which input files exist.
|
||||
Then read only those files from \`.cw/input/\`.
|
||||
Then read the files from \`.cw/input/\`.
|
||||
|
||||
Possible files:
|
||||
### Assignment Files (your work target)
|
||||
- \`initiative.md\` — Initiative details (frontmatter: id, name, status)
|
||||
- \`phase.md\` — Phase details (frontmatter: id, number, name, status; body: description)
|
||||
- \`phase.md\` — Phase details (frontmatter: id, name, status; body: description)
|
||||
- \`task.md\` — Task details (frontmatter: id, name, category, type, priority, status; body: description)
|
||||
- \`pages/\` — Initiative pages (one file per page; frontmatter: title, parentPageId, sortOrder; body: markdown content)`;
|
||||
- \`pages/\` — Initiative pages (one file per page; frontmatter: title, parentPageId, sortOrder; body: markdown content)
|
||||
|
||||
### Context Files (read-only background)
|
||||
If \`contextFiles\` is present in the manifest, these provide read-only context about what already exists:
|
||||
- \`context/phases/\` — Existing phases (frontmatter: id, name, status, dependsOn; body: description)
|
||||
- \`context/tasks/\` — Existing tasks (frontmatter: id, name, phaseId, parentTaskId, category, type, priority, status; body: description)
|
||||
|
||||
Context files are for reference only — do not duplicate or contradict their content in your output.`;
|
||||
|
||||
export const ID_GENERATION = `
|
||||
## ID Generation
|
||||
|
||||
@@ -25,6 +25,10 @@ export interface AgentInputContext {
|
||||
pages?: import('./content-serializer.js').PageForSerialization[];
|
||||
phase?: import('../db/schema.js').Phase;
|
||||
task?: import('../db/schema.js').Task;
|
||||
/** All phases for the initiative (read-only context for agents) */
|
||||
phases?: Array<import('../db/schema.js').Phase & { dependsOn?: string[] }>;
|
||||
/** All tasks for the initiative (read-only context for agents) */
|
||||
tasks?: import('../db/schema.js').Task[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,58 @@ import {
|
||||
buildRefinePrompt,
|
||||
buildDecomposePrompt,
|
||||
} from '../../agent/prompts/index.js';
|
||||
import type { PhaseRepository } from '../../db/repositories/phase-repository.js';
|
||||
import type { TaskRepository } from '../../db/repositories/task-repository.js';
|
||||
import type { PageRepository } from '../../db/repositories/page-repository.js';
|
||||
import type { Phase, Task } from '../../db/schema.js';
|
||||
import type { PageForSerialization } from '../../agent/content-serializer.js';
|
||||
|
||||
async function gatherInitiativeContext(
|
||||
phaseRepo: PhaseRepository | undefined,
|
||||
taskRepo: TaskRepository | undefined,
|
||||
pageRepo: PageRepository | undefined,
|
||||
initiativeId: string,
|
||||
): Promise<{
|
||||
phases: Array<Phase & { dependsOn?: string[] }>;
|
||||
tasks: Task[];
|
||||
pages: PageForSerialization[];
|
||||
}> {
|
||||
const [rawPhases, deps, initiativeTasks, pages] = await Promise.all([
|
||||
phaseRepo?.findByInitiativeId(initiativeId) ?? [],
|
||||
phaseRepo?.findDependenciesByInitiativeId(initiativeId) ?? [],
|
||||
taskRepo?.findByInitiativeId(initiativeId) ?? [],
|
||||
pageRepo?.findByInitiativeId(initiativeId) ?? [],
|
||||
]);
|
||||
|
||||
// Merge dependencies into each phase as a dependsOn array
|
||||
const depsByPhase = new Map<string, string[]>();
|
||||
for (const dep of deps) {
|
||||
const arr = depsByPhase.get(dep.phaseId) ?? [];
|
||||
arr.push(dep.dependsOnPhaseId);
|
||||
depsByPhase.set(dep.phaseId, arr);
|
||||
}
|
||||
const phases = rawPhases.map((ph) => ({
|
||||
...ph,
|
||||
dependsOn: depsByPhase.get(ph.id) ?? [],
|
||||
}));
|
||||
|
||||
// Collect tasks from all phases (some tasks only have phaseId, not initiativeId)
|
||||
const taskIds = new Set(initiativeTasks.map((t) => t.id));
|
||||
const allTasks = [...initiativeTasks];
|
||||
if (taskRepo) {
|
||||
for (const ph of rawPhases) {
|
||||
const phaseTasks = await taskRepo.findByPhaseId(ph.id);
|
||||
for (const t of phaseTasks) {
|
||||
if (!taskIds.has(t.id)) {
|
||||
taskIds.add(t.id);
|
||||
allTasks.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { phases, tasks: allTasks, pages };
|
||||
}
|
||||
|
||||
export function architectProcedures(publicProcedure: ProcedureBuilder) {
|
||||
return {
|
||||
@@ -117,13 +169,7 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) {
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
let pages: import('../../agent/content-serializer.js').PageForSerialization[] | undefined;
|
||||
if (ctx.pageRepository) {
|
||||
const rawPages = await ctx.pageRepository.findByInitiativeId(input.initiativeId);
|
||||
if (rawPages.length > 0) {
|
||||
pages = rawPages;
|
||||
}
|
||||
}
|
||||
const context = await gatherInitiativeContext(ctx.phaseRepository, ctx.taskRepository, ctx.pageRepository, input.initiativeId);
|
||||
|
||||
const prompt = buildBreakdownPrompt();
|
||||
|
||||
@@ -134,7 +180,12 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) {
|
||||
mode: 'breakdown',
|
||||
provider: input.provider,
|
||||
initiativeId: input.initiativeId,
|
||||
inputContext: { initiative, pages },
|
||||
inputContext: {
|
||||
initiative,
|
||||
pages: context.pages.length > 0 ? context.pages : undefined,
|
||||
phases: context.phases.length > 0 ? context.phases : undefined,
|
||||
tasks: context.tasks.length > 0 ? context.tasks : undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -285,6 +336,8 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) {
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
const context = await gatherInitiativeContext(ctx.phaseRepository, ctx.taskRepository, ctx.pageRepository, phase.initiativeId);
|
||||
|
||||
const prompt = buildDecomposePrompt();
|
||||
|
||||
return agentManager.spawn({
|
||||
@@ -294,7 +347,14 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) {
|
||||
mode: 'decompose',
|
||||
provider: input.provider,
|
||||
initiativeId: phase.initiativeId,
|
||||
inputContext: { initiative, phase, task },
|
||||
inputContext: {
|
||||
initiative,
|
||||
phase,
|
||||
task,
|
||||
pages: context.pages.length > 0 ? context.pages : undefined,
|
||||
phases: context.phases.length > 0 ? context.phases : undefined,
|
||||
tasks: context.tasks.length > 0 ? context.tasks : undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user