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:
      - ConfigHooks
      - ConfigOverlay
    Usages:
      - checkpoints
    From: goga/config/hooks
  - Types:
      - resolve_runtime_dir
    Usages:
      - runtime-paths
    From: goga/runtime
  - Usages:
      - list-pipelines
      - run-pipeline
      - pipeline-cli
      - describe-pipelines
      - describe-pipeline
      - registering-hooks
    From: goga/pipeline
  - 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:
  convention: .goga/usages/conventions.md
  click: .goga/usages/cooks/click.md
  afm: .goga/usages/cooks/afm.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 `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 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 `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.
  Use the `registering-hooks` practice for the tool-package hook
  subscription surface of the pipeline domain actions behind the
  checkpoint surface the container-side run delivers.

  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, persistent afm state,
  signal handling) and the info launcher (minimal read-only shape — none of
  those). The host owns the launch mechanics only: image, dockerfile, port,
  mounts, hosts, proxy variables, env-file base layers, engine variables,
  container name, signal handling, and the structural section guards. Run
  parameters — the task env layer and the agent — resolve in-container.

  The run form carries the CLI environment entries twice from one source
  (per `extra-env-carriage`): the raw KEY=VALUE lines travel in the env-file,
  and the same entries travel encoded as the dedicated engine payload
  variable. The configuration task env layer stays out of the env-file — the
  in-container run coordination applies it around the afm launch. Use
  `checkpoints` for the host config amendment delivery: the host delivery
  serves the host-owned launch fields, and amendments into run-parameter
  fields stay applied-but-unconsumed — silently.

  Everything the in-container CLI consumes (run options) travels as command
  arguments after the image; everything the afm binary, agents, and tools
  consume travels through the env-file. The env-file carries environment
  layers only — run options never enter it.

  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_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.

  The run form produces the AFM_DOCKER_FILE_ROOTS environment variable —
  the file-manager roots of the afm dashboard. The value is composed from
  the actual directory mounts of the launch — the payload contract lives
  in the `afm` practice — and written into the container env-file through
  `collect_file_roots` and `encode_file_roots`. The listing and info
  forms produce no such variable.

---

"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 and card
            forms; forwarded into the in-container argv as one -s flag per
            name; 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; deliver the config amendment checkpoint per the
       `checkpoints` practice via the `ConfigHooks` checkpoint surface —
       amend_config(config=...) — and treat the effective configuration
       of the `ConfigOverlay` as
       the launch's configuration: the missing-pipeline-section guard and
       every downstream read of host-owned launch fields (image,
       dockerfile, proxy, hosts) address the effective configuration; a
       missing pipeline section is a clean error. Amendments into
       run-parameter fields (pipeline.env, pipeline.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 (closed surface)
    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)
       2.5. Reject leading-dash argv tokens: a `workflow` value starting with
            "-", a pipeline `name` starting with "-", or a `skip` name
            starting with "-" is a clean error (exit 1) — argv-form
            parsability, not stage-name validation. Click would swallow a
            flag-looking next token as an option value (e.g. passing
            -s --no-workflow parses with the flag as the skip name), and
            the in-container argparse parser would abort late — after the
            whole launch ceremony — with "expected one argument". The -w
            check acts wherever the flag is passed; the name and -s checks
            act in the run and card forms only — the listing forms
            silently ignore -s
    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, the
         workflow flags, and the skip names
       - 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
    - The amendment summary lines of the `ConfigOverlay` print to stderr
      (nothing when empty); stdout is unchanged
    - 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 forms silently ignore -e/--env, --proxy, -c/--clean,
      -s/--skip, -p/--parallel, and --add-host — no side effects; --clean
      deletes nothing
    - The card form forwards the skip names — the same flags produce the same
      composition in card and run forms
    - -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 and card forms forward `skip` as parsed — one -s flag per name,
      no validation, no defaulting; the run form forwards `parallel` as
      parsed
    - Reject leading-dash tokens for `workflow`, the pipeline `name`, and
      `skip` values (argv-form parsability — non-dash names stay forwarded
      unvalidated, unknown or not)

    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] | None, 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. The host owns the launch mechanics only — the
    run parameters (the task env layer, the agent) resolve in-container.

    `name`: pipeline name without extension
    `config`: the host-effective project configuration, consumed for the
              host-owned launch fields only (image, dockerfile, proxy,
              hosts); the run-parameter fields (pipeline.env,
              pipeline.agent) are consumed nowhere on the host
    `extra_env`: raw KEY=VALUE strings (default empty) 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`: 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 as engine variables.
    `hosts`: resolved host→IP dict (CLI entries merged on top of
             config.pipeline.hosts by the caller; CLI wins on host-key
             conflict; default None — no extra hosts). 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 in-container argv
                as -w <workflow>. When None AND `no_workflow` is False, the
                argv carries neither workflow flag — 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, --no-workflow joins the in-container argv,
                   forcing the in-container run coordination to skip workflow
                   resolution entirely. Mutually exclusive with `workflow`
                   (enforced by the caller).
    `skip`: stage names (default empty). Each name appends one -s <name> to
            the in-container argv; names are forwarded as-is (no validation,
            no dedup).
    `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 persistent afm state host directory via
       `resolve_pipeline_runtime_dir` and ensure it exists (idempotent)
    6. When `clean` is True: wipe the resolved directory via
       `clean_pipeline_runtime_dir` before launch (strictly pre-launch,
       never in finally)
    7. Install a SIGTERM/SIGINT handler (raises SystemExit with code 128 +
       signum) BEFORE creating the env-file — the leak-prevention invariant
       (a signal during the env-file setup or the `docker_update` build must
       unwind to the finally block and unlink the secret file). The runner's
       handler later nests under this one
    8. Compute the workflow log decision (host-side, BEFORE container
       launch):
       - When `no_workflow` is True — no workflow log
       - Else when `workflow` is not None (explicit --workflow, file already
         validated by the caller) — log the given name
       - Else (auto-match fallback) — compose auto_match_path as
         <cwd>/.goga/workflows/<name>.yml (a name whose resolved path
         escapes the workflows directory — a ".." segment or an absolute
         prefix — is a silent no-log, mirroring the in-container
         containment guard); log the pipeline name exactly when that file
         exists; otherwise no log
    9. 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.
    10. Create a private env-file layering, in order: home.env as the BASE
        (lowest-priority) layer, then git identity, then the raw `extra_env`
        KEY=VALUE strings (forwarded as-is, no validation), then the engine
        variables — AFM_DIR set to the in-container persistent state path,
        AFM_DOCKER_FILE_ROOTS set to the encoded file-manager roots of the
        launch (composed via `collect_file_roots` from the home.docker.run
        tokens and encoded via `encode_file_roots`; the roots never diverge
        from the actual mounts of the launch), and, when `proxy` is
        non-None, HTTP_PROXY, HTTPS_PROXY, and NO_PROXY (fixed at
        localhost,127.0.0.1) — then the CLI entries payload line:
        GOGA_EXTRA_ENV set to the value of `encode_extra_env` over the same
        `extra_env` strings (one source, per `extra-env-carriage` — the
        payload and the env-file lines compose from the same parsed values
        on every launch). The configuration task env layer (pipeline.env)
        never enters the env-file. The engine layer is written after the CLI
        lines and skips a key the CLI explicitly supplied — an explicit user
        entry for an engine variable (the documented
        -e AFM_DOCKER_FILE_ROOTS=... escape hatch of the `afm` practice)
        keeps winning over the launcher-produced value
    11. Assemble the docker-run inputs to hand to `DockerRunner`:
        - args: -m goga.pipeline run <name> --port <port> [-w <workflow>]
          [--no-workflow] [-s <name>]... [--parallel <parallel>] — the
          workflow flags appended exactly as given (neither flag on
          auto-match), one -s per skip name, --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 5)
          read-write at the in-container persistent state path],
          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), --add-host per host,
          --env-file, --entrypoint python3, image, args
    12. 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 env-file is already written (step 10),
        and a fatal build unwinds to the finally which unlinks it
    13. 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
    14. 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 7
        (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
    15. In finally: 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)
    16. Return the container exit code

    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 file-manager roots payload delivered
    through AFM_DOCKER_FILE_ROOTS in step 10 — the schema, the encoding,
    and the roots scope live there.
    Apply the `extra-env-carriage` practice for the CLI entries payload
    line of step 10 and the ladder order of the env-file layers.
    Apply the `run-pipeline` practice for the in-container contract the
    argv addresses — the load-and-amend, the afm configuration authorship,
    and the launch layer the container side composes.
    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)
    - env-file content: home.env (base) + git identity + the raw CLI -e
      strings + AFM_DIR + AFM_DOCKER_FILE_ROOTS + (when proxy) the proxy
      triple + GOGA_EXTRA_ENV — the engine variables follow the CLI lines;
      the configuration task env layer never enters
    - The payload line and the CLI lines compose from the same parsed values
      on every launch — they never diverge
    - AFM_DOCKER_FILE_ROOTS is written into the env-file on every run
      launch: the value comes from `collect_file_roots` and
      `encode_file_roots` over the home.docker.run tokens; repeated
      launches with unchanged mounts produce the identical value; an
      explicit user -e AFM_DOCKER_FILE_ROOTS=... entry wins
    - home.env is the lowest-priority env layer; the CLI -e lines 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; no afm config
      overlay exists on any launch — the afm config.yaml is written
      in-container by the run coordination
    - AFM_DIR=/home/goga/pipeline is provided via the env-file
    - 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
    - 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 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 file)
    - Propagate afm's non-zero exit code as the container's exit code
    - The run argv carries the workflow decision exactly as given: -w <name>,
      --no-workflow, or neither (in-container auto-match); the skip names
      append one -s <name> per entry — forwarded as parsed, no validation
    - 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 8). 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 the CLI env — home.env is the
      lowest-priority (base) layer
    - Delete only the 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 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, written by the container) and /home/goga/pipeline (state)
    - Do not compose file-manager roots from any mount other than the
      project mount and the home.docker.run directory mounts — engine
      mounts (the persistent afm state) never become roots
    - Do not resolve the agent or write any afm configuration — the afm
      config.yaml is authored in-container by the run coordination
    - Do not write the configuration task env layer (pipeline.env) into the
      env-file — it applies in-container around the afm launch
    - Do not encode the CLI entries from any source other than the same
      parsed values written into the env-file
    - 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 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
    - Do not write or interpret any GOGA_WORKFLOW_NAME, GOGA_WORKFLOW_DISABLED,
      or GOGA_SKIP_STAGES entry — user-supplied values of those names (via -e
      or home.env) may travel in the env-file verbatim and stay inert: no
      warning, no error

"run_pipeline_info_container(name: str | None, info: bool, config: ProjectConfig, hosts: dict[str, str], update: bool, workflow: str | None, no_workflow: bool, skip: tuple[str, ...]) -> 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
    `skip`: stage names forwarded to the card form — one -s <name> appended
            to the card argv per entry
    `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,
         plus one -s NAME per skip entry
    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 persistent afm state mount, no
      caller-side signal handler
    - The card argv carries the workflow decision and the skip names exactly
      as given: explicit -w WORKFLOW, --no-workflow, or neither (in-container
      auto-match), and one -s NAME per skip entry
    - The listing and info forms write nothing to the project or runtime
      directories and launch no agents

    Constraints:
    - Do not publish a port, mount an 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 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":
  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

"FileRoot(id: str, label: str, container_path: str, mount_read_only: bool, kind: str)":
  location: file_roots.py
  annotations: |
    Record of one file-manager root — a container directory the user may
    browse in the afm dashboard. Carries the stable data model of the roots
    payload; the field set and meanings follow the schema in the `afm`
    practice.

    `id`: stable identifier, unique within the list; the fixed string
          "project" identifies the project root
    `label`: display name shown to the user
    `container_path`: in-container mount target of the browsable directory
    `mount_read_only`: whether the mount is read-only
    `kind`: "project" for the project root, "extra" for a directory mount
            resolved from home.docker.run volume tokens

    Apply the `convention` practice for the data-model rules of this record.

    Requirements:
    - Immutable data record per the `convention` practice
    - Field order is stable and matches the payload schema in the `afm`
      practice

    Constraints:
    - No behavior — the type carries data only
  properties:
    "id -> str": |
      Stable identifier of the root, unique within the list; the fixed
      string "project" for the project root.
    "label -> str": |
      Display name: "project" for the project root; the full container path
      for extra roots.
    "container_path -> str": |
      In-container mount target of the browsable directory.
    "mount_read_only -> bool": |
      True when the mount is read-only.
    "kind -> str": |
      Root kind: "project" or "extra".

"collect_file_roots(tokens: list[str]) -> roots: list[FileRoot]":
  location: file_roots.py
  annotations: |
    Compose the file-manager roots of a run launch from the docker volume
    tokens of the home configuration: the project root first, then one root
    per directory mount declared by the tokens.

    `tokens`: docker run tokens of home.docker.run — already
              shell-tokenized; consumed verbatim per the
              `home-configuration` practice
    `roots`: the ordered root list — the project root first, extra roots in
             token order

    Algorithm:
    1. Start the list with the project root: id "project", label "project",
       container path /workspace, read-write, kind "project"
    2. Scan `tokens` for volume declarations — the short -v and long
       --volume flag forms; unrecognized tokens are skipped without failing
    3. For each volume declaration whose host part exists as a directory:
       derive one extra root — kind "extra", label equal to the container
       path, the read-only flag from the presence of the read-only mode in
       the token, the id derived deterministically from the container path;
       a later declaration targeting an already-rooted container path
       supersedes the earlier root — the surviving root carries the later
       declaration's fields and position (its mount shadows the earlier)
    4. Skip declarations whose host part is a named volume, a file, or a
       missing path — only directories become roots
    5. Return the list: the project root first, extra roots in token order

    Requirements:
    - Deterministic — unchanged tokens produce an identical list on every
      call
    - Every extra-root id is unique within the list and never collides with
      "project"
    - At most one root per container path — when several volume
      declarations target the same container path, only the last
      declaration in token order yields the root; earlier declarations
      targeting an already-rooted container path are superseded (their
      mount is shadowed by the later one)
    - Only existing host directories become extra roots

    Constraints:
    - Do not re-split or re-quote `tokens` — consume the already-tokenized
      list
    - Do not fail on unrecognized or malformed tokens — skip them
    - Do not include engine mounts — the afm state mount never becomes a
      root

"encode_file_roots(roots: list[FileRoot]) -> value: str":
  location: file_roots.py
  annotations: |
    Encode the roots list into the value of the AFM_DOCKER_FILE_ROOTS
    environment variable: a compact JSON payload with a fixed field order,
    carried as one standard-base64 string with padding, per the `afm`
    practice.

    `roots`: the ordered root list
    `value`: the base64 string written as the variable value

    Algorithm:
    1. Compose the payload object — the fixed schema version and the roots
       list, each root carrying its fields in the stable schema order
    2. Serialize as compact UTF-8 JSON without whitespace
    3. Encode with standard base64, padding kept, as a single line
    4. Return the encoded string

    Requirements:
    - Deterministic — the same list always produces the identical string
    - The output decodes with standard base64 (padding) into the compact
      JSON of the schema in the `afm` practice

    Constraints:
    - Do not vary separators, field order, or padding — the canonical form
      is fixed
    - Do not wrap or split the output — one line, no whitespace

---

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 host owns the launch mechanics only: the run form can first bring
  the repository onto the requested work (the -t/--topic switch-or-create)
  on the host, describes the afm dashboard file-manager roots and the CLI
  entries payload to the container env-file, and leaves the run parameters —
  the task env layer and the agent — to the in-container coordination. The
  runtime boundary to the in-container pipeline is docker — this cell has no
  Python Type Imports from it.
