# ORCH-101 (replication foundation): every host-specific value is interpolated
# as ${VAR:-default}; the defaults equal the current production values, so an
# empty environment resolves to a byte-for-byte equivalent of the previous file
# (zero regression, BR-5). Compose reads ${VAR} from the project `.env` /shell —
# NOT from a service's env_file (so .env.staging does NOT interpolate); the
# Settings-shared names (ORCH_AGENT_HOME_DIR, ORCH_STAGING_PORT, ...) are read
# by pydantic from env_file AND by compose from .env — one name per fact (D1).
# Container-side paths (/app/data, /repos, /opt/claude-code, docker.sock) are a
# container-layout convention, NOT host values — deliberately not parametrised.
# See docs/operations/REPLICATION.md for the full variable map.
services:
  orchestrator:
    build:
      context: .
      # ORCH-101 (D5): uid/gid/home move as ONE coherent group with the runtime
      # user: and the mount targets below (ORCH-040 invariant).
      args:
        APP_UID: ${ORCH_RUN_UID:-1000}
        APP_GID: ${ORCH_RUN_GID:-1000}
        APP_HOME: ${ORCH_AGENT_HOME_DIR:-/home/slin}
    container_name: orchestrator
    restart: unless-stopped
    # ORCH-040: бежим под uid:gid хоста (slin=1000:1000), а не root, чтобы
    # артефакты конвейера (worktree + docs) создавались как slin:slin и git на
    # хосте работал без ручного chown. Доступ к docker.sock сохранён через
    # group_add: ["999"] (МИНА 1 — НЕ удалять). См. ADR-001 ORCH-040.
    user: "${ORCH_RUN_UID:-1000}:${ORCH_RUN_GID:-1000}"
    # init: true injects docker-init (tini) as PID 1 so reparented grandchild
    # processes from the claude/node subprocess tree are reaped (no zombies, B-2).
    init: true
    network_mode: host
    # ORCH-101 (D5): the prod port is configurable on the compose layer (the
    # Dockerfile CMD keeps its exec-form 8500 default — ADR-001 D5); the default
    # resolves byte-for-byte to the previous image CMD. Reuses the existing
    # ORCH_DEPLOY_PROD_TARGET_PORT (no second truth about the prod port).
    command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "${ORCH_DEPLOY_PROD_TARGET_PORT:-8500}"]
    volumes:
      - ./data:/app/data
      - ${ORCH_HOST_REPOS_DIR:-/home/slin/repos}:/repos
      - /var/run/docker.sock:/var/run/docker.sock
      - ${ORCH_HOST_CLAUDE_CODE_DIR:-/usr/lib/node_modules/@anthropic-ai/claude-code}:/opt/claude-code:ro
      - ${ORCH_HOST_NODE_BIN:-/usr/bin/node}:/usr/bin/node:ro
      - ${ORCH_HOST_CLAUDE_DIR:-/home/slin/.claude}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude
      - ${ORCH_HOST_CLAUDE_JSON:-/home/slin/.claude.json}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude.json:ro
      # ORCH-040: target согласован с HOME (launcher: settings.agent_home_dir),
      # не /root/.ssh — обе стороны двигаются одной переменной ORCH_AGENT_HOME_DIR.
      - ${ORCH_HOST_SSH_DIR:-/home/slin/.orchestrator-ssh}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.ssh:ro
    env_file: .env
    environment:
      - ORCH_REPOS_DIR=/repos
      - ORCH_HOST_REPOS_DIR=${ORCH_HOST_REPOS_DIR:-/home/slin/repos}
      # legacy enduro deployer (read via os.environ, keep as-is):
      - DEPLOY_SSH_USER=${ORCH_DEPLOY_SSH_USER:-slin}
      - DEPLOY_SSH_HOST=127.0.0.1
      - DEPLOY_HOOK_SCRIPT=${DEPLOY_HOOK_SCRIPT:-/home/slin/bin/enduro-deploy-hook.sh}
      # ORCH-036 self-deploy (read via pydantic ORCH_ prefix; host-network -> 127.0.0.1, ssh key mounted):
      - ORCH_DEPLOY_SSH_USER=${ORCH_DEPLOY_SSH_USER:-slin}
      - ORCH_DEPLOY_SSH_HOST=127.0.0.1
      - ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh
      - ORCH_DEPLOY_HOST_REPO_PATH=${ORCH_DEPLOY_HOST_REPO_PATH:-/home/slin/repos/orchestrator}
    group_add:
      - "${ORCH_DOCKER_GID:-999}"

  # ORCH-100 (FND/F1b): sidecar-watchdog — the monitoring brain in a SEPARATE
  # container (observer separated from observed, ADR-001 D2). Deploying it builds
  # ONLY this service — the prod `orchestrator` is NOT rebuilt/restarted.
  #   * network_mode: host -> /metrics reachable at http://127.0.0.1:8500/metrics
  #     and host interfaces visible for memory/disk reads.
  #   * docker.sock mounted :ro AND the code is GET-only (double read-only guard).
  #   * host disk paths bind-mounted :ro so shutil.disk_usage sees the host FS but
  #     can never write (opt-in disk ceiling, D6).
  #   * mem_limit caps the thin stdlib daemon (D2): OOM = early "sidecar grew" signal.
  #   * WATCHDOG_ENABLED=false (or simply not starting the service) -> inert.
  orchestrator-watchdog:
    build:
      context: .
      dockerfile: watchdog/Dockerfile
    container_name: orchestrator-watchdog
    restart: unless-stopped
    init: true
    network_mode: host
    # ORCH-111 (adr-0041 D6): share the host PID-namespace so the sidecar's /proc
    # reflects the host and the proc_blocking collector can see orphaned pytest
    # subprocesses. Privilege is read-only and ONLY on the observer; the signal
    # is default-off (WATCHDOG_PROC_ENABLED=false) -> no behaviour change unless
    # opted in. NOT a volume, so the host-paths-read-only compose test is unaffected.
    pid: host
    mem_limit: 128m
    mem_reservation: 32m
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${ORCH_HOST_REPOS_DIR:-/home/slin/repos}:/repos:ro
      - ./data:/app/data:ro
    # Optional env_file (required: false): a missing .env.watchdog must NOT fail
    # `docker compose up` for the prod orchestrator (self-hosting safety). Absent
    # file -> WATCHDOG_* defaults, no token -> fail-safe (logs, does not send).
    env_file:
      - path: .env.watchdog
        required: false
    group_add:
      - "${ORCH_DOCKER_GID:-999}"

  # ORCH-31: staging instance (port 8501, isolated DB).
  # Starts ONLY with: docker compose --profile staging up -d orchestrator-staging
  # Normal "docker compose up -d" does NOT start this service.
  orchestrator-staging:
    profiles:
      - staging
    build:
      context: .
      args:
        APP_UID: ${ORCH_RUN_UID:-1000}
        APP_GID: ${ORCH_RUN_GID:-1000}
        APP_HOME: ${ORCH_AGENT_HOME_DIR:-/home/slin}
    container_name: orchestrator-staging
    restart: unless-stopped
    # ORCH-040: тот же uid хоста, что и у prod (см. комментарий выше / ADR-001).
    user: "${ORCH_RUN_UID:-1000}:${ORCH_RUN_GID:-1000}"
    init: true
    network_mode: host
    # ORCH-101 (D4): the same ORCH_STAGING_PORT that settings.staging_port reads —
    # the image_freshness rebuild target and the listening port can never drift.
    command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "${ORCH_STAGING_PORT:-8501}"]
    volumes:
      - ./data/staging:/app/data
      - ${ORCH_HOST_REPOS_DIR:-/home/slin/repos}:/repos
      - /var/run/docker.sock:/var/run/docker.sock
      - ${ORCH_HOST_CLAUDE_CODE_DIR:-/usr/lib/node_modules/@anthropic-ai/claude-code}:/opt/claude-code:ro
      - ${ORCH_HOST_NODE_BIN:-/usr/bin/node}:/usr/bin/node:ro
      - ${ORCH_HOST_CLAUDE_DIR:-/home/slin/.claude}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude
      - ${ORCH_HOST_CLAUDE_JSON:-/home/slin/.claude.json}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude.json:ro
      # ORCH-040: target согласован с HOME (settings.agent_home_dir), не /root/.ssh.
      - ${ORCH_HOST_SSH_DIR:-/home/slin/.orchestrator-ssh}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.ssh:ro
    env_file: .env.staging
    environment:
      - ORCH_REPOS_DIR=/repos
      - ORCH_HOST_REPOS_DIR=${ORCH_HOST_REPOS_DIR:-/home/slin/repos}
      - DEPLOY_SSH_USER=${ORCH_DEPLOY_SSH_USER:-slin}
      - DEPLOY_SSH_HOST=127.0.0.1
      - DEPLOY_HOOK_SCRIPT=${DEPLOY_HOOK_SCRIPT:-/home/slin/bin/enduro-deploy-hook.sh}
      # Staging DB is isolated via ./data/staging volume mount.
      # Inside the container the path remains /app/data/orchestrator.db (same default),
      # but on the host it physically lives at ./data/staging/orchestrator.db —
      # completely separate from prod ./data/orchestrator.db.
      - ORCH_DB_PATH=/app/data/orchestrator.db
    group_add:
      - "${ORCH_DOCKER_GID:-999}"
