Add 'detailing' activity state derived from active detail agents (mode=detail, status running/waiting_for_input). Initiative cards show pulsing "Detailing" indicator. Phase sidebar items show spinner during active detailing and "Review changes" when the agent finishes.
111 lines
3.9 KiB
TypeScript
111 lines
3.9 KiB
TypeScript
/**
|
|
* Phase Dispatch Router — queue, dispatch, state, child tasks
|
|
*/
|
|
|
|
import { TRPCError } from '@trpc/server';
|
|
import { z } from 'zod';
|
|
import type { Task } from '../../db/schema.js';
|
|
import type { ProcedureBuilder } from '../trpc.js';
|
|
import { requirePhaseDispatchManager, requirePhaseRepository, requireTaskRepository } from './_helpers.js';
|
|
|
|
export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) {
|
|
return {
|
|
queuePhase: publicProcedure
|
|
.input(z.object({ phaseId: z.string().min(1) }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const phaseDispatchManager = requirePhaseDispatchManager(ctx);
|
|
await phaseDispatchManager.queuePhase(input.phaseId);
|
|
return { success: true };
|
|
}),
|
|
|
|
queueAllPhases: publicProcedure
|
|
.input(z.object({ initiativeId: z.string().min(1) }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const phaseDispatchManager = requirePhaseDispatchManager(ctx);
|
|
const phaseRepo = requirePhaseRepository(ctx);
|
|
const phases = await phaseRepo.findByInitiativeId(input.initiativeId);
|
|
let queued = 0;
|
|
for (const phase of phases) {
|
|
if (phase.status === 'approved') {
|
|
await phaseDispatchManager.queuePhase(phase.id);
|
|
queued++;
|
|
}
|
|
}
|
|
return { success: true, queued };
|
|
}),
|
|
|
|
dispatchNextPhase: publicProcedure
|
|
.mutation(async ({ ctx }) => {
|
|
const phaseDispatchManager = requirePhaseDispatchManager(ctx);
|
|
return phaseDispatchManager.dispatchNextPhase();
|
|
}),
|
|
|
|
getPhaseQueueState: publicProcedure
|
|
.query(async ({ ctx }) => {
|
|
const phaseDispatchManager = requirePhaseDispatchManager(ctx);
|
|
return phaseDispatchManager.getPhaseQueueState();
|
|
}),
|
|
|
|
createChildTasks: publicProcedure
|
|
.input(z.object({
|
|
parentTaskId: 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 parentTask = await taskRepo.findById(input.parentTaskId);
|
|
if (!parentTask) {
|
|
throw new TRPCError({
|
|
code: 'NOT_FOUND',
|
|
message: `Parent task '${input.parentTaskId}' not found`,
|
|
});
|
|
}
|
|
if (parentTask.category !== 'detail') {
|
|
throw new TRPCError({
|
|
code: 'BAD_REQUEST',
|
|
message: `Parent task must have category 'detail', got '${parentTask.category}'`,
|
|
});
|
|
}
|
|
|
|
const numberToId = new Map<number, string>();
|
|
const created: Task[] = [];
|
|
|
|
for (const taskInput of input.tasks) {
|
|
const task = await taskRepo.create({
|
|
parentTaskId: input.parentTaskId,
|
|
phaseId: parentTask.phaseId,
|
|
initiativeId: parentTask.initiativeId,
|
|
name: taskInput.name,
|
|
description: taskInput.description,
|
|
type: taskInput.type,
|
|
order: taskInput.number,
|
|
status: 'pending',
|
|
});
|
|
numberToId.set(taskInput.number, task.id);
|
|
created.push(task);
|
|
}
|
|
|
|
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;
|
|
}),
|
|
};
|
|
}
|