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

+1 -1
View File
@@ -45,7 +45,7 @@ export function FirstRun(p: { project: Project; onDone: (r: FirstRunResult) => v
return (
<box flexDirection="column" width="100%" height="100%" paddingLeft={2} paddingTop={1}>
<text fg={c().title} attributes={TextAttributes.BOLD}>
☤ {theme().branding.agentName} — first time here
{theme().branding.agentName} — first time here
</text>
<text fg={c().text} wrapMode="word">
{p.project.root}
+36 -4
View File
@@ -5,6 +5,16 @@ import { estimateTokens } from "../../provider/tokens.ts"
import { useTui } from "../context.ts"
import type { Item } from "../state.ts"
import { clipDiff } from "../diffclip.ts"
import { TOOL_SPECS } from "../../harness.ts"
import { resolveCall } from "../../tool/names.ts"
import { splitPrototypes } from "../../prototype/fence.ts"
/** A call the transcript does not draw: a plan change (the panel above the prompt follows it) or a
* tool the spec marks hidden — under its former name too (a stored session's `todo`). */
const undrawn = (name: string) => {
const block = TOOL_SPECS[resolveCall(name, {}).name]?.block
return block === "plan" || block === "hidden"
}
const FILETYPES: Record<string, string> = { ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", mjs: "javascript", md: "markdown", zig: "zig" }
export const filetypeOf = (diff: string) => FILETYPES[/^\+\+\+ \S*\.(\w+)/m.exec(diff)?.[1] ?? ""]
@@ -14,7 +24,7 @@ function argSummary(args: unknown): string {
if (!args || typeof args !== "object") return ""
const a = args as Record<string, unknown>
if (Array.isArray(a.questions)) return (a.questions as { header?: string }[]).map((q) => q.header).filter(Boolean).join(" · ") || "asking"
if (Array.isArray(a.todos)) return `${a.todos.length} item${a.todos.length === 1 ? "" : "s"}`
if (Array.isArray(a.phases)) return String(a.title ?? "").slice(0, 120)
if (typeof a.patch === "string") return [...a.patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)].map((m) => m[1]).join(", ")
const v = a.description ?? a.command ?? a.path ?? a.pattern ?? a.id ?? ""
return String(v).split("\n")[0]!.slice(0, 120)
@@ -38,11 +48,33 @@ function UserMessage(p: { item: Extract<Item, { kind: "user" }> }) {
)
}
/** A reply as markdown — except a completed prototype fence, which is saved as a file
* (state.ts) and drawn as one line saying so in its place; while it still streams it is the
* code block it is so far. */
function AssistantMessage(p: { item: Extract<Item, { kind: "assistant" }> }) {
const { theme, md } = useTui()
const segments = () => (p.item.text.includes("```") || p.item.text.includes("~~~") ? splitPrototypes(p.item.text) : [])
const whole = () => !segments().some((s) => s.kind === "prototype")
return (
<box paddingLeft={2} marginTop={1}>
<markdown content={p.item.text.trim()} syntaxStyle={md()} streaming={p.item.streaming} fg={theme().colors.text} conceal />
<box paddingLeft={2} marginTop={1} flexDirection="column">
<Show when={!whole()} fallback={<markdown content={p.item.text.trim()} syntaxStyle={md()} streaming={p.item.streaming} fg={theme().colors.text} conceal />}>
<For each={segments()}>
{(seg, i) => {
const saved = () => (seg.kind === "prototype" && seg.closed ? p.item.prototypes?.[seg.start] : undefined)
const last = () => i() === segments().length - 1
return (
<Show
when={saved()}
fallback={<markdown content={(seg.kind === "text" ? seg.text : seg.raw).trim()} syntaxStyle={md()} streaming={p.item.streaming && last()} fg={theme().colors.text} conceal />}
>
<text fg={theme().colors.muted} wrapMode="none" truncate>
{`prototype · ${saved()!.title || saved()!.kind} · saved to ${saved()!.path}`}
</text>
</Show>
)
}}
</For>
</Show>
</box>
)
}
@@ -229,7 +261,7 @@ export function Messages() {
<Match when={item.kind === "reasoning"}>
<Reasoning item={item as Extract<Item, { kind: "reasoning" }>} />
</Match>
<Match when={item.kind === "tool"}>
<Match when={item.kind === "tool" && !undrawn((item as Extract<Item, { kind: "tool" }>).name)}>
<ToolLine item={item as Extract<Item, { kind: "tool" }>} />
</Match>
<Match when={item.kind === "notice"}>
+30
View File
@@ -0,0 +1,30 @@
import { For, Show } from "solid-js"
import { open, summary, type TaskStatus } from "../../plan/plan.ts"
import { useTui } from "../context.ts"
const MARK: Record<TaskStatus, string> = { todo: "☐", doing: "▸", done: "☑", dropped: "✗" }
/** The session's plan, above the prompt while anything on it is still to do: the active phase
* and the count, the task being done, what is next, the last two done — at most six lines. */
export function PlanPanel() {
const { theme, view } = useTui()
const c = () => theme().colors
const digest = () => (view.state.livePlan ? summary(view.state.livePlan) : undefined)
return (
<Show when={view.state.livePlan && open(view.state.livePlan)}>
<box flexDirection="column" paddingLeft={1}>
<text fg={c().label} wrapMode="none" truncate>
{digest()?.head}
</text>
<For each={digest()?.lines ?? []}>
{(t) => (
<text wrapMode="none" truncate fg={t.status === "doing" ? c().accent : t.status === "done" ? c().dim : c().text}>
{" "}
{MARK[t.status]} {t.text}
</text>
)}
</For>
</box>
</Show>
)
}
+22 -7
View File
@@ -1,12 +1,25 @@
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import { createSignal, For, Show } from "solid-js"
import type { PlanReply } from "../../tool/plan_exit.ts"
import { TASK_MARK, type Plan } from "../../plan/plan.ts"
import type { PlanReply, PlanRequest } from "../../tool/plan_submit.ts"
import { useTui } from "../context.ts"
export interface PendingPlan {
path: string
text: string
/** The plan as the card draws it: render()'s lines, each with what it is, so the ids and marks
* stay on screen (a markdown box would read `[t1]` as a link and conceal it). */
export function planLines(plan: Plan): { kind: "title" | "text" | "phase" | "todo" | "doing" | "done" | "dropped"; text: string }[] {
const out: ReturnType<typeof planLines> = [{ kind: "title", text: plan.title }]
if (plan.summary) out.push({ kind: "text", text: plan.summary })
if (plan.findings.length) out.push({ kind: "text", text: `Findings: ${plan.findings.map((f) => `[${f.id}] ${f.text}`).join("; ")}` })
if (plan.objectives.length) out.push({ kind: "text", text: `Objectives: ${plan.objectives.map((o) => `[${o.id}] ${o.text} (${o.status})`).join("; ")}` })
for (const ph of plan.phases) {
out.push({ kind: "phase", text: `${ph.id} ${ph.title} (${ph.status})` })
for (const t of ph.tasks) out.push({ kind: t.status, text: `[${t.id}] ${TASK_MARK[t.status]} ${t.text}${t.note ? ` — ${t.note}` : ""}` })
}
return out
}
export interface PendingPlan extends PlanRequest {
resolve: (r: PlanReply) => void
}
@@ -19,7 +32,7 @@ const CHOICES = [
/** plan_submit's card: the plan itself (scroll with pgup/pgdn), then a | m | r | ?; esc keeps planning. */
export function PlanCard(p: { pending: PendingPlan }) {
const { theme, md, dims } = useTui()
const { theme, dims } = useTui()
const c = () => theme().colors
const [sel, setSel] = createSignal(0)
const [typing, setTyping] = createSignal<"revise" | "back">()
@@ -54,9 +67,11 @@ export function PlanCard(p: { pending: PendingPlan }) {
})
return (
<box border borderStyle="rounded" borderColor={c().info} flexDirection="column" paddingLeft={1} paddingRight={1} title={` plan · ${p.pending.path} `} titleColor={c().info}>
<box border borderStyle="rounded" borderColor={c().info} flexDirection="column" paddingLeft={1} paddingRight={1} title={` plan · ${p.pending.plan.title} `} titleColor={c().info}>
<scrollbox ref={(r: ScrollBoxRenderable) => (box = r)} maxHeight={Math.max(6, Math.floor(dims().height / 2))} scrollX={false}>
<markdown content={p.pending.text} syntaxStyle={md()} fg={c().text} conceal />
<box flexDirection="column">
<For each={planLines(p.pending.plan)}>{(l) => <text fg={l.kind === "title" ? c().title : l.kind === "phase" ? c().accent : l.kind === "doing" ? c().accent : l.kind === "done" || l.kind === "dropped" ? c().dim : c().text} attributes={l.kind === "title" || l.kind === "phase" ? TextAttributes.BOLD : 0} wrapMode="word">{l.text}</text>}</For>
</box>
</scrollbox>
<Show
when={!typing()}
+1 -1
View File
@@ -1,7 +1,7 @@
import { TextAttributes } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import { createSignal, For, Show } from "solid-js"
import type { Answer } from "../../tool/question.ts"
import type { Answer } from "../../tool/ask_user.ts"
import { useTui } from "../context.ts"
import type { PendingQuestion } from "../state.ts"
+1 -1
View File
@@ -65,7 +65,7 @@ export function Setup(p: { error: Error; onRetry: () => void; onQuit: () => void
return (
<box flexDirection="column" width="100%" height="100%" paddingLeft={2} paddingTop={1} paddingRight={2} gap={1}>
<text fg={c().title} attributes={TextAttributes.BOLD}>
☤ {theme().branding.agentName} — {noModel() ? "no model to talk to yet" : "the configuration has a mistake"}
{theme().branding.agentName} — {noModel() ? "no model to talk to yet" : "the configuration has a mistake"}
</text>
{/* "No model configured" is what the rest of the screen explains; anything else (a model
that does not exist, a file that does not parse) is shown as it is. */}
+3 -5
View File
@@ -16,9 +16,9 @@ export function reading(p: { total: number; done: number; processed: number; ms:
return parts.join(" · ")
}
// Hermes' kawaii spinner: a braille frame, a face that changes every few seconds, what is going on
// (a verb while it thinks), and the whole task's time and output tokens — from the prompt until
// the last reply of the task, subagents included.
// The working line: a braille frame, what is going on (a verb while it thinks), and the whole
// task's time and output tokens — from the prompt until the last reply of the task, subagents
// included.
export function Spinner() {
const { theme, view } = useTui()
const [tick, setTick] = createSignal(0)
@@ -41,12 +41,10 @@ export function Spinner() {
if (s.phase === "writing") return "writing"
return "waiting for the model"
}
const face = () => (view.state.phase === "thinking" ? pick(sp().thinking, 30) : pick(sp().waiting, 30))
return (
<box paddingLeft={1} height={1}>
<text wrapMode="none" truncate>
<span style={{ fg: theme().colors.accent }}>{pick(sp().frames, 1)} </span>
<span style={{ fg: theme().colors.label }}>{face()} </span>
<span style={{ fg: theme().colors.muted }}>{what()}… </span>
<span style={{ fg: theme().colors.dim }}>
{elapsed()} · {tokens()} · esc to stop
-35
View File
@@ -1,35 +0,0 @@
import { For, Show } from "solid-js"
import { useTui } from "../context.ts"
const MARK = { pending: "☐", in_progress: "▸", completed: "☑", cancelled: "✗" } as const
/** The todo tool's list, above the prompt while anything on it is still open. */
export function Todos() {
const { theme, view } = useTui()
const c = () => theme().colors
const open = () => view.state.todos.some((t) => t.status === "pending" || t.status === "in_progress")
const done = () => view.state.todos.filter((t) => t.status === "completed").length
// The item in progress, what is left, then the latest done — at most six lines.
const shown = () => {
const t = view.state.todos
const order = [...t.filter((x) => x.status === "in_progress"), ...t.filter((x) => x.status === "pending"), ...t.filter((x) => x.status === "completed").slice(-2)]
return order.slice(0, 6)
}
return (
<Show when={open()}>
<box flexDirection="column" paddingLeft={1}>
<text fg={c().label}>
todo {done()}/{view.state.todos.length}
</text>
<For each={shown()}>
{(t) => (
<text wrapMode="none" truncate fg={t.status === "in_progress" ? c().accent : t.status === "completed" ? c().dim : c().text}>
{" "}
{MARK[t.status]} {t.content}
</text>
)}
</For>
</box>
</Show>
)
}
+1 -1
View File
@@ -9,7 +9,7 @@ export function Welcome() {
return (
<box border borderStyle="rounded" borderColor={c().border} flexDirection="column" paddingLeft={1} paddingRight={1} marginTop={1} flexShrink={0}>
<text fg={c().title} attributes={TextAttributes.BOLD}>
☤ {theme().branding.agentName} <span style={{ fg: c().dim, attributes: 0 }}>{VERSION}</span>
{theme().branding.agentName} <span style={{ fg: c().dim, attributes: 0 }}>{VERSION}</span>
</text>
<text fg={c().text} wrapMode="none" truncate>
<span style={{ fg: c().label }}>model </span>