From 536cdf08a12ba0bb6690cd4ff4d2ea5a6d1022b8 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 3 Mar 2026 13:42:37 +0100 Subject: [PATCH] feat: Propagate task summaries and input context to execution agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Execution agents were spawning blind — no input files, no knowledge of what predecessor tasks accomplished. This adds three capabilities: 1. summary column on tasks table — completeTask() reads the finishing agent's result.message and stores it on the task record 2. dispatchNext() gathers full initiative context (initiative, phase, sibling tasks, pages) and passes it as inputContext so agents get .cw/input/task.md, initiative.md, phase.md, and context directories 3. context/tasks/*.md files now include the summary field in frontmatter so dependent agents can see what prior agents accomplished --- apps/server/agent/file-io.ts | 1 + apps/server/agent/prompts/shared.ts | 3 +- apps/server/container.ts | 2 + apps/server/db/schema.ts | 1 + apps/server/dispatch/manager.ts | 99 ++++++++++++++++++- apps/server/drizzle/0026_add_task_summary.sql | 3 + apps/server/drizzle/meta/_journal.json | 7 ++ docs/database.md | 3 +- docs/dispatch-events.md | 12 ++- 9 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 apps/server/drizzle/0026_add_task_summary.sql diff --git a/apps/server/agent/file-io.ts b/apps/server/agent/file-io.ts index c7455e5..4bdfab4 100644 --- a/apps/server/agent/file-io.ts +++ b/apps/server/agent/file-io.ts @@ -247,6 +247,7 @@ export function writeInputFiles(options: WriteInputFilesOptions): void { type: t.type, priority: t.priority, status: t.status, + summary: t.summary, }, t.description ?? '', ); diff --git a/apps/server/agent/prompts/shared.ts b/apps/server/agent/prompts/shared.ts index a23896f..b678700 100644 --- a/apps/server/agent/prompts/shared.ts +++ b/apps/server/agent/prompts/shared.ts @@ -25,7 +25,8 @@ Read \`.cw/input/manifest.json\` first, then read listed files from \`.cw/input/ **Context Files (read-only)** Present when \`contextFiles\` exists in manifest: - \`context/phases/\` — frontmatter: id, name, status, dependsOn; body: description -- \`context/tasks/\` — frontmatter: id, name, phaseId, parentTaskId, category, type, priority, status; body: description +- \`context/tasks/\` — frontmatter: id, name, phaseId, parentTaskId, category, type, priority, status, summary; body: description + Completed tasks include a \`summary\` field with what the previous agent accomplished. Do not duplicate or contradict context file content in your output. `; diff --git a/apps/server/container.ts b/apps/server/container.ts index 1c8198d..62726af 100644 --- a/apps/server/container.ts +++ b/apps/server/container.ts @@ -190,6 +190,8 @@ export async function createContainer(options?: ContainerOptions): Promise 0 ? context.phases : undefined, + tasks: context.tasks.length > 0 ? context.tasks : undefined, + pages: context.pages.length > 0 ? context.pages : undefined, + }; + } catch (err) { + log.warn({ taskId: task.id, err }, 'failed to gather initiative context for dispatch'); + } + } + // Spawn agent with task (alias auto-generated by agent manager) const agent = await this.agentManager.spawn({ taskId: nextTask.taskId, @@ -375,6 +404,7 @@ export class DefaultDispatchManager implements DispatchManager { prompt: buildExecutePrompt(task.description || task.name), baseBranch, branchName, + inputContext, }); log.info({ taskId: nextTask.taskId, agentId: agent.id }, 'task dispatched'); @@ -460,6 +490,71 @@ export class DefaultDispatchManager implements DispatchManager { return task.type.startsWith('checkpoint:'); } + /** + * Store the completing agent's result summary on the task record. + */ + private async storeAgentSummary(taskId: string, agentId?: string): Promise { + if (!agentId || !this.agentRepository) return; + try { + const agentRecord = await this.agentRepository.findById(agentId); + if (agentRecord?.result) { + const result: AgentResult = JSON.parse(agentRecord.result); + if (result.message) { + await this.taskRepository.update(taskId, { summary: result.message }); + } + } + } catch (err) { + log.warn({ taskId, agentId, err }, 'failed to store agent summary on task'); + } + } + + /** + * Gather initiative context for passing to execution agents. + * Reuses the same pattern as architect.ts gatherInitiativeContext. + */ + private async gatherInitiativeContext(initiativeId: string): Promise<{ + phases: Array; + tasks: Task[]; + pages: PageForSerialization[]; + }> { + const [rawPhases, deps, initiativeTasks, pages] = await Promise.all([ + this.phaseRepository?.findByInitiativeId(initiativeId) ?? [], + this.phaseRepository?.findDependenciesByInitiativeId(initiativeId) ?? [], + this.taskRepository.findByInitiativeId(initiativeId), + this.pageRepository?.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]; + for (const ph of rawPhases) { + const phaseTasks = await this.taskRepository.findByPhaseId(ph.id); + for (const t of phaseTasks) { + if (!taskIds.has(t.id)) { + taskIds.add(t.id); + allTasks.push(t); + } + } + } + + // Only include implementation tasks — planning tasks are irrelevant noise + const implementationTasks = allTasks.filter(t => !isPlanningCategory(t.category)); + + return { phases, tasks: implementationTasks, pages }; + } + /** * Determine if a task requires approval before being marked complete. * Checks task-level override first, then falls back to initiative setting. diff --git a/apps/server/drizzle/0026_add_task_summary.sql b/apps/server/drizzle/0026_add_task_summary.sql new file mode 100644 index 0000000..0f95bb8 --- /dev/null +++ b/apps/server/drizzle/0026_add_task_summary.sql @@ -0,0 +1,3 @@ +-- Add summary column to tasks table. +-- Stores the completing agent's result message for propagation to dependent tasks. +ALTER TABLE tasks ADD COLUMN summary TEXT; diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index c6aa940..fea8954 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1771718400000, "tag": "0025_fix_agents_fk_constraints", "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1771804800000, + "tag": "0026_add_task_summary", + "breakpoints": true } ] } \ No newline at end of file diff --git a/docs/database.md b/docs/database.md index ad29c3b..0b2f85f 100644 --- a/docs/database.md +++ b/docs/database.md @@ -51,6 +51,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r | status | text enum | 'pending_approval' \| 'pending' \| 'in_progress' \| 'completed' \| 'blocked' | | requiresApproval | integer/boolean nullable | null = inherit from initiative | | order | integer | default 0 | +| summary | text nullable | Agent result summary — propagated to dependent tasks as context | | createdAt, updatedAt | integer/timestamp | | ### task_dependencies @@ -191,4 +192,4 @@ Key rules: - See [database-migrations.md](database-migrations.md) for full workflow - Snapshots stale after 0008; migrations 0008+ are hand-written -Current migrations: 0000 through 0024 (25 total). +Current migrations: 0000 through 0026 (27 total). diff --git a/docs/dispatch-events.md b/docs/dispatch-events.md index 48b718e..054458a 100644 --- a/docs/dispatch-events.md +++ b/docs/dispatch-events.md @@ -56,11 +56,13 @@ AccountCredentialsRefreshedEvent { accountId, expiresAt, previousExpiresAt? } 1. **Queue** — `queue(taskId)` fetches task dependencies, adds to internal Map 2. **Dispatch** — `dispatchNext()` finds highest-priority task with all deps complete -3. **Priority order**: high > medium > low, then oldest first (FIFO within priority) -4. **Checkpoint skip** — Tasks with type starting with `checkpoint:` skip auto-dispatch -5. **Planning skip** — Planning-category tasks (research, discuss, plan, detail, refine) skip auto-dispatch — they use the architect flow -6. **Approval check** — `completeTask()` checks `requiresApproval` (task-level, then initiative-level) -7. **Approval flow** — If approval required: status → `pending_approval`, emit `task:pending_approval` +3. **Context gathering** — Before spawn, `dispatchNext()` gathers initiative context (initiative, phase, tasks, pages) and passes as `inputContext` to the agent. Agents receive `.cw/input/task.md`, `initiative.md`, `phase.md`, `context/tasks/`, `context/phases/`, and `pages/`. +4. **Priority order**: high > medium > low, then oldest first (FIFO within priority) +5. **Checkpoint skip** — Tasks with type starting with `checkpoint:` skip auto-dispatch +6. **Planning skip** — Planning-category tasks (research, discuss, plan, detail, refine) skip auto-dispatch — they use the architect flow +7. **Summary propagation** — `completeTask()` reads the completing agent's `result.message` and stores it on the task's `summary` column. Dependent tasks see this summary in `context/tasks/.md` frontmatter. +8. **Approval check** — `completeTask()` checks `requiresApproval` (task-level, then initiative-level) +9. **Approval flow** — If approval required: status → `pending_approval`, emit `task:pending_approval` ### DispatchManager Methods