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:
YeonGyu-Kim
2026-02-01 22:42:28 +09:00
committed by GitHub
parent 491df05b63
commit 8d29a1c5c7
32 changed files with 2070 additions and 684 deletions
+2
View File
@@ -0,0 +1,2 @@
export { createTask } from "./task"
export type { TaskObject, TaskStatus, TaskCreateInput, TaskListInput, TaskGetInput, TaskUpdateInput, TaskDeleteInput } from "./types"
+768
View File
@@ -0,0 +1,768 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { existsSync, rmSync, mkdirSync, writeFileSync, readdirSync } from "fs"
import { join } from "path"
import type { TaskObject } from "./types"
import { createTask } from "./task"
const TEST_STORAGE = ".test-task-tool"
const TEST_DIR = join(process.cwd(), TEST_STORAGE)
const TEST_CONFIG = {
new_task_system_enabled: true,
sisyphus: {
tasks: {
storage_path: TEST_STORAGE,
claude_code_compat: true,
},
},
}
const TEST_SESSION_ID = "test-session-123"
const TEST_ABORT_CONTROLLER = new AbortController()
const TEST_CONTEXT = {
sessionID: TEST_SESSION_ID,
messageID: "test-message-123",
agent: "test-agent",
abort: TEST_ABORT_CONTROLLER.signal,
}
describe("task_tool", () => {
let taskTool: ReturnType<typeof createTask>
beforeEach(() => {
if (existsSync(TEST_STORAGE)) {
rmSync(TEST_STORAGE, { recursive: true, force: true })
}
mkdirSync(TEST_DIR, { recursive: true })
taskTool = createTask(TEST_CONFIG)
})
async function createTestTask(title: string, overrides: Partial<Parameters<typeof taskTool.execute>[0]> = {}): Promise<string> {
const args = {
action: "create" as const,
title,
...overrides,
}
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
return (result as { task: TaskObject }).task.id
}
afterEach(() => {
if (existsSync(TEST_STORAGE)) {
rmSync(TEST_STORAGE, { recursive: true, force: true })
}
})
// ============================================================================
// CREATE ACTION TESTS
// ============================================================================
describe("create action", () => {
test("creates task with required title field", async () => {
//#given
const args = {
action: "create" as const,
title: "Implement authentication",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("task")
expect(result.task).toHaveProperty("id")
expect(result.task.title).toBe("Implement authentication")
expect(result.task.status).toBe("open")
})
test("auto-generates T-{uuid} format ID", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.id).toMatch(/^T-[a-f0-9-]+$/)
})
test("auto-records threadID from session context", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task).toHaveProperty("threadID")
expect(typeof result.task.threadID).toBe("string")
})
test("sets status to open by default", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.status).toBe("open")
})
test("stores optional description field", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
description: "Detailed description of the task",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.description).toBe("Detailed description of the task")
})
test("stores dependsOn array", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
dependsOn: ["T-dep1", "T-dep2"],
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.dependsOn).toEqual(["T-dep1", "T-dep2"])
})
test("stores parentID when provided", async () => {
//#given
const args = {
action: "create" as const,
title: "Subtask",
parentID: "T-parent123",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.parentID).toBe("T-parent123")
})
test("stores repoURL when provided", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
repoURL: "https://github.com/code-yeongyu/oh-my-opencode",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.repoURL).toBe("https://github.com/code-yeongyu/oh-my-opencode")
})
test("returns result as JSON string with task property", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
//#then
expect(typeof resultStr).toBe("string")
const result = JSON.parse(resultStr)
expect(result).toHaveProperty("task")
})
test("initializes dependsOn as empty array when not provided", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.dependsOn).toEqual([])
})
})
// ============================================================================
// LIST ACTION TESTS
// ============================================================================
describe("list action", () => {
test("returns all non-completed tasks by default", async () => {
//#given
const args = {
action: "list" as const,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("tasks")
expect(Array.isArray(result.tasks)).toBe(true)
})
test("excludes completed tasks from list", async () => {
//#given
const args = {
action: "list" as const,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
const completedTasks = result.tasks.filter((t: TaskObject) => t.status === "completed")
expect(completedTasks.length).toBe(0)
})
test("applies ready filter when requested", async () => {
//#given
const args = {
action: "list" as const,
ready: true,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("tasks")
expect(Array.isArray(result.tasks)).toBe(true)
})
test("respects limit parameter", async () => {
//#given
const args = {
action: "list" as const,
limit: 5,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.tasks.length).toBeLessThanOrEqual(5)
})
test("returns result as JSON string with tasks array", async () => {
//#given
const args = {
action: "list" as const,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
//#then
expect(typeof resultStr).toBe("string")
const result = JSON.parse(resultStr)
expect(Array.isArray(result.tasks)).toBe(true)
})
test("filters by status when provided", async () => {
//#given
const args = {
action: "list" as const,
status: "in_progress" as const,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
const allInProgress = result.tasks.every((t: TaskObject) => t.status === "in_progress")
expect(allInProgress).toBe(true)
})
})
// ============================================================================
// GET ACTION TESTS
// ============================================================================
describe("get action", () => {
test("returns task by ID", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "get" as const,
id: testId,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("task")
})
test("returns null for non-existent task", async () => {
//#given
const args = {
action: "get" as const,
id: "T-nonexistent",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task).toBeNull()
})
test("returns result as JSON string with task property", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "get" as const,
id: testId,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
//#then
expect(typeof resultStr).toBe("string")
const result = JSON.parse(resultStr)
expect(result).toHaveProperty("task")
})
test("returns complete task object with all fields", async () => {
//#given
const args = {
action: "get" as const,
id: "T-test123",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
if (result.task !== null) {
expect(result.task).toHaveProperty("id")
expect(result.task).toHaveProperty("title")
expect(result.task).toHaveProperty("status")
expect(result.task).toHaveProperty("threadID")
}
})
})
// ============================================================================
// UPDATE ACTION TESTS
// ============================================================================
describe("update action", () => {
test("updates task title", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "update" as const,
id: testId,
title: "Updated title",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("task")
expect(result.task.title).toBe("Updated title")
})
test("updates task description", async () => {
//#given
const testId = await createTestTask("Test task", { description: "Initial description" })
const args = {
action: "update" as const,
id: testId,
description: "Updated description",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.description).toBe("Updated description")
})
test("updates task status", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "update" as const,
id: testId,
status: "in_progress" as const,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.status).toBe("in_progress")
})
test("updates dependsOn array", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "update" as const,
id: testId,
dependsOn: ["T-dep1", "T-dep2", "T-dep3"],
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.dependsOn).toEqual(["T-dep1", "T-dep2", "T-dep3"])
})
test("returns error for non-existent task", async () => {
//#given
const args = {
action: "update" as const,
id: "T-nonexistent",
title: "New title",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("error")
expect(result.error).toBe("task_not_found")
})
test("returns result as JSON string with task property", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "update" as const,
id: testId,
title: "Updated",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
//#then
expect(typeof resultStr).toBe("string")
const result = JSON.parse(resultStr)
expect(result).toHaveProperty("task")
})
test("updates multiple fields at once", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "update" as const,
id: testId,
title: "New title",
description: "New description",
status: "completed" as const,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task.title).toBe("New title")
expect(result.task.description).toBe("New description")
expect(result.task.status).toBe("completed")
})
})
// ============================================================================
// DELETE ACTION TESTS
// ============================================================================
describe("delete action", () => {
test("removes task file physically", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "delete" as const,
id: testId,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("success")
expect(result.success).toBe(true)
})
test("returns success true on successful deletion", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "delete" as const,
id: testId,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.success).toBe(true)
})
test("returns error for non-existent task", async () => {
//#given
const args = {
action: "delete" as const,
id: "T-nonexistent",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("error")
expect(result.error).toBe("task_not_found")
})
test("returns result as JSON string", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "delete" as const,
id: testId,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
//#then
expect(typeof resultStr).toBe("string")
const result = JSON.parse(resultStr)
expect(result).toHaveProperty("success")
})
})
// ============================================================================
// EDGE CASE TESTS
// ============================================================================
describe("edge cases", () => {
test("detects circular dependency (A depends on B, B depends on A)", async () => {
//#given
const args = {
action: "create" as const,
title: "Task A",
dependsOn: ["T-taskB"],
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
// Should either prevent creation or mark as circular
expect(result).toHaveProperty("task")
})
test("handles task depending on non-existent ID", async () => {
//#given
const args = {
action: "create" as const,
title: "Task with missing dependency",
dependsOn: ["T-nonexistent"],
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
// Should either allow or return error
expect(result).toHaveProperty("task")
})
test("ready filter returns true for empty dependsOn", async () => {
//#given
const args = {
action: "list" as const,
ready: true,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
const tasksWithNoDeps = result.tasks.filter((t: TaskObject) => t.dependsOn.length === 0)
expect(tasksWithNoDeps.length).toBeGreaterThanOrEqual(0)
})
test("ready filter includes tasks with all completed dependencies", async () => {
//#given
const args = {
action: "list" as const,
ready: true,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(Array.isArray(result.tasks)).toBe(true)
})
test("ready filter excludes tasks with incomplete dependencies", async () => {
//#given
const args = {
action: "list" as const,
ready: true,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(Array.isArray(result.tasks)).toBe(true)
})
test("handles empty title gracefully", async () => {
//#given
const args = {
action: "create" as const,
title: "",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
// Should either reject or handle empty title
expect(result).toBeDefined()
})
test("handles very long title", async () => {
//#given
const longTitle = "A".repeat(1000)
const args = {
action: "create" as const,
title: longTitle,
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toBeDefined()
})
test("handles special characters in title", async () => {
//#given
const args = {
action: "create" as const,
title: "Task with special chars: !@#$%^&*()",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toBeDefined()
})
test("handles unicode characters in title", async () => {
//#given
const args = {
action: "create" as const,
title: "任務 🚀 Tâche",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toBeDefined()
})
test("preserves all TaskObject fields in round-trip", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
description: "Test description",
dependsOn: ["T-dep1"],
parentID: "T-parent",
repoURL: "https://example.com",
}
//#when
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result.task).toHaveProperty("id")
expect(result.task).toHaveProperty("title")
expect(result.task).toHaveProperty("description")
expect(result.task).toHaveProperty("status")
expect(result.task).toHaveProperty("dependsOn")
expect(result.task).toHaveProperty("parentID")
expect(result.task).toHaveProperty("repoURL")
expect(result.task).toHaveProperty("threadID")
})
})
})
+253
View File
@@ -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()
}
}
+61
View File
@@ -0,0 +1,61 @@
import { z } from "zod"
export const TaskStatusSchema = z.enum(["open", "in_progress", "completed"])
export type TaskStatus = z.infer<typeof TaskStatusSchema>
export const TaskObjectSchema = z
.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
status: TaskStatusSchema,
dependsOn: z.array(z.string()).default([]),
repoURL: z.string().optional(),
parentID: z.string().optional(),
threadID: z.string(),
})
.strict()
export type TaskObject = z.infer<typeof TaskObjectSchema>
// Action input schemas
export const TaskCreateInputSchema = z.object({
title: z.string(),
description: z.string().optional(),
dependsOn: z.array(z.string()).optional(),
repoURL: z.string().optional(),
parentID: z.string().optional(),
})
export type TaskCreateInput = z.infer<typeof TaskCreateInputSchema>
export const TaskListInputSchema = z.object({
status: TaskStatusSchema.optional(),
parentID: z.string().optional(),
})
export type TaskListInput = z.infer<typeof TaskListInputSchema>
export const TaskGetInputSchema = z.object({
id: z.string(),
})
export type TaskGetInput = z.infer<typeof TaskGetInputSchema>
export const TaskUpdateInputSchema = z.object({
id: z.string(),
title: z.string().optional(),
description: z.string().optional(),
status: TaskStatusSchema.optional(),
dependsOn: z.array(z.string()).optional(),
repoURL: z.string().optional(),
parentID: z.string().optional(),
})
export type TaskUpdateInput = z.infer<typeof TaskUpdateInputSchema>
export const TaskDeleteInputSchema = z.object({
id: z.string(),
})
export type TaskDeleteInput = z.infer<typeof TaskDeleteInputSchema>