feat: Propagate task summaries and input context to execution agents
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
This commit is contained in:
@@ -15,12 +15,15 @@ import type {
|
||||
TaskDispatchedEvent,
|
||||
TaskPendingApprovalEvent,
|
||||
} from '../events/index.js';
|
||||
import type { AgentManager } from '../agent/types.js';
|
||||
import type { AgentManager, AgentResult } from '../agent/types.js';
|
||||
import type { TaskRepository } from '../db/repositories/task-repository.js';
|
||||
import type { MessageRepository } from '../db/repositories/message-repository.js';
|
||||
import type { AgentRepository } from '../db/repositories/agent-repository.js';
|
||||
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
|
||||
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
|
||||
import type { Task } from '../db/schema.js';
|
||||
import type { PageRepository } from '../db/repositories/page-repository.js';
|
||||
import type { Task, Phase } from '../db/schema.js';
|
||||
import type { PageForSerialization } from '../agent/content-serializer.js';
|
||||
import type { DispatchManager, QueuedTask, DispatchResult } from './types.js';
|
||||
import { phaseBranchName, taskBranchName, isPlanningCategory, generateInitiativeBranch } from '../git/branch-naming.js';
|
||||
import { buildExecutePrompt } from '../agent/prompts/index.js';
|
||||
@@ -64,6 +67,8 @@ export class DefaultDispatchManager implements DispatchManager {
|
||||
private eventBus: EventBus,
|
||||
private initiativeRepository?: InitiativeRepository,
|
||||
private phaseRepository?: PhaseRepository,
|
||||
private agentRepository?: AgentRepository,
|
||||
private pageRepository?: PageRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -182,6 +187,9 @@ export class DefaultDispatchManager implements DispatchManager {
|
||||
// Determine if approval is required
|
||||
const requiresApproval = await this.taskRequiresApproval(task);
|
||||
|
||||
// Store agent result summary on the task for propagation to dependent tasks
|
||||
await this.storeAgentSummary(taskId, agentId);
|
||||
|
||||
if (requiresApproval) {
|
||||
// Set to pending_approval instead of completed
|
||||
await this.taskRepository.update(taskId, { status: 'pending_approval' });
|
||||
@@ -367,6 +375,27 @@ export class DefaultDispatchManager implements DispatchManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Gather initiative context for the agent's input files
|
||||
let inputContext: import('../agent/types.js').AgentInputContext | undefined;
|
||||
if (task.initiativeId) {
|
||||
try {
|
||||
const initiative = await this.initiativeRepository?.findById(task.initiativeId);
|
||||
const phase = task.phaseId ? await this.phaseRepository?.findById(task.phaseId) : undefined;
|
||||
const context = await this.gatherInitiativeContext(task.initiativeId);
|
||||
|
||||
inputContext = {
|
||||
initiative: initiative ?? undefined,
|
||||
task,
|
||||
phase: phase ?? undefined,
|
||||
phases: context.phases.length > 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<void> {
|
||||
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<Phase & { dependsOn?: string[] }>;
|
||||
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<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];
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user