Imports:
  - Types:
      - ensure_topic
    Usages:
      - ensuring
    From: goga/topics
  - Types:
      - ProjectConfig
      - load_project_config
      - HomeConfig
      - load_home_config
    Usages:
      - project-configuration
      - home-configuration
    From: goga/config
  - Types:
      - resolve_wrapper_path
      - resolve_credential_mounts
    Usages:
      - resolve-wrapper-path
      - resolve-credential-mounts
    From: goga/agents
  - Types:
      - resolve_runtime_dir
    Usages:
      - runtime-paths
    From: goga/runtime
  - Usages:
      - list-pipelines
      - run-pipeline
      - pipeline-cli
      - describe-pipelines
      - describe-pipeline
    From: goga/pipeline
  - Types:
      - docker_update
      - docker_build_if_not_exist
      - DockerRunner
    Usages:
      - docker-builder
      - docker-runner
      - docker-image-version
    From: goga/docker

Usages:
  convention: .goga/usages/conventions.md
  click: .goga/usages/cooks/click.md
  afm: .goga/usages/cooks/afm.md
  agent-wrappers: .goga/usages/cooks/agent-as-claude-wrappers.md
  docker-auth-mounts: .goga/usages/cooks/docker-auth-mounts.md
  git: |
    External git binary invoked via subprocess.run (check=True,
    capture_output=True). Set GIT_TERMINAL_PROMPT=0 in the env to suppress
    interactive prompts. Read-only inspection (git identity for the
    container env-file). Mock the subprocess call in tests per `convention`.

Annotations: |
  The `convention` 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 build the single pipeline command: command
  registration, the optional positional argument, the pass_context decorator,
  echo, and exit-code propagation.

  Use the `afm` practice for the in-container binary contract (CLI shape,
  PATH-resolved invocation, exit codes).

  Use the `agent-wrappers` practice for the in-container wrapper naming
  convention referenced when generating the afm client.command config.

  Use the `pipeline-cli` practice for the in-container subcommand surface
  (the listing and run subcommands with the info modifiers), the
  `list-pipelines` and `describe-pipelines` practices for the listing and
  overview output contracts, and the `run-pipeline` and `describe-pipeline`
  practices for the run and card semantics. These practices describe how the
  container-side command behaves — this cell invokes that contract through
  docker without importing any Types from goga/pipeline.

  This cell is the host-side launcher only. Every form assembles a docker
  command and invokes python -m goga.pipeline inside the goga Docker image.
  The runtime boundary to goga/pipeline is docker, not Python Imports; the
  host never reads pipeline files directly. Two launch shapes exist: the run
  launcher (full shape — allocated port, env-file, afm-config tmpfile,
  persistent afm state, credential mounts, signal handling) and the info
  launcher (minimal read-only shape — none of those).

  The command surface is five explicit forms:
  - "goga pipeline --list" (or -l) — the flat list of pipeline names
  - "goga pipeline --list --info" (or -l -i) — the overview with descriptions
  - "goga pipeline NAME --info" (or -i) — the card of one pipeline
  - "goga pipeline NAME" — the run
  - an invocation with neither a name nor --list — an error before any
    docker activity

  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.

  Use `resolve_credential_mounts` and the `resolve-credential-mounts`
  practice to obtain the credential files bind-mounted read-only into the
  container; use the `docker-auth-mounts` practice for the read-only mount
  rule. Use `resolve_runtime_dir` and the `runtime-paths` practice to compute
  the host-side runtime directory. Use `docker_update` and the
  `docker-builder` practice to refresh the image when requested. Use
  `docker_build_if_not_exist` and the `docker-builder` practice for the
  first-run safety net. Use `DockerRunner` and the `docker-runner` practice
  to assemble the docker run command and manage the container lifecycle.
  Every launch through `DockerRunner` is gated by the host–image version
  check; the `docker-image-version` practice covers the image-side version
  probe.

  The optional -t/--topic flag moves the repository onto the requested
  work before the run form launches: the identifier resolves through
  `ensure_topic` — an exact branch name, an exact topic slug, or their
  prefix — and, when nothing hosts it, fresh work is created: the
  branch named as entered, from the current HEAD, and the topic
  directory of the year. The switch or the creation completes before
  any docker activity. The optional --todo flag (no short form — -t
  stays with --topic) requests the todo entry of the work: after the
  switch onto hosted work, or after the fast creation, the external
  editor opens with the topic's todo.md — the entry belongs to the
  domain orchestration. Without an interactive terminal the flag is a
  clean error before any git or docker activity. The listing and info
  forms silently skip the whole topic procedure.

  Use the `ensuring` practice for the consumer patterns of the topics
  facade used by the topic procedure — the identifier resolution and the
  switch-or-create orchestration.
  Use the `git` practice for the git identity read into the container
  env-file.
  Use the `click` practice for the -t/--topic option: a long form and a
  short alias sharing a single Option, and exit-code propagation.

---

"pipeline(ctx: click.Context, name: str | None, list_requested: bool, info: bool, topic: str | None, todo: bool, extra_env: tuple[str, ...], proxy: str | None, add_host: tuple[str, ...], clean: bool, update: bool, workflow: str | None, no_workflow: bool, skip: tuple[str, ...], parallel: int | None)":
  location: pipeline.py
  annotations: |
    Single CLI command "goga pipeline" with five explicit forms. Every form
    launches the goga Docker container and invokes the in-container
    entrypoint inside it; the host never reads pipeline files directly. The
    optional -t/--topic flag moves the repository onto the requested work
    before the run form starts — creating it when nothing hosts the
    identifier.

    `ctx`: Click execution context, used to propagate exit codes (per the
           `click` practice)
    `name`: optional positional argument — pipeline name without extension.
            With `info` → the card form; without → the run form.
    `list_requested`: flag from the --list/-l click option — requests the
            listing forms; combined with `info` selects the overview.
    `info`: flag from the --info/-i click option — a modifier meaning "show
            instead of act": with `list_requested` → the overview, with
            `name` → the card; on its own it selects no form.
    `topic`: optional identifier from the -t/--topic option (a long form
             and a short alias on a single click option) — a branch name, a
             topic slug, or their prefix; the year scope is always the
             current year. Run form only: brings the repository onto the
             requested work via `ensure_topic` before any docker activity —
             switching onto the hosting branch, or creating fresh work (the
             branch named as entered and the topic directory of the year)
             when nothing hosts the identifier. Silently ignored in the
             flat list, overview, and card forms — not an error.
    `todo`: flag from the --todo click option — request the todo entry
            of the requested work; run form only: after the switch or
            the fast creation of the topic procedure, the external
            editor opens with the topic's todo.md inside `ensure_topic`
            — a branch without a topic gets its topic directory created
            first. Without an interactive terminal a clean error
            precedes any git or docker activity. Given without `topic`
            in the run form -> a clean error: the entry needs
            requested work — before any git or docker activity.
            Silently ignored in the flat list, overview, and card
            forms — not an error.
    `extra_env`: raw KEY=VALUE strings from the repeatable -e/--env option,
            forwarded into the container env-file in the run form only.
    `proxy`: optional HTTP/HTTPS proxy URL from the --proxy option; when
            None, falls back to config.pipeline.proxy. Run form only.
    `add_host`: raw "HOST:IP" strings from the repeatable --add-host option;
            merged on top of config.pipeline.hosts in the run form; a
            deliberate no-op in the listing and info forms.
    `clean`: flag from the -c/--clean option — wipes the persistent afm
            state directory before launch. Run form only.
    `update`: flag from the -u/--update option — refreshes the image before
            the listing in the flat-list form and before launch in the run
            form; a no-op in the overview and card forms.
    `workflow`: optional workflow name from the -w/--workflow option
            (without the .yml extension); the file
            <cwd>/.goga/workflows/<workflow>.yml must exist — verified
            before any container launch. Applies to the run and card forms.
            Mutually exclusive with `no_workflow`.
    `no_workflow`: flag from the --no-workflow option — disables workflow
            application. Run and card forms. Mutually exclusive with
            `workflow`.
    `skip`: raw stage names from the repeatable -s/--skip option. Run form
            only; forwarded into the container env-file; names are validated
            in-container only.
    `parallel`: optional int from the -p/--parallel option capping
            concurrently executing stages. Run form only; None means
            unbounded.

    Apply the `click` practice for the command surface: a single command
    (not a group), the optional positional `name` argument, the
    pass_context pattern, and exit-code propagation.
    Apply the `convention` CLI command docstring rule for the --help text
    (rendered verbatim by Click; omit Args/Returns/Raises) and
    intra-package imports.

    Algorithm:
    1. Load the configuration via `load_project_config` — fail fast on
       schema errors; a missing pipeline section is a clean error
    2. Validate the argument form — every check before any docker activity:
       2.1. `list_requested` AND `name` provided → mutually-exclusive error
            (clean message, exit 1)
       2.2. `name` absent AND `list_requested` absent → the error
            'Missing pipeline name. Use "goga pipeline --list" to list
            available pipelines, or provide a pipeline name.' to stderr,
            exit 1, nothing to stdout, no topic procedure, no image
            refresh and no first-run build
       2.3. `workflow` provided AND `no_workflow` set → mutually-exclusive
            error (exit 1)
       2.4. `workflow` provided → reject a name whose resolved path escapes
            <cwd>/.goga/workflows/ (a ".." segment or an absolute prefix)
            as a clean "invalid workflow name" error (exit 1) — never
            resolve into the wider filesystem; then verify
            <cwd>/.goga/workflows/<workflow>.yml exists; a missing file is
            a clean error (exit 1)
    3. Topic procedure (run form only — `name` given, `list_requested`
       and `info` unset): `todo` without `topic` is a clean error
       before any git or docker activity — the entry needs requested
       work. With
       `topic` given, bring the repository onto the requested
       work via `ensure_topic` with the `todo` flag — a switch onto the
       hosting branch when one hosts the identifier, or the fast
       creation when nothing does: the branch named as entered, from
       the current HEAD — the configuration base is never read — and
       the topic directory of the year, ensured on a branch without a
       topic. Under `todo` the editor entry starts only after the
       switch or the creation. Echo the single result line to stdout
       once, immediately after the topic procedure and before the
       step-4 dispatch; the forms that skip the procedure print no
       topic line. Every git action and the entry happen on the host
       before any docker activity. Several candidates without an
       interactive terminal, a dirty working tree on a switch mutation,
       an unusable or occupied name without a terminal, or `todo`
       without an interactive terminal abort the command with a
       non-zero exit before any image refresh, build, or launch. The
       flat list, overview, and card forms skip the procedure silently
       — passing -t or --todo there is not an error and has no effect.
    4. Dispatch by form:
       - flat list — `run_pipeline_info_container` with name=None,
         info=False; `update` applies (image refresh before the listing)
       - overview — `run_pipeline_info_container` with name=None, info=True;
         the refresh is not performed
       - card — `run_pipeline_info_container` with the name, info=True, and
         the workflow flags
       - the three info forms pass hosts resolved from config.pipeline.hosts
         only — CLI --add-host entries are not merged (deliberate no-op,
         per the flag matrix)
       - run — resolve the proxy (CLI over config) and the hosts (CLI over
         config), then `run_pipeline_container` with the full argument set
    5. Propagate the returned exit code via the click context (per the
       `click` practice)

    Requirements:
    - Expose -l/--list and -i/--info as click flags alongside the run
      options; long and short forms behave identically
    - Register -t/--topic with both forms on a single click Option — both
      bind the `topic` parameter identically
    - When the topic procedure ran, echo exactly one stdout line — the
      result line of the topic procedure (a switch, a branch created from
      a remote-tracking ref, the already-on-host confirmation, or the
      fresh-work creation) — before the launch; no topic line in the
      flat list, overview, and card forms
    - Every step-2 check runs before any git or docker activity — an
      argument-form error never switches a branch, refreshes, builds, or
      launches an image
    - The topic procedure (step 3) runs before any docker activity; a topic
      error never launches an image
    - The listing and info forms silently ignore -t/--topic — no message,
      no side effects
    - Without an interactive terminal, `todo` aborts the command before
      any git or docker activity — the error precedes the topic
      procedure
    - The run form rejects `todo` without `topic` — a clean error
      before any git or docker activity
    - The listing and info forms silently ignore --todo — no message,
      no side effects
    - A repeated run already on the host continues without switching — the
      call is idempotent
    - The listing and info forms silently ignore -e/--env, --proxy,
      -c/--clean, -s/--skip, -p/--parallel, and --add-host — no side
      effects; --clean deletes nothing
    - -u/--update refreshes the image only in the flat-list and run forms
    - The -w file-existence validation and the -w/--no-workflow exclusivity
      apply whenever the flags are passed, in every form
    - The run form forwards `skip` and `parallel` as parsed — no validation,
      no defaulting

    Constraints:
    - Do not expose separate list/run/info subcommands — the surface is one
      command with flags
    - Do not invoke afm directly from the host — afm runs inside the
      container
    - Do not read pipeline files; do not read workflow files beyond the -w
      existence check — discovery and composition are in-container
    - Do not import any Type from goga/pipeline — the runtime boundary is
      docker only
    - Do not print anything to stdout for the argument-form errors — stderr
      and the exit code carry them
    - Do not validate the KEY=VALUE format, the "HOST:IP" format beyond the
      single-colon split, or the --skip stage names — forwarded as-is
    - Do not default or validate `parallel`
    - Do not manage the stages of the hosting pipeline — goga prints no
      stages and generates no skips; continuation belongs to the pipeline
      itself
    - Do not switch back to the previous branch after the launch — the run
      finishes on the host branch
    - Do not pass the topic name into the container — the container sees
      the branch through the mounted project

"run_pipeline_container(name: str, config: ProjectConfig, extra_env: tuple[str, ...], proxy: str | None, hosts: dict[str, str], clean: bool, update: bool, workflow: str | None, no_workflow: bool, skip: tuple[str, ...], parallel: int | None) -> exit_code: int":
  location: run_pipeline_container.py
  annotations: |
    Host-side docker launcher for the run form. Launches the goga Docker
    container to run "python -m goga.pipeline run" and returns the
    container's exit code.

    `name`: pipeline name without extension
    `config`: loaded project configuration (provides image, pipeline.agent,
              pipeline.env)
    `extra_env`: raw KEY=VALUE strings (default empty) forwarded into the
                 container env-file. When the default empty tuple is passed,
                 no extra environment variables are forwarded.
    `proxy`: resolved HTTP/HTTPS proxy URL (CLI overrides config in the
             caller). When non-None, the launcher writes HTTP_PROXY,
             HTTPS_PROXY, and NO_PROXY=localhost,127.0.0.1 into the
             container env-file.
    `hosts`: resolved host→IP dict (CLI entries merged on top of
             config.pipeline.hosts by the caller; CLI wins on host-key
             conflict). Each entry becomes a docker run --add-host HOST:IP
             flag.
    `clean`: when True, wipe the persistent afm state host directory before
             launch via `clean_pipeline_runtime_dir`.
    `update`: when True, refresh the image via `docker_update` before
              launching the container — build when a project Dockerfile is
              declared (config.dockerfile set, fatal on failure), else pull
              (warning, non-fatal). When False (default), skip the refresh.
    `workflow`: optional workflow name forwarded into the container env-file
                as GOGA_WORKFLOW_NAME=<workflow>. When None AND
                `no_workflow` is False, the env-file carries neither
                workflow env var — the in-container run coordination attempts
                the basename auto-match fallback. The caller (the `pipeline`
                click command) has already validated file existence on the
                host when this is non-None.
    `no_workflow`: when True, write GOGA_WORKFLOW_DISABLED=1 into the
                   container env-file, forcing the in-container run
                   coordination to skip workflow resolution entirely.
                   Mutually exclusive with `workflow` (enforced by the
                   caller).
    `skip`: stage names (default empty). Joined comma-separated into the
            GOGA_SKIP_STAGES container env-file entry when non-empty.
    `parallel`: optional int (None when absent) capping concurrently
                executing stages. Appended to the in-container argv as
                --parallel <parallel> ONLY when not None; omitted when None.
    `exit_code`: container exit code (0 success; 2 argparse error
                 in-container; 127 afm missing inside the container;
                 non-zero pipeline failure)

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

    Load the home configuration via `load_home_config` (per the
    `home-configuration` practice) early — an empty `HomeConfig` when
    ~/.goga/config.yml is absent (no-op). home.env is the lowest-priority env
    layer; home.docker.run is appended to the docker run (passed as
    extra_args to `DockerRunner`); home.docker.build is forwarded to image
    build (build branch only).

    Algorithm:
    1. Verify docker availability; raise ClickException when missing
    2. Verify config.image is set; raise ClickException when None
    3. Allocate a free localhost TCP port for the dashboard
    4. Generate a unique container_name
    5. Resolve the agent wrapper path via `resolve_wrapper_path` (per the
       `resolve-wrapper-path` practice) using config.pipeline.agent, ONLY
       when config.pipeline.agent is not None; otherwise the wrapper path is
       None. Do not validate the agent value — absence of the wrapper file
       is surfaced by afm itself. pipeline.agent is OPTIONAL: when it is
       None the agent is expected to come from the workflow (per-stage
       command overrides) or from afm's own defaults, so None is carried
       through, not rejected.
    6. Install a SIGTERM/SIGINT handler (raises SystemExit with code 128 +
       signum) BEFORE creating any temp file — the leak-prevention invariant
       (a signal during tmpfile/env-file setup or the `docker_update` build
       must unwind to the finally block and unlink the secret files). The
       runner's handler later nests under this one. Then create a private
       afm-config tmpfile carrying the launcher-side fields: client.command
       set to the resolved wrapper path (written ONLY when the wrapper path
       is not None — i.e. when config.pipeline.agent is configured; omitted
       otherwise so per-stage workflow agents or afm's own defaults cover
       the absent global default), theme: goga (dashboard theme),
       open_browser: false (the dashboard is reached via the host-printed
       http://localhost:<port> URL; afm must not attempt to open a browser
       inside the container), proxy.enabled: false (afm's own internal
       outbound proxy provider is disabled; goga manages the outbound proxy
       through the container env-file via HTTP_PROXY / HTTPS_PROXY /
       NO_PROXY), and prompts_dir: /home/goga/pipeline/prompts (the
       in-container prompts directory, derived from the known
       AFM_DIR=/home/goga/pipeline value — NOT from an additional argument
       or config field; the directory is populated by the in-container run
       coordination from the four goga-packaged defaults plus any inline
       overrides from the pipeline-file header — see the `run-pipeline` and
       `afm` practices). The tmpfile is a read-only overlay over the
       persistent afm state directory mounted in step 11
    7. Resolve the persistent afm state host directory via
       `resolve_pipeline_runtime_dir` and ensure it exists (idempotent).
       Executed BEFORE the step-6 signal handler and temp files — no secret
       file exists yet at this point, and the directory must be on disk
       under every exit path
    8. When `clean` is True: wipe the resolved directory via
       `clean_pipeline_runtime_dir` before launch (also before the step-6
       handler and temp files — the wipe is strictly pre-launch, never in
       finally)
    9. Compute the workflow env-file entries (host-side, BEFORE container
       launch):
       - When `no_workflow` is True:
         * workflow_env = {"GOGA_WORKFLOW_DISABLED": "1"}
         * workflow_log_name = None (no log emitted)
       - Else when `workflow` is not None (explicit --workflow, file already
         validated by the caller):
         * workflow_env = {"GOGA_WORKFLOW_NAME": workflow}
         * workflow_log_name = workflow
       - Else (auto-match fallback):
         * Compose auto_match_path as <cwd>/.goga/workflows/<name>.yml
         * When the auto-match file exists:
           - workflow_env = {} (in-container resolution handles the
             basename fallback)
           - workflow_log_name = name (the candidate)
         * Else:
           - workflow_env = {} (in-container resolution will silent-miss)
           - workflow_log_name = None (no log — file does not exist)
    10. When workflow_log_name is not None: print
        "Pipeline running with workflow WORKFLOW_LOG_NAME" to stdout.
        This cell surfaces only this workflow log line — it does NOT print
        any dashboard URL line.
    11. Create a private env-file layering home.env as the BASE
        (lowest-priority) layer, then config.pipeline.env, git identity, the
        `extra_env` KEY=VALUE strings (forwarded as-is, no validation),
        AFM_DIR set to the in-container persistent state path, the
        workflow_env entries (GOGA_WORKFLOW_NAME and/or GOGA_WORKFLOW_DISABLED
        per step 9), the GOGA_SKIP_STAGES entry when `skip` is non-empty
        (joined comma-separated via ",".join(skip); omitted when `skip` is
        empty), and — when `proxy` is non-None — HTTP_PROXY, HTTPS_PROXY,
        and NO_PROXY (fixed at localhost,127.0.0.1). Project config and CLI
        override home.env on key conflict.
    12. Assemble the docker-run inputs to hand to `DockerRunner`:
        - args: -m goga.pipeline run <name> --port <port>
          [--parallel <parallel>] (--parallel <parallel> appended ONLY when
          not None; omitted when None)
        - params: name=<container_name>, rm=True, entrypoint=python3,
          workdir=/workspace, p=<port>:<port>, v=[project:/workspace (set as
          container working dir), the persistent afm state host dir (step 7)
          read-write at the in-container persistent state path, the afm-config
          tmpfile (step 6) read-only at the in-container afm config path as a
          client.command overlay, 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 to DockerRunner.run (NOT part of params; appended
          verbatim AFTER the translated flags and BEFORE the image, never
          translated to an --extra-args flag). Empty list when the home file
          is absent
        - The assembled form: --rm, --name, -p <port>:<port>, the project
          bind-mount as /workspace (container working directory), the
          persistent afm state host dir read-write at the in-container
          persistent state path (survives across runs), the afm-config
          tmpfile read-only at the in-container afm config path (independent
          of AFM_DIR; see the `afm` practice — the persistent dir supplies
          the rest of afm state), --add-host per host, --env-file, each
          credential mount from `resolve_credential_mounts` read-only,
          --entrypoint python3, image, args
    13. Call `docker_build_if_not_exist` with config.image, config.dockerfile,
        and extra_args=home.docker.build — the first-run safety net. 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 ClickException, exit 1, launch skipped).
        home.docker.build tokens reach the build branch verbatim (extra_args
        — appended after the translated params flags, before -f); ignored on
        no-op branches. Runs inside the try so the leak-prevention invariant
        covers this window: the secret tmpfile/env-file are already written
        (steps 6, 11), and a fatal build unwinds to the finally which unlinks
        them.
    14. 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), else pull (warning,
        non-fatal). home.docker.build tokens are forwarded in the build
        branch only (ignored on the pull branch). When `update` is False:
        skip the refresh.
    15. Launch the container via
        DockerRunner(config.image).run(args, extra_args=home.docker.run,
        **params): (extra_args is a SEPARATE keyword; params is unpacked via
        **) the runner installs its own SIGTERM/SIGINT handler (exit 128 +
        signum), which nests under the caller handler installed in step 6
        (saves and restores it), runs docker run, streams stdout/stderr, and
        returns the exit code. The runner finally performs the guaranteed
        docker kill and restores to the caller handler
    16. In finally: delete the afm-config tmpfile and delete the env-file,
        and restore the caller-installed SIGTERM/SIGINT handler to the
        original. Do NOT delete the persistent afm state host directory — it
        survives across runs. 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
    17. Return the container exit code

    Apply the `agent-wrappers` practice for the wrapper path semantics
    referenced in step 5.
    Apply the `resolve-wrapper-path` practice when calling
    `resolve_wrapper_path` in step 5.
    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 and the first-run safety net.
    Apply the `docker-runner` practice for the docker run launch and
    lifecycle.
    Apply the `afm` practice for the in-container afm config.yaml contract,
    including the prompts_dir field written in step 6.
    Apply the `run-pipeline` practice for the in-container prompt
    materialization contract that populates the prompts_dir directory, and
    for the workflow environment contract (GOGA_WORKFLOW_NAME /
    GOGA_WORKFLOW_DISABLED) that this launcher sets via the env-file in
    steps 9-11.
    Apply the `home-configuration` practice for loading the optional
    machine-wide home config (home.env as the lowest-priority env layer;
    home.docker.run appended to the docker run; home.docker.build forwarded
    to image build in the build branch only).

    Requirements:
    - Use the same port value in -p <port>:<port> and in --port <port>
      (single source — the port allocated in step 3)
    - afm-config tmpfile is owner-only (private). It carries the
      launcher-side fields: client.command: <resolved wrapper path> (the
      value MUST be the absolute path returned by `resolve_wrapper_path`,
      never a bare agent name; the client: block is written ONLY when
      config.pipeline.agent is configured — when it is None the block is
      omitted, so per-stage workflow agents or afm's own defaults cover the
      absent global default), theme: goga, open_browser: false,
      proxy.enabled: false, and prompts_dir: /home/goga/pipeline/prompts.
      theme, open_browser, proxy.enabled, and prompts_dir are static
      launcher-side constants — they are NOT configurable via the goga
      ProjectConfig or CLI
    - env-file content: home.env (base) + config.pipeline.env + git identity
      + `extra_env` KEY=VALUE strings (forwarded as-is, no validation) +
      AFM_DIR=/home/goga/pipeline + (when proxy is non-None)
      HTTP_PROXY/HTTPS_PROXY/NO_PROXY
    - home.env is the lowest-priority env layer; project config
      (config.pipeline.env) and CLI extra_env override home.env on key
      conflict. home.docker.run tokens are appended to the docker run
      (passed as extra_args to `DockerRunner`). home.docker.build tokens are
      forwarded to image build (passed as extra_args to
      `docker_build_if_not_exist` / `docker_update`; build branch only).
      An absent home file yields an empty `HomeConfig` — no effect
    - Persistent afm state directory mount is read-write; the tmpfile
      client.command overlay is read-only
    - The client.command tmpfile mount target is FIXED at
      /home/goga/.afm/config.yaml and does NOT depend on AFM_DIR — afm
      always reads config.yaml from ~/.afm/config.yaml. AFM_DIR controls
      only afm state (flows, run-state), not config.yaml discovery
    - Credential mounts come from `resolve_credential_mounts` — every
      returned tuple is an existing file, no re-check needed
    - 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
    - AFM_DIR=/home/goga/pipeline is provided via the env-file
    - SIGTERM/SIGINT during the run must result in exit code 128 + signum
      (130 for SIGINT, 143 for SIGTERM); the caller installs its
      SIGTERM/SIGINT handler BEFORE writing the secret tmpfile/env-file, and
      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 secret files)
    - Propagate afm's non-zero exit code as the container's exit code
    - Workflow env-file entries follow the decision matrix in step 9:
      --no-workflow → GOGA_WORKFLOW_DISABLED=1; explicit --workflow X →
      GOGA_WORKFLOW_NAME=X; auto-match fallback → neither env var (the
      container resolves the basename). The decision is taken host-side so
      the workflow log is accurate
    - Write GOGA_SKIP_STAGES=<csv> into the container env-file when `skip`
      is non-empty
    - The "Pipeline running with workflow NAME" log line is printed ONLY
      when a workflow will actually be applied: explicit --workflow X (file
      already validated by the caller) OR basename auto-match file exists on
      the host (existence check at step 9). When --no-workflow is set OR the
      auto-match file does not exist, NO workflow log is printed
    - Append --parallel <parallel> to the in-container run argv ONLY when
      `parallel` is not None (absent ⇒ no flag ⇒ afm unbounded); the Docker
      -p <port>:<port> port-publish token is distinct from the Click
      -p/--parallel option

    Constraints:
    - Refresh an EXISTING image only when `update` is True via
      `docker_update` — the default is no refresh. 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 apply home.env to docker build — home.env is a docker run
      container-env layer only (no --build-arg)
    - Do NOT let home.env override project config or CLI env — home.env is
      the lowest-priority (base) layer
    - Delete only the tmpfile and env-file in finally — the persistent afm
      state host directory survives across runs
    - --clean wipes the persistent afm state host directory BEFORE launch;
      the directory survives across runs (no wipe in finally under any exit
      path)
    - Do not filter credential mounts by config.pipeline.agent — detection
      is agent-agnostic (see the `resolve-credential-mounts` practice)
    - 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 the project
      directory — in-container afm state belongs in /home/goga/.afm/
      (config.yaml) and /home/goga/pipeline (state)
    - Do not write the afm config into the project directory — use a tmpfile
      and a read-only mount
    - Do not invoke the in-container entrypoint other than via "docker run
      python -m goga.pipeline"
    - Do not import any Type from goga/pipeline — runtime boundary only
    - Do not validate config.pipeline.agent against a hardcoded whitelist —
      absence of the wrapper is surfaced by afm
    - Do not write a bare agent name into client.command — always write the
      resolved absolute wrapper path
    - Do not derive prompts_dir from an additional CLI option, config field,
      or runtime argument — the value is fixed at
      /home/goga/pipeline/prompts and follows from the known
      AFM_DIR=/home/goga/pipeline constant
    - Do not surface any dashboard URL line on stdout — this cell prints
      only the workflow log line (when applicable) and forwards the docker
      output stream
    - Do not default `parallel` — None ⇒ unbounded; forward as-is
    - Do not let the user -p/--parallel Click option collide with the Docker
      -p <port>:<port> port-publish token — they are in different namespaces

"run_pipeline_info_container(name: str | None, info: bool, config: ProjectConfig, hosts: dict[str, str], update: bool, workflow: str | None, no_workflow: bool) -> exit_code: int":
  location: run_pipeline_info_container.py
  annotations: |
    Host-side read-only docker launcher for the listing and info forms.
    Launches the goga Docker container in a minimal shape and invokes
    python -m goga.pipeline inside it. Returns the container's exit code.

    `name`: pipeline name without extension for the card form; None for the
            listing forms
    `info`: True for the overview and card forms; False for the flat list
    `config`: loaded project configuration (image, dockerfile)
    `hosts`: resolved host→IP mapping from config.pipeline.hosts
    `update`: when True and `info` is False — refresh the image before the
              listing; ignored in the overview and card forms
    `workflow`: optional workflow name forwarded to the card form as an
                explicit -w argument
    `no_workflow`: when True — --no-workflow is forwarded to the card form
    `exit_code`: container exit code

    Apply the `docker-builder` practice for the image refresh and the
    first-run safety net.
    Apply the `docker-runner` practice for the docker run launch and
    lifecycle.
    Apply the `home-configuration` practice for the optional machine-wide
    home config (home.docker.run appended to every docker run;
    home.docker.build forwarded to image building).
    Apply the `pipeline-cli` practice for the in-container subcommand
    surface the argv addresses.

    Algorithm:
    1. Verify docker availability and config.image; clean errors on missing
    2. Load the home configuration; home.docker.run joins every docker run,
       home.docker.build joins image building
    3. Compose the in-container argv:
       - flat list: -m goga.pipeline list
       - overview: -m goga.pipeline list --info
       - card: -m goga.pipeline run NAME --info, plus -w WORKFLOW when
         `workflow` is provided or --no-workflow when `no_workflow` is set
    4. Generate a unique container name
    5. Run the first-run safety net `docker_build_if_not_exist` — builds the
       local image when it is absent and a project Dockerfile is declared
       (fatal on failure), no-op otherwise
    6. When `update` is True and `info` is False — refresh the image via
       `docker_update` (build when a project Dockerfile is declared, fatal;
       else pull, non-fatal warning); the overview and card forms skip the
       refresh entirely
    7. Launch the container via `DockerRunner` with the minimal parameter
       set: --rm, the unique name, the project bind-mount as the container
       working directory, one --add-host per resolved host, entrypoint
       python3 — no port publish, no env-file, no extra mounts; the runner's
       built-in signal handling and guaranteed kill remain
    8. Return the container exit code

    Requirements:
    - The minimal shape holds for all three forms: no published port, no
      env-file, no afm-config tmpfile, no persistent afm state mount, no
      credential mounts, no caller-side signal handler
    - The card argv carries the workflow decision exactly as given:
      explicit -w WORKFLOW, --no-workflow, or neither (in-container
      auto-match)
    - The listing and info forms write nothing to the project or runtime
      directories and launch no agents

    Constraints:
    - Do not publish a port, mount a tmpfile or env-file, or mount the
      persistent afm state — the info path is read-only
    - Do not refresh the image in the overview and card forms
    - Do not write any file on the host — no tmpfile, no env-file, no
      cleanup
    - Do not import any Type from goga/pipeline — the runtime boundary is
      docker only

"resolve_pipeline_runtime_dir(pipeline_name: str) -> runtime_dir: Path":
  location: run_pipeline_container.py
  annotations: |
    Compute the host-side path of the persistent afm state directory for the
    given pipeline in the current project on the current git branch.

    `pipeline_name`: pipeline name without extension (the run-form name
                     argument)
    `runtime_dir`: absolute host path under
                   ~/.goga/runtime/pipelines/<project>/<branch>/<sanitized_name>/
                   where <sanitized_name> is pipeline_name with every colon
                   replaced by a hyphen (the path is used as a docker
                   bind-mount source, where an unsanitized colon is parsed
                   by docker as the source:target[:mode] separator →
                   "invalid mode").

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

    Algorithm:
    1. Sanitize pipeline_name for use as a path segment: replace every colon
       with a hyphen (colon → hyphen). The host state-dir path is a docker
       bind-mount source, so an unsanitized colon would be parsed by docker
       as the source:target[:mode] separator (e.g. "pybuggy:api-feature" →
       docker "invalid mode"). The ORIGINAL pipeline_name (NOT the sanitized
       segment) is still used for the in-container run argv and the workflow
       auto-match basename, so this step affects only the host state-dir path
       segment.
    2. Delegate to `resolve_runtime_dir`("pipelines", <sanitized_name>) —
       the routine composes the full path from cwd, git branch, and the
       supplied suffix
    3. Return the resulting path

    Requirements:
    - Return value is an absolute path
    - The pipeline_name path segment has every colon replaced with a hyphen
      (sanitization is THIS facade's responsibility, NOT
      resolve_runtime_dir's — resolve_runtime_dir passes suffix_parts
      through verbatim per its "caller owns the namespace" contract)
    - Pure with respect to the filesystem — does not create the directory
      (creation is the caller's responsibility)

    Constraints:
    - Do not create the directory here — the caller handles creation
    - Do not validate that pipeline_name corresponds to an existing pipeline
      file
    - Do not sanitize the name for any use OTHER than the host state-dir
      path segment — the in-container run argv and the workflow auto-match
      basename use the ORIGINAL pipeline_name; only the host path segment is
      sanitized

"clean_pipeline_runtime_dir(pipeline_runtime_dir: Path) -> none: None":
  location: run_pipeline_container.py
  annotations: |
    Recursively wipe the persistent afm state directory and recreate it
    empty. Called before container launch when the --clean flag is set.

    `pipeline_runtime_dir`: host path computed by `resolve_pipeline_runtime_dir`

    Apply the `convention` 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 `pipeline_runtime_dir` was computed by
      `resolve_pipeline_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

---

Author: Goga
CreatedAt: 19/08/26
Description: |
  Host-side CLI wrapper cell for the single pipeline command. Every form —
  the flat list, the overview, the card, and the run — launches the goga
  Docker container and invokes the in-container pipeline entrypoint inside
  it; the run form can first bring the repository onto the requested work
  (the -t/--topic switch-or-create) on the host. The runtime boundary to
  the in-container pipeline is docker — this cell has no Python Type
  Imports from it.
