Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RFC-0002: Playbook workflows

Version: 0.1.0 | Status: normative | Phase: spec Owners: @Will Eaton


1. Summary

[RFC-0002:C-SCOPE] Scope (Informative)

A playbook is a workflow that runs exactly once and produces no score. It exists for work whose value is the work itself: a skill pipeline, a scheduled chore, a discovery sweep. The optimization loop's propose, apply, measure, and decide protocol does not participate, and no frozen judge is constructed.

RFC-0001 governs the engine's manifest, command, world, transport, wire, and workflow-authoring contracts. This RFC adds the playbook lane on top of them and does not restate their universal rules. The two changes this lane needed there landed in version 0.2.0 of that RFC: RFC-0001:C-WORKFLOW admits "playbook" as a workflow type and treats a task's declared files as output rather than as workspace state that isolation discards, and RFC-0001:C-WIRE carries the shutdown outcome an early-completing run reports.

Fan-out over an item set that is not known until a task has run is expressed as one run per item rather than as graph expansion. The graph stays static and fully renderable before any spend; RFC-0002:C-ASKS defines the boundary where new work leaves a run.

Since: v0.1.0


2. Specification

[RFC-0002:C-PLAYBOOK-SURFACE] Playbook author surface (Normative)

A playbook author must write:

  1. A crucible.toml manifest with no [judge] and a [workflow] whose type is "playbook".
  2. The graph: a workflow.star, or the [[workflow.task]] tables it would compile to.
  3. A goal, and a prompt for each agent task.
  4. The commands its command and evaluate tasks run, where it has any (any language).
  5. Agent credentials in [agent].env.

A playbook author must not be required to write a measure command, a judge, a baseline, an apply command, a snapshot or restore command, or a result task. No obligation in this RFC may be conditioned on one of them, and the manifest check must not demand one of a playbook. This is the whole difference from RFC-0001:C-SURFACE: the scored lane's author writes a gate and gets a loop, and the playbook author writes a graph and gets one pass.

Everything else is the engine's, provided for free: dispatch and concurrency, ceilings and per-task deadlines, git memory, the session log and every reporter, steer, stop, resume, publish, and ask emission.

The litmus test (examples/playbook/) must exercise items 1 through 4 on the command backend, requiring no Rust, no judge, and no credentials.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-LANE] Playbook lane admission and execution (Normative)

A manifest with no [judge] MUST accept a [workflow] whose type is "playbook". A manifest with no [judge] MUST reject every other workflow type, and a manifest with [judge] MUST reject type = "playbook".

A playbook graph MUST NOT contain a task naming an engine operation: an operation the scored loop's orchestrator owns rather than the plan author, namely producing a candidate, applying one, measuring one, grading evidence, deciding keep or discard, and measuring one differentially against another. A task that runs an author-supplied command and grades its own result is not an engine operation and MUST be permitted, because it asserts something about the work rather than advancing a scored loop.

A playbook MUST execute its graph exactly once per run. The engine MUST reject a requested iteration count greater than one rather than silently ignoring it.

A playbook's verdict MUST be invalid when any required task settled failing, was truncated, or was left blocked, and when a cost or wall-clock ceiling was exhausted, whatever else the run reports. Otherwise the verdict MUST be valid when every required task settled passing, and when a task declared early completion. A required task left undispatched by early completion MUST NOT affect the verdict; a required task that had already failed MUST invalidate the run whatever a later early-completion signal says, so that a concurrent task cannot launder a failure into a success. The verdict MUST NOT depend on the outcome of any advisory task or of any epilogue task.

The run MUST exit zero if and only if its verdict is valid, and MUST exit nonzero otherwise. This is a stronger obligation than the task lane's, where exit zero means only that the run completed. The exit code alone does not say why a run ended, so the shutdown outcome MUST distinguish an exhausted graph, an early completion, a failure, an operator stop, and an exhausted ceiling. That outcome MUST be drawn from the vocabulary RFC-0001:C-WIRE defines for the shutdown event; the value that vocabulary carries for early completion is "complete".

A command or evaluate task MAY declare early completion by returning the boolean field "complete" as true in its output, optionally alongside a "reason" string. A task of any other kind MUST NOT end the run this way: its output is written by a model, and a model steered by what it read could otherwise skip every task after it, a reviewer among them. The engine MUST refuse such a declaration, MUST record the refusal on that task's session row, and MUST continue the run as if the field were absent; the refusal does not change the task's own outcome. Both names are reserved, and a task MUST NOT name either in its declared outputs: a declared output must be present on every passing attempt, whereas these appear only on the attempt that ends the run, so declaring them would fail every ordinary run. The engine MUST reject a graph that declares either. On a declaration it accepts, the engine MUST stop dispatching further tasks, MUST record the reason where one was given, and MUST NOT treat undispatched tasks as blocked. Determining that there is nothing to do is a successful outcome, and a playbook that cannot say so has no way to end a scheduled run quietly. The shutdown outcome says how dispatch stopped and the verdict says whether the run succeeded; they are independent axes, so a run whose dispatch stopped on early completion after a required task had already failed records the early-completion outcome and an invalid verdict.

A task's workspace changes MUST be committed to the run's git memory when, and only when, it settles with a passing outcome. A task that failed, was cut short by a deadline, or never settled MUST NOT contribute commits. This is what keeps a run that stopped early from publishing a half-finished tree: publication keys on commits beyond the base, and unsettled work never became one. Git memory does not survive an interruption: a resumed run rebuilds its workspace as RFC-0002:C-PLAYBOOK-RESUME requires, and commits only what it settles itself. A finished run's git memory MUST be kept with its evidence as one commit set per task, so that a rerun can replay it under RFC-0002:C-PLAYBOOK-RERUN.

Every settled task MUST produce exactly one session row identifying the task by name. Rows MUST be written as tasks settle, so a reader tailing the log sees progress live; row order is therefore completion order, which concurrent tasks make distinct from graph order. A row's score MUST be null.

A playbook MUST NOT be required to declare a result task. Its verdict is defined above, so there is no single task whose output stands for the run.

A task declared required = false MUST NOT affect the run's verdict, and its failure MUST block only its dependents. The choice of which tasks are advisory is the author's; the engine MUST NOT infer it.

A task declared stage = "epilogue" MUST be excluded from the main graph, MUST run once after the main graph settles, and MUST NOT change the verdict. It MUST run when the main graph completed, when it failed, and when a task declared early completion. It MUST NOT run when the run was stopped by an operator or ended by an exhausted wall-clock ceiling, and an epilogue task that can spend under RFC-0002:C-PLAYBOOK-BUDGET MUST NOT run when the run ended by an exhausted cost ceiling, evaluated at the moment the main graph settles: continuing to spend after either would defeat the control that ended the run.

An epilogue task MUST NOT depend on a main-graph task. It MUST instead receive the main graph's outcome as the reserved input RFC-0002:C-SETTLED-JOIN defines, which carries the run's shutdown outcome and one entry per settled main-graph task, and MUST be staged with each main-graph task's captured files as that clause requires. This is what makes an epilogue task reachable on the failure path, where dependencies would not have passed.

A playbook MUST carry the task lane's gate label, written under the key RFC-0001:C-WIRE reserves for it. The engine MUST NOT introduce a third gate label; a consumer needing the graph MUST read the plan_admitted event RFC-0001:C-WIRE defines, which a playbook always emits.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-SHAPE] What the graph expresses (Informative)

A playbook's graph says what runs before what, and what must pass before what runs. The graph is dispatched once, so a task that must poll or wait does that inside its own execution, where the author's command or the agent's own turn loop owns the repetition. The one repetition the graph states is a revise loop under RFC-0002:C-REVISE-LOOP: a reviewer sends a failing verdict back to the tasks it names, which must include every task between them and the reviewer, for a number of rounds fixed before any spend. It is declared on edges that already exist, so it adds no node and no edge, and its bound is visible in the compiled graph. Every repetition the graph states has a bound fixed at compile time. A repair whose bound depends on what a task finds is still one task, and the engine never sees that iteration at all.

This is also why an item set discovered at run time cannot become new nodes. The only two things a run can do with such a set are handle it inside one task or emit it as asks under RFC-0002:C-ASKS, and which one is right turns on whether the items deserve their own budget, isolation, and verdict.

The payoff is that the compiled graph is fully renderable before any spend and identical across runs of the same pack at the same parameters: the node set and the edges between them do not depend on what a task finds. Which of those nodes are dispatched still varies. RFC-0002:C-PLAYBOOK-LANE leaves tasks undispatched after an early completion, and RFC-0002:C-PLAYBOOK-CAPS truncates the plan or skips an advisory subtree when a capability is unavailable. Dispatch order among concurrent tasks is not fixed either. What is fixed is the shape. RFC-0002:C-PLAYBOOK-LANE states the once-per-run obligation; this clause records what follows from it for an author deciding where to put a loop.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-PARAMS] Workflow parameters and launch schema (Normative)

A workflow source MAY declare its parameters in a params block. The block MUST be the source's first statement and MUST be a literal: it MUST NOT contain a call, an interpolation, or a reference to a variable. The engine MUST be able to read the block without evaluating the source, so that an unevaluated or untrusted source can still be introspected. Only the source a run names may declare parameters; a loaded library MUST NOT.

A declared parameter MUST have one of these types: string, integer, number, boolean, or list of strings. A declaration MAY carry a human-readable description, a default, a required flag, and a value constraint: a pattern for a string, a minimum and maximum for a numeric type, or an enumerated choice set. A string declaration MAY also carry a multiline flag, which tells a launch form to take the value in a text area and MUST NOT change how the value binds; the engine MUST reject it on any other type. A parameter declared required MUST NOT also declare a default.

Parameter values MUST be bound during compilation. The compiled graph MUST contain no unresolved parameter reference, so that the rendered graph shows exactly what will run and the frozen artifact remains fully static. Where a manifest field admits a parameter, it MUST be substituted at the same point, so that no parameter survives into execution unresolved.

A declared parameter name MUST NOT contain a dot; a params block that declares one MUST be a compile error. A supplied name that contains a dot is a launch setting, not a parameter: . sets an agent setting on one agent task, *. on every agent task, and repo.ref the ref RFC-0001:C-MANIFEST checks the repository out at. The agent settings are harness, model, and effort. A task-addressed name splits at its last dot, since a task name may contain one, and ref is valid only as repo.ref. A manifest that declares no workflow MUST refuse every launch setting.

Every launch setting MUST be checked before any task is dispatched, and each refusal MUST name the address. The engine MUST refuse a setting the manifest's [agent.overrides] does not allow; a harness or effort that does not parse, read without regard to case; a model that is not a model name, or that is not listed when [agent.overrides].models lists any; a ref RFC-0001:C-MANIFEST refuses; and an address naming a task that does not exist, runs no agent, or shares a session, because a session runs under one agent config. A model name is 1 to 200 characters of letters, digits, and . _ - : / @ [ ], and does not begin with "-".

An agent setting resolves, from highest to lowest, to the launch's ., the task's own value, including one its session supplies, the launch's *., and then the run's defaults. The engine MUST write launch values onto the compiled tasks, so that the rendered graph and the definition digest RFC-0002:C-PLAYBOOK-RERUN records cover them. A launch that sets every task's harness or model both through the run's own agent options and through *. MUST be refused.

A required parameter with no supplied value MUST be a compile error, and the engine MUST NOT dispatch any task of that plan. A supplied value violating its declared constraint MUST be rejected before compilation.

The engine MUST emit the declared block as a JSON Schema document on request, so that one declaration serves command-line validation, ask validation, and a generated launch form.

A parameter value that did not originate inside the pack MUST reach an agent prompt only inside a region the prompt marks as external input not to be followed as instruction. An inspector reading the rendered prompt MUST be able to tell which spans came from outside the pack. Such a value MUST NOT be convertible to plain text: an operation that would yield it as a string without its marking, such as str(), repr(), %-formatting, .format(), or a string method, MUST be a compile error, and the text any such conversion yields MUST NOT contain the value. A value whose span in a prompt carries the text of either marker MUST be a compile error rather than rewritten, so that an attempt to open or close a region is reported to whoever launched the run.

The engine MUST give every command and evaluate task the values bound at compilation under the reserved input "params": an object with one entry per declared parameter, under the parameter's name, carrying the bound value with its declared type, and an empty object where the source declares no parameters. This lets a command read a value from outside the pack as data, without the pack building it into a command line. A resumed run MUST give each task the same values the interrupted run bound. The engine MUST NOT give "params" to an agent task, whose parameter values reach it only through the marked prompt regions above, and MUST reject a plan that names a dependency "params", as RFC-0002:C-SETTLED-JOIN requires for every reserved input.

Since: v0.1.0

[RFC-0002:C-TASK-FILES] Declared file outputs between tasks (Normative)

A task MAY declare emits_files: the workspace-relative paths its output includes as files. Each declared path MUST be relative, MUST NOT traverse outside the workspace, and MUST NOT reach a file outside the workspace through a link of any kind. A symbolic link is refused per path component; a hard link is not distinguishable by resolving the path, so the engine MUST establish confinement by a means that does not rely on resolution alone.

When a task passes, the engine MUST capture each declared file. A declared file absent after an otherwise-passing attempt MUST convert that attempt to a measured failure at the producing task, MUST NOT be retried, and MUST block its dependents. This mirrors the rule for declared JSON output fields: output drift fails where it happened. A task that settles failing is captured too, under the additional provenance and withholding rules of RFC-0002:C-SETTLED-JOIN, and its set is staged only along the edges that clause names.

A declared file MAY carry a schema_file(), under the rules RFC-0001:C-WORKFLOW states for it. When the task passes, the engine MUST parse the captured copy of such a file as JSON and validate it against the schema. Content that is not JSON, or that the schema does not admit, MUST convert the attempt to a measured failure at the producing task like an absent file: it MUST NOT be retried, its note MUST name the file and follow RFC-0001:C-WORKFLOW's rule for schema notes, and the task's set MUST NOT be staged. A failing attempt's files are not validated. An agent task that declares repair under RFC-0001:C-WORKFLOW is shown an absent or mismatched file as a repair note before its attempt settles.

Captured files MUST be staged into a task's workspace before it is dispatched, for every ancestor on its dependency paths and not only its direct dependencies. A pipeline whose later stages need an earlier stage's artifact is the ordinary case, and requiring each hop to re-emit what it received would reproduce by hand the state files this replaces. JSON outputs keep the narrower direct-dependency rule.

Staged files MUST be namespaced by the producing task's name, so that two producers declaring the same path cannot collide. Staged files MUST be read-only to the consuming task. An instance of a mapped task is named after its node and its item key, which is runtime data: the engine MUST NOT refuse a key for holding a path separator or a dot, and where an instance's name forms the directory its files are captured or staged under, the engine MUST escape the key so that the directory is one path component and distinct keys name distinct directories, writing %, /, , NUL, and a leading . as %25, %2F, %5C, %00, and %2E. Everywhere else, the instance's name, its session-log rows, its inputs, and the matching of folded results on resume and rerun, the key MUST stay raw.

Captured files MUST be staged for a dependent even when the producing task ran in disposable isolation. A declared file is part of a task's output, not part of the workspace state that isolation discards.

The total size of files captured in one run MUST be bounded, the bound MUST be operator-configurable and discoverable before a run starts, and exceeding it MUST fail the producing task naming the bound rather than truncating silently.

emits_files MUST NOT be used as a channel between runs. Material that must outlive its run MUST be referenced by published artifact location instead.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-CAPS] Task capability requirements (Normative)

A playbook MUST execute on one substrate for its whole life. A task therefore cannot be moved to reach a capability, and privileged or specialized work MUST be reached by asking a mediated broker instead.

A needs declaration on a task MUST name a capability that the run can verify is available before dispatch. Whether a substrate provides that capability directly or a broker provides it on request is not the task's concern.

A required task whose needs is not available MUST truncate the whole plan before dispatch, and the engine MUST report that verdict before any spend. An advisory task in the same position MUST be skipped along with its dependents, leaving the verdict unaffected.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-COMPOSITE] Judgeless composite domains (Normative)

A composite domain MUST be permitted without [judge] when its workflow type is playbook. A playbook over several repositories combines work, not scores, so a composite MUST NOT be required to carry a judge merely because it has more than one component.

A judgeless composite MUST NOT produce a combined score, and the engine MUST NOT construct one.

A component MAY carry its own publish target. A component whose workspace holds commits beyond the base it started from, and that carries a target, MUST publish one draft pull request; a playbook has no propose-and-apply cycle, so this commit comparison is what stands in for the scored loop's notion of a change. A component with no target MUST publish nothing, which is the ordinary case for a playbook whose output is a review, a report, or a filed ask rather than a diff.

Since: v0.1.0

[RFC-0002:C-ASKS] Work emission (Normative)

A playbook MAY emit asks: descriptions of work for another run to perform. An ask MUST carry a key, the workflow it names, and that workflow's parameter values. An ask's parameter values MUST satisfy the named workflow's declared schema, and the receiving orchestrator MUST reject an ask that does not.

The key MUST be supplied by the emitter and MUST be stable across runs for the same underlying item, so that a receiving orchestrator can recognize a repeat without interpreting the ask's contents. What counts as the same item is the emitter's judgment and cannot be checked by the engine; the obligation is verified by auditing emitted keys on the session log against what they referred to.

A run MUST NOT dispatch an ask it emitted. Admission belongs to the receiving orchestrator, which owns deduplication, exclusion policy, and rate limits; an emitting run therefore cannot widen its own blast radius.

The number of asks one run may emit MUST be bounded, the bound MUST be operator-configurable and discoverable before a run starts, and reaching it MUST fail the emitting task rather than silently dropping the remainder.

Emitted asks MUST be recorded on the session log, so that what a run proposed is auditable independently of what was admitted.

An ask MUST NOT carry file content. Material a receiving run needs MUST be referenced by published artifact location.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-BUDGET] Cost and time ceilings (Normative)

A playbook MUST run under two ceilings supplied by whatever launched it: a total cost ceiling and a total wall-clock ceiling. A playbook source MUST NOT declare either, so that a pack cannot raise a limit its operator set.

The engine MUST refuse to dispatch any task of a playbook for which either ceiling is missing. Unsupervised scheduled work with no stated limit is the failure this rule exists to prevent, so the refusal MUST come before any spend rather than at the first overrun. A run that ends because a ceiling was exhausted MUST name which one in its shutdown outcome.

A task can spend when its attempt asks a model: an agent task, a route a decision model decides, and the scored loop's candidate-producing turn. A revise loop can spend when any task of its body can, because its rounds dispatch as one unit. Every other task cannot spend, and the engine MUST NOT hold it to the cost ceiling.

The engine MUST check the cost ceiling before every dispatch of a task that can spend, and each lane of a fan-out, whether an instance of a mapped task or a member of a batch of isolated tasks, is a dispatch of its own: the engine MUST admit lanes one at a time as each starts, never a batch as a whole. The ceiling is reached once the run's settled spend, plus what attempts still in flight have reported spending so far, reaches it. Once the ceiling refuses a lane, the engine MUST refuse every later lane that can spend, even where a figure in flight later settles lower. The engine MUST run at most a bounded number of lanes of one dispatch at once, so that what lanes already in flight can carry past the ceiling is bounded by that width. A lane or task not dispatched because the cost ceiling was reached MUST settle blocked, naming the cost ceiling.

The engine MUST NOT terminate an attempt already in flight on cost grounds, so attempts in flight when the ceiling is reached MAY carry the total past it. Once the ceiling refuses a dispatch, or an attempt carries the total past it, the run MUST be invalid, and the engine MUST block every further dispatch and retry of a task that can spend. It MUST NOT block a task that cannot spend: such a task MUST still dispatch under its own join and retry its transport failures, so the run collects and reports the work it already paid for.

The engine MUST terminate every attempt in flight when the wall-clock ceiling is reached, MUST invalidate the run, and MUST block all further dispatch and retries. This is deliberately the opposite of the cost rule, and a reader who pattern-matches from one to the other will get it wrong: an attempt's cost is settled only once it finishes, so killing that attempt would spend the money and learn nothing, whereas a run past its window is over no matter what any task is still doing. The wall-clock ceiling therefore blocks every task, including one that cannot spend.

Separately from the run's ceilings, every attempt of every task MUST execute under a bounded wall-clock deadline: a hung task spends nothing, so the cost ceiling cannot bound it. The run's wall-clock ceiling is both the operator's default deadline and its maximum. A task that declares no limit is bounded by the ceiling alone; a task MAY declare a shorter per-attempt limit, and a declared limit exceeding the ceiling MUST be rejected before any task is dispatched. An attempt's deadline is its task's declared limit counted from the start of that attempt, or the instant the run's wall-clock ceiling falls if that is earlier, so a deadline never extends an attempt past the ceiling. Attempts dispatched together MUST each run under their own deadline.

When an attempt reaches its task's own limit, the engine MUST terminate the attempt together with every process it started and MUST settle it as a measured failure whose note names the limit. That failure propagates like any other failure rather than ending the run directly. The engine MUST NOT retry it: rerunning an attempt that exceeded its limit repeats the time it spent without adding evidence. An attempt the run's wall-clock ceiling terminates instead settles as a failure naming the ceiling, and the run ends on that ceiling as the paragraph above requires.

A wall-clock ceiling reached while an epilogue task is in flight MUST terminate that attempt. Either ceiling reached during the epilogue MUST block further dispatch as the paragraphs above require, and MUST NOT change a verdict already determined by the main graph. The epilogue exists to report on the run, so letting its own spend rewrite the run's verdict would defeat it.

Every ceiling and deadline in force MUST be discoverable by the pack author and the operator before a run starts.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-RESUME] Resumption (Normative)

A resumed playbook MUST NOT re-dispatch a task that already settled. The engine MUST fold prior task results from the session log and MUST treat them as terminal, feeding their outputs to dependents exactly as a live execution would.

A resumed playbook MUST reconstruct its workspace from the pristine checkout plus the declared file outputs of every folded task that settled passing, staged under the rules in RFC-0002:C-TASK-FILES. Files captured from a task that settled failing MUST NOT be restored into the workspace; they reach a dependent joining "settled" through its staged inputs only. It MUST NOT attempt to reconstruct the workspace as some particular task left it: when concurrent tasks were in flight at the interruption, no such state is well defined, and a task's declared output is the only part of its work the contract ever promised would survive.

Spend and elapsed time recorded before the interruption MUST count against the ceilings on resumption. A crash MUST NOT reset either.

A resumed playbook MUST read the manifest variant under RFC-0001:C-MANIFEST that the interrupted run read, and the engine MUST refuse a resume that names another, because a variant can change what the run reaches without changing its graph.

A resumed run MUST NOT re-dispatch the main graph when the folded results cover every main-graph task, and equally when any folded result declared early completion. Early completion leaves tasks undispatched by design, so a resume that only checked for full coverage would dispatch them again on every restart. It MUST still dispatch any epilogue task that has no folded result, under the conditions in RFC-0002:C-PLAYBOOK-LANE. The run's summary, shutdown, and publication MUST each happen exactly once across the original run and all of its resumptions.

Since: v0.1.0

[RFC-0002:C-PLAYBOOK-RERUN] Rerunning from a task (Normative)

A rerun is a new run of a playbook that names a finished run of the same playbook as its parent and one or more main-graph tasks as its selection. It has its own run id, ceilings, session log, verdict, and publication. The engine MUST refuse a rerun whose parent has not shut down, and one whose selection names a task the current graph does not contain.

The engine MUST rerun every selected task, every task downstream of a selected task, every main-graph task the parent did not settle passing, every main-graph task the parent's graph did not contain, and every epilogue task. Where a rerun task is a member of a revise loop under RFC-0002:C-REVISE-LOOP, the engine MUST rerun every member of that loop. A mapped task is reused or rerun as one node, never per instance. Every other main-graph task is reused.

The engine MUST record, for every task in the graph it admits, a definition digest over the compiled task with its parameters bound and over the definition digests of its dependencies. The engine MUST refuse a rerun in which a reused task's definition digest differs from the digest the parent recorded for it, and MUST name each such task. Editing a task therefore reruns it only when it is selected or downstream of a selection; an edit anywhere else is refused rather than silently reused. The definition covers what the graph declares. A file a task reads from the pack or the workspace is not part of it, and an edit to one MUST NOT by itself refuse a rerun.

A rerun MUST start from the commit its parent's workspace started from, not from the target's current head, and MUST refuse when that commit is unavailable. A checkout holding the same tree is the same start. A workspace with no repository of its own starts from a commit of the pack's injected files alone, so a rerun of one starts from its own. Before dispatching any task, the engine MUST replay onto that commit, in the order the parent settled them, the commits each reused task contributed to the parent's git memory under RFC-0002:C-PLAYBOOK-LANE, and MUST refuse the rerun when any replay fails. Replayed commits are the rerun's git memory, so a rerun publishes them as its own.

The engine MUST treat each reused task's parent result as terminal and MUST feed its output and captured files to dependents exactly as a live execution would, under RFC-0002:C-TASK-FILES and RFC-0002:C-SETTLED-JOIN. The engine MUST refuse a rerun when the parent's session log, captured files, or task commits are unavailable, naming what is missing; it MUST NOT reconstruct a reused task's output any other way.

Each reused task MUST appear in the rerun's session log as a settled row marked reused and naming the run that produced it, which for a task the parent itself reused is the run its parent names. Spend recorded for reused rows MUST NOT count against the rerun's ceilings, and MUST be reported apart from the rerun's own spend.

Since: v0.1.0

[RFC-0002:C-REPORTS] Typed workflow reports (Normative)

A playbook MAY declare an engine-owned report task with a destination kind and an optional result task selector. The selected task MUST be a task in the same graph. Selection MUST NOT create a dependency from the report epilogue to the main graph. After the main graph settles, the report task MUST receive only the selected task's declared JSON output when that task passed, never its prompt, stdout, workspace, undeclared files, or credentials. When the selected task did not pass, the report MUST retain the selected task's terminal status without projecting partial output.

A selected report result MUST be bounded before delivery. The operator MUST configure a maximum encoded size, and exceeding it MUST fail the report task without truncation. Values MUST retain their JSON types through the report snapshot. A report renderer MUST escape text for its destination and MUST NOT interpret result values as destination-native payload fragments.

The Slack destination MUST support a structured card projection containing an engine-authored header, run verdict and cost, the selected result fields, bounded task status context, and a controller-owned run link. The engine MUST construct the destination-native payload; a pack MUST NOT supply raw Slack blocks, attachments, webhook URLs, channels, actions, or identifiers. Delivery failure MUST settle a required report task as failing.

The persisted session result for a report task MUST record whether delivery succeeded but MUST NOT persist the webhook credential. An observer MUST be able to distinguish a delivered report, a rendering failure, an oversized result, and a destination failure.

A report MAY declare a template: a file in the pack rendered into the body of the report. The template MUST be able to read the run's verdict, its cost, and the bounded task status context the card carries. Where the selected task passed, the template MUST receive the same bounded, typed snapshot of its result the card projects; where it did not, the template MUST receive only that task's terminal status. A template MUST NOT be able to read any field the selected task did not declare. The renderer MUST escape every value a template inserts, as it escapes a projected field. The rendered body MUST be bounded by an operator-configured size, and exceeding it MUST fail the report task without truncation.

A report MAY name one declared field of its selected task as its severity field. The engine MUST reject a severity field on a report that selects no task, and one the selected task does not declare. Where the selected task passed and that field holds "good", "warning", or "danger", the Slack destination MUST render the card with the accent that value names, whatever the run's verdict: the accent is the pack's reading of its own result, and the card carries the verdict separately. Where the field holds any other value, and where the selected task skipped or was not taken, the destination MUST render the card neutral. Where the selected task failed, transport-failed, or was blocked, it MUST render the card as "danger". The severity field MUST NOT reach the destination as a payload fragment: it selects one of the engine's accents and nothing else.

Since: v0.1.0

[RFC-0002:C-SETTLED-JOIN] Joining on a settled dependency (Normative)

A task MAY declare join = "settled". The engine MUST accept "all", "passed", and "settled", and MUST reject every other value. A task declaring join = "settled" MUST declare at least one dependency. The engine MUST reject join = "settled" on a reducer task, on the engine-owned report task, and on any task naming an engine operation, whose contracts are defined over the passing set or over a fixed typed context.

The engine MUST dispatch a task joining "settled" once every one of its dependencies has reached a terminal status, whatever that status is: passed, failed, not taken under RFC-0001:C-ROUTE, skipped whether the task declared the skip or the substrate did, transport-failed after its retries, or blocked. A settled join MUST NOT make a dependency's runnability a condition of the dependent's; the engine MUST treat such a task as runnable wherever the substrate satisfies its own capability requirement, whatever its dependencies require.

A settled join governs dependency status and nothing else. The engine MUST NOT dispatch such a task once dispatch has stopped for any other reason, and a task left undispatched by a required task settling other than passing, by an exhausted cost ceiling where it can spend under RFC-0002:C-PLAYBOOK-BUDGET, by an exhausted wall-clock ceiling, or by a truncated graph MUST settle blocked whatever its join. An epilogue task is exempt from the required failure alone, because RFC-0002:C-PLAYBOOK-LANE requires it to run when the main graph failed: a required failure MUST NOT block it whatever its join, and an exhausted ceiling or a truncated graph MUST block it as it blocks any other task. Where dispatch stopped because a task declared early completion, RFC-0002:C-PLAYBOOK-LANE governs: the engine MUST NOT treat an undispatched task as blocked, and a settled join MUST NOT cause it to dispatch.

The engine MUST give a task joining "settled" one entry per declared dependency, under that dependency's name and present for every dependency whatever its status, carrying that dependency's terminal status as the token RFC-0001:C-WIRE defines for the task-result event, the engine's note or null where it recorded none, the dependency's JSON output or null, and whether a file set captured in this run was staged for this consumer. The entry MUST carry the output rather than be the output. For a mapped dependency the entry MUST additionally carry, per instance, that instance's status, note, output, and staged-file flag, and that per-instance mapping MUST be present and empty where the node produced no instances; a consumer distinguishes an empty fan-out from a node that never expanded by the entry's status. The engine MUST NOT wrap a reserved input, and MUST reject a plan that names a dependency after one: the reserved key is written after the envelope is built, so it would overwrite that dependency's entry. The engine MUST give every epilogue task a reserved input named "outcome", built when that task is dispatched, carrying under "exit" the token RFC-0001:C-WIRE defines for the shutdown event's outcome and under "tasks" one entry per settled main-graph task, under that task's name, each entry being the entry a task joining "settled" would receive for that task, per_instance included for a mapped task, with its staged-file flag saying whether that task's set was staged for this epilogue task. This is the input RFC-0002:C-PLAYBOOK-LANE requires an epilogue task to receive.

The engine MUST give a task joining "all" or "passed" each contributing dependency's JSON output directly, and MUST NOT give it an entry for a non-mapped dependency that settled failing, transport-failed, or was blocked. The engine MUST NOT give a task joining "all" an entry for a dependency it declares optional under RFC-0001:C-ROUTE that settled not taken, mapped or not. A mapped dependency's fold MUST continue to reach such a task on exactly the terms it reaches it today, whatever the fold's status: a reducer over a lossy fan-out is the case join = "passed" exists for.

A task that ran and measured a failure MUST retain that attempt's structured output. The engine MUST retain an evaluate task's own graded object, MUST retain a command task's final stdout line where it parses as a JSON object although the process exited nonzero, and MUST NOT retain output from an attempt that ended in transport failure. Where a retained object from a nonzero exit carries a non-empty string field "note", the engine MUST record it as the failure's note after the exit status, in place of the standard error tail, so the task's own reason reaches every reader of the note. Where an evaluate task's retained graded object did not pass and carries such a field, the engine MUST record it as the failure's note in place of the engine's own, followed by the threshold and log handles that note would name. The engine MUST NOT treat a retained failure output as a mapped task's source list: a task fanning out over another task's field MUST fail, naming the absent source, unless that source settled passing.

A task MAY settle itself failing by returning the string field "status" as "fail" in its output. The engine MUST record such a task as failed, MUST retain the returned object as that task's output, MUST record its non-empty string field "note", where present, as the task's note, and MUST NOT retry it. This is the third engine-meaningful value of the field that already carries "skipped"; the engine MUST ignore every other value. The field may narrow a task's settlement and MUST NOT widen it: "fail" and "skipped" MUST settle the task that way whatever its exit status or grade, and "pass" MUST NOT settle passing a task that exited nonzero or whose evaluation did not pass.

Declared JSON output fields and declared file paths are owed by a passing attempt only. The engine MUST NOT check declared fields on a failing attempt, and MUST NOT guarantee a task joining "settled" that a failed dependency's output carries them.

The engine MUST capture a task's declared files when it settles passing and when it settles failing, under the confinement, atomic-publication, file-mode, and run-total-size obligations of RFC-0002:C-TASK-FILES. Capture MUST happen before the task's workspace changes are discarded, and the engine MUST NOT require isolated execution to capture on failure. On a failing attempt the engine MUST capture only paths that attempt itself wrote, determined by comparing each declared path's content in the root the attempt ran in against what that path held there before the attempt started, and MUST treat a declared path whose content did not change as absent: a file an earlier task left in the workspace is that task's evidence, not this one's, whether or not the workspace's version control can see it. A declared path that is absent, is not a regular file, or was not written by a failing attempt MUST NOT change that task's status; the engine MUST publish none of the set, MUST remove any set published for that task by an earlier attempt or run, and MUST record the capture problem with the failure note. Where an absent declared path converts an otherwise-passing attempt into the measured failure RFC-0002:C-TASK-FILES requires, the engine MUST retain that attempt's structured output as the failure's. Where the run-total size bound is exceeded by an attempt that has already failed, the engine MUST enforce it by publishing none of the set and recording the bound with the failure note. The engine MUST NOT capture declared files from a task that skipped, that transport-failed, or that was never dispatched, and MUST remove any set such a task published in an earlier run.

The engine MUST stage the declared files of a task that settled failing only into a task joining "settled" that declares it as a dependency and, for a main-graph task, into every epilogue task, and MUST NOT stage them into any other task. The engine MUST stage every settled main-graph task's complete set, whether that task passed or failed, into every epilogue task under that task's name, as RFC-0002:C-TASK-FILES stages an ancestor's, because an epilogue task cannot declare a main-graph dependency to receive it through. The engine MUST stage a passing ancestor's declared files under the rules of RFC-0002:C-TASK-FILES unchanged. A staged set MUST be complete; the engine MUST NOT stage a partial set. One instance of a mapped producer MUST be staged under the instance's own name, its key escaped as RFC-0002:C-TASK-FILES states.

Capture is not commit. A task that settled other than passing MUST NOT contribute a commit to the run's git memory, and its undeclared workspace changes MUST be discarded, as RFC-0002:C-PLAYBOOK-LANE requires. A captured failure set MUST NOT make its producer a passing dependency, MUST NOT satisfy a join of "all" or "passed", and MUST NOT affect the run's verdict.

A resumed run MUST treat a folded non-passing result as satisfying a settled join exactly as a live result does, and MUST NOT re-dispatch the task to regenerate its evidence. Captured sets MUST remain in engine-owned run state, and the engine MUST NOT restore a non-passing task's captured files into the workspace. Where a captured set did not survive the interruption the engine MUST still dispatch the consumer, giving it that dependency's status and output, or an epilogue task that task's outcome entry, reporting no staged files, and MUST NOT re-dispatch the producer.

The admitted-plan event MUST carry "settled" under the key it already carries a task's join, and the engine MUST NOT add a companion field for it; a consumer MUST treat an unrecognised join token as opaque rather than inferring readiness from it. The task-result event MUST NOT change shape: a failing task's retained output MUST travel in the field that already carries a passing task's.

Since: v0.1.0

[RFC-0002:C-REVISE-LOOP] A reviewer's bounded send-back (Normative)

A task MAY declare revise, naming one or more tasks, together with max_rounds. The named tasks are the targets and the declaring task is their reviewer; the targets and the reviewer together are the loop's body. The engine MUST reject revise without max_rounds and max_rounds without revise, MUST reject max_rounds outside 2 to 5 inclusive, MUST reject a revise that names no task or names one task twice, and MUST reject a target from which the reviewer is not reachable through dependencies. The engine MUST reject a body that omits a task lying on a dependency path between two body tasks: a round re-runs the body, and a task between two body tasks outside it would read a result a later round replaces. The engine MUST reject a loop in which any body task is anything other than an agent, command, or evaluate task, or maps over a list. The engine MUST reject a task that is in two bodies, and a reviewer that is itself a target: loops do not nest or chain. The engine MUST reject revise in every workflow type other than "playbook". The name "revision" is a reserved input under RFC-0002:C-SETTLED-JOIN, and the engine MUST reject a dependency named after it.

The engine MUST NOT dispatch the loop's first round until every dependency outside the body of every body task has settled, and MUST NOT dispatch a task outside the body that depends on a body task until every body task has settled under its own name. Where any body task cannot run on the substrate, the engine MUST dispatch the body's tasks as tasks outside any loop and MUST settle each one that cannot run as RFC-0002:C-PLAYBOOK-CAPS requires.

A round dispatches the body's tasks one at a time in dependency order, each under its own join and when against this round's results for the body and the settled results outside it. A body task whose join those results do not satisfy MUST settle blocked for that round, and one whose when excludes it MUST settle not taken for that round, as any task would. The engine MUST NOT begin another round unless the reviewer's round settled failing and fewer than max_rounds rounds have run, and MUST begin one when both hold and no ceiling is exhausted. A reviewer round that passed, skipped, transport-failed, was blocked, or was not taken ends the loop. Before every round after the first, and before every dispatch within a round that the task's join and when would allow, the engine MUST check the cost and wall-clock ceilings. An exhausted ceiling ends the loop and halts the run as it would halt any dispatch. Within a round, a body task not dispatched for it MUST settle blocked on that ceiling; a loop ended by a ceiling between rounds leaves each body task settled as its last round did. An epilogue loop outlives a required failure in the main graph as any epilogue task does.

From the second round on, the engine MUST give each target the reserved input "revision", carrying under "round" the round about to run, under "max_rounds" the declared bound, under "reviewer" the reviewer's name, and under "review" the reviewer's previous round as the entry RFC-0002:C-SETTLED-JOIN defines for a settled dependency. Where the engine holds a complete file set captured from that failing reviewer round, it MUST stage that set into each target under the reviewer's name, and the entry's staged-file flag MUST say whether it did. The engine MUST NOT give "revision" to a target in the first round or to the reviewer in any round. A body task that declares a session MUST resume that session in every round.

Each round of each body task MUST produce one session row named after the task followed by "[round-N]", written as that round settles and carrying that round's status, output, note, and spend. A first round that dispatched no body task MUST produce no round rows. Once the loop ends, each body task MUST produce one row under its own name carrying its last round's status, output, note, blocked reason, and transport cause, and the sum of every round's spend. A round row MUST NOT affect the verdict or halt the run; the rows under the tasks' own names do, exactly as RFC-0002:C-PLAYBOOK-LANE states for any task. A dependent of any body task MUST read the last round's output and captured files.

Every round settles its work as any task does. A passing round's workspace changes MUST be committed to the run's git memory and a failing round's MUST be discarded, as RFC-0002:C-PLAYBOOK-LANE requires, so a later round runs on top of every earlier passing round. A round's captured file set MUST replace the set its task published in an earlier round under the rules of RFC-0002:C-TASK-FILES and RFC-0002:C-SETTLED-JOIN.

The admitted-plan event MUST carry a reviewer's targets under "revise", as a string when it names one and as a list in the order it names them when it names several, and its bound under "max_rounds", and MUST carry an empty string and zero for every other task, so a reader that predates chains reads every plan without one.

Since: v0.1.0

[RFC-0002:C-RUN-HISTORY] History of a launch series (Normative)

A launch series is the set of runs one standing launch started. A run started any other way belongs to no series. A series history is a read-only view of what earlier runs in the series recorded, and is separate from a schedule cursor, which carries a position rather than a log. A run contributes to it only through its record task's declared output, and no run can alter a record already written. Nothing here restricts what a task reads from outside the engine, such as a repository or a published artifact.

A playbook MAY name one task as its history record. The engine MUST reject a mapped record task. Each run records that task's terminal status under the vocabulary RFC-0001:C-WIRE defines for the task-result event, and its declared JSON output where it passed or null where it did not; a revise target or reviewer records the result under its own name. Where the record task never settled, because the run stopped early, was stopped by an operator, or ended on a ceiling before dispatching it, the run records a null status and a null output. The record task MAY be an epilogue task, so that a run whose main graph failed still records what it found. A playbook that names no record task records only its outcome.

A task MAY declare that it reads history, with a depth. The engine MUST reject a depth outside 1 to 30, and MUST reject a record task or a history depth in any workflow type other than "playbook". The engine MUST give a task that declares a depth the reserved input "history": an object carrying under "records" a list of at most depth records, ordered by the end time of the run each describes, oldest first, and under "dropped" the number of records within this task's depth that the size bound below removed. A run that belongs to no series, or whose series has no prior terminal run, MUST receive an empty list and zero. The engine MUST reject a plan that names a dependency "history", as RFC-0002:C-SETTLED-JOIN requires for every reserved input.

A run of a series is terminal once the orchestrator has recorded that it ended, whether or not the run itself wrote a shutdown event. Each record MUST carry the prior run's identifier, its start and end times, its shutdown outcome under the vocabulary RFC-0001:C-WIRE defines for the shutdown event or null where the run wrote none, its verdict or null where it has none, the pack revision it ran, a controller-owned link to it, and the record task's name, status, and output as recorded above. The engine and the orchestrator MUST NOT add any other field to a record.

History MUST include every terminal run of the series: a run that failed, was stopped by an operator, ended on a cost or wall-clock ceiling, or ended without a shutdown event is part of the history as much as one that passed. A run that has not reached a terminal state MUST NOT appear.

The orchestrator that starts a run in a series MUST supply the series' most recent terminal runs by end time, up to the engine's ceiling of 30, and the engine MUST give each task only the most recent records its depth allows. The total encoded size of the history one task receives MUST be bounded by an operator-configured limit that is discoverable before a run starts. Where the records within a task's depth exceed it, the engine MUST drop whole records from the oldest end and MUST NOT truncate a record.

History MUST reach an agent prompt only inside a region the prompt marks as external input, under the same obligation RFC-0002:C-PLAYBOOK-PARAMS states for parameter values: an earlier run's record may carry text an earlier agent wrote. A resumed run MUST give each task the same history the interrupted run gave it.

The admitted-plan event MUST carry the playbook's record task under "history_record", or an empty string where it names none, and each task's depth under "history_depth", zero where it declares none.

Since: v0.1.0

[RFC-0002:C-PACK-BASE] Base packs (Normative)

A pack's files are the regular files under its root, excluding any path with a segment named state, .git, or workspace. Files and non-regular entries under an excluded segment are ignored, and a save, registration, or import MUST report the paths it ignored. A pack file's path MUST be relative to the root, use / separators, be valid UTF-8, and contain no empty, ., or .. segment and no newline. The controller MUST refuse to save, register, or import a pack that holds a symbolic link or another non-regular file outside an excluded segment. No file mode is part of a pack: every pack file MUST be delivered to a run and published with mode 0755.

A pack's digest MUST be the string tree1: followed by the lowercase hex SHA-256 of its tree summary: one line per pack file, in byte-wise order of path, each line the lowercase hex SHA-256 of the file's bytes, two spaces, the path, and a newline. The digest MUST NOT depend on file mode, timestamps, or how the pack is archived, compressed, stored, or delivered. A pack's revision is its digest, and every pin, comparison, and approval of a pack MUST use it.

A launch MUST record the digest of the pack it runs, and dispatch MUST deliver the stored tree under that digest and no other. Per-run inputs that are not pack content, such as steering, MUST be delivered beside the pack's tree and MUST NOT be written into it. A standing launch pinned to a pack MUST record that pack's digest. A standing launch that runs a draft's latest saved version that compiles MUST NOT record a digest; each launch it creates MUST record the digest of the draft's latest saved version that compiles at the time of that firing.

A draft version MAY name a base: a pack identified by its digest. A base digest is available to a subject when at least one registered or imported pack that has not been deleted and that the subject may read under RFC-0003:C-READ-SCOPING has that digest as its current revision or as one of its prior revisions. A pack registered from a source repository or imported MUST NOT name a base, and a base MUST NOT be expressed in the pack's manifest. The controller MUST refuse to save a draft version whose base digest is not available to the caller, and MUST decide availability before any other check that depends on the base. When the base is not available the controller MUST return only one refusal, which names the base digest as given and states that the base is not available, so that a caller cannot tell a missing base from one it may not read; no refusal that depends on the base's content may be returned instead. The one exception is a replaced digest, which MUST be refused as the migration paragraph of this clause requires.

Saving a draft version that names a base MUST flatten it: the base's files with the overlay's files placed over them by path. An overlay file at a path that is a base file's path MUST replace that file whole, every base file at a path the overlay does not hold MUST be kept unchanged, and contents MUST NOT be merged, so an overlay cannot remove a base file. The controller MUST refuse to save, register, or import a pack, and to save a draft version, in which one file's path followed by / is a prefix of another file's path, across the base and the overlay when there is a base. An overlay MAY be empty or MAY equal part of the base; the flattened digest then equals the base's digest. The flattened tree is the pack: its digest is the version's digest, and compilation, exposure extraction, approval, launch, and delivery MUST use the flattened tree and no other.

A draft version saved on a base MUST record as provenance the base digest, the digest of the overlay's files computed as above, every overlay path, and which of those replaced a base path, and MUST return them with the version. A pack registered from that draft version MUST carry and return the same provenance. Provenance digests MUST NOT keep any stored tree. For each registered or imported pack that the requesting subject may read and that holds a version's base digest as a prior revision, the controller MUST report, with the version, that pack and its current revision digest. A pack the subject may not read MUST NOT be reported or counted.

Saving on a base copies the base's files into the pack. A subject that may read the saved pack reads those files, and reading, launching, sharing, or approving the saved pack MUST NOT require any action on the base. The availability check at save is the only authorization applied to the base's content, and a later deletion of the base or loss of access to it MUST NOT affect packs already saved on it.

The controller MUST keep a pack's stored tree, unchanged under its digest, while any draft version, registered or imported pack, launch, or standing launch records that digest. A registered or imported pack records its current revision and every prior revision.

The controller MUST declare a delivery budget, measured as the size in bytes of the gzipped tar the controller delivers as a run's pack ConfigMap, and MUST make the value retrievable. A launch delivered by image volume (RFC-0002:C-PACK-PUBLISH) is not checked against it at dispatch. The controller MUST refuse to save, register (including by draft publish), or import a pack whose tree exceeds the delivery budget in force at that time, and MUST refuse to dispatch a launch whose delivered tree exceeds the budget in force at dispatch, with a refusal that names the budget and the size. Limits on the file count and per-file size of a draft MUST apply to its overlay, not to the flattened tree.

A digest recorded before this clause took effect MUST be replaced by the digest of the same stored tree, computed after applying this clause's definition of a pack's files, and every pin, approval, base reference, and run record holding it MUST be rewritten to the replacement. Several replaced digests MAY map to one replacement. A stored tree that cannot be converted, because it holds a non-regular file, an invalid path, or a path collision outside an excluded segment, MUST stay recorded under its old digest as unconvertible by every resource that holds it, and every request that would compile, deliver, publish, launch, or use it as a base MUST be refused with a refusal that names the reason. The controller MUST refuse a request that supplies a replaced digest: to a caller who may read some resource that recorded it, with a refusal that names it as superseded and gives its replacement; to any other caller, exactly as for an unknown digest, and as a base with the unavailable-base refusal.

Approval of a pack saved on a base is governed by RFC-0003:C-APPROVALS, and publishing packs to an OCI registry by RFC-0002:C-PACK-PUBLISH.

Since: v0.1.0

[RFC-0002:C-PACK-PUBLISH] Publishing packs to an OCI registry (Normative)

Publishing packs to an OCI registry (OCI publication) is optional. With no pack registry configured, the controller MUST NOT attempt it, and saving, registering, importing, approving, launching, and dispatching MUST NOT require a pack registry to be configured or reachable.

A dispatch target is registry-trusted when it is owned by the platform administrator team or a platform administrator has marked it so. The mark MUST NOT be set or cleared by any other subject, including through update on the target, and every change to it MUST be audited. Pull access to the pack registry MUST be limited to the controller and to registry-trusted targets, and MUST NOT be granted to any user or team principal. A target that is not registry-trusted MUST receive packs only through controller-built delivery, never by registry pull. The pack registry is not a read path for people: reading a pack is decided under RFC-0003:C-READ-SCOPING through the controller.

When a pack registry is configured, the controller MUST publish every registered pack revision, every imported pack revision, and every approved draft graduation, including those that existed before the registry was configured. OCI publication MUST NOT be refused for size. The artifact MUST use the OCI empty config and layers of media type application/vnd.oci.image.layer.v1.tar+gzip whose extraction in order, a later file replacing an earlier one at the same path, reproduces exactly the pack's files. A pack saved on a base MUST have two layers, the base's files and then the overlay's files, while the base's stored tree is held; when it is no longer held, the pack MUST be published as one layer holding its tree, and its publication state MUST record that the base layer is not shared. A pack with no base has one layer. A layer's bytes MUST be determined by the files it holds and the controller's layer encoding version alone, so that every pack published on one base under one encoding version carries an identical first layer. The layer layout applies only to the first publication of a digest: when the tag for a pack's digest already exists in the repository, the controller MUST NOT push a new manifest and MUST record the existing manifest digest as the pack's published manifest. The manifest MUST carry the annotation dev.crucible.pack.digest with the pack's digest as RFC-0002:C-PACK-BASE spells it, and dev.crucible.pack.kind with the value playbook.

Every published pack MUST be published to one repository under the tag tree1- followed by the hex part of its digest. The controller MUST NOT move that tag to another manifest, and MUST keep it while any draft version, registered pack, launch, or standing launch records the digest.

Each registered pack revision, imported pack revision, and approved draft graduation MUST carry a publication state of unpublished, pending, published with the manifest digest, or failed with the last error, readable by any subject that may read the pack. A failed OCI publication MUST NOT undo or block the registration, import, or approval it follows. A network error or a registry response of 429 or 5xx is transient; any other registry error is permanent. While a transient failure is retried with backoff, the state MUST be pending with the last error. The controller MUST NOT retry a permanent failure until a subject allowed the update action under RFC-0003:C-DECISION on the registered pack, the import, or the graduated draft requests a retry. Changing or removing the pack registry MUST reset every affected publication state to unpublished.

A launch MAY be delivered by image volume only when its target is registry-trusted and can mount OCI artifacts and the pack's publication state is published. The mount MUST reference the recorded manifest digest. Every other launch MUST be delivered as the pack ConfigMap and is subject to the delivery budget of RFC-0002:C-PACK-BASE.

Since: v0.1.0

[RFC-0002:C-HUMAN-DECISION] Human-decided routes (Normative)

A human-decided route takes its answers from a person allowed to approve the run, and holds the route's dependents until one answers or the request expires. It is how a playbook keeps an expensive or irreversible branch, such as a compute job an agent turn proposed, from starting before someone has seen what it would act on. The questions, labels, and when coverage rules of RFC-0001:C-ROUTE apply to it unchanged.

A human-decided route MAY ask two kinds of question no other decider asks. A multiple choice takes one or more of its declared labels rather than one. A pick names a list field of one of the route's dependencies as its source, and takes one value, or with multiple one or more, from the strings that field holds when the request opens. A pick has no labels, so no when may name it. A route decided by a model or a dependency's output MUST NOT ask either kind, and a plan that does MUST be rejected before any task is dispatched. A pick whose source is not a dependency of the route, or whose producer declares emits without the field or types it as anything but a list of strings, MUST be rejected the same way. When the request opens, a pick's source output MUST hold a non-empty list of strings; otherwise the route MUST fail naming the source, before the request opens. A human-decided route MUST NOT name a question "decision", the key its output records the decision under.

A human-decided route MUST declare needs = "human", verified before dispatch under RFC-0002:C-PLAYBOOK-CAPS. A run whose orchestrator cannot take an answer from a person does not have that capability.

When a human-decided route is dispatched, the engine MUST open a decision request with the orchestrator. The request MUST carry the run, the route's name, its questions with their declared labels and, for each pick, the values it offers, the evidence and its digest under RFC-0002:C-DECISION-EVIDENCE, and the time it expires. A request is identified by its run and route name. The orchestrator MUST hold at most one request per pair, and an open for a pair that already has one MUST return that request, with any answer recorded on it, rather than open another or reset its expiry. The engine MUST obtain the answer through requests it initiates; the orchestrator MUST NOT be required to open a connection to the run. Failure to reach the orchestrator is a transport failure and retries as one, and MUST NOT answer or expire the request.

While a request is open, the route runs no agent turn and spends nothing. Tasks that do not depend on the route MUST continue to dispatch as the graph allows. The wait counts against the run's wall-clock ceiling as any attempt does under RFC-0002:C-PLAYBOOK-BUDGET.

A human-decided route MAY declare a timeout. A declared timeout MUST be positive, and one exceeding the run's wall-clock ceiling MUST be rejected before any task is dispatched; with none declared, the ceiling is the timeout. Expiry MUST be counted from the time the request was first opened, and neither a restart nor a resume MAY extend it. When a request expires unanswered, the engine MUST record every question as "uncertain" and settle the route passing. This replaces, for a human-decided route only, the measured failure RFC-0002:C-PLAYBOOK-BUDGET prescribes when an attempt reaches its own limit: an unanswered request is an outcome the author routes, and the coverage rule of RFC-0001:C-ROUTE requires that they route it. When the run's wall-clock ceiling falls first, the route settles as that clause prescribes for the ceiling.

An answer MUST give every question exactly one of its options, or for a multiple choice or a multiple pick one or more distinct options. A choice's options are its declared labels, and a pick's are the values its request offers. "uncertain" MUST NOT be accepted as an answer. An answer MAY carry a note of bounded size, and the bound MUST be operator-configured and discoverable before a run starts. The orchestrator MUST accept an answer only from a subject allowed approve on the run under RFC-0003:C-DECISION, only while the request is open, and only when it names the evidence digest the request carries. It MUST refuse any other answer with a refusal that names the reason. The first accepted answer is final. A later answer that repeats it exactly from the same subject MUST be acknowledged without effect, and any other later answer MUST be refused. Stopping the run while a request is open MUST withdraw the request, and an expired or withdrawn request MUST refuse every answer.

The route's output MUST carry, for each choice, the resolved label with probability 1, or "uncertain" with an empty distribution and confidence 0, in the shape RFC-0001:C-ROUTE gives an output-decided route. For a multiple choice it MUST also carry every chosen label in the order given, each with probability 1, and the resolved label is the first of them; a when on it runs its task when any label it lists was chosen. For each pick, the output MUST carry the chosen values as a list of strings under the question's id, in the order given, and an expired request gives an empty list. A task MAY map over that list under RFC-0001:C-WORKFLOW. It MUST also carry the decision: the deciding user principal, the time, the evidence digest, and the note or null, or, for an expired request, a null principal and the outcome "expired". This output is the record RFC-0002:C-PLAYBOOK-RESUME folds, so a resumed run never asks again for a route that settled. A resumed run whose human-decided route has no folded result MUST open its request again, and MUST settle on any answer recorded while the run was down.

A note is text from outside the pack. It MUST reach an agent prompt only inside a region the prompt marks as external input, under the obligation RFC-0002:C-PLAYBOOK-PARAMS states for parameter values.

Since: v0.1.0

[RFC-0002:C-DECISION-EVIDENCE] Decision evidence and notification (Normative)

The evidence of a decision request is what the approver decides on. It MUST consist of exactly: each of the route's dependencies with its terminal status, its declared JSON output where it passed, and its declared files staged under RFC-0002:C-TASK-FILES, as the route's join would give them to it as inputs; the run's spend, elapsed time, and both ceilings at the time the request opened; for each task whose when names a question of the route, its name, kind, needs, and the labels that dispatch it, so the approver sees what each answer starts; the rendered review, where the route declares one; and for each pick question, the values it offers. Nothing a task did not declare MAY appear in it: no prompt, stdout, undeclared workspace file, or credential.

A human-decided route MAY declare a review: a template file in the pack, rendered into a CommonMark document shown above the rest of the evidence. The template MUST be able to read every part of the evidence above and nothing else. The renderer MUST escape every value the template inserts, and the controller MUST display the rendered review without executing any raw HTML in it.

The total encoded size of the evidence MUST be bounded, and the bound MUST be operator-configured and discoverable before a run starts. Evidence exceeding it MUST fail the route before the request opens, naming the bound, and MUST NOT be truncated.

The evidence MUST have a digest computed over its canonical encoding, rendered review and file contents included. The digest MUST be recorded on the request and on the decision. The controller MUST present the approver the evidence that digest names, and an answer is bound to the evidence it names: RFC-0002:C-HUMAN-DECISION refuses an answer naming any other digest.

The controller MUST present a request's evidence to an approver on one page together with the controls that answer it, with each question's options as the only choices, and with whether a question takes more than one. Declared JSON outputs MUST be shown as structured values that keep their JSON types. Declared files MUST be shown inline by media type at least for PNG, JPEG, GIF, and SVG images, Markdown, CSV as a table, JSON as a structured value, and HTML as an interactive document; a file of any other type, and a file whose inline presentation fails, MUST be offered for download. An HTML file is how a pack ships a visualization: a self-contained document whose scripts run when it is shown.

Every file in the evidence is untrusted: an agent may have written it. The controller MUST show SVG and HTML evidence in a context that cannot run script with the controller's origin, read its cookies or storage, call its API, navigate the page that embeds it, or make any network request. A visualization therefore has to carry its scripts and data inline.

The controller MUST list the open decision requests a subject may answer, and MUST publish an event on its live event stream when a request opens and when it closes, delivered only to subjects that may read the run. The web UI, while open in a browser that granted it notification permission, MUST raise a system notification for each request the signed-in subject may answer, linking to the request's evidence page, and MUST NOT raise one for a request the subject cannot answer. Delivery to a browser with no open session of the web UI is outside this clause.

Since: v0.1.0


Changelog

v0.1.0 (2026-08-21)

Initial draft

Added

  • C-PACK-PUBLISH: registered, imported, and approved packs are published to a configured OCI registry under immutable tags; saves and launches never depend on the registry
  • C-PACK-BASE: a pack's digest is SHA-256 over its canonical tree; a draft may name a base, flattened at save into one tree with one digest, base and overlay digests kept as provenance; an unavailable base refused at save; the delivery budget checked at save and dispatch
  • C-PACK-BASE: a pack's digest is a tree hash (tree1:); a draft may name a base, flattened at save into one tree with one digest and recorded provenance; an unavailable base is refused at save; the delivery budget is checked at save, registration, import, and dispatch; old digests are rewritten
  • C-PACK-PUBLISH: optional OCI publication of registered, imported, and approved packs, readable only by the controller and dispatch targets, under digest-derived tags with a publication state
  • C-HUMAN-DECISION, C-DECISION-EVIDENCE: a human-decided route opens one decision request per run and route, polled by the run, answered against an evidence digest, recorded as uncertain on expiry; the controller shows the evidence with sandboxed visualizations and notifies approvers

Changed

  • a settled join forwards a dependency that settled not taken
  • converting external text to a plain string, and a supplied value carrying a marker, are compile errors
  • the run's wall-clock ceiling is the default and maximum per-attempt deadline; a timed-out attempt fails measured and is not retried
  • C-TASK-FILES: a declared file may carry a schema, validated from the captured copy when the task passes
  • C-TASK-FILES: an absent or mismatched file is a repair note for an agent task that declares repair
  • C-PLAYBOOK-BUDGET admits each fan-out lane against settled and in-flight spend under a bounded width, and the cost ceiling blocks only tasks that can spend, so commands after it still run; C-SETTLED-JOIN and C-PLAYBOOK-LANE follow
  • C-PLAYBOOK-PARAMS: a supplied dotted name is a launch setting (., *., repo.ref), checked before dispatch and written onto the compiled tasks; a declared parameter name cannot contain a dot
  • C-PLAYBOOK-PARAMS: a string parameter may carry a multiline flag for launch forms; it does not change binding
  • C-SETTLED-JOIN: a failing task's own note, from a self-declared failure or a command's nonzero exit, is the note the engine records
  • C-SETTLED-JOIN: an all-join task gets no entry for an optional dependency that settled not taken (RFC-0001:C-ROUTE)
  • C-PLAYBOOK-RESUME: a resume reads the manifest variant the interrupted run read
  • C-SETTLED-JOIN: a failing evaluation's own note is recorded with its threshold and log handles, and a declared status fails or skips a task whatever its exit status or grade but never passes one
  • C-PLAYBOOK-LANE: only a command or evaluate task may declare early completion; any other task's declaration is refused on its session row and the run continues
  • C-TASK-FILES: a mapped instance's key may hold a separator or a dot; its captured and staged directory escapes %, /, , NUL, and a leading . as %25, %2F, %5C, %00, and %2E, and the key stays raw in names, rows, inputs, and resume
  • C-SETTLED-JOIN: an instance is staged under its own name with its key escaped

Fixed

  • C-PLAYBOOK-LANE no longer claims git memory survives a resume; C-PLAYBOOK-RESUME governs the rebuilt workspace