Harness spec 2.0.0: background work is a job — commands and subagents alike, seen and steered

One registry per process, every job owned by a session: bash's background commands (and a command
still running at its timeout, which now goes to the background instead of being killed) and
subagents started with task background: true. job_list, job_output, job_stop (the former
bash_list, bash_output, bash_kill) and the new job_send and report; a finished job wakes the owning
session in every front end with its marked note. The TUI counts jobs, lists them (/jobs), opens a
command's output, enters a subagent and messages it, moves a command to the background (ctrl+b)
and asks before quitting with jobs running. The link gains the jobs capability. Subagents get a
session row of their own, the spec's preamble and agent texts, and their errors reach the report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Fable 5.1 committed 2026-10-20 16:17:00 +00:00
1 parent aaa3e3a44f
commit 2183040db8
75 files changed
+3614 -437

No files matched your search

+7 -2
View File
@@ -8,7 +8,7 @@ import { ConfigError, loadConfig } from "./config/load.ts"
import { asMode, EFFORTS, MODES, type Effort, type Mode } from "./config/schema.ts"
import { isLocal, localRoot } from "./config/paths.ts"
import { SettingError } from "./config/settings.ts"
import { renderJson, renderPlain, terminalAsker } from "./headless.ts"
import { renderJson, renderPlain, settleJobs, terminalAsker } from "./headless.ts"
import { enterLocal, localFor, LocalError, localRefusal, localTmp, LOCAL_FLAGS } from "./project/local.ts"
import { findProject, setTrust, trustOf } from "./project/root.ts"
import { effortsFor } from "./provider/effort.ts"
@@ -86,6 +86,7 @@ Options for run:
--json events as JSON lines
--reasoning show the model's reasoning
--no-store do not record the session
--no-wait do not wait for background jobs the prompt started; stop them
`
function fail(msg: string, code = 1): never {
@@ -264,6 +265,7 @@ async function main() {
json: { type: "boolean" },
reasoning: { type: "boolean" },
"no-store": { type: "boolean" },
"no-wait": { type: "boolean" },
},
})
let prompt = positionals.join(" ").trim()
@@ -291,7 +293,10 @@ async function main() {
// connect timeout; a failure is a notice, not a stop).
await app.mcpReady
const atts = attachmentsFor(prompt, app.engine.o.toolCtx, app.engine.model.spec.vision === true)
const reason = await app.engine.prompt(prompt, atts.flatMap((a) => (a.image ? [a.text, a.image] : [a.text])))
// Through the turns, like every front end: a job's note wakes the session the same way.
const reason = await app.turns.prompt(prompt, atts.flatMap((a) => (a.image ? [a.text, a.image] : [a.text])))
// Jobs the prompt started: their notes come back as turns of their own, unless told not to wait.
await settleJobs(app, !values["no-wait"])
// Refused before the prompt was written (capacity, a cancel at once): no empty session is left
// to crowd `lembas sessions`. Never shared, so nobody else is told.
app.discardIfEmpty()