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

+56 -5
View File
@@ -16,7 +16,7 @@ import { hardlineRules } from "./permission/hardline.ts"
import { defaultEffort } from "./provider/effort.ts"
import { clientFor, familyOf, resolveModel } from "./provider/index.ts"
import { discoverContext, unload } from "./provider/discover.ts"
import { assembleSystem, instructionFiles } from "./prompt/assemble.ts"
import { assembleSystem, instructionFiles, knowledgeScope } from "./prompt/assemble.ts"
import { describeRepo, repoState } from "./git/repo.ts"
import { findProject, persistProjectRule, trustOf, type Project } from "./project/root.ts"
import type { Message } from "./provider/types.ts"
@@ -38,6 +38,7 @@ import { Settings } from "./settings.ts"
import { Library, type Embedder } from "./library/store.ts"
import { instances as lembasInstances, keyFile as lembasKeyFile } from "./lembas/login.ts"
import { PERSONAL_KEYS, personalConfig, pushPersonalization } from "./lembas/personal.ts"
import type { Plan } from "./plan/plan.ts"
import { LIBRARY_SERVER } from "./mcp/index.ts"
import type { McpServer } from "./config/schema.ts"
import { embedderFor } from "./library/embed.ts"
@@ -72,6 +73,21 @@ const UNATTENDED: Record<Mode, string | undefined> = {
auto: undefined,
}
/** What only a client knows about a session (harness/acp.md, `Context` in `session/new` and
* `session/prompt`'s `_meta.lembas`): the pieces of the prompt an instance holds, read into the
* prompt this device composes from the spec's texts. Each is absent where nothing was said. */
export interface SessionContext {
/** The chat's authored prompt, under `## Instructions from <instance>`. */
instructions?: string
/** The account's personalization, in place of this device's; null: the device's own. */
personalization?: { personality: string; personality_custom: string; instructions: string } | null
/** The knowledge bases this session may search (named to the model, and the scope sent on
* every library call); an empty list: the account's whole library. */
library?: { bases: { id: string; name: string }[] }
/** Whether the model may write a prototype, and what a page may load. */
prototypes?: { offered: boolean; sources: string }
}
export interface App {
bus: Bus
engine: Engine
@@ -119,6 +135,10 @@ export interface App {
embedder?: Embedder
/** Stop whatever runs: the model's reply. */
cancel(): void
/** What the client holds for this session (the link): read into every prompt from now on. */
context: SessionContext
/** Replace it (`{}`: none), naming the instance it came from for the prompt's heading. */
setContext(ctx: SessionContext, from?: string): void
}
/** This machine's library tools, which `library: lembas` replaces with the instance's. */
@@ -243,6 +263,9 @@ export function createApp(o: AppOptions): App {
loaded.warnings.push((e as Error).message)
}
const libraryCtx = { lib: library, project: project.root, bases: loaded.config.knowledge, embedder }
// The client's context for this session (setContext), and whose it is.
let context: SessionContext = {}
let contextFrom = "the instance"
// library: lembas — the account's library through the instance's /mcp, in place of this
// machine's. Needs the one instance logged in to, with a token that has the library scope.
@@ -279,7 +302,20 @@ export function createApp(o: AppOptions): App {
sessions: store ? { store, current: () => engine.sessionId } : undefined,
settings: { get: (k: string) => settings.get(k), list: () => settings.list(), set: (k: string, v: unknown, sc?: "session" | "global" | "project") => settings.set(k, v, sc), models: () => modelRefs() },
})
const system = (m: Mode, current: ResolvedModel, subagent?: { name: string; instructions: string }, at?: { cwd: string; root: string }) =>
/** Whether knowledge_search is offered (engine.offered's rule): a base with documents in scope,
* or the instance's library standing in for this machine's. */
const knowledgeOffered = () => Boolean(lembasLibrary) || library.bases().some((b) => b.documents > 0 && (!libraryCtx.bases || libraryCtx.bases.includes(b.name)))
/** The library block's scope: the client's bases over the link, else this machine's `knowledge:`. */
const libraryBlock = () => {
if (!knowledgeOffered()) return undefined
const names = lembasLibrary ? context.library?.bases.map((b) => b.name) : libraryCtx.bases
return { scope: knowledgeScope(names) }
}
/** Prototypes: offered by the client for this session, or by `prototypes: true` for a terminal's. */
const prototypesBlock = () => (context.prototypes?.offered || (!context.prototypes && loaded.config.prototypes === true) ? { sources: context.prototypes?.sources ?? "" } : undefined)
/** The three personal keys this session uses: the client's for it, else the config's. */
const personal = () => (context.personalization ? personalConfig({ enabled: true, available: true, ...context.personalization }) : loaded.config)
const system = (m: Mode, current: ResolvedModel, subagent?: { name: string; instructions: string }, at?: { cwd: string; root: string }, plan?: Plan) =>
assembleSystem({
modelRef: current.ref,
family: familyOf(current),
@@ -300,9 +336,13 @@ export function createApp(o: AppOptions): App {
skills: frozen.skills,
manageSkills: true,
// Read on every prompt: /personality and /settings change it without a restart.
...personalityOf(loaded.config),
...personalityOf(personal()),
mcp: mcp.instructions(),
roster: roster(current.ref),
plan,
...(context.instructions ? { instance: { name: contextFrom, instructions: context.instructions } } : {}),
library: libraryBlock(),
prototypes: prototypesBlock(),
})
// The other configured models, for the task tool's `model`, or a switch.
const roster = (current: string) =>
@@ -394,7 +434,7 @@ export function createApp(o: AppOptions): App {
compaction,
perm: childPerm,
toolCtx: at ? { ...baseToolCtx(), root: at.root, cwd: at.cwd, projectDir: undefined } : baseToolCtx(),
system: (mode, current) =>
system: (mode, current, plan) =>
system(
mode,
current,
@@ -405,6 +445,7 @@ export function createApp(o: AppOptions): App {
: spec.instructions,
},
at,
plan,
),
})
const onAbort = () => child.cancel()
@@ -438,7 +479,7 @@ export function createApp(o: AppOptions): App {
compaction,
perm,
toolCtx: baseToolCtx(),
system: (m, current) => system(m, current),
system: (m, current, plan) => system(m, current, undefined, undefined, plan),
persistRule: trusted ? (rule) => persistProjectRule(project, rule) : undefined,
spawn,
fallback(current, tried) {
@@ -634,6 +675,16 @@ export function createApp(o: AppOptions): App {
cancel() {
engine.cancel()
},
get context() {
return context
},
setContext(ctx, from) {
context = ctx
if (from) contextFrom = from
// The instance library answers within the chat's scope: the session's id rides on every
// call to it while the client has said what the session may search.
engine.o.toolCtx.libraryScope = ctx.library ? engine.sessionId : undefined
},
rename(title) {
const id = engine.sessionId
if (!id || !title.trim()) return