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:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"}
|
||||
@@ -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