feat(task): refactor to Claude Code style individual tools

- Split unified Task tool into individual tools (TaskCreate, TaskGet, TaskList, TaskUpdate)
- Update schema to Claude Code field names (subject, blockedBy, blocks, activeForm, owner, metadata)
- Add OpenCode Todo API sync layer (todo-sync.ts)
- Implement Todo sync on task create/update for continuation enforcement
- Add comprehensive tests for all tools (96 tests total)
- Update AGENTS.md documentation

Breaking Changes:
- Field names changed: title→subject, dependsOn→blockedBy, open→pending
- Tool names changed: task→task_create, task_get, task_list, task_update

Closes: todo-continuation-enforcer now sees Task-created items
This commit is contained in:
YeonGyu-Kim
2026-02-02 12:01:43 +09:00
parent 6288251a67
commit 92639ca38f
16 changed files with 2224 additions and 64 deletions
+97
View File
@@ -0,0 +1,97 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import { join } from "path"
import type { OhMyOpenCodeConfig } from "../../config/schema"
import type { TaskObject } from "./types"
import { TaskObjectSchema, TaskCreateInputSchema } from "./types"
import {
getTaskDir,
writeJsonAtomic,
acquireLock,
generateTaskId,
} from "../../features/claude-tasks/storage"
import { syncTaskToTodo } from "./todo-sync"
export function createTaskCreateTool(
config: Partial<OhMyOpenCodeConfig>
): ToolDefinition {
return tool({
description: `Create a new task with auto-generated ID and threadID recording.
Auto-generates T-{uuid} ID, records threadID from context, sets status to "pending".
Returns minimal response with task ID and subject.`,
args: {
subject: tool.schema.string().describe("Task subject (required)"),
description: tool.schema.string().optional().describe("Task description"),
activeForm: tool.schema.string().optional().describe("Active form (present continuous)"),
metadata: tool.schema
.record(tool.schema.string(), tool.schema.unknown())
.optional()
.describe("Task metadata"),
blockedBy: tool.schema
.array(tool.schema.string())
.optional()
.describe("Task IDs blocking this task"),
blocks: tool.schema
.array(tool.schema.string())
.optional()
.describe("Task IDs this task blocks"),
repoURL: tool.schema.string().optional().describe("Repository URL"),
parentID: tool.schema.string().optional().describe("Parent task ID"),
},
execute: async (args, context) => {
return handleCreate(args, config, context)
},
})
}
async function handleCreate(
args: Record<string, unknown>,
config: Partial<OhMyOpenCodeConfig>,
context: { sessionID: string }
): Promise<string> {
try {
const validatedArgs = TaskCreateInputSchema.parse(args)
const taskDir = getTaskDir(config)
const lock = acquireLock(taskDir)
if (!lock.acquired) {
return JSON.stringify({ error: "task_lock_unavailable" })
}
try {
const taskId = generateTaskId()
const task: TaskObject = {
id: taskId,
subject: validatedArgs.subject,
description: validatedArgs.description ?? "",
status: "pending",
blocks: validatedArgs.blocks ?? [],
blockedBy: validatedArgs.blockedBy ?? [],
activeForm: validatedArgs.activeForm,
metadata: validatedArgs.metadata,
repoURL: validatedArgs.repoURL,
parentID: validatedArgs.parentID,
threadID: context.sessionID,
}
const validatedTask = TaskObjectSchema.parse(task)
writeJsonAtomic(join(taskDir, `${taskId}.json`), validatedTask)
syncTaskToTodo(validatedTask)
return JSON.stringify({
task: {
id: validatedTask.id,
subject: validatedTask.subject,
},
})
} finally {
lock.release()
}
} catch (error) {
if (error instanceof Error && error.message.includes("Required")) {
return JSON.stringify({ error: "validation_error", message: error.message })
}
return JSON.stringify({ error: "internal_error" })
}
}