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:
HomerandClaude Fable 5.1 committed 2026-10-18 04:02:12 +02:00
1 parent 6e70ff46d0
commit 837f38c744
96 files changed
+3259 -527

No files matched your search

+21 -9
View File
@@ -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)
})
})