mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-18 23:02:14 +00:00
198 lines
5.7 KiB
TypeScript
198 lines
5.7 KiB
TypeScript
// Project: pi-teams
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { runHook } from "./hooks";
|
|
import { withLock } from "./lock";
|
|
import type { TaskFile } from "./models";
|
|
import { sanitizeName, taskDir } from "./paths";
|
|
import { teamExists } from "./teams";
|
|
|
|
export function getTaskId(teamName: string): string {
|
|
const dir = taskDir(teamName);
|
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
const ids = files.map((f) => parseInt(path.parse(f).name, 10)).filter((id) => !Number.isNaN(id));
|
|
return ids.length > 0 ? (Math.max(...ids) + 1).toString() : "1";
|
|
}
|
|
|
|
function getTaskPath(teamName: string, taskId: string): string {
|
|
const dir = taskDir(teamName);
|
|
const safeTaskId = sanitizeName(taskId);
|
|
return path.join(dir, `${safeTaskId}.json`);
|
|
}
|
|
|
|
export async function createTask(
|
|
teamName: string,
|
|
subject: string,
|
|
description: string,
|
|
activeForm = "",
|
|
metadata?: Record<string, any>,
|
|
): Promise<TaskFile> {
|
|
if (!subject || !subject.trim()) throw new Error("Task subject must not be empty");
|
|
if (!teamExists(teamName)) throw new Error(`Team ${teamName} does not exist`);
|
|
|
|
const dir = taskDir(teamName);
|
|
const lockPath = dir;
|
|
|
|
return await withLock(lockPath, async () => {
|
|
const id = getTaskId(teamName);
|
|
const task: TaskFile = {
|
|
id,
|
|
subject,
|
|
description,
|
|
activeForm,
|
|
status: "pending",
|
|
blocks: [],
|
|
blockedBy: [],
|
|
metadata,
|
|
};
|
|
fs.writeFileSync(path.join(dir, `${id}.json`), JSON.stringify(task, null, 2));
|
|
return task;
|
|
});
|
|
}
|
|
|
|
export async function updateTask(
|
|
teamName: string,
|
|
taskId: string,
|
|
updates: Partial<TaskFile>,
|
|
retries?: number,
|
|
): Promise<TaskFile> {
|
|
const p = getTaskPath(teamName, taskId);
|
|
|
|
return await withLock(
|
|
p,
|
|
async () => {
|
|
if (!fs.existsSync(p)) throw new Error(`Task ${taskId} not found`);
|
|
const task: TaskFile = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
const updated = { ...task, ...updates };
|
|
|
|
if (updates.status === "deleted") {
|
|
fs.unlinkSync(p);
|
|
return updated;
|
|
}
|
|
|
|
fs.writeFileSync(p, JSON.stringify(updated, null, 2));
|
|
|
|
if (updates.status === "completed") {
|
|
await runHook(teamName, "task_completed", updated);
|
|
}
|
|
|
|
return updated;
|
|
},
|
|
retries,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Submits a plan for a task, updating its status to "planning".
|
|
* @param teamName The name of the team
|
|
* @param taskId The ID of the task
|
|
* @param plan The content of the plan
|
|
* @returns The updated task
|
|
*/
|
|
export async function submitPlan(teamName: string, taskId: string, plan: string): Promise<TaskFile> {
|
|
if (!plan || !plan.trim()) throw new Error("Plan must not be empty");
|
|
return await updateTask(teamName, taskId, { status: "planning", plan });
|
|
}
|
|
|
|
/**
|
|
* Evaluates a submitted plan for a task.
|
|
* @param teamName The name of the team
|
|
* @param taskId The ID of the task
|
|
* @param action The evaluation action: "approve" or "reject"
|
|
* @param feedback Optional feedback for the evaluation (required for rejection)
|
|
* @param retries Number of times to retry acquiring the lock
|
|
* @returns The updated task
|
|
*/
|
|
export async function evaluatePlan(
|
|
teamName: string,
|
|
taskId: string,
|
|
action: "approve" | "reject",
|
|
feedback?: string,
|
|
retries?: number,
|
|
): Promise<TaskFile> {
|
|
const p = getTaskPath(teamName, taskId);
|
|
|
|
return await withLock(
|
|
p,
|
|
async () => {
|
|
if (!fs.existsSync(p)) throw new Error(`Task ${taskId} not found`);
|
|
const task: TaskFile = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
|
|
// 1. Validate state: Only "planning" tasks can be evaluated
|
|
if (task.status !== "planning") {
|
|
throw new Error(
|
|
`Cannot evaluate plan for task ${taskId} because its status is '${task.status}'. ` +
|
|
`Tasks must be in 'planning' status to be evaluated.`,
|
|
);
|
|
}
|
|
|
|
// 2. Validate plan presence
|
|
if (!task.plan || !task.plan.trim()) {
|
|
throw new Error(`Cannot evaluate plan for task ${taskId} because no plan has been submitted.`);
|
|
}
|
|
|
|
// 3. Require feedback for rejections
|
|
if (action === "reject" && (!feedback || !feedback.trim())) {
|
|
throw new Error("Feedback is required when rejecting a plan.");
|
|
}
|
|
|
|
// 4. Perform update
|
|
const updates: Partial<TaskFile> =
|
|
action === "approve"
|
|
? { status: "in_progress", planFeedback: "" }
|
|
: { status: "planning", planFeedback: feedback };
|
|
|
|
const updated = { ...task, ...updates };
|
|
fs.writeFileSync(p, JSON.stringify(updated, null, 2));
|
|
return updated;
|
|
},
|
|
retries,
|
|
);
|
|
}
|
|
|
|
export async function readTask(teamName: string, taskId: string, retries?: number): Promise<TaskFile> {
|
|
const p = getTaskPath(teamName, taskId);
|
|
if (!fs.existsSync(p)) throw new Error(`Task ${taskId} not found`);
|
|
return await withLock(
|
|
p,
|
|
async () => {
|
|
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
},
|
|
retries,
|
|
);
|
|
}
|
|
|
|
export async function listTasks(teamName: string): Promise<TaskFile[]> {
|
|
const dir = taskDir(teamName);
|
|
return await withLock(dir, async () => {
|
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
const tasks: TaskFile[] = files
|
|
.map((f) => {
|
|
const id = parseInt(path.parse(f).name, 10);
|
|
if (Number.isNaN(id)) return null;
|
|
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
})
|
|
.filter((t) => t !== null);
|
|
return tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
|
|
});
|
|
}
|
|
|
|
export async function resetOwnerTasks(teamName: string, agentName: string) {
|
|
const dir = taskDir(teamName);
|
|
const lockPath = dir;
|
|
|
|
await withLock(lockPath, async () => {
|
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
for (const f of files) {
|
|
const p = path.join(dir, f);
|
|
const task: TaskFile = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
if (task.owner === agentName) {
|
|
task.owner = undefined;
|
|
if (task.status !== "completed") {
|
|
task.status = "pending";
|
|
}
|
|
fs.writeFileSync(p, JSON.stringify(task, null, 2));
|
|
}
|
|
}
|
|
});
|
|
}
|