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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 316 additions and 75 deletions

View File

@ -84,6 +84,40 @@ Oh-My-OpenCode is a **plugin for OpenCode**. You will frequently need to examine
--- ---
## CRITICAL: ENGLISH-ONLY POLICY (NEVER DELETE THIS SECTION)
> **THIS SECTION MUST NEVER BE REMOVED OR MODIFIED**
### All Project Communications MUST Be in English
This is an **international open-source project**. To ensure accessibility and maintainability:
| Context | Language Requirement |
|---------|---------------------|
| **GitHub Issues** | English ONLY |
| **Pull Requests** | English ONLY (title, description, comments) |
| **Commit Messages** | English ONLY |
| **Code Comments** | English ONLY |
| **Documentation** | English ONLY |
| **AGENTS.md files** | English ONLY |
### Why This Matters
- **Global Collaboration**: Contributors from all countries can participate
- **Searchability**: English keywords are universally searchable
- **AI Agent Compatibility**: AI tools work best with English content
- **Consistency**: Mixed languages create confusion and fragmentation
### Enforcement
- Issues/PRs with non-English content may be closed with a request to resubmit in English
- Commit messages must be in English - CI may reject non-English commits
- Translated READMEs exist (README.ko.md, README.ja.md, etc.) but the primary docs are English
**If you're not comfortable writing in English, use translation tools. Broken English is fine - we'll help fix it. Non-English is not acceptable.**
---
## OVERVIEW ## OVERVIEW
O P E N C O D E plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3 Flash). 34 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 11 specialized agents, full Claude Code compatibility. "oh-my-zsh" for O P E N C O D E. O P E N C O D E plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3 Flash). 34 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 11 specialized agents, full Claude Code compatibility. "oh-my-zsh" for O P E N C O D E.

View File

@ -24,17 +24,17 @@
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/picomatch": "^3.0.2", "@types/picomatch": "^3.0.2",
"bun-types": "latest", "bun-types": "1.3.6",
"typescript": "^5.7.3", "typescript": "^5.7.3",
}, },
"optionalDependencies": { "optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.1.11", "oh-my-opencode-darwin-arm64": "3.2.1",
"oh-my-opencode-darwin-x64": "3.1.11", "oh-my-opencode-darwin-x64": "3.2.1",
"oh-my-opencode-linux-arm64": "3.1.11", "oh-my-opencode-linux-arm64": "3.2.1",
"oh-my-opencode-linux-arm64-musl": "3.1.11", "oh-my-opencode-linux-arm64-musl": "3.2.1",
"oh-my-opencode-linux-x64": "3.1.11", "oh-my-opencode-linux-x64": "3.2.1",
"oh-my-opencode-linux-x64-musl": "3.1.11", "oh-my-opencode-linux-x64-musl": "3.2.1",
"oh-my-opencode-windows-x64": "3.1.11", "oh-my-opencode-windows-x64": "3.2.1",
}, },
}, },
}, },
@ -110,7 +110,7 @@
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@ -226,19 +226,19 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.1.11", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-tMQJrMq2aY+EnfYLTqxQ16T4MzcmFO0tbUmr0ceMDtlGVks18Ro4mnPnFZXk6CyAInIi72pwYrjUlH38qxKfgQ=="], "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.2.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-IvhHRUXTr/g/hJlkKTU2oCdgRl2BDl/Qre31Rukhs4NumlvME6iDmdnm8mM7bTxugfCBkfUUr7QJLxxLhzjdLA=="],
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.1.11", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-hBbNvp5M2e8jI+6XexbbwiFuJWRfGLCheJKGK1+XbP4akhSoYjYdt2PO08LNfuFlryEMf/RWB43sZmjwSWOQlQ=="], "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.2.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-V2JbAdThAVfhBOcb+wBPZrAI0vBxPPRBdvmAixAxBOFC49CIJUrEFIRBUYFKhSQGHYWrNy8z0zJYoNQm4oQPog=="],
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.1.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-mnHmXXWzYt7s5qQ80HFaT+3hprdFucyn4HMRjZzA9oBoOn38ZhWbwPEzrGtjafMUeZUy0Sj3WYZ4CLChG26weA=="], "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.2.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-SeT8P7Icq5YH/AIaEF28J4q+ifUnOqO2UgMFtdFusr8JLadYFy+6dTdeAuD2uGGToDQ3ZNKuaG+lo84KzEhA5w=="],
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.1.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-4dgXCU1By/1raClTJYhIhODomIB4l/5SRSgnj6lWwcqUijURH9HzN00QYzRfMI0phMV2jYAMklgCpGjuY9/gTA=="], "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.2.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-wJUEVVUn1gyVIFNV4mxWg9cYo1rQdTKUXdGLfiqPiyQhWhZLRfPJ+9qpghvIVv7Dne6rzkbhYWdwdk/tew5RtQ=="],
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.1.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vfv4w4116lYFup5coSnsYG3cyeOE6QFYQz5fO3uq+90jCzl8nzVC6CkiAvD0+f8+8aml56z9+MznHmCT3tEg7Q=="], "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.2.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-p/XValXi1RRTZV8mEsdStXwZBkyQpgZjB41HLf0VfizPMAKRr6/bhuFZ9BDZFIhcDnLYcGV54MAVEsWms5yC2A=="],
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.1.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-f7gvxG/GjuPqlsiXjXTVJU8oC28mQ0o8dwtnj1K2VHS1UTRNtIXskCwfc0EU4E+icAQYETxj3LfaGVfBlyJyzg=="], "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.2.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-G7aNMqAMO2P+wUUaaAV8sXymm59cX4G9aVNXKAd/PM6RgFWh2F4HkXkOhOdHKYZzCl1QRhjh672mNillYsvebg=="],
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.1.11", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-LevsDHYdYwD4a+St3wmwMbj4wVh9LfTVE3+fKQHBh70WAsRrV603gBq2NdN6JXTd3/zbm9ZbHLOZrLnJetKi3Q=="], "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.2.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-pyqTGlNxirKxQgXx9YJBq2y8KN/1oIygVupClmws7dDPj9etI1l8fs/SBEnMsYzMqTlGbLVeJ5+kj9p+yg7YDA=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],

View File

@ -253,6 +253,8 @@ export const ExperimentalConfigSchema = z.object({
truncate_all_tool_outputs: z.boolean().optional(), truncate_all_tool_outputs: z.boolean().optional(),
/** Dynamic context pruning configuration */ /** Dynamic context pruning configuration */
dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(), dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
/** Enable experimental task system for Todowrite disabler hook */
task_system: z.boolean().optional(),
}) })
export const SkillSourceSchema = z.union([ export const SkillSourceSchema = z.union([

View File

@ -37,3 +37,4 @@ export { createStopContinuationGuardHook, type StopContinuationGuard } from "./s
export { createCompactionContextInjector, type SummarizeContext } from "./compaction-context-injector"; export { createCompactionContextInjector, type SummarizeContext } from "./compaction-context-injector";
export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter"; export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
export { createPreemptiveCompactionHook } from "./preemptive-compaction"; export { createPreemptiveCompactionHook } from "./preemptive-compaction";
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";

View File

@ -0,0 +1,10 @@
export const HOOK_NAME = "tasks-todowrite-disabler"
export const BLOCKED_TOOLS = ["TodoWrite", "TodoRead"]
export const REPLACEMENT_MESSAGE = `TodoRead/TodoWrite are disabled because experimental.task_system is enabled.
Use the new task tools instead:
- TaskCreate: Create new tasks with auto-generated IDs
- TaskUpdate: Update task status, add dependencies
- TaskList: List active tasks with dependency info
- TaskGet: Get full task details
IMPORTANT: 1 task = 1 delegate_task. Maximize parallel execution by running independent tasks concurrently.`

View File

@ -0,0 +1,137 @@
import { describe, expect, test } from "bun:test"
const { createTasksTodowriteDisablerHook } = await import("./index")
describe("tasks-todowrite-disabler", () => {
describe("when experimental.task_system is enabled", () => {
test("should block TodoWrite tool", async () => {
// given
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
const input = {
tool: "TodoWrite",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("TodoRead/TodoWrite are disabled")
})
test("should block TodoRead tool", async () => {
// given
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
const input = {
tool: "TodoRead",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("TodoRead/TodoWrite are disabled")
})
test("should not block other tools", async () => {
// given
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
const input = {
tool: "Read",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
})
describe("when experimental.task_system is disabled or undefined", () => {
test("should not block TodoWrite when flag is false", async () => {
// given
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: false } })
const input = {
tool: "TodoWrite",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
test("should not block TodoWrite when experimental is undefined", async () => {
// given
const hook = createTasksTodowriteDisablerHook({})
const input = {
tool: "TodoWrite",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
test("should not block TodoRead when flag is false", async () => {
// given
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: false } })
const input = {
tool: "TodoRead",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
})
describe("error message content", () => {
test("should include replacement message with task tools info", async () => {
// given
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
const input = {
tool: "TodoWrite",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: {},
}
// when / then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow(/TaskCreate|TaskUpdate|TaskList|TaskGet/)
})
})
})

View File

@ -0,0 +1,29 @@
import { BLOCKED_TOOLS, REPLACEMENT_MESSAGE } from "./constants";
export interface TasksTodowriteDisablerConfig {
experimental?: {
task_system?: boolean;
};
}
export function createTasksTodowriteDisablerHook(
config: TasksTodowriteDisablerConfig,
) {
const isTaskSystemEnabled = config.experimental?.task_system ?? false;
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> },
) => {
if (!isTaskSystemEnabled) {
return;
}
const toolName = input.tool as string;
if (BLOCKED_TOOLS.some((blocked) => blocked.toLowerCase() === toolName.toLowerCase())) {
throw new Error(REPLACEMENT_MESSAGE);
}
},
};
}

View File

@ -36,6 +36,7 @@ import {
createCompactionContextInjector, createCompactionContextInjector,
createUnstableAgentBabysitterHook, createUnstableAgentBabysitterHook,
createPreemptiveCompactionHook, createPreemptiveCompactionHook,
createTasksTodowriteDisablerHook,
} from "./hooks"; } from "./hooks";
import { import {
contextCollector, contextCollector,
@ -269,6 +270,12 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
? createSisyphusJuniorNotepadHook(ctx) ? createSisyphusJuniorNotepadHook(ctx)
: null; : null;
const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler")
? createTasksTodowriteDisablerHook({
experimental: pluginConfig.experimental,
})
: null;
const questionLabelTruncator = createQuestionLabelTruncatorHook(); const questionLabelTruncator = createQuestionLabelTruncatorHook();
const subagentQuestionBlocker = createSubagentQuestionBlockerHook(); const subagentQuestionBlocker = createSubagentQuestionBlockerHook();
@ -464,8 +471,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
modelCacheState, modelCacheState,
}); });
const newTaskSystemEnabled = pluginConfig.new_task_system_enabled ?? false; const taskSystemEnabled = pluginConfig.experimental?.task_system ?? false;
const taskToolsRecord: Record<string, ToolDefinition> = newTaskSystemEnabled const taskToolsRecord: Record<string, ToolDefinition> = taskSystemEnabled
? { ? {
task_create: createTaskCreateTool(pluginConfig, ctx), task_create: createTaskCreateTool(pluginConfig, ctx),
task_get: createTaskGetTool(pluginConfig), task_get: createTaskGetTool(pluginConfig),
@ -714,10 +721,11 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
await questionLabelTruncator["tool.execute.before"]?.(input, output); await questionLabelTruncator["tool.execute.before"]?.(input, output);
await claudeCodeHooks["tool.execute.before"](input, output); await claudeCodeHooks["tool.execute.before"](input, output);
await nonInteractiveEnv?.["tool.execute.before"](input, output); await nonInteractiveEnv?.["tool.execute.before"](input, output);
await commentChecker?.["tool.execute.before"](input, output); await commentChecker?.["tool.execute.before"]?.(input, output);
await directoryAgentsInjector?.["tool.execute.before"]?.(input, output); await directoryAgentsInjector?.["tool.execute.before"]?.(input, output);
await directoryReadmeInjector?.["tool.execute.before"]?.(input, output); await directoryReadmeInjector?.["tool.execute.before"]?.(input, output);
await rulesInjector?.["tool.execute.before"]?.(input, output); await rulesInjector?.["tool.execute.before"]?.(input, output);
await tasksTodowriteDisabler?.["tool.execute.before"]?.(input, output);
await prometheusMdOnly?.["tool.execute.before"]?.(input, output); await prometheusMdOnly?.["tool.execute.before"]?.(input, output);
await sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output); await sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output);
await atlasHook?.["tool.execute.before"]?.(input, output); await atlasHook?.["tool.execute.before"]?.(input, output);

View File

@ -123,7 +123,6 @@ export function loadPluginConfig(
config = { config = {
...config, ...config,
new_task_system_enabled: config.new_task_system_enabled ?? false,
}; };
log("Final merged config", { log("Final merged config", {

View File

@ -405,7 +405,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
LspCodeActionResolve: false, LspCodeActionResolve: false,
"task_*": false, "task_*": false,
teammate: false, teammate: false,
...(pluginConfig.new_task_system_enabled ? { todowrite: false, todoread: false } : {}), ...(pluginConfig.experimental?.task_system ? { todowrite: false, todoread: false } : {}),
}; };
type AgentWithPermission = { permission?: Record<string, unknown> }; type AgentWithPermission = { permission?: Record<string, unknown> };

View File

@ -213,6 +213,13 @@ export function migrateConfigFile(configPath: string, rawConfig: Record<string,
} }
} }
if (rawConfig.experimental && typeof rawConfig.experimental === "object") {
const exp = rawConfig.experimental as Record<string, unknown>
if ("task_system" in exp && exp.task_system !== undefined) {
needsWrite = true
}
}
if (needsWrite) { if (needsWrite) {
try { try {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-") const timestamp = new Date().toISOString().replace(/[:.]/g, "-")

View File

@ -16,12 +16,19 @@ export function createTaskCreateTool(
config: Partial<OhMyOpenCodeConfig>, config: Partial<OhMyOpenCodeConfig>,
ctx?: PluginInput, ctx?: PluginInput,
): ToolDefinition { ): ToolDefinition {
return tool({ return tool({
description: `Create a new task with auto-generated ID and threadID recording. 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". Auto-generates T-{uuid} ID, records threadID from context, sets status to "pending".
Returns minimal response with task ID and subject.`, Returns minimal response with task ID and subject.
args: {
**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)"), subject: tool.schema.string().describe("Task subject (required)"),
description: tool.schema.string().optional().describe("Task description"), description: tool.schema.string().optional().describe("Task description"),
activeForm: tool.schema activeForm: tool.schema

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."
})
}, },
}) })
} }

View File

@ -23,14 +23,18 @@ export function createTaskUpdateTool(
config: Partial<OhMyOpenCodeConfig>, config: Partial<OhMyOpenCodeConfig>,
ctx?: PluginInput, ctx?: PluginInput,
): ToolDefinition { ): ToolDefinition {
return tool({ return tool({
description: `Update an existing task with new values. description: `Update an existing task with new values.
Supports updating: subject, description, status, activeForm, owner, metadata. Supports updating: subject, description, status, activeForm, owner, metadata.
For blocks/blockedBy: use addBlocks/addBlockedBy to append (additive, not replacement). For blocks/blockedBy: use addBlocks/addBlockedBy to append (additive, not replacement).
For metadata: merge with existing, set key to null to delete. For metadata: merge with existing, set key to null to delete.
Syncs to OpenCode Todo API after update.`, Syncs to OpenCode Todo API after update.
args: {
**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)"), id: tool.schema.string().describe("Task ID (required)"),
subject: tool.schema.string().optional().describe("Task subject"), subject: tool.schema.string().optional().describe("Task subject"),
description: tool.schema.string().optional().describe("Task description"), description: tool.schema.string().optional().describe("Task description"),

View File

@ -7,7 +7,7 @@ import { createTask } from "./task"
const TEST_STORAGE = ".test-task-tool" const TEST_STORAGE = ".test-task-tool"
const TEST_DIR = join(process.cwd(), TEST_STORAGE) const TEST_DIR = join(process.cwd(), TEST_STORAGE)
const TEST_CONFIG = { const TEST_CONFIG = {
new_task_system_enabled: true, experimental: { task_system: true },
sisyphus: { sisyphus: {
tasks: { tasks: {
storage_path: TEST_STORAGE, storage_path: TEST_STORAGE,
@ -35,10 +35,10 @@ describe("task_tool", () => {
taskTool = createTask(TEST_CONFIG) 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 = { const args = {
action: "create" as const, action: "create" as const,
title, subject,
...overrides, ...overrides,
} }
const resultStr = await taskTool.execute(args, TEST_CONTEXT) const resultStr = await taskTool.execute(args, TEST_CONTEXT)
@ -61,7 +61,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Implement authentication", subject: "Implement authentication",
} }
//#when //#when
@ -71,15 +71,15 @@ describe("task_tool", () => {
//#then //#then
expect(result).toHaveProperty("task") expect(result).toHaveProperty("task")
expect(result.task).toHaveProperty("id") expect(result.task).toHaveProperty("id")
expect(result.task.title).toBe("Implement authentication") expect(result.task.subject).toBe("Implement authentication")
expect(result.task.status).toBe("open") expect(result.task.status).toBe("pending")
}) })
test("auto-generates T-{uuid} format ID", async () => { test("auto-generates T-{uuid} format ID", async () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
} }
//#when //#when
@ -94,7 +94,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
} }
//#when //#when
@ -110,7 +110,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
} }
//#when //#when
@ -118,14 +118,14 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr) const result = JSON.parse(resultStr)
//#then //#then
expect(result.task.status).toBe("open") expect(result.task.status).toBe("pending")
}) })
test("stores optional description field", async () => { test("stores optional description field", async () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
description: "Detailed description of the task", description: "Detailed description of the task",
} }
@ -141,8 +141,8 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
dependsOn: ["T-dep1", "T-dep2"], blockedBy: ["T-dep1", "T-dep2"],
} }
//#when //#when
@ -150,14 +150,14 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr) const result = JSON.parse(resultStr)
//#then //#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 () => { test("stores parentID when provided", async () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Subtask", subject: "Subtask",
parentID: "T-parent123", parentID: "T-parent123",
} }
@ -173,7 +173,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
repoURL: "https://github.com/code-yeongyu/oh-my-opencode", repoURL: "https://github.com/code-yeongyu/oh-my-opencode",
} }
@ -189,7 +189,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
} }
//#when //#when
@ -205,7 +205,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
} }
//#when //#when
@ -213,7 +213,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr) const result = JSON.parse(resultStr)
//#then //#then
expect(result.task.dependsOn).toEqual([]) expect(result.task.blockedBy).toEqual([])
}) })
}) })
@ -398,7 +398,7 @@ describe("task_tool", () => {
//#then //#then
if (result.task !== null) { if (result.task !== null) {
expect(result.task).toHaveProperty("id") expect(result.task).toHaveProperty("id")
expect(result.task).toHaveProperty("title") expect(result.task).toHaveProperty("subject")
expect(result.task).toHaveProperty("status") expect(result.task).toHaveProperty("status")
expect(result.task).toHaveProperty("threadID") expect(result.task).toHaveProperty("threadID")
} }
@ -416,7 +416,7 @@ describe("task_tool", () => {
const args = { const args = {
action: "update" as const, action: "update" as const,
id: testId, id: testId,
title: "Updated title", subject: "Updated subject",
} }
//#when //#when
@ -425,7 +425,7 @@ describe("task_tool", () => {
//#then //#then
expect(result).toHaveProperty("task") expect(result).toHaveProperty("task")
expect(result.task.title).toBe("Updated title") expect(result.task.subject).toBe("Updated subject")
}) })
test("updates task description", async () => { test("updates task description", async () => {
@ -462,13 +462,13 @@ describe("task_tool", () => {
expect(result.task.status).toBe("in_progress") expect(result.task.status).toBe("in_progress")
}) })
test("updates dependsOn array", async () => { test("updates blockedBy array additively", async () => {
//#given //#given
const testId = await createTestTask("Test task") const testId = await createTestTask("Test task")
const args = { const args = {
action: "update" as const, action: "update" as const,
id: testId, id: testId,
dependsOn: ["T-dep1", "T-dep2", "T-dep3"], addBlockedBy: ["T-dep1", "T-dep2", "T-dep3"],
} }
//#when //#when
@ -476,7 +476,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr) const result = JSON.parse(resultStr)
//#then //#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 () => { test("returns error for non-existent task", async () => {
@ -484,7 +484,7 @@ describe("task_tool", () => {
const args = { const args = {
action: "update" as const, action: "update" as const,
id: "T-nonexistent", id: "T-nonexistent",
title: "New title", subject: "New subject",
} }
//#when //#when
@ -501,7 +501,7 @@ describe("task_tool", () => {
const args = { const args = {
action: "update" as const, action: "update" as const,
id: "../package", id: "../package",
title: "New title", subject: "New subject",
} }
//#when //#when
@ -519,7 +519,7 @@ describe("task_tool", () => {
const args = { const args = {
action: "update" as const, action: "update" as const,
id: "T-nonexistent", id: "T-nonexistent",
title: "New title", subject: "New subject",
} }
//#when //#when
@ -537,7 +537,7 @@ describe("task_tool", () => {
const args = { const args = {
action: "update" as const, action: "update" as const,
id: testId, id: testId,
title: "Updated", subject: "Updated",
} }
//#when //#when
@ -555,7 +555,7 @@ describe("task_tool", () => {
const args = { const args = {
action: "update" as const, action: "update" as const,
id: testId, id: testId,
title: "New title", subject: "New subject",
description: "New description", description: "New description",
status: "completed" as const, status: "completed" as const,
} }
@ -565,7 +565,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr) const result = JSON.parse(resultStr)
//#then //#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.description).toBe("New description")
expect(result.task.status).toBe("completed") expect(result.task.status).toBe("completed")
}) })
@ -668,8 +668,8 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Task A", subject: "Task A",
dependsOn: ["T-taskB"], blockedBy: ["T-taskB"],
} }
//#when //#when
@ -685,8 +685,8 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Task with missing dependency", subject: "Task with missing dependency",
dependsOn: ["T-nonexistent"], blockedBy: ["T-nonexistent"],
} }
//#when //#when
@ -710,7 +710,7 @@ describe("task_tool", () => {
const result = JSON.parse(resultStr) const result = JSON.parse(resultStr)
//#then //#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) expect(tasksWithNoDeps.length).toBeGreaterThanOrEqual(0)
}) })
@ -748,7 +748,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "", subject: "",
} }
//#when //#when
@ -762,10 +762,10 @@ describe("task_tool", () => {
test("handles very long title", async () => { test("handles very long title", async () => {
//#given //#given
const longTitle = "A".repeat(1000) const longSubject = "A".repeat(1000)
const args = { const args = {
action: "create" as const, action: "create" as const,
title: longTitle, subject: longSubject,
} }
//#when //#when
@ -780,7 +780,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Task with special chars: !@#$%^&*()", subject: "Task with special chars: !@#$%^&*()",
} }
//#when //#when
@ -795,7 +795,7 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "任務 🚀 Tâche", subject: "任務 🚀 Tâche",
} }
//#when //#when
@ -810,9 +810,9 @@ describe("task_tool", () => {
//#given //#given
const args = { const args = {
action: "create" as const, action: "create" as const,
title: "Test task", subject: "Test task",
description: "Test description", description: "Test description",
dependsOn: ["T-dep1"], blockedBy: ["T-dep1"],
parentID: "T-parent", parentID: "T-parent",
repoURL: "https://example.com", repoURL: "https://example.com",
} }
@ -823,10 +823,10 @@ describe("task_tool", () => {
//#then //#then
expect(result.task).toHaveProperty("id") expect(result.task).toHaveProperty("id")
expect(result.task).toHaveProperty("title") expect(result.task).toHaveProperty("subject")
expect(result.task).toHaveProperty("description") expect(result.task).toHaveProperty("description")
expect(result.task).toHaveProperty("status") expect(result.task).toHaveProperty("status")
expect(result.task).toHaveProperty("dependsOn") expect(result.task).toHaveProperty("blockedBy")
expect(result.task).toHaveProperty("parentID") expect(result.task).toHaveProperty("parentID")
expect(result.task).toHaveProperty("repoURL") expect(result.task).toHaveProperty("repoURL")
expect(result.task).toHaveProperty("threadID") expect(result.task).toHaveProperty("threadID")