{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Workflow",
"description": "Top-level `workflow.yaml` shape.",
"type": "object",
"required": [
"events"
],
"properties": {
"budgets": {
"description": "Whole-tree spend limits (ARCH §6 \"Budgets (v0.7)\"). Checked at every model-call boundary before invoking the adapter; every value is derived at check time from on-disk `Usage` events, step timestamps, and branch depth — the harness stores no running counter (PRINCIPLES \"Single source of truth\"). Omitted → that limit is unbounded. What ships is `max_depth: 5` and nothing else (ARCH §6 \"No SPEND ships bounded; depth does\"): the two spend ceilings stay off under the 2026-08-16 operator ruling, while depth — the tree's only prohibition on growth, and not a spend judgement — is declared. Declaring a ceiling is config an operator adds and removing one deletes config, never code.",
"default": {},
"allOf": [
{
"$ref": "#/definitions/Budgets"
}
]
},
"compaction": {
"anyOf": [
{
"$ref": "#/definitions/CompactionConfig"
},
{
"type": "null"
}
]
},
"context_files": {
"description": "The `context_files:` list (ARCH §3.3 *Context files ride the next tool result*, `docs/DESIGN_CONTEXT_ECONOMY.md` §6): file **names** looked for in every directory on the path from the enclosing repository's top level down to the agent's working directory, each carried once on a tool result. Empty — the block omitted — discovers nothing, which is the general path with the policy absent; the shipped list is `template/workflow.yaml`'s.",
"type": "array",
"items": {
"type": "string"
}
},
"events": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"retry": {
"description": "Retry policy for a step's model call (ARCH §2.10, §4.4): the harness owns the retry loop (brazen never retries), reading the attempt cap and backoff from here. Omitted uses [`RetryConfig:: default`].",
"default": {
"backoff": "exponential",
"max_attempts": 3
},
"allOf": [
{
"$ref": "#/definitions/RetryConfig"
}
]
},
"tool_control": {
"description": "The tool-control seam (ARCH §3.3 *Tool control*, §6): the adjudicator consulted before every granted tool invocation executes — pass, refuse, or hold. Omitted → no control is consulted and the tool window is unchanged; no control is shipped ([`ToolControl`]).",
"anyOf": [
{
"$ref": "#/definitions/ToolControl"
},
{
"type": "null"
}
]
},
"tool_output": {
"description": "Per-stream byte bound on the transcript projection of a tool result (ARCH §3.3 *Bounded transcript projection*, bl-d5fa). Omitted → tool output reaches the transcript unbounded; the shipped default lives in `template/workflow.yaml` ([`ToolOutputBound`]).",
"anyOf": [
{
"$ref": "#/definitions/ToolOutputBound"
},
{
"type": "null"
}
]
}
},
"definitions": {
"Backoff": {
"description": "Closed set of backoff policies between retry attempts (ARCH §6). Exponential is the shipped policy; adding another is a code change, like every other closed workflow set.",
"type": "string",
"enum": [
"exponential"
]
},
"Budgets": {
"description": "Whole-tree spend limits (ARCH §6 `budgets:` block, v0.7). Each limit is optional; an omitted limit is unbounded. All three are derived live from disk at check time — never stored — and are a whole-tree ceiling: any driver (root or subagent) checks the tree's total spend against the single frozen limit, with no per-dispatch inheritance (ARCH §6; the tree shares one `steps/` per §2.2/§2.3).",
"type": "object",
"properties": {
"max_depth": {
"description": "The deepest *allowed* dispatch depth (root agent = 0; each dispatch is one deeper). An agent deeper than this exhausts on its first model call, and a dispatch that would land a child deeper is refused before the fork (ARCH §6 \"The depth boundary\").",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"max_total_tokens": {
"description": "Cap on tokens summed across *every* attempt segment of every step's `response.json` in the conversation tree — failed and superseded attempts are billed too (ARCH §6/§8; the last-segment-authoritative rule governs *context*, not billing).",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
},
"max_wall_seconds": {
"description": "Cap on wall-clock seconds summed per step from `meta.json`'s `started_at`→`ended_at`; each span already includes the backoff sleeps between that step's attempts (ARCH §6 \"wall is wall\", §2.10).",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
}
}
},
"CompactionConfig": {
"description": "Optional `compaction:` block (ARCH §6).",
"type": "object",
"required": [
"intermediate"
],
"properties": {
"intermediate": {
"$ref": "#/definitions/IntermediateCompaction"
}
}
},
"CompactionTrigger": {
"description": "Closed set of intermediate-compaction triggers (ARCH §6).",
"type": "string",
"enum": [
"every_n_commits",
"every_t_seconds",
"on_flush",
"window_percent"
]
},
"IntermediateCompaction": {
"description": "Configuration for intermediate compaction triggers.",
"type": "object",
"required": [
"trigger"
],
"properties": {
"extract_bytes": {
"description": "Byte cap on the landing's **extract** — `summary/NNN.refs.md`, the deterministic compaction product code derives from what the compaction removes from context (ARCH §2.7, `docs/DESIGN_CONTEXT_ECONOMY.md` §5.3). Omitted → no extract is written, severable like `tool_output:`. Bytes, never tokens: the extract is a file in the tree, so the bound is stated in the unit the tree has.",
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
},
"keep_recent": {
"description": "Most recent commits kept out of the compaction span (ARCH §2.6): the compactor forks off `HEAD~keep_recent` — the compaction point — so the retained tail survives verbatim and replays on top of the landing. Omitted → `0`, the point is the tip. Must stay below `n` under `every_n_commits` (validated), else every landing re-arms.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"keep_recent_tokens": {
"description": "The retained tail as **provider-reported prompt tokens** rather than commits (`docs/DESIGN_CONTEXT_ECONOMY.md` §5.2): the compaction point is the oldest model-entry commit whose usage leaves the stretch above it costing at most `n` prompt tokens to append, in the provider's own count — no tokenizer, no stored counter. One tail or the other, never both (validated).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"n": {
"description": "Required when `trigger == every_n_commits` (commit count), `every_t_seconds` (seconds) or `window_percent` (percent of the model's context window, `1..=100`). Ignored for `on_flush`.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"trigger": {
"$ref": "#/definitions/CompactionTrigger"
}
}
},
"RetryConfig": {
"description": "Harness-owned retry policy (ARCH §6 `retry:` block). One `bz` process per attempt (§4.4); a retryable in-band `Error` re-invokes `bz` with the identical request up to `max_attempts`, sleeping the backoff between attempts (§2.10).",
"type": "object",
"required": [
"backoff",
"max_attempts"
],
"properties": {
"backoff": {
"$ref": "#/definitions/Backoff"
},
"max_attempts": {
"description": "Attempt cap per model call — `1` disables retry (a single attempt). Each attempt is one `bz` invocation appending one segment to `response.json` (§4.4).",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
},
"ToolControl": {
"description": "The `tool_control:` block: the control binary the seam consults.",
"type": "object",
"required": [
"command"
],
"properties": {
"command": {
"description": "The control executable — an absolute path or a name resolved on `PATH` at spawn (the §4.2 `adapter:` override idiom: config names a binary, the OS resolves it). Invoked with no arguments; the invocation under adjudication arrives as JSON on stdin (§3.3 *Tool control*). Must be non-empty (validated at load).",
"type": "string"
}
},
"additionalProperties": false
},
"ToolOutputBound": {
"description": "The `tool_output:` block. Both fields are byte counts — litany has no tokenizer, so bytes are the only honest unit (§3.3). `Copy` because it is two words of policy handed down the executor path by value.",
"type": "object",
"required": [
"head_bytes",
"tail_bytes"
],
"properties": {
"head_bytes": {
"description": "Bytes kept from the start of each stream — the command banner, the part that says what ran.",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"tail_bytes": {
"description": "Bytes kept from the end of each stream — the failure tail, the part that says how it ended.",
"type": "integer",
"format": "uint",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}