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

@@ -33,7 +33,7 @@
1. **tRPC procedure** calls `agentManager.spawn(options)`
2. Manager generates alias (adjective-animal), creates DB record
3. `AgentProcessManager.createWorktree()` — creates git worktree at `.cw-worktrees/agent/<alias>/`
4. `file-io.writeInputFiles()` — writes `.cw/input/` with initiative, pages, phase, task as frontmatter
4. `file-io.writeInputFiles()` — writes `.cw/input/` with assignment files (initiative, pages, phase, task) and read-only context dirs (`context/phases/`, `context/tasks/`)
5. Provider config builds spawn command via `buildSpawnCommand()`
6. `spawnDetached()` — launches detached child process with file output redirection
7. `FileTailer` watches output file, fires `onEvent` and `onRawContent` callbacks

View File

@@ -117,9 +117,9 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
| Procedure | Type | Description |
|-----------|------|-------------|
| spawnArchitectDiscuss | mutation | Discussion agent |
| spawnArchitectBreakdown | mutation | Breakdown agent (generates phases) |
| spawnArchitectBreakdown | mutation | Breakdown agent (generates phases). Passes full initiative context (existing phases, tasks, pages) |
| spawnArchitectRefine | mutation | Refine agent (generates proposals) |
| spawnArchitectDecompose | mutation | Decompose agent (generates tasks) |
| spawnArchitectDecompose | mutation | Decompose agent (generates tasks). Passes full initiative context (sibling phases, tasks, pages) |
### Dispatch
| Procedure | Type | Description |

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/routetree.gen.ts","./src/router.tsx","./src/vite-env.d.ts","./src/components/accountcard.tsx","./src/components/actionmenu.tsx","./src/components/agentactions.tsx","./src/components/agentoutputviewer.tsx","./src/components/createinitiativedialog.tsx","./src/components/decisionlist.tsx","./src/components/dependencyindicator.tsx","./src/components/errorboundary.tsx","./src/components/executiontab.tsx","./src/components/freetextinput.tsx","./src/components/inboxdetailpanel.tsx","./src/components/inboxlist.tsx","./src/components/initiativecard.tsx","./src/components/initiativeheader.tsx","./src/components/initiativelist.tsx","./src/components/messagecard.tsx","./src/components/optiongroup.tsx","./src/components/phaseaccordion.tsx","./src/components/progressbar.tsx","./src/components/progresspanel.tsx","./src/components/projectpicker.tsx","./src/components/questionform.tsx","./src/components/refinespawndialog.tsx","./src/components/registerprojectdialog.tsx","./src/components/skeleton.tsx","./src/components/spawnarchitectdropdown.tsx","./src/components/statusbadge.tsx","./src/components/statusdot.tsx","./src/components/taskdetailmodal.tsx","./src/components/taskrow.tsx","./src/components/editor/blockdraghandle.tsx","./src/components/editor/blockselectionextension.ts","./src/components/editor/contentproposalreview.tsx","./src/components/editor/contenttab.tsx","./src/components/editor/deletesubpagedialog.tsx","./src/components/editor/pagebreadcrumb.tsx","./src/components/editor/pagelinkdeletiondetector.ts","./src/components/editor/pagelinkextension.tsx","./src/components/editor/pagetitlecontext.tsx","./src/components/editor/pagetree.tsx","./src/components/editor/phasecontenteditor.tsx","./src/components/editor/refineagentpanel.tsx","./src/components/editor/slashcommandlist.tsx","./src/components/editor/slashcommands.ts","./src/components/editor/tiptapeditor.tsx","./src/components/editor/slash-command-items.ts","./src/components/execution/breakdownsection.tsx","./src/components/execution/executioncontext.tsx","./src/components/execution/phaseactions.tsx","./src/components/execution/phasedetailpanel.tsx","./src/components/execution/phasesidebaritem.tsx","./src/components/execution/phasewithtasks.tsx","./src/components/execution/phaseslist.tsx","./src/components/execution/progresssidebar.tsx","./src/components/execution/taskmodal.tsx","./src/components/execution/index.ts","./src/components/pipeline/pipelinegraph.tsx","./src/components/pipeline/pipelinephasegroup.tsx","./src/components/pipeline/pipelinestagecolumn.tsx","./src/components/pipeline/pipelinetab.tsx","./src/components/pipeline/pipelinetaskcard.tsx","./src/components/pipeline/index.ts","./src/components/review/commentform.tsx","./src/components/review/commentthread.tsx","./src/components/review/diffviewer.tsx","./src/components/review/filecard.tsx","./src/components/review/hunkrows.tsx","./src/components/review/linewithcomments.tsx","./src/components/review/reviewsidebar.tsx","./src/components/review/reviewtab.tsx","./src/components/review/dummy-data.ts","./src/components/review/index.ts","./src/components/review/parse-diff.ts","./src/components/review/types.ts","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/sonner.tsx","./src/components/ui/textarea.tsx","./src/hooks/index.ts","./src/hooks/useautosave.ts","./src/hooks/usedebounce.ts","./src/hooks/useliveupdates.ts","./src/hooks/useoptimisticmutation.ts","./src/hooks/usephaseautosave.ts","./src/hooks/userefineagent.ts","./src/hooks/usespawnmutation.ts","./src/hooks/usesubscriptionwitherrorhandling.ts","./src/layouts/applayout.tsx","./src/lib/_type-check-temp.ts","./src/lib/invalidation.ts","./src/lib/markdown-to-tiptap.ts","./src/lib/parse-agent-output.ts","./src/lib/trpc.ts","./src/lib/utils.ts","./src/routes/__root.tsx","./src/routes/agents.tsx","./src/routes/inbox.tsx","./src/routes/index.tsx","./src/routes/settings.tsx","./src/routes/initiatives/$id.tsx","./src/routes/initiatives/index.tsx","./src/routes/settings/health.tsx","./src/routes/settings/index.tsx"],"errors":true,"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/routetree.gen.ts","./src/router.tsx","./src/vite-env.d.ts","./src/components/accountcard.tsx","./src/components/actionmenu.tsx","./src/components/agentactions.tsx","./src/components/agentoutputviewer.tsx","./src/components/changesetbanner.tsx","./src/components/createinitiativedialog.tsx","./src/components/decisionlist.tsx","./src/components/dependencyindicator.tsx","./src/components/errorboundary.tsx","./src/components/executiontab.tsx","./src/components/freetextinput.tsx","./src/components/inboxdetailpanel.tsx","./src/components/inboxlist.tsx","./src/components/initiativecard.tsx","./src/components/initiativeheader.tsx","./src/components/initiativelist.tsx","./src/components/messagecard.tsx","./src/components/optiongroup.tsx","./src/components/phaseaccordion.tsx","./src/components/progressbar.tsx","./src/components/progresspanel.tsx","./src/components/projectpicker.tsx","./src/components/questionform.tsx","./src/components/refinespawndialog.tsx","./src/components/registerprojectdialog.tsx","./src/components/skeleton.tsx","./src/components/spawnarchitectdropdown.tsx","./src/components/statusbadge.tsx","./src/components/statusdot.tsx","./src/components/taskdetailmodal.tsx","./src/components/taskrow.tsx","./src/components/editor/blockdraghandle.tsx","./src/components/editor/blockselectionextension.ts","./src/components/editor/contenttab.tsx","./src/components/editor/deletesubpagedialog.tsx","./src/components/editor/pagebreadcrumb.tsx","./src/components/editor/pagelinkdeletiondetector.ts","./src/components/editor/pagelinkextension.tsx","./src/components/editor/pagetitlecontext.tsx","./src/components/editor/pagetree.tsx","./src/components/editor/phasecontenteditor.tsx","./src/components/editor/refineagentpanel.tsx","./src/components/editor/slashcommandlist.tsx","./src/components/editor/slashcommands.ts","./src/components/editor/tiptapeditor.tsx","./src/components/editor/slash-command-items.ts","./src/components/execution/breakdownsection.tsx","./src/components/execution/executioncontext.tsx","./src/components/execution/phaseactions.tsx","./src/components/execution/phasedetailpanel.tsx","./src/components/execution/phasesidebaritem.tsx","./src/components/execution/phasewithtasks.tsx","./src/components/execution/phaseslist.tsx","./src/components/execution/progresssidebar.tsx","./src/components/execution/taskmodal.tsx","./src/components/execution/index.ts","./src/components/pipeline/pipelinegraph.tsx","./src/components/pipeline/pipelinephasegroup.tsx","./src/components/pipeline/pipelinestagecolumn.tsx","./src/components/pipeline/pipelinetab.tsx","./src/components/pipeline/pipelinetaskcard.tsx","./src/components/pipeline/index.ts","./src/components/review/commentform.tsx","./src/components/review/commentthread.tsx","./src/components/review/diffviewer.tsx","./src/components/review/filecard.tsx","./src/components/review/hunkrows.tsx","./src/components/review/linewithcomments.tsx","./src/components/review/reviewsidebar.tsx","./src/components/review/reviewtab.tsx","./src/components/review/dummy-data.ts","./src/components/review/index.ts","./src/components/review/parse-diff.ts","./src/components/review/types.ts","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/sonner.tsx","./src/components/ui/textarea.tsx","./src/hooks/index.ts","./src/hooks/useautosave.ts","./src/hooks/usedebounce.ts","./src/hooks/useliveupdates.ts","./src/hooks/useoptimisticmutation.ts","./src/hooks/usephaseautosave.ts","./src/hooks/userefineagent.ts","./src/hooks/usespawnmutation.ts","./src/hooks/usesubscriptionwitherrorhandling.ts","./src/layouts/applayout.tsx","./src/lib/invalidation.ts","./src/lib/markdown-to-tiptap.ts","./src/lib/parse-agent-output.ts","./src/lib/trpc.ts","./src/lib/utils.ts","./src/routes/__root.tsx","./src/routes/agents.tsx","./src/routes/inbox.tsx","./src/routes/index.tsx","./src/routes/settings.tsx","./src/routes/initiatives/$id.tsx","./src/routes/initiatives/index.tsx","./src/routes/settings/health.tsx","./src/routes/settings/index.tsx"],"errors":true,"version":"5.9.3"}

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