yolop 0.14.0

Yolop — a terminal coding agent built on everruns-runtime
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    types: [opened, reopened, synchronize]
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  CARGO_INCREMENTAL: 0

jobs:
  # Classify the diff once, up front, so the rest of the pipeline can scope
  # itself to what actually changed. A docs-only PR (e.g. a README badge tweak)
  # must not spin up the whole Rust toolchain, burn a live-provider API budget,
  # or occupy the scarce macOS/Windows runners. GitHub treats a job skipped by
  # `if:` as a passing required check, so skipped legs never block merge.
  changes:
    name: Detect changes
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      rust: ${{ steps.filter.outputs.rust }}
      docs: ${{ steps.filter.outputs.docs }}
      eval-analyzers: ${{ steps.filter.outputs.eval-analyzers }}
      terminal-bench: ${{ steps.filter.outputs.terminal-bench }}
    steps:
      - uses: actions/checkout@v7

      - uses: dorny/paths-filter@v4
        id: filter
        with:
          filters: |
            rust:
              - '**/*.rs'
              - '**/Cargo.toml'
              - 'Cargo.lock'
              - 'rust-toolchain.toml'
              - '**/build.rs'
              - '.github/workflows/ci.yml'
            docs:
              - '**/*.md'
            # The regression-gate analyzers under harness_basic are pure Python
            # and decide whether the nightly Search Efficiency Eval passes. They
            # never touch the Rust toolchain, so give them their own fast leg that
            # runs whenever an analyzer or its tests change.
            eval-analyzers:
              - 'evals/harness_basic/*.py'
              - 'evals/harness_basic/tests/**'
              - '.github/workflows/ci.yml'
            # The terminal_bench study and its Harbor agent adapter. Its tests
            # need no Docker, network, or API key, but they do import the study's
            # third-party deps, so they get their own uv leg rather than joining
            # the stdlib-only analyzer job above.
            terminal-bench:
              - 'evals/terminal_bench/**'
              - '.github/workflows/ci.yml'

  # Cheap guard for the documentation boundary (README/docs must not link into
  # internal knowledge/ or .agents/). Runs whenever Markdown changes — including
  # docs-only PRs, where the Rust `lint` job below is skipped — and needs no
  # Rust toolchain, so it stays fast.
  docs-boundary:
    name: Docs boundary
    needs: changes
    if: needs.changes.outputs.docs == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v7

      - name: Validate release and knowledge metadata
        run: |
          python3 scripts/validate_okf.py knowledge --check-links
          python3 scripts/test_validate_okf.py
          python3 scripts/test_publish_order.py
          test "$(python3 scripts/publish_order.py | tail -1)" = yolop

      - name: Public docs boundary
        run: |
          if grep -R -n -E --include='*.md' '\]\([^)]*(knowledge/|\.agents/)' README.md docs; then
            echo "::error::Public documentation must not link to internal knowledge/ or .agents/ files."
            exit 1
          fi

  eval-analyzers:
    name: Eval analyzers (Python)
    needs: changes
    if: needs.changes.outputs.eval-analyzers == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v7

      - name: Run analyzer unit tests
        # Standard library only — no third-party deps, so the system python is
        # enough and there's nothing to install.
        run: python3 -m unittest discover -s evals/harness_basic/tests -v

  terminal-bench-study:
    name: Terminal-Bench study (Python)
    needs: changes
    if: needs.changes.outputs.terminal-bench == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Run study + adapter unit tests
        working-directory: evals/terminal_bench
        # Hermetic by design: no Docker, no network beyond the dependency
        # install, no provider key. uv resolves the study's deps (mira-eval for
        # the protocol types, harbor for the agent base class and ATIF models).
        env:
          PYTHONPATH: .
        run: uv run --python 3.12 --with mira-eval --with harbor -m unittest discover -s tests -v

  lint:
    name: Format + Clippy
    needs: changes
    if: needs.changes.outputs.rust == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.94.0
        with:
          components: rustfmt, clippy

      - name: Cache Rust
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "debug"

      - name: Format check
        run: cargo fmt --all -- --check

      - name: Clippy
        run: cargo clippy --all-targets --all-features -- -D warnings

  test:
    name: Build + Tests + Offline Smoke
    needs: changes
    if: needs.changes.outputs.rust == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@v7

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.94.0

      - name: Cache Rust
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "debug"

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-llvm-cov

      - name: Build
        run: cargo build --locked --all-targets

      - name: Unit and integration tests with coverage
        # The live provider tests skip themselves here (no API keys and
        # YOLOP_REQUIRE_LIVE_TESTS unset); they run in the `live-smoke` job below.
        run: |
          cargo llvm-cov --no-report --all-features --workspace
          cargo llvm-cov report --lcov --output-path lcov.info
          cargo llvm-cov report --summary-only

      - name: Upload coverage to Codecov
        if: always()
        uses: codecov/codecov-action@v7
        with:
          files: lcov.info
          fail_ci_if_error: ${{ github.event_name == 'push' }}
          token: ${{ secrets.CODECOV_TOKEN }}

      - name: Upload coverage artifact
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: lcov-coverage
          path: lcov.info
          if-no-files-found: error

  live-smoke:
    name: Live Provider Smoke (Doppler)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    # Gate this job out entirely on fork PRs and dependabot PRs where
    # DOPPLER_TOKEN is not exposed. When the job does run, it must actually
    # run the tests — YOLOP_REQUIRE_LIVE_TESTS=1 (below) turns a missing
    # provider key into a hard failure so the live check can't report green
    # without exercising anything.
    if: (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && github.actor != 'dependabot[bot]'
    needs: [lint, test]
    env:
      DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
    steps:
      - uses: actions/checkout@v7

      - name: Verify DOPPLER_TOKEN is set
        run: |
          if [ -z "${DOPPLER_TOKEN}" ]; then
            echo "::error::DOPPLER_TOKEN is required for the live smoke job but is not set. Configure the repo secret or update the job's outer 'if:' to skip this environment."
            exit 1
          fi

      - name: Install Doppler CLI
        uses: dopplerhq/cli-action@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.94.0

      - name: Cache Rust
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "debug"

      - name: Build the test binary (offline)
        run: cargo test --no-run --test integration

      - name: Live provider smoke tests
        env:
          YOLOP_REQUIRE_LIVE_TESTS: "1"
        run: doppler run -- cargo test --test integration -- --nocapture

  sandbox-contract:
    name: Sandbox Contract (${{ matrix.os }})
    needs: changes
    if: needs.changes.outputs.rust == 'true'
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v7

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.94.0

      - name: Cache Rust
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "sandbox-${{ runner.os }}"

      - name: Run sandbox contract suite
        run: cargo test sandbox -- --nocapture

  # The (comparatively scarce) Windows runner only spins up when the change
  # actually touches Rust — not on doc- or eval-only changes.
  windows:
    name: Windows Build + Shell Smoke
    needs: changes
    if: needs.changes.outputs.rust == 'true'
    runs-on: windows-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v7

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@1.94.0

      - name: Cache Rust
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "windows"

      - name: Build the yolop binary
        run: cargo build --locked -p yolop

      - name: Sandbox + shell tests
        run: cargo test -p yolop sandbox -- --nocapture

      - name: Offline smoke (PowerShell shell)
        run: cargo run -p yolop -- --provider llmsim -p "hi"