Imports:
  - Types:
      - ProjectConfig
      - load_project_config
      - HomeConfig
      - load_home_config
    Usages:
      - project-configuration
      - home-configuration
    From: goga/config
  - Types:
      - ConfigHooks
      - ConfigOverlay
    Usages:
      - checkpoints
    From: goga/config/hooks
  - Types:
      - build AS run_build
    Usages:
      - build-usage
      - registering-hooks
    From: goga/build
  - Types:
      - resolve_runtime_dir
    Usages:
      - runtime-paths
    From: goga/runtime
  - Types:
      - docker_update
      - docker_build_if_not_exist
      - DockerRunner
      - encode_extra_env
    Usages:
      - docker-builder
      - docker-runner
      - docker-image-version
      - extra-env-carriage
    From: goga/docker

Usages:
  conventions: .goga/usages/conventions.md
  click: .goga/usages/cooks/click.md

Annotations: |
  The `conventions` practice is used for:
  - Working with the codebase
  - Organizing the REPL development cycle
  - Debugging and testing
  - Organizing the test infrastructure
  - Understanding the general principles and rules of development and
    testing in the project
  Use the `click` practice to create the command.

  Use the `checkpoints` practice for the config amendment checkpoint at
  the project-configuration load moment — the command loads the authored
  configuration, delivers the checkpoint, consumes the effective
  configuration of the returned `ConfigOverlay`, and prints the amendment
  summary lines to stderr.

  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 the `registering-hooks` practice for the tool-package hook subscription
  surface of the build domain actions behind the checkpoint surface the run
  delivers.
  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. Every launch through `DockerRunner` is gated by the
  host–image version check; the `docker-image-version` practice covers the
  image-side version probe.

  The command surfaces the review-phase tri-state pair to the user and
  forwards it verbatim — the tri-state resolves in-container. The flag
  surface carries no --worktree and no --skip-finalize; --review-patience
  addresses build.review.additional.patience
  and --base-ref addresses build.review.base_ref (forwarding only —
  precedence resolves in-container); skip_manifest_check stays a CLI-only
  pre-check toggle. The task env (build.env) is not written into the
  container env-file — it is forwarded for the tasks-pass env layer
  in-container; the env-file carries the base layers (home.env, git
  identity, CLI -e, proxy).

  Guards split by nature: the structural build-section guard stays
  host-side on the host-effective configuration — a build-less config fails
  before any docker activity; the agent value guard lives in the
  in-container build domain, where an amendment can supply the agent.

  The CLI environment entries travel twice from one source (per
  `extra-env-carriage`): the raw KEY=VALUE lines in the env-file, and the
  same entries encoded as the dedicated engine payload variable, so the
  in-container build applies them above its task env layers at the pass
  launches. The task env (build.env) stays out of the env-file.

  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(ctx: click.Context, plan: str, dry_run: bool, skip_manifest_check: bool, session_timeout: str | None, idle_timeout: str | None, wait: str | None, max_iterations: int | None, review_patience: int | None, base_ref: str | None, extra_env: tuple[str, ...], proxy: str | None, add_host: tuple[str, ...], update: bool, clean: bool, skip_review: bool | None)":
  location: build.py
  annotations: |
    CLI wrapper for the build command. Runs python -m goga.build inside a
    Docker container.

    `ctx`: Click execution context, used to propagate exit codes (per the
           `click` practice)
    `plan`: path to the plan file
    `dry_run`: flag from the --dry-run click option — print the assembled
              docker command and exit 0 without launching the container
    `skip_manifest_check`: flag from the --skip-manifest-check click option —
              the CLI-only pre-check toggle, forwarded in-container
    `session_timeout`: optional value from the --session-timeout click
              option, forwarded in-container
    `idle_timeout`: optional value from the --idle-timeout click option,
              forwarded in-container
    `wait`: optional value from the --wait click option, forwarded
              in-container
    `max_iterations`: optional int from the --max-iterations click option,
              forwarded in-container
    `review_patience`: optional int from the --review-patience click option —
              addresses build.review.additional.patience; forwarding only,
              precedence resolves in-container
    `base_ref`: optional value from the --base-ref click option — review
              diff base (branch name or commit hash); addresses
              build.review.base_ref; forwarding only, precedence resolves
              in-container
    `extra_env`: raw KEY=VALUE strings from the repeatable -e/--env click
              option, forwarded into the container env-file verbatim AND
              encoded into the CLI entries payload variable — one source,
              the same parsed values in both places (per
              `extra-env-carriage`)
    `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.
    `skip_review`: tri-state from the --skip-review / --no-skip-review click pair —
                   True / False / None (flag not given). Forwarded in-container as
                   cli_flags; combined in-container with build.review.skip
                   (CLI wins).

    CLI options (via click):
    - --dry-run, --skip-manifest-check
    - --session-timeout, --idle-timeout, --wait
    - --max-iterations, --review-patience (addresses
      build.review.additional.patience)
    - --base-ref REF (str) — review diff base (branch name or commit hash);
      addresses build.review.base_ref; forwarded to the container
      and applied to the review pass only (per the `build-usage`
      practice)
    - --skip-review / --no-skip-review (bool pair) — skip the review phase;
      tri-state, overrides build.review.skip
    - -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 `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 13/17);
    home.docker.build is forwarded to image build (steps 15/16, build branch).
    1. Verify docker availability; raise ClickException when missing
    2. Load configuration via `load_project_config` (practice `project-configuration`) — get `ProjectConfig`;
       deliver the config amendment checkpoint per the `checkpoints`
       practice right after the authored load via the `ConfigHooks`
       checkpoint surface — amend_config(config=...) — the config.build
       is None guard and every downstream access to host-owned launch
       fields address the effective configuration of the `ConfigOverlay`.
       Amendments into run-parameter fields (build.env, build.agent) stay
       applied-but-unconsumed — the host consumes them nowhere, silently,
       no warning. A load or
       checkpoint failure — ValueError covers the hard action,
       ImportError a broken tool package facade — wraps into the
       same user-facing ClickException, never a raw traceback. The home
       configuration load stays authored-only
    2.1. When config.build is None → raise ClickException(
        "build section is required in .goga/config.yml to run 'goga build'").
        Host-side structural guard — runs BEFORE any config.build access and
        BEFORE the docker command is assembled, on the host-effective
        configuration, so the in-container goga.build never starts on a
        build-less config (no docker run). The agent value guard lives
        in-container: the effective agent may arrive as a container-side
        amendment, so a host check on the authored value would reject a run
        an amendment could have satisfied
    3. Collect cli_flags from click parameters; forward the review pair:
       skip_review True → --skip-review; False → --no-skip-review; None →
       neither flag (the tri-state survives to the container); forward
       --base-ref and --review-patience by the existing value-option
       pattern — the flag with its value joins cli_flags only when the
       option is set; unset (None) adds no flag, leaving the decision to
       the container config
    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, then the raw CLI extra env KEY=VALUE
       strings (forwarded as-is, no validation — CLI overrides home.env on
       key conflict), then the engine variables — when the resolved proxy is
       non-None: HTTP_PROXY, HTTPS_PROXY, and
       NO_PROXY (fixed at localhost,127.0.0.1) to the container env,
       skipping a key the CLI explicitly supplied — then the CLI entries
       payload line: GOGA_EXTRA_ENV set to the value of `encode_extra_env`
       over the same parsed CLI strings (one source, per
       `extra-env-carriage`);
       the task env (build.env) is NOT written into the env-file — it is
       applied in-container at the pass launches
    8. Verify ProjectConfig.image is set; raise ClickException when None
    9. 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
    10. 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)
    11. 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)
    12. Generate a unique container_name
    13. Assemble the docker-run inputs to hand to `DockerRunner`:
        - args: the post-image command — -m goga.build <plan> + cli_flags
        - params: the 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 10) read-write at /workspace/.ralphex], 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 launcher behavior 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), --add-host, --env-file, image, args
    14. On dry_run: print the assembled command and exit 0 without launching
    15. 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, exit 1, launch
        skipped). Runs inside the try so the leak-prevention invariant covers
        this window: the secret env-file is already written (step 9), and a
        fatal build unwinds to the finally which unlinks it
    16. 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.
    17. 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 9 (saves and
        restores it), runs docker run, streams stdout/stderr, and returns the
        exit code
    18. 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)
    - The amendment summary lines of the `ConfigOverlay` print to stderr
      (nothing when empty)
    - home.env is the lowest-priority env layer (applied in the env-file only);
      CLI extra env overrides 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.
      The env-file carries the base layers (home.env, git identity, CLI -e,
      proxy) only — build.env reaches the container solely as the
      in-container tasks-pass env layer
    - 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
    - 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)
    - The --skip-review/--no-skip-review pair is forwarded verbatim; the host does
      NOT resolve the tri-state against config — resolution belongs to the
      in-container build
    - No worktree handling anywhere on the surface — no --worktree flag, no
      worktree guard, no worktree-related rejection
    - The host does NOT resolve --base-ref precedence against config —
      forwarding only; the unset case resolves in-container (CLI >
      build.review.base_ref > omit)

    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 CLI env — home.env is the
      lowest-priority (base) layer; CLI extra env wins on key conflict
      (build.env is not part of the env-file at all)
    - Do not guard the agent value on the host — the effective agent may
      arrive as a container-side amendment; the guard lives in-container
    - Do not encode the CLI entries from any source other than the same
      parsed values written into the env-file
    - 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 mount anything under /workspace other than:
      1. the project directory at /workspace, and
      2. 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 10)

    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.
