Reference

Contract

The contract is the repo’s source of truth for readiness and execution.

referencemaintainersintermediatestable2026-04-11

What this file is for

Use ota.yaml when you want one repo-local file to explain readiness, execution, and safe automation.

The file tells ota what the repo needs before it is runnable, how it should run, and what agents may safely do.

Only version and project are required for a valid contract. Everything else is optional unless the repo needs it.

  • Use ota.yaml as the source of truth for setup, execution, and automation intent.
  • Use it when humans, CI, and agents need the exact same readiness and execution contract.
  • Use it when you need more than README-level conventions to make readiness and tasks deterministic.
  • Start with version + project, then add only the fields your repo actively relies on.
Minimal valid contractyaml
version: 1project:  name: example-repo

Current high-signal fields

These fields are the ones teams are now using most for contract trust and automation safety.

Keep them explicit when they apply instead of relying on implied behavior.

  • metadata.ota.minimum_version gates contracts that require a newer ota binary than the current host.
  • Compatibility failures now report the contract minimum, current binary identity, detected unsupported contract feature when one is known, and the next install/rebuild step.
  • agent.posture declares whether this slice is strict readiness, contract authoring, or infra authoring.
  • agent.exceptions.sensitive_writes documents narrow sensitive-path exceptions without widening posture.
  • tasks.<name>.effects.writes declares durable task writes so safe-task claims can be validated structurally.
  • tasks.<name>.action.kind: ensure_bundle composes multiple deterministic setup actions in one task without shell orchestration.
  • tasks.<name>.effects.network makes network dependency explicit when setup or verification paths fetch from registries or call remote APIs.
  • tasks.<name>.effects.network_kind narrows that dependency lane (dependency_hydration vs broader remote-call execution) so warnings stay precise.
  • tasks.<name>.effects.external_state marks out-of-repo mutation such as Docker, database, or hosted-service state.
  • tasks.<name>.when.checks declares deterministic execution guards so task nodes can be skipped cleanly when preconditions do not pass.
  • execution.contexts.<name>.only_on keeps host support explicit so unsupported platforms fail early and clearly.
Focused contract snippetyaml
tasks:  setup:    safe_for_agent: true    when:      checks:        - web-changed    effects:      writes:        - .env.local      network: true  services:up:    effects:      external_state:        - docker execution:  contexts:    app:      backend: container      only_on:        - linux        - macos agent:  posture: readiness_strict  writable_paths:    - src    - docs    - .github/workflows  exceptions:    sensitive_writes:      - .github/workflows metadata:  ota:    minimum_version: "1.6.17"

version (required)

version tells ota which contract shape it should parse.

  • Treat version as the parser contract gate and set it first.
  • Set this first; if it is wrong, validation stops before ota trusts any other field.
  • Today the only supported version is 1, so this is the parser compatibility gate.
  • Treat a mismatch as a hard blocker because execution and readiness semantics can change between schema versions.
Minimal versionyaml
version: 1

project (required)

project gives ota stable repo identity and an operations anchor for reporting.

  • Use project whenever you need deterministic repo identity across CI, receipts, and automation outputs.
  • project.name is required and must be non-empty so all consumers produce stable identifiers.
  • Set project.type only when behavior or assumptions are truly type-specific.
  • Use project.description as operator context, not a changelog; keep it short and actionable.
Project exampleyaml
project:  name: example-repo  type: application  description: Example repo for the public docs

toolchains

toolchains defines managed ecosystem environments that ota can diagnose and, when explicitly allowed, fulfill on selected run paths.

  • Use toolchains when ota must understand more than does this executable exist? and the ecosystem owns components, targets, or provider-backed preparation.
  • toolchains own ecosystems through provider-defined capabilities; runtimes and tools remain direct check surfaces for capabilities not owned by a declared toolchain.
  • Current shipped ownership is provider-defined, not free-form: today ota derives Rust capability ownership from toolchains.rust with provider: rustup, Node runtime/executable plus declared Corepack package-manager ownership from toolchains.node with provider: corepack, Java plus javac ownership from toolchains.java with provider: sdkman, and Python runtime ownership from toolchains.python with provider: uv.
  • Current shipped support is intentionally narrow: top-level toolchains, task-scoped requirements.toolchains, Rustup-backed diagnosis and run-path fulfillment for Rust, Corepack-backed diagnosis for Node, SDKMAN-backed diagnosis for Java, uv-backed diagnosis and run-path fulfillment for Python, and hard validation errors for overlapping toolchain-owned capabilities.
  • The shipped toolchain contracts today are toolchains.rust with provider: rustup, toolchains.node with provider: corepack, toolchains.java with provider: sdkman, and toolchains.python with provider: uv.
  • Those shipped contracts are fixed name/provider pairs: toolchains.rust must use provider: rustup, toolchains.node must use provider: corepack, toolchains.java must use provider: sdkman, and toolchains.python must use provider: uv.
  • The shared provider-agnostic toolchain fields are currently provider, version, fulfillment, required, only_on, and platforms.<os>.version; profile, components, and targets remain Rustup-specific compatibility fields rather than a generic ecosystem-wide toolchain schema.
  • Ota validates and interprets those extra fields through the shipped provider contracts: Rustup currently owns field legality, capability ownership, fulfillment behavior, and Rust-shaped field validation for toolchains.rust, Corepack-backed Node toolchains stay check-only and currently allow provider-scoped package_managers, SDKMAN-backed Java toolchains own java plus javac while staying check-only, and uv-backed Python toolchains own the Python runtime with optional run-path fulfillment. Wrong name/provider pairings fail validation directly.
  • toolchains.<name>.version is the source of truth for diagnosis, and with provider: rustup plus fulfillment: run it must be one installable Rustup reference such as stable, beta, nightly, or 1.94.0.
  • toolchains.node with provider: corepack currently supports only fulfillment: none; ota diagnoses the Node runtime there, rejects duplicate tools.node, and lets declared package_managers own Corepack activation for tools such as pnpm or yarn.
  • toolchains.java with provider: sdkman currently supports only fulfillment: none; ota diagnoses owned java and javac there, while Maven or Gradle still stay under tools in the current shipped boundary.
  • toolchains.python with provider: uv can diagnose the owned Python runtime and, with fulfillment: run, can provision one installable uv Python reference such as 3.12 or 3.13; duplicate runtimes.python ownership is invalid.
  • Legacy split Node contracts (runtimes.node with standalone tools.pnpm/tools.yarn) stay valid but now trigger a migration advisory; prefer toolchains.node ownership for deterministic package-manager remediation.
  • toolchains.<name>.components and .targets let the managed ecosystem own capabilities such as rustfmt or target triples without shell setup glue.
  • toolchains.<name>.fulfillment defaults to none; use run only when selected ota run or workflow ota up paths may provision that toolchain on the run path.
  • Use toolchains for managed ecosystems, runtimes for simple unmanaged runtime checks, tools for standalone commands, and native_prerequisites for host-native build bundles.
  • If a toolchain already owns the capability, keep one owner. Duplicate ownership under toolchains, runtimes, or tools is invalid and fails validation.

Use toolchains to move managed ecosystem truth out of shell setup and into the contract.

Rustup-backed toolchainyaml
toolchains:  rust:    provider: rustup    version: "1.94.0"    profile: minimal    components:      - rustfmt    targets:      - x86_64-unknown-linux-musl    fulfillment: run tasks:  setup:    requirements:      toolchains:        - rust    run: cargo fetch

runtimes

runtimes defines what language/toolchain execution requires before normal work can run.

  • Use runtimes when startup depends on language/toolchain compatibility across execution hosts.
  • Use runtimes for simple unmanaged runtime version checks when no declared toolchain owns the selected capability.
  • runtimes.<name> is the minimum toolchain boundary for that language, and it is the field ota checks before normal work starts.
  • runtimes.<name>.version is the source of truth for readiness and install comparison on every host.
  • Keep runtimes.<name>.required at true unless a degraded execution path is explicit and acceptable.
  • Scope toolchain requirements with runtimes.<name>.only_on to avoid false failures on unrelated environments.
  • runtimes.<name>.provider and .distribution are provenance fields, not enforcement switches, so they should make installs and failures easier to diagnose.
  • Use runtimes.<name>.platforms.<os> when install provenance differs across OS while the logical runtime stays the same.
  • When a managed ecosystem already lives under toolchains, prefer that owner and avoid repeating the same capability here.
  • Platform keys must be valid OS values and must be within only_on, otherwise validation fails before execution begins.

Use the shorthand string form when the runtime only needs a version gate and no OS or provenance overrides.

Runtime version formsyaml
runtimes:  node: "22"  python: ">=3.12"  java: "<=21"  go: "^1.24"

Add version + only_on when the runtime matters only on one operating system.

OS-scoped runtime requirementyaml
runtimes:  pwsh:    version: "7.6.0"    only_on:      - windows

Add provider, distribution, and platforms only when install provenance differs across operating systems.

Java runtime with OS overridesyaml
runtimes:  java:    version: "21"    provider: sdkman    distribution: temurin    only_on:      - windows      - macos    platforms:      windows:        distribution: zulu

tools

tools declares CLI dependencies required by tasks before execution begins.

  • Use tools when CI, agents, and local dev need stable tool gates before execution.
  • Use tools for standalone commands on PATH, not for capabilities already owned by a declared managed toolchain.
  • tools.<name> can be shorthand (pnpm: "10") or a structured object; pick shorthand for simple gates and structured form when you need overrides.
  • tools.<name>.version is what ota doctor and check enforce before task execution.
  • tools.<name>.acquisition declares how ota can activate or provision that tool safely when a selected workflow/task requires it.
  • Use provider: corepack when the repo truth is a package-manager activation path such as pnpm through corepack prepare ... --activate instead of a global install.
  • Use provider: command when the repo truth is one explicit shell acquisition lane for that tool and you want ota to surface and execute that lane directly.
  • tools.<name>.required: false allows a warning-only state for optional workflows or migration periods.
  • tools.<name>.only_on avoids failing checks on unsupported hosts by limiting requirement evaluation.
  • tools.<name>.platforms.<os>.version lets one tool have different pinned minima per platform.
  • Selected non-native task/workflow paths (container or remote) do not inherit host-global tool fallback by default; declare those requirements on tasks.<name>.requirements.tools or execution.contexts.<name>.requirements.tools for the selected path.
  • When a managed ecosystem already lives under toolchains, prefer that owner and avoid repeating the same capability here.
  • tools.<name>.platforms keys must be valid OS values and consistent with only_on.

Use the shorthand string form when the tool only needs a version gate and no platform-specific override.

Tool version formsyaml
tools:  pnpm: "10"  go: ">=1.24"  java: "<=21"  maven: "^3.9"

Add version + only_on when the tool is required only on one operating system.

OS-scoped tool requirementyaml
tools:  pwsh:    version: "7.6.0"    only_on:      - windows

Attach acquisition truth to the tool, then let selected task requirements decide when it actually applies.

Tool acquisition via Corepackyaml
tools:  pnpm:    version: ">=10.22.0"    acquisition:      provider: corepack      package: pnpm      version: "10.22.0" tasks:  setup:    requirements:      tools:        pnpm: ">=10.22.0"

Use this when the repo has one explicit acquisition lane for the tool and ota should surface that exact command instead of vague install guidance.

Tool acquisition via commandyaml
tools:  bun:    version: ">=1.2.0"    acquisition:      provider: command      shell: sh      run: curl -fsSL https://bun.sh/install | sh tasks:  setup:    requirements:      tools:        bun: ">=1.2.0"

Native prerequisites

native_prerequisites describes host OS build-tool bundles that are not language runtimes or normal CLI tools.

Use it for Linux compiler packages, macOS Xcode Command Line Tools, or Windows Visual Studio Build Tools. Ota diagnoses these through the selected platform precondition and gives install guidance; it does not silently install host build tools.

  • Define the reusable bundle once under native_prerequisites.
  • Use a top-level check only when one precondition is portable; otherwise put check on each platform entry.
  • Use platforms.<os>.requires when runtime, tool, toolchain, env, or precondition dependencies belong to the native bundle instead of the task itself.
  • Reference it from tasks.<name>.requirements.native only on the front door that needs it.
  • Use activation.kind: visual_studio_dev_shell when the native task must run inside the Visual Studio Developer Shell on Windows.
  • Use activation.kind: command when the native task needs a shell-scoped environment activation such as bash, zsh, sh, pwsh, or cmd before checks and task bodies run.
  • When a selected native prerequisite declares an activation, ota up and ota run apply that activation to the real native task body instead of assuming the terminal was prepared manually.
  • If one task references multiple native prerequisites for the same platform, their activation hints must agree.
  • Native prerequisite guidance stays additive: Ota can surface apt, brew, winget, choco, scoop, install, and note-based provisioning hints in ota up --dry-run, ota doctor, and receipts without turning host build-tool installation into an implicit side effect.
  • Use tools.<name>.acquisition for safe tool activation such as Corepack-managed pnpm.
  • Use policy-backed provisioning when Ota is allowed to install runtimes or tools from approved sources.
Task-scoped native build toolsyaml
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" 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" tasks:  install:    run: pnpm install    requirements:      native:        - node-native-build-tools

env

env defines which variables are owned by the repo and how each variable is resolved for validation and execution.

  • Use env when deterministic precedence and secret handling are required before execution.
  • env.vars lists contract-owned keys, and you should use it for values that gate readiness (required) or constrain execution behavior (allowed).
  • env.vars.<NAME>.default defines contract-level fallback values for non-secret settings when Ota is the execution driver.
  • env.sources is ordered intentionally and ota loads only the sources the contract explicitly declares.
  • env.sources[].kind is curated, not open-ended: ota ships dotenv, properties, json, yaml, and toml today; path stays relative to the contract, and must_exist: true makes readiness fail when the file is missing.
  • env.vars.<NAME>.required marks a hard precondition versus advisory behavior; use strict required: true only when the task cannot proceed safely without the value.
  • env.vars.<NAME>.default is a controlled fallback, but it is forbidden when secret is true to avoid exposing derived secret values.
  • env.vars.<NAME>.allowed validates resolved values after all precedence sources are applied, so bad external overrides can still fail deterministically.
  • env.vars.<NAME>.secret redacts output and receipts and blocks execution on remote backend when any resolved value is secret.
  • env.vars.<NAME>.prepend and .append apply only to PATH, where they control deterministic executable lookup order across task runs.
  • env resolution is: task env override (if present) → workspace policy values (workspace mode only) → org policy values → process env → declared sources (in order) → contract default.
  • Execution env is a separate layer: context env, then task env, then selected mode env, with ota-injected OTA_WORKSPACE and fallback cache env added only when needed.
  • When debugging misses, inspect this order in ota env before changing task bodies.
Env exampleyaml
env:  vars:    WEB_ORIGIN:      default: "http://127.0.0.1:3000"    API_BASE_URL:      default: "http://127.0.0.1:8080"    REQUEST_TIMEOUT_SECONDS:      default: "15"    APP_ENV:      default: local      allowed:        - local        - ci    DATABASE_URL:      required: true      secret: true    RELEASE_CHANNEL:      default: stable      allowed:        - stable        - canary    PATH:      prepend:        - ./.venv/bin      append:        - /opt/ota/bin  sources:    - kind: dotenv      path: .env      must_exist: true 

services

services describes supporting infrastructure such as databases, queues, or local dependencies.

  • Use services for deterministic infrastructure startup and readiness when dependency behavior affects workloads.
  • services.<name>.producer is the canonical cross-repo ownership surface when another repo in the same ota.workspace.yaml owns the service runtime and this repo should consume that readiness truth instead of duplicating local manager YAML.
  • services.<name>.producer.repo names the owning workspace repo, producer.task names the producing task, and producer.listener stays optional only when that producer exposes exactly one declared listener.
  • The shipped producer-owned service slice stays intentionally explicit today: producer.address_view is host-view only, defaults to host, and the producer listener must declare one fixed project.host endpoint.
  • Producer-owned services must not also declare local lifecycle truth such as manager, provider, start, stop, healthcheck, endpoints, readiness, or timeout; ownership lives in one place.
  • services.<name>.manager is the typed control-plane contract for service orchestration; kind: compose is shipped and lets ota derive Compose service identity without guessing from shell snippets.
  • services.<name>.provider, .start, .stop, and .healthcheck remain legacy compatibility mode for host-bound service control.
  • services.<name>.endpoints.<context> projects the truthful address and port from each named execution context, so host and container reachability stop pretending they are the same thing.
  • services.<name>.readiness supports four forms: legacy from + run, reusable from + probe, structured endpoint probing with from + kind (tcp/http), or structured compose state probing with kind: compose_health.
  • Structured services.<name>.readiness.kind: tcp proves listener reachability for one declared endpoint projection; structured kind: http uses the same request/response contract as task readiness, but anchored by from instead of listener.
  • Structured services.<name>.readiness.kind: compose_health reads compose-managed container health (healthy) directly and does not require endpoint projections or host-port probing.
  • kind: compose_health requires services.<name>.manager.kind: compose and must not declare endpoint-probe fields such as from, method, path, headers, success, body, or timeout.
  • Use services.<name>.readiness.probe when the same transport and timeout truth should be reused from top-level readiness.probes instead of re-declaring transport details on the service; from still selects the service endpoint projection and execution context.
  • For structured HTTP service readiness, path is required and must start with /; method, headers, success.status, and body.contains are optional tightening controls for endpoint truth, but body.contains must not be combined with method: HEAD.
  • Structured service readiness also supports interval, timeout, retries, and start_period; when retries is omitted, ota keeps waiting by default, and when it is declared the failure budget becomes explicit and bounded.
  • services.<name>.required flips failures from hard errors (true) to warnings (false) for optional infrastructure.
  • Use services.<name>.depends_on when startup order matters; explicit order prevents consumers from racing on partial initialization.
  • services.<name>.timeout caps legacy startup and healthcheck operations; structured service readiness uses services.<name>.readiness.timeout instead.
  • services must declare at least one of manager, provider, start, stop, healthcheck, endpoints, or readiness, so empty service contracts are rejected.
  • services are evaluated by ota doctor and started by ota up; legacy healthchecks stay on the host compatibility path, while structured service readiness runs from the declared context and can surface the projected endpoint in findings.
  • Use required: false for optional infra that is useful but not required to keep the repo runnable.
  • Use depends_on to model readiness dependencies explicitly; order comes from the graph, not script line order.
  • Use endpoints whenever host and workload contexts reach the service differently instead of overloading localhost assumptions.
Service exampleyaml
services:  postgres:    required: true    manager:      kind: compose      name: local      file: compose.yaml      service: postgres    endpoints:      app:        address: postgres        port: 5432    readiness:      from: app      kind: tcp      interval: 5s      timeout: 3s      retries: 5      start_period: 10s

surfaces

surfaces declares reusable endpoint truth that can be attached to runtimes and workflows without duplicating listener structure.

Use it when one endpoint definition should stay canonical while multiple tasks or operations expose the same surface.

  • Use surfaces to declare reusable ports/paths once, then attach them where that endpoint is reused.
  • For each surface, <name>.kind is required and accepts http, https, or tcp.
  • <name>.port is required and must be a fixed numeric port.
  • <name>.label, <name>.purpose, and optional visibility: public|internal can improve workflow and topology UX without changing runtime behavior.
  • <name>.path is optional for HTTP surfaces and defaults to /.
  • <name>.readiness is optional and is the reusable readiness declaration for that surface.
  • <name>.readiness.kind is required when readiness is declared and can be http or tcp.
  • Use readiness.method, readiness.headers, readiness.success.status, and readiness.body.contains to tighten HTTP truth.
  • Use readiness.interval, readiness.timeout, readiness.retries, and readiness.start_period to control probe cadence and failure budget.
  • Surfaces are not standalone operational URLs; they become operational when attached through runtime or workflow fields.
  • Use tasks.<name>.runtime.surfaces list form for default publication or the object form as an attachment override when one runtime needs explicit bind/project control.
  • Use workflows.<name>.readiness.surfaces for workflow proof and { surface: <name> } in workflow exposes when one path should resolve the selected run task's attached host URL without repeating a literal.
  • ota execution topology reports both declared surfaces and the normalized listener truth produced by each attached runtime, plus additive attachment intent when one runtime used explicit bind/project overrides.
Surfaces exampleyaml
surfaces:  backend:    kind: http    port: 5678    path: /    readiness:      kind: http      path: /healthz/readiness      timeout: 10000  frontend:    kind: http    port: 8080    path: /

checks

checks is a declarative pre-run test list for readiness gates that should stay separate from regular task scripts.

  • Use checks when validation should be explicit and machine-repeatable before tasks are allowed to run.
  • checks is a dedicated readiness surface for preconditions and health validation, keeping them separate from business task scripts.
  • checks.<i>.name is the stable machine key for check output; changing it changes alert and diff stability.
  • checks.<i>.kind is precondition, health, file, or changed_files, and that choice changes how readiness is evaluated.
  • checks.<i>.severity must be error, warn, or info, and is how you decide whether failed checks are blocking or advisory.
  • checks.<i>.run is a shell command, checks.<i>.probe reuses a named readiness.probes declaration, checks.<i>.path + checks.<i>.expect declares filesystem checks, and checks.<i>.changed_files declares git-diff path checks.
  • Checks must declare exactly one of run, probe, path, or changed_files.
  • checks.<i>.timeout is optional; with probe it is inherited from the referenced probe unless the check has an explicit timeout.
  • Use kind: precondition for prerequisite commands, kind: health for readiness probes, kind: file for deterministic filesystem expectations, and kind: changed_files when gating should follow git path changes.
  • For kind: file, choose expect: exists (file or directory), file, directory, or missing based on the exact repo-state contract you want to enforce.
  • For kind: changed_files, set only paths for default HEAD comparison, set both base_ref and head_ref for explicit CI ranges, and enable include_untracked when new files should count as changed.
  • Severity choice is policy intent: use error to block readiness/execution, warn for actionable non-blocking findings, and info for signal-only checks.
  • Set timeout when a check can hang or run unpredictably (especially in CI); leave it unset for fast deterministic checks, and override probe timeout only when that check needs a stricter budget.
  • Use checks for gates that should behave identically across CI, local, and automation usage.
Check exampleyaml
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: repo-tests    kind: health    severity: warn    run: pnpm test -- --runInBand  - name: web-changed    kind: changed_files    severity: info    changed_files:      paths:        - apps/web/**      include_untracked: true

tasks

tasks is the command surface humans and agents run day to day.

Each task defines executable behavior, sequencing, and service prerequisites so execution is auditable and machine-reproducible.

  • Use tasks to declare the stable command graph that ota run, ota up, and agents execute.
  • Define exactly one of run, script, or structured launch as the executable source unless variants or mode branches cover the selected path explicitly.
  • Use run for the simple shell shorthand, script for multiline shell, and launch when ota should render and reason about packaged command or container starts without hiding them inside one shell string.
  • tasks.<name>.context binds a task to a named execution context; omitted tasks inherit execution.default_context.
  • Declare tasks.<name>.requires_services when readiness depends on infrastructure but service startup should not be encoded as an internal task edge.
  • Use requires_services when a task needs Postgres, Redis, or another canonical service ready first, but the service should still live under services rather than the task graph.
  • For setup, setup.requires_services also tells ota up which services belong in the pre-setup phase; required services not listed there are started after setup.
  • A task may define base execution (run/script) and still define variants; base execution is the fallback when no OS variant matches.
  • tasks.<name>.runtime is optional and allows task-specific workload shaping, such as long-running listener declarations.
  • tasks.<name>.runtime.listeners.<listener>.http: <port> and .tcp: <port> are shorthand for common local fixed-port listeners; use the full listener form when bind and projected host behavior differ.
  • tasks.<name>.runtime.backend_binding optionally binds that runtime-bearing task to one declared execution.shared_backends.<name> group when multiple long-running tasks should share one ota-managed backend boundary.
  • tasks.<name>.execution defines optional mode-aware execution branches with default_mode and per-mode overrides for context, lifecycle, env, run/script/launch, and runtime.
  • tasks.<name>.variants.<i>.when.os is required and must be unique, which gives deterministic variant selection by OS.
  • If a task omits both base and variant bodies, validation blocks it as an empty action.
  • tasks.<name>.depends_on creates required execution edges and is where you model sequencing instead of putting dependencies in shell scripts.
  • tasks.<name>.after_success expresses what should run next on success and is evaluated with the same task dependency checks.
  • tasks.<name>.after_failure expresses what should run next on failure and is evaluated with the same task dependency checks.
  • tasks.<name>.after_always expresses what should run after task completion regardless of outcome and is evaluated with the same task dependency checks.
  • tasks.<name>.inputs describe typed CLI inputs; input names must be lowercase snake_case and each allowed value must contain the default when set so validation stays strict.
  • tasks.<name>.inputs.<name>.default lets the task run without flags when safe values are inferred.
  • tasks.<name>.targets.<target> declares a first-class target binding for that task; use a short stable target name such as api or admin because ota uses it in receipts and runtime export.
  • Choose exactly one target identity shape: service for repo-managed producer topology, or url for one explicit declared URL.
  • tasks.<name>.targets.<target>.service.task names the producer task that owns the service you want to follow, and it must point at an existing service task in the same contract unless you also declare service.member for a monorepo member producer.
  • tasks.<name>.targets.<target>.service.member is optional and names another declared workspace.members producer member; the current shipped cross-member slice is intentionally narrow: address_view: host stays activation.mode: manual only, while address_view: topology / address_view: internal resolve only when consumer and producer share one declared backend binding on the active plane and are the only member-target shapes that support ensure_started / restart_ready / ensure_running / ensure_ready.
  • tasks.<name>.targets.<target>.service.listener picks the exact producer listener to target; it must match a declared runtime.listeners entry on that producer task, but it may be omitted when the producer exposes exactly one declared listener name.
  • tasks.<name>.targets.<target>.service.address_view chooses the reachable address shape: host, topology, or internal.
  • tasks.<name>.targets.<target>.url is the fixed declared URL form; use it when the target should not resolve through repo-managed service topology.
  • Use address_view: host when the consumer may run in a different execution plane and should target the producer's published host URL.
  • Use address_view: topology only when ota can truthfully resolve the current topology address for that task shape. Today that means either native caller host projection or a shared declared backend boundary on the active plane; address_view: internal resolves the producer's bind endpoint only when caller and producer share that declared backend boundary.
  • tasks.<name>.targets.<target>.override_input is optional and names an input on the same task that operators may use to override the resolved binding with staging, preview, or another explicit URL.
  • tasks.<name>.targets.<target>.activation.mode is optional and controls whether ota should also auto-start and observe the producer before the consumer runs; the shipped modes are manual, ensure_started, restart_ready, ensure_running, and ensure_ready, but url targets support manual only.
  • Target resolution precedence is explicit override input first, then resolved binding URL, then compatibility literal input default when one is declared and binding resolution is unavailable.
  • When override_input is omitted, ota exports the resolved binding to task execution as OTA_TARGET_<TARGET> so the task still gets a real runtime value.
  • tasks.<name>.env overlays repo env for that task only and should only be used for scoped execution overrides.
  • tasks.<name>.internal marks orchestration plumbing tasks (commonly setup) that stay in task graphs, still run through depends_on and hooks, and are hidden from default ota tasks discovery unless --all is used.
  • tasks.<name>.description defines short operator intent.
  • tasks.<name>.notes adds context and expectations for automation and execution.
  • tasks.<name>.category groups related operations for reporting and discoverability.
  • tasks.<name>.safe_for_agent is an explicit trust gate for automation execution and should be set whenever a task is safe enough for unattended agent runs.
  • tasks.<name>.safe_for_agent defaults to false when omitted, and omission is equivalent to explicitly setting safe_for_agent: false.
  • Task dependency graphs are validated for cycles so you cannot build infinite execution loops by contract shape alone.

Start here for general task authoring: descriptions, contexts, dependency edges, success/failure hooks, and OS variants.

Task exampleyaml
tasks:  compose:up:    description: Start Postgres locally    context: host    run: docker compose up -d postgres  setup:    internal: true    description: Install dependencies    context: app    run: pnpm install    safe_for_agent: true  collect-test-diagnostics:    description: Capture test diagnostics after a failed run    context: app    script: |      mkdir -p .tmp/test-artifacts      pnpm test -- --reporter=default --reporter=junit > .tmp/test-artifacts/junit.xml || true  publish-test-summary:    description: Persist a lightweight success summary after a passing run    context: app    script: |      mkdir -p .tmp/test-artifacts      printf 'test suite passed' > .tmp/test-artifacts/summary.txt  cleanup-test-artifacts:    description: Remove temporary test artifacts    context: app    script: |      rm -rf .tmp/test-artifacts  test:    description: Run the validation suite    context: app    depends_on:      - setup    after_success:      - publish-test-summary    after_failure:      - collect-test-diagnostics    after_always:      - cleanup-test-artifacts    variants:      - when:          os: linux        run: pnpm test -- --runInBand      - when:          os: macos        run: pnpm test -- --runInBand      - when:          os: windows        script: |          pnpm test -- --runInBand

Use this shape when one task should follow another repo-managed service through service.task, service.listener, service.address_view, and optional override_input.

Task target binding exampleyaml
tasks:  dev:    run: pnpm dev:api    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              primary: true              port:                mode: fixed                value: 8080              path: /   sandbox:    inputs:      base_url:        description: Override the API base URL when needed    targets:      api:        service:          task: dev          listener: http          address_view: host        override_input: base_url    run: pnpm dev:sandbox   probe:api:    targets:      api:        service:          task: dev          listener: http          address_view: host    run: curl -fsS "$OTA_TARGET_API/health"

tasks.<name>.runtime.readiness

Use tasks.<name>.runtime.readiness when a long-running task must prove that its service is genuinely usable before ota treats it as ready.

This is the task-runtime readiness surface for service tasks. It is different from services.<name>.readiness, which is the service-manager path for infrastructure declared under services.

Both surfaces now share the same structured tcp / http probe model and timing controls; the difference is that task readiness anchors probe truth with listener, while top-level service readiness anchors it with from plus one declared endpoint projection.

Use the Readiness Model page when you need the behavioral explanation for how these fields affect startup, activation, listener reachability, and confirmed endpoint truth.

  • Use probe: <name> when the runtime should reuse one top-level readiness.probes.<name> declaration instead of repeating transport fields inline.
  • Use signal_probes: [<name>, ...] when one runtime needs explicit multi-surface liveness (for example API plus worker listener). Each signal probe should target the same task runtime listener plane with target.kind: task and target.name: <task>; use target.address_view: host for projected host listeners, or target.address_view: internal for fixed same-task native listener probes.
  • Use kind: tcp when readiness truth is simply that the declared listener accepts connections.
  • Use kind: http when one real application path proves readiness more truthfully than raw socket reachability and no reusable probe already owns that transport truth.
  • For probe: <name>, ota reuses the declared transport and timeout contract while the selected listener still determines the runtime endpoint; runtime-local polling controls stay on interval, retries, and start_period.
  • listener is required for inline kind: tcp / kind: http, and optional for probe: <name> when the default projected listener is not the runtime endpoint you want to prove.
  • path is required for kind: http and should be the smallest truthful app-health route, not a broad business endpoint.
  • method is optional for kind: http; omit it for the normal GET default, or set it explicitly when the probe must stay on HEAD or another supported request form.
  • headers is optional for kind: http; use it when the health route needs explicit request headers such as Accept: application/json.
  • success.status is optional for kind: http; when omitted, ota accepts any 2xx or 3xx, and when declared it becomes the exact accepted status-code set.
  • body.contains is optional for kind: http; use it when the response body must contain one exact substring such as "status":"UP" after the status code matches, but do not combine it with method: HEAD.
  • interval is optional and sets the wait between readiness probe attempts; omit it when ota's default small internal poll cadence is fine.
  • timeout is optional and sets the per-attempt probe timeout for inline kind: tcp / kind: http readiness; when probe: <name> is used, timeout belongs on the shared top-level probe instead.
  • retries is optional and sets the consecutive failed probe budget before activation fails; omit it when ota should keep waiting until the task becomes ready, exits, or the run is interrupted.
  • start_period is optional and delays the first probe after startup begins; use it when the app needs a warmup window before any readiness check is meaningful.
  • For kind: tcp, do not declare method, headers, success, or body; those fields are HTTP-only. Timing controls such as interval, timeout, retries, and start_period still apply to TCP readiness.
  • Choose the smallest truthful readiness contract. A weak probe may turn a merely running process into a false ready state.

Use this when the service must prove both status code and body content before ota treats it as ready.

HTTP readiness with response validationyaml
tasks:  dev:    run: ./gradlew bootRun    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: 8080          project:            host:              address: 127.0.0.1              port:                mode: fixed                value: 8080

Use this when an accepting listener is the actual readiness truth and no stronger application route exists.

TCP readiness with timing controlsyaml
tasks:  dev:    run: redis-server --port 6379    runtime:      kind: service      readiness:        kind: tcp        listener: redis        interval: 1s        timeout: 500ms        retries: 20        start_period: 2s      listeners:        redis:          protocol: tcp          bind:            address: 127.0.0.1            port:              mode: fixed              value: 6379          project:            host:              address: 127.0.0.1              port:                mode: fixed                value: 6379

readiness

Use readiness when one probe definition should be declared once and reused by checks, workflows, and service/task readiness references.

Use this when endpoint truth is shared and should remain one canonical declaration instead of repeating transport details.

  • Use readiness.probes.<name> for reusable named probes and checks.<i>.probe to reuse one probe from the check surface.
  • probes.<name>.kind is http or tcp.
  • probes.<name>.url is a literal external http:// URL probe form.
  • probes.<name>.target is optional topology-derived probing through task or service targets.
  • probes.<name>.target.kind can be task or service.
  • Task targets require .target.name and .target.listener; service targets require .target.name and optional .target.endpoint.
  • Use .target.endpoint when the service declares multiple endpoints, otherwise it is required to disambiguate.
  • probes.<name>.target.name resolves the target task or service; it must match declared names.
  • probes.<name>.target.address_view is optional for task targets and defaults to host.
  • probes.<name>.target.observer.kind is command_host (default) or task.
  • probes.<name>.target.observer.task is required when observer.kind is task, and observer details do not apply to service targets.
  • probes.<name>.target.observer lets observer.kind: task resolve through another task’s effective execution plane.
  • HTTP probe fields include method, path, headers, success.status, and body.contains.
  • For convenience, expect_status remains supported as shorthand for a one-status HTTP acceptance list.
  • probes.<name>.timeout is required in milliseconds; it gates each probe attempt.
  • probes.<name>.interval, .timeout, .retries, .start_period should be used when HTTP and TCP truth need explicit cadence and bounded behavior.
  • Literal URL probes stay first-class for external endpoints; target-based probes should be used for repo-driven topology references.
  • kind: tcp currently resolves task and service targets without method/header/body framing.
Top-level readiness probesyaml
readiness:  probes:    backend-ready:      kind: http      target:        kind: task        name: dev:api        listener: backend        address_view: host      method: GET      path: /healthz/readiness      headers:        x-ota-probe: workflow      success:        status: [200]      timeout: 10000

workflows

Use workflows when repo behavior should be expressed as canonical operational paths instead of implicit task ordering.

Use one workflow for each operational intent when prepare, setup, run, readiness, services, and exposures differ.

The contract page is the source of truth for the field boundary: prepare is explicit host file prep, setup is repo preparation, and run is the primary operational path.

  • Declare default when workflows is present; it names the canonical repo workflow.
  • workflows.<name>.intent is optional intent metadata such as local_development.
  • workflows.<name>.description is optional human context for operators.
  • workflows.<name>.prepare.task, workflows.<name>.setup.task, and workflows.<name>.run.task wire host file prep, canonical setup, and the primary run shape.
  • workflows.<name>.prepare.task must reference one native action task. It is for deterministic local file preparation such as copy_if_missing, ensure_env_file, ensure_file, or ensure_directory, not shell setup, service startup, or runtime launch.
  • For workflow-scoped commands, Ota follows explicit workflow scope first: host file prep from workflows.<name>.prepare.task (if any), setup from workflows.<name>.setup.task (if any), then runtime from workflows.<name>.run.task.
  • If a workflow omits setup.task, Ota does not fall back to legacy repo-level tasks.setup for that workflow path.
  • workflows.<name>.services.required lists service dependencies for that operational path.
  • workflows.<name>.readiness.checks lets workflow-specific checks override or extend repo-wide checks.
  • workflows.<name>.readiness.probes reuses named probes for workflow-specific readiness.
  • workflows.<name>.readiness.surfaces exposes reusable, attached surfaces for workflow-specific checks.
  • workflows.<name>.exposes can point at attached surface names or literal URLs.
  • workflows.<name>.exposes supports fixed URLs and object { surface: <name> } forms.
  • ota up uses the default workflow by default and targets the prepare/setup/run phases when declared.
  • ota up runs prepare.task before required services or setup. Execution planning carries that field as additive workflow context, but prepare does not become the concrete execution task.
  • If setup.task already depends on the same action task, direct ota run setup still follows the task graph while workflow ota up avoids running the same prepare action twice.
  • ota doctor and check evaluate workflow readiness and services before falling back to repo defaults.
  • agent.entrypoint / agent.default_task remain agent hints; workflow default is the repo operational canonical path for humans and CI.
Workflow exampleyaml
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:        - backend-ready      surfaces:        - backend    exposes:      - surface: backend      - http://127.0.0.1:5678

prepare vs setup vs run

  • prepare is explicit host bootstrap before setup and must stay on one native action task
  • setup is repo preparation such as dependency install or generated artifact bootstrap
  • run is the workflow's primary operational path such as a dev server, app runtime, worker, or packaged launch
  • do not put ordinary shell setup or runtime startup in prepare
  • do not use setup as a hidden runtime phase
  • do not add prepare unless the workflow genuinely needs one explicit host bootstrap step before setup
Prepare vs setup vs runyaml
workflows:  app:    prepare:      task: setup:env:local    setup:      task: setup    run:      task: dev
Host file prep before container-backed setupyaml
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

execution

execution tells ota where task bodies should run and which backend settings are needed.

Ota supports three additive authoring patterns: single-context shorthand, named contexts, and named contexts with extends.

  • Use execution when local parity depends on whether tasks run on host, in OCI containers, or on remote providers.
  • Use single-context shorthand (preferred + lifecycle + backends) for simple repos with one primary execution shape.
  • Use named contexts when one repo needs multiple execution planes with different backend assumptions.
  • Use execution.contexts.<name>.extends as the first-class deduplication path for multi-context repos when several contexts share one base shape and only a small override set differs.
  • Choose one default execution declaration mode per contract: shorthand-only or named contexts. Do not combine root shorthand with execution.default_context / execution.contexts.
  • Inheritance merge rules are deterministic: scalars override, maps merge recursively, and lists replace.
  • Runtime selection uses that resolved merged context shape across ota run, ota up, ota doctor, and ota execution plan; inheritance is not documentation-only.
  • Do not switch backend families across extends (for example container parent to native child); ota rejects that shape to keep inheritance semantics explicit.
  • extends is additive inheritance inside one execution family, not a generic reuse mechanism where a child inherits arbitrary fields and then swaps backend later.
  • extends reuses declaration shape, not mutable backend identity: inheriting attachments.isolated_paths does not make different context names share one dependency-isolation volume.
  • extends is a strong contract feature for keeping multi-context repos DRY and aligned; it remains optional so lean one-context repos can stay on shorthand.
  • execution.default_context is the normal plane for tasks without explicit task-level context.
  • execution.supported is the allowed backend allow-list; use execution.preferred to force one when needed via CLI override.
  • execution.contexts.<name>.backend is one of native, container, or remote; only this context is used for task execution when selected by task context or default context.
  • Use native only for host execution; in that case context-level lifecycle, container, and remote keys are invalid.
  • Use container when tasks should run in OCI; execution.contexts.<name>.container.image is required, and execution.contexts.<name>.lifecycle must be explicit (persistent or ephemeral).
  • When you need to control container runtime choice, use optional execution.contexts.<name>.container.engines (for example docker/podman). Ota picks the first installed engine from that declared list.
  • If execution.contexts.<name>.container.engines is omitted, Ota falls back to docker.
  • Use execution.contexts.<name>.container.resources.memory.minimum/default when container execution needs explicit memory guarantees and a stable default request.
  • Use execution.shared_backends.<name> when multiple long-running tasks should intentionally share one ota-owned backend boundary instead of only sharing a context by coincidence.
  • Use it when the boundary itself matters: ota should own reuse, shared lifecycle, shared fulfillment, and truthful co-location semantics for more than one workload.
  • Do not use it when tasks are independent and only need to call each other through normal host-published endpoints; plain task execution or target binding is enough in that case.
  • execution.shared_backends.<name>.backend is the backend family for that shared boundary; the current shipped families are local container, local native, and remote remote.
  • execution.shared_backends.<name>.scope is required and must match the backend family (local for container/native, remote for remote shared backends).
  • execution.shared_backends.<name>.lifecycle is required and becomes the lifecycle truth for the shared backend group.
  • execution.shared_backends.<name>.context is optional but recommended when the shared backend must stay pinned to one named execution context.
  • execution.contexts.<name>.requirements.runtimes and .requirements.tools describe what that execution plane needs; they do not by themselves mean ota will provision it automatically.
  • execution.shared_backends.<name>.fulfillment is the shared-backend runtime-intent switch: use none to fail clearly on missing requirements and run to let ota attempt approved run-path provisioning before any bound task body or dependency uses that backend.
  • execution.contexts.<name>.fulfillment is the direct container-context form of the same opt-in intent; the current slice supports container contexts only and uses run to prepare the selected execution container on the actual run path.
  • Org policy still decides whether ota may actually fulfill those requirements and which sources/versions are approved; fulfillment: run does not bypass policy.
  • execution.shared_backends.<name>.environment is currently container-only: use profile or image_alias for policy-backed approval, image for literal compatibility, and source only alongside a literal image.
  • An empty execution.shared_backends.<name>.environment: {} is valid for container shared backends when the repo wants policy default_profile resolution to choose the effective backend image; if no default profile applies, ota falls back to the task/container image and keeps shape validation honest.
  • execution.shared_backends.<name> is the shipped shared-boundary surface now. scope: remote is now shipped for backend: remote, and remote producer auto-start is now shipped in two honest slices when caller and producer share one declared remote backend binding: built-in remote providers use address_view: host with one fixed project.host endpoint or shared-remote address_view: topology / address_view: internal with remote-plane probes, and readiness may be tcp or http; backend-provider remote activation now also covers shared-remote address_view: host / address_view: topology / address_view: internal when the matching backend_provider extension declares activation.provider_managed_cleanup: true so ota can own cleanup and restart honestly. Built-in providers are ssh (user@host), tsh (user@host), kubectl (pod/ota-dev), and daytona (sandbox-dev).
  • ota execution plan, ota run, and run receipts now resolve the same effective shared-backend image for both explicit-context and inferred-context backend bindings.
  • Shared local backends now keep backend truth and workload truth separate: the backend still owns image/lifecycle/fulfillment identity, while bound workloads may differ in listeners, readiness, commands, and publications.
  • Ota rejects real workload-local conflicts inside that shared boundary, including conflicting in-backend bind endpoints and conflicting fixed host publications.
  • Run receipts and summaries now surface backend-fulfillment evidence separately from task failure: requirements_satisfied means the backend was already ready, fulfilled means ota provisioned successfully, missing_requirements means fulfillment was not allowed/selected, and failed means ota tried to prepare the backend but did not complete successfully.
  • Use remote when tasks run off-host.
  • Important remote fields:
  • execution.contexts.<name>.remote.provider is required and can be daytona, ssh, tsh, kubectl, or a backend_provider extension key.
  • execution.contexts.<name>.remote.target is required and identifies the remote boundary.
  • execution.contexts.<name>.remote.cwd is optional and sets the working directory on that remote boundary.
  • execution.contexts.<name>.remote.ssh.config_file and .identity_file are optional and are valid only for provider: ssh.
  • Default SSH recommendation: omit remote.ssh and let normal OpenSSH behavior handle ~/.ssh/config, SSH agent/default identity selection, and host aliases. Add explicit SSH hints only when the repo must force one config or key path.
  • execution.contexts.<name>.requirements.runtimes and .requirements.tools override or augment root requirements only for that context, keeping host/container constraints honest.
  • execution.contexts.<name>.attachments.compose is a list of compose manager names used to wire host/network reachability for that context.
  • Keep those attachment names aligned with services.<name>.manager.name and the Compose project name you use for normal docker compose workflows; otherwise ota may look for the wrong network namespace during ota up and ota run.
  • Example: if the repo naturally runs under Compose project qredex-core, keep attachments.compose: [qredex-core], set service managers to name: qredex-core, and use plain docker compose ... in that repo or explicit docker compose -p qredex-core ....
  • Legacy fields execution.preferred + execution.lifecycle + execution.backends.container/execution.backends.remote are the shorthand-only path; once a repo declares named contexts, those root defaults are no longer valid.
  • execution.backends.remote.provider shares the same constraints as context-level remote providers.
  • For legacy declarations, set execution.backends.container.image when execution.preferred is container, and execution.backends.remote.target when execution.preferred is remote.
  • Task execution and receipts resolve a concrete backend through context + context_name or fallback logic, so you can inspect effective behavior in ota execution plan, ota run, ota up, and doctor output.

Pattern 1: single-context shorthand (lean repos). Use the shorthand shape when one execution plane is enough and the repo does not need named contexts yet.

Pattern 1yaml
execution:  preferred: container  lifecycle: ephemeral  backends:    container:      image: node:24-bookworm

Pattern 2: named contexts (explicit multiple planes). Use named contexts when one repo has multiple execution planes and each plane should stay explicit.

Pattern 2yaml
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

Pattern 3: named contexts with extends (deduped multi-context). Use extends when multiple contexts share one base shape and only a small override set differs.

Pattern 3yaml
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

Invalid: crossing backend families through extends. Ota rejects this shape because extends is additive within one backend family, not a way to inherit and then switch families.

Invalid exampleyaml
execution:  default_context: app  contexts:    host:      backend: native      requirements:        tools:          docker: "*"     app:      extends: host      backend: container      lifecycle: ephemeral      container:        image: node:24-bookworm

Use this fuller shape when one repo really needs host, container, and remote execution planes together.

Full multi-plane exampleyaml
execution:  default_context: app  contexts:    app-base:      backend: container      lifecycle: persistent      container:        image: ghcr.io/ota/dev:latest        engines:          - docker          - podman      requirements:        runtimes:          node: "22"        tools:          pnpm: "10"    host:      backend: native      requirements:        tools:          docker: "*"    app:      extends: app-base      attachments:        compose:          - local      container:        resources:          memory:            minimum: 2GiB            default: 3GiB    remote-admin:      backend: remote      remote:        provider: ssh        target: root@ci.example        cwd: /workspace/ota      requirements:        runtimes:          node: "22"      lifecycle: ephemeral    remote-ci:      backend: remote      lifecycle: ephemeral      remote:        provider: ssh        target: root@ci.example        cwd: /workspace/repo      requirements:        runtimes:          node: "22"        tools:          pnpm: "10"

Container Execution

A Dockerfile builds an image, but ota.yaml still defines what is required, safe, and what readiness means for this repo.

  • Use a Dockerfile for environment image construction and ota.yaml for readiness/execution decisions.
  • ota run and ota up consume this contract, so changing Dockerfile logic without updating contract requirements creates drift.
  • If you already ship a Dockerfile, still document required tools/runtimes and service behavior in ota.yaml so CI and agents stay deterministic.
  • This split lets you rebuild images without changing when readiness gates should block or pass.
Simple modeltext
Dockerfile  -> builds the imageota.yaml    -> declares what the repo needsota run/up  -> executes against the declared environment
Example contractyaml
version: 1project:  name: sample-java-serviceruntimes:  java: ">=21"tools:  maven: "*"tasks:  setup:    internal: true    run: mvn -q dependency:go-offline  test:    run: mvn testexecution:  preferred: container  lifecycle: persistent  backends:    container:      image: eclipse-temurin:21-jdk

agent

agent tells ota which execution and edit boundaries an AI agent may use without guessing.

  • Use agent to give automation a trusted execution boundary instead of relying on repository conventions.
  • agent.posture declares the default authority model for edits.
  • Supported agent.posture values are readiness_strict, contract_authoring, and infra_authoring.
  • Use readiness_strict for normal readiness slices where contract files, CI, runtime topology, lockfiles, and env/config stay sensitive by default.
  • Use contract_authoring when editing ota.yaml is intentionally part of the allowed slice.
  • Use infra_authoring when CI and runtime-topology files are intentionally part of the allowed slice.
  • agent.entrypoint is the first task an agent should run when entering a repo so context is established consistently and safely.
  • agent.default_task is the recurring verification path after normal follow-up changes.
  • agent.safe_tasks is the allowlist that gates what automation can execute by default.
  • Ota builds the safe execution set from tasks marked safe_for_agent: true plus names listed under agent.safe_tasks; explicit safe_for_agent: false adds no extra behavior beyond omission.
  • agent.verify_after_changes is the mandatory verification surface the repo expects after file edits.
  • agent.writable_paths and agent.protected_paths define what can and cannot be edited by automation.
  • agent.exceptions.sensitive_writes is the narrow exception list for sensitive paths when the declared posture is still otherwise correct.
  • agent.inferred_boundary.reviewed records whether the starter or inferred boundary has operator confirmation.
  • agent.inferred_boundary.provenance.writable_paths is the source list behind inferred writable paths.
  • agent.inferred_boundary.provenance.protected_paths is the source list behind inferred protected paths.
  • agent.inferred_boundary is only valid with at least one provenance entry, and each provenance list must be non-empty.
  • Use agent.writable_paths and agent.protected_paths for the confirmed active boundary after operator review.
  • agent.bootstrap.ota.note explains the bootstrap intent; .sh/.powershell provide the commands for the same bootstrap intent.
  • agent.bootstrap.ota is optional, but if present it must provide at least one shell command and should pin ota version (OTA_VERSION or version flag) so bootstrap stays deterministic.
  • agent.notes is the human handoff layer that should describe repo operating expectations beyond field-level structure.
Posture valuesyaml
agent:  posture: readiness_strict agent:  posture: contract_authoring agent:  posture: infra_authoring
Agent exampleyaml
agent:  posture: readiness_strict  entrypoint: setup  default_task: test  safe_tasks:    - setup    - test  verify_after_changes:    - test  writable_paths:    - src    - docs    - .github/workflows  exceptions:    sensitive_writes:      - .github/workflows  protected_paths:    - ota.yaml  bootstrap:    ota:      note: Setup project-local toolchain aliases if required.      sh: scripts/setup-agent.sh      powershell: scripts/setup-agent.ps1  notes: Keep agent edits narrow and rerun test after changes.

exports

exports carries repo-owned hints for downstream tooling and is intentionally not part of readiness evaluation.

  • Use exports for downstream integrations, not for readiness or execution behavior.
  • exports is a freeform compatibility map for downstream tooling, not readiness checks.
  • exports keys are consumed by integrations that opt in, so schema changes are a consumer-facing contract decision.
  • Keep exports stable for systems that build on it (agent, docs, artifact publish, etc.).
Exports exampleyaml
exports:  agent:    include_readme: true

policies

policies is not a readiness execution surface.

  • Use policies only as repository-local metadata for policy-aware tooling; execution and readiness stay in typed sections.
  • Treat policies as a repo-local extension map for tooling that reads it explicitly.
  • Core policy surfaces (env, version_policy, provisioning, adapter_bootstrap) are enforced from the effective org policy source (OTA_POLICY, nearest ancestor .ota/org-policy.yaml, or workspace.policy) and are rejected when declared in ota.yaml.
  • policies.env in ota.yaml is not a soft override; repo contracts fail validation and must move approved values into the effective org policy source under policies.env.values.
  • Use this separation to keep org-level control explicit and avoid policy drift across repos.
  • Any additional keys here must be consumed by tooling that owns their schema, otherwise you only create brittle, undocumented coupling.
Policies exampleyaml
policies:  annotations:    tags:      - core      - infra

extensions

extensions declares explicit extension seams for check, export, and remote execution behaviors without changing core contract semantics.

  • Use extensions when you need platform integrations without overloading the core contract with implementation details.
  • extensions.<name>.kind must be check_provider, export_provider, or backend_provider and determines the invocation pathway.
  • extensions.<name>.command is the executable invoked by Ota; keep this deterministic and version-managed.
  • extensions.<name>.api_version must be greater than 0, with larger values requiring compatible protocol support.
  • extensions.<name>.description is optional context; .config is extension-specific structured input and is passed directly.
  • check_provider adds check capabilities, export_provider adds artifact export behaviors, backend_provider defines execution transport adapters.
  • Use backend_provider only when local policy or platform requirements require a custom execution transport.
  • Do not duplicate first-class repo fields behind extensions; keep behavior discoverable and intentional.
Extension exampleyaml
extensions:  demo:    kind: check_provider    command: ota-ext-demo    api_version: 1    description: Example check provider descriptor   remote-shell:    kind: backend_provider    command: ota-ext-remote-shell    api_version: 1    description: Example remote execution backend

metadata

metadata is an open map for repo-owned descriptive information that should not alter execution semantics.

  • Use metadata for ownership and context, and keep operational truth in typed sections above.
  • metadata accepts arbitrary YAML and should only carry descriptive information.
  • metadata.ota.detect is reserved for detect ownership metadata; keep it mapping-shaped and preserve ownership entries written under metadata.ota.detect.field_ownership.
  • metadata.ota.minimum_version is the reserved compatibility hint for contracts that require a newer ota binary; keep it as a semver string such as 1.6.16.
  • When ota can identify a newer shipped contract feature, compatibility errors now name that unsupported contract feature before the upgrade step instead of only saying the binary is too old.
  • Those errors also report the current binary identity and point operators back to ota --version --json as the confirmation surface after install or rebuild.
  • Keep schema_version for non-additive contract-generation changes; additive compatibility growth should extend contract_capabilities instead.
  • metadata.ota.detect is used to remember whether fields were inferred as merged or intentionally pinned as manual before a merge/rewrite path.
  • If metadata.ota or metadata.ota.detect is repurposed as a scalar or list, detect merge can fail because ownership state cannot be stored reliably.
  • Use metadata for cataloging and ownership fields that help humans, but avoid encoding correctness logic there.
  • If you need policy or validation behavior, put it in typed sections instead of metadata so checks stay deterministic.
  • Execution and safety constraints must stay in typed contract sections, otherwise readiness checks become non-deterministic.
Metadata exampleyaml
metadata:  team: platform  owner: ota  created_at: 2026-03-23  ota:    minimum_version: "1.6.15"    detect:      field_ownership:        project.name: merged        tools.pnpm: manual

workspace

workspace is only for a monorepo root that coordinates multiple repos.

  • workspace.type is monorepo today and only marks an orchestration root.
  • workspace.members is an ordered repo path list, and order impacts dependent bootstrap behavior.
  • Use this only at the root that owns planning; each member stays authoritative for its own contract execution data.
  • Keep workspace out of normal single-repo contracts to avoid collapsing repo boundaries and execution ownership.
  • Do not use workspace when a single ota.yaml should act as a standalone repo contract.
Workspace exampleyaml
workspace:  type: monorepo  members:    - apps/web    - services/api

Full example

This example shows the full contract shape in one place so users can see how the pieces fit together.

Full contract exampleyaml
version: 1project:  name: example-repo  type: application  description: Example repo for the public docsenv:  vars:    OTA_ENV:      required: true      default: local      allowed:        - local        - ci    DATABASE_URL:      required: trueservices:  postgres:    required: true    manager:      kind: compose      name: local      file: compose.yaml      service: postgres    endpoints:      app:        address: postgres        port: 5432    readiness:      from: app      run: pg_isready -h postgres -p 5432checks:  - name: node-installed    kind: precondition    severity: error    run: node --version    timeout: 10  - name: repo-tests    kind: health    severity: warn    run: pnpm test -- --runInBandtasks:  compose:up:    description: Start Postgres locally    context: host    run: docker compose up -d postgres  setup:    internal: true    description: Install dependencies    context: app    run: pnpm install    safe_for_agent: true  test:    description: Run the validation suite    context: app    depends_on:      - setup    variants:      - when:          os: linux        run: pnpm test -- --runInBand      - when:          os: macos        run: pnpm test -- --runInBand      - when:          os: windows        script: |          pnpm test -- --runInBandexecution:  default_context: app  contexts:    host:      backend: native      requirements:        tools:          docker: "*"    app:      backend: container      lifecycle: persistent      attachments:        compose:          - local      requirements:        runtimes:          node: "22"          python: ">=3.12"        tools:          pnpm: "10"          go: "1.24"      container:        image: ghcr.io/ota/dev:latest        engines:          - docker          - podmanexports:  docs:    include_readme: trueextensions:  demo:    kind: check_provider    command: ota-ext-demo    api_version: 1policies:  annotations:    tags:      - coreagent:  entrypoint: setup  default_task: test  safe_tasks:    - setup    - test  verify_after_changes:    - test  writable_paths:    - src    - docs  protected_paths:    - ota.yaml  notes: Keep agent edits narrow and rerun test after changes.metadata:  team: platformworkspace:  type: monorepo  members:    - apps/web    - services/api