fix: Persist and expose task dependencies from detail output

Detail agents define task dependencies in YAML frontmatter but they were
silently dropped — never written to the task_dependencies table. This
caused all tasks to dispatch in parallel regardless of intended ordering,
and the frontend showed no dependency information.

- Add fileIdToDbId mapping and second-pass dependency creation in
  output-handler.ts (mirrors existing phase dependency pattern)
- Add task_dependency to changeset entry entityType enum
- Add listPhaseTaskDependencies tRPC procedure for batch querying
- Wire blockedBy in PhaseDetailPanel and PhaseWithTasks from real data
- Clarify dependency semantics in detail prompt
This commit is contained in:
Lukas May
2026-03-03 13:46:29 +01:00
parent 536cdf08a1
commit 9b91ffe0e5
9 changed files with 90 additions and 11 deletions

View File

@@ -498,6 +498,7 @@ export class OutputHandler {
const phaseInput = readFrontmatterFile(join(agentWorkdir, '.cw', 'input', 'phase.md'));
const phaseId = (phaseInput?.data?.id as string) ?? null;
const entries: CreateChangeSetEntryData[] = [];
const fileIdToDbId = new Map<string, string>();
// Load existing tasks for dedup — prevents duplicates when multiple agents finish concurrently
const existingTasks = phaseId ? await this.taskRepository.findByPhaseId(phaseId) : [];
@@ -506,6 +507,9 @@ export class OutputHandler {
for (const [i, t] of tasks.entries()) {
if (existingNames.has(t.title)) {
log.info({ agentId, task: t.title, phaseId }, 'skipped duplicate task');
// Map deduped file ID to existing DB ID for dependency resolution
const existing = existingTasks.find(et => et.name === t.title);
if (existing) fileIdToDbId.set(t.id, existing.id);
continue;
}
try {
@@ -518,6 +522,7 @@ export class OutputHandler {
category: (t.category as any) ?? 'execute',
type: (t.type as any) ?? 'auto',
});
fileIdToDbId.set(t.id, created.id);
existingNames.add(t.title); // prevent dupes within same agent output
entries.push({
entityType: 'task',
@@ -536,6 +541,29 @@ export class OutputHandler {
}
}
// Second pass: create task dependencies
let depSortOrder = entries.length;
for (const t of tasks) {
const taskDbId = fileIdToDbId.get(t.id);
if (!taskDbId || t.dependencies.length === 0) continue;
for (const depFileId of t.dependencies) {
const depDbId = fileIdToDbId.get(depFileId);
if (!depDbId) continue;
try {
await this.taskRepository.createDependency(taskDbId, depDbId);
entries.push({
entityType: 'task_dependency',
entityId: `${taskDbId}:${depDbId}`,
action: 'create',
newState: JSON.stringify({ taskId: taskDbId, dependsOnTaskId: depDbId }),
sortOrder: depSortOrder++,
});
} catch (err) {
log.warn({ agentId, taskDbId, depFileId, err: err instanceof Error ? err.message : String(err) }, 'failed to create task dependency');
}
}
}
if (entries.length > 0) {
try {
const cs = await this.changeSetRepository!.createWithEntries({

View File

@@ -13,7 +13,7 @@ ${CODEBASE_EXPLORATION}
<output_format>
Write one file per task to \`.cw/output/tasks/{id}.md\`:
- Frontmatter: \`title\`, \`category\` (execute|research|discuss|plan|detail|refine|verify|merge|review), \`type\` (auto|checkpoint:human-verify|checkpoint:decision|checkpoint:human-action), \`dependencies\` (list of task IDs)
- Frontmatter: \`title\`, \`category\` (execute|research|discuss|plan|detail|refine|verify|merge|review), \`type\` (auto|checkpoint:human-verify|checkpoint:decision|checkpoint:human-action), \`dependencies\` (list of task IDs that must complete before this task can start)
- Body: Detailed task description
</output_format>
@@ -59,6 +59,7 @@ Parallel tasks must not modify the same files. Include a file list per task:
Files: src/db/schema/users.ts (create), src/db/migrations/001_users.sql (create)
\`\`\`
If two tasks touch the same file or one needs the other's output, add a dependency.
Tasks with no dependencies run in parallel. Add a dependency when one task needs another's output or modifies the same files.
</file_ownership>
<task_sizing>