Imports:
  - Types:
      - Config
      - load_config
    Usages:
      - configuration
    From: goga/config
  - Types:
      - build AS run_build
    Usages:
      - build-usage
    From: goga/build
  - Types:
      - resolve_credential_mounts
    Usages:
      - resolve-credential-mounts
    From: goga/agents
  - Types:
      - resolve_runtime_dir
    Usages:
      - runtime-paths
    From: goga/runtime
  - Types:
      - docker_update
      - docker_build_if_not_exist
      - DockerRunner
    Usages:
      - docker-builder
      - docker-runner
    From: goga/docker

Usages:
  conventions: .goga/usages/conventions.md
  click: .goga/usages/cooks/click.md
  docker-auth-mounts: .goga/usages/cooks/docker-auth-mounts.md

Annotations: |
  The `conventions` practice is used for:
  - working with the codebase
  - organizing the REPL development cycle
  - debugging and testing
  - organizing test infrastructure
  - understanding general development and testing principles/rules in the project
  Use the `click` practice to create the command.
  Use `load_config` and the `configuration` practice to read .goga/config.yml.
  Use the `build-usage` practice for the in-container build invocation contract.
  Use `resolve_credential_mounts` and the `resolve-credential-mounts`
  practice to obtain the list of credential files to bind-mount read-only into
  the container.
  Use the `docker-auth-mounts` practice for the read-only credential-mount rule
  applied when assembling the docker run command.
  Use `resolve_runtime_dir` and the `runtime-paths` practice to
  compute the host-side runtime directory path for the current project and git
  branch.
  Use `docker_update` and the `docker-builder` practice to refresh the image when
  --update is set: build when a project Dockerfile is declared (config.dockerfile
  set, fatal on failure), else pull (warning, non-fatal).
  Use `docker_build_if_not_exist` and the `docker-builder` practice for the
  first-run safety net — runs unconditionally before launch: builds the local
  image when it is absent AND config.dockerfile is declared (fatal on failure),
  no-op otherwise. This closes the corner case where dockerfile is set but the
  image was never built and --update is not passed (a bare docker run would
  fail with "No such image").
  Use `DockerRunner` and the `docker-runner` practice to assemble the docker run
  command and manage the container lifecycle (SIGTERM/SIGINT handler, guaranteed
  docker kill, signal-handler restore). The runner is a thin executor — this
  command resolves the per-command inputs (mounts, hosts, env-file, runtime-dir)
  and passes them as params; host-side cleanup (env-file, .ralphex) stays in this
  command's own finally.

  The command runs goga.build inside a Docker container.

  This command is the host-side launcher for `run_build`: it
  assembles a docker command that invokes "python -m goga.build <plan>"
  inside the container, where `run_build` runs as the in-container entrypoint
  (per the `build-usage` practice). The host does not call `run_build` directly.

  The Docker image is sourced from Config.image — the top-level field shared
  by build and pipeline. See the `configuration` practice for the schema.

  Ralphex runtime isolation: the host bind-mounts a centralized runtime
  directory at /workspace/.ralphex inside the container (a nested bind-mount
  layered on top of the project directory mount at /workspace). The physical
  bytes live under ~/.goga/runtime/builds/<normalized_project>/<branch>/ on the
  host, never in the user's project directory. ralphex's own .ralphex/
  auto-detection in cwd finds this mount without changes. The directory survives
  across runs of the same project on the same branch by default; pass --clean to
  wipe it before launch. Use `resolve_runtime_dir` to compute the
  host directory; the in-container `run_build` is unaware of the host path.

---

"build(plan: str, proxy: str | None, add_host: tuple[str, ...], update: bool, clean: bool)":
  location: build.py
  annotations: |
    CLI wrapper for the build command. Runs python -m goga.build inside a
    Docker container.

    `plan`: path to the plan file
    `proxy`: optional HTTP/HTTPS proxy URL from the --proxy click option.
             When None, falls back to config.build.proxy. The resolved value
             (CLI wins over config) drives HTTP_PROXY/HTTPS_PROXY/NO_PROXY in
             the container env-file.
    `add_host`: raw "HOST:IP" strings collected from the repeatable --add-host
                click option (empty tuple when absent). Merged on top of
                config.build.hosts; on key conflict, CLI wins. Each entry
                becomes a docker run --add-host HOST:IP flag.
    `update`: flag from the --update/-u click option. When True, refresh the
              image via `docker_update` before launching the container — build
              when a project Dockerfile is declared (config.dockerfile set),
              else pull. When False (default), skip the refresh entirely and use
              the local image. Independent of `docker_build_if_not_exist` (the
              first-run safety net runs unconditionally before this branch).
    `clean`: flag from the --clean click option. When True, wipe and recreate
             the persistent ralphex runtime host directory via
             `clean_build_runtime_dir` BEFORE docker run. When False (default),
             keep the existing directory as-is so ralphex progress files
             survive across runs of the same project on the same branch.

    CLI options (via click):
    - --dry-run, --worktree, --skip-finalize, --skip-manifest-check
    - --session-timeout, --idle-timeout, --wait
    - --max-iterations, --review-patience
    - -e / --env KEY=VALUE (multiple) — pass environment variables to the container
    - --proxy URL (str) — HTTP/HTTPS proxy URL; overrides config.build.proxy
    - --add-host HOST:IP (multiple) — add a docker run --add-host entry; merges on top of config.build.hosts
    - --clean (flag) — wipe the persistent ralphex runtime host directory before launch
    - --update / -u (flag) — pull the image before launching the container

    Apply the `click` practice for the command surface and option declarations.
    Apply the `conventions` practice for docstring style and intra-package imports.
    Apply the `build-usage` practice for the in-container build invocation contract.
    Apply the `resolve-credential-mounts` practice for the credential-mount
    consumer pattern.
    Apply the `docker-auth-mounts` practice for the read-only mount rule.
    Apply the `docker-builder` practice for the --update build-vs-pull refresh.
    Apply the `docker-runner` practice for the docker run launch and lifecycle.

    Algorithm:
    1. Verify docker availability; raise ClickException when missing
    2. Load configuration via `load_config` (practice `configuration`) — get `Config`
    2b. When config.build is None → raise ClickException(
        "build section is required in .goga/config.yml to run 'goga build'").
        Host-side guard — runs BEFORE any config.build access and BEFORE the
        docker command is assembled, so the in-container goga.build never
        starts on a build-less config (no docker run).
    3. Collect cli_flags from click parameters
    4. Resolve the proxy: take `proxy` when not None, otherwise fall back to
       config.build.proxy
    5. Resolve hosts: merge config.build.hosts with parsed `add_host` entries;
       split each "HOST:IP" string into a (host, ip) pair; CLI entries override
       config entries on host-key conflict
    6. Read git identity and assemble it as git env vars; tolerate absent git
       config and continue without error
    7. Assemble the container env: combine task_executor env, git identity env,
       and CLI extra env (task_executor env takes precedence over git env)
    8. When the resolved proxy is non-None: add HTTP_PROXY, HTTPS_PROXY, and
       NO_PROXY (fixed at localhost,127.0.0.1) to the container env
    9. Verify Config.image is set; raise ClickException when None
    10. Install a SIGTERM/SIGINT handler (raises SystemExit with code 128 + signum)
        BEFORE writing the secret env-file — the leak-prevention invariant (a
        signal in the window between the env-file write and the `DockerRunner`
        launch, which includes the `docker_update` build, must unwind to the
        finally block and unlink the secret file). The runner's handler later
        nests under this one. Then write the assembled env to a private env-file
        for the container
    11. Resolve the host runtime directory via `resolve_build_runtime_dir`:
        - call `resolve_runtime_dir`("builds") (the routine composes
          ~/.goga/runtime/builds/<normalized_project>/<branch>/ from cwd + git
          branch)
        - ensure the directory exists (idempotent mkdir with parents)
    12. When `clean` is True: wipe and recreate the host runtime directory via
        `clean_build_runtime_dir` BEFORE docker run. When `clean` is False: keep
        the existing directory as-is (ralphex progress files survive across runs)
    13. Generate a unique container_name
    14. Assemble the docker-run inputs to hand to `DockerRunner`:
        - args: the post-image command — -m goga.build <plan> + cli_flags
        - params: the pre-refactor docker-run options, translated to flags by the
          shared rule in `docker-runner` — name=<container_name>, rm=True,
          entrypoint=python3, workdir=/workspace, v=[CWD:/workspace, the resolved
          host runtime dir (step 11) read-write at /workspace/.ralphex, and each
          credential mount read-only], add_host=<each resolved HOST:IP>,
          env_file=<env-file path>
        - The assembled form preserves the pre-refactor launcher exactly (same
          options, same effect): --rm, --name, --entrypoint, mounts via -v
          (project at /workspace; the resolved host runtime dir at
          /workspace/.ralphex as a nested bind-mount, so ralphex auto-detects
          .ralphex/ in cwd and the bytes land in the host runtime dir, never in
          the project dir; each credential mount from `resolve_credential_mounts`
          read-only), --add-host, --env-file, image, args
    15. On dry_run: print the assembled command and exit 0 without launching
    16. Call `docker_build_if_not_exist` with config.image and config.dockerfile
        — the first-run safety net, runs UNCONDITIONALLY at launch entry. No-op
        when the image already exists or when no Dockerfile is set. When the
        image is absent and a Dockerfile is declared: build it (fatal on failure
        — surfaces as a ClickException via the D5 mapper, exit 1, launch
        skipped). Runs inside the try so the D7 leak-prevention invariant covers
        this window: the secret env-file is already written (step 10), and a
        fatal build unwinds to the finally which unlinks it
    17. When `update` is True: call `docker_update` with config.image and
        config.dockerfile — build when a project Dockerfile is declared (fatal on
        failure), else pull (warning, non-fatal). When `update` is False: skip the
        refresh.
    18. Launch the container via `DockerRunner`(config.image).run(args, params):
        the runner installs its own SIGTERM/SIGINT handler (exit 128 + signum),
        which nests under the caller handler installed in step 10 (saves and
        restores it), runs docker run, streams stdout/stderr, and returns the
        exit code
    19. In finally: delete the env-file and remove ``.ralphex/`` from the project
        directory via `_cleanup_ralphex_in_project` to enforce the contract
        invariant that no ``.ralphex/`` appears in the project directory under any
        exit path, and restore the caller-installed SIGTERM/SIGINT handler to the
        original. The docker kill and the runner's signal-handler restore happen
        inside the runner finally (which runs before this finally); the runner's
        restore returns to the caller handler, then this finally restores the
        original

    Requirements:
    - Docker must be installed and available in PATH
    - Image source: Config.image (top-level)
    - Git identity flows into the container as GIT_AUTHOR_NAME/EMAIL,
      GIT_COMMITTER_NAME/EMAIL; absent git config does not block the build
    - When `proxy` is non-None (CLI or config), HTTP_PROXY/HTTPS_PROXY/NO_PROXY
      are written to the env-file; NO_PROXY is fixed at "localhost,127.0.0.1"
    - CLI --add-host entries are merged on top of config.build.hosts;
      on host-key conflict, the CLI entry wins
    - Credential mounts come from `resolve_credential_mounts` — every returned
      tuple is an existing file, no re-check needed
    - Image refresh runs only when `update` is True via `docker_update`: build
      when config.dockerfile is set (fatal on failure), else pull (warning,
      non-fatal)
    - First-run safety net runs UNCONDITIONALLY at launch entry via
      `docker_build_if_not_exist`: when config.image is absent locally AND
      config.dockerfile is declared, build it (fatal on failure — surfaces as
      ClickException, exit 1, launch skipped); no-op when the image is present
      or no Dockerfile is declared. This closes the corner case where dockerfile
      is set but the image was never built and --update is not passed — without
      this net a bare docker run would fail with "No such image"
    - The host runtime directory under
      ~/.goga/runtime/builds/<normalized_project>/<branch>/ MUST exist before
      docker run — Docker may refuse to bind-mount a non-existent host path
    - The directory at /workspace/.ralphex inside the container is the
      nested bind-mount target; ralphex writes there via its cwd-relative
      .ralphex/ literal and auto-detection, no config change required
    - The host runtime directory survives across runs of the same project
      on the same branch by default; ralphex progress files persist for resuming
      interrupted builds
    - After container exit, remove ``.ralphex/`` from the project directory
      via `_cleanup_ralphex_in_project` to enforce the contract invariant that
      no ``.ralphex/`` appears in the project directory under any exit path,
      including crash/SIGKILL. ``.ralphex/`` is never legitimate user data —
      removal is unconditional when the directory exists
    - The caller installs its SIGTERM/SIGINT handler BEFORE writing the secret
      env-file; the runner's handler nests under it (leak prevention — a signal
      in the setup window, including the `docker_update` build, must unwind to
      the finally block and unlink the env-file)

    Constraints:
    - Do not refresh an EXISTING image by default — refresh only when `update`
      is True. The unconditional `docker_build_if_not_exist` at launch entry is
      NOT a refresh: it builds only when the image is absent AND a Dockerfile is
      declared (first-run build, not a rebuild of an existing image)
    - Do not write --add-host for hosts absent in both config and CLI
    - Do not validate the "HOST:IP" format of --add-host entries beyond a
      single-colon split — Docker itself reports malformed entries
    - Do not expose a separate --no-proxy CLI option; NO_PROXY is fixed at
      "localhost,127.0.0.1" whenever proxy is set
    - Do not auto-add --add-host entries to NO_PROXY
    - Do not filter credential mounts by the configured task_executor agent —
      detection is agent-agnostic (see `resolve-credential-mounts` practice)
    - Do not mount anything under /workspace other than:
      (a) the project directory at /workspace, and
      (b) the resolved host runtime directory at /workspace/.ralphex
    - Wipe the host runtime directory only when `clean` is True — the default
      preserves ralphex state across runs
    - --clean wipes the host runtime directory BEFORE launch; the directory
      survives across runs (no wipe in finally under any exit path)
    - Do not pass the host runtime path into the container via env-file
      or CLI args — the container sees only /workspace/.ralphex and must not
      know about ~/.goga/runtime/builds/...
    - The in-container `run_build` keeps using cwd-relative .ralphex/; the
      nested mount resolves that into the host runtime directory transparently

"resolve_build_runtime_dir() -> runtime_dir: Path":
  location: build.py
  annotations: |
    Compute the host-side path of the persistent ralphex runtime directory for
    the current project on the current git branch. This directory is bind-mounted
    into the build container at /workspace/.ralphex so ralphex state never
    touches the user's project directory.

    `runtime_dir`: absolute host path under
                   ~/.goga/runtime/builds/<normalized_project>/<branch>/

    Apply the `conventions` practice for docstring style and intra-package imports.

    Algorithm:
    1. Delegate to `resolve_runtime_dir`("builds") — no suffix
       parts (build has no further namespace under <branch>/)
    2. Return the resulting path

    Requirements:
    - Return value is an absolute path
    - Pure with respect to the filesystem — does not create the directory
      (creation is the caller's responsibility in `build` Algorithm step 11)

    Constraints:
    - Do not create the directory here — the caller handles creation
    - Do not add a suffix — build has no further namespace under <branch>/

"clean_build_runtime_dir(host_dir: Path) -> none: None":
  location: build.py
  annotations: |
    Recursively wipe the persistent ralphex runtime directory and recreate it
    empty. Called before container launch when the --clean flag is set.

    `host_dir`: host path computed by `resolve_build_runtime_dir`

    Apply the `conventions` practice for docstring style and intra-package imports.

    Algorithm:
    1. When the directory exists, remove it recursively
    2. Recreate the directory (empty), including any missing parents

    Requirements:
    - Idempotent — repeated calls on an already-clean directory do not raise
    - The caller guarantees `host_dir` was computed by
      `resolve_build_runtime_dir`

    Constraints:
    - Do not raise when the directory does not exist — the mkdir in step 2
      creates it
    - Do not selectively preserve any files — the wipe is total

"_cleanup_ralphex_in_project(project_dir: Path) -> none: None":
  location: build.py
  annotations: |
    Remove ``.ralphex/`` from the project directory if present.

    The `build` constraint forbids ``.ralphex/`` in the project directory
    under any exit path; ``.ralphex/`` is never legitimate user data.

    `project_dir`: host project directory potentially containing ``.ralphex/``

    Apply the `conventions` practice for docstring style and intra-package imports.

    Algorithm:
    1. Remove ``project_dir``/.ralphex recursively if it exists; tolerate a
       concurrent removal race so a vanishing directory does not raise

    Requirements:
    - Idempotent — no-op when the directory does not exist
    - Tolerate a concurrent removal race; any other filesystem error propagates
      so unexpected state surfaces

    Constraints:
    - Do not check whether the directory is empty — removal is unconditional
    - Do not touch any path outside ``project_dir``/.ralphex

---

Author: Goga
CreatedAt: 20/05/26

Description: |
  CLI wrapper for the goga build command. Host-side launcher that runs
  python -m goga.build inside a Docker container. Bind-mounts a centralized
  ralphex runtime host directory at /workspace/.ralphex so ralphex state never
  touches the user's project directory.
