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

+22 -4
View File
@@ -12,9 +12,10 @@ import type { AskReply, Asker, AttachmentInfo, Event } from "../bus/index.ts"
import { asMode, type Mode } from "../config/schema.ts"
import type { ImagePart } from "../provider/types.ts"
import { commandSet, sourcesOf } from "../session/commands.ts"
import type { PlanReply } from "../tool/plan_exit.ts"
import type { QuestionReply, QuestionRequest } from "../tool/question.ts"
import type { PlanReply, PlanRequest } from "../tool/plan_submit.ts"
import type { QuestionReply, QuestionRequest } from "../tool/ask_user.ts"
import { clampMode, effortOf, markRemote, permissionParams, planParams, planReplyFrom, questionParams, questionReplyFrom, replyFrom, STOP, type PromptResult } from "./agent.ts"
import type { SessionContext } from "../app.ts"
import { sessionTurnFile, sessionTurns, type TurnLog } from "./turns.ts"
import { dropAttachments, preparePrompt } from "./prompt.ts"
import { connectHub, Hub } from "./hub.ts"
@@ -60,6 +61,8 @@ export interface ShareHooks {
deleted(): void
/** Shared or not, mode, model or effort changed: redraw. */
changed(): void
/** The web UI rewound the session (`_lembas/session/rewind`): the conversation is shorter now. */
rewound?(): void
}
let current: Share | undefined
@@ -259,7 +262,7 @@ export class Share {
}
/** plan_submit in the shared session, likewise. */
plan(req: { path: string; text: string }, callId: string | undefined, local: LocalAsk<PlanReply>): Promise<PlanReply> {
plan(req: PlanRequest, callId: string | undefined, local: LocalAsk<PlanReply>): Promise<PlanReply> {
return this.card(local, "hub/plan", (id) => planParams(id, req, callId, { patient: true }), (a) => planReplyFrom(a), (r) => (r.kind === "approve" ? `approved (${r.mode})` : r.kind === "revise" ? "to be revised" : "not decided"))
}
@@ -465,6 +468,9 @@ export class Share {
const turnId = typeof p?.turnId === "string" && p.turnId ? p.turnId : crypto.randomUUID()
// A turn id taken already: the original's answer.
if (this.turns.has(turnId)) return this.turns.run(turnId, () => Promise.reject(new Error("unreachable")))
// What the client holds for the session (its instructions, personalization, library scope,
// prototypes), passed on by the hub: read into the prompt here as the service would.
if (p?.context && typeof p.context === "object") this.app.setContext(p.context as SessionContext, typeof p?.instance === "string" ? p.instance : undefined)
// The blocks, expanded here as this terminal's keyboard would; `text` from a hub of
// an earlier build.
const blocks = Array.isArray(p?.prompt) ? p.prompt : [{ type: "text", text: String(p?.text ?? "") }]
@@ -495,7 +501,19 @@ export class Share {
// The terminal's turns: `_lembas/session/status` for a shared session asks here.
peer.handle("hub/status", (p) => {
this.mine(p)
return { ...(this.turns.running ? { turnId: this.turns.running } : {}), queued: this.turns.queued(), ...(this.turns.last ? { lastTurnId: this.turns.last } : {}) }
return { ...(this.turns.running ? { turnId: this.turns.running } : {}), queued: this.turns.queued(), ...(this.turns.last ? { lastTurnId: this.turns.last } : {}), plan: this.app.engine.plan ?? null }
})
// The web UI is editing or regenerating from a turn: that turn and the later ones go, as /undo
// drops one — the files stay.
peer.handle("hub/rewind", (p) => {
this.mine(p)
if (this.app.turns.current) throw new RpcError(INVALID_PARAMS, "this session is working — rewind it when the reply is done")
const turnId = String(p?.turnId ?? "")
const reverted = this.app.turns.rewind(turnId)
if (reverted === undefined) throw new RpcError(INVALID_PARAMS, `this session has no turn ${turnId}`)
this.hooks.rewound?.()
this.hooks.notice(`the web UI rewound ${reverted} turn${reverted === 1 ? "" : "s"} — the files they changed stay as they are`)
return { reverted }
})
peer.handle("hub/steer", (p) => {
this.mine(p)