diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index e643c0293b7b..504e0c56fc9f 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -238,6 +238,15 @@ If the spec is clear enough to decompose: "labels": ["module:config", "file:src/config/config.ts"], "depends_on": [], "priority": 0 + }, + { + "title": "Write OAuth2 login handler", + "description": "Implement the login route using the config schema from the previous task", + "acceptance_criteria": "Handler validates token, returns 401 on failure. Tests pass.", + "task_type": "implementation", + "labels": ["module:auth", "file:src/auth/login.ts"], + "depends_on": ["Add OAuth2 config schema"], // use exact title strings from this batch, never numeric indexes + "priority": 1 } ] } @@ -250,7 +259,8 @@ RULES FOR GOOD TASK DECOMPOSITION: 5. Tasks with no shared module:/file: labels can run in parallel 6. Do not create tasks for work not explicitly required by the issue 7. Validate your own output: check that no depends_on creates a cycle before responding -8. Respond with ONLY the JSON object — no markdown, no explanation, no code blocks`, +8. Respond with ONLY the JSON object — no markdown, no explanation, no code blocks + 9. depends_on values must be the EXACT title string of another task in this batch — never use numbers, indexes, or abbreviations`, }, "developer-pipeline": { name: "developer-pipeline", diff --git a/packages/opencode/src/tasks/composer.ts b/packages/opencode/src/tasks/composer.ts index f07171ff9121..f69b72882351 100644 --- a/packages/opencode/src/tasks/composer.ts +++ b/packages/opencode/src/tasks/composer.ts @@ -12,7 +12,7 @@ const ComposerTasksSchema = z.object({ acceptance_criteria: z.string().min(1), task_type: z.enum(["implementation", "test", "research"]), labels: z.array(z.string().max(100)).default([]), - depends_on: z.array(z.string().min(1).max(200)).default([]), + depends_on: z.array(z.string().min(1).max(200)).default([]), priority: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4)]), }) diff --git a/packages/opencode/test/tasks/composer.test.ts b/packages/opencode/test/tasks/composer.test.ts index c794d93a8259..27c2608f264a 100644 --- a/packages/opencode/test/tasks/composer.test.ts +++ b/packages/opencode/test/tasks/composer.test.ts @@ -852,4 +852,56 @@ test("runComposer rolls back tasks on partial creation failure", async () => { } Store.createTask = originalCreate +}) + +test("runComposer rejects numeric depends_on values", async () => { + const mockSpawn = async () => + JSON.stringify({ + status: "ready", + tasks: [ + { + title: "Task A", + description: "desc", + acceptance_criteria: "criteria", + task_type: "implementation" as const, + labels: ["module:test"], + depends_on: [], + priority: 0, + }, + { + title: "Task B", + description: "desc", + acceptance_criteria: "criteria", + task_type: "implementation" as const, + labels: ["module:test"], + depends_on: [1], // numeric — should be rejected + priority: 1, + }, + ], + }) + + let threw = false + try { + await runComposer( + { + jobId: "job-1", + projectId: "test-project", + pmSessionId: "session-1", + issueNumber: 123, + issueTitle: "Add feature", + issueBody: "Please add a feature.", + }, + mockSpawn, + ) + } catch (e) { + threw = true + const msg = (e as Error).message + if (!msg.includes("validation failed")) { + throw new Error(`Expected validation error for numeric depends_on, got: ${msg}`) + } + } + + if (!threw) { + throw new Error("Expected runComposer to throw validation error for numeric depends_on") + } }) \ No newline at end of file