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:
@@ -7,7 +7,7 @@
|
|||||||
import { eq, asc } from 'drizzle-orm';
|
import { eq, asc } from 'drizzle-orm';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { DrizzleDatabase } from '../../index.js';
|
import type { DrizzleDatabase } from '../../index.js';
|
||||||
import { tasks, type Task } from '../../schema.js';
|
import { tasks, taskDependencies, type Task } from '../../schema.js';
|
||||||
import type {
|
import type {
|
||||||
TaskRepository,
|
TaskRepository,
|
||||||
CreateTaskData,
|
CreateTaskData,
|
||||||
@@ -85,4 +85,16 @@ export class DrizzleTaskRepository implements TaskRepository {
|
|||||||
|
|
||||||
await this.db.delete(tasks).where(eq(tasks.id, id));
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,11 @@ export interface TaskRepository {
|
|||||||
* Throws if task not found.
|
* Throws if task not found.
|
||||||
*/
|
*/
|
||||||
delete(id: string): Promise<void>;
|
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { PhaseRepository } from '../db/repositories/phase-repository.js';
|
|||||||
import type { PlanRepository } from '../db/repositories/plan-repository.js';
|
import type { PlanRepository } from '../db/repositories/plan-repository.js';
|
||||||
import type { DispatchManager } from '../dispatch/types.js';
|
import type { DispatchManager } from '../dispatch/types.js';
|
||||||
import type { CoordinationManager } from '../coordination/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';
|
import { buildDiscussPrompt, buildBreakdownPrompt } from '../agent/prompts.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -928,6 +928,67 @@ export const appRouter = router({
|
|||||||
return repo.update(id, data);
|
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
|
// Architect Spawn Procedures
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user