Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/incremental-todo-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Require the todo tool to update multi-step lists after each completed task.
3 changes: 3 additions & 0 deletions packages/opencode/src/tool/todowrite.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Skip when:

## Rules
- Update status in real time; don't batch completions
- After creating a list, call this tool before starting the first item.
- After completing each item, call this tool before starting the next item, with the completed item marked `completed` and the next item marked `in_progress`.
- Do not complete multiple items or continue through multiple steps without updating the list between them.
- Mark `completed` only after the required work is actually done, including any required verification. Never based on intent.
- Keep exactly one `in_progress` while work remains
- If blocked or partial, keep it `in_progress` and add a follow-up todo describing the blocker
Expand Down
10 changes: 10 additions & 0 deletions packages/opencode/test/kilocode/todowrite-description.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, test } from "bun:test"
import DESCRIPTION_WRITE from "../../src/tool/todowrite.txt"

describe("todowrite description", () => {
test("requires an update between each task", () => {
expect(DESCRIPTION_WRITE).toContain("call this tool before starting the first item")
expect(DESCRIPTION_WRITE).toContain("After completing each item, call this tool before starting the next item")
expect(DESCRIPTION_WRITE).toContain("Do not complete multiple items or continue through multiple steps")
})
})
79 changes: 79 additions & 0 deletions packages/opencode/test/kilocode/todowrite-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect } from "bun:test"
import { createKiloClient } from "@kilocode/sdk/v2"
import { Effect } from "effect"
import { cliIt } from "../lib/cli-process"

const todos = [
[
{ content: "Inspect files", status: "in_progress", priority: "high" },
{ content: "Implement fix", status: "pending", priority: "medium" },
{ content: "Run checks", status: "pending", priority: "medium" },
{ content: "Report results", status: "pending", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "in_progress", priority: "medium" },
{ content: "Run checks", status: "pending", priority: "medium" },
{ content: "Report results", status: "pending", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "completed", priority: "medium" },
{ content: "Run checks", status: "in_progress", priority: "medium" },
{ content: "Report results", status: "pending", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "completed", priority: "medium" },
{ content: "Run checks", status: "completed", priority: "medium" },
{ content: "Report results", status: "in_progress", priority: "low" },
],
[
{ content: "Inspect files", status: "completed", priority: "high" },
{ content: "Implement fix", status: "completed", priority: "medium" },
{ content: "Run checks", status: "completed", priority: "medium" },
{ content: "Report results", status: "completed", priority: "low" },
],
] as const

describe("todowrite end-to-end", () => {
cliIt.live(
"persists every sequential update through the real CLI session",
({ llm, opencode }) =>
Effect.gen(function* () {
const server = yield* opencode.serve()
const client = createKiloClient({ baseUrl: server.url })
const session = yield* Effect.promise(() =>
client.session.create({
permission: [{ permission: "*", action: "allow", pattern: "*" }],
}),
)
const sessionID = session.data?.id
if (!sessionID) throw new Error("test session was not created")

for (const list of todos) yield* llm.tool("todowrite", { todos: list })
yield* llm.text("done")

const result = yield* opencode.run("complete this four-step task", {
extraArgs: ["--attach", server.url, "--session", sessionID, "--auto"],
timeoutMs: 90_000,
})
opencode.expectExit(result, 0)

expect(JSON.stringify(yield* llm.inputs)).toContain(
"After completing each item, call this tool before starting the next item",
)
const saved = yield* Effect.promise(() => client.session.todo({ sessionID }))
const final = todos[todos.length - 1]?.map((todo) => ({ ...todo }))
expect(saved.data).toEqual(final)

const messages = yield* Effect.promise(() => client.session.messages({ sessionID }))
const calls =
messages.data?.flatMap((message) =>
message.parts.filter((part) => part.type === "tool" && part.tool === "todowrite"),
) ?? []
expect(calls).toHaveLength(todos.length)
}),
120_000,
)
})
Loading