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".

Any task MAY declare early completion by returning the boolean field "complete" as true in its output, optionally alongside a "reason" string. 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. The engine MUST then 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 lets a resumed run rebuild from settled work alone, and 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.

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 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. 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. It does not say how many times anything runs. The graph is dispatched once, so a task that must retry, poll, or converge does that inside its own execution, where the author's command or the agent's own turn loop owns the repetition. A repair loop is therefore one task, not an edge back into the graph, and the engine never sees the 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 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 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.

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.

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.

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.

The engine MUST NOT terminate an attempt already in flight on cost grounds, so a completed attempt MAY carry the total past the cost ceiling. Any cost overrun MUST invalidate the run and MUST block all further dispatch and retries.

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: a cost total is known only once an attempt finishes, so killing that attempt would spend the money and learn nothing, whereas elapsed time is known continuously and a run past its window is over no matter what any task is still doing.

Separately from the run's ceilings, every task MUST execute under a bounded wall-clock deadline. Exceeding it MUST settle that task as a failure, which propagates like any other failure rather than ending the run directly: a hung task spends nothing, so the cost ceiling cannot bound it. The operator MUST set a default deadline and a maximum; a task MAY declare a shorter one, and a declared deadline exceeding the operator's maximum MUST be rejected before dispatch. A deadline MUST NOT extend a task past the run's wall-clock ceiling; whichever bound falls first settles the task.

A ceiling reached while an epilogue task is in flight MUST terminate that attempt and MUST block further dispatch, 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 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-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.

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, 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, 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 this entry with its output and staged-file flag omitted. 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. 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. 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, 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.

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 MUST NOT stage them into any other task. 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.

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, 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


Changelog

v0.1.0 (2026-08-21)

Initial draft