feat(12-04): add createTasksFromDecomposition procedure

- Add createDependency method to TaskRepository interface
- Implement createDependency in DrizzleTaskRepository
- Add createTasksFromDecomposition procedure for bulk task creation
- Procedure verifies plan exists before creating tasks
- Creates tasks in order, building number-to-ID map
- Creates task dependencies after all tasks exist
- Dependencies mapped from task numbers to IDs
This commit is contained in:
Lukas May
2026-02-01 11:46:49 +01:00
parent 89ec64ab2e
commit 66ad2ec6ef
3 changed files with 82 additions and 2 deletions

View File

@@ -7,7 +7,7 @@
import { eq, asc } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { DrizzleDatabase } from '../../index.js';
import { tasks, type Task } from '../../schema.js';
import { tasks, taskDependencies, type Task } from '../../schema.js';
import type {
TaskRepository,
CreateTaskData,
@@ -85,4 +85,16 @@ export class DrizzleTaskRepository implements TaskRepository {
await this.db.delete(tasks).where(eq(tasks.id, id));
}
async createDependency(taskId: string, dependsOnTaskId: string): Promise<void> {
const id = nanoid();
const now = new Date();
await this.db.insert(taskDependencies).values({
id,
taskId,
dependsOnTaskId,
createdAt: now,
});
}
}

View File

@@ -58,4 +58,11 @@ export interface TaskRepository {
* Throws if task not found.
*/
delete(id: string): Promise<void>;
/**
* Create a dependency between two tasks.
* The task identified by taskId will depend on dependsOnTaskId.
* Both tasks must exist.
*/
createDependency(taskId: string, dependsOnTaskId: string): Promise<void>;
}

View File

@@ -16,7 +16,7 @@ import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { PlanRepository } from '../db/repositories/plan-repository.js';
import type { DispatchManager } from '../dispatch/types.js';
import type { CoordinationManager } from '../coordination/types.js';
import type { Phase, Plan } from '../db/schema.js';
import type { Phase, Plan, Task } from '../db/schema.js';
import { buildDiscussPrompt, buildBreakdownPrompt } from '../agent/prompts.js';
/**
@@ -928,6 +928,67 @@ export const appRouter = router({
return repo.update(id, data);
}),
/**
* Create tasks from decomposition agent output.
* Creates all tasks in order, then creates dependencies from number mappings.
*/
createTasksFromDecomposition: publicProcedure
.input(z.object({
planId: z.string().min(1),
tasks: z.array(z.object({
number: z.number().int().positive(),
name: z.string().min(1),
description: z.string(),
type: z.enum(['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action']).default('auto'),
dependencies: z.array(z.number().int().positive()).optional(),
})),
}))
.mutation(async ({ ctx, input }) => {
const taskRepo = requireTaskRepository(ctx);
const planRepo = requirePlanRepository(ctx);
// Verify plan exists
const plan = await planRepo.findById(input.planId);
if (!plan) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `Plan '${input.planId}' not found`,
});
}
// Create tasks in order, building number-to-ID map
const numberToId = new Map<number, string>();
const created: Task[] = [];
for (const taskInput of input.tasks) {
const task = await taskRepo.create({
planId: input.planId,
name: taskInput.name,
description: taskInput.description,
type: taskInput.type,
order: taskInput.number,
status: 'pending',
});
numberToId.set(taskInput.number, task.id);
created.push(task);
}
// Create dependencies after all tasks exist
for (const taskInput of input.tasks) {
if (taskInput.dependencies && taskInput.dependencies.length > 0) {
const taskId = numberToId.get(taskInput.number)!;
for (const depNumber of taskInput.dependencies) {
const dependsOnTaskId = numberToId.get(depNumber);
if (dependsOnTaskId) {
await taskRepo.createDependency(taskId, dependsOnTaskId);
}
}
}
}
return created;
}),
// ===========================================================================
// Architect Spawn Procedures
// ===========================================================================