ota Contract Reference
July 29, 2026 ยท View on GitHub
This document describes the canonical ota.yaml authoring surface accepted by the shipped parser and validator.
Use this page as the canonical field and validation reference for the shipped contract surface.
For machine-readable contract publication, use
json-schemas/contract.json locally or
https://dist.ota.run/spec/json-schemas/latest/contract.json in CI, editors, and other tooling.
For machine-readable docs ownership publication, use
published-docs/canonical-docs.json locally or
https://dist.ota.run/spec/published-docs/latest/canonical-docs.json when a downstream surface
needs the canonical upstream source boundary for this page.
When you need operator guidance for targets, shared backends, activation, and backend fulfillment,
follow it with local-service-topology.md.
Backward-compatibility parsing may continue to accept older shapes, but new and updated contracts
should normalize to the canonical surfaces documented here.
Minimal contract
version: 1
project:
name: my-repo
In practice, most useful contracts also define tasks, runtimes, toolchains, or checks.
Primary sections at a glance
version(required): schema version for the contract itself. Today this is1.project(required): stable repo identity and high-level classification.toolchains: managed ecosystem capabilities such as Rust, Node, Java, or Python.orchestrators: repo-level task and environment mediation such asmise.runtimes: required language/runtime versions for the repo to be runnable.tools: external CLI and tool dependencies the repo expects on PATH.env: required environment variables, defaults, allowed values, and provenance-aware resolution.services: supporting services such as databases, queues, or local infra.checks: explicit preconditions and health checks that should pass.tasks: named commands humans and agents can run deterministically.readiness: reusable named readiness probes for workflow and check reuse.workflows: canonical operational paths built from setup/run tasks, required services, and readiness gates.execution: where tasks run, such as native, container, or remote backends.agent: AI-agent task hints and writable-path boundaries.exports: downstream generation preferences and export metadata.policies: repo-local policy overlays and guardrails.workspace: monorepo root/member mapping for multi-repo orchestration.
Top-level fields
version: 1
project:
name: my-repo
description: Optional description
type: application
execution:
preferred: native
supported:
- native
extensions:
demo:
kind: check_provider
command: ota-ext-demo
api_version: 1
toolchains:
rust:
version: "1.94.0"
components:
- rustfmt
fulfillment:
mode: run
node:
version: "22"
package_managers:
pnpm: "10.0.0"
fulfillment:
source: mise
mode: run
orchestrators:
mise:
kind: mise
required: true
config_files:
- mise.toml
activation:
trust: true
prepare:
install: true
env:
vars:
OTA_ENV:
required: true
default: local
allowed:
- local
- ci
surfaces:
backend:
kind: http
port: 5678
tasks:
setup:
run: pnpm install
workflows:
default: app
app:
intent: local_development
setup:
task: setup
checks:
- name: node-installed
kind: precondition
severity: error
run: node --version
agent:
entrypoint: setup
metadata:
team: platform
Top-level extensions is now recognized as adapter contract data.
Each entry is a typed adapter descriptor with kind, command, and api_version, plus optional
description, activation, and config.
Supported kinds today are check_provider, export_provider, and backend_provider.
check_provider is runnable with ota extensions --run <name> when api_version: 1 is declared.
export_provider is runnable with ota extensions --publish <name> when api_version: 1 is declared.
backend_provider is reserved for task execution backends, is discoverable in the contract, and
can be named by execution.backends.remote.provider when the repo wants a custom execution
backend. Runtime backend providers receive a structured JSON request on stdin and via
OTA_BACKEND_PROVIDER_REQUEST_JSON, then return a structured JSON response on stdout. The
request includes the extension id, kind, api version, command context, repo context path, working
directory, task name, task command, execution mode, target, cwd, and resolved environment values.
When a backend provider should participate in non-manual target activation, declare
activation.provider_managed_cleanup: true; that tells ota the provider can also handle the
follow-up activation_probe and activation_cleanup command contexts for activation-started
producer services.
The validator requires kind to be one of the supported kinds, command to be non-empty, and
api_version to be greater than zero.
Real-world use cases:
- upload a release artifact bundle to an internal endpoint
- publish scan or compliance reports through one standard adapter
- expose a custom check provider, export target generator, or execution backend in a stable contract slot
Example:
extensions:
release-upload:
kind: export_provider
command: ota-ext-upload
api_version: 1
description: Upload the release bundle to the artifact endpoint
config:
endpoint: https://artifacts.example.com/upload
artifact: dist/release.zip
extensions:
remote-shell:
kind: backend_provider
command: ota-ext-remote-shell
api_version: 1
description: Execute tasks through a custom remote backend
config:
transport: ssh
workspace_root: /workspace
Use ota extensions to inspect this contract data. Use ota extensions --run <name> for
check_provider descriptors and ota extensions --publish <name> for export_provider descriptors.
For the staged execution boundary and V6 target contract, see
extension-execution-boundary.md.
version
version: 1
Current validator support is only for 1.
project
Required.
project:
name: ota
description: Execution governance for humans and AI agents
type: cli
Fields:
name: required, non-empty stringdescription: optional stringtype: optional string
Use project for stable repo identity only. Churn-heavy descriptive fields such as author,
created_at, or publishing metadata should live under metadata unless ota grows a dedicated
package or distribution contract later.
Readiness note:
- set
project.typeto library-style values such assdkorlibrarywhen the repo is not meant to expose runnable entrypoint tasks; in that case,ota doctortreats missingtasksas a warning instead of a blocking error.
workspace
Optional.
Current V3 support is repo-level monorepo declaration:
workspace:
type: monorepo
members:
- api
- web
Fields:
type: currently onlymonorepomembers: required, non-empty list of member paths relative to the root contract directory
Current behavior:
- the root contract remains a normal
ota.yaml - member contracts live at
<member>/ota.yaml - member contracts inherit the root contract and override only what they declare
- member contracts must not declare a top-level
workspaceblock - validating the root monorepo contract also validates every declared merged member contract
- repo commands can target a member with
--member <name> - repo commands run from inside a member directory automatically load the merged member contract
- current member targeting expects the named member to be declared in
workspace.members
artifacts
Optional.
Use artifacts when a task generates a named repo-local artifact that later tasks consume. This
keeps code-generation ownership separate from broad effects.writes bookkeeping.
artifacts:
typescript-sdk:
kind: generated_source
producer: sdk:generate
paths:
- sdk/typescript/src/api/client.gen.ts
inputs:
- core/schema
-
kind: generated_source: a normal generated artifact. Consumers explicitly depend on its producer task before Ota checks that its declared outputs exist. It may additionally declarereplaywhen its existing generated lineage also needs an explicit regeneration and promotion authority chain. -
kind: replay_baseline: a baseline-first artifact. It must declarereplay; use it when no other generated-artifact lineage applies. -
Any artifact with
replayadditionally declares:replay: authority_manifest: replay/recorded-baseline.ota.json consumption: read_onlyFor a
generated_sourceartifact, producer safety remains the ordinary task-governance decision. A task that requires the artifact and lists its producer in top-leveldepends_onremains an ordinary lineage consumer; it runs the current producer and does not consume promoted replay authority. A task that requires that artifact without the unconditional producer dependency is a replay consumer. A dedicatedreplay_baselineartifact's producer must not be agent-safe, and every consumer of that dedicated artifact uses replay authority.ota baseline recordis the separate explicit recording command; it is never inferred from an agent-safe task. The artifact keeps its declaredkind, so replay authority does not erase generated-source lineage. Runota baseline record --artifact <name>to issue a receipt-bound recorded attestation, then explicitly select that exact attestation withota baseline promote --artifact <name> --attestation <path>. Ota rejects a selected replay workflow that includes the producer, and it rejects replay consumer closures that declare writes overlapping the baseline outputs, so replay cannot regenerate or mutate its own authority through a normal verification path. The committed authority manifest, not local.otahistory and not a hand-authored digest, is the portable replay authority. Itstrust_root: scm_reviewrelies on the repository delivery path to review that committed selection; Ota does not verify reviewer or signer provenance. Consumers verify its complete output identities before execution;consumption: read_onlyis strict and requires an enforceable runner-owned ephemeral-container boundary for the entire selected closure. Ota snapshots the promoted outputs outside the writable workspace mount and mounts those snapshots read-only at their declared paths.verify_unchangedis not a weaker read-only mount: it detects a changed output after execution. Dependency and post-hook steps are part of the selected strict closure and receive the same runner-owned read-only mounts. Command-capable typed preparation remains typed in the contract and is projected through that boundary; a step that cannot execute or be projected there is refused before it runs.consumption: verify_unchangedis the non-strict fallback for a backend such as native execution: Ota verifies the selected authority before launch, re-captures the complete output manifest after the consumer ends, and fails withreplay_artifact_mutation_detectedif the baseline changed. It reports a detected write and never claims that it refused one. Replay-baseline symlinks must resolve within the declared artifact output boundary; Ota rejects escaping targets instead of mounting a link that can resolve into the mutable worktree. -
producer: required task name that materializes the artifact -
paths: required non-empty repo-relative output paths owned by the artifact -
inputs: optional repo-relative source paths that define the declared derivation boundary -
ordinary generated-artifact consumers declare
requires_artifacts: [typescript-sdk]and directly depend on the producer task; replay consumers declarerequires_artifactswithout that producer dependency, because Ota resolves their promoted authority before the consumer body starts -
presence is not freshness: Ota does not infer currentness from timestamps or Git state
exports and policies
Optional.
These sections are the current shipped overlay surfaces for downstream generation and policy-driven guardrails.
Use them when you want the contract to describe derived outputs or repo-local policy intent without turning those derived artifacts into a second source of execution truth.
Current guidance:
exportsshould describe export preferences or downstream artifact intentpoliciesshould describe repo-local policy overlays and guardrails- repo contracts must not declare
policies.env; approved env values now live in the org policy pack underpolicies.env.values - repo contracts must not declare
policies.version_policy,policies.provisioning, orpolicies.adapter_bootstrap; approved version and provisioning authority now live in.ota/org-policy.yaml policies.env.valuesis the shipped approved-value map for environment variables in the org policy pack- neither section should replace core readiness fields such as
tasks,services, orchecks - newer spec drafts discuss additional policy and readiness-gate behavior; those are not part of the current shipped parser unless the implementation explicitly accepts them
execution
Optional.
execution:
preferred: native
lifecycle: persistent
supported:
- native
- container
- remote
backends:
container:
image: ghcr.io/ota/dev:latest
remote:
provider: ssh
target: sandbox-dev
cwd: /workspace
# Optional for provider: ssh only. When omitted, ota uses normal OpenSSH
# behavior (`~/.ssh/config`, agent/default identity selection, and host aliases).
ssh:
config_file: ~/.ssh/work.conf
identity_file: ~/.ssh/work_rsa
# Context model (shipped)
default_context: app
contexts:
host:
backend: native
requirements:
tools:
docker: "*"
podman: "*"
app:
backend: container
lifecycle: persistent
fulfillment: run
container:
image: ghcr.io/ota/dev:latest
requirements:
runtimes:
node: ">=24.14.1"
tools:
npm: ">=10.5"
attachments:
compose:
- local
Named-context inheritance example (additive to existing context and shorthand support):
execution:
default_context: development
contexts:
node-base:
backend: container
lifecycle: ephemeral
container:
image: node:24-bookworm
attachments:
isolated_paths:
- node_modules
- .next
development:
extends: node-base
container:
resources:
memory:
minimum: 2GiB
default: 3GiB
Execution authoring patterns (choose one default execution declaration mode):
- Single-context shorthand (lean repos with one execution shape):
execution:
preferred: container
lifecycle: ephemeral
backends:
container:
image: node:24-bookworm
- Named contexts (repos with multiple explicit execution planes):
execution:
default_context: development
contexts:
development:
backend: container
lifecycle: ephemeral
container:
image: node:24-bookworm
verify:
backend: container
lifecycle: ephemeral
container:
image: node:24-bookworm
- Named contexts with
extends(multi-context repos that want less repetition):
execution:
default_context: development
contexts:
node-base:
backend: container
lifecycle: ephemeral
container:
image: node:24-bookworm
development:
extends: node-base
container:
resources:
memory:
minimum: 2GiB
default: 3GiB
verify:
extends: node-base
extends is optional. It reduces repetition for named contexts; it does not replace shorthand for simple repos.
Named-context execution is the canonical selector whenever execution.default_context /
execution.contexts are present. In that mode:
execution.preferredis not allowedexecution.lifecycleandexecution.backendsmay still be used as root defaults that named contexts inherit from when context-local values are omitted
Supported backend values:
nativecontainerremote
Supported lifecycle values:
persistentephemeral
Current validation rule:
- if
preferredis set andsupportedis not empty,preferredmust also appear insupported execution.preferred: containerrequiresexecution.backends.container.imageexecution.backends.container.enginescan list supported OCI engine CLIs in preference order; when omitted, ota falls back todockerexecution.preferred: remoterequiresexecution.backends.remote.providerexecution.preferred: remoterequiresexecution.backends.remote.target- remote target guidance by provider:
daytona:sandbox-devssh/tsh:user@hostkubectl:pod/ota-devexecution.default_contextdeclares the context used when task-levelcontextis not setexecution.contextsdefines backend and requirement surfaces per contextexecution.contexts.<name>.extendslets a named context inherit from one parent context to avoid repetition- each
execution.contexts.<name>requires:backendand matching backend settings; contexts may declare those settings inline or inherit them from root defaults (execution.lifecycle/execution.backends)- optional
only_onto scope the context to supported host OS values (linux,macos,windows) - optional
only_archto scope the context to supported host architectures (for examplex64,arm64) - optional
container.resources.memory.minimumandcontainer.resources.memory.defaultfor container contexts - optional
envfor context-wide environment defaults that apply before task-level and mode-level env overrides - optional
requirements.<runtimes|tools>to scope readiness checks to that context - optional
attachments.composeto attach container workloads to compose project networks - optional
attachments.isolated_pathsto mount Ota-managed, engine-owned named volumes over workspace-relative dependency paths such asnode_modules
- inheritance merge rules for
extends:- scalar fields override within a backend family (
lifecycle, image/target/provider) - maps merge recursively (
container.resources,env,requirements,attachments) - lists replace (
container.engines,attachments.compose,attachments.isolated_paths)
- scalar fields override within a backend family (
- backend-family switches across
extendsare rejected (for example inheriting from acontainerparent and setting childbackend: native) extendsis additive inheritance within one backend family, not a generic "inherit anything, then replacebackendlater" escape hatch- invalid example:
- parent
backend: native, childextends: parent, childbackend: container - ota rejects this because the parent and child do not share one execution shape
- parent
Current implementation:
ota runresolves a task context fromtasks.<name>.contextandexecution.default_context, then executes that context's backend- runtime selection consumes resolved named contexts after
extendsmerge, soota run,ota up,ota doctor, andota execution planexecute the merged concrete context shape instead of partial parent/child declarations - when a selected context declares
only_onand/oronly_arch,ota doctor,ota up, and task execution fail early and explicitly on unsupported host OSes or architectures instead of falling through to later command noise execution.contextsare used for context-scoped requirement checks and receiptstasks.<name>.contextlets a task declare a non-default execution context- named contexts can now share a base execution shape through
extends, while shorthand remains the lean authoring path for shorthand-only repos ota runnow supports container execution when context or legacy config providesexecution.*.container.image- the container path uses the first available configured container engine, mounts the effective contract directory at
/workspace, overlays any declaredattachments.isolated_pathswith Ota-managed named volumes, and runs task bodies withsh -lc - ota injects
OTA_WORKSPACEinto task execution so backend-aware workspace-relative paths stay explicit without hardcoding/workspace - ota also injects
OTA_HOST_WORKSPACEinto task execution so host-launched tasks can still refer to the real repo path even when the selected backend or helper workflow path also publishesOTA_WORKSPACE - ota also injects
OTA_HOST_HOMEinto task execution so selected workflow instances can derive stable host-owned clone or cache roots without shellecho $HOME/%USERPROFILE%glue - ota injects
OTA_HOST_UIDandOTA_HOST_GIDon Unix-like hosts so host-launched service and compose paths can pass the real host user/group ids into deterministic env interpolation without shellid -u/id -gglue; on non-Unix hosts those values are absent unless the contract resolves them some other way - task env precedence is: resolved context env, then
tasks.<name>.env, then selectedtasks.<name>.execution.modes.<mode>.env - ota-derived cache env is fallback-only and currently covers
MAVEN_OPTSfor isolated.m2,NUGET_PACKAGESfor isolated.nuget/packages,NPM_CONFIG_CACHEfor isolated.npm,PNPM_STORE_DIRfor isolated.pnpm-store,GRADLE_USER_HOMEfor isolated.gradle,PIP_CACHE_DIRfor isolated.pip-cache, andPOETRY_CACHE_DIRfor isolated.pypoetry-cache; explicit task or context env still wins - container contexts can declare
container.resources.memoryso ota requests a deterministic container memory limit;ota run --memory <size>overrides one run while keeping task identity and internal listener bind ports unchanged ota upnow runs thesetuptask in the task's resolved context backendota runsupports remote execution when the resolved context or legacyexecution.backends.remotedeclaresproviderandtarget- current shipped remote providers are
daytona,ssh,tsh, andkubectl - the current remote path shells out to the local provider CLI with optional
execution.backends.remote.cwd ota upruns itssetuptask through the same remote context/backend path when remote execution is selected or explicitly overridden- remote provisioning and remote workspace selection are still out of scope today
Current lifecycle meaning:
persistent: whenexecution.preferred: containeris configured,ota runand thesetuptask insideota upreuse a persistent named container for the effective contract directoryephemeral: whenexecution.preferred: containeris configured,ota runand thesetuptask insideota upuse a freshrun --rmcontainer with the first available configured engine for each invocation- outside backend-backed task execution, such as service startup, service readiness, and diagnosis, lifecycle remains advisory today
- explicit
ota run/ task-backedota uplifecycle overrides are never advisory: a selected native or remote task path without a managed shared backend refuses--ephemeralor--persistentbefore execution starts
Current command behavior:
ota doctorwarns whenephemeralis declared and surfaces container dependency isolation in execution summaries when contexts declareattachments.isolated_pathsota validateandota doctoralso warn whendepends_oncrosses execution boundaries in a way that drops in-place prep value, and when a declared isolated cache path is likely unused by the tool configurationota runprints a lifecycle note on stderr and can execute via the configured container backendota runcan also override execution mode and lifecycle for one invocation with--mode,--lifecycle, or the shorthand--ephemeralota upcan also override execution mode and lifecycle for thesetupphase with--mode,--lifecycle, or the shorthand--ephemeralota upprints the same lifecycle note on stderr when itssetupphase uses backend-backed executionota cleanremoves current contract-derived Ota-managed persistent containers and dependency-isolation volumes for container contextsota cleanalso rediscovers drifted Ota-managed persistent containers and dependency-isolation volumes by ownership metadata (dev.ota.managed, cleanup-kind/lifecycle labels, and repo ownership token), even when the contract has drifted away from the original declaration- persistent container reconciliation treats execution shape drift as recreate-worthy, including Compose attachment namespace changes from
execution.contexts.<name>.attachments.compose - repo cleanup identity is anchored by
.ota/state/ownership-idand tracked repo-used engines in.ota/state/managed-engines, so drifted cleanup can stay scoped to the repo instead of matching byproject.name ota cleancurrently has no remote cleanup action; remote-backed repos reportNo cleanup neededtodayota doctorchecks the required backend CLI for the selected execution context or preferred backend and reports unsupported shipped remote providers earlyota doctorwarns on suspicious remote target shape (ssh/tshwithoutuser@host,kubectlnot startingpod/)ota doctorevaluates context-specific requirements for declared contexts- when preconditions already contain blocking errors,
ota doctorstops before later service/check readiness probing so blocked setups stay bounded and the primary blocker remains obvious ota upstill runs service startup, service readiness, and diagnosis on the host unless the resolved execution path is containerized
services
Optional.
services:
api:
required: true
manager:
kind: compose
name: local
file: compose.yaml
service: api
endpoints:
host:
address: 127.0.0.1
port: 3000
readiness:
from: host
kind: http
path: /health
success:
status: [200]
depends_on:
- postgres
postgres:
required: true
manager:
kind: compose
name: local
file: compose.yaml
service: postgres
endpoints:
host:
address: 127.0.0.1
port: 5432
app:
address: postgres
port: 5432
readiness:
from: host
kind: tcp
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
redis:
required: true
manager:
kind: compose
name: local
file: compose.yaml
service: redis
readiness:
kind: compose_health
postgres-host:
manager:
kind: host
start:
exe: brew
args:
- services
- start
- postgresql@17
stop:
exe: brew
args:
- services
- stop
- postgresql@17
endpoints:
host:
address: 127.0.0.1
port: 5432
readiness:
from: host
kind: tcp
Fields:
required: optional booleanproducer: optional object declaring that this service is owned by another workspace repo task instead of local service-manager truthproducer.repo: required workspace repo name declared underota.workspace.yamlproducer.task: required producing task name in that repo'sota.yamlproducer.listener: optional named runtime listener on that producer task; omit it only when the producer exposes exactly one declared listenerproducer.address_view: optional reachable address shape; the current shipped cross-repo service slice supportshostonly and defaults tohostmanager: optional object with:kind: compose|hostengine: optional compose CLI engine forkind: compose;dockerby default,podmanalso supportedname: compose project name (composerequired)service: compose service name whenkind: composefile: optional compose file path whenkind: composeenv_file: optional Compose interpolation file whenkind: composeprofiles: optional Compose profile list whenkind: composestart: optional structured host start command whenkind: hoststop: optional structured host stop command whenkind: host
endpoints: optional named projections of reachable service address/portdepends_on: optional list of service namesreadiness: optional explicit readiness check that runs in a named execution contextreadiness.from: context name that owns the runtime for the checkreadiness.endpoint: optional named endpoint projection to use when one context has multiple service endpoints- structured
readiness.kind:tcp,http, orcompose_health - structured
readiness.method: optional HTTP method, defaultGET - structured
readiness.path: required for structured HTTP readiness and must start with/ - structured
readiness.headers: optional HTTP request headers - structured
readiness.success.status: optional exact accepted HTTP status-code set - structured
readiness.body.contains: optional exact required response substring; it must not be combined withmethod: HEAD - structured
readiness.interval: optional wait between probe attempts - structured
readiness.timeout: optional per-attempt probe timeout - structured
readiness.retries: optional failed probe budget before the service readiness gate fails; when omitted, Ota uses a bounded default budget (120 probe attempts) and reports failure when that budget is exhausted - structured
readiness.start_period: optional delay before the first structured readiness probe
Current behavior:
- services are part of the accepted V1 contract surface
services.<name>.produceris the canonical cross-repo service-ownership surface when a required service is produced by another repo in the sameota.workspace.yaml- producer-owned services stay intentionally explicit today:
- only
producer.address_view: hostis supported - the producer listener must declare one fixed
project.hostendpoint ota doctor,ota up, andota runmay reuse or start that producer through the owning repo contract before the consumer proceeds
- only
- producer-owned services must not also declare local manager truth such as
manager,endpoints, orreadiness tasks.<name>.requires_servicesremains the consumer-side dependency truth; producer ownership lives onservices.<name>, not inside each consumer task- service declarations should model local service ownership with
manager, reachability withendpoints, and readiness withreadiness services.<name>.readinesshas three canonical forms:- reusable probe form:
from+probe(+ optional polling controls such asinterval,retries, andstart_period) - structured probe form:
from+kind(+pathfor HTTP, with optional request/response/timing controls) - structured compose-health form:
kind: compose_healthfor compose-managed container health state without endpoint/host-port probing
- reusable probe form:
- unknown
depends_onreferences are invalid - service dependency cycles are invalid
readiness_gateis a later-spec draft field and is not accepted by the current shipped parserota doctorevaluates manager-owned service readiness through the declared control plane and endpoint topology- for
manager.kind: compose,ota doctorderives compose lifecycle commands from manager metadata - for
manager.kind: host, canonical lifecycle ownership lives onmanager.start/manager.stop; legacy top-levelstart/stopstill parse for compatibility, but new authoring should keep host service lifecycle under the manager block - for
manager.kind: hostwithmanager.host.kind: systemd, ota derives lifecycle from the declared unit instead of requiring shellsystemctlglue services.<name>.lifecycle.teardown_assertion: manager_inactivedeclares that a future lifecycle-proof lane may require the manager's positive inactive-state observation after transaction-owned teardown; it is valid only with a typed manager, never an inverse readiness probe. Systemd observation also requires the declared unit to resolve as loaded; an unknown unit is not treated as inactiveservices.<name>.lifecycle.teardown_assertion: boundary_terminatedis a narrower lifecycle proof capability for structured host-manager commands. It is admitted only when the selected lifecycle workflow resolves to an Ota-owned ephemeral container session; Ota runs the declaredmanager.startandmanager.stopcommands inside that session and must confirm removal of the exact session before publishing the terminal observation. It does not claim the host manager is inactive or that application output was proved- for
manager.kind: host,ota doctorruns readiness checks in the resolved host command context - legacy
services.<name>.readiness.runstill parses for compatibility, but new authoring should keep service readiness on structuredreadiness.kindor reusablereadiness.probe services.<name>.readiness.fromselects the execution context for service readinessservices.<name>.readiness.endpointselects one named endpoint projection whenfromalone is ambiguousservices.<name>.readiness.probecan reference one top-levelreadiness.probes.<name>declaration so service-manager readiness reuses the same transport and timeout truth as checks and workflows whilefrom/endpointstill select the service endpoint projection- structured
services.<name>.readiness.kind: httpprobes the declared endpoint with the same request/response model shipped for task runtime readiness - structured
services.<name>.readiness.kind: tcpprobes the declared endpoint for listener reachability from the declared context - structured
services.<name>.readiness.kind: compose_healthreads the compose-managed container health status directly (healthy) and does not requirereadiness.fromorservices.<name>.endpoints - structured
services.<name>.readiness.kind: systemd_activereads systemd unit state directly (systemctl is-active --quiet) and does not requirereadiness.fromorservices.<name>.endpoints kind: compose_healthrequiresservices.<name>.manager.kind: composeand must not declare endpoint-probe fields such asfrom,endpoint,method,path,headers,success,body, ortimeoutkind: systemd_activerequiresservices.<name>.manager.kind: hosttogether withservices.<name>.manager.host.kind: systemdand must not declare endpoint-probe fields such asfrom,endpoint,method,path,headers,success,body, ortimeout- reusable and structured top-level service readiness use the same default wait model as task runtime readiness: when
retriesis omitted, Ota uses the default bounded budget and reports failure after the limit is reached; declaringretriesmakes that budget explicit and tuned for the service services.<name>.endpoints.<name>declares one named endpoint projection:context: optional execution context for that projection; when omitted, the endpoint name is also the context name for backward compatibilityaddress: required reachable address for that context projectionport: required reachable port for that context projection
- failed required service readiness checks are blocking errors
- failed optional service readiness checks are warnings
- timed out required service readiness checks are blocking errors
- timed out optional service readiness checks are warnings
- required services without declared readiness produce a warning because readiness cannot be verified yet
ota upstarts required services, and required-service dependencies, in declared dependency order beforesetupota uptreats each required service readiness check as a readiness gate before moving on to dependents- ota still does not provide deep service orchestration beyond explicit contract commands
toolchains
Optional.
Use toolchains when ota must understand more than "does this executable exist?" This is the
managed ecosystem layer. It owns capability truth for language environments such as Rust, Node,
Java, Python, Go, Ruby, and .NET, without forcing that truth into ad hoc shell setup.
Current shipped scope is intentionally narrow:
- top-level
toolchains - top-level
orchestrators - execution-context-scoped
execution.contexts.<name>.requirements.toolchains - task-scoped
requirements.toolchains - canonical shipped fulfillment paths for Rust, Node, Java, Python, Go, Ruby, and .NET toolchains
- explicit non-canonical run-path fulfillment when
fulfillment.sourcepoints at another shipped source such asmise - task execution mediation through
tasks.<name>.execution.orchestrator - duplicate ownership is invalid when the same prerequisite is declared under both
toolchainsandruntimesortools
Example:
toolchains:
rust:
version: "1.94.0"
profile: minimal
components:
- rustfmt
targets:
- x86_64-unknown-linux-musl
fulfillment:
mode: run
node:
version: "24.15.0"
package_managers:
pnpm: "10.33.4"
fulfillment:
source: mise
mode: run
orchestrators:
mise:
kind: mise
required: true
config_files:
- mise.toml
activation:
trust: true
prepare:
install: true
tasks:
setup:
requirements:
toolchains:
- rust
run: cargo fetch
server:verify:
run: //server:ci-unit
execution:
orchestrator:
ref: mise
mode: task
Rules:
- toolchain names must not be empty
versionmust not be empty- shared toolchain fields are
version,fulfillment,required,only_on, andplatforms.<os>.version; legacyprovideris still accepted for compatibility, but it is no longer the canonical public shape - shipped toolchain names are fixed:
toolchains.rust,toolchains.node,toolchains.java,toolchains.python,toolchains.go,toolchains.ruby, andtoolchains.dotnet requireddefaults totrueand controls whether missing or mismatched toolchains are blocking- ota validates and interprets toolchains through a shipped ownership contract for each toolchain
name; the canonical shipped fulfillment sources are
rustup,corepack,sdkman,uv,go,ruby, anddotnet - supported
fulfillment.modevalues today are onlynoneandrun fulfillment.mode: noneis the default diagnose-only lane; use it when ota should check the toolchain truth but not provision or activate it on the selected pathfulfillment.mode: runallows selectedota run/ workflowota upexecution paths to provision the declared toolchain on the run path; use it only when the selected repo path should let ota own fulfillment through the declared sourcefulfillment.sourceis optional when the toolchain uses its canonical shipped fulfillment pathfulfillment.source: miseis the current non-canonical supported source for repos whose selected path is mediated bymise- legacy flat
fulfillment: run/fulfillment: nonestill parse for compatibility, butota validateandota doctornow warn and push authors onto structuredfulfillment - legacy
providermust match the shipped canonical source for that toolchain name; mismatched legacy providers fail validation, and matching legacy providers now also warn so authors migrate onto canonical toolchain-owned structuredfulfillment profile,components, andtargetsremain Rust-specific managed-surface fieldspackage_managersremains the toolchain-owned package-manager surface for Node, Python, and Ruby where applicabletoolchains.python.package_managerscurrently acceptsuvandpoetry;uvremains the canonical Python fulfillment source today, and ota can now use that same selected Python fulfillment lane to install declared Poetry versions on the selected run path before task execution- standalone
tools.poetryremains temporarily accepted for compatibility, butota validateandota doctornow warn and recommend migrating Poetry ownership totoolchains.python.package_managers.poetry only_on, when set, scopes the toolchain tolinux,macos, orwindowsplatformsmay overrideversion,profile,components,package_managers, andtargetsper OS usinglinux,macos, orwindowsplatformsentries must also appear inonly_onwhenonly_onis declaredprofile, when set, must not be emptycomponentsandtargetsentries must not be empty- for canonical Rust fulfillment with
fulfillment.mode: run,versionmust be one installable Rustup toolchain reference such asstable,beta,nightly, or1.94.0 - for canonical Python fulfillment with
fulfillment.mode: run,versionmust be one installable uv Python reference such as3.12,3.12.10, or3.13 - canonical Node, Java, Go, Ruby, and .NET fulfillment all support
fulfillment.mode: runon the selected path; provisioning authority still stays with org policy and backend requirement fulfillment - for canonical Ruby fulfillment with
fulfillment.mode: run, ota currently uses the selected Ruby to hydrate the declared Bundler lane structurally viaruby -S gem install bundler --no-document --version <constraint>whentoolchains.ruby.package_managers.bundleris declared fulfillment.source: misewithfulfillment.mode: runallows ota to usemise installfor the selected toolchain and any declared package-manager entries on that pathfulfillment.source: misemust not be combined with managed-surface fields such ascomponentsortargets- duplicate ownership is invalid; if the same Rust capability is also declared under
runtimesortools, validation fails and the duplicate must be removed; the same applies totoolchains.nodeversusruntimes.nodeortools.node, andtoolchains.pythonversusruntimes.python,toolchains.goversusruntimes.go,toolchains.rubyversusruntimes.ruby, andtoolchains.dotnetversusruntimes.dotnetortools.dotnet
Ownership boundary:
- use
toolchainsfor managed ecosystems - use
orchestratorswhen the repo has one declared manager that mediates trust, install, and task execution on the selected path - use
runtimesfor simple unmanaged runtime version checks - use
toolsfor standalone commands on PATH - use
native_prerequisitesfor host-native build bundles and shell activation - current shipped ownership is name-defined, not free-form: ota derives Rust capability ownership
from
toolchains.rust, Node runtime/executable plus declared package-manager ownership fromtoolchains.node, Java plusjavacownership fromtoolchains.java, Python runtime ownership fromtoolchains.python, Go runtime ownership fromtoolchains.go, Ruby runtime plus Bundler ownership fromtoolchains.ruby, and .NET runtime/CLI ownership fromtoolchains.dotnet
If a declared toolchain owns the capability, require the toolchain. Do not also require the same runtime or tool unless it is deliberately standalone outside that toolchain.
For the full ownership model and migration examples, see toolchains-runtimes-tools.md.
orchestrators
Optional.
Use orchestrators when the repo has one declared manager that owns selected-path trust,
environment preparation, and mediated task execution.
Example:
orchestrators:
mise:
kind: mise
required: true
config_files:
- mise.toml
activation:
trust: true
prepare:
install: true
devenv:
kind: devenv
required: true
config_files:
- devenv.nix
launcher:
exe: nix
args:
- run
- github:cachix/devenv/main#devenv
- --
Rules:
- orchestrator names must not be empty
- shipped orchestrator kinds are currently
mise,devbox, anddevenv config_filesentries must not be emptylauncher.exe, when declared, must not be emptylauncher.args, when declared, must not contain empty entriesactivation.trust: trueis currently supported only formiseprepare.install: trueis currently supported formiseanddevbox, but notdevenvlauncheris the first-class lane for repos whose orchestrator is executed through another host command instead of a direct PATH binary; ota then requires and probes the launcher executable on the selected path instead of the orchestrator name itself- orchestrators do not replace
toolchains; usetoolchainsfor capability truth andtasks.<name>.execution.orchestratorwhen the selected task body must run through that manager
runtimes
Optional.
Simple form:
runtimes:
node: "22"
python: ">=3.12"
Detailed form:
runtimes:
java:
version: "21"
distribution: temurin
node:
version: "22"
provider: volta
pwsh:
version: "7.6.0"
only_on:
- windows
platforms:
windows:
distribution: zulu
Rules:
- runtime names must not be empty
- versions must not be empty
requireddefaults totrueand controls whether missing or mismatched runtimes are blockingonly_on, when set, scopes the runtime tolinux,macos, orwindowsprovider, when set, must not be emptydistribution, when set, must not be emptyplatformsmay overrideversion,provider, anddistributionper OS usinglinux,macos, orwindowsplatformsentries must also appear inonly_onwhenonly_onis declared- workspace overlays may specialize member runtime requirements, but the winning value must be explainable
- when a managed ecosystem is already declared under
toolchains, prefer that owner and avoid repeating the same capability here
Version syntax examples:
8is an example of an exact required version>=8is an example of accepting any version at or above8^8is an example of a compatible version range, usually the same major line- ota compares numeric version parts and accepts common prefixes such as
go1.24.2orv1.24.2 - use
>=when you want to accept newer versions explicitly - use
^when you want to express compatibility rather than a strict floor
Runtime detail fields:
required: optional boolean; defaults totrueonly_on: optional OS inclusion list; if omitted, the runtime is required on all supported OSesprovider: optional runtime manager or provisioning source hint such asvoltadistribution: optional runtime flavor where version alone is not sufficient, especially Java distributions such astemurin,corretto,graalvm,oracle, orzuluplatforms: optional per-OS overrides keyed bylinux,macos, orwindows
Use only_on to scope where a runtime is required, and use platforms only when values change on a matching OS.
required: false keeps the runtime active but downgrades missing/version mismatch findings to warnings.
Root fields act as the default values, and the matching platforms.<os> entry overrides them for that OS.
Use runtimes for simple unmanaged runtime checks. For the ownership boundary with managed
toolchains and standalone tools, see toolchains-runtimes-tools.md.
tools
Optional.
Simple form:
tools:
pnpm: "10"
uv: "0.6.0"
Detailed form:
tools:
pnpm:
version: "10"
acquisition:
provider: corepack
package: pnpm
version: "10.0.0"
helm:
version: ">=3.8"
platforms:
linux:
acquisition:
provider: apt
package: helm
macos:
acquisition:
provider: brew
package: helm
source_config:
tap_name: vendor/tap
tap_url: https://github.com/vendor/homebrew-tap
windows:
acquisition:
provider: winget
package: Helm.Helm
pwsh:
version: "7.6.0"
only_on:
- windows
bun:
version: ">=1.2.0"
acquisition:
provider: command
shell: sh
run: curl -fsSL https://bun.sh/install | sh
yq:
version: "4.52.5"
acquisition:
provider: release_asset
source_config:
asset_by_platform:
linux_x86_64: https://example.com/releases/v{version}/yq_linux_amd64
linux_aarch64: https://example.com/releases/v{version}/yq_linux_arm64
macos_x86_64: https://example.com/releases/v{version}/yq_darwin_amd64
macos_aarch64: https://example.com/releases/v{version}/yq_darwin_arm64
windows_x86_64: https://example.com/releases/v{version}/yq_windows_amd64.exe
version_args:
- --version
migrate:
version: "4.19.1"
acquisition:
provider: release_asset
source_config:
asset_by_platform:
macos_aarch64:
url: https://github.com/golang-migrate/migrate/releases/download/v{version}/migrate.darwin-arm64.tar.gz
archive:
format: tar_gz
executable_path: migrate
version_args:
- -version
Rules:
- tool names must not be empty
- versions must not be empty
requireddefaults totrueand controls whether missing or mismatched tools are blockingonly_on, when set, scopes the tool tolinux,macos, orwindowsplatformsmay overrideversionandacquisitionper OS usinglinux,macos, orwindowsplatformsentries must also appear inonly_onwhenonly_onis declaredacquisitionoptionally declares how ota can activate or provision the tool safely when a selected task/workflow requires itacquisition.provider: supported values arecorepack,command,release_asset,apt,brew,winget,choco, andscoopacquisition.packageandacquisition.versionare required forprovider: corepacktool nodecannot useprovider: corepack; declare Node undertoolchains.nodeinstead, and use structuredfulfillmentthere when ota should own package-manager activation on the selected path.acquisition.shellandacquisition.runare required forprovider: commandprovider: release_assetis provisioning-owned rather than activation-owned; it must declaresource_config.asset_by_platform, may declare optionalsource_config.version_args, and must not declarepackage,version,shell, orrun- each
source_config.asset_by_platform.<platform>entry may be either a direct asset URL string or an object withurlplus optional archive extraction metadata source_config.asset_by_platform.<platform>.archive.formatcurrently supportstar_gzandzipsource_config.asset_by_platform.<platform>.archive.executable_pathtells ota which file inside the extracted archive becomes the final executable in.ota/state/source-managed/bin- package-manager-backed tool acquisition (
apt,brew,winget,choco,scoop) is provisioning-owned rather than activation-owned; ota keeps the tool identity undertoolsand emits a provisioning request for the selected task/workflow path instead of treating the tool as a native prerequisite release_assetuses the tool key as the executable identity and projects an exact selected-pathrelease-assetprovisioning request instead of asking authors to hide binary downloads in shell glue- package-manager-backed tool acquisition may also declare provider-owned
source_configwhen the package manager itself needs explicit source truth such as a Homebrew tap, Winget source, Chocolatey feed, Scoop bucket, or apt sources list - package-manager-backed tool acquisition may declare
packagewhen the install identifier differs from the tool key; it must not declareversion,shell, orrun source_configmust not be empty and is valid on package-manager-backed acquisition andrelease_assetprovider: corepackactivates package-manager-managed tools such aspnpmthroughcorepack enable && corepack prepare <package>@<version> --activateprovider: commandruns one explicit shell command as the acquisition lane for that tool; use it when the repo truth is "this tool becomes available through this command", not "install it any way you want"provider: release_assettells ota to download an approved executable artifact into its source-managed tool path when the selected task/workflow path requires that tool; when a platform asset declaresarchive, ota downloads the archive, extracts the declared executable, and installs that executable into the same managed path- selected task/workflow paths may still require a release-asset tool with a wildcard such as
tools: { yq: "*" }; ota keeps the exact owned version fromtools.<name>.versionfor doctor, dry-run, and execution instead of downgrading selected acquisition truth to the wildcard - native follow-up diagnosis also checks ota's managed
.ota/state/source-managed/bintool path forrelease_assettools after fulfillment, so repo-owned standalone binaries do not immediately misdiagnose as missing just because they were not installed into a host-global PATH - package-manager-backed acquisition belongs in
tools, notnative_prerequisites; usenative_prerequisitesfor host-native bundles such as compiler stacks, Xcode CLT, or Visual Studio Build Tools - Corepack
packageandversionvalues must be shell-safe tokens; use package names likepnpmand activation versions like10.22.0 - command acquisition
shellmay usesh,bash,zsh,pwsh, orcmd - some tool keys map to different executables; for example,
tools.mavenis checked viamvn - workspace overlays may specialize member tool requirements, but provenance must remain visible in diagnosis output
- when a managed ecosystem is already declared under
toolchains, prefer that owner and avoid repeating the same capability here - selected non-native task/workflow paths (container or remote) do not automatically inherit
host-global
toolsfallback when no scoped tool requirements are declared; declare non-native tool requirements on the selected task path (tasks.<name>.requirements.tools) or selected execution context (execution.contexts.<name>.requirements.tools) - selected execution contexts may also own toolchain capability truth directly through
execution.contexts.<name>.requirements.toolchains; use that when a managed ecosystem such as Python or Node applies only on one named path instead of the whole repo
Use only_on to scope where a tool is required, and use platforms only when values change on a matching OS.
required: false keeps the tool active but downgrades missing/version mismatch findings to warnings.
Root fields act as the default values, and the matching platforms.<os> entry overrides them for that OS.
acquisition is attached to tool truth, while tasks.<name>.requirements.tools selects when that
tool actually applies. Use tools.<name>.acquisition for tool availability. Use
native_prerequisites for OS-native build bundles such as compilers or Visual Studio Build Tools.
Use tools for standalone commands, not for toolchain-owned capabilities such as Rustup-managed
cargo or rustfmt. For the ownership boundary, see
toolchains-runtimes-tools.md.
native_prerequisites
Optional.
Use native_prerequisites for OS-native build-tool bundles that are not language runtimes or CLI
tools. This is the right fit for prerequisites such as Linux compiler packages, macOS Xcode Command
Line Tools, or Windows Visual Studio Build Tools. Ota diagnoses these through the selected
platform precondition check or a structured platform probe and gives OS-specific install guidance.
For selected native task paths, ota up and ota run may also fulfill declared package-manager
guidance from this surface (apt, brew, winget, choco, scoop) before rerunning
preconditions. Advisory host setup such as xcode_clt, visual_studio, install, and note
remains explicit guidance rather than a silent host mutation path.
Example:
native_prerequisites:
node-native-build-tools:
description: Native compiler toolchain for packages with native addons
platforms:
linux:
check: node-native-build-tools-linux
apt:
- build-essential
- python3
macos:
check: node-native-build-tools-macos
xcode_clt: true
windows:
visual_studio:
components:
- Microsoft.VisualStudio.Component.VC.Tools.x86.x64
requires:
runtimes:
python: ">=3.10"
winget:
- Microsoft.VisualStudio.2022.BuildTools
activation:
kind: visual_studio_dev_shell
arch: x64
nix-shell:
description: Repo-local shell environment that exports compiler and package paths
platforms:
linux:
check: nix-shell-ready
activation:
kind: command
shell: bash
run: source .env/nix-shell.sh
checks:
- name: node-native-build-tools-linux
kind: precondition
severity: error
run: sh -c "cc --version && python3 --version"
- name: node-native-build-tools-macos
kind: precondition
severity: error
run: sh -c "xcode-select -p && python3 --version"
- name: nix-shell-ready
kind: precondition
severity: error
run: env | grep NIX_CC
tasks:
setup:
run: pnpm install
requirements:
native:
- node-native-build-tools
Rules:
- each native prerequisite must declare at least one
platforms.<os>guidance entry native_prerequisites.<name>.checkmay provide a shared fallback check; otherwise eachplatforms.<os>.checkmust reference a declaredkind: preconditioncheck, unless the selected platform entry declares a structured Ota-owned probe such aswindows.visual_studioplatformsmay uselinux,macos, andwindows- platform entries may declare
apt,brew,winget,choco,scoop,xcode_clt,visual_studio,activation,install, ornoteguidance platforms.windows.visual_studio.componentslists Visual Studio Installer component IDs that Ota checks withvswhere; this is preferred over embedding a raw PowerShellvswherecommand inchecks.runplatforms.<os>.requiresmay declareruntimes,tools,toolchains,env, andchecksthat belong to that native prerequisite on that host OS; tasks should reference the native bundle throughrequirements.nativeinstead of duplicating those checks at the task levelactivation.kind: visual_studio_dev_shellis the Windows MSVC activation hint for checks and native task execution that requirecl/MSVC tools from a Visual Studio Developer shell;archdefaults tox64activation.kind: commandis the generic task-scoped shell-environment activation form; it must declare bothshellandrun, and ota executes that shell activation before the selected native check or native task body- when a native task path selected by
ota uporota runreferences a Windows native prerequisite withactivation.kind: visual_studio_dev_shell, ota runs that task inside the activated Developer Shell instead of assuming the user already opened one manually - when a native task path selected by
ota uporota runreferencesactivation.kind: command, ota captures the declared shell environment and applies it to the selected native task body; the same activation also wraps the selected precondition check path - when one task references multiple native prerequisites for the same platform, any declared activation hints must agree; conflicting activation kinds or architectures are rejected
- Ota only evaluates native prerequisites selected by
tasks.<name>.requirements.native ota doctorremains non-mutating for native prerequisitesota upandota runmay fulfill selected native prerequisite package-manager guidance fromapt,brew,winget,choco, andscoop, then rerun preconditions- keep package-manager lanes aligned to the platform entry they live under; for example, prefer
aptunderlinux,brewundermacos, andwinget/choco/scoopunderwindowsinstead of mixing likely wrong-OS package-manager lanes into the same platform entry - avoid mixing opaque
installshell glue with manager-owned package lanes on the same platform entry when the shell command is only there to install host packages; keep package-manager truth underapt/brew/winget/choco/scoop, and reserveinstallfor the remaining manual step only when no first-class lane owns it yet - when an org policy pack is active, native prerequisite package fulfillment must also be approved
under
policies.native_packages.<manager>.approved; Ota does not silently bypass that policy gate - use policy-backed provisioning for standalone tools or runtimes Ota is allowed to install
through approved source selection; use
policies.native_packagesfor host package-manager bundles owned bynative_prerequisites
env
Optional.
env:
vars:
OTA_ENV:
required: true
secret: false
default: local
allowed:
- local
- ci
PATH:
prepend:
- ./node_modules/.bin
- /opt/ota/bin
sources:
- kind: dotenv
path: .env.local
- kind: dotenv
path: .env
must_exist: true
- kind: properties
path: config/runtime.properties
- kind: json
path: config/runtime.json
- kind: yaml
path: config/runtime.yaml
- kind: toml
path: config/runtime.toml
profiles:
docker-build:
sources:
- kind: dotenv
path: .env.docker-build
env:
REDIS_HOST: redis
NEXTAUTH_URL: http://web:3000
render:
dotenv:
path: .env.docker-build
include:
- DATABASE_URL
Fields:
vars: env-variable requirements keyed by env namesources: ordered declared env sourcesprofiles: optional reusable env overlay profiles keyed by profile name
This is not only a validation surface. Root env is the repo-wide execution contract ota uses to
resolve values before ota run and ota up start a process.
env.vars.<NAME> fields:
required: optional booleansecret: optional boolean; secret values are redacted in execution receipts and are not passed through remote shell wrappersdefault: optional stringallowed: optional list of allowed valuesprepend: optional list of path entries to add before the resolved value when the env key isPATH; these take priorityappend: optional list of path entries to add after the resolved value when the env key isPATH; these act as fallback locations
env.sources[] fields:
kind: source type; ota ships curateddotenv,properties,json,yaml, andtomlpath: source path relative to the contract directorymust_exist: optional boolean; whentrue, the source artifact itself is part of readiness
Declared source rules:
- source files are loaded only when explicitly declared in
env.sources - precedence is unchanged: policy values, then process env, then declared sources in order, then
default propertiesis a flat key-value sourcejsonmust have an object rootyamlmust have an object roottomlmust have a table root- nested
json,yaml, andtomlobjects flatten with.before env-key normalization - only scalar leaf values are allowed in structured sources: string, number, bool
null, arrays, object leaf values, and unsupported scalar classes such as TOML datetimes are rejected- for
properties,json,yaml, andtoml, ota normalizes keys by trimming, replacing.,-, whitespace,/, and:with_, collapsing repeated separators, and uppercasing the final env key - if two keys in the same declared source normalize to the same env key, ota fails that source load explicitly
env.profiles.<name> fields:
sources: optional ordered declared env sources prepended for the selected profileenv_files: optional ordered repo-relative dotenv overlays injected into selected workflow tasks before task-ownedenv_filesenv: optional literal env overlay injected into selected workflow tasks before task-ownedenvrender.dotenv.path: optional repo-relative dotenv artifact path ota should materialize during workflow executionrender.dotenv.template: optional repo-relative dotenv template ota should use as the base content before applying rendered env overlays; keep it separate fromrender.dotenv.pathrender.dotenv.include: optional ordered env names ota should resolve and emit into that dotenv artifactrender.files[]: optional ordered structured artifact renders ota should materialize during workflow executionrender.files[].path: required repo-relative output path for the rendered artifactrender.files[].format: required structured format; current shipped values arejsonandtomlrender.files[].sources: required ordered repo-relative source chunks merged from lowest to highest precedence after placeholder substitutionrender.files[].merge_into_existing: optional boolean; when true, ota treats an existing output file as the lowest-precedence merge layer before applying the declared source chunks
Profile rules:
- profiles do not replace repo-wide
env.vars; they specialize how one selected workflow path resolves and injects env truth - profile
sourcesare prepended ahead of rootenv.sourcesso profile-owned source truth wins before falling back to the repo baseline - profile
env_filesandenvare low-precedence workflow overlays: task-ownedenv_files, taskenv, and mode-branchenvstill win when they declare the same values - rendered dotenv artifacts are injected automatically into selected workflow tasks as the first
profile-owned
env_file; do not duplicate the same path inenv_files - rendered dotenv artifacts are re-rendered deterministically on each workflow-owned execution
path before they are consumed:
ota up,ota proof runtime, and directota runfor tasks in the selected workflow task closure all materialize the artifact from contract truth - when
render.dotenv.templateis declared, ota starts from that template and then replaces the declared profile/env keys, so workflow-owned compose interpolation no longer requires a separateensure_env_fileprepare task - rendered structured artifacts are also re-materialized deterministically on workflow-owned execution paths, so generated JSON/TOML config truth no longer needs repo-local merge helpers
- structured render source chunks stay immutable repo truth; ota writes only the declared
render.files[].pathartifact and merges latersources[]entries over earlier ones - selected workflow-instance env overlays participate in structured render placeholder substitution, so one workflow family can materialize per-instance client config without flattening that truth into helper scripts
- use profiles when one workflow/runtime shape needs a truthful env overlay without hiding that selection in shell glue or ad hoc setup notes
Example:
version: 1
project:
name: workflow-rendered-env
env:
vars:
DATABASE_URL:
required: true
profiles:
compose:
sources:
- kind: dotenv
path: .env.local
env:
REDIS_HOST: redis
render:
dotenv:
path: .env.compose
template: .env.example
include:
- DATABASE_URL
services:
api:
manager:
kind: compose
name: local
file: docker-compose.yml
service: api
env_file: .env.compose
tasks:
dev:
run: pnpm dev
requires_services:
- api
workflows:
default: compose-dev
compose-dev:
env:
profile: compose
run:
task: dev
PATH is a standard search-path env var, so it is the one env key that supports structured
composition. Most env vars are simple single values instead.
Use PATH when the repo needs to control executable search order. Use ordinary env values like
JAVA_HOME when the repo needs one explicit location.
Examples:
env:
vars:
PATH:
prepend:
- ./node_modules/.bin
- /usr/local/cargo/bin
If the existing PATH is /usr/local/bin:/usr/bin:/bin, the final value becomes
./node_modules/.bin:/usr/local/cargo/bin:/usr/local/bin:/usr/bin:/bin.
env:
vars:
JAVA_HOME:
required: true
default: /opt/jdk-22
This sets one explicit Java location. It does not merge with other values.
env:
vars:
DISCORD_TOKEN:
required: true
secret: true
SUPABASE_URL:
required: true
sources:
- kind: dotenv
path: .env.local
- kind: dotenv
path: .env
must_exist: true
This makes declared source loading explicit instead of magical:
- ota reads
.env.local, then.env .env.localis optional.envmust exist- process env and
policies.env.valuesstill outrank both files
env:
vars:
APP_PORT:
required: true
FEATURE_FLAGS_BETA_ENABLED:
required: true
sources:
- kind: properties
path: config/runtime.properties
- kind: json
path: config/runtime.json
With:
config/runtime.properties:app.port=8080config/runtime.json:{"feature flags":{"beta-enabled":true}}
ota resolves:
APP_PORT=8080FEATURE_FLAGS_BETA_ENABLED=true
policies:
env:
values:
DATABASE_URL: postgres://policy.internal/app
RELEASE_CHANNEL: stable
This is the org policy side of env resolution:
- the repo contract still declares the env names in
env.vars - policy can supply the winning value through
policies.env.values - policy does not invent new repo requirements on its own
tasks:
test:
env:
CI: "true"
NODE_ENV: test
This sets ordinary task-scoped env values directly.
env:
vars:
DATABASE_URL:
secret: true
POSTGRES_PASSWORD:
secret: true
execution:
contexts:
host:
backend: native
services:
postgres:
endpoints:
host:
address: 127.0.0.1
port: 5432
tasks:
test:
requires_services:
- postgres
env_bindings:
DATABASE_URL:
from_service:
service: postgres
view: host
format: url
scheme: postgres
username: postgres
password_env: POSTGRES_PASSWORD
database: app_test
DB_HOST:
from_service:
service: postgres
view: host
format: host
This derives task env values from declared service endpoints. It is useful when the same task can
run natively or inside a container: Ota keeps the service dependency in requires_services, then
projects the selected service view into env without hand-writing container host aliases such as
host.docker.internal.
When a service exposes more than one endpoint in the same view/context, declare
from_service.endpoint to pick the exact named service endpoint instead of relying on view-only
selection.
Use password_env for real credentials. The referenced env var must be declared under env.vars
with secret: true, and the generated env value (for example DATABASE_URL) must also be declared
secret so Ota can redact it in receipts and summaries.
Literal password is supported only for local/dev fixtures with non-sensitive credentials, for
example a disposable postgres password in a test container:
env:
vars:
DATABASE_URL:
secret: true
execution:
contexts:
host:
backend: native
services:
postgres:
endpoints:
host:
address: 127.0.0.1
port: 5432
tasks:
test:
env_bindings:
DATABASE_URL:
from_service:
service: postgres
format: url
scheme: postgres
username: postgres
password: postgres
database: app_test
Do not store production, shared, or personal credentials in password; use password_env instead.
Example:
env:
vars:
PATH:
prepend:
- ./node_modules/.bin
- /opt/ota/bin
If the existing PATH is /usr/local/bin:/usr/bin:/bin, the final value becomes
./node_modules/.bin:/opt/ota/bin:/usr/local/bin:/usr/bin:/bin.
Policy-aware env selection and workspace inheritance are described in Environment variables.
Current behavior:
runprefers approved org-policy env values, then process environment, then declared env sources in order, thendefault- declared env values are injected into backend execution after resolution, so the spawned task process sees the same chosen value across native, container, and remote backends
runrejects disallowed valuesdoctorreports missing required vars, invalid values, and missing or invalid declared env sourcessecret: truemay not be combined with a default value- secret env values are redacted in execution receipts
- remote task execution rejects secret env values instead of inlining them into remote shell command strings
PATHcan be composed fromprependentries, the resolved base value, andappendentries- ota does not permanently mutate the user's shell session; resolved env values apply to the process ota starts
Resolution and provenance:
- repo-declared requirements remain the canonical source of truth
- workspace overlays may add or specialize member values when explicitly configured
- policy-derived values should be reported distinctly from repo-declared values
- execution receipts should explain which layer supplied the value that won
doctoranddetectshould expose provenance instead of flattening the result into a bare string
surfaces
Optional.
For the operator guide to what surfaces are, when to add them, and how they relate to listener shorthand and full listeners, see surfaces.md.
surfaces:
backend:
kind: http
label: Backend API
purpose: Primary application API for local development
visibility: internal
port: 5678
path: /
readiness:
kind: http
path: /healthz/readiness
timeout: 10000
frontend:
kind: http
label: Editor UI
purpose: Browser-facing editor surface
visibility: public
port: 8080
path: /
readiness:
kind: http
path: /
timeout: 10000
Fields:
<name>.kind: requiredhttp,https, ortcp<name>.port: required fixed port number<name>.label: optional short operator-facing label for command and topology rendering<name>.purpose: optional short purpose string for operators and docs<name>.visibility: optionalpublicorinternalmetadata for output and UX grouping<name>.path: optional HTTP/HTTPS path; defaults to/for HTTP/HTTPS surfaces<name>.readiness: optional reusable readiness contract for that surface<name>.readiness.kind: required when readiness is declared;httportcp<name>.readiness.path: optional for HTTP readiness when the surface path is already sufficient; otherwise required<name>.readiness.method: optional HTTP method; defaults toGET<name>.readiness.headers: optional HTTP request headers<name>.readiness.success.status: optional accepted HTTP status list<name>.readiness.body.contains: optional required response substring<name>.readiness.interval: optional polling interval<name>.readiness.timeout: optional per-attempt timeout<name>.readiness.retries: optional consecutive failure budget<name>.readiness.start_period: optional delay before the first probe
Current behavior:
- surfaces are reusable endpoint truth, not standalone operational URLs
- a surface becomes operational only when a service task runtime attaches it through
tasks.<name>.runtime.surfaces - attached surfaces normalize into the existing runtime listener model with conservative loopback defaults
kind: httpsreuses the existing HTTPS listener protocol and HTTP readiness semantics without inventing separate certificate or trust-management contract fields- workflows may reference attached surfaces for readiness and exposes without repeating host URLs
ota execution topologyreports both top-level declared surfaces and the normalized listener shape on attached runtimesota execution topologyalso reports additivesurface_attachmentson task runtimes so machine consumers can see whether one attached surface used defaults or explicit bind/project overrides
tasks
Optional.
tasks:
setup:docker:images:
description: Pre-pull registry-backed docker dependencies
category: setup
prepare:
kind: dependency_hydration
medium: container_images
source:
kind: docker_compose
engine: docker
cwd: docker
files:
- docker-compose.base.yml
- docker-compose.dev.yml
env_files:
- .env.compose
targets:
- redis
- database
requirements:
tools:
docker: "*"
effects:
network: true
network_kind: container_image_hydration
external_state:
- docker
setup:container:deps:
description: Hydrate container-owned dependencies through a declared Compose service
category: setup
prepare:
kind: dependency_hydration
medium: package_dependencies
source:
kind: node_package_manager
cwd: app
manager: npm
mode: ci
compose:
kind: run
service: api
workdir: /workspace/app
rm: true
requirements:
tools:
docker: "*"
effects:
adapter_state:
- compose_volume:node_modules
network: true
network_kind: dependency_hydration
setup:
description: Install dependencies
category: setup
run: pnpm install
safe_for_agent: true
effects:
writes:
- node_modules
build:
context: app
requires_services:
- postgres
depends_on:
- setup
run: pnpm build
db:migrate:
context: host
adapter_inputs:
overlays:
compose:
cwd: docker
files:
- docker-compose.yml
compose:
kind: exec
detach: true
service: api
workdir: /workspace
exe: bundle
args:
- exec
- rails
- db:migrate
requirements:
tools:
docker: "*"
dev:
context: app
run: pnpm dev
runtime:
kind: service
readiness:
kind: http
listener: http
method: GET
path: /health
headers:
Accept: application/json
success:
status: [200]
body:
contains: '"status":"UP"'
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
listeners:
http:
protocol: http
bind:
address: 0.0.0.0
port:
mode: fixed
value: 3000
project:
host:
address: 127.0.0.1
port:
mode: auto
path: /
# Common local listener shorthand:
#
# listeners:
# http:
# http: 3000
#
# This is authoring sugar only. Ota normalizes it to the full listener form with:
# - bind address `127.0.0.1`
# - fixed bind port `3000`
# - projected host `127.0.0.1:3000`
# - projected host path `/` for HTTP
#
# Use the full `protocol` / `bind` / `project` form whenever bind address, host address,
# host-port mode, primary projection, or path needs to be customized.
package:
depends_on:
- build
run: tar -czf dist/release.tar.gz dist/
upload:
depends_on:
- package
run: ./scripts/upload-artifact.sh dist/release.tar.gz
verify:
description: Run the canonical verification entrypoint
aggregate:
tasks:
- build
- upload
extensions:
release-upload:
kind: export_provider
command: ota-ext-upload
api_version: 1
description: Upload the release bundle to the artifact endpoint
config:
endpoint: https://artifacts.example.com/upload
artifact: dist/release.tar.gz
Compose execution notes:
- use
tasks.<name>.composewhen the truthful task body is a finitedocker|podman composelane ota should own structurally, whether that is a service-sideexec/run/attachcommand or a stagedcompose up,compose build, or project-scopedcompose downadapter invocation - use
compose.kind: attachwhen the truthful lane re-attaches to an existing interactive session inside a declared Compose service, such astmux attachinside a running dev container compose.detach: trueis supported forkind: execwhen the truthful lane starts a detached in-service bootstrap or background process and Ota should own that launch directly;kind: upalso supportsdetach: truefor staged service-group bring-upcompose.rm: trueremains valid only forkind: run
Fields:
-
description: optional string -
notes: optional multiline guidance for humans and agents -
category: optional string -
env: optional map of fixed task-scoped environment overrides -
env_bindings: optional map of task env values derived from declared services -
inputs: optional map of named task inputs -
context: optional execution context name -
run: optional string for a single shell-compatible command -
script: optional string for an inline multiline shell script -
command: optional structured finite command body -
compose: optional structured Compose execution body fordocker|podman compose exec/run/attach/up/down/build -
prepare: optional first-class finite preparation body for machine-readable setup or dependency hydration -
launch: optional structured launch source for inspectable command or packaged container starts -
action: optional first-class native setup action for small cross-platform repo-file mutations -
aggregate: optional first-class aggregate body for named dependency-closure entrypoints -
effects: optional structured side-effect metadata for the task body -
requirements: optional task-scoped prerequisite surface for this executable path -
execution: optional mode-aware execution branches for one task intent -
when: optional execution-guard conditions for the selected task node -
runtime: optional long-running workload shape for endpoint-bearing tasks -
runtime_boundary: optional canonical runtime sandbox baseline for this task lane -
variants: optional list of conditional task executions -
requires_services: optional list of service names that must be ready before the task body runs -
requires_artifacts: optional list of named generated artifacts the task consumes; each consumer must directly depend on the named artifact's producer task -
replay_inputs: optional repo-owned replay identity artifacts captured before the selected task or workflow closure begins; each entry declaresid, a repo-relativepath, and one of:kind: static_filefor a generic immutable repo file the deterministic lane consumes directly, such as a frozen fixture, SQLite store, lockfile-shaped baseline, or other named replay input; these are narrowing replay evidencekind: presentation_profilefor a declared execution-presentation profile file when the lane's output shape depends on a named rendering or normalization policy; matching identity closes only the named presentation-semantics class for that lanekind: comparator_profilefor declared comparison semantics such as equivalence, tolerance, ignored labels, or threshold posture; matching identity narrows comparator drift but does not by itself make the lane hermeticexpected_identity: optional immutablesha256:<64 lowercase hex characters>pin. When declared, Ota hashes the input before execution and blocks the selected closure before any task starts if the observed identity differs. Ota never rewrites this value; intentional input changes require a reviewed contract update. replay inputs cannot overlap declared closure writes
-
replay-input kind guide:
- use
static_filewhen the file is part of the deterministic selected-lane input set - use
presentation_profilewhen the file controls how runtime output is shaped or normalized before comparison - use
comparator_profilewhen the file controls how Ota should compare or judge the result
- use
-
example:
verify: replay_inputs: - id: recorded_sql kind: static_file path: data/fixture.jsonl expected_identity: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - id: frozen_store kind: static_file path: data/store.db - id: runtime-presentation kind: presentation_profile path: replay/presentation-profile.yaml - id: equivalence kind: comparator_profile path: replay/comparator-profile.yaml -
witnessed_observations.query_traces: optional JSONL query-trace artifacts emitted by a prior or external execution; each entry declares anidand repo-relativepath. Ota preserves these as attested observations in the receipt rather than treating per-query identities as current-run replay inputs. Each nonblank line must containid(subject),run(non-negative integer), andsql(query text); the selected closure must not write the trace path. -
depends_on: optional list of task names -
only_on: optional host OS inclusion list (linux,macos,windows). When declared, the task and any selected dependency closure that reaches it are unavailable on other hosts; Ota refuses before provisioning or execution. Use this for platform support truth. Keepvariants.<i>.when.osfor choosing an alternate body on a supported host. -
safe_for_agent: optional boolean (falsewhen omitted) -
internal: optional boolean; marks orchestration plumbing tasks that stay in the graph but are hidden from defaultota tasksdiscovery surfaces
effects fields:
writes: optional list of normalized relative paths the task body is expected to mutateworkspace_writes: optional list of normalized workspace-relative paths the task body is expected to mutate when the runnable path truthfully writes outside the repo root, such as a sibling checkout at../uinetwork: optional boolean; settruewhen the task requires network access or reaches out to remote services during executionnetwork_kind: optional network lane classifier (broad,dependency_hydration,container_image_hydration,service_readiness,integration_test, ortool_bootstrap) for networked task pathsadapter_state: optional list of lowercase<adapter_family>:<state_name>tokens naming durable adapter-owned state the task mutates, such ascompose_volume:bundle_dataexternal_state: optional list of lowercase tokens naming out-of-repo state the task mutates, such asdockerorpostgres
Task-effect rules:
- use
effects.writesfor durable repo paths the task mutates directly - use
effects.workspace_writeswhen the task mutates sibling or workspace-relative filesystem paths outside the repo root but still inside the intended local workspace layout - use
effects.network: truewhen the task depends on networked fetches or remote calls and that dependency should stay explicit for CI and agent execution - use
effects.network_kind: dependency_hydrationfor finite package, module, chart, or other dependency acquisition lanes such as lockfile-backed package-manager install; keep - use
effects.network_kind: container_image_hydrationfor Compose-managed image pull lanes, includingprepare.medium: container_imagesand Compose startup paths that may pull declared images; keep image identity and runtime receipt evidence explicit rather than collapsing it into package dependency hydration - use
effects.network_kind: service_readinessfor finite health or protocol assertions against an endpoint of a declared repo-managed service; pair it withrequires_servicesand keep it distinct from external integration truth - use
effects.network_kind: integration_testfor live, staging, or remote-backed verification lanes that depend on real services, credentials, or seeded environments beyond the local repo - test tasks that declare
requires_servicesshould classify a repo-managed endpoint probe asservice_readinessand a live, staging, or remote-backed lane asintegration_test; validate/doctor surface an advisory when that service-verification truth is left broad or omitted - use
effects.network_kind: tool_bootstrapfor finite contract-owned tool installation lanes such as bootstrappinguvthroughpip; keepeffects.network_kind: broad(or omitnetwork_kind) for wider API/remote-call execution - use
effects.external_statewhen the task mutates state outside the repo filesystem, such as Docker resources, databases, or hosted services - use
effects.adapter_statewhen durable state lives behind an adapter boundary instead of a repo path, such as a Compose volume that persists Bundler gems ornode_modules effects.network_kindrequireseffects.network: true- keep entries relative, normalized, and free of
..segments - keep
effects.workspace_writesentries normalized and workspace-relative; they may include..segments for sibling layout truth, but must not be absolute or drive-prefixed - keep
effects.adapter_stateentries as lowercase<adapter_family>:<state_name>tokens so adapter-owned durability stays machine-readable instead of collapsing back to prose - keep
effects.external_stateentries as lowercase tokens so the side-effect surface stays machine-readable instead of turning into prose - prefer shipped canonical
effects.external_statetokens when they fit:docker,postgres,redis,mysql,mariadb,kafka,rabbitmq,elasticsearch,opensearch,s3,gcs,azure_blob,cloudflare,kubernetes,terraform - avoid obvious repo-local aliases when a shipped token already exists; for example use
dockerinstead ofdocker_compose,postgresinstead ofpostgresql, andkubernetesinstead ofk8s effects.writesis contract truth for agent-safety review, not a log of every transient scratch fileeffects.workspace_writesis the explicit widening for sibling/workspace materialization truth; do not weakeneffects.writesjust to encode out-of-repo paths- when a task is agent-safe, declared writes should stay inside
agent.writable_pathswhen that boundary is declared - agent-safe task writes must not overlap
agent.protected_paths - agent-safe tasks must not declare
effects.workspace_writestoday; repo-scopedagent.writable_paths/agent.protected_pathsdo not yet model sibling workspace boundaries
aggregate fields:
aggregate.tasks: required non-empty ordered list of task names ota should execute as the aggregate body
aggregate rules:
- use
aggregatewhen the task is only a named dependency-closure entrypoint and does not have its own command body aggregateis a task body, so it is mutually exclusive withrun,script,command,compose,prepare,launch, andactionaggregate.tasksentries must resolve to known tasksaggregate.tasksorder is preserved during execution- aggregate-member failures become the aggregate task result
aggregatemust not be combined with task-local execution fields such ascontext,env,inputs,targets,requires_services,runtime,effects,requirements,variants, orexecution- aggregate tasks are user-facing entrypoints; other tasks should not reference them from
depends_onor hook edges aggregate.tasksmust reference executable child tasks, not other aggregate entrypoints- keep prerequisite edges in the child tasks; do not duplicate aggregate membership under
depends_on
compose fields:
compose.kind: required Compose execution shape; ota shipsexec,run,attach,up,down,build,stop,restart,rm,logs, andpscompose.engine: optional Compose CLI engine;dockerby default,podmanalso supportedcompose.service: required Compose service name forkind: exec,run, orattachcompose.services: optional ordered service list forkind: uporkind: buildcompose.workdir: optional in-container working directory passed throughcompose exec/run -wcompose.exe: required executable to run inside the selected service forkind: exec,run, orattachcompose.args: optional argument list passed tocompose.exeforkind: exec,run, orattachcompose.rm: optionalcompose run --rm; valid only withkind: runcompose.build: optionalcompose run --build; valid only withkind: runcompose.service_ports: optionalcompose run --service-ports; valid only withkind: runcompose.force_recreate: optionalcompose up --force-recreate; valid only withkind: upcompose.force: optionalcompose rm -f; valid only withkind: rmcompose.follow: optionalcompose logs -f; valid only withkind: logscompose.remove_volumes: optionalcompose down -v; valid only withkind: downcompose.timeout_seconds: optionalcompose down -t <seconds>graceful shutdown timeout; valid only withkind: downcompose.tty: optional TTY preservation forexecandrun; omitted means ota adds-Tfor deterministic non-interactive execution.kind: attachis always interactive and preserves TTY by definition.
compose rules:
- use
composewhen the repo truth is a service-side finite command such asbundle exec rails db:migrate,python manage.py migrate, ornpm run lintinside a declared Compose service, or when one finite staged task truthfully ownsdocker compose up [-d] [--force-recreate] <services...>,docker compose build [services...],docker compose restart [services...],docker compose rm [-f] [services...],docker compose logs [-f] [services...],docker compose ps [services...], ordocker compose down composeis a task body, so it is mutually exclusive withrun,script,command,prepare,launch,action, andaggregatecomposecurrently requiresrequirements.tools.dockerorrequirements.tools.podmanto keep the host Compose engine truthful- keep host-side Compose adapter truth under
adapter_inputs.composeor workflow overlays; keep service-side command truth or staged compose subcommand truth undercompose compose.kind: exec,run, andattachrequirecompose.servicecompose.kind: upusescompose.servicesfor staged service-group activation and must not declarecompose.servicecompose.kind: builduses optionalcompose.servicesfor staged image build selection and must not declarecompose.servicecompose.kind: stop,restart,rm,logs, andpsuse optionalcompose.servicesfor staged service selection and must not declarecompose.servicecompose.kind: downis project-scoped and must not declarecompose.serviceorcompose.servicescompose.workdir,compose.exe, andcompose.argsonly apply tocompose.kind: exec,run, orattachcompose.rmis only valid withcompose.kind: runcompose.buildis only valid withcompose.kind: runcompose.service_portsis only valid withcompose.kind: runcompose.force_recreateis only valid withcompose.kind: upcompose.forceis only valid withcompose.kind: rmcompose.followis only valid withcompose.kind: logscompose.remove_volumesis only valid withcompose.kind: downcompose.timeout_secondsis only valid withcompose.kind: downcompose.detachis only valid withcompose.kind: execorcompose.kind: upcompose.kind: attachmust not declarecompose.rmorcompose.detach
prepare fields:
prepare.kind: required preparation classifier; ota currently shipsdependency_hydration,tool_bootstrap, andsequenceprepare.kind: sequenceprepare.steps: required non-empty ordered list of child setup stepsprepare.steps.kind: ordered step kind;sequenceaccepts the structural prepare kinds (dependency_hydration,tool_bootstrap,sequence) plus deterministic native bootstrap mutations (copy_if_missing,ensure_env_file,ensure_file,ensure_directory,ensure_git_checkout,ensure_git_template,ensure_container_network,reset_compose_service_volume)
prepare.kind: tool_bootstrapprepare.tool: required bootstrap target; ota currently shipsuv,playwright_browsers, andcypress_browsersprepare.browsers: optional explicit Playwright browser subset; ota currently shipschromium,firefox,webkit,chrome, andmsedgeprepare.with_deps: optional Playwright dependency lane forplaywright install --with-depsprepare.source.kind: pipprepare.source.exe: required Python executable ota should use for-m pip install ...prepare.source.kind: poetryprepare.source.cwd: required repo-relative working directory forpoetry run ...bootstrapprepare.source.kind: node_package_managerprepare.source.cwd: required repo-relative working directory for the bootstrap invocationprepare.source.manager: required package manager; ota currently shipsnpm,pnpm,yarn, andbunprepare.source.filter: optional workspace/package selector for manager-owned targeted hydration or bootstrap; ota currently only owns this shape formanager: pnpmand renders it before the manager action
prepare.kind: dependency_hydrationprepare.medium: container_imagesprepare.source.kind: docker_composeprepare.source.engine: optional compose CLI engine;dockerby default,podmanalso supportedprepare.source.cwd: required repo-relative working directory for the compose invocationprepare.source.file: optional single-file compatibility alias relative toprepare.source.cwdprepare.source.files: optional ordered compose file stack relative toprepare.source.cwd; declare at least one compose file throughprepare.source.fileorprepare.source.filesprepare.source.env_files: optional ordered compose interpolation env-file stack relative toprepare.source.cwdprepare.targets: required non-empty list of concrete dependencies ota should hydrate
prepare.medium: package_dependenciesprepare.source.kind: node_package_managerprepare.source.cwd: required repo-relative working directory for the install invocationprepare.source.manager: required package manager; ota currently shipsnpm,pnpm,yarn, andbunprepare.source.mode: required install mode; ota currently shipsinstallandciprepare.source.frozen_lockfile: optional explicit lockfile strictness forpnpm install --frozen-lockfile,yarn install --immutable, orbun install --frozen-lockfileprepare.source.inline_builds: optional explicit Yarn inline-build ownership foryarn install --inline-builds; valid only withmanager: yarnandmode: installprepare.source.force: optional explicit npm override ownership fornpm install --forceornpm ci --force; valid only withmanager: npmandmode: installormode: ci, and should be treated as an exceptional hydration lane rather than a normal defaultprepare.source.compose: optional Compose invocation wrapper for typed hydration through a declared service containerprepare.source.compose.kind: required Compose execution shape; ota shipsexecandrunprepare.source.compose.engine: optional Compose CLI engine;dockerby default,podmanalso supportedprepare.source.compose.service: required Compose service nameprepare.source.compose.workdir: optional in-container working directory passed throughcompose exec/run -wprepare.source.compose.rm: optionalcompose run --rm; valid only withkind: runprepare.source.compose.tty: optional TTY preservation; omitted means ota adds-Tfor deterministic non-interactive execution
prepare.source.kind: bundlerprepare.source.cwd: required repo-relative working directory for the Bundler invocationprepare.source.path: optional repo-relative bundle install path; required for the repo-local gem lane, omitted for compose-wrapped lanes that truthfully use the container-default Bundler pathprepare.source.kind: composerprepare.source.cwd: required repo-relative working directory for the Composer install invocationprepare.source.kind: uvprepare.source.cwd: required repo-relative working directory for the uv hydration invocationprepare.source.mode: optional uv hydration mode; ota currently shipssync,pip_requirements, andpip_local_projectprepare.source.default_index: optional authoritative default Python package index projected asuv --index-url <url>prepare.source.indexes: optional ordered additional Python package indexes projected as repeateduv --extra-index-url <url>for compatibility across supported uv releasesprepare.source.offline: optional cache-only hydration posture projected asuv --offline; this prevents network access but does not make an undeclared local cache source replayableprepare.source.requirements_file: required repo-relative requirements file whenprepare.source.mode: pip_requirements; omit it formode: syncprepare.source.local_project: required source-cwd-relative local project declaration whenprepare.source.mode: pip_local_projectprepare.source.local_project.path: required source-cwd-relative directory containing the local project'spyproject.toml;..segments are valid only when the resolved path remains inside the contract rootprepare.source.local_project.editable: optional editable posture projected asuv pip install -eprepare.source.local_project.extras: optional ordered extras list projected onto the declared local project targetprepare.source.local_project.groups: optional ordered dependency groups projected as separateuv pip install --group <path>/pyproject.toml:<group>invocations after the primary local-project installprepare.source.local_project.lockfile: optional source-cwd-relative lockfile whose identity Ota captures alongside the local project manifest before execution;..segments are valid only when the resolved path remains inside the contract root- Ota separately records a clean Git source identity for the local project when it is available. An editable local project is replay-acquitting only when both receipts carry resolved hydration posture, its declared lockfile identity, and its clean source identity. A missing lockfile or dirty/unavailable source remains narrowing evidence; Doctor reports that boundary.
prepare.source.kind: poetryprepare.source.cwd: required repo-relative working directory for the Poetry install invocationprepare.source.groups: optional dependency-group list forpoetry install --with ...or--only ...prepare.source.group_mode: optional group selector; ota currently shipswithandonlyprepare.source.no_root: optionalpoetry install --no-rootprepare.source.kind: go_modulesprepare.source.cwd: required repo-relative working directory for thego mod downloadinvocationprepare.source.kind: helmprepare.source.cwd: required repo-relative working directory for thehelm dependency build .invocationprepare.source.kind: mavenprepare.source.cwd: required repo-relative working directory for the Maven invocationprepare.source.wrapper: optional explicit wrapper ownership; whentrue, ota runs./mvnw ..., otherwise it runsmvn ...prepare.source.mode: optional Maven hydration goal; ota currently shipsresolveandgo_offlineprepare.source.skip_tests: optional-DskipTestsfor hydration lanes that should keep test execution disabled while Maven resolves dependenciesprepare.source.kind: gradleprepare.source.cwd: required repo-relative working directory for the Gradle invocationprepare.source.wrapper: optional explicit wrapper ownership; whentrue, ota runs./gradlew dependencies, otherwise it runsgradle dependenciesprepare.source.kind: cargoprepare.source.cwd: required repo-relative working directory for thecargo fetchinvocationprepare.source.kind: dotnet_restoreprepare.source.cwd: required repo-relative working directory for thedotnet restoreinvocationprepare.source.config_file: optional repo-relative NuGet config passed asdotnet restore --configfile <path>prepare.source.sources: optional explicit restore feeds passed as repeateddotnet restore --source <url>arguments
prepare rules:
- use
preparewhen the task is a finite setup phase ota should understand structurally instead of as opaque shell glue prepareis an executable task body, so a task may declarepreparewithoutrunpreparestill needs explicitrequirementsandeffects; ota should understand both intent and side effectsprepare.kind: sequenceuses the parent task'srequirements,effects, and execution path for each ordered child stepprepare.kind: sequencemust declare at least one child step underprepare.steps- use
prepare.kind: sequencewhen one honest setup lane needs more than one typed finite step, such as env-file materialization plus Playwright browser bootstrap, or Node hydration plus Python hydration in one repo-levelsetuptask prepare.kind: tool_bootstrapcurrently requireseffects.network: trueandeffects.network_kind: tool_bootstrap; add toolchain requirements that match the selected bootstrap sourceprepare.kind: tool_bootstrapwithsource.kind: pipcurrently requiresrequirements.toolchains: [python]prepare.kind: tool_bootstrapwithsource.kind: poetrycurrently requiresrequirements.toolchains: [python]and currently only supportsprepare.tool: playwright_browsersprepare.kind: tool_bootstrapwithsource.kind: node_package_managercurrently requiresrequirements.toolchains: [node]and currently only supportsprepare.tool: playwright_browsersorprepare.tool: cypress_browsersprepare.kind: tool_bootstrapwithprepare.browserscurrently only applies toprepare.tool: playwright_browsersprepare.kind: tool_bootstrapwithprepare.with_deps: truecurrently only applies toprepare.tool: playwright_browsersprepare.source.filtercurrently only applies tosource.kind: node_package_managerwithmanager: pnpm; dependency hydration uses it for a scopedpnpm --filter <selector> install, while browser tool bootstrap uses it for its selected package scope- use
prepare.kind: tool_bootstrapwhen the task truth is contract-owned tool installation rather than repo dependency hydration; for example bootstrappinguvthroughpipor downloading Playwright or Cypress browsers through a repo-owned Node package manager prepare.kind: dependency_hydrationwithmedium: container_imagesandsource.kind: docker_composerequiresrequirements.tools.docker,effects.network: true, andeffects.network_kind: container_image_hydration; keep the declared Compose files and selected image targets explicit so image-pull truth is not collapsed into package dependency hydrationprepare.kind: dependency_hydrationwithmedium: package_dependenciesrequireseffects.network: trueandeffects.network_kind: dependency_hydration; add tool or toolchain requirements that match the selected hydration source and wrapperprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: node_package_managercurrently requiresrequirements.toolchains: [node],effects.network: true,effects.network_kind: dependency_hydration, and durable state ineffects.writesoreffects.adapter_state; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Node toolchain truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: bundlercurrently requiresrequirements.toolchains: [ruby],effects.network: true,effects.network_kind: dependency_hydration, and durable state ineffects.writesoreffects.adapter_state; host-side repo-local gem hydration also requiresprepare.source.path, while compose-wrapped lanes may omit it when Bundler truthfully uses the container-default install path; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Ruby toolchain truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: composercurrently requiresrequirements.tools.composer,effects.network: true,effects.network_kind: dependency_hydration, and durable state ineffects.writesoreffects.adapter_state; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Composer tool truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: uvcurrently requiresrequirements.toolchains: [python],effects.network: true,effects.network_kind: dependency_hydration, and durable state ineffects.writesoreffects.adapter_state; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Python toolchain truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: poetrycurrently requiresrequirements.toolchains: [python],effects.network: true,effects.network_kind: dependency_hydration, and durable state ineffects.writesoreffects.adapter_state; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Python toolchain truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: go_modulescurrently requiresrequirements.toolchains: [go],effects.network: true, andeffects.network_kind: dependency_hydration; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Go toolchain truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: helmcurrently requiresrequirements.tools.helm,effects.network: true,effects.network_kind: dependency_hydration, and durable state ineffects.writesoreffects.adapter_stateprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: mavencurrently requiresrequirements.toolchains: [java],effects.network: true, andeffects.network_kind: dependency_hydration; whenprepare.source.wrapperis absent or false, it also requiresrequirements.tools.maven; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Java or Maven tool truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: gradlecurrently requiresrequirements.toolchains: [java],effects.network: true, andeffects.network_kind: dependency_hydration; whenprepare.source.wrapperis absent or false, it also requiresrequirements.tools.gradle; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Java or Gradle tool truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: cargocurrently requiresrequirements.toolchains: [rust],effects.network: true, andeffects.network_kind: dependency_hydration; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host Rust toolchain truth for the in-container commandprepare.kind: dependency_hydrationwithmedium: package_dependenciesandsource.kind: dotnet_restorecurrently requiresrequirements.toolchains: [dotnet],effects.network: true, andeffects.network_kind: dependency_hydration; when the lane is wrapped throughprepare.source.compose, keep the host wrapper truthful withrequirements.tools.dockerorrequirements.tools.podmanand do not duplicate host dotnet toolchain truth for the in-container command- for an ephemeral container context that runs
dotnet restorefollowed bydotnet build --no-restoreordotnet test --no-restore, declareattachments.isolated_paths: [.nuget/packages]; ota mounts that path as one engine-owned volume and derivesNUGET_PACKAGES=/workspace/.nuget/packagesfor every task in the context so restored packages remain available without polluting the repo worktree
- for an ephemeral container context that runs
prepare.source.manager: pnpmcurrently usesmode: install; usefrozen_lockfile: truewhen the repo truth is strictpnpm install --frozen-lockfileprepare.source.manager: npmcurrently supportsmode: installormode: ci; usemode: ciwhen the repo truth is lockfile-strict npm hydrationprepare.source.manager: npmmay also declareforce: truewhen the repo truth is explicitlynpm install --forceornpm ci --force; keep that override deliberate because it weakens normal npm safety semanticsprepare.source.manager: yarncurrently usesmode: install; usefrozen_lockfile: truewhen the repo truth is strictyarn install --immutableprepare.source.manager: yarnmay also declareinline_builds: truewhen the repo truth isyarn install --inline-buildsprepare.source.manager: buncurrently usesmode: install; usefrozen_lockfile: truewhen the repo truth is strictbun install --frozen-lockfileprepare.source.composewraps the typed hydration lane throughdocker|podman compose exec/run; keep package-manager truth underprepare.source.kindand service/container truth underprepare.source.compose- see
examples/reference/task-prepare-compose-hydration/ota.yamlfor the canonical compose-wrapped Node package hydration shape prepare.source.kind: bundlercurrently executes one of two narrow canonical hydration lanes:- host-side or explicit repo-local path truth:
bundle config set path <path> && bundle install - compose-wrapped container-default path truth:
bundle install
- host-side or explicit repo-local path truth:
prepare.source.kind: composercurrently executes the narrow canonical Composer hydration lane:composer installprepare.source.kind: uvcurrently executes one of three narrow canonical uv hydration lanes:uv syncuv pip install -r <requirements_file>uv pip install [-e] <local_project>[extras...]followed by ordereduv pip install --group <local_project>/pyproject.toml:<group>for each declared group- optional
--default-index <url>, ordered repeated--index <url>, and--offlineare projected from the declared source posture; leave index selection undeclared only when the repo intentionally accepts ambient uv/pip configuration, in which case Ota records resolved provenance as unavailable rather than guessing from the host - for the local-project lane, Ota captures the declared local project's
pyproject.tomlidentity and any declaredlockfileidentity before execution so dry-run, doctor, and replay-grade preflight can refuse a missing or changed contract-owned baseline early
prepare.source.kind: poetrycurrently executes the narrow canonical Poetry hydration lane:poetry install, with optional--withor--onlygroup selection and optional--no-rootprepare.source.kind: go_modulescurrently executes the narrow canonical Go module hydration lane:go mod downloadprepare.source.kind: helmcurrently executes the narrow canonical Helm chart hydration lane: ota readsChart.yaml, seeds any declared HTTP(S) chart repositories into isolated repo-owned Helm repository state under.ota/state/helm/..., then runshelm dependency build .prepare.source.kind: mavencurrently executes the narrow canonical Maven hydration lane:./mvnw -q dependency:resolveor./mvnw -q dependency:go-offlinewhen wrapper-owned, otherwisemvn -q dependency:resolveormvn -q dependency:go-offlineprepare.source.kind: gradlecurrently executes the narrow canonical Gradle hydration lane:./gradlew dependencieswhen wrapper-owned, otherwisegradle dependenciesprepare.source.kind: cargocurrently executes the narrow canonical Cargo hydration lane:cargo fetchprepare.source.kind: dotnet_restorecurrently executes the narrow canonical .NET hydration lane:dotnet restore- optional
--configfile <path>whenprepare.source.config_fileis declared - optional repeated
--source <url>whenprepare.source.sources[]is declared - keep repository-owned feed selection in
config_filewhen that is the real NuGet truth;sources[]is a command override, not a second copy of the config file - on the first canonical machine carrier,
ota up --json, Ota records the selected lane as a typedreceipt.evaluated_inputs[]hydration_provenancerecord. Its nested declared posture stays separate from runner-resolved feed identities; it publisheshydration_provenance.resolved.source_identities[]for active config-backed feeds andresolution: unavailableplusresolution_errorinstead of claiming a config was resolved when it could not read, parse, or resolve it without ambient environment substitution; an undeclaredambient_defaultsource posture is alsounavailablebecause user/global NuGet configuration is not contract-owned source truth
prepare.kind: tool_bootstrapcurrently executes two narrow canonical tool-bootstrap lanes:- Python lane:
<exe> -m pip install --disable-pip-version-check -q uv - Poetry lane for
prepare.tool: playwright_browsers:poetry run playwright install [browsers...]poetry run playwright install --with-deps [browsers...]
- Node lane for
prepare.tool: playwright_browsers:npx playwright install [browsers...]npx playwright install --with-deps [browsers...]pnpm exec playwright install [browsers...]pnpm exec playwright install --with-deps [browsers...]pnpm --filter <selector> exec playwright install [browsers...]pnpm --filter <selector> exec playwright install --with-deps [browsers...]yarn playwright install [browsers...]yarn playwright install --with-deps [browsers...]bunx playwright install [browsers...]bunx playwright install --with-deps [browsers...]
- Node lane for
prepare.tool: cypress_browsers:npx cypress installpnpm cypress installyarn cypress installbunx cypress install
- Python lane:
preparedoes not replace workflow-owned host bootstrap; workflow prepare is still the explicit host bootstrap lane and now points at one native finite owner (prepare.taskorprepare.action)execution.orchestrator.mode: execcan mediate command-backedprepare.kind: dependency_hydrationandprepare.kind: tool_bootstrapexecution.orchestrator.mode: subcommandis the direct orchestrator CLI lane for structured taskcommandbodies andlaunch.kind: command; use it for truth likedevenv testordevenv uprather than forcing those lanes intotaskorexec- mixed/native
prepare.kind: sequencestays outside orchestrator mediation in the current shipped slice
Example:
tasks:
setup:
description: Materialize local env and hydrate frontend and backend dependencies
prepare:
kind: sequence
steps:
- kind: ensure_env_file
path: .env.local
vars:
APP_ENV:
value: local
- kind: dependency_hydration
medium: package_dependencies
source:
kind: node_package_manager
cwd: .
manager: pnpm
mode: install
frozen_lockfile: true
- kind: dependency_hydration
medium: package_dependencies
source:
kind: uv
cwd: .
requirements:
toolchains:
- node
- python
effects:
writes:
- node_modules
- .venv
network: true
network_kind: dependency_hydration
Command-backed prepare mediation example:
orchestrators:
devbox:
kind: devbox
required: true
config_files:
- devbox.json
prepare:
install: true
toolchains:
node:
version: "*"
package_managers:
pnpm: "*"
tasks:
setup:
prepare:
kind: dependency_hydration
medium: package_dependencies
source:
kind: node_package_manager
cwd: .
manager: pnpm
mode: install
requirements:
toolchains:
- node
effects:
writes:
- node_modules
network: true
network_kind: dependency_hydration
execution:
orchestrator:
ref: devbox
mode: exec
execution fields:
default_mode: optionalnative,container, orremotemodes: optional backend mapmodes.<mode>.context: optional context override for that modemodes.<mode>.depends_on: optional task dependency override for that modemodes.<mode>.lifecycle: optional lifecycle override for that mode (container mode only)runtime_boundary.filesystem.repo_root_mode: optional repo-root mount posture for the selected lane (read_onlyorwritable)runtime_boundary.filesystem.writable_paths: optional ordered repo-relative writable carve-outs for the selected laneruntime_boundary.filesystem.protected_paths: optional ordered repo-relative protected carve-outs for the selected laneruntime_boundary.network.default: optional default outbound posture for the selected lane (denyorallow)runtime_boundary.network.outbound_targets: optional ordered explicit outbound target truth for the selected lanekind: requiredhost,domain, orservice_aliasvalue: required literal target valuedestination_shape: optionalsingle_purpose_host,multi_tenant_host,relay_host, orsend_hostdestination_constraint: optional narrower effective-destination truth for lanes where first-hop host allowlisting is not sufficientkind: requiredcallback_host_allowlist,recipient_domain_allowlist,downstream_host_allowlist,bucket_scope,tenant_scope, orsms_destination_allowlistvalues: required ordered constrained destination valuessource_posture: requiredrepo_local_authoritative,shared_pinned_authoritative, ornon_authoritativeenforcement: requiredauthoritative_runtime_enforced,authoritative_app_enforced, oradvisory_onlyshared_pin: required whensource_posture: shared_pinned_authoritativeref: required pinned shared truth identifierfreshness: requiredfresh,warning, orblocking
env_files: optional ordered repo-relative dotenv overlays injected into the task process before task-levelenvadapter_inputs.overlays.compose.cwd: canonical optional repo-relative adapter working directory ota should enter before executing the selecteddocker composeorpodman composetask path; declared compose env files and compose files stay repo-relative in the contract and are projected relative to this adapter root at runtimeadapter_inputs.overlays.compose.env_files: canonical optional ordered repo-relative compose interpolation files projected to the selected task mode throughCOMPOSE_ENV_FILES; use this for task-owneddocker composeorpodman composeadapter input truth rather than process dotenv injectionadapter_inputs.overlays.compose.files: canonical optional ordered repo-relative compose file list projected to the selected task mode throughCOMPOSE_FILEadapter_inputs.overlays.compose.profiles: canonical optional ordered compose profile list projected to the selected task mode throughCOMPOSE_PROFILESadapter_inputs.overlays.compose.project_name: canonical optional compose project name projected to the selected task mode throughCOMPOSE_PROJECT_NAMEadapter_inputs.overlays.bake.cwd: canonical optional repo-relative adapter working directory ota should enter before executing the selecteddocker buildx baketask path; declared Bake files stay repo-relative in the contract and are projected relative to this adapter root at runtimeadapter_inputs.overlays.bake.files: canonical optional ordered repo-relative Bake file list projected to the selected task mode throughBUILDX_BAKE_FILEadapter_inputs.overlays.helm.cwd: canonical optional repo-relative Helm adapter working directory ota should enter before executing the selected Helm task path; declared chart and values-file paths stay repo-relative in the contract and are projected relative to this adapter root at runtimeadapter_inputs.overlays.helm.values_files: canonical optional ordered repo-relative Helm values-file list ota projects into the selected Helm task path as repeated-f/--valuesargumentsadapter_inputs.overlays.helm.chart: canonical optional repo-relative Helm chart path ota projects into the selected Helm task path instead of hard-coding chart selection in argvadapter_inputs.overlays.helm.release_name: canonical optional Helm release name ota projects into the selected Helm task path instead of hard-coding release naming in argvadapter_inputs.overlays.helm.namespace: canonical optional Helm namespace ota projects into the selected Helm task path instead of hard-coding--namespacein argvadapter_inputs.compose.*/adapter_inputs.bake.*/adapter_inputs.helm.*: compatibility aliases for existing contracts; preferadapter_inputs.overlays.<family>.*for new authoring- the public map shape is generalized, and shipped runtime semantics currently exist for overlay families
compose,bake, andhelm; other family keys are rejected by validation until ota ships their runtime and governance model modes.<mode>.env: optional env map merged over task-levelenvmodes.<mode>.env_files: optional ordered repo-relative dotenv overlays appended after task-levelenv_filesmodes.<mode>.adapter_inputs.overlays.compose.cwd: optional compose adapter working-directory override for that modemodes.<mode>.adapter_inputs.overlays.compose.env_files: optional ordered repo-relative compose interpolation files appended after task-leveladapter_inputs.overlays.compose.env_filesmodes.<mode>.adapter_inputs.overlays.compose.files: optional ordered repo-relative compose file list appended after task-leveladapter_inputs.overlays.compose.filesmodes.<mode>.adapter_inputs.overlays.compose.profiles: optional ordered compose profile list appended after task-leveladapter_inputs.overlays.compose.profilesmodes.<mode>.adapter_inputs.overlays.compose.project_name: optional compose project name override for that modemodes.<mode>.adapter_inputs.overlays.bake.cwd: optional Bake adapter working-directory override for that modemodes.<mode>.adapter_inputs.overlays.bake.files: optional ordered repo-relative Bake file list appended after task-leveladapter_inputs.overlays.bake.filesmodes.<mode>.adapter_inputs.overlays.helm.cwd: optional Helm adapter working-directory override for that modemodes.<mode>.adapter_inputs.overlays.helm.values_files: optional ordered repo-relative Helm values-file list appended after task-leveladapter_inputs.overlays.helm.values_filesmodes.<mode>.adapter_inputs.overlays.helm.chart: optional repo-relative Helm chart override for that modemodes.<mode>.adapter_inputs.overlays.helm.release_name: optional Helm release-name override for that modemodes.<mode>.adapter_inputs.overlays.helm.namespace: optional Helm namespace override for that modemodes.<mode>.run: optional single-line command override for that modemodes.<mode>.script: optional multiline script override for that modemodes.<mode>.command: optional structured finite command override for that modemodes.<mode>.prepare: optional structured preparation override for that modemodes.<mode>.launch: optional structured launch override for that modemodes.<mode>.runtime: optional runtime/listener override for that mode
when fields:
checks: optional list of check names that must pass before this task node executes
when rules:
when.checksgates the selected task node only; dependency ordering and service requirements are still declared separately- allowed condition-check kinds are
precondition(withrun),file,env, andchanged_files - probe-driven preconditions are not valid condition checks for
when.checks - condition checks run before dependency/service startup for the selected task node; if any condition fails, ota skips the task deterministically
execution mode rules:
--modechanges execution plane, not task identity; one task name can carry multiple mode branchesdefault_modecan stand alone when the task-levelrun/scriptalready describes the default path- when a branch is selected, branch values override task-level values for
context,depends_on,lifecycle,env,run/script/command/prepare/launch, andruntime execution.runtime_boundaryis the repo baseline for that task lane; task-localruntime_boundarycan narrow or replace it for the selected task instead of leaving sandbox posture split across agent metadata and shell conventionsmodes.<mode>.depends_onreplaces the task-level dependency list for that mode; omit it when the task-leveldepends_onalready matches the selected execution plane- use
modes.<mode>.depends_onwhen one task keeps the same identity but needs different preflight on host vs container instead of cloning tasks likebuild:host - use
env_filesfor task-process dotenv overlays; useadapter_inputs.overlays.compose.*when one task path ownsdocker composeadapter root, interpolation input, compose file selection, compose profiles, or project naming and that ownership should stay declarative instead of being hard-coded into the shell body - use
adapter_inputs.overlays.bake.*when one task path ownsdocker buildx bakeadapter root or file selection and that truth should project through a first-class adapter surface instead of shellcd ... &&/-f - use
adapter_inputs.overlays.helm.*when one task path owns Helm chart root, values-file selection, release naming, or namespace truth and that truth should project through a first-class adapter surface instead of shellcd ... && helm ..., chart positionals, or--namespace - keep
adapter_inputs.overlays.<family>non-empty: empty family markers are a governance smell and validate/doctor warn on them; either declare concrete adapter-owned fields or omit the family entirely - do not infer that arbitrary families are executable just because the map is generic; today ota only executes overlay semantics for
compose,bake, andhelm, and validator rejects unsupported families - validate/doctor warn when Compose adapter root or file/profile/project truth stays hard-coded in shell
docker compose --project-directory ...,cd ... && docker compose ...,--env-file,-f,--file,--profile,-p, or--project-nameinstead ofadapter_inputs.overlays.compose.* - validate/doctor warn when Bake file truth stays hard-coded in shell
docker buildx bake -f/--fileflags, or when Bake adapter root stays hard-coded in shellcd ... && docker buildx bake ..., instead ofadapter_inputs.overlays.bake.* - validate/doctor warn when Helm chart, values-file, namespace, or adapter-root truth stays hard-coded in shell
cd ... && helm ..., Helm-f/--values, or Helm-n/--namespaceflags, instead ofadapter_inputs.overlays.helm.* - when a selected branch omits
run/script/command/prepare/launch, ota falls back to the task-level execution body (including OS variants) - when a task declares
execution.modes, an explicit--modemust resolve to a declared branch unless it matchesdefault_mode; unsupported explicit overrides fail early with a mode-branch error - use
modes.<mode>only for mode-specific overrides; you do not need an empty branch such asmodes.native: {}just to pair withdefault_mode: native modes.native.lifecycleandmodes.remote.lifecycleare invalid; lifecycle is only valid for container execution
command fields:
command.exe: required executable name or path; this is generic process truth rather than an npm-specific field, and may benpm,pnpm,yarn,bun,node,python3,go,bundle,docker, an absolute path, or a repo-local executable pathcommand.args: optional argument listcommand.cwd: optional repo-relative working directory for that structured finite command; use it when the task truth is one executable plus stable argv rooted in a repo subdirectory instead of hidingcd ... && ...shell glue inrunorscriptcommand.interaction: optional terminal interaction posture for the child process; controls whether the command may inherit a real interactive TTY from the host terminal; omitting this field is equivalent toautoauto(default): when Ota itself is attached to a real TTY and the execution backend is native, stdin/stdout/stderr are inherited so the child process detects an interactive terminal; in agent mode and ordinary non-TTY CI or other non-TTY contexts the process uses non-interactive captured executionforbidden: always use non-interactive execution with stdin closed regardless of context; the child process never sees a TTYrequired: likeautoin a human TTY context, but refuses execution with a preflight error when no interactive terminal can be provided (ordinary CI, agent mode, non-TTY); use this when the task cannot make progress without interactive prompts (for example, Wrangler OAuth login).requiredtakes precedence over--stream: Ota passes the terminal through and does not claim pipe-captured command output for that invocation.
Ota does not maintain an allowlist for command.exe. The examples above are illustrative, not exhaustive; the executable may be any repo-truthful binary or path as long as the contract declares its requirements honestly.
launch fields:
launch.kind: requiredcommand,compose, orcontainerlaunch.kind: commandlaunch.exe: required executable name or pathlaunch.args: optional argument listlaunch.cwd: optional repo-relative working directory for that structured long-running process start; use it when the service launch truth is subdirectory-rooted but still one executable plus stable argvlaunch.runtime_projection.listener: optional explicitruntime.listeners.<name>selector for supported server adapters whose bind argv should be projected from canonical runtime listener truth instead of duplicated inlaunch.argslaunch.runtime_projection.adapter: required whenlaunch.runtime_projection.listeneris set; currentlyuvicorn,rails, andnextjs. Usenextjswith a direct structurednext devinvocation (for examplepnpm exec next dev --turbopack), not a package-script wrapper that already owns host or port flags.
launch.kind: composelaunch.engine: optional compose CLI engine;dockerby default,podmanalso supportedlaunch.action: required compose launch action; currentlyuplaunch.services: optional ordered service list for scopedcompose uplaunch.detach: optional detached startup flag forcompose up -d
launch.kind: containerlaunch.image: required packaged runtime imagelaunch.engine: optional engine override; defaults todockerlaunch.args: optional container command argumentslaunch.name: optional stable container namelaunch.remove: reserved for future non-service container launches; omit it for service runtimes in this slicelaunch.volumes: optional named-volume mountslaunch.volumes[].nameorlaunch.volumes[].source: required source identifierlaunch.volumes[].target: required container path
action fields:
action.kind: required action kind; currentlycopy_if_missing,ensure_env_file, orensure_file,ensure_directory,ensure_virtualenv,ensure_git_checkout,ensure_git_template,ensure_git_checkouts,ensure_container_network,build_container_image,reset_compose_service_volume, orensure_bundleactionis the first-class surface for deterministic preparation and explicitly governed local materialization. File-preparation actions are native-only because they mutate the host working tree directly;build_container_imageis a direct Docker-owned task action and materializes external container state instead and Ota does not yet claim one cross-backend persistence/ownership model for container or remote mutationsaction.kind: copy_if_missingaction.from: required repo-relative source fileaction.to: required repo-relative destination file
action.kind: ensure_env_fileaction.path: required repo-relative env-file path to create/updateaction.template: optional repo-relative seed file copied whenaction.pathis missingaction.template_mode: optionalmissingorreplace(defaultmissing)action.vars: required key map for env keys ota should enforceaction.vars.<KEY>.value: literal value for the declared keyaction.vars.<KEY>.random: generated value for the declared keyaction.vars.<KEY>.from_env: copy the effective resolved value of another declared env name into this keyaction.vars.<KEY>.mode: optionalmissing,replace, orremove(defaultmissing)action.vars.<KEY>.random.bytes: optional byte length (default32)action.vars.<KEY>.random.encoding: optionalhexorbase64(defaulthex)
action.kind: ensure_fileaction.path: required repo-relative file path to create when missing- exactly one bootstrap source is required:
action.template: copy this repo-relative template fileaction.value: write this literal file contentaction.random: generate random file content
action.random.bytes: optional byte length (default32)action.random.encoding: optionalhexorbase64(defaulthex)
action.kind: ensure_directoryaction.path: required repo-relative directory path to create when missing
action.kind: ensure_virtualenvaction.path: required repo-relative virtualenv path to create when missingaction.provider: optional virtualenv provider; currentlyuv(defaultuv)action.python: optional Python selector. An explicit path is forwarded unchanged. For a version selector such as"3.12", native execution prefers a version-compatible local interpreter matching the host architecture, then passes that absolute path touv; when no such local candidate exists, Ota forwards the selector to the provider unchanged.
action.kind: ensure_git_checkoutaction.path: required repo-relative checkout path to create when missingaction.source.git: required Git remote URL or clone sourceaction.source.ref: optional Git ref Ota should check out after cloneaction.remotes: optional ordered list of declared Git remotes Ota should reconcile inside the materialized checkoutaction.remotes[].name: required Git remote name such asoriginorupstreamaction.remotes[].git: required Git remote URL Ota should add or set for that name
action.kind: ensure_git_templateaction.path: required repo-relative scaffold path to create when missingaction.source.git: required Git remote URL or clone sourceaction.source.ref: optional Git ref Ota should check out before Ota strips inherited Git metadata
action.kind: ensure_git_checkoutsaction.checkouts: required ordered list of git checkouts Ota should materialize- each
action.checkouts[]entry uses the same fields and validation rules asaction.kind: ensure_git_checkout
action.kind: ensure_container_networkaction.provider: optional container runtime provider; currentlydocker(defaultdocker)action.name: required container network name to inspect/create
action.kind: build_container_imageaction.provider: optional container runtime provider; currentlydocker(defaultdocker)action.file: required repo-relative Dockerfile pathaction.context: required repo-relative Docker build contextaction.tag: required local image tag; it must not be empty or contain whitespace- this is a direct task action, not an
ensure_bundleorprepare.stepsentry: building a local image is material execution with an explicit Docker side effect, not idempotent host bootstrap
action.kind: reset_compose_service_volumeaction.provider: optional compose runtime provider; currentlydocker(defaultdocker)action.service: required non-empty compose service name ota should stop, remove, and restartaction.volume: required non-empty container volume name ota should remove before restartaction.compose.cwd: optional repo-relative compose adapter working directoryaction.compose.env_files: optional ordered compose interpolation files projected throughCOMPOSE_ENV_FILESaction.compose.files: optional ordered compose file list projected throughCOMPOSE_FILEaction.compose.profiles: optional ordered compose profile list projected throughCOMPOSE_PROFILESaction.compose.project_name: optional compose project name projected throughCOMPOSE_PROJECT_NAME
action.kind: ensure_bundleaction.steps: required ordered list of deterministic bootstrap steps- each
action.steps[]entry uses one of:kind: copy_if_missingkind: ensure_env_filekind: ensure_filekind: ensure_directorykind: ensure_virtualenvkind: ensure_git_checkoutkind: ensure_git_templatekind: ensure_git_checkoutskind: ensure_container_networkkind: reset_compose_service_volume
- each step uses the same fields and validation rules as the corresponding top-level action kind
Use action.kind: copy_if_missing for setup steps like creating .env.local from
.env.example without depending on POSIX test / cp or PowerShell conditionals. The action is
idempotent: if to already exists, Ota leaves it untouched.
Use action when the task is deterministic host repo preparation such as copying templates,
seeding env files, creating bootstrap files, or creating directories. Do not use action for
arbitrary command execution or backend-local mutation paths; use run, script, command, or
launch there instead.
Use action.kind: ensure_env_file when a setup path needs deterministic env bootstrap without a
shell script. It creates action.path (optionally seeded from action.template) and appends only
missing keys by default. Use action.vars.<KEY>.mode: replace when a workflow-scoped overlay must
rewrite specific keys deterministically. Use action.template_mode: replace when the destination
should be re-derived from action.template on every run before applying the declared key updates;
this is the governed replacement for shell copy-plus-sed env normalization. Use
action.vars.<KEY>.from_env when one generated env file should project already-declared Ota env
truth into a workflow-specific overlay, and use action.vars.<KEY>.mode: remove when stale keys
must be deleted deterministically instead of relying on shell mutation.
When a dependent task declares the same path under env_files, Ota treats a preceding declared
ensure_env_file action in its closure as planned setup output during dry-run. This keeps previews
usable from a clean checkout while real execution still validates the rendered dotenv file after
its dependencies complete.
Use action.kind: ensure_file when setup needs one deterministic bootstrap file (for example a
secret token file) without shell glue. It creates action.path once from one explicit source
(template, value, or random) and leaves existing files untouched on repeat runs.
Use action.kind: ensure_directory when setup needs a deterministic repo-local directory without
shell glue. It creates action.path when missing, no-ops when it already exists as a directory,
and fails if the path already exists as a non-directory.
Use action.kind: ensure_virtualenv when setup truthfully owns creation of one repo-local Python
virtualenv such as .venv without shell glue. Keep dependency installation itself under
prepare.kind: dependency_hydration so Ota owns virtualenv materialization and package hydration
as separate first-class setup truths instead of collapsing both into one opaque shell lane.
Use action.kind: ensure_git_checkout when setup truthfully owns deterministic materialization of
one sibling checkout, vendored dependency repo, or other Git-backed working tree without shell
bootstrap glue. Ota clones action.source.git into action.path only when that path is missing,
optionally checks out action.source.ref, optionally reconciles declared action.remotes[] by
adding missing remotes or updating existing remote URLs, and then leaves existing directories
otherwise untouched on repeat runs. action.path may stay repo-internal (vendor/wagtail) or
point at a declared sibling / workspace-relative target (../ui) as long as it remains a
relative path without an absolute prefix. This is intentionally a materialization surface, not an
update/reset surface: use it when bootstrap needs โmake sure this checkout exists and has the
declared remote wiringโ, not when setup should implicitly pull, fetch, or rewrite repository
history that is already present. When action.source.ref is omitted, Ota intentionally tracks the
remote default branch head and ota validate / ota doctor warn that the checkout is moving-head
pressure truth rather than deterministic proof truth.
Use action.kind: ensure_git_template when setup truthfully owns deterministic factory
materialization from a Git-backed scaffold that should become a fresh local repository instead of
remaining a clone of the upstream template. Ota clones action.source.git into action.path
only when that path is missing, optionally checks out action.source.ref, removes inherited Git
metadata from the cloned scaffold, initializes a fresh Git repository in place, and then leaves
existing directories untouched on repeat runs. This is the governed replacement for shell flows
like git clone ..., rm -rf .git, and git init when a repo or starter skill documents
template-style bootstrap.
Use action.kind: ensure_git_checkouts when setup truthfully owns several deterministic sibling or
vendored Git checkouts and repeating ensure_git_checkout entries would flatten one cohesive
materialization lane into low-level boilerplate. Ota applies the declared checkouts in order, uses
the same idempotent semantics as ensure_git_checkout for each entry, and keeps moving-head
advisories per checkout when source.ref is omitted.
Use action.kind: ensure_container_network when setup needs one deterministic external container
network without shell glue. It inspects the named provider-owned network, creates it only when
missing, and keeps Docker network bootstrap on a first-class declarative surface instead of
hard-coding docker network inspect/create logic into run, script, or command.
Use action.kind: build_container_image when a task must materialize a Dockerfile-backed local
image for a declared container or Compose lane. Ota invokes docker build --file <file> --tag <tag> <context> from the repository working directory and records the Dockerfile-to-tag action in task
discovery. Keep the build's network and Docker state effects explicit on the task; do not hide a
locally tagged image build in run, script, or command.
Use action.kind: reset_compose_service_volume when one setup or recovery lane truthfully owns a
destructive Compose-managed data reset such as โstop the service, remove its named volume, then
restart that serviceโ. Keep compose adapter cwd, env files, file selection, profiles, and project
name on the structured action.compose.* fields instead of hiding them in shell
docker compose ... flags.
Use action.kind: ensure_bundle when setup needs multiple deterministic bootstrap mutations in one
task (for example env file seeding plus secret file creation plus cache directory creation, or one
shared Docker network plus host file prep) without shell orchestration. Ota executes steps in
order, preserves the same idempotent semantics as each step kind, and keeps validation/error
reporting inside the contract surface.
requirements fields:
requirements.runtimes: optional runtime requirements that apply only to this task pathrequirements.tools: optional tool requirements that apply only to this task pathrequirements.any_of: optional context/backend-scoped alternative requirement branches for path-scoped requirements (runtimes,tools,toolchains,native,env,checks) useful for "A or B" prerequisite modeling without shell conditionalsrequirements.native: optional native prerequisite names from top-levelnative_prerequisitesrequirements.env: optional list of names from top-levelenv.varsrequirements.checks: optional list of names from top-levelchecks
requirements rules:
- use top-level
runtimes,tools, and requiredenv.varsonly when the prerequisite is truly repo-global - use
tasks.<name>.requirementswhen a prerequisite belongs only to one contributor, quickstart, or packaged-runtime path command.exeandlaunch.kind: commandboth implicitly scope their executable as a task-path tool requirement (default version*) so selected-workflow precondition diagnosis and activation surfaces include those executables even whenrequirements.toolsis omitted- declare
requirements.tools.<name>explicitly when you need to pin a version, attach acquisition metadata, or override defaults for that executable requirements.toolsis task-path truth and can be self-contained: tool names do not need a matching top-leveltools.<name>declaration to validate- if a required tool is owned by one or more declared toolchains, keep ownership deterministic by
declaring
requirements.toolchainsexplicitly on that task path requirements.any_of[]entries must declare at least one requirement (runtimes,tools,toolchains,native,env, orchecks) plus a deterministic selector (when.contextorwhen.backend) so Ota can resolve one branch on the selected path- use
requirements.any_offor disjunctive prerequisite paths such as "host-local service lane" vs "docker-host lane" while keeping each branch explicit and reviewable in contract truth - use
requirements.nativewhen a task needs host-native build tools but Ota should diagnose and guide instead of silently installing OS packages - workflow-aware readiness commands evaluate the selected workflow's
prepare.task/setup.task/run.taskdependency closure and merge those task-scoped requirements before diagnosing preconditions - an explicitly selected workflow with no
setup.taskhas no setup prerequisite phase; legacytasks.setupfallback is reserved for the unselected default compatibility path requirements.checksmust reference top-level checks declared withkind: precondition,kind: file,kind: env, orkind: changed_files- when the selected workflow task closure declares any task-scoped requirements, unreferenced
top-level precondition checks are treated as reusable definitions rather than global gates for
that path; reference a check from
requirements.checkswhen that task path needs it requirements.envmust reference declared top-levelenv.varsnames; it does not create new env requirements inline- selected task/workflow evaluation treats
requirements.envas path-scoped required env truth: the referenced top-level names become required forota doctor,ota env --task,ota up, and task execution on that selected path even whenenv.vars.<name>.requiredis not repo-globaltrue
launch rules:
- each task must declare exactly one executable source:
runscriptcommandlaunchaction
runstays the simple shell shorthandscriptstays the multiline shell escape hatchcommandis for finite argv-owned execution that Ota should model structurally without shell parsinglaunchis for structured, inspectable long-running starts that Ota should render and reason about without hiding everything inside one shell stringactionis for small built-in setup mutations that should stay deterministic and cross-platform instead of becoming shell-specific snippets- prefer
commandfor finite task bodies such asuv run pytest,poetry run pytest,bundle exec rake test, ornpm run buildwhen the repo truth is one executable plus a stable argument vector rather than shell composition; if that executable should run from a repo subdirectory, prefercommand.cwdover fakecd ... && ...shell glue - for long-running service processes, prefer
launch.kind: commandover opaque shellrunorscript; if the service start is rooted in a repo subdirectory, preferlaunch.cwdover fake shellcd ... && ... - when a supported long-running adapter would otherwise duplicate bind flags already declared
under
runtime.listeners, preferlaunch.runtime_projectionso Ota projects bind argv from canonical listener truth instead of carrying--host/--portor-b/-ptwice - for long-running Compose stack startup, prefer
launch.kind: composeover raw shell orlaunch.kind: commandcarryingdocker compose up ...argv - pair
launch.kind: commandwithruntime.kind: serviceplusruntime.surfacesorruntime.listeners;launchstarts the process, whileruntimedeclares what becomes reachable and how readiness is proved launch.runtime_projectioncurrently binds only to explicitruntime.listeners.<name>entries; keep the listener declared explicitly when the launch adapter should project bind argv from runtime-owned listener truthlaunch.runtime_projectionrequires a fixedruntime.listeners.<name>.bind.addressplus fixedbind.port.value; Ota rejects conflicting manual bind flags inlaunch.argsfor the supported adapter- pair
launch.kind: composewithruntime.kind: service;launchowns the persistentcompose upstart, whileruntimestill declares what becomes reachable and how readiness is proved ota validateandota doctorwarn when a task path resolves to shellrunorscripttogether withruntime.kind: service; migrate that path tolaunch.kind: commandorlaunch.kind: composeunless the task is truly a shell-oriented finite escape hatch- reserve
runfor finite shell tasks, pipelines, or real escape-hatch cases where a structured executable shape would be misleading or unavailable - reserve
scriptfor multiline finite shell escape hatches, not long-running service ownership commandreuses existing task env, input, receipt, dependency, and agent-safety behaviorlaunch.kind: commandreuses existing task env, input, receipt, dependency, and agent-safety behaviorlaunch.kind: composekeeps host-side Compose cwd, env-file, file-stack, profile, and project-name truth underadapter_inputs.overlays.compose.*, and keeps only persistentcompose uplaunch truth underlaunchlaunch.kind: containeris a task launch source, not an execution context- service tasks that use
launch.kind: containerstill treatruntime.surfacesas the canonical public endpoint truth; launch must not create a competing published-port contract - container launch service tasks are persistent Ota-managed services in this slice; Ota may replace an existing named launch container only when its ownership labels prove it belongs to the same repo and task family
- packaged containers must attach surfaces with container-safe publication overrides when the
default loopback bind is not valid, for example
bind.address: 0.0.0.0plus a loopback host projection
Examples:
tasks:
test:
command:
exe: uv
args: [run, pytest]
dev:
launch:
kind: command
exe: bundle
args: [exec, rails, server]
runtime_projection:
listener: api
adapter: rails
runtime:
kind: service
listeners:
api:
protocol: http
bind:
address: 0.0.0.0
port:
mode: fixed
value: 3000
project:
host:
address: 127.0.0.1
port:
mode: fixed
value: 3000
path: /
primary: true
surfaces: [api]
quickstart:
launch:
kind: command
exe: npx
args: [n8n]
runtime:
kind: service
surfaces:
- backend
selfhost:
adapter_inputs:
overlays:
compose:
env_files:
- .env.selfhost
launch:
kind: compose
engine: docker
action: up
detach: true
services: [langfuse, langfuse-worker]
runtime:
kind: service
surfaces:
- web
packaged:
launch:
kind: container
image: docker.n8n.io/n8nio/n8n
volumes:
- name: n8n_data
target: /home/node/.n8n
runtime:
kind: service
surfaces:
backend:
bind:
address: 0.0.0.0
port:
mode: fixed
value: 5678
project:
host:
address: 127.0.0.1
port:
mode: fixed
value: 5678
path: /
primary: true
runtime fields:
kind: currentlyservicebackend_binding: optional shared backend binding name declared underexecution.shared_backendssurfaces: optional list of reusable top-level runtime surfaces declared undersurfaceslisteners: named listener maplisteners.<name>.http: <port>: shorthand for the common local HTTP listener shapelisteners.<name>.tcp: <port>: shorthand for the common local TCP listener shapelisteners.<name>.protocol:http,https, ortcplisteners.<name>.bind.address: bind address inside the task execution contextlisteners.<name>.bind.port.mode:fixedordiscoverlisteners.<name>.bind.port.value: required whenmode: fixedlisteners.<name>.project.host.address: host-visible address for the projected listenerlisteners.<name>.project.host.port.mode:fixedorautolisteners.<name>.project.host.port.value: required when host portmode: fixedlisteners.<name>.project.host.primary: optional boolean; mark exactly one projected listener as primary when multiple listeners are projectedlisteners.<name>.project.host.path: optional URL path forhttpandhttpslisteners.<name>.project.publication.compose.service: optional explicit service owner for a nativedocker compose uppublication when ota should remap that host-visible port through--host-port
Listener shorthand rules:
- shorthand is authoring sugar only; ota normalizes it into the full listener model internally
http: <port>expands to:protocol: httpbind.address: 127.0.0.1- fixed bind port
<port> - fixed host projection
127.0.0.1:<port> - host projection path
/
tcp: <port>expands to:protocol: tcpbind.address: 127.0.0.1- fixed bind port
<port> - fixed host projection
127.0.0.1:<port>
- shorthand cannot be mixed with
protocol,bind, orproject - shorthand supports exactly one of
httportcp - use the verbose form when bind address, host address, host-port mode, primary projection, or path must be customized
Surface attachment rules:
- use top-level
surfaceswhen one endpoint meaning should stay shared across tasks and workflows; see surfaces.md runtime.surfacessupports two attachment forms:- list form like
runtime.surfaces: [backend]for default publication - object form like
runtime.surfaces.backendfor attachment overrides
- list form like
runtime.surfaces.<name>still refers to the declared top-level reusablesurfaces.<name>; object form means "attach that surface with publication overrides for this runtime", not "redefine the surface"runtime.surfaces.<name>attachment overrides are publication-only:bindprojectproject.host.primary
bindis where the workload listens inside its execution contextproject.hostis the host-facing projected endpoint ota reports, checks, and exposes- each attached surface normalizes into the same runtime listener model used by explicit
runtime.listeners - when an explicit
runtime.readiness.listenerhas a host projection but is not attached throughruntime.surfaces,ota validateandota doctoremit an advisory. Promote that endpoint to a named top-level surface when it is an operator-facing URL; keep raw listeners only for runtime-private endpoints that should not become reusable contract surface - topology JSON now also exposes additive
runtime.surface_attachments.<name>intent alongside the normalized listener truth - attached surface names become normalized listener names
- a runtime must not attach an unknown surface
- a runtime may intentionally declare
runtime.listeners.<name>and also attachruntime.surfaces.<name>when one published surface maps 1:1 to one explicit listener; this is the canonical same-name publication shape for workflow exposes and surface-backed runtime proof runtime.surfaces.<name>.bind.portmust preserve the declared top-level surface port withmode: fixed- if a runtime attaches exactly one surface, has no inline
runtime.readiness, and that surface declares readiness, ota derives the equivalent runtime readiness automatically - if a runtime attaches multiple surfaces, has no inline
runtime.readiness, and exactly one attached surface is markedproject.host.primary: true, ota derives runtime readiness from that primary surface
runtime mode semantics:
bind.port.mode: fixed: the task must listen on one explicit port inside its execution contextbind.port.mode: discover: ota discovers the final listening port after the task starts; use this only for native tasks where the process may auto-bump to a free portproject.host.port.mode: fixed: ota uses one explicit host port and the contract should treat that URL as stableproject.host.port.mode: auto: ota injects runtime URL env values before command execution and reports the resolved URL in receipts and JSON output; ephemeral container runs pre-reserve a host port, while persistent container runs reconcile the named container and then resolve the current published host mappingota run <task> --host-port <port>can override one run's published host/public port on the selected primary projected listener when that listener usesproject.host.port.mode: fixed; the workload bind port stays unchanged- for native structured
docker compose uplanes,--host-portalso requires explicit publication ownership throughproject.publication.compose.service; ota uses that declared compose service to render a temporary override stack instead of guessing service publication truth ota run <task> --memory <size>can override one run's requested container memory for container execution while preserving contract/task intent- with multiple projected listeners, mark one listener as
project.host.primary: true; ota uses that listener forOTA_PUBLIC_URLand primary endpoint rendering
Current execution rules:
- native tasks may use
bind.port.mode: fixedordiscover - native tasks with
bind.port.mode: discovermust not also declareproject.host.port.mode: fixed - container tasks with
project.hostmust usebind.port.mode: fixed - container tasks may use
project.host.port.mode: fixedorauto - remote execution contexts do not support
runtime.kind: servicehost projection yet - loopback-only container binds such as
127.0.0.1orlocalhostmust not be projected tohost - for container tasks with
project.host.port.mode: auto, ota verifies resolved host publication; ephemeral runs retry bounded times on host-port conflict before failing, and persistent runs recreate mismatched containers when reconciliation cannot safely reuse the existing publication shape --host-portrejects invalid shapes before task spawn: listeners withproject.host.port.mode: auto, no projected host listeners, ambiguous multi-listener projection without one primary listener, or native compose publications that omitproject.publication.compose.service- container memory precedence is:
--memoryoverride, thenexecution.contexts.<name>.container.resources.memory.default, thenexecution.contexts.<name>.container.resources.memory.minimum, then engine default - when
execution.contexts.<name>.container.resources.memory.minimumis declared, ota rejects--memoryvalues below the minimum before task spawn
Use cases:
- use
runfor one command you would normally type in a shell - use
scriptwhen the task needs multiple lines, shell setup, or cleanup steps - use
envwhen a task needs fixed environment values that should override repo-level env for that task - use
inputswhen a task needs named per-run values likebase_url,tenant, ormode - use
contextwhen one task should run in a different execution plane from the repo default - use
execution.modeswhen one task intent should run differently across modes (for examplestartcontainer by default andstart --mode nativeon host) without splitting intostartandstart:host - use
runtime.kind: servicewhen a task is a long-running workload that should publish a deterministic host endpoint - use
bind.port.mode: fixedwhen the app should stay on one known internal port - use
bind.port.mode: discoverwhen a native dev server may choose its final port at runtime - use
project.host.port.mode: autowhen the host port should be conflict-free but does not need to stay fixed - use
execution.contexts.<name>.container.resources.memory.minimum/defaultwhen container workloads need explicit memory truth to stay reliable across environments - use
descriptionfor the short summary andnotesfor the task purpose plus extra guidance - use
requires_serviceswhen a task needs canonical services brought up through their manager before the task runs - use
depends_onto model a build/package/upload chain without hiding order in shell scripts
Task input semantics:
inputsare declared intasks.<name>.inputs- each input name is lowercase snake_case in the contract and becomes
--kebab-caseon the CLI - ota injects resolved values into the task process as
OTA_INPUT_<NAME> defaultsupplies a value when the caller omits the inputrequired: truemakes the input mandatory unless a default is presentallowedlimits the accepted values for that input- task inputs override repo-level env only for the task they belong to
- task dependencies do not inherit the parent taskโs declared inputs
- if every declared input has a default, the task can be run with no input flags
- task input names may overlap ota command flags such as
modeorjobs; when they do, put ota command flags before the task and task inputs after the task requires_servicesresolves declared services before the task body and keeps lifecycle ownership withservices.<name>.manager- the selected workflow setup task's
requires_servicesentries become the pre-setup service phase forota up setup.requires_servicesremains the compatibility fallback when the default workflow setup task issetupworkflows.<default>.services.requireddefines the canonical post-setup service plane forota up; repo-levelservices.<name>.requiredremains the fallback when no workflow services are declaredruntime.listenerskeep workload ingress with the task instead of overloadingservicesota runrecords the resolved runtime endpoint in receipts and JSON output when ota can authoritatively resolve itota upreports workload endpoints for runtime-bearing tasks it actually executes while bringing the selected workflow to readiness- container runtime listeners export these env values before process start when host projection resolves:
OTA_PUBLIC_URLOTA_PUBLIC_HOSTOTA_PUBLIC_PORTOTA_PUBLIC_URL_<LISTENER>OTA_PUBLIC_URLis the primary projected listener URL; useOTA_PUBLIC_URL_<LISTENER>for secondary listeners
Task target binding semantics:
tasks.<name>.targets.<target>declares first-class local topology target identity<target>is the consumer-local target name- use a short stable name such as
api,admin, orbilling - ota uses that name for evidence and, when
override_inputis omitted, for runtime export asOTA_TARGET_<TARGET>
- use a short stable name such as
- each target binding declares exactly one identity shape:
serviceurl
urlis a fixed declared URL target- use it when the target is explicit and should not resolve through repo-managed service topology
- value must be non-empty
service.member,service.repo,service.task,service.listener, and optionalservice.address_view(topology,host,internal) declare a repo-managed service targetservice.memberis an optional monorepo member selector- use it when the producer lives in another member declared under
workspace.members - value must name an existing declared monorepo member
- current shipped cross-member slice is intentionally narrow:
address_view: hostalways works when the producer declares a fixedproject.hostendpoint and remainsactivation.mode: manualonlyaddress_view: topology/address_view: internalwork only when consumer and producer share one declared backend binding on the active plane- non-manual activation (
ensure_started,restart_ready,ensure_running,ensure_ready) is shipped only for those shared-backendtopology/internalmember targets
- use it when the producer lives in another member declared under
service.repois an optional workspace repo selector- use it when the producer lives in another repo declared under
ota.workspace.yaml - value must name an existing declared workspace repo
- current shipped workspace cross-repo slice is intentionally explicit:
- only
address_view: hostis supported - the producer listener must declare one fixed
project.hostendpoint activation.mode: ensure_started,restart_ready,ensure_running, andensure_readymay reuse or start that producer through the owning repo contract before the consumer runs
- only
- use it when the producer lives in another repo declared under
service.taskis the producing task name- use it when the target should follow a repo-managed service task instead of a guessed literal URL
- value must name an existing service task in the same contract, or in
service.member/service.repowhen one selector is present
service.listeneris the named listener exposed by the producing task runtime- use it when the producer exposes more than one endpoint and the consumer must target one specific listener
- value must name an existing listener under
tasks.<producer>.runtime.listeners - it may be omitted only when the producing service task exposes exactly one declared listener name across its service runtime shapes
service.address_viewtells ota which reachable address shape to resolve for the consumerhost: use the producer's published host URLtopology: use the current local topology address when ota can resolve it truthfullyinternal: use the producer's in-backend address when ota can resolve that internal plane truthfully- omit it only when the default resolution rules for that binding are already sufficient and explicit in the surrounding contract
- optional
override_inputpoints at a declared task input used as an explicit operator override channel- use it when an operator may intentionally point the consumer at another target such as staging, preview, or a separately started local app
- value must name an input declared on the same consuming task
- optional
activation.modecontrols whether ota should auto-start and observe the local producer service before the consumer task runsmanual= resolve target only; never auto-start the producerensure_started= when ota resolves a local target binding and no explicit override input wins, reuse the producer if it already appears reachable or start it without waiting for listener reachability or deeper readinessrestart_ready= when ota resolves a local target binding and no explicit override input wins, restart a currently reachable producer and wait for readiness before continuingensure_running= when ota resolves a local target binding and no explicit override input wins, reuse the producer if the declared target listener is already reachable or start it and wait until that listener becomes reachableensure_ready= when ota resolves a local target binding and no explicit override input wins, reuse the producer if already reachable or start it and wait until ready- practical rule of thumb:
- use
manualwhen target resolution is enough and the producer must already exist - use
ensure_startedwhen ota should start the producer and hand off immediately - use
restart_readywhen ota should bounce a reachable producer and verify it again - use
ensure_runningwhen listener reachability is enough - use
ensure_readywhen the producer declares a deeper readiness contract that should be satisfied before the consumer runs
- use
urltargets supportactivation.mode: manualonly- service runtimes may declare
runtime.readinesswhen โreadyโ must mean more than โthe socket is accepting connectionsโ - resolution precedence is:
- explicit
override_inputvalue supplied by the operator - resolved target binding URL
- compatibility literal input default (when declared and binding resolution is unavailable)
- explicit
- when
override_inputis omitted, resolved target bindings are exported to task execution asOTA_TARGET_<TARGET>(for example targetapi->OTA_TARGET_API) ota runrecords target-resolution evidence in run receipts JSON underreceipt.steps[*].target_resolutions- target-activation evidence is recorded alongside target resolution under
receipt.steps[*].target_resolutions[*].activation- human meaning:
started_started= ota started the producer without waiting for listener reachability or deeper readinessreused_started= ota reused a producer that already appeared started enough for the target edgestarted_running= ota started the producer and waited for the declared listener to become reachablereused_running= ota found the declared listener already reachable and reused the producerrestarted_ready= ota found the producer already reachable, restarted it deliberately, and waited for readiness againstarted_ready= ota started the producer and waited for readinessreused_ready= ota found the producer already ready and reused it
- human meaning:
- topology resolution rules are:
- native caller: resolves from declared fixed
project.hostendpoint unless caller and producer share one declared nativeruntime.backend_binding, in which case ota resolves to the producer fixed bind endpoint inside that shared boundary - container caller: resolves only when caller and producer share one declared container
runtime.backend_binding; ota resolves to the producer fixed bind endpoint inside that shared boundary - remote caller: resolves only when caller and producer share one declared remote
runtime.backend_binding; ota resolves to the producer fixed bind endpoint inside that shared boundary
- native caller: resolves from declared fixed
- internal resolution rules are:
- container caller: resolves only when caller and producer share one declared container
runtime.backend_binding; ota resolves to the producer fixed bind endpoint inside that shared boundary - native caller: resolves only when caller and producer share one declared native
runtime.backend_binding; ota resolves to the producer fixed bind endpoint inside that shared boundary - remote caller: resolves only when caller and producer share one declared remote
runtime.backend_binding; ota resolves to the producer fixed bind endpoint inside that shared boundary - unresolved topology and unresolved
internalviews fail clearly at run time without host/bridge guessing
- container caller: resolves only when caller and producer share one declared container
- current non-manual activation constraints:
- only
servicetargets participate in activation;urltargets are always manual - explicit operator override inputs skip producer auto-start and preserve the override value
- compatibility literal default fallbacks do not auto-start and fail clearly if
ensure_started,restart_ready,ensure_running, orensure_readywas requested ensure_startedlaunches the producer and returns immediately after startup is handed off; it does not wait for listener reachability or deeper readinessrestart_readystops a currently reachable producer through ota's owned cleanup path, then starts it again and waits for readinessensure_runningwaits only for the declared target listener plane, even when the producer also declares a deeperruntime.readinesscontract- when the producer service task declares
runtime.readiness, ota waits for that readiness contract instead of treating an open listener socket as sufficient - the current shipped slice supports actual producer auto-start only when ota can own the producer honestly:
- persistent container producer services
- unix native producer services started through the activation-owned native path
- built-in remote producer services (
ssh,tsh,kubectl,daytona) only when the caller and producer share one declared remote backend binding:address_view: hostrequires a fixedproject.hostendpointaddress_view: topologyandaddress_view: internalmay probe the fixed remote-plane bind endpoint- readiness may be
tcporhttp
- backend-provider remote producer services only when the caller and producer share one declared remote backend binding and the matching
backend_providerextension declaresactivation.provider_managed_cleanup: true:address_view: hostuses the listener fixedproject.hostendpoint- shared-remote
address_view: topology/address_view: internaluse the listener fixedbind.port.valueon the remote plane through provider-ownedactivation_probe ensure_startedhands startup off immediatelyrestart_readycleans up the currently reachable provider-owned producer, then restarts it and waits for readiness againensure_runningwaits for the declared reachable endpoint on the selected planeensure_readymay wait for deeper declaredruntime.readiness
- unsupported producer backend shapes fail clearly instead of guessing orchestration
- stream-mode runs show an explicit activation wait phase while ota is starting or waiting on the producer readiness contract
- on interrupt, ota cleans up producer services that this consumer run activation-started; reused producers are left running intentionally
- only
Current runtime.readiness support for service tasks:
- startup semantics:
- for
ota runservice tasks, the declared readiness budget is authoritative during startup - ota now fails startup when the configured
start_period/interval/timeout/retriesbudget is exhausted before the declared runtime endpoint becomes reachable - this is current runtime behavior, not a separate contract field such as
readiness.enforcement ota run --streamshows live readiness attempt progress while ota is still waiting- all run modes keep the final readiness-budget summary on startup failure, including attempts, timeout per attempt, interval, start period, and the last probe failure
- once readiness is observed, the service is considered started and normal long-running task behavior resumes
- for
probe: <name>- references one top-level
readiness.probes.<name>declaration - reuses that probe's transport and timeout contract while the selected listener still determines the runtime endpoint
- may optionally declare
listenerwhen the readiness target should bind to one non-default runtime listener explicitly - may declare
signal_probes: [<name>, ...]to require additional named task-listener probes before Ota reports the runtime as ready; each signal probe must target the same task runtime (target.kind: task,target.name: <task>) and may use:target.address_view: hostfor projected host listener probestarget.address_view: internalfor fixed same-task listener probes on native runtime paths
- may still declare
interval,retries, andstart_periodto control polling semantics for this runtime - must not also declare inline
kind,method,path,headers,success,body, ortimeout
- references one top-level
kind: http- requires
listener - requires
path - optional
methodsupportsGETandHEAD; default isGET - optional
headersadds request headers to the readiness probe - optional
success.statusoverrides the accepted HTTP status codes; default is any2xxor3xx - optional
body.containsrequires the response body to contain one exact substring after the status code matches, and it must not be used withmethod: HEAD - optional
intervalsets the wait between probe attempts; when omitted, ota uses the current small internal poll cadence - optional
timeoutsets the per-attempt probe timeout - optional
retriessets the consecutive failed probe budget before activation fails - optional
start_perioddelays the first readiness probe after activation starts - requires the referenced listener to declare
project.host, except for shared-remoteensure_readyon built-in remote providers where ota may instead probe the declared remote-plane listener address and fixedbind.port.value - ota waits for the declared response contract from the selected probe endpoint
- requires
kind: tcp- requires
listener - must not declare
method,headers,success, orbody - may still declare
interval,timeout,retries, andstart_periodto control readiness wait behavior - for local host-projected readiness, requires the referenced listener to declare
project.host - for shared-remote
ensure_ready, built-in remote providers may instead use the listenerbind.port.valueon the remote plane - ota waits until the selected probe endpoint accepts TCP connections or is listening on the shared remote plane
- requires
Shared local backend semantics:
execution.shared_backends.<name>declares an explicit ota-owned shared backend boundary for co-located long-running tasks- required fields:
scopebackendlifecycle
- optional fields:
contextto pin the shared backend to one named execution contextfulfillment(noneorrun) to control whether ota may prepare missing backend requirements on the actualota runpathenvironmentto declare backend environment intent for policy-governed image/profile resolution:profile(policy-backed fulfillment profile name)image_alias(policy-backed image alias name)image(literal image intent for compatibility)source(optional source class, valid only with literalimage)- an empty
environment: {}is allowed when the repo wants policydefault_profileresolution to choose the effective backend image - if no policy
default_profileresolves, ota falls back to the task/container image and shared-backend shape validation follows that same fallback instead of assuming one synthetic shared image
- tasks opt in through
tasks.<name>.runtime.backend_binding: <name> execution.shared_backends.<name>.backendcurrently supports:containernativeremote
- current constraints by backend family:
containermay useenvironment, shared publications, and container-shape reconciliationnativeis currentlyscope: local+lifecycle: persistentonly, and does not supportenvironmentremoteis currentlyscope: remote+lifecycle: persistentonly, and does not supportenvironment- remote service listeners are currently contract-driven fixed endpoints: declare
bind.port.mode: fixed,bind.port.value, and ifproject.hostis used declareproject.host.port.mode: fixed
- contract meaning:
requirementsstill declare what the backend or context needsfulfillmentdeclares whether ota may try to make that true on theota runpath- org policy still decides which provisioning sources and versions are approved
- shared backend identity is deterministic and drives:
- persistent container family/shape reconciliation for create/reuse/recreate
- topology/internal addressability for shared
container,native, andremotetarget bindings when ota can prove that shared boundary - backend-scoped run-path fulfillment when the group declares
fulfillment: run - receipt evidence in
receipt.steps[*].shared_local_backend(name,backend,lifecycle, declared environment intent, effective profile/image/source/registry, effective identity, and reuse state when known) - receipt evidence in
receipt.steps[*].backend_fulfillmentwhen ota probes or prepares the backend- human meaning:
requirements_satisfied= the backend already had what the contract requiredfulfilled= ota had to provision something and finished successfullymissing_requirements= requirements were missing and the contract/policy did not allow run-path fulfillmentfailed= ota attempted fulfillment or setup, but it did not complete successfully
- human meaning:
ota execution plan,ota run, and run receipts all resolve the same effective backend image for explicit-context and inferred-context shared backend groups
- validation rules include:
- binding must reference declared
execution.shared_backends.<name> - binding backend family must match task runtime backend
- when a local backend omits
context, bound tasks must not span multiple resolved contexts
- binding must reference declared
- current slice constraints:
- shared backend groups must resolve one deterministic backend shape within their shipped backend family (same effective image, dependency-isolation shape, and memory shape where those dimensions apply)
- bound workloads may differ in commands, listeners, readiness, and publications
- ota rejects real workload-local conflicts inside that shared boundary, including conflicting in-backend bind endpoints and conflicting fixed host publications
- persistent shared backend reconciliation uses the shared union of declared workload publications, while per-task runtime evidence and listener resolution remain task-scoped
- non-manual target activation currently auto-starts:
- persistent container producer services
- unix native producer services
- built-in remote producer services for shared-remote
address_view: host/address_view: topology/address_view: internaladdress_view: hostuses the listener fixedproject.hostendpoint- shared-remote
address_view: topology/address_view: internaluse the listener fixedbind.port.valueon the remote plane ensure_startedhands startup off immediately;restart_readybounces a reachable producer and waits for readiness;ensure_runningobserves listener reachability;ensure_readymay observetcporhttpruntime readiness- backend-provider remote activation now covers shared-remote
address_view: host/address_view: topology/address_view: internalwhenactivation.provider_managed_cleanup: true - built-in remote providers:
ssh:user@hosttsh:user@hostkubectl:pod/ota-devdaytona:sandbox-dev
- for
provider: ssh, omitremote.sshunless the repo must force a non-default SSH config or identity file; when omitted, ota delegates host alias and identity selection to normal OpenSSH behavior
- shared backends currently ship as:
- local
container - local
native - remote
remote
- local
- fulfillment currently acts on the effective shared backend requirement union for container, native, and remote shared backend families
- profile/alias environment intent requires an active org policy pack under
.ota/org-policy.yaml(policies.backend_environment) - policy may govern allowed/denied
sourceclasses and registries for the effective backend image fulfillment: nonefails clearly when required runtimes or tools are missing, whilefulfillment: runattempts approved provisioning before any bound task body or dependency task uses that backend
- later slices are expected to relax some of that strictness by extending the same model, not by replacing it with guessed addressing or implicit backend-sharing behavior
Direct container-context fulfillment semantics:
execution.contexts.<name>.fulfillmentis valid only forbackend: container- valid values are:
nonerun
execution.contexts.<name>.requirementsstill declares what that execution plane needs;fulfillmentdoes not replace or infer the requirements surfacefulfillment: runtells ota to satisfy declared context requirements on the actualota runpath when policy-approved provisioning is availablefulfillment: nonekeeps the same requirements truth but fails clearly instead of mutating the execution environment- org policy still governs whether ota may provision and which sources/versions are approved;
fulfillment: runis runtime intent, not a policy bypass - when the active org policy enables
strict_versions, ota also treats already-installed repo-satisfying versions as missing if they are not policy-compliant, andfulfillment: runmay repair them with an approved exact version - current slice behavior:
- persistent container contexts are fulfilled immediately against the resolved persistent execution container
- ephemeral container contexts are fulfilled inside the same named ephemeral execution container before the task body runs
- this first direct-ephemeral slice currently supports non-service task execution only; ota does not yet claim service-task run-path fulfillment for direct ephemeral container contexts
- validation rules:
backend: nativecontexts must not declarefulfillmentbackend: remotecontexts must not declarefulfillment
Example:
tasks:
dev:
run: echo dev
runtime:
kind: service
listeners:
http:
protocol: http
bind:
address: 127.0.0.1
port:
mode: fixed
value: 8080
project:
host:
address: 127.0.0.1
port:
mode: fixed
value: 8080
api-automation-tests:
description: Run API automation tests
notes: |
Use this to verify the API against a running local service.
Prefer after `ota run setup` and before merging contract changes.
inputs:
base_url:
description: API base URL for the live suite
default: http://localhost:8080
suite_mode:
description: Run mode for the API suite
default: standard
allowed:
- standard
- contract-drift
skip_api:
description: Skip API execution and build reports only
default: false
allowed:
- true
- false
targets:
api:
service:
task: dev
listener: http
address_view: topology
override_input: base_url
version:bump:
inputs:
version:
description: New release version for the Java SDK
required: true
Run it as:
ota run api-automation-tests
ota run api-automation-tests --base-url http://localhost:8080 --suite-mode contract-drift
ota run version:bump --version minor
ota run version:bump --version 0.2.0
ota run version:bump --version major
Input fields:
description: optional stringnotes: optional multiline string for purpose, when to use, and any operator guidancerequired: optional booleandefault: optional stringallowed: optional list of accepted string values
Input rules:
- input names must use lowercase snake_case
- input names must not collide with reserved
ota run/ota workspace runflag names and aliases such asbackend,jobs,json,lifecycle,member,mode,receipt, orstream - if an existing contract already uses one of those names, rename it to a task-specific variant such as
suite_mode,output_json,target_member, orexecution_backendbefore upgrading defaultmust be non-empty when presentallowedvalues must be non-empty- when
allowedis declared,defaultmust be one of the allowed values required: truecannot be satisfied by an empty value
Mode-aware task example:
tasks:
start:
description: Start the app
requires_services:
- postgres
execution:
default_mode: container
modes:
native:
context: host
env:
DB_URL: jdbc:postgresql://127.0.0.1:5432/app
launch:
kind: command
exe: mvn
args: [spring-boot:run]
container:
context: app
lifecycle: persistent
env:
DB_URL: jdbc:postgresql://postgres:5432/app
launch:
kind: command
exe: mvn
args:
- spring-boot:run
- -Dspring-boot.run.arguments=--server.address=0.0.0.0,--server.port=8080
runtime:
kind: service
listeners:
http:
protocol: http
bind:
address: 0.0.0.0
port:
mode: fixed
value: 8080
project:
host:
address: 127.0.0.1
port:
mode: auto
path: /
Run it as:
ota run start
ota run start --mode native
ota run start --mode container
Example script forms:
tasks:
build:
run: mvn package
dev:
script: |
lsof -ti:8080 | xargs kill -9 || true
mvn spring-boot:run
Variant fields:
when.os: required for each current variant entry; supported values arelinux,macos, andwindows- optional one execution override:
run,script,command, orcompose - optional
env/env_files/env_bindings/inputs/requirementswhen the task keeps the same body but needs OS-scoped process or requirement truth - optional
adapter_inputswhen the task keeps the same body but needs OS-scoped Compose/Bake/Helm overlay truth
Hook fields:
after_success: optional ordered list of task names to run only after the task body exits successfullyafter_failure: optional ordered list of task names to run only after the task body exits with a failureafter_always: optional ordered list of task names to run after either success or failure, but only when the task body was actually attempted
Example post-outcome hooks:
tasks:
build:
run: pnpm build
depends_on: [setup]
after_success: [verify-dist]
after_failure: [collect-build-diagnostics]
after_always: [cleanup-temp]
Rules:
- task names must not be empty
- tasks must declare exactly one task body:
run,script,command,compose,prepare,launch,action, oraggregate, unless the task intentionally resolves through variants or execution-mode inheritance - input names must use lowercase snake_case
- input defaults must not be empty
- input allowed values must not be empty
runmust be non-empty when presentscriptmust be non-empty when present- variant entries must declare
when.os - variant entries may declare one execution override (
run,script,command, orcompose) and/or non-emptyenv,env_files,env_bindings,inputs,requirements, oradapter_inputs - variant entries must not declare more than one execution override
- duplicate variants for the same
when.osare rejected - dependency references must resolve to known tasks
- aggregate member references must resolve to known tasks
- hook references must resolve to known tasks
- task dependency cycles are rejected
- aggregate membership and hook edges participate in the same task cycle detection as
depends_on depends_onis the canonical prerequisite edge between executable tasks;aggregateis the canonical body for named dependency-closure entrypointsrequires_servicesreferences must resolve to known services- each required service must declare an actionable manager or readiness surface so ota can enforce the requirement
Current execution model:
runandscriptare shell-compatible execution formscommandis the structured finite argv execution formaggregateexecutes its declaredaggregate.tasksin order and records the parent aggregate task as the requested entrypoint- task
envvalues are applied when ota runs the task and override repo-level env with the same name - selected variant
env,env_files, andenv_bindingsoverlay onto the task-level process input truth before backend-mode-specific overrides apply - when variants are declared, ota resolves the best matching
when.osentry first and falls back to the default execution - if the selected variant only declares task-input overlays (
env,env_files,env_bindings,inputs,requirements, oradapter_inputs), ota keeps the base task body and applies the selected OS-scoped overlays on top of task-level truth depends_onruns before the task body- dependency-plane selection is explicit: when the parent task resolves to a backend and a dependency truthfully supports that same backend through
execution.default_modeorexecution.modes.<mode>, ota keeps the dependency on that plane for the invocation; otherwise the dependency falls back to its own canonical backend selection aggregatereplaces fake no-op wrappers such asrun: "true"for verification entrypoints likeverifyafter_successruns only when the task body exits0after_failureruns only when the task body exits non-zeroafter_alwaysruns after either branch, but only when the task body actually ran- hook tasks run in declared order
- tasks marked
internal: true(commonlysetup) remain normal graph nodes fordepends_onand hooks, still run when referenced directly, and are hidden from defaultota tasksoutput unless--allis requested - hook failures affect the final task result for the parent task
- richer non-shell execution remains intentionally narrow; use
launchfor packaged starts andactionfor first-class setup mutations Ota can make cross-platform - reusable probes are now shipped through top-level
readiness.probes,checks[].probe,workflows.<name>.readiness.probes, and runtime/service readinessprobereferences - use task names to describe intent:
setup,dev,dev_clean,test,lint
readiness
Optional.
Use this section when one readiness target should be declared once and reused across workflow readiness and explicit named checks.
readiness:
probes:
backend-ready:
kind: http
target:
kind: task
name: backend
listener: backend
address_view: host
method: GET
path: /healthz/readiness
headers:
x-ota-probe: workflow
success:
status: [200]
timeout: 10000
Fields:
probes.<name>.kind:httportcpprobes.<name>.url: optional absolutehttp://URL for literal URL probesprobes.<name>.target: optional topology-derived targettarget.kind:taskorservicetarget.name: required task or service nametarget.listener: required for task targetstarget.address_view: optional for task targets; defaults tohosttarget.observer: optional for task targetsobserver.kind:command_host(default) ortaskobserver.task: required whenobserver.kind: task
target.endpoint: optional for service targets; required when the service declares more than one endpointtarget.observeris not valid for service targets
probes.<name>.method: optional HTTP method (GETby default,HEADsupported)probes.<name>.path: required for target-basedkind: httpprobesprobes.<name>.headers: optional HTTP headers forkind: httpprobes.<name>.success.status: optional accepted HTTP status list forkind: httpprobes.<name>.body.contains: optional HTTP body substring match forkind: httpprobes.<name>.expect_status: optional shorthand for one accepted HTTP status whensuccess.statusis omittedprobes.<name>.timeout: required integer timeout in milliseconds
Current behavior:
- top-level probes are canonical reusable readiness definitions
- literal
urlprobes stay first-class for external or intentionally non-topological endpoints - target-based probes can resolve from declared task listeners or service endpoints instead of copying host/port values into one URL string
checks[].probecan reference a named probe instead of repeating a shell commandworkflows.<name>.readiness.probescan reference probes directly when the workflow should be ready as soon as those probes passtasks.<name>.runtime.readiness.probeandservices.<name>.readiness.probestill reuse the named probe transport and timeout contract while keeping their own runtime/service endpoint selection semanticskind: httpsupports literalurlprobes and topology-derivedtargetprobeskind: tcpcurrently supports topology-derivedtargetprobes- reusable
kind: httpprobes now use the same request-shaping surface Ota already ships for runtime and service readiness:method,headers,success.status, andbody.contains - for plain
200, authors may omit bothexpect_statusandsuccess.status - both
expect_statusandsuccess.statusare fully supported for non-default success rules:- use
expect_statuswhen one shorthand status is clearer - use
success.statuswhen you want multiple accepted statuses
- use
- task-target probes without
target.observerstill resolve from ota's invoking command plane, sotarget.address_view: hostremains the correct default when one published host endpoint is the truth you want to reuse directly - task-target probes may now declare
target.observer.kind: taskplustarget.observer.taskwhentopology,internal, or one caller-relativehostview should be resolved exactly as that observer task sees it from its effective backend plane - observer-backed task probes reuse the same target-binding semantics ota already ships for task targets instead of inventing a probe-only topology model
- unsupported schemes such as
https://are rejected during validation instead of silently downgraded - probe execution is direct inside ota; it does not depend on
curl,node, or other repo-local tools
workflows
Optional.
For the operator guide to what workflows are, when to add them, and how they relate to tasks, surfaces, and agent hints, see workflows.md.
readiness:
probes:
app-ready:
kind: http
url: http://127.0.0.1:5678/healthz/readiness
success:
status: [200]
timeout: 10000
workflows:
default: app
app:
intent: local_development
description: Canonical local app workflow
prepare:
task: setup:env:local
setup:
task: setup
run:
task: dev
services:
required:
- postgres
readiness:
probes:
- app-ready
surfaces:
- backend
exposes:
- surface: backend
- http://127.0.0.1:5678
Fields:
default: required whenworkflowsis declared; names the canonical repo workflow<name>.intent: optional workflow classification such aslocal_development<name>.description: optional operator-facing summary<name>.notes: optional multiline notes shown duringota workflowsandota tasks --workflowsummaries<name>.runtime_boundary: optional canonical runtime sandbox baseline for the selected workflow pathfilesystem.repo_root_mode: optional repo-root mount posture (read_onlyorwritable)filesystem.writable_paths: optional ordered repo-relative writable carve-outsfilesystem.protected_paths: optional ordered repo-relative protected carve-outsnetwork.default: optional default outbound posture (denyorallow)network.outbound_targets: optional ordered explicit outbound target truthkind: requiredhost,domain, orservice_aliasvalue: required literal target valuedestination_shape: optionalsingle_purpose_host,multi_tenant_host,relay_host, orsend_hostdestination_constraint: optional narrower effective-destination truth for lanes where first-hop host allowlisting is not sufficientkind: requiredcallback_host_allowlist,recipient_domain_allowlist,downstream_host_allowlist,bucket_scope,tenant_scope, orsms_destination_allowlistvalues: required ordered constrained destination valuessource_posture: requiredrepo_local_authoritative,shared_pinned_authoritative, ornon_authoritativeenforcement: requiredauthoritative_runtime_enforced,authoritative_app_enforced, oradvisory_onlyshared_pin: required whensource_posture: shared_pinned_authoritativeref: required pinned shared truth identifierfreshness: requiredfresh,warning, orblocking
<name>.adapter_inputs.overlays.compose.cwd: canonical optional repo-relative adapter working directory the workflow should project into selected compose task paths when that path does not already declare one<name>.adapter_inputs.overlays.compose.env_files: canonical optional ordered repo-relative adapter-owned compose interpolation files the workflow should project into selected compose task paths<name>.adapter_inputs.overlays.compose.files: canonical optional ordered repo-relative adapter-owned compose file overlays the workflow should project into selected compose task paths<name>.adapter_inputs.overlays.compose.profiles: canonical optional ordered adapter-owned compose profile list the workflow should project into selected compose task paths<name>.adapter_inputs.overlays.compose.project_name: canonical optional adapter-owned compose project name the workflow should project into selected compose task paths when that path does not already declare one<name>.adapter_inputs.overlays.bake.cwd: canonical optional repo-relative adapter working directory the workflow should project into selecteddocker buildx baketask paths when that path does not already declare one<name>.adapter_inputs.overlays.bake.files: canonical optional ordered repo-relative adapter-owned Bake file overlays the workflow should project into selecteddocker buildx baketask paths<name>.adapter_inputs.overlays.helm.cwd: canonical optional repo-relative adapter working directory the workflow should project into selected Helm task paths when that path does not already declare one<name>.adapter_inputs.overlays.helm.values_files: canonical optional ordered repo-relative adapter-owned Helm values-file overlays the workflow should project into selected Helm task paths<name>.adapter_inputs.overlays.helm.chart: canonical optional repo-relative Helm chart path the workflow should project into selected Helm task paths<name>.adapter_inputs.overlays.helm.release_name: canonical optional Helm release name the workflow should project into selected Helm task paths<name>.adapter_inputs.overlays.helm.namespace: canonical optional Helm namespace the workflow should project into selected Helm task paths<name>.adapter_inputs.compose.*/<name>.adapter_inputs.bake.*/<name>.adapter_inputs.helm.*: compatibility aliases for existing workflow contracts; prefer<name>.adapter_inputs.overlays.<family>.*for new authoring- the workflow overlay map is generalized structurally, and shipped workflow runtime semantics currently exist for
compose,bake, andhelm; unsupported overlay families fail validation instead of silently acting as inert metadata <name>.env.profile: optional env profile name fromenv.profiles<name>.env.compose_env_file_services: optional compose-managed services that should consume the selected workflow profile's rendered dotenv artifact asservices.<service>.manager.env_file<name>.prepare.task: optional native finite task ota should run first as explicit host preparation for that workflow- must reference a declared task with one finite body:
run,script,command,prepare, oraction - must not reference a
launchtask or a task withruntime - must resolve to native execution
- must reference a declared task with one finite body:
<name>.prepare.action: optional workflow-owned deterministic host prepare action ota should run first for that workflow- must declare one first-class
actionbody such ascopy_if_missing,ensure_env_file,ensure_container_network, orensure_bundle - current workflow prepare shape requires exactly one of
<name>.prepare.taskor<name>.prepare.action
- must declare one first-class
<name>.setup.task: optional task ota should treat as the preparation phase for that workflow<name>.run.task: optional task ota should treat as the primary runnable surface for that workflow<name>.attach.task: optional task ota should treat as the canonical interactive re-attach lane for that workflow- use this when the truthful workflow path starts in the background and a separate named task re-attaches to an existing interactive session
ota up --attachuses this lane after readiness when it is declared instead of assuming the run task itself stays foreground
<name>.services.required: optional services that belong to that workflow<name>.readiness.checks: optional readiness checks that belong to that workflow<name>.readiness.probes: optional reusable readiness probes that belong to that workflow<name>.readiness.surfaces: optional attached runtime surfaces that belong to that workflow's selected run task<name>.readiness.signal.checks: optional non-gating checks surfaced as informational readiness signals<name>.readiness.signal.probes: optional non-gating reusable probes surfaced as informational readiness signals<name>.readiness.signal.surfaces: optional non-gating attached surfaces surfaced as informational readiness signals- a readiness entry must be declared in exactly one lane (
readiness.*orreadiness.signal.*) <name>.exposes: optional human-readable endpoints or URLs the workflow is expected to surface- literal string form keeps a fixed URL
- object form
{ surface: <name> }resolves through the selected workflow run task
Service manager fields:
services.<name>.manager.kind:composeorhostservices.<name>.manager.engine: optional compose CLI engine forkind: compose;dockerby default,podmanalso supported. Keep taskcommand.exe/launch.exealigned to the same engine when the task body directly executes the compose CLI.services.<name>.manager.name: optional manager/project name; required today forkind: composeservices.<name>.manager.file: optional compose file path forkind: composeservices.<name>.manager.files: optional ordered compose file stack forkind: composeservices.<name>.manager.env_file: optional repo-relative compose env-file path forkind: composeservices.<name>.manager.env_files: optional ordered repo-relative compose env-file stack forkind: composeservices.<name>.manager.profiles: optional compose profile list forkind: composeservices.<name>.manager.service: optional compose service name override; required today forkind: composeservices.<name>.manager.host: optional typed host-manager owner forkind: hostkind:systemdunit: required systemd unit name ota should start/stop/checkscope: optionalsystemoruser; defaults tosystem
ota assist declare-service --manager host --host-unit <unit> --style systemd-activeis the canonical authoring path when ota should own one systemd-managed host service directlyservices.<name>.manager.start/manager.stop: optional explicit host lifecycle commands forkind: host; do not combine these with typedmanager.hostownership because ota derives lifecycle from the typed host manager- validate/doctor warn when tasks still shell
systemctl start,stop, oris-activedirectly for service ownership that should live on the typedmanager.host+readiness.kind: systemd_activesurface
Workflow env adapter rules:
- use
<name>.env.compose_env_file_serviceswhen the workflow owns one rendered dotenv artifact and named compose services should consume that exact file - treat
<name>.adapter_inputs.*as the shared workflow adapter overlay surface: declare the base adapter-owned truth there once, then let task-local adapter inputs carry only the narrower additions that are specific to one selected path - ota resolves that workflow overlay through one adapter-field registry across the shipped Compose, Bake, and Helm families, so workflow/task/mode precedence, duplicate-ownership governance, and runtime env projection stay aligned on one contract-owned field map instead of family-specific drift
- use
<name>.adapter_inputs.overlays.compose.*when one workflow owns adapter-scoped compose input truth for the selected runnable path and task-local compose adapter inputs should only carry narrower path-specific additions - use
<name>.adapter_inputs.overlays.bake.*when one workflow owns the base Bake adapter root or file stack for selecteddocker buildx baketask paths and task-local adapter inputs should only carry narrower additions - use
<name>.adapter_inputs.overlays.helm.*when one workflow owns the base Helm adapter root, values-file stack, chart selection, release naming, or namespace for selected Helm task paths and task-local adapter inputs should only carry narrower additions - this keeps compose adapter input ownership on the workflow surface instead of duplicating
the same
manager.env_filepath across services - use
services.<name>.manager.files/.env_fileswhen the managed compose service identity itself depends on an ordered overlay stack, such as a sidecar service declared only through an additional compose file; keep task or workflowadapter_inputs.overlays.compose.*for selected runnable-path ownership - when the selected workflow run path includes a compose-running task, ota also projects that
rendered dotenv artifact into
tasks.<name>.adapter_inputs.overlays.compose.env_filesinstead of misrouting it through processenv_files - ota applies
<name>.adapter_inputs.overlays.compose.cwdonly when the selected compose task path does not already declare one; this lets one workflow owndocker/or similar adapter roots without forcing shellcd ... && docker compose ...glue back into task bodies - ota prepends
<name>.adapter_inputs.overlays.compose.filesahead of task-localadapter_inputs.overlays.compose.files, preserving declared task additions without letting workflow-owned base compose files drift back into shelldocker compose -fflags - ota prepends
<name>.adapter_inputs.overlays.compose.profilesahead of task-localadapter_inputs.overlays.compose.profiles, preserving narrower task additions without forcing workflow profile truth back into shelldocker compose --profile ...flags - ota applies
<name>.adapter_inputs.overlays.compose.project_nameonly when the selected task path does not already declare one; validate/doctor warn if task-local compose project naming duplicates workflow truth - ota applies
<name>.adapter_inputs.overlays.bake.cwdonly when the selected Bake task path does not already declare one; this lets one workflow own a subdirectory-rooted Bake lane without forcing shellcd ... && docker buildx bake ...glue back into task bodies - ota prepends
<name>.adapter_inputs.overlays.bake.filesahead of task-localadapter_inputs.overlays.bake.files, preserving narrower Bake file additions without forcing workflow truth back into shelldocker buildx bake -fflags - ota applies
<name>.adapter_inputs.overlays.helm.cwdonly when the selected Helm task path does not already declare one, so one workflow can own a chart-rooted Helm lane without forcing shellcd ... && helm ...glue back into task bodies - ota prepends
<name>.adapter_inputs.overlays.helm.values_filesahead of task-localadapter_inputs.overlays.helm.values_files, preserving narrower values-file additions without forcing workflow truth back into shell Helm-fflags - ota applies
<name>.adapter_inputs.overlays.helm.chart,.release_name, and.namespaceonly when the selected Helm task path does not already declare them - every referenced service must declare
manager.kind: compose - the selected profile must declare
render.dotenv - if a referenced service also declares
manager.env_file, it must match the workflow-owned rendered dotenv path exactly <name>.adapter_inputs.overlays.compose.env_files/.filesmust stay repo-relative and must not escape the repo<name>.adapter_inputs.overlays.compose.profiles[*]must not be empty<name>.adapter_inputs.overlays.bake.filesmust stay repo-relative and must not escape the repo<name>.adapter_inputs.overlays.helm.values_filesand.chartmust stay repo-relative and must not escape the repo<name>.adapter_inputsrequires the selected workflow run path to include task paths that support each declared adapter input family
Compatibility:
<name>.env.adapter_inputs.*remains accepted as a compatibility lane for older contracts<name>.env.compose_filesand<name>.env.compose_project_nameremain accepted as compatibility aliases for existing contractsservices.<name>.manager.fileandservices.<name>.manager.env_fileremain accepted as single-entry compatibility aliases; usemanager.filesandmanager.env_fileswhen a compose service owns an ordered overlay stack- new and updated contracts should use
<name>.adapter_inputs.overlays.*for workflow-owned adapter truth - do not declare the compatibility aliases together with the canonical
adapter_inputs.overlays.compose.files/.project_namefields
Prepare vs setup vs run:
prepare- host-side deterministic bootstrap before setup
- must point to one native finite prepare owner: either
prepare.taskor inlineprepare.action - use it for file or network preparation such as
copy_if_missing,ensure_env_file,ensure_container_network, orensure_bundle
setup- repo preparation
- use it for dependency install, generated artifacts, and other normal bootstrap work
run- primary operational path
- use it for the app, dev server, worker, packaged command, or packaged container launch the workflow is meant to make useful
Do not blur these boundaries:
- do not put ordinary shell setup or runtime startup in
prepare - do not use
setupas a hidden runtime phase - do not add
prepareunless the workflow genuinely needs one explicit host bootstrap step before setup
Current behavior:
- workflows do not replace
tasks,services, orchecks; they compose those primitives into one canonical operational path - use
workflows.<name>.instanceswhen one workflow is really a named family of runtime instances such asws0,ws1, orstaging/prod-previewrather than a single flat environment workflows.<name>.instances.defaultselects the implicit instance forota up --workflow <name>and other selected-workflow commands- select a non-default instance with
ota up --workflow <name>@<instance>,ota proof runtime --workflow <name>@<instance>, or other workflow-selecting commands - use
workflows.<name>.instances.generated.<family>when one workflow owns a bounded repeated instance family such asws1..ws8and the repeated overlays should stay on the existing instance boundary instead of being duplicated across many explicit named items - generated instance families are finite and deterministic: each family declares one
prefix,start,end, andtemplate; ota expands only those concrete selectors and does not invent open-ended instance names or general expression evaluation - use
workflows.<name>.instances.<instance>.topology.requires_instanceswhen one selected instance must bring up another declared instance first, such asws1+depending onws0 - keep instance truth bounded and explicit: this first-class lane is for instance-specific env overrides, task adapter input overrides, and surface port/path overlays, not arbitrary free-form templating across the whole contract
- generated instance templates may interpolate
${OTA_WORKFLOW_INSTANCE}and${OTA_WORKFLOW_INSTANCE_INDEX}inside string-valued instance overlay fields such as env values, compose project names, or overlay paths - use
workflows.<name>.instances.<instance>.envwhen every task on the selected workflow path should inherit one instance-specific env value such as a cloned workspace root - use
workflows.<name>.instances.<instance>.tasks.<task>.adapter_inputswhen one selected task path needs instance-specific compose project naming, bake files, or other adapter-owned truth without splitting the workflow into repo-local shell variants - use
tasks.<name>.variantswithvariants.<i>.env,variants.<i>.env_files,variants.<i>.env_bindings,variants.<i>.inputs,variants.<i>.requirements, orvariants.<i>.adapter_inputswhen one task keeps the same body but needs OS-scoped process, prerequisite, or adapter overlays such as Linux-only host uid/gid interpolation, service-derived URLs, input defaults/allowed values, platform-specific tool requirements, Compose files, env files, or profiles - use
workflows.<name>.instances.<instance>.tasks.<task>.runtimewhen one selected task path needs instance-specific service listener publication or readiness selection and the base task already owns explicitruntimetruth - use
workflows.<name>.proof.claim: boundedwhen a workflow is a real, archive-backed proof lane but has no declared dependency seam. It opts the workflow intoproof_breadthassurance without claiming repo-wide completion; Ota keeps the claimunknownuntil a matching immutable proof archive exists. A bounded claim requiresworkflows.<name>.run.taskso it always binds to an executed lane rather than contract prose. - use
workflows.<name>.proof.negative_controlswhen one declared service seam has a separate, finite failure-control task that should run only when explicitly selected byota proof runtime --negative-control <id>; the task must remain outside the normal workflow closure and must not require the controlled service. It names the marker-bound seam obligation it negates and the expected typed failure. Ota records a validated control only when the task writes a matching transaction-bound failure attestation after the expected failure; a generic non-zero exit remains invalid evidence and does not itself prove causality - use
workflows.<name>.proof.lifecycleto declare a bounded lifecycle-proof lane from existingservices.<name>manager truth.services[]contains only canonical service references and an optionalassertion.tasknames one finite task Ota executes after those services are ready. The assertion's full dependency closure must remain finite and outside the workflow's normal closure; lifecycle declarations never copy start, stop, readiness, or status commands. - keep workflow-instance runtime specialization on the task runtime boundary: override existing listener bind/project ports or readiness fields there instead of inventing a separate workflow-level listener model
- workflow-instance task runtime overlays currently merge onto an existing top-level task
runtime; they do not invent new listeners or replace runtime ownership from scratch runtime_boundaryfollows the same selected-path precedence ladder:execution.runtime_boundaryis the repo baseline,workflows.<name>.runtime_boundarycan specialize the selected workflow path, andtasks.<name>.runtime_boundaryremains the narrowest selected-lane owner when the workflow resolves through that task- generated instance templates may derive repeated port families without shell math:
surfaces.<name>.port_stride,tasks.<name>.runtime.listeners.<listener>.bind.port.stride, andtasks.<name>.runtime.listeners.<listener>.project.host.port.stridemultiply the generated instance index and add it to the declared base port value - use
workflows.<name>.instances.<instance>.surfaces.<surface>when the selected instance publishes the same surface shape on different host ports or paths and Ota should keep proof, exposure, and command guidance aligned on the selected instance - use
prepare.taskwhen the workflow needs one explicit host-side finite normalization/bootstrap step before setup and that step already deserves its own reusable task identity - use
prepare.actionwhen the workflow itself honestly owns one deterministic bootstrap action or bundle and creating a synthetic helper task would only add glue - use
env.profilewhen one workflow owns a truthful runtime env overlay or declared-source specialization and that selection should stay declarative instead of being repeated across tasks or hidden in shell - use
env.profile.render.dotenvwhen that workflow also needs one concrete dotenv artifact on disk for compose interpolation or another repo-owned runtime input, and Ota should materialize it automatically instead of routing through a synthetic prepare action - add
env.profile.render.dotenv.templatewhen that artifact should preserve repo-owned baseline entries from an example file while Ota deterministically overlays the workflow-specific values - do not use workflow
preparefor service startup or runtime launch; that still belongs insetup.task,services, orrun.task - use
services.<name>.manager.env_filewhen one compose-managed service depends on a workflow/runtime-specific interpolation file and Ota should own thatdocker compose --env-file ...input instead of repeating it inside shell commands - use
services.<name>.manager.profileswhen one compose-managed service owns stable profile selection and Ota should pass thosedocker compose --profile ...inputs declaratively instead of repeating them inside shell commands doctordiagnoses the default workflow by default when it declares workflow readiness probes, workflow readiness checks, or workflow services- selected-workflow
doctor,up, andproof runtimepaths applyworkflows.<name>.env.profilebefore evaluating task env overlays and declared env sources checkfollows the same selected workflow readiness boundary when a workflow declares explicit readiness probes or checks, and otherwise falls back to the repo-widecheckssurfacedoctorandcheckmay also validateworkflows.<name>.readiness.surfacesthrough the selected workflow run task without hardcoding host URLs into the workflow- workflow
exposesmay point at attached surfaces instead of repeating host URLs that the contract already owns undersurfaces - use
checks[].probewhen a named check should reuse a named readiness probe outside that workflow-scoped path or when the repo does not declare workflows ota upnow targets the default workflow instead of assuming repo-widesetupsemantics- if
workflows.<default>.prepare.taskor.prepare.actionis declared,ota upruns that host prepare phase before required service startup or setup - execution planning carries workflow
prepareas additive workflow context, but it does not become the concrete execution identity forota execution plan
Bounded proof shape:
workflows:
verify:
run:
task: gate
proof:
claim: bounded
This declares a truthful proof claim for a finite verification lane such as an offline replay,
build, or deterministic test gate. It does not claim a dependency seam, causal interaction, or
broader repository completion. Run ota proof runtime --workflow verify --archive; Doctor emits
proof_breadth: unknown until the archive matches the current contract, source identity, and
execution scope.
Negative-control shape:
workflows:
app:
run:
task: serve
proof:
seam_observations:
- id: postgres-marker
dependency: postgres
producer_task: write-proof-marker
task: observe-proof-marker
marker_env: OTA_PROOF_SEAM_MARKER
negative_controls:
- id: postgres-unavailable
dependency: postgres
obligation: postgres-marker
task: verify:postgres-unavailable
intervention:
kind: dependency_endpoint_override
expected_failure: dependency_unavailable
The contract declares the controlled dependency, the already-observed green seam obligation, the
separate task, the intervention family, and a typed expected failure. Ota executes the task only when selected. It supplies
the active transaction, control, obligation, and transient attestation coordinates; the control
must write a matching dependency_unavailable attestation after that failure is actually caught.
A successful control task, an unrelated non-zero exit, or a stale/mismatched attestation is
invalid, not a fault test. This surface does not infer disruption from prose or fabricate a
generic fault injector. Until a matching control validates, an otherwise exercised seam retains
the machine-readable dependency_causality_not_proved boundary.
intervention.kind is a bounded contract declaration of what the finite control task changes:
dependency_disruptionblocks or removes the declared dependency from the control lane.dependency_endpoint_overrideredirects the control lane away from the declared dependency.null_substitutionreplaces the declared dependency with a bounded null implementation.
This is not the proof result. Ota records the declared intervention beside runner-derived control
status, failure mode, transaction binding, and attestation digest. Only a matching
expected_missing_effect attestation promotes the dependency evidence to fault_tested.
That promotion is deliberately limited to the declared seam obligation. The proof receipt retains
dependency_output_shaping_not_proved for that same obligation, so consumers cannot read a
validated control as proof that the dependency shaped a broader application output.
Seam-observation shape:
workflows:
app:
proof:
seam_observations:
- id: postgres-marker
dependency: postgres
producer_task: write-proof-marker
task: observe-proof-marker
marker_env: OTA_PROOF_SEAM_MARKER
Ota issues one opaque marker for the proof, injects it into the declared producer task, and then
runs the finite observer before cleanup. The observer never receives the marker directly. It
receives a transaction identifier and runner-owned transient attestation path, and must recover
the marker through the declared dependency before writing its JSON attestation. Ota verifies the
transaction id, observation id, and marker before promoting the dependency to exercised; failed
or ambiguous observations retain dependency_exercise_not_proved.
The observer is deliberately narrow: it must be finite, remain outside the normal workflow
closure, require the named service, and have every prerequisite already owned by the normal
workflow closure. The observed service must also be part of that normal closure. This prevents a
proof-only task from silently repeating setup or minting an exercised claim for a service outside
the selected runtime path. producer_task must be in the selected workflow closure and require
the observed service. The marker value and transient attestation are never rendered in output;
the receipt retains only transaction and attestation digests. Ota carries marker bindings internally
and injects the raw marker only into producer_task; contract-owned environment defaults cannot
replace it at execution time.
- if
setup.taskalready depends on the same action task, direct task execution still follows the task graph while workflowota upavoids running the same prepare action twice - if
workflows.<default>.setup.taskis declared,ota upuses that task as the setup phase - if
workflows.<default>.run.taskis declared and the task has a service runtime,ota upactivates that task as part of readiness tasks.setupremains the compatibility fallback for the unselected default path;ota up --workflow <name>does not invent a setup phase when that workflow omitssetup.taskagent.default_taskandagent.entrypointremain agent-facing hints, but the default workflow is now the canonical repo operational path
Use this shape when a repo needs .env.local or another deterministic local file before setup, but
the actual setup path should still stay in the repo's preferred execution plane:
tasks:
setup:env:local:
execution:
default_mode: native
action:
kind: copy_if_missing
from: .env.example
to: .env.local
setup:
run: pnpm install
depends_on:
- setup:env:local
workflows:
default: app
app:
prepare:
task: setup:env:local
setup:
task: setup
run:
task: dev
This keeps the boundary honest:
- direct
ota run setupstill follows the task graph and bootstraps the file if needed ota upshows and runs one explicit host prepare phase before setup- the repo does not need to make all of setup native just to create one local file
checks
Optional.
checks:
- name: node-installed
kind: precondition
severity: error
run: node --version
timeout: 10
- name: backend-ready
kind: health
severity: error
probe: backend-ready
- name: workspace-dependencies-installed
kind: file
severity: error
path: node_modules
expect: directory
- name: compose-env-compatible
kind: env
severity: error
env:
path: .env.compose
assertions:
- key: REDIS_HOST
host:
allowed:
- redis
- cache
- key: DATABASE_URL
url_host:
policy: not_loopback
- key: APP_ENV
not_equals:
- development
- local
- name: app-source-changed
kind: changed_files
severity: info
changed_files:
paths:
- apps/web/**
include_untracked: true
Fields:
name: required, non-empty stringkind:precondition,health,file,env, orchanged_filesseverity:error,warn, orinforun: optional shell command when the check is command-backedprobe: optional probe reference when the check is probe-backedpath: optional repo-relative path when the check is file-backedscope: optional forkind: file;repo(default) keeps the path inside the repo, whileworkspaceallows a relative sibling path such as../task-sdk/schema.jsonexpect: required forkind: file; one ofexists,file,directory, ormissingenv: required forkind: envenv.path: required repo-relative dotenv file pathenv.assertions: required non-empty assertionskey: required env key- exactly one of
equals,not_equals,state,host, orurl_host not_equals: optional non-empty list of disallowed exact valuesstate: optionalpresentormissinghost/url_host: declare exactly one of:policy: currentlynot_loopbackallowed: non-empty list of allowed hostnames
changed_files: required forkind: changed_fileschanged_files.paths: required non-empty repo-relative path matcherschanged_files.base_ref: optional git base ref for diff rangechanged_files.head_ref: optional git head ref for diff rangechanged_files.include_untracked: optional boolean; when true, untracked files matchingpathsalso satisfy the check
timeout: optional integer in milliseconds
Choose check kind by intent:
- use
kind: preconditionfor prerequisite commands that must pass before setup or run (runtime/tool presence, host capability checks, policy gates) - use
kind: healthwithprobefor readiness/liveness that should reuse one declaredreadiness.probes.<name>contract - use
kind: filefor deterministic filesystem expectations without shell drift (node_modulesexists, lockfile is present, bootstrap file is intentionally missing, sibling workspace schema input is present) - use
kind: envfor deterministic dotenv assertions without shell grep drift (compose-compatible host rewrites, non-loopback service hosts, exact required values, key presence/absence) - use
kind: changed_fileswhen the gate should depend on whether a path set changed in git rather than host/runtime state
kind: file + expect decision:
expect: existswhen either file or directory is acceptableexpect: filewhen only regular file presence should satisfy the checkexpect: directorywhen only directory presence should satisfy the checkexpect: missingwhen absence is required (for example enforce no generated artifact in tree)
kind: file + scope decision:
- omit
scopeor usescope: repowhen the path must stay inside the repo root - use
scope: workspacewhen the truthful input is a sibling or parent-relative workspace path such as../task-sdk/schema.json scope: workspacestill requires a relative path and still rejects absolute paths
kind: env decision:
- use
state: presentwhen only key presence matters - use
state: missingwhen one key must stay absent from a workflow-scoped overlay - use
not_equalswhen one key must avoid a small set of known-bad exact values such aslocalhost,development, orlocal - use
host.policy: not_loopbackwhen one env key should point at a service/container hostname rather thanlocalhost,127.0.0.1, or::1 - use
url_host.policy: not_loopbackwhen the env value is a URL and only the URL host should be checked - use
host.allowedwhen one env key must resolve to one of a small set of truthful service host names such asredisorcache - use
url_host.allowedwhen the env value is a URL/DSN and only specific hostnames such aspostgresordbare valid - use
equalswhen one exact deterministic value must be present
kind: changed_files decision:
- set only
pathsto compare againstHEADby default - set both
base_refandhead_reffor explicit CI ranges (for example PR base to branch head) - set
include_untracked: truewhen new untracked files in the matcher set should count as changed
severity decision:
- use
errorwhen failure should block readiness and execution - use
warnwhen failure is actionable but should not block the main path - use
infowhen the check is informational signal only (for example change-scope hints)
timeout decision:
- set timeout for checks that can hang or run unpredictably on shared CI agents
- keep timeout unset for fast deterministic checks (ota uses normal completion behavior)
- for probe-backed checks, set timeout on the check only when this check needs a tighter override than the shared probe timeout
Current behavior:
upuses preconditions before setupdoctorruns configured checks and reports findings by severity- checks must declare exactly one of
run,probe,path,env, orchanged_files checks[].probemust reference a namedreadiness.probes.<name>declaration- file checks use the filesystem directly and do not invoke a shell; prefer them over
run: test -d ...or other OS-specific shell checks for file and directory state - repo-scoped file checks stay inside the repo boundary by default; widen to
scope: workspaceonly when the contract truth really depends on sibling workspace inputs - env checks parse dotenv files directly and should be preferred over shell
grep,findstr, or ad hoc scripting when the contract needs deterministic env-file assertions - validate/doctor emit governance warnings for obvious shell file-state and env-file checks that
should be rewritten as first-class
kind: fileorkind: envchecks - validate/doctor also warn when task bodies rewrite
.env*files through obvious shell mutation (sed -i,perl -pi) that should instead useaction.kind: ensure_env_filewith explicit replacement keys - validate/doctor warn when task bodies hard-code compose shell flags such as
docker compose --env-file ...,-f,--file,--profile,-p, or--project-name; move that ownership to taskadapter_inputs.overlays.compose.*orservices.<name>.manager.env_fileso Ota can reason about it - changed-files checks evaluate tracked diffs via git (
base_ref..head_refwhen both refs are declared, otherwise againstHEAD) and may include untracked matches wheninclude_untracked: true - probe-backed checks use the check timeout when one is declared, otherwise they inherit the probe timeout
- when
timeoutis set,doctorfails the check if it does not finish within the configured millisecond budget - human output identifies probe-backed failures as probes instead of pretending a shell command was run
agent
Optional.
agent:
entrypoint: setup
default_task: test
safe_tasks:
- setup
- test
verify_after_changes:
- test
writable_paths:
- src
- docs
exceptions:
sensitive_writes:
- .github/workflows
protected_paths:
- Cargo.lock
- LICENSE
inferred_boundary:
reviewed: false
provenance:
writable_paths:
- detect:semantic_root_inference
- detect:stack_source_scan
protected_paths:
- detect:contract_file_default
- detect:detected_control_files
bootstrap:
ota:
note: Only install ota if it is missing and installation is approved.
source:
kind: version
version: v1.6.16
notes: Keep agent edits narrow and add regressions for behavioral changes.
For deterministic unreleased proof, the same bootstrap surface may pin an exact git revision:
agent:
bootstrap:
ota:
source:
kind: git_rev
rev: e71931d6a41cc52a15966e0125725b67a05cc073
For active pressure testing, the bootstrap surface may intentionally track a branch tip:
agent:
bootstrap:
ota:
source:
kind: branch
branch: branch-name
Ota renders the matching shell and PowerShell installer commands from bootstrap.ota.source.
Legacy bootstrap.ota.sh / bootstrap.ota.powershell commands are still accepted for
compatibility, and Ota will infer version, git_rev, or branch truth from those commands
when it can.
Current validation rules:
entrypointmust reference a known task when setdefault_taskmust reference a known task when setsafe_tasksentries must reference known tasks- every
refusal_canariesentry must declare exactly one knowntaskor knownworkflow verify_after_changesentries must reference known taskswritable_pathsentries must not be empty and must be normalized relative pathsexceptions.sensitive_writesentries must not be empty and must be normalized relative pathsprotected_pathsentries must not be empty and must be normalized relative pathswritable_pathsentries must not duplicateprotected_paths; protected descendants may still carve out narrower exceptions inside a broader writable rootexceptions.sensitive_writesentries must overlap a declaredwritable_pathsboundary- task
effects.writesentries must be normalized relative paths when present - task
effects.external_stateentries must be non-empty lowercase tokens when present - agent-safe task writes must not overlap declared
protected_paths - when
writable_pathsis declared, agent-safe task writes must stay inside that writable boundary inferred_boundary.provenance.writable_pathsentries must not be empty when presentinferred_boundary.provenance.protected_pathsentries must not be empty when presentinferred_boundarymust include at least one provenance entry when presentbootstrap.otamust include at least one ofsource,sh, orpowershellwhen presentbootstrap.ota.source.kind: versionmust declare a pinned ota release likev1.6.21bootstrap.ota.source.kind: git_revmust declare a full 40-character commit SHAbootstrap.ota.source.kind: branchmust declare a non-empty branch name- unpinned
bootstrap.ota.sh/bootstrap.ota.powershellcommands now warn duringota validateso repo agent bootstrap stays deterministic across release drift bootstrap.ota.source.kind: branchalso warns duringota validate/ota doctor, because branch tracking is pressure truth rather than deterministic proof truth
Current implementation treats this as contract surface and validation input. It is not yet a full agent runtime layer.
Starter contracts commonly use a minimal default AI-agent block when the detector has enough
confidence to write one and can infer safe writable paths. The block is stored under agent, and
it gives an AI agent the safe paths and tasks it should use first. That default usually includes
setup as the entrypoint when present, test as the verification task when present, test in
verify_after_changes when present, ota.yaml in protected_paths, and a short note pointing at the matching
ota run <task> command. When ota itself should be installable by an agent, the starter block
can also include an approved bootstrap.ota entry with the shell and PowerShell install
commands.
Agent semantics:
posturedeclares the default authority boundary for agent editsreadiness_strictis the default and treats repo-contract, CI, runtime-topology, env/config, and lockfile writable paths as sensitivecontract_authoringexplicitly authorizes repo-contract editinginfra_authoringexplicitly authorizes CI and runtime-topology editing
entrypointis the first task an AI agent should use to get oriented in the repodefault_taskis the normal verification task to run when no more specific task is neededsafe_tasksare the tasks an AI agent can run without broad riskrefusal_canariesdeclare negative controls that must be refused by the real agent runner; each entry names only ataskorworkflow, while Ota derives the current refusal reason and closure at execution time- run a declared task canary with
ota run --agent --expect-refusal <task>, or a declared workflow canary withota up --agent --expect-refusal --workflow <workflow>; either command exits0only when the runner refuses before task or workflow execution begins - when a repo declares a matching safe or verification task, agents should prefer
ota run <task>over raw package-manager or language-tool commands; fall back only when no truthful Ota task exists or when isolating an Ota defect safe_for_agent: falseis just the default; omit it unless explicittrueimproves readability- task
effects.writesmakes the expected durable writes explicit so agent-safe task claims can be checked structurally - task
effects.networkmakes connectivity dependence explicit for agent-safe and CI-visible task review - task
effects.external_statemarks out-of-repo mutation such as Docker, database, or hosted-service state - agent-safe tasks with
effects.networkoreffects.external_statecan surface contract advisories so unattended execution risk stays explicit during validate and doctor flows - first-class
prepare.kind: dependency_hydrationlanes are treated more narrowly than opaque shell network use: when the agent-safe task path is already on ota's typed dependency-hydration surface, ota keeps the network truth explicit but does not emit the generic agent-safe dependency-hydration warning just for that bounded lane verify_after_changesare the tasks an AI agent should rerun after modifying fileswritable_pathsare the paths an AI agent may editexceptions.sensitive_writesrecords narrow intentional exceptions for sensitive writable paths or broader writable boundaries when the declaredpostureis still otherwise correct- when
ota.yamlis intentionally writable, keep it explicit: starter contracts fromota initprotectota.yamlby default, and any broader contract-authoring slice should pair writableota.yamlauthority withexceptions.sensitive_writes: [ota.yaml] - use
exceptions.sensitive_writesonly when the path is already writable throughagent.writable_paths, the path is sensitive, and the contract intentionally grants that authority; do not use it merely to say a protected path is important - do not use
exceptions.sensitive_writesfor normal readiness slices where the agent should not edit the contract, CI, topology, env/config, or lockfiles; put those files underprotected_pathsinstead protected_pathsare the paths an AI agent should avoid editing casually- lockfiles, env/config files, runtime-topology files, CI workflow files, and repo contracts
should usually stay under
protected_pathsfor readiness slices unless the contract explicitly authorizes that authoring scope inferred_boundary.reviewed: falsemeans ota inferred the current agent boundary but the repo author has not confirmed it yetinferred_boundary.provenanceexplains which starter or detector heuristics produced the current writable and protected boundarybootstrap.otaprovides an approvedotainstall path for agents when the binary is missingbootstrap.ota.noteshould explain when that install path may be usedbootstrap.ota.sourceis the canonical install truth for ota bootstrapkind: versionis the normal released proof lanekind: git_revis the deterministic unreleased proof lanekind: branchis the active pressure-testing lane and is intentionally non-deterministic
- first-time consumers should treat
bootstrap.ota.sourceas directly executable bootstrap truth through the official installer mapping:kind: versionmaps to the release installer lane- POSIX shell:
curl -fsSL https://dist.ota.run/install.sh | OTA_VERSION=<version> sh - PowerShell:
$env:OTA_VERSION='<version>'; irm https://dist.ota.run/install.ps1 | iex
- POSIX shell:
kind: git_revmaps to the git installer lane- POSIX shell:
curl -fsSL https://dist.ota.run/install.sh | OTA_GIT_REV=<rev> sh -s -- --from-git - PowerShell:
$env:OTA_GIT_REV='<rev>'; & ([scriptblock]::Create((irm https://dist.ota.run/install.ps1))) -FromGit
- POSIX shell:
kind: branchmaps to the same git installer lane- POSIX shell:
curl -fsSL https://dist.ota.run/install.sh | OTA_GIT_BRANCH=<branch> sh -s -- --from-git - PowerShell:
$env:OTA_GIT_BRANCH='<branch>'; & ([scriptblock]::Create((irm https://dist.ota.run/install.ps1))) -FromGit
- POSIX shell:
- GitHub Actions jobs that need direct
otacommands should consume that same truth through the publicota-run/setup@v1action insource: contractmode instead of hardcoding release/source install choices separately in workflow YAML bootstrap.ota.shandbootstrap.ota.powershellremain compatibility fields; when omitted, ota renders the approved shell and PowerShell install commands frombootstrap.ota.sourcenotesis free-form repo guidance for humans and AI agentsota detect --mergeandota detect --rewriterefuse to write protected paths declared by the existing contract
Authoring ergonomics:
- readiness slice (default): keep
posture: readiness_strict, keep lockfiles/env/config/topology/CI/contract underprotected_paths, and leaveexceptions.sensitive_writesempty - contract-authoring slice: allow writable
ota.yamlintentionally and acknowledge it withexceptions.sensitive_writes: [ota.yaml] - infra-authoring slice: allow writable CI/topology files intentionally and acknowledge only those specific sensitive paths
- use
bootstrap.otaonly when you want agents to self-install ota if missing; preferbootstrap.ota.sourceover raw shell strings, keep deterministic proof onversionorgit_rev, and usebranchonly for active pressure lanes
metadata
Optional.
metadata:
team: platform
owner: ota
author: ota Maintainers
created_at: 2026-03-23
ota:
detect:
field_ownership:
project.name: merged
tools.pnpm: merged
tools.curl: manual
field_source_class:
project.name: environment_toolchain
tools.pnpm: environment_toolchain
This is an open map for extra repo-specific values.
ota detect --write, ota detect --merge, and ota detect --rewrite record ota-managed detect
fields under metadata.ota.detect.field_ownership using merged.
When ota writes detector-owned fields, it can also record the additive detector-governance class
for those fields under metadata.ota.detect.field_source_class, for example
environment_toolchain, task_command, runtime_service, ci_verification,
agent_boundary, workspace_bootstrap, or heuristic.
The metadata.ota.detect subtree is ota-reserved and must remain mapping-shaped. If metadata.ota
or metadata.ota.detect is repurposed as a scalar or list, detect merge cannot persist ownership
metadata and will fail until that path is repaired.
metadata.ota.minimum_version is the reserved compatibility hint for contracts that require a
newer ota binary than the one an operator may have installed. Keep it as a semver string such as
1.6.16. Current ota builds validate it, reject contracts whose declared minimum exceeds the
running binary, and use it to explain newer-field parse failures more clearly than a raw
unknown field message. Compatibility failures now report the contract minimum, the current
binary identity, any detected unsupported contract feature, and the next upgrade or rebuild step;
ota --version --json is the confirmation surface for that operator path.
You can also pin curated fields explicitly with manual there when detector silence should not be
treated as contract drift. When a field has no detect ownership entry, ota treats the existing
contract value as manual by default.
Full example
See: