diff --git a/docs/agent.md b/docs/agent.md index 71ee15c..c84bf3f 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -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//` -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 diff --git a/docs/server-api.md b/docs/server-api.md index e341e5c..2e941a0 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -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 | diff --git a/packages/web/tsconfig.app.tsbuildinfo b/packages/web/tsconfig.app.tsbuildinfo index 243adcb..2c91839 100644 --- a/packages/web/tsconfig.app.tsbuildinfo +++ b/packages/web/tsconfig.app.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/src/agent/file-io.ts b/src/agent/file-io.ts index 7399d19..83bb981 100644 --- a/src/agent/file-io.ts +++ b/src/agent/file-io.ts @@ -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', ); } diff --git a/src/agent/prompts/breakdown.ts b/src/agent/prompts/breakdown.ts index c70e1ea..f753d4f 100644 --- a/src/agent/prompts/breakdown.ts +++ b/src/agent/prompts/breakdown.ts @@ -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 diff --git a/src/agent/prompts/decompose.ts b/src/agent/prompts/decompose.ts index 6cf1d04..dedf9a0 100644 --- a/src/agent/prompts/decompose.ts +++ b/src/agent/prompts/decompose.ts @@ -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) diff --git a/src/agent/prompts/shared.ts b/src/agent/prompts/shared.ts index 56948b6..0a85cc8 100644 --- a/src/agent/prompts/shared.ts +++ b/src/agent/prompts/shared.ts @@ -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 diff --git a/src/agent/types.ts b/src/agent/types.ts index 370f1ac..b91c15b 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -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; + /** All tasks for the initiative (read-only context for agents) */ + tasks?: import('../db/schema.js').Task[]; } /** diff --git a/src/trpc/routers/architect.ts b/src/trpc/routers/architect.ts index faa217e..d3e09f1 100644 --- a/src/trpc/routers/architect.ts +++ b/src/trpc/routers/architect.ts @@ -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; + 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(); + 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, + }, }); }), };