Implement unified Claude Tasks system with single multi-action tool (#1356)
* chore: pin bun-types to 1.3.6 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * chore: exclude test files and script from tsconfig 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor: remove sisyphus-swarm feature Remove mailbox types and swarm config schema. Update docs. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor: remove legacy sisyphus-tasks feature Remove old storage and types implementation, replaced by claude-tasks. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(claude-tasks): add task schema and storage utilities - Task schema with Zod validation (pending, in_progress, completed, deleted) - Storage utilities: getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock - Atomic writes with temp file + rename - File-based locking with 30s stale threshold 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools/task): add task object schemas Add Zod schemas for task CRUD operations input validation. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskCreate tool Create new tasks with sequential ID generation and lock-based concurrency. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskGet tool Retrieve task by ID with null-safe handling. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskUpdate tool with claim validation Update tasks with status transitions and owner claim validation. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskList tool and exports - TaskList for summary view of all tasks - Export all claude-tasks tool factories from index 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(hooks): add task-reminder hook Remind agents to use task tools after 10 turns without task operations. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(config): add disabled_tools setting and tasks-todowrite-disabler hook - Add disabled_tools config option to disable specific tools by name - Register tasks-todowrite-disabler hook name in schema 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(config-handler): add task_* and teammate tool permissions Grant task_* and teammate permissions to atlas, sisyphus, prometheus, and sisyphus-junior agents. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(delegate-task): add execute option for task execution Add optional execute field with task_id and task_dir for task-based delegation. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * fix(truncator): add type guard for non-string outputs Prevent crashes when output is not a string by adding typeof checks. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * chore: export config types and update task-resume-info - Export SisyphusConfig and SisyphusTasksConfig types - Add task_tool to TARGET_TOOLS list 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor(storage): remove team namespace, use flat task directory * feat(task): implement unified task tool with all 5 actions * fix(hooks): update task-reminder to track unified task tool * refactor(tools): register unified task tool, remove 4 separate tools * chore(cleanup): remove old 4-tool task implementation * refactor(config): use new_task_system_enabled as top-level flag - Add new_task_system_enabled to OhMyOpenCodeConfigSchema - Remove enabled from SisyphusTasksConfigSchema (keep storage_path, claude_code_compat) - Update index.ts to gate on new_task_system_enabled - Update plugin-config.ts default for config initialization - Update test configs in task.test.ts and storage.test.ts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: resolve typecheck and test failures - Add explicit ToolDefinition return type to createTask function - Fix planDemoteConfig to use 'subagent' mode instead of 'all' --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { existsSync, readdirSync, unlinkSync } from "fs"
|
||||
import { join } from "path"
|
||||
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||
import type {
|
||||
TaskObject,
|
||||
TaskCreateInput,
|
||||
TaskListInput,
|
||||
TaskGetInput,
|
||||
TaskUpdateInput,
|
||||
TaskDeleteInput,
|
||||
} from "./types"
|
||||
import {
|
||||
TaskObjectSchema,
|
||||
TaskCreateInputSchema,
|
||||
TaskListInputSchema,
|
||||
TaskGetInputSchema,
|
||||
TaskUpdateInputSchema,
|
||||
TaskDeleteInputSchema,
|
||||
} from "./types"
|
||||
import {
|
||||
getTaskDir,
|
||||
readJsonSafe,
|
||||
writeJsonAtomic,
|
||||
acquireLock,
|
||||
generateTaskId,
|
||||
listTaskFiles,
|
||||
} from "../../features/claude-tasks/storage"
|
||||
|
||||
export function createTask(config: Partial<OhMyOpenCodeConfig>): ToolDefinition {
|
||||
return tool({
|
||||
description: `Unified task management tool with create, list, get, update, delete actions.
|
||||
|
||||
**CREATE**: Create a new task. Auto-generates T-{uuid} ID, records threadID, sets status to "open".
|
||||
**LIST**: List tasks. Excludes completed by default. Supports ready filter (all dependencies completed) and limit.
|
||||
**GET**: Retrieve a task by ID.
|
||||
**UPDATE**: Update task fields. Requires task ID.
|
||||
**DELETE**: Physically remove task file.
|
||||
|
||||
All actions return JSON strings.`,
|
||||
args: {
|
||||
action: tool.schema
|
||||
.enum(["create", "list", "get", "update", "delete"])
|
||||
.describe("Action to perform: create, list, get, update, delete"),
|
||||
title: tool.schema.string().optional().describe("Task title (required for create)"),
|
||||
description: tool.schema.string().optional().describe("Task description"),
|
||||
status: tool.schema
|
||||
.enum(["open", "in_progress", "completed"])
|
||||
.optional()
|
||||
.describe("Task status"),
|
||||
dependsOn: tool.schema
|
||||
.array(tool.schema.string())
|
||||
.optional()
|
||||
.describe("Task IDs this task depends on"),
|
||||
repoURL: tool.schema.string().optional().describe("Repository URL"),
|
||||
parentID: tool.schema.string().optional().describe("Parent task ID"),
|
||||
id: tool.schema.string().optional().describe("Task ID (required for get, update, delete)"),
|
||||
ready: tool.schema.boolean().optional().describe("Filter to tasks with all dependencies completed"),
|
||||
limit: tool.schema.number().optional().describe("Maximum number of tasks to return"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
const action = args.action as "create" | "list" | "get" | "update" | "delete"
|
||||
|
||||
switch (action) {
|
||||
case "create":
|
||||
return handleCreate(args, config, context)
|
||||
case "list":
|
||||
return handleList(args, config)
|
||||
case "get":
|
||||
return handleGet(args, config)
|
||||
case "update":
|
||||
return handleUpdate(args, config)
|
||||
case "delete":
|
||||
return handleDelete(args, config)
|
||||
default:
|
||||
return JSON.stringify({ error: "invalid_action" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCreate(
|
||||
args: Record<string, unknown>,
|
||||
config: Partial<OhMyOpenCodeConfig>,
|
||||
context: { sessionID: string }
|
||||
): Promise<string> {
|
||||
const validatedArgs = TaskCreateInputSchema.parse(args)
|
||||
const taskDir = getTaskDir(config)
|
||||
const lock = acquireLock(taskDir)
|
||||
|
||||
try {
|
||||
const taskId = generateTaskId()
|
||||
const task: TaskObject = {
|
||||
id: taskId,
|
||||
title: validatedArgs.title,
|
||||
description: validatedArgs.description,
|
||||
status: "open",
|
||||
dependsOn: validatedArgs.dependsOn ?? [],
|
||||
repoURL: validatedArgs.repoURL,
|
||||
parentID: validatedArgs.parentID,
|
||||
threadID: context.sessionID,
|
||||
}
|
||||
|
||||
const validatedTask = TaskObjectSchema.parse(task)
|
||||
writeJsonAtomic(join(taskDir, `${taskId}.json`), validatedTask)
|
||||
|
||||
return JSON.stringify({ task: validatedTask })
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleList(
|
||||
args: Record<string, unknown>,
|
||||
config: Partial<OhMyOpenCodeConfig>
|
||||
): Promise<string> {
|
||||
const validatedArgs = TaskListInputSchema.parse(args)
|
||||
const taskDir = getTaskDir(config)
|
||||
|
||||
if (!existsSync(taskDir)) {
|
||||
return JSON.stringify({ tasks: [] })
|
||||
}
|
||||
|
||||
const files = listTaskFiles(config)
|
||||
if (files.length === 0) {
|
||||
return JSON.stringify({ tasks: [] })
|
||||
}
|
||||
|
||||
const allTasks: TaskObject[] = []
|
||||
for (const fileId of files) {
|
||||
const task = readJsonSafe(join(taskDir, `${fileId}.json`), TaskObjectSchema)
|
||||
if (task) {
|
||||
allTasks.push(task)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out completed tasks by default
|
||||
let tasks = allTasks.filter((task) => task.status !== "completed")
|
||||
|
||||
// Apply status filter if provided
|
||||
if (validatedArgs.status) {
|
||||
tasks = tasks.filter((task) => task.status === validatedArgs.status)
|
||||
}
|
||||
|
||||
// Apply parentID filter if provided
|
||||
if (validatedArgs.parentID) {
|
||||
tasks = tasks.filter((task) => task.parentID === validatedArgs.parentID)
|
||||
}
|
||||
|
||||
// Apply ready filter if requested
|
||||
if (args.ready) {
|
||||
tasks = tasks.filter((task) => {
|
||||
if (task.dependsOn.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// All dependencies must be completed
|
||||
return task.dependsOn.every((depId) => {
|
||||
const depTask = allTasks.find((t) => t.id === depId)
|
||||
return depTask?.status === "completed"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Apply limit if provided
|
||||
const limit = args.limit as number | undefined
|
||||
if (limit !== undefined && limit > 0) {
|
||||
tasks = tasks.slice(0, limit)
|
||||
}
|
||||
|
||||
return JSON.stringify({ tasks })
|
||||
}
|
||||
|
||||
async function handleGet(
|
||||
args: Record<string, unknown>,
|
||||
config: Partial<OhMyOpenCodeConfig>
|
||||
): Promise<string> {
|
||||
const validatedArgs = TaskGetInputSchema.parse(args)
|
||||
const taskDir = getTaskDir(config)
|
||||
const taskPath = join(taskDir, `${validatedArgs.id}.json`)
|
||||
|
||||
const task = readJsonSafe(taskPath, TaskObjectSchema)
|
||||
|
||||
return JSON.stringify({ task: task ?? null })
|
||||
}
|
||||
|
||||
async function handleUpdate(
|
||||
args: Record<string, unknown>,
|
||||
config: Partial<OhMyOpenCodeConfig>
|
||||
): Promise<string> {
|
||||
const validatedArgs = TaskUpdateInputSchema.parse(args)
|
||||
const taskDir = getTaskDir(config)
|
||||
const lock = acquireLock(taskDir)
|
||||
|
||||
try {
|
||||
const taskPath = join(taskDir, `${validatedArgs.id}.json`)
|
||||
const task = readJsonSafe(taskPath, TaskObjectSchema)
|
||||
|
||||
if (!task) {
|
||||
return JSON.stringify({ error: "task_not_found" })
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if (validatedArgs.title !== undefined) {
|
||||
task.title = validatedArgs.title
|
||||
}
|
||||
if (validatedArgs.description !== undefined) {
|
||||
task.description = validatedArgs.description
|
||||
}
|
||||
if (validatedArgs.status !== undefined) {
|
||||
task.status = validatedArgs.status
|
||||
}
|
||||
if (validatedArgs.dependsOn !== undefined) {
|
||||
task.dependsOn = validatedArgs.dependsOn
|
||||
}
|
||||
if (validatedArgs.repoURL !== undefined) {
|
||||
task.repoURL = validatedArgs.repoURL
|
||||
}
|
||||
if (validatedArgs.parentID !== undefined) {
|
||||
task.parentID = validatedArgs.parentID
|
||||
}
|
||||
|
||||
const validatedTask = TaskObjectSchema.parse(task)
|
||||
writeJsonAtomic(taskPath, validatedTask)
|
||||
|
||||
return JSON.stringify({ task: validatedTask })
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(
|
||||
args: Record<string, unknown>,
|
||||
config: Partial<OhMyOpenCodeConfig>
|
||||
): Promise<string> {
|
||||
const validatedArgs = TaskDeleteInputSchema.parse(args)
|
||||
const taskDir = getTaskDir(config)
|
||||
const lock = acquireLock(taskDir)
|
||||
|
||||
try {
|
||||
const taskPath = join(taskDir, `${validatedArgs.id}.json`)
|
||||
|
||||
if (!existsSync(taskPath)) {
|
||||
return JSON.stringify({ error: "task_not_found" })
|
||||
}
|
||||
|
||||
unlinkSync(taskPath)
|
||||
|
||||
return JSON.stringify({ success: true })
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user