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:
Lukas May
2026-02-10 10:18:55 +01:00
parent 118f6d0d51
commit bf898cb86e
9 changed files with 156 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[];
}
/**

View File

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