Orientation
Topology decision guide
How to choose between repo-wide requirements, execution-context requirements, service managers, and task prerequisites.
Recommended next
Start with the decision
Use the smallest contract scope that stays truthful.
If a dependency is shared everywhere, keep it at the repo level. If it only belongs to one execution plane, scope it to that context. If it belongs only to one front door, put it under tasks.<name>.requirements. If the dependency is host-native build tooling, define it under native_prerequisites and select it from the task that needs it. If Ota should own a service's lifecycle, put it under services. If a task only needs a service ready before it runs, declare that with tasks.<name>.requires_services.
shared everywhere -> top-level runtimes/toolsone execution plane -> execution.contexts.<name>.requirementsone front door -> tasks.<name>.requirementshost-native build tooling -> native_prerequisites + requirements.nativeservice lifecycle -> services.<name>.managertask needs a ready service -> tasks.<name>.requires_servicesWhen the base context is too broad
If one host task needs only a shell and a file write, while the rest of the host-side workflow needs npm, Poetry, or other heavier tooling, do not put those tools on the broad host context.
Split the context instead. Keep host light, then add a narrower shared context such as host-dev for build, lint, test, and runtime tasks that really share the same dependency plane.
execution: default_context: host contexts: host: backend: native host-dev: extends: host requirements: runtimes: python: ">=3.12,<3.14" node: "^22.12.0" tools: poetry: ">=1.8" npm: ">=10.5.0" tasks: setup:config-basic: context: host run: make setup-config-basic build: context: host-dev run: make build lint:frontend: context: host-dev run: make lint-frontendContract baseline vs task-scoped requirements
Keep shared contract requirements at the repo root.
Keep contributor-only, quickstart-only, or Docker-only prerequisites on the task path that actually needs them.
Task-path requirements.tools are self-contained: tool names do not need duplicate top-level tools.<name> entries just to validate.
Use tasks.<name>.requirements.any_of when one task has explicit alternative prerequisite lanes (for example host-local vs docker-host) across runtimes, tools, toolchains, native, env, and checks.
Contract-wide
Top-level runtimes/tools
Use this for dependencies that every task, check, and execution path needs across the repo.
Task-scoped
Task requirements
Use this when only one front door needs a runtime, tool, env var, or precondition check.
toolchains: node: provider: corepack version: "24" tools: pnpm: version: ">=10" acquisition: provider: corepack package: pnpm version: "10.22.0" 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" - name: workspace-dependencies-installed kind: file severity: error path: node_modules expect: directory tasks: setup:env-local: action: kind: ensure_env_file path: .env.local template: .env.example vars: DATABASE_URL: value: postgresql://localhost:5432/app JWT_SECRET: random: bytes: 32 encoding: hex dev: run: pnpm dev depends_on: - setup:env-local requirements: runtimes: node: ">=24" tools: pnpm: ">=10" native: - node-native-build-tools checks: - workspace-dependencies-installed docker:run: launch: kind: container image: ghcr.io/example/app:latest requirements: tools: docker: "*" setup:services: context: host run: bash scripts/setup-local-services.sh requirements: any_of: - label: local-services when: context: host tools: psql: "*" native: - node-native-build-tools env: - DATABASE_URL checks: - workspace-dependencies-installed - label: docker-services when: context: docker-host tools: docker: "*" toolchains: - nodeService-scoped lifecycle
Keep service-specific control and readiness with the service manager when Ota owns that service.
A task can still require the service without becoming the service owner.
services: postgres: manager: kind: compose readiness: from: app run: pg_isready -h postgres -p 5432Service lifecycle vs task prerequisites
Services own lifecycle and readiness. Tasks can ask for those services without turning them into dependency edges.
That keeps the service canonical and keeps the task graph honest.
Own the service
Service-owned lifecycle
Use this when Ota should own service lifecycle and readiness instead of leaving that work to a task wrapper.
Depend on it
Task-declared prerequisite
Use this when a task should wait for a ready service before it runs, without turning the service into a dependency edge.
- When the task is
setup,setup.requires_servicesalso becomes the pre-setup service phase forota up: ota starts and verifies those services beforesetup, then brings the remaining required services up after setup.
services: postgres: manager: kind: composetasks: db:integration: context: app requires_services: - postgres run: mvn testNative, container, and remote
Pick the execution plane that matches the real work.
Native
Use native when the repo should run directly on the host OS and its local toolchain.
Container
Use container when the app or task needs an isolated image, deterministic toolchain, or shared compose-backed services.
Remote
Use remote when execution must happen on a declared transport target instead of the local machine.
How Ota turns the choice into payoff
ota doctorshows the missing piece first, so the decision is visible instead of implicit.ota upnow uses setup-aware service orchestration:setup.requires_servicesruns first as the pre-setup service phase, then setup runs, then ota starts the remaining required services before final readiness diagnosis.ota runcan demand ready services by name, so tasks do not need to duplicate service ownership.- Receipts and JSON keep the same truth for humans, CI, and agents, so the contract stays reviewable.
ota doctorota upota run db:integrationota receipt --jsonCommon mistakes
- Do not put Docker on the top level just because one context needs it.
- Do not use
depends_onwhen you mean a service prerequisite; userequires_servicesinstead. - Do not turn services into ordinary tasks just to get a startup order.
- Do not use
compose:upas the canonical service path once typed managers exist.
tasks: compose:up: run: docker compose up -d db:integration: depends_on: - compose:up