feat(task-system): add experimental task system with Claude Code spec alignment (#1415)

* feat(hooks): add tasks-todowrite-disabler hook to block TodoRead/TodoWrite

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(task-tools): add parallel execution guidance to descriptions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(index): migrate task system to experimental.task_system flag

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: update AGENTS.md for experimental task system

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(task-tests): align test field names with Claude Code spec (subject, blockedBy, addBlockedBy)

* fix: address Cubic review feedback

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: add optional chaining for tasksTodowriteDisabler null check

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-02-03 12:11:23 +09:00
committed by GitHub
parent 1e587c55dc
commit dea13a37a6
15 changed files with 316 additions and 75 deletions
+11 -4
View File
@@ -16,12 +16,19 @@ export function createTaskCreateTool(
config: Partial<OhMyOpenCodeConfig>,
ctx?: PluginInput,
): ToolDefinition {
return tool({
description: `Create a new task with auto-generated ID and threadID recording.
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: {
Returns minimal response with task ID and subject.
**IMPORTANT - Dependency Planning for Parallel Execution:**
Use \`blockedBy\` to specify task IDs that must complete before this task can start.
Calculate dependencies carefully to maximize parallel execution:
- Tasks with no dependencies can run simultaneously
- Only block a task if it truly depends on another's output
- Minimize dependency chains to reduce sequential bottlenecks`,
args: {
subject: tool.schema.string().describe("Task subject (required)"),
description: tool.schema.string().optional().describe("Task description"),
activeForm: tool.schema
+4 -1
View File
@@ -70,7 +70,10 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
}
})
return JSON.stringify({ tasks: summaries })
return JSON.stringify({
tasks: summaries,
reminder: "1 task = 1 delegate_task. Maximize parallel execution by running independent tasks (tasks with empty blockedBy) concurrently."
})
},
})
}
+8 -4
View File
@@ -23,14 +23,18 @@ export function createTaskUpdateTool(
config: Partial<OhMyOpenCodeConfig>,
ctx?: PluginInput,
): ToolDefinition {
return tool({
description: `Update an existing task with new values.
return tool({
description: `Update an existing task with new values.
Supports updating: subject, description, status, activeForm, owner, metadata.
For blocks/blockedBy: use addBlocks/addBlockedBy to append (additive, not replacement).
For metadata: merge with existing, set key to null to delete.
Syncs to OpenCode Todo API after update.`,
args: {
Syncs to OpenCode Todo API after update.
**IMPORTANT - Dependency Management:**
Use \`addBlockedBy\` to declare dependencies on other tasks.
Properly managed dependencies enable maximum parallel execution.`,
args: {
id: tool.schema.string().describe("Task ID (required)"),
subject: tool.schema.string().optional().describe("Task subject"),
description: tool.schema.string().optional().describe("Task description"),
+45 -45
View File
@@ -7,7 +7,7 @@ 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,
experimental: { task_system: true },
sisyphus: {
tasks: {
storage_path: TEST_STORAGE,
@@ -35,10 +35,10 @@ describe("task_tool", () => {
taskTool = createTask(TEST_CONFIG)
})
async function createTestTask(title: string, overrides: Partial<Parameters<typeof taskTool.execute>[0]> = {}): Promise<string> {
async function createTestTask(subject: string, overrides: Partial<Parameters<typeof taskTool.execute>[0]> = {}): Promise<string> {
const args = {
action: "create" as const,
title,
subject,
...overrides,
}
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
@@ -61,7 +61,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Implement authentication",
subject: "Implement authentication",
}
//#when
@@ -71,15 +71,15 @@ describe("task_tool", () => {
//#then
expect(result).toHaveProperty("task")
expect(result.task).toHaveProperty("id")
expect(result.task.title).toBe("Implement authentication")
expect(result.task.status).toBe("open")
expect(result.task.subject).toBe("Implement authentication")
expect(result.task.status).toBe("pending")
})
test("auto-generates T-{uuid} format ID", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
}
//#when
@@ -94,7 +94,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
}
//#when
@@ -110,7 +110,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
}
//#when
@@ -118,14 +118,14 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr)
//#then
expect(result.task.status).toBe("open")
expect(result.task.status).toBe("pending")
})
test("stores optional description field", async () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
description: "Detailed description of the task",
}
@@ -141,8 +141,8 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
dependsOn: ["T-dep1", "T-dep2"],
subject: "Test task",
blockedBy: ["T-dep1", "T-dep2"],
}
//#when
@@ -150,14 +150,14 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr)
//#then
expect(result.task.dependsOn).toEqual(["T-dep1", "T-dep2"])
expect(result.task.blockedBy).toEqual(["T-dep1", "T-dep2"])
})
test("stores parentID when provided", async () => {
//#given
const args = {
action: "create" as const,
title: "Subtask",
subject: "Subtask",
parentID: "T-parent123",
}
@@ -173,7 +173,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
repoURL: "https://github.com/code-yeongyu/oh-my-opencode",
}
@@ -189,7 +189,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
}
//#when
@@ -205,7 +205,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
}
//#when
@@ -213,7 +213,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr)
//#then
expect(result.task.dependsOn).toEqual([])
expect(result.task.blockedBy).toEqual([])
})
})
@@ -398,7 +398,7 @@ describe("task_tool", () => {
//#then
if (result.task !== null) {
expect(result.task).toHaveProperty("id")
expect(result.task).toHaveProperty("title")
expect(result.task).toHaveProperty("subject")
expect(result.task).toHaveProperty("status")
expect(result.task).toHaveProperty("threadID")
}
@@ -416,7 +416,7 @@ describe("task_tool", () => {
const args = {
action: "update" as const,
id: testId,
title: "Updated title",
subject: "Updated subject",
}
//#when
@@ -425,7 +425,7 @@ describe("task_tool", () => {
//#then
expect(result).toHaveProperty("task")
expect(result.task.title).toBe("Updated title")
expect(result.task.subject).toBe("Updated subject")
})
test("updates task description", async () => {
@@ -462,13 +462,13 @@ describe("task_tool", () => {
expect(result.task.status).toBe("in_progress")
})
test("updates dependsOn array", async () => {
test("updates blockedBy array additively", async () => {
//#given
const testId = await createTestTask("Test task")
const args = {
action: "update" as const,
id: testId,
dependsOn: ["T-dep1", "T-dep2", "T-dep3"],
addBlockedBy: ["T-dep1", "T-dep2", "T-dep3"],
}
//#when
@@ -476,7 +476,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr)
//#then
expect(result.task.dependsOn).toEqual(["T-dep1", "T-dep2", "T-dep3"])
expect(result.task.blockedBy).toEqual(["T-dep1", "T-dep2", "T-dep3"])
})
test("returns error for non-existent task", async () => {
@@ -484,7 +484,7 @@ describe("task_tool", () => {
const args = {
action: "update" as const,
id: "T-nonexistent",
title: "New title",
subject: "New subject",
}
//#when
@@ -501,7 +501,7 @@ describe("task_tool", () => {
const args = {
action: "update" as const,
id: "../package",
title: "New title",
subject: "New subject",
}
//#when
@@ -519,7 +519,7 @@ describe("task_tool", () => {
const args = {
action: "update" as const,
id: "T-nonexistent",
title: "New title",
subject: "New subject",
}
//#when
@@ -537,7 +537,7 @@ describe("task_tool", () => {
const args = {
action: "update" as const,
id: testId,
title: "Updated",
subject: "Updated",
}
//#when
@@ -555,7 +555,7 @@ describe("task_tool", () => {
const args = {
action: "update" as const,
id: testId,
title: "New title",
subject: "New subject",
description: "New description",
status: "completed" as const,
}
@@ -565,7 +565,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr)
//#then
expect(result.task.title).toBe("New title")
expect(result.task.subject).toBe("New subject")
expect(result.task.description).toBe("New description")
expect(result.task.status).toBe("completed")
})
@@ -668,8 +668,8 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Task A",
dependsOn: ["T-taskB"],
subject: "Task A",
blockedBy: ["T-taskB"],
}
//#when
@@ -685,8 +685,8 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Task with missing dependency",
dependsOn: ["T-nonexistent"],
subject: "Task with missing dependency",
blockedBy: ["T-nonexistent"],
}
//#when
@@ -710,7 +710,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr)
//#then
const tasksWithNoDeps = result.tasks.filter((t: TaskObject) => t.dependsOn.length === 0)
const tasksWithNoDeps = result.tasks.filter((t: TaskObject) => t.blockedBy.length === 0)
expect(tasksWithNoDeps.length).toBeGreaterThanOrEqual(0)
})
@@ -748,7 +748,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "",
subject: "",
}
//#when
@@ -762,10 +762,10 @@ describe("task_tool", () => {
test("handles very long title", async () => {
//#given
const longTitle = "A".repeat(1000)
const longSubject = "A".repeat(1000)
const args = {
action: "create" as const,
title: longTitle,
subject: longSubject,
}
//#when
@@ -780,7 +780,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Task with special chars: !@#$%^&*()",
subject: "Task with special chars: !@#$%^&*()",
}
//#when
@@ -795,7 +795,7 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "任務 🚀 Tâche",
subject: "任務 🚀 Tâche",
}
//#when
@@ -810,9 +810,9 @@ describe("task_tool", () => {
//#given
const args = {
action: "create" as const,
title: "Test task",
subject: "Test task",
description: "Test description",
dependsOn: ["T-dep1"],
blockedBy: ["T-dep1"],
parentID: "T-parent",
repoURL: "https://example.com",
}
@@ -823,10 +823,10 @@ describe("task_tool", () => {
//#then
expect(result.task).toHaveProperty("id")
expect(result.task).toHaveProperty("title")
expect(result.task).toHaveProperty("subject")
expect(result.task).toHaveProperty("description")
expect(result.task).toHaveProperty("status")
expect(result.task).toHaveProperty("dependsOn")
expect(result.task).toHaveProperty("blockedBy")
expect(result.task).toHaveProperty("parentID")
expect(result.task).toHaveProperty("repoURL")
expect(result.task).toHaveProperty("threadID")