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