Imports:
  - Types:
      - ProjectConfig
      - load_project_config
      - HomeConfig
      - load_home_config
    Usages:
      - project-configuration
      - home-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_project_config` and the `project-configuration` practice to read
  .goga/config.yml. Use `load_home_config` and the `home-configuration` practice
  for the optional machine-wide home config layer (env base-layering, docker.run
  tokens for every docker run, docker.build tokens forwarded to image build).
  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 ProjectConfig.image — the top-level field shared
  by build and pipeline. See the `project-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
    - -c / --clean (flag) — wipe the persistent ralph-loop runtime host directory before launch; short alias of --clean
    - --update / -u (flag) — pull the image before launching the container

    Apply the `click` practice for the command surface and option declarations.
    Apply the `conventions` CLI command docstring rule for the command's --help
    text (rendered verbatim by Click; omit Args/Returns/Raises) 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.
    Apply the `home-configuration` practice for loading the optional machine-wide
    home config (home.env base-layering in the env-file; home.docker.run appended
    to every docker run; home.docker.build forwarded to image build).

    Algorithm:
    Load the home configuration via `load_home_config` (per the
    `home-configuration` practice) — an empty `HomeConfig` when the home file
    is absent (no-op). home.env is the lowest-priority env layer (step 7);
    home.docker.run is appended to every docker run (step 14/18);
    home.docker.build is forwarded to image build (steps 16/17, build branch).
    1. Verify docker availability; raise ClickException when missing
    2. Load configuration via `load_project_config` (practice `project-configuration`) — get `ProjectConfig`
    2.1. 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).
    2.2. When config.build.task_executor.agent is None → raise ClickException(
        "build.task_executor.agent is required in .goga/config.yml to run 'goga build'").
        The agent is optional at the loader level (None when absent/empty), but the
        build command resolves it into the in-container wrapper path and cannot run
        without it. Host-side guard — runs BEFORE any agent access to avoid a
        downstream TypeError.
    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 layering home.env as the BASE (lowest-priority)
       layer, then git identity env, task_executor env, and CLI extra env
       (home.env < git identity < task_executor env < CLI extra env on key
       conflict — project config and CLI override home.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 ProjectConfig.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>
        - extra_args: home.docker.run — raw docker tokens passed as a SEPARATE
          keyword argument to DockerRunner.run (NOT part of params: they are
          appended verbatim AFTER the translated params flags and BEFORE the
          image, and are NEVER translated to an --extra-args flag). Empty list
          when the home file is absent
        - 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, config.dockerfile,
        and extra_args=home.docker.build — the first-run safety net, runs
        UNCONDITIONALLY at launch entry. No-op when the image already exists or
        when no Dockerfile is set (extra_args forwarded in the build branch only).
        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,
        config.dockerfile, and extra_args=home.docker.build — build when a
        project Dockerfile is declared (fatal on failure; the build branch
        forwards extra_args), else pull (warning, non-fatal; extra_args ignored).
        When `update` is False: skip the refresh.
    18. Launch the container via `DockerRunner`(config.image).run(args, extra_args=home.docker.run, **params):
        extra_args is a SEPARATE keyword (NOT inside params, which is
        unpacked via **); 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: ProjectConfig.image (top-level)
    - home.env is the lowest-priority env layer (applied in the env-file only);
      project config (task_executor env) and CLI extra env override it on key
      conflict. home.docker.run tokens are appended to every docker run;
      home.docker.build tokens are forwarded to image build
      (docker_build_if_not_exist / docker_update build branch). An absent home
      file yields an empty HomeConfig — no effect.
    - 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)
    - Expose -c as a short alias of --clean (equivalent behavior)

    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 pass home.env to docker build — home.env is a docker run container-env
      layer only (no --build-arg). The build branch of `docker_build_if_not_exist`
      / `docker_update` forwards only home.docker.build, never home.env
    - Do NOT let home.env override project config or CLI env — home.env is the
      lowest-priority (base) layer; task_executor env and CLI extra env win on
      key conflict
    - 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.
