Harness spec 1.1.0: one plan, prototypes and the library shared; the CLI implements it
The spec: plan_write and plan_update (new, shared, offered in every mode) and plan_submit (now shared) keep one structured plan — findings, objectives, phases of tasks — in place of the todo list, which lives on as a former name of plan_write. The shared system text is one text for both projects, with variables for what differs; the mode texts, a prototypes block and a library block are shared. acp.md: the client hands the device what only it knows (context, library, prototypes), the plan travels as a structure (plan_state), library calls carry the chat's scope, and a client may rewind a session. loop.json says its numbers as numbers. Four conformance areas: plan, prototype, library_scope, toolcall_index. The CLI: the plan is kept per session in the store and drawn above the prompt and on the approval card; the prompt is assembled from the spec's texts with their variables filled; the link reads the instance's instructions, personalization, knowledge scope and prototype offer, sends the plan as a structure and answers rewind; library calls to the instance carry the session as scope; a prototype written in the terminal is saved as a file and named in the transcript; a tool-call fragment without an index continues the last call; the default theme is Moria; dead exports and Hermes leftovers removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
96 files changed
+3259
-527
No files matched your search
+21
-9
@@ -6,9 +6,11 @@ import { applyPatchTool } from "../src/tool/apply_patch.ts"
|
||||
import { bashKillTool, bashOutputTool, bashTool } from "../src/tool/bash.ts"
|
||||
import { editTool } from "../src/tool/edit.ts"
|
||||
import { multieditTool } from "../src/tool/multiedit.ts"
|
||||
import { formatAnswers, questionSchema, unattendedReply, type QuestionRequest } from "../src/tool/question.ts"
|
||||
import { formatAnswers, questionSchema, unattendedReply, type QuestionRequest } from "../src/tool/ask_user.ts"
|
||||
import { readTool } from "../src/tool/read.ts"
|
||||
import { todoTool } from "../src/tool/todo.ts"
|
||||
import { planUpdateTool } from "../src/tool/plan_update.ts"
|
||||
import { planWriteTool } from "../src/tool/plan_write.ts"
|
||||
import type { Plan } from "../src/plan/plan.ts"
|
||||
import type { ToolContext } from "../src/tool/tool.ts"
|
||||
|
||||
function ctx(): ToolContext {
|
||||
@@ -163,15 +165,25 @@ describe("file tools", () => {
|
||||
expect(readFileSync(join(c.root, "f.txt"), "utf8")).toBe("ONE\ntwo\nthree\nFOUR\n")
|
||||
})
|
||||
|
||||
test("todo replaces the list and reports progress", async () => {
|
||||
test("plan_write stores the plan with ids and shows it; plan_update changes it by id, and an unknown id changes nothing", async () => {
|
||||
const c = ctx()
|
||||
const todos = { items: [] as { content: string; status: "pending" | "in_progress" | "completed" | "cancelled" }[] }
|
||||
// Through the schema, as the engine does: it turns the bare string into a pending item.
|
||||
const args = todoTool.schema.parse({ todos: [{ content: "read", status: "completed" }, { content: "fix", status: "in_progress" }, "test"] })
|
||||
const r = await todoTool.run(args, { ...c, todos })
|
||||
expect(r.output).toBe("[x] read\n[>] fix\n[ ] test")
|
||||
let stored: Plan | undefined
|
||||
const plan = { get: () => stored, set: (p: Plan | null) => (stored = p ?? undefined) }
|
||||
// Through the schema, as the engine does: a bare string is a task, a phase sent as its title is empty.
|
||||
const args = planWriteTool.schema.parse({ title: "Fix it", findings: ["cookie not renewed"], phases: [{ title: "Code", tasks: [{ text: "read", status: "done" }, { text: "fix", status: "doing" }, "test"] }, "Verify"] })
|
||||
const r = await planWriteTool.run(args, { ...c, plan })
|
||||
expect(r.output).toBe("# Fix it\nFindings: [f1] cookie not renewed\n## p1 Code (pending)\n[t1] [x] read\n[t2] [>] fix\n[t3] [ ] test\n## p2 Verify (pending)")
|
||||
expect(r.title).toBe("1/3 done")
|
||||
expect(todos.items).toHaveLength(3)
|
||||
expect(stored?.steps).toEqual(["read", "fix", "test"])
|
||||
const u = await planUpdateTool.run(planUpdateTool.schema.parse({ tasks: [{ id: "t2", status: "done" }, { id: "t3", status: "dropped", note: "covered" }], add_tasks: [{ phase: "p2", text: "run the suite" }] }), { ...c, plan })
|
||||
expect(u.output).toBe("# Fix it\nFindings: [f1] cookie not renewed\n## p1 Code (pending)\n[t1] [x] read\n[t2] [x] fix\n[t3] [~] test — covered\n## p2 Verify (pending)\n[t4] [ ] run the suite")
|
||||
expect(u.title).toBe("2/3 done")
|
||||
const bad = await planUpdateTool.run(planUpdateTool.schema.parse({ tasks: [{ id: "t9", status: "done" }] }), { ...c, plan })
|
||||
expect(bad).toEqual({ output: 'Unknown task id "t9". Known: t1, t2, t3, t4.', isError: true })
|
||||
expect(stored?.phases[1]!.tasks).toHaveLength(1)
|
||||
// No plan yet: plan_update says so instead of inventing one.
|
||||
stored = undefined
|
||||
expect((await planUpdateTool.run({ tasks: [{ id: "t1", status: "done" }] }, { ...c, plan })).isError).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user