oh-my-opencode/src/tools/agent-teams/team-task-store.ts
2026-02-14 13:33:29 +09:00

158 lines
4.1 KiB
TypeScript

import { existsSync, readdirSync, unlinkSync } from "node:fs"
import { join } from "node:path"
import {
acquireLock,
ensureDir,
generateTaskId,
readJsonSafe,
writeJsonAtomic,
} from "../../features/claude-tasks/storage"
import { getTeamTaskDir, getTeamTaskPath } from "./paths"
import { TeamTask, TeamTaskSchema } from "./types"
import { validateTaskId, validateTeamName } from "./name-validation"
function assertValidTeamName(teamName: string): void {
const validationError = validateTeamName(teamName)
if (validationError) {
throw new Error(validationError)
}
}
function assertValidTaskId(taskId: string): void {
const validationError = validateTaskId(taskId)
if (validationError) {
throw new Error(validationError)
}
}
function withTaskLock<T>(teamName: string, operation: () => T): T {
assertValidTeamName(teamName)
const taskDir = getTeamTaskDir(teamName)
ensureDir(taskDir)
const lock = acquireLock(taskDir)
if (!lock.acquired) {
throw new Error("team_task_lock_unavailable")
}
try {
return operation()
} finally {
lock.release()
}
}
export function readTeamTask(teamName: string, taskId: string): TeamTask | null {
assertValidTeamName(teamName)
assertValidTaskId(taskId)
return readJsonSafe(getTeamTaskPath(teamName, taskId), TeamTaskSchema)
}
export function readTeamTaskOrThrow(teamName: string, taskId: string): TeamTask {
const task = readTeamTask(teamName, taskId)
if (!task) {
throw new Error("team_task_not_found")
}
return task
}
export function listTeamTasks(teamName: string): TeamTask[] {
assertValidTeamName(teamName)
const taskDir = getTeamTaskDir(teamName)
if (!existsSync(taskDir)) {
return []
}
const files = readdirSync(taskDir)
.filter((file) => file.endsWith(".json") && file.startsWith("T-"))
.sort((a, b) => a.localeCompare(b))
const tasks: TeamTask[] = []
for (const file of files) {
const taskId = file.replace(/\.json$/, "")
if (validateTaskId(taskId)) {
continue
}
const task = readTeamTask(teamName, taskId)
if (task) {
tasks.push(task)
}
}
return tasks
}
export function createTeamTask(
teamName: string,
subject: string,
description: string,
activeForm?: string,
metadata?: Record<string, unknown>,
): TeamTask {
assertValidTeamName(teamName)
if (!subject.trim()) {
throw new Error("team_task_subject_required")
}
return withTaskLock(teamName, () => {
const task: TeamTask = {
id: generateTaskId(),
subject,
description,
activeForm,
status: "pending",
blocks: [],
blockedBy: [],
...(metadata ? { metadata } : {}),
}
const validated = TeamTaskSchema.parse(task)
writeJsonAtomic(getTeamTaskPath(teamName, validated.id), validated)
return validated
})
}
export function writeTeamTask(teamName: string, task: TeamTask): TeamTask {
assertValidTeamName(teamName)
assertValidTaskId(task.id)
const validated = TeamTaskSchema.parse(task)
writeJsonAtomic(getTeamTaskPath(teamName, validated.id), validated)
return validated
}
export function deleteTeamTaskFile(teamName: string, taskId: string): void {
assertValidTeamName(teamName)
assertValidTaskId(taskId)
const taskPath = getTeamTaskPath(teamName, taskId)
if (existsSync(taskPath)) {
unlinkSync(taskPath)
}
}
export function readTaskFromDirectory(taskDir: string, taskId: string): TeamTask | null {
assertValidTaskId(taskId)
return readJsonSafe(join(taskDir, `${taskId}.json`), TeamTaskSchema)
}
export function resetOwnerTasks(teamName: string, ownerName: string): void {
assertValidTeamName(teamName)
withTaskLock(teamName, () => {
const tasks = listTeamTasks(teamName)
for (const task of tasks) {
if (task.owner !== ownerName) {
continue
}
const next: TeamTask = {
...task,
owner: undefined,
status: task.status === "completed" ? "completed" : "pending",
}
writeTeamTask(teamName, next)
}
})
}
export function withTeamTaskLock<T>(teamName: string, operation: () => T): T {
assertValidTeamName(teamName)
return withTaskLock(teamName, operation)
}