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

+21 -4
View File
@@ -17,7 +17,7 @@ export type Event =
* `turnId`: the turn it starts, the web UI's message id or one made here, so a client
* matches the turn to its own row by id rather than by timing. `attachments`: what went with it
* (pasted images, @files, a web upload), for chips beside the text. */
| { type: "prompt"; text: string; turnId?: string; attachments?: AttachmentInfo[] }
| { type: "prompt"; text: string; turnId?: string; attachments?: AttachmentInfo[]; /** The harness started the turn, not the person: a job's note (jobs/note.ts), beginning with its mark. */ machine?: true }
| { type: "step"; n: number }
/** The session was named (or renamed). */
| { type: "title"; id: string; title: string }
@@ -54,8 +54,14 @@ export type Event =
| { type: "plan"; plan: Plan | undefined }
/** The conversation was replaced by a summary of it (/compact, or automatically). */
| { type: "compacted"; summary: string }
/** A subagent's tool call finished (shown under the task line). */
| { type: "sub_tool"; callId: string; name: string; title: string; isError?: boolean }
/** A subagent's tool call finished (shown under the task line); `jobId`: its job, when it runs as one. */
| { type: "sub_tool"; callId: string; jobId?: string; name: string; title: string; isError?: boolean }
/** A background job of the session began, has news (new output, a subagent's step — at most four
* a second per job), or ended (jobs/registry.ts). */
| { type: "job"; state: "start" | "update" | "end"; job: import("../jobs/registry.ts").JobInfo }
/** The note about that job was given to the model: inside a turn at its next step, or as the
* `machine` turn whose `prompt` follows. */
| { type: "job_note"; jobId: string; text: string }
/** The permission mode changed from inside a turn (plan approved). */
| { type: "mode"; mode: Mode }
/** The session's model or effort changed, for whatever reason — /model, /effort, the settings
@@ -108,7 +114,18 @@ export type AskReply =
/** Whoever answers permission questions: the TUI's dialog, a y/n prompt, or a policy in headless runs. */
export interface Asker {
/** `preview`: the change the call would make (a unified diff), or why it could not be worked out. */
ask(req: { tool: string; args: unknown; request: PermissionRequest; decision: Decision; preview?: { diff?: string; error?: string }; /** What the model says the call is for. */ purpose?: string }): Promise<AskReply>
ask(req: {
tool: string
args: unknown
request: PermissionRequest
decision: Decision
preview?: { diff?: string; error?: string }
/** What the model says the call is for. */
purpose?: string
/** Set when a subagent running as a background job asks, so the card can say which — it may
* ask while the session itself is idle. */
source?: { jobId: string; agent: string; description: string }
}): Promise<AskReply>
/** The ask_user tool. Absent: the tool reports that nobody can be asked. `callId`: the tool call's
* id, which keys the card on another screen. */
question?(req: QuestionRequest, callId?: string): Promise<QuestionReply>