Skip to content

Services

cairn services owns the multi-service environment a spec pool needs: docker, conditional data seeding, and tmux session management — all config-driven, started once before the pool and stopped after the last spec. cairn services status is the read-only check; the lifecycle itself runs automatically on cairn run unless disabled.

cairn services status

bash
cairn services status
cairn services status --config ./cairntrace.config.yml --project my-app

Reports the current state of the configured services environment (docker, seed freshness, tmux session). --project <name> overrides the project name (default: from config); --config <path> picks an explicit config. Output supports --format json|yaml|md.

The lifecycle (on cairn run)

When services: is configured, cairn run starts the environment once, runs the spec pool, then tears it down. The phases:

yaml
version: 1
environments:
  local: {}
services:
  docker:
    command: "docker compose up -d"
    reuseExisting: true
    readinessCheck: "curl -sf http://localhost:27017"
    healthcheck:
      command: "curl -sf http://localhost:9200/_cluster/health | grep -q green"
      intervalSeconds: 15
      retries: 5
  seed:
    command: "yarn demo-import"
    ttlSeconds: 21600
    freshnessCheck: "mongosh --quiet --eval 'db.count()' mongodb://localhost:27017/db"
    # Always run after seed (even when skipped as fresh) — lightweight fixture ensure.
    postCommands:
      - "mongosh mongodb://localhost:27017/db --quiet tools/ensure-fixture.js"
  tmux:
    session: myapp
    reuseExisting: true
    waitForReadyBeforeNext: true # opt-in: gate each later window on prior readyOn
    options:
      - { key: mouse, value: "on" }
    env:
      NODE_ENV: development
    windows:
      - name: web
        cwd: web-app
        command: "yarn serve"
        readyOn: { url: http://localhost:8080 }
        healthcheck:
          command: "curl -sf http://localhost:8080/healthz"
          intervalSeconds: 20
          retries: 3
  artifacts:
    when: on-failure
    capture: [lifecycle, tmux, docker, seed]
    maxLinesPerSource: 2000
    maxBytesPerSource: 524288
    maxBytesPerRun: 8388608
  stash:
    enabled: true
    autoStash: always
    capture: [tmux, docker, seed]
    tags: [services, myapp]
  teardown:
    - "tmux kill-session -t myapp"
    - "docker compose down"
  • dockercommand runs once; reuseExisting: true skips if the readiness check already passes. readinessCheck gates startup; healthcheck polls until green or retries is exhausted.
  • seed — runs after docker is healthy. Freshness is tracked at ~/.cairntrace/services/<project>.seed.json with a three-layer check (fingerprint + TTL + optional data-level command). A fresh-enough seed is reused; otherwise the seed command re-runs. Optional postCommands always run after that decision (skip or complete) — use them for lightweight fixture ensure scripts the bulk import does not ship.
  • tmux — a named session with one or more windows, each with its own cwd, command, readyOn, and healthcheck. readyOn can be { url } or { text }. By default Cairn boots every window before waiting for readiness. Set waitForReadyBeforeNext: true to boot in declaration order and wait for each window's readyOn before creating or starting the next; all windows share the single readyTimeoutMs deadline, and a dead/disappeared pane fails immediately. On reuse, missing windows are created and idle panes (shell prompt, no running service) are re-launched; busy panes are left alone. If docker was freshly started this run, the whole session is recreated so app processes reconnect to new containers. Cairn waits for the interactive shell before send-keys and clears pane history first so readyOn text cannot match stale scrollback.
  • artifacts — saves bounded, redacted lifecycle, docker/provisioner command output, tmux, local Compose, and seed/post-command evidence inside each run while the services are still alive. A remote provisioner such as Chalupa retains its launch/tunnel transcript without probing an unrelated local Compose project. The block is optional and defaults to when: on-failure, all four sources, 2,000 lines and 512 KiB per source, and 8 MiB total per run. Set when: always while stabilizing a suite or never to disable it. Collection errors are recorded in services/manifest.json and never change the test verdict.
  • stash — optionally saves session artifacts (tmux panes, docker logs, seed output) in the local file.cheap vault. It does not upload or replicate them.
  • teardown — after the last spec. When tmux reuse is on (the default), cairn leaves the session alive and also skips docker compose down so infra the live panes need is not torn out from under them. With tmux.reuseExisting: false, full teardown runs (tmux kill + docker down).

Reading service artifacts from a run

Runs that capture service evidence keep it inside the self-contained artifact pack under services/: services/manifest.json describes the capture and services/tmux/<window>.log contains each sanitized tmux window log. Use the run-aware log commands so the evidence remains tied to the exact behavioral run that produced it:

bash
cairn logs latest --services
cairn logs latest --service web-api
cairn logs previous --service worker

When ref is omitted, service lookup uses latest. Cairntrace checks the selected run first. If it has no run-local service pack (or no matching tmux window), the command falls back to the legacy pane logs under ~/.cairntrace/services (or CAIRN_SERVICES_LOG_ROOT). This compatibility fallback keeps older runs inspectable while new captures move into run directories managed by the normal retention policy.

Skipping and per-environment overrides

bash
cairn run flows/x.yml --no-services           # skip the whole lifecycle
cairn run flows/x.yml --services-dry-run       # print the plan and exit; do not run specs

The printed plan keeps commands readable while replacing interpolated environment and selected-vault secret values with [redacted].

Per-environment overrides replace --no-services for remote envs:

yaml
version: 1
services:
  tmux:
    session: myapp
    windows:
      - name: web
        command: "bun run dev"
environments:
  dev:
    services: false          # disable all services (app is already deployed remotely)
  staging:
    services:                # partial block deep-merges over the top-level one
      seed:
        command: "bun run seed:staging"
        ttlSeconds: 3600
    secrets:                 # an env-level secrets block REPLACES the top-level one
      provider: tvault
      tvault:
        project: myapp-staging
  remote:
    services:                # keep docker/seed phases, drop inherited local tmux windows
      tmux: false

A partial services: block deep-merges over the top-level one. An env-level secrets: block replaces the top-level one entirely. Inside a partial services: block, tmux: false removes only the inherited local tmux windows while keeping the docker and seed phases — for apps running remotely over a tunnel that still own provisioning and seeding.

TinyVault seeding

secrets.provider: tvault resolves only the invocation's explicit secrets.keys, required, and root-spec/imported-action placeholder names, then supplies that scoped set to the seed command. It never exports a whole project or mutates global process.env; publisher-only and TinyVault client-control variables are removed from service children. The tvault: block supports direct (project) or inheritance (group + env) mode. See Secrets for the status command and the cairn secrets diagnostic.

Validation

cairn config validate --json validates the config file — the zod schema plus cross-field .refine() rules: unique window names, readyOn constraints, and tvault provider requires a tvault: block with either project or group+env. Run it before relying on a services block in CI.

See also

  • Configuration — the services: schema and env resolution
  • Secrets — the TinyVault integration the seed step uses
  • Stashservices.stash persists session artifacts to fcheap

Local-first browser specs for coding agents. Released under the MIT License.