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

+31 -11
View File
@@ -217,21 +217,42 @@ test("the question card: pick by number, multiple with space, ask back — and t
expect(result).toContain('↩ counter-question: "what does docker cost?"')
})
test("esc dismisses the questions; the todo panel shows open work", async () => {
test("esc dismisses the questions; the plan panel shows open work, and a plan call is not drawn as a tool line", async () => {
t = await tui([
{ chunks: [toolCall(0, "t1", "todo", JSON.stringify({ todos: [{ content: "read the code", status: "completed" }, { content: "fix the bug", status: "in_progress" }, { content: "run tests", status: "pending" }] })), toolCall(1, "q1", "ask_user", '{"question":"Go on?","options":["yes","no"]}')] },
{ chunks: [toolCall(0, "t1", "plan_write", JSON.stringify({ title: "The bug", phases: [{ title: "Work", status: "active", tasks: [{ text: "read the code", status: "done" }, { text: "fix the bug", status: "doing" }, "run tests"] }] })), toolCall(1, "q1", "ask_user", '{"question":"Go on?","options":["yes","no"]}')] },
{ chunks: [delta({ content: "Carrying on." })] },
])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Go on?")).toBe(true)
expect(t.frame()).toContain("todo 1/3")
expect(t.frame()).toContain("plan 1/3 · The bug")
expect(t.frame()).toContain("▸ fix the bug")
expect(t.frame()).toContain("☐ run tests")
expect(t.frame()).not.toContain("plan_write")
t.input.pressEscape()
expect(await t.until("Carrying on.")).toBe(true)
expect(t.fake.requests[1].messages.at(-1).content).toContain("dismissed")
})
test("the former todo tool still fills the panel; /plan show prints the plan with its ids; /plan clear drops it", async () => {
t = await tui([
{ chunks: [toolCall(0, "t1", "todo", JSON.stringify({ todos: [{ content: "read the code", status: "completed" }, { content: "fix the bug", status: "in_progress" }] }))] },
{ chunks: [delta({ content: "Working." })] },
])
await t.input.typeText("go")
t.input.pressEnter()
expect(await t.until("Working.")).toBe(true)
expect(t.frame()).toContain("plan 1/2 · Work")
await t.input.typeText("/plan show")
t.input.pressEnter()
expect(await t.until("[t2] [>] fix the bug")).toBe(true)
expect(t.frame()).toContain("## p1 Work (active)")
await t.input.typeText("/plan clear")
t.input.pressEnter()
expect(await t.until("plan cleared")).toBe(true)
expect(t.frame()).not.toContain("plan 1/2")
})
test("@ lists files; tab takes one; the model receives the file", async () => {
t = await tui([{ chunks: [delta({ content: "It says hello world." })] }])
await t.input.typeText("what does @hel")
@@ -306,22 +327,21 @@ test("pasting an image's path (quoted, as a file manager does) inserts an @refer
expect(t.frame()).toContain(`@${t.cwd}/my\\ shot.png`)
})
test("the plan card: the plan shown as markdown; a approves and the status bar says edit", async () => {
test("the plan card: the plan shown with its ids; a approves and the status bar says edit", async () => {
t = await tui([
{ chunks: [toolCall(0, "w1", "write", JSON.stringify({ path: ".agent/plans/2026-09-29-x.md", content: "# The plan\n\n## Changes\n- **one** thing\n" }))] },
{ chunks: [toolCall(0, "x1", "plan_submit", '{"path":".agent/plans/2026-09-29-x.md"}')] },
{ chunks: [toolCall(0, "w1", "plan_write", JSON.stringify({ title: "The plan", summary: "Two changes.", phases: [{ title: "Changes", tasks: ["one thing", "another"] }] }))] },
{ chunks: [toolCall(0, "x1", "plan_submit", "{}")] },
{ chunks: [delta({ content: "Implementing now." })] },
])
await t.input.typeText("/plan tidy up")
t.input.pressEnter()
expect(await t.until("Approve — edit mode")).toBe(true)
expect(await t.until("one thing")).toBe(true) // markdown is parsed off-thread; it draws a moment later
const f = t.frame()
expect(f).toContain("plan · .agent/plans/2026-09-29-x.md")
expect(f).toContain("plan · The plan")
const card = f.slice(f.indexOf("╭─ plan ·"))
expect(card).toContain("The plan")
expect(card).toContain("one thing")
expect(card).not.toContain("**one**") // rendered, not raw
expect(card).toContain("Two changes.")
expect(card).toContain("p1 Changes (pending)")
expect(card).toContain("[t1] [ ] one thing")
expect(f).toContain(" plan · fake/coder")
await t.input.typeText("a")
expect(await t.until("Implementing now.")).toBe(true)