diff --git a/AGENTS.md b/AGENTS.md index bca64f0ab..1d3e8d8b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 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. diff --git a/bun.lock b/bun.lock index 45677e56d..50747e1ee 100644 --- a/bun.lock +++ b/bun.lock @@ -24,17 +24,17 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/picomatch": "^3.0.2", - "bun-types": "latest", + "bun-types": "1.3.6", "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.1.11", - "oh-my-opencode-darwin-x64": "3.1.11", - "oh-my-opencode-linux-arm64": "3.1.11", - "oh-my-opencode-linux-arm64-musl": "3.1.11", - "oh-my-opencode-linux-x64": "3.1.11", - "oh-my-opencode-linux-x64-musl": "3.1.11", - "oh-my-opencode-windows-x64": "3.1.11", + "oh-my-opencode-darwin-arm64": "3.2.1", + "oh-my-opencode-darwin-x64": "3.2.1", + "oh-my-opencode-linux-arm64": "3.2.1", + "oh-my-opencode-linux-arm64-musl": "3.2.1", + "oh-my-opencode-linux-x64": "3.2.1", + "oh-my-opencode-linux-x64-musl": "3.2.1", + "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=="], - "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=="], @@ -226,19 +226,19 @@ "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=="], diff --git a/src/config/schema.ts b/src/config/schema.ts index 35d4ef4bf..19b4ad89a 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -253,6 +253,8 @@ export const ExperimentalConfigSchema = z.object({ truncate_all_tool_outputs: z.boolean().optional(), /** Dynamic context pruning configuration */ dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(), + /** Enable experimental task system for Todowrite disabler hook */ + task_system: z.boolean().optional(), }) export const SkillSourceSchema = z.union([ diff --git a/src/hooks/index.ts b/src/hooks/index.ts index b136fa87b..407e7e46a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -37,3 +37,4 @@ export { createStopContinuationGuardHook, type StopContinuationGuard } from "./s export { createCompactionContextInjector, type SummarizeContext } from "./compaction-context-injector"; export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter"; export { createPreemptiveCompactionHook } from "./preemptive-compaction"; +export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler"; diff --git a/src/hooks/tasks-todowrite-disabler/constants.ts b/src/hooks/tasks-todowrite-disabler/constants.ts new file mode 100644 index 000000000..a63ecc312 --- /dev/null +++ b/src/hooks/tasks-todowrite-disabler/constants.ts @@ -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.` diff --git a/src/hooks/tasks-todowrite-disabler/index.test.ts b/src/hooks/tasks-todowrite-disabler/index.test.ts new file mode 100644 index 000000000..928e3c251 --- /dev/null +++ b/src/hooks/tasks-todowrite-disabler/index.test.ts @@ -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/) + }) + }) +}) diff --git a/src/hooks/tasks-todowrite-disabler/index.ts b/src/hooks/tasks-todowrite-disabler/index.ts new file mode 100644 index 000000000..2fee8a18e --- /dev/null +++ b/src/hooks/tasks-todowrite-disabler/index.ts @@ -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 }, + ) => { + if (!isTaskSystemEnabled) { + return; + } + + const toolName = input.tool as string; + if (BLOCKED_TOOLS.some((blocked) => blocked.toLowerCase() === toolName.toLowerCase())) { + throw new Error(REPLACEMENT_MESSAGE); + } + }, + }; +} diff --git a/src/index.ts b/src/index.ts index e5e2c059d..c37b84157 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ import { createCompactionContextInjector, createUnstableAgentBabysitterHook, createPreemptiveCompactionHook, + createTasksTodowriteDisablerHook, } from "./hooks"; import { contextCollector, @@ -269,6 +270,12 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { ? createSisyphusJuniorNotepadHook(ctx) : null; + const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler") + ? createTasksTodowriteDisablerHook({ + experimental: pluginConfig.experimental, + }) + : null; + const questionLabelTruncator = createQuestionLabelTruncatorHook(); const subagentQuestionBlocker = createSubagentQuestionBlockerHook(); @@ -464,8 +471,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { modelCacheState, }); - const newTaskSystemEnabled = pluginConfig.new_task_system_enabled ?? false; - const taskToolsRecord: Record = newTaskSystemEnabled + const taskSystemEnabled = pluginConfig.experimental?.task_system ?? false; + const taskToolsRecord: Record = taskSystemEnabled ? { task_create: createTaskCreateTool(pluginConfig, ctx), task_get: createTaskGetTool(pluginConfig), @@ -714,10 +721,11 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { await questionLabelTruncator["tool.execute.before"]?.(input, output); await claudeCodeHooks["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 directoryReadmeInjector?.["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 sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output); await atlasHook?.["tool.execute.before"]?.(input, output); diff --git a/src/plugin-config.ts b/src/plugin-config.ts index 3f5608392..b95942fd4 100644 --- a/src/plugin-config.ts +++ b/src/plugin-config.ts @@ -123,7 +123,6 @@ export function loadPluginConfig( config = { ...config, - new_task_system_enabled: config.new_task_system_enabled ?? false, }; log("Final merged config", { diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 49e85c588..19c6dd02c 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -405,7 +405,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { LspCodeActionResolve: false, "task_*": false, teammate: false, - ...(pluginConfig.new_task_system_enabled ? { todowrite: false, todoread: false } : {}), + ...(pluginConfig.experimental?.task_system ? { todowrite: false, todoread: false } : {}), }; type AgentWithPermission = { permission?: Record }; diff --git a/src/shared/migration.ts b/src/shared/migration.ts index 6106e5c87..e50042469 100644 --- a/src/shared/migration.ts +++ b/src/shared/migration.ts @@ -213,6 +213,13 @@ export function migrateConfigFile(configPath: string, rawConfig: Record + if ("task_system" in exp && exp.task_system !== undefined) { + needsWrite = true + } + } + if (needsWrite) { try { const timestamp = new Date().toISOString().replace(/[:.]/g, "-") diff --git a/src/tools/task/task-create.ts b/src/tools/task/task-create.ts index 8fce7413e..f1eb41888 100644 --- a/src/tools/task/task-create.ts +++ b/src/tools/task/task-create.ts @@ -16,12 +16,19 @@ export function createTaskCreateTool( config: Partial, 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 diff --git a/src/tools/task/task-list.ts b/src/tools/task/task-list.ts index 0328bfc5a..d7ba921bc 100644 --- a/src/tools/task/task-list.ts +++ b/src/tools/task/task-list.ts @@ -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." + }) }, }) } diff --git a/src/tools/task/task-update.ts b/src/tools/task/task-update.ts index c56382ea1..d529c408f 100644 --- a/src/tools/task/task-update.ts +++ b/src/tools/task/task-update.ts @@ -23,14 +23,18 @@ export function createTaskUpdateTool( config: Partial, 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"), diff --git a/src/tools/task/task.test.ts b/src/tools/task/task.test.ts index 7bd9809c4..7fafded0a 100644 --- a/src/tools/task/task.test.ts +++ b/src/tools/task/task.test.ts @@ -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[0]> = {}): Promise { + async function createTestTask(subject: string, overrides: Partial[0]> = {}): Promise { 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")