Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
5bf5e4a
{
fblgit Sep 21, 2025
caea693
{
fblgit Sep 21, 2025
0b3092c
{
fblgit Sep 21, 2025
9917790
{
fblgit Sep 21, 2025
0121141
{
fblgit Sep 21, 2025
576f0d0
{
fblgit Sep 21, 2025
b0fd6a4
{
fblgit Sep 21, 2025
e7f5c0f
{
fblgit Sep 21, 2025
aa91ac2
{
fblgit Sep 21, 2025
52de551
{
fblgit Sep 21, 2025
9caa896
{
fblgit Sep 21, 2025
a708b27
{
fblgit Sep 21, 2025
eabc739
{
fblgit Sep 21, 2025
0cbd66f
{
fblgit Sep 21, 2025
ca060eb
{
fblgit Sep 21, 2025
730483c
{
fblgit Sep 21, 2025
d99e72c
{
fblgit Sep 21, 2025
6a2d595
{
fblgit Sep 21, 2025
572d440
{
fblgit Sep 21, 2025
85f0568
{
fblgit Sep 21, 2025
af48538
{
fblgit Sep 21, 2025
e70324e
{
fblgit Sep 21, 2025
e4abe5a
{
fblgit Sep 21, 2025
d40f5f0
{
fblgit Sep 21, 2025
9e2686a
{
fblgit Sep 21, 2025
7f3280e
{
fblgit Sep 21, 2025
93853ba
{
fblgit Sep 21, 2025
ed2bbad
{
fblgit Sep 21, 2025
d1fda25
{
fblgit Sep 21, 2025
f269971
{
fblgit Sep 21, 2025
3022aa7
{
fblgit Sep 21, 2025
dda5e56
{
fblgit Sep 21, 2025
fd6dbad
{
fblgit Sep 21, 2025
b6ee2fe
{
fblgit Sep 21, 2025
c26d93c
{
fblgit Sep 21, 2025
9231640
{
fblgit Sep 21, 2025
54a1003
{
fblgit Sep 21, 2025
8fbd922
{
fblgit Sep 21, 2025
8c56729
{
fblgit Sep 21, 2025
5111f6d
{
fblgit Sep 21, 2025
f557d26
{
fblgit Sep 21, 2025
2ca3a8a
{
fblgit Sep 21, 2025
064407e
{
fblgit Sep 21, 2025
7fa93bd
{
fblgit Sep 21, 2025
744ba82
{
fblgit Sep 21, 2025
cba921e
{
fblgit Sep 21, 2025
9d2a923
{
fblgit Sep 21, 2025
3d68467
{
fblgit Sep 21, 2025
9c00847
{
fblgit Sep 21, 2025
f60024b
{
fblgit Sep 21, 2025
755c7fd
{
fblgit Sep 21, 2025
6e511e7
{
fblgit Sep 21, 2025
2cbf4e3
{
fblgit Sep 21, 2025
ade463e
{
fblgit Sep 21, 2025
fa2bfa5
revert
fblgit Sep 21, 2025
65662e9
{
fblgit Sep 21, 2025
11b3691
{
fblgit Sep 21, 2025
5b17352
{
fblgit Sep 21, 2025
586a97c
{
fblgit Sep 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/handlers/task/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./task.delete.handler"; // NEW delete handler
export * from "./task.context.handler"; // NEW context generation
export * from "./task.decompose.handler"; // NEW decompose handler
export * from "./task.create_project.handler"; // NEW project creation
export * from "./task.get_project.handler"; // NEW get project with hierarchy

// Task attachment handlers - key-value store for tasks
export * from "./task.create_attachment.handler";
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/handlers/task/task.create_project.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class TaskCreateProjectHandler {

// Map subtask dependencies to real task IDs
const realDependencies = subtask.dependencies
.map(depId => subtaskMapping[depId])
.map((depId: string) => subtaskMapping[depId])
.filter(Boolean); // Remove any unmapped dependencies

if (realDependencies.length > 0) {
Expand Down Expand Up @@ -285,7 +285,7 @@ export class TaskCreateProjectHandler {
const subtaskTaskId = subtaskMapping[subtask.id];
if (subtaskTaskId) {
dependencyMapping[subtaskTaskId] = subtask.dependencies
.map(depId => subtaskMapping[depId])
.map((depId: string) => subtaskMapping[depId])
.filter(Boolean);
}
}
Expand Down
207 changes: 207 additions & 0 deletions apps/server/src/handlers/task/task.get_project.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { EventHandler, Instrumented, Resilient } from "@/core/decorator";
import type { EventContext } from "@/core/context";
import { taskGetProjectInput, taskGetProjectOutput } from "@/schemas/task.schema";
import type { TaskGetProjectInput, TaskGetProjectOutput } from "@/schemas/task.schema";
import { getRedis } from "@/core/redis";
import { registry } from "@/core/registry";

@EventHandler({
event: "task.get_project",
inputSchema: taskGetProjectInput,
outputSchema: taskGetProjectOutput,
persist: false,
rateLimit: 30,
description: "Fetch project-structured tasks with hierarchy and attachments"
})
export class TaskGetProjectHandler {
@Instrumented(3600) // Cache for 1 hour
@Resilient({
rateLimit: { limit: 30, windowMs: 60000 },
timeout: 10000,
circuitBreaker: { threshold: 5, timeout: 30000 }
})
async handle(input: TaskGetProjectInput, ctx: EventContext): Promise<TaskGetProjectOutput> {
const redis = getRedis();
let projectId = input.projectId;
let parentTaskId = input.taskId;

// If only taskId provided, get projectId from task metadata
if (!projectId && parentTaskId) {
const taskData = await ctx.prisma.task.findUnique({
where: { id: parentTaskId }
});
if (!taskData?.metadata) {
throw new Error(`Task ${parentTaskId} not found or has no metadata`);
}
const metadata = taskData.metadata as any;
projectId = metadata.projectId;
if (!projectId) {
throw new Error(`Task ${parentTaskId} is not a project task`);
}
}

// If only projectId provided, get parentTaskId from Redis
if (projectId && !parentTaskId) {
const projectKey = `cb:project:${projectId}`;
const redisTaskId = await redis.pub.hget(projectKey, "parentTaskId");
if (redisTaskId) {
parentTaskId = redisTaskId;
}
if (!parentTaskId) {
// Fallback to PostgreSQL
const projectTask = await ctx.prisma.task.findFirst({
where: {
metadata: {
path: ["projectId"],
equals: projectId
}
}
});
if (!projectTask) {
throw new Error(`Project ${projectId} not found`);
}
parentTaskId = projectTask.id;
}
}

// Fetch parent task with attachments
const parentTask = await ctx.prisma.task.findUnique({
where: { id: parentTaskId },
include: {
attachments: {
orderBy: { createdAt: "desc" }
}
}
});

if (!parentTask) {
throw new Error(`Parent task ${parentTaskId} not found`);
}

// Fetch all subtasks for this project
const subtasks = await ctx.prisma.task.findMany({
where: {
metadata: {
path: ["projectId"],
equals: projectId
}
},
include: {
attachments: {
where: {
key: {
startsWith: "subtask_context"
}
},
orderBy: { createdAt: "desc" }
}
},
orderBy: { createdAt: "asc" }
});

// Filter out the parent task from subtasks
const actualSubtasks = subtasks.filter(t => t.id !== parentTaskId);

// Get project metadata from Redis cache or attachments
const projectKey = `cb:project:${projectId}`;
let projectMetadata: any = {};

const redisData = await redis.pub.hgetall(projectKey);
if (redisData && Object.keys(redisData).length > 0) {
projectMetadata = {
description: redisData.description || "",
status: redisData.status || "unknown",
constraints: redisData.constraints ? JSON.parse(redisData.constraints) : [],
requirements: redisData.requirements ? JSON.parse(redisData.requirements) : [],
estimatedMinutes: redisData.estimatedMinutes ? parseInt(redisData.estimatedMinutes) : undefined,
createdAt: redisData.createdAt || new Date().toISOString(),
createdBy: redisData.createdBy || ctx.instanceId
};
} else {
// Fallback to attachments
const projectAttachment = parentTask.attachments.find(a =>
a.key.startsWith("project_") && a.type === "json"
);
if (projectAttachment?.value) {
const attachmentData = projectAttachment.value as any;
projectMetadata = {
description: attachmentData.description || "",
status: attachmentData.status || "unknown",
constraints: attachmentData.constraints || [],
requirements: attachmentData.requirements || [],
estimatedMinutes: attachmentData.estimatedMinutes,
createdAt: attachmentData.createdAt || parentTask.createdAt.toISOString(),
createdBy: attachmentData.createdBy || ctx.instanceId
};
}
}

// Get decomposition data for strategy and complexity
const decompositionAttachment = parentTask.attachments.find(a =>
a.key.startsWith("decomposition_") && a.type === "json"
);
if (decompositionAttachment?.value) {
const decomposition = decompositionAttachment.value as any;
projectMetadata.strategy = decomposition.strategy;
projectMetadata.totalComplexity = decomposition.totalComplexity;
}

// Calculate stats
const stats = {
totalTasks: actualSubtasks.length + 1, // Include parent
pendingTasks: actualSubtasks.filter(t => t.status === "pending").length,
inProgressTasks: actualSubtasks.filter(t => t.status === "in_progress").length,
completedTasks: actualSubtasks.filter(t => t.status === "completed").length,
failedTasks: actualSubtasks.filter(t => t.status === "failed").length
};

// Add parent task status to stats
if (parentTask.status === "pending") stats.pendingTasks++;
else if (parentTask.status === "in_progress") stats.inProgressTasks++;
else if (parentTask.status === "completed") stats.completedTasks++;
else if (parentTask.status === "failed") stats.failedTasks++;

// Format response
return {
projectId: projectId!,
parentTask: {
id: parentTask.id,
text: parentTask.text,
status: parentTask.status as any,
priority: parentTask.priority,
createdAt: parentTask.createdAt.toISOString(),
updatedAt: parentTask.updatedAt.toISOString(),
metadata: parentTask.metadata as any,
attachments: parentTask.attachments.map(a => ({
key: a.key,
type: a.type as any,
value: a.value,
createdAt: a.createdAt.toISOString()
}))
},
subtasks: actualSubtasks.map(task => {
const metadata = task.metadata as any || {};
return {
id: task.id,
text: task.text,
status: task.status as any,
priority: task.priority,
specialist: metadata.specialist,
complexity: metadata.complexity,
estimatedMinutes: metadata.estimatedMinutes,
dependencies: metadata.dependencies || [],
createdAt: task.createdAt.toISOString(),
updatedAt: task.updatedAt.toISOString(),
attachments: task.attachments.map(a => ({
key: a.key,
type: a.type as any,
value: a.value,
createdAt: a.createdAt.toISOString()
}))
};
}),
projectMetadata,
stats
};
}
}
69 changes: 68 additions & 1 deletion apps/server/src/schemas/task.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,71 @@ export const taskCreateProjectOutput = z.object({
});

export type TaskCreateProjectInput = z.infer<typeof taskCreateProjectInput>;
export type TaskCreateProjectOutput = z.infer<typeof taskCreateProjectOutput>;
export type TaskCreateProjectOutput = z.infer<typeof taskCreateProjectOutput>;

// task.get_project
export const taskGetProjectInput = z.object({
projectId: z.string().min(1).optional(),
taskId: z.string().min(1).optional(),
}).refine(data => data.projectId || data.taskId, {
message: "Either 'projectId' or 'taskId' must be provided",
path: ["projectId"],
});

export const taskGetProjectOutput = z.object({
projectId: z.string(),
parentTask: z.object({
id: z.string(),
text: z.string(),
status: TaskStatus,
priority: z.number(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
metadata: z.record(z.string(), z.unknown()).nullable(),
attachments: z.array(z.object({
key: z.string(),
type: AttachmentType,
value: z.unknown().optional(),
createdAt: z.string()
})).optional(),
}),
subtasks: z.array(z.object({
id: z.string(),
text: z.string(),
status: TaskStatus,
priority: z.number(),
specialist: z.string().optional(),
complexity: z.number().optional(),
estimatedMinutes: z.number().optional(),
dependencies: z.array(z.string()).optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
attachments: z.array(z.object({
key: z.string(),
type: AttachmentType,
value: z.unknown().optional(),
createdAt: z.string()
})).optional(),
})),
projectMetadata: z.object({
description: z.string(),
status: z.string(),
constraints: z.array(z.string()).optional(),
requirements: z.array(z.string()).optional(),
estimatedMinutes: z.number().optional(),
strategy: z.enum(["parallel", "sequential", "mixed"]).optional(),
totalComplexity: z.number().optional(),
createdAt: z.string().datetime(),
createdBy: z.string().optional(),
}),
stats: z.object({
totalTasks: z.number(),
pendingTasks: z.number(),
inProgressTasks: z.number(),
completedTasks: z.number(),
failedTasks: z.number(),
})
});

export type TaskGetProjectInput = z.infer<typeof taskGetProjectInput>;
export type TaskGetProjectOutput = z.infer<typeof taskGetProjectOutput>;
8 changes: 8 additions & 0 deletions apps/web/src/components/ClaudeBenchLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IconRobot,
IconBroadcast,
IconBook,
IconFolders,
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { ModeToggle } from "@/components/mode-toggle";
Expand Down Expand Up @@ -38,6 +39,13 @@ export function ClaudeBenchLayout({ children }: ClaudeBenchLayoutProps) {
<IconListDetails className="text-neutral-700 dark:text-neutral-200 h-5 w-5 shrink-0" />
),
},
{
label: "Projects",
href: "/projects",
icon: (
<IconFolders className="text-neutral-700 dark:text-neutral-200 h-5 w-5 shrink-0" />
),
},
{
label: "Events",
href: "/events",
Expand Down
Loading