Imports:
  - Types:
      - compile_flow
      - translate_role
      - parse_dsl
      - FlowStage
    Usages:
      - compile-flow
      - parse-dsl
      - serialize-flow
      - memory-emission
    From: goga/pipeline/compiler
  - Types:
      - run_flow
    Usages:
      - run-flow
    From: goga/afm
  - Types:
      - ensure_in_docker
      - decode_extra_env
    Usages:
      - ensure-in-docker
      - extra-env-carriage
    From: goga/docker
  - Types:
      - parse_workflow
      - WorkflowDocument
      - WorkflowStage
    Usages:
      - parse-workflow
      - memory
    From: goga/pipeline/workflow
  - Types:
      - resolve_project_name
      - load_project_config
    Usages:
      - project-configuration
    From: goga/config
  - Types:
      - ConfigHooks
      - ConfigOverlay
    From: goga/config/hooks
  - Types:
      - resolve_wrapper_path
    Usages:
      - resolve-wrapper-path
    From: goga/agents
  - Types:
      - PipelineHooks
      - PipelineIdentity
      - WorkflowDecision
      - WorkIdentity
      - WorkflowOverlay
      - CompositionStage
    Usages:
      - checkpoints
    From: goga/pipeline/hooks
  - Types:
      - resolve_current_branch_name
      - resolve_topic_dir
      - resolve_topic_status
      - assemble_status_scale
    Usages:
      - topic-paths
      - topic-statuses
    From: goga/history

Usages:
  convention: .goga/usages/conventions.md
  afm: .goga/usages/cooks/afm.md
  argparse: |
    Use the standard library argparse module for in-container CLI parsing.
    Two subcommands: list (with an optional --info flag) and run (a required
    name, an optional --info flag, the workflow flags -w/--workflow NAME and
    --no-workflow, a repeatable -s/--skip NAME — all three available with and
    without --info, --port required only without --info, and an optional
    --parallel N).
  cli_entrypoint: |
    The in-container CLI is launched through the package runpy entrypoint
    (python -m goga.pipeline). The package __main__ module MUST stay a thin
    wrapper: it imports `pipeline_cli` from the local cli module and calls
    it with process argv under the standard __main__ guard. The
    `pipeline_cli` implementation itself MUST live in the cli module —
    never in the __main__ module — so that importing the goga.pipeline
    package does not pull __main__ into sys.modules and trigger a runpy
    RuntimeWarning about __main__ being pre-imported. The single piece of
    logic permitted in __main__.py besides the runpy delegation is the
    in-container docker guard (`ensure_in_docker`, per the
    `ensure-in-docker` practice): it is invoked as the very first
    statement of the __main__ guard block, before `pipeline_cli` is called,
    so host-side invocations of python -m goga.pipeline fail loudly before
    any pipeline work. The guard does NOT move into `pipeline_cli` itself
    — `pipeline_cli` stays a pure parse-and-dispatch routine whose
    contract is unchanged by the guard. The guard invocation MUST be
    covered by tests for both branches: the success path (GOGA_DOCKER=1)
    proceeds to `pipeline_cli`, and the refusal path (marker unset or not
    "1") writes to stderr and exits with code 1 before `pipeline_cli` is
    reached.
  default_prompts: |
    The four default agent prompt files ship inside the installed goga
    package at
    goga/assets/afm/prompts/{planning,implementation,review,summary}.md.
    The directory contains exactly four files; the file stem matches the
    canonical afm agent name. Three of them (planning, implementation,
    review) correspond to the overridable DSL roles planner/executor/
    reviewer (the role-field-name → stem mapping is resolved via
    `translate_role`, imported from goga/pipeline/compiler); summary
    is NOT overridable from the DSL — summary.md is always materialized
    from the default. Resolution from the installed package location is an
    implementation detail — the consumer may use any standard mechanism
    (e.g. importlib.resources or Path(__file__) composition) that yields
    the absolute path to the package's goga/assets/afm/prompts/
    directory at runtime. All four files are expected to exist in a
    properly installed image — when a default is missing AND no inline
    override is supplied for the corresponding role, materialization
    fails with a readable error before launch.

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

  This cell owns the pipeline workflow surface: discovery of *.yml pipeline
  files across the project and user pipeline directories, the pipeline-file
  entity model, the informational surface (overview and card), run
  coordination, and the in-container CLI entrypoint `pipeline_cli`.
  Subprocess execution is delegated to `run_flow` — afm run is invoked inside
  the container, never directly.

  Build the data models `PipelineEntry`, `PipelineSummary`, `PipelineCard`,
  and `CardStage` with the standard library dataclasses module (kw_only=True
  per `convention`); `PipelineSource` with the standard library enum module
  (str-backed). Use pathlib.Path for directory and path resolution.

  The pipeline directories are <cwd>/.goga/pipelines/ (project-level,
  user-authored) and ~/.goga/pipelines/ (user-level, populated by goga
  connect). On a name conflict, the project source wins. The workflow
  directory is <cwd>/.goga/workflows/ (project-level).

  Workflow resolution is one rule set shared by run coordination and the
  card: an explicit workflow name, forced disabling, and the basename
  auto-match of the pipeline name are resolved by `resolve_workflow`, so the
  card always reflects the workflow a run with the same flags would apply.
  Use `parse-workflow` for the accepted workflow-file structure.

  The CLI skip names arrive as an explicit parameter of run coordination and
  the card: applied in-memory onto the resolved workflow before compilation.
  The CLI performs no stage-name validation — unknown names surface as the
  compiler's structural error.

  The run coordination and the card compose through the pipeline hooks zone:
  the workflow amendment is delivered after the runner-skip merge and before
  compilation, the run notifications fire around the runner launch, and the
  card reports the contributing tools. An explicit workflow disable turns
  the layer off — the raw authored DSL composes and no amendment delivers;
  a silent auto-match miss keeps the layer active onto the empty base. The
  branch, topic, and status facts resolve in the operation before the
  delivery — the checkpoints read nothing. With no tool packages installed
  the overlay is the passthrough — every form behaves exactly as with the
  amendment layer absent.
  Use the `checkpoints` practice for the checkpoint surface of the zone.
  Use the `topic-paths` practice for the topic directory resolution behind
  the work identity and the `topic-statuses` practice for the status facts
  of the run events.

  The run coordination owns the in-container run parameters: the opening
  load-and-amend of the project configuration (the effective configuration
  feeds everything downstream), the afm configuration authorship, and the
  launch environment composition around the afm launch. The config amendment
  delivery of the load moment follows the one-action-two-moments rule — the
  same action, vocabulary, and semantics as the host delivery; each side
  consumes only the fields it owns. Use `project-configuration` for the
  authored load, `afm` for the written
  configuration-file contract, `resolve-wrapper-path` for the agent command
  resolution, and `extra-env-carriage` for the CLI entries carriage of the
  launch layer.

  The listing, overview, and card forms load no configuration and deliver no
  checkpoint — only the run form performs the load-and-amend. Docker-level
  fields of the effective configuration (image, proxy, hosts) are
  applied-but-unconsumed in-container — silently, summary lines only.

  The cell runs inside the goga Docker image when invoked through
  python -m goga.pipeline; the host-side launcher lives in
  goga/commands/pipeline (docker runtime boundary — no Python Imports).

  Use the `argparse` practice for the in-container CLI parsing and the
  `cli_entrypoint` practice for the thin __main__ delegation with the
  in-container docker guard (`ensure-in-docker`).

  Compilation step: run coordination invokes `compile_flow` (see
  `compile-flow`) to transform the discovered pipeline-file into an afm
  flow-file at runtime inside the container; use `parse-dsl` and
  `serialize-flow` for the intermediate stages when a lower-level view is
  required. Use `memory-emission` for the compiled memory surface of the
  flow-file — the top-level memory block and the per-stage memory keys.

  Prompt materialization step: after compilation, run coordination
  materializes the four default agent prompt files (per `default_prompts`)
  into <AFM_DIR>/prompts/ and applies per-role inline overrides.

  Informational surface: the overview lists every discovered pipeline with
  its description from the DSL header; the card reports the name, the
  description, and the post-workflow stage composition produced by the same
  compilation machine a run uses. The card applies the CLI skip names through the same in-memory merge a run
  applies, and workflow-file skip directives arrive through the shared
  compilation machine — both are part of the composition a run with the same
  flags executes. The card writes nothing into the project or
  runtime directories.

---

"PipelineEntry(name: str, source: PipelineSource)":
  location: pipeline_entry.py
  annotations: |
    Describe a single pipeline-file discovered by `list_pipelines`: its `name`
    and where it comes from (`source`).

    `name`: pipeline name without extension (e.g. "deploy"); the .yml extension
            is implied and never stored here
    `source`: origin of the pipeline — `PipelineSource` enum value

    Build the data model with the standard library dataclasses module (NOT
    pydantic, per project convention; pydantic is treated as tech debt). Use
    @dataclass(kw_only=True) and validate `name` at construction time
    (raising ValueError on invalid input).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - `name` must not contain path separators ("/", "\\") or the .yml extension
    - `name` must not be empty

  properties:
    "name -> str": |
      Pipeline name without extension.
    "source -> PipelineSource": |
      Origin of the pipeline: `PipelineSource`.PROJECT for project-level
      <cwd>/.goga/pipelines/, `PipelineSource`.USER for user-level ~/.goga/pipelines/.

"PipelineSource()":
  location: pipeline_entry.py
  annotations: |
    str-backed Enum declaring the origin of a pipeline-file.

    Modeled via the standard library enum module; str-mixin so values
    serialize as plain strings (per the `convention` practice).

  properties:
    "PROJECT = \"project\"": |
      Origin = project-level <cwd>/.goga/pipelines/.
    "USER = \"user\"": |
      Origin = user-level ~/.goga/pipelines/.

"PipelineSummary(name: str, source: PipelineSource, description: str, display_name: str = \"\")":
  location: pipeline_summary.py
  annotations: |
    Describe one row of the overview: the discovered pipeline name, its
    origin, its DSL header description, and the authored header name.

    `name`: pipeline name without extension
    `source`: origin of the pipeline — `PipelineSource` value
    `description`: pipeline description taken from the DSL header
    `display_name`: authored pipeline name from the DSL header; may differ
                    from `name`; defaults to an empty string

    Build with the standard library dataclasses module and
    @dataclass(kw_only=True) (per `convention`).

    Requirements:
    - Use @dataclass(kw_only=True)
    - `name` follows the `PipelineEntry` validation rules — non-empty, no
      path separators, no .yml suffix
    - `display_name` defaults to an empty string — constructions without it
      remain valid

  properties:
    "name -> str": |
      Pipeline name without extension.
    "source -> PipelineSource": |
      Origin of the pipeline.
    "description -> str": |
      Description from the DSL header.
    "display_name -> str": |
      Authored pipeline name from the DSL header; may differ from the
      discovered stem.

"PipelineCard(name: str, description: str, stages: list[CardStage], provenance: list[str] = [])":
  location: pipeline_card.py
  annotations: |
    Describe the card of a single pipeline: the authored name and
    description, the ordered stage rows, and the tools whose contributions
    shaped the composition.

    `name`: pipeline name from the DSL header
    `description`: pipeline description from the DSL header
    `stages`: stage rows in execution order — one per compiled stage
    `provenance`: the tools whose contributions committed into the
                  composition, in enumeration order; empty when none
                  contributed

    Build with the standard library dataclasses module and
    @dataclass(kw_only=True) (per `convention`).

    Requirements:
    - Use @dataclass(kw_only=True)
    - `provenance` defaults to an empty list — constructions without it
      remain valid

  properties:
    "name -> str": |
      Pipeline name from the DSL header.
    "description -> str": |
      Pipeline description from the DSL header.
    "stages -> list[CardStage]": |
      Stage rows in execution order; loop-expanded copies appear as
      separate rows.
    "provenance -> list[str]": |
      The tools whose contributions committed into the composition, in
      enumeration order; empty when none contributed.

"CardStage(id: str, title: str)":
  location: pipeline_card.py
  annotations: |
    Describe one stage row of the card.

    `id`: stage identifier
    `title`: stage display title

    Build with the standard library dataclasses module and
    @dataclass(kw_only=True) (per `convention`).

    Requirements:
    - Use @dataclass(kw_only=True)

  properties:
    "id -> str": |
      Stage identifier.
    "title -> str": |
      Stage display title — the display name of the compiled `FlowStage`.

"list_pipelines(project_dir: Path, user_dir: Path) -> entries: list[PipelineEntry]":
  location: list_pipelines.py
  annotations: |
    Discover pipeline files across two source directories and return them as
    `entries`: a list of `PipelineEntry`-s.

    `project_dir`: project-level pipelines directory (typically <cwd>/.goga/pipelines/)
    `user_dir`: user-level pipelines directory (typically ~/.goga/pipelines/)
    `entries`: list of `PipelineEntry`-s, one per unique pipeline name

    Algorithm:
    1. Scan flat *.yml files (non-recursive) in `project_dir`; for each valid
       stem, record a `PipelineEntry` with the name set to the stem and source
       set to `PipelineSource`.PROJECT. Skip stems that fail `PipelineEntry`
       validation (invalid chars, .yml suffix, empty) silently.
    2. Scan flat *.yml files (non-recursive) in `user_dir`; for each valid stem
       not already present from step 1, record a `PipelineEntry` with source
       set to `PipelineSource`.USER.
    3. Return the combined `entries` list.

    Apply the `convention` practice for the filesystem scanning code
    (relative imports, logging, docstring style).

    Requirements:
    - Scan only the top level of each directory — do not descend into subdirectories
    - Drop the .yml extension when forming the entry name
    - A missing source directory is treated as empty (no error)
    - Skip stems that fail `PipelineEntry` validation silently — they are not pipelines

    Constraints:
    - Do not parse or validate the contents of pipeline files — only their names
      matter here
    - When a name exists in both sources, the project source wins (no duplicate
      entries)

"describe_pipelines(project_dir: Path, user_dir: Path) -> summaries: list[PipelineSummary]":
  location: describe_pipelines.py
  annotations: |
    Compose the overview of every discovered pipeline: each entry paired with
    the authored name and description from its DSL header.

    `project_dir`: project-level pipelines directory (absolute)
    `user_dir`: user-level pipelines directory (absolute)
    `summaries`: one `PipelineSummary` per discovered pipeline, in discovery
                 order

    Apply `convention` for filesystem reading, logging, and docstring style.

    Algorithm:
    1. Discover entries via `list_pipelines`
    2. For each entry: compose the pipeline-file path inside its source
       directory, read the file, parse it via `parse_dsl`, and take the header
       name and description
    3. Build a `PipelineSummary` from the entry name, entry source, the
       header description, and the authored header name (the summary
       display_name field)
    4. Return the summaries in discovery order

    Requirements:
    - The description and the authored header name (summary display_name
      field) come from the DSL header; the name is the discovered stem
    - A damaged pipeline file (unreadable or structurally invalid) aborts the
      whole overview with a readable error — no partial list, no silent
      skips, no placeholder markers
    - Read the filesystem only; write nothing

    Constraints:
    - Do not sort or filter the summaries — discovery order is preserved
    - Do not compile pipeline files — the overview reads headers only
    - Do not extend the discovery contract — names and sources come from
      `list_pipelines` unchanged

"resolve_workflow(pipeline_name: str, workflow_name: str | None, no_workflow: bool) -> workflow: WorkflowDocument | None":
  location: resolve_workflow.py
  annotations: |
    Resolve the optional workflow for a pipeline: the single rule set shared
    by run coordination and the card.

    `pipeline_name`: pipeline name without extension (the basename auto-match
                     key)
    `workflow_name`: optional explicit workflow name (without the .yml
                     extension)
    `no_workflow`: when True, workflow application is disabled
    `workflow`: the parsed `WorkflowDocument`, or None when no workflow applies

    Apply `parse-workflow` for the accepted structure and the parse contract.
    Apply `convention` for docstring style.

    Algorithm:
    1. When `no_workflow` is True — return None
    2. Compose the workflow-file path under <cwd>/.goga/workflows/: named
       `workflow_name`.yml when provided (an empty string counts as not
       provided), otherwise the pipeline basename `pipeline_name`.yml
    3. Containment guard: when the composed path resolves outside the
       <cwd>/.goga/workflows/ directory (a ".." segment or an absolute
       prefix in the name) — return None (silent miss; workflow files are
       project-only, never resolved into the wider filesystem)
    4. When the composed file exists — parse it via `parse_workflow` and
       return the resulting `WorkflowDocument`; structural errors propagate
       with their readable messages
    5. Otherwise — return None (silent miss; an absent workflow-file is not an
       error)

    Requirements:
    - The resolution rules are exactly: disabled → None; explicit name → that
      file (an empty string counts as no name — the basename auto-match
      applies); otherwise basename auto-match; a name escaping the workflows
      directory → None; missing file → None
    - Workflow files are project-only — the directory resolves from the
      current working directory, and a composed path outside it is a silent
      miss, never a traversal into the wider filesystem

    Constraints:
    - Do not treat a missing workflow-file as an error — the auto-match is
      opt-in
    - Do not validate workflow contents beyond what `parse_workflow`
      enforces
    - Do not read environment variables — callers own their flag sources

"order_stages(stages: list[FlowStage]) -> ordered: list[FlowStage]":
  location: order_stages.py
  annotations: |
    Order flow stages for execution: a topological sort over the depends_on
    references of each `FlowStage`.

    `stages`: flow stages in declaration order
    `ordered`: the same stages ordered for execution

    Apply `convention` for docstring style.

    Algorithm:
    1. Build the ordering graph from the depends_on references of each
       `FlowStage`
    2. Repeatedly emit, among the stages whose references are all already
       emitted, the one earliest in declaration order
    3. Treat a reference naming no stage in the list as satisfied — dangling
       references are the flow executor's concern, not an ordering error
    4. When no remaining stage qualifies (a reference cycle), append the
       remaining stages in declaration order

    Requirements:
    - Deterministic — declaration order is the tie-break at every step and
      the fallback under cycles
    - The result contains every input stage exactly once — none dropped,
      none duplicated
    - Pure — the input list and its stages are not mutated

    Constraints:
    - Do not validate references for dangling ids, cycles, or duplicates
    - Do not rewrite depends_on — ordering only

"describe_pipeline(name: str, project_dir: Path, user_dir: Path, workflow: str | None, no_workflow: bool, skip: list[str] | None = None) -> card: PipelineCard":
  location: describe_pipeline.py
  annotations: |
    Compose the card of a single pipeline: name, description, the
    post-workflow stage composition as it would execute, and the tools
    that contributed to it.

    `name`: pipeline name without extension
    `project_dir`: project-level pipelines directory (absolute)
    `user_dir`: user-level pipelines directory (absolute)
    `workflow`: optional explicit workflow name (without the .yml extension)
    `no_workflow`: when True, workflow application is disabled
    `skip`: stage names to skip — merged exactly as a run with the same flags
            merges them; None and an empty list both mean no skip
    `card`: `PipelineCard` — the pipeline name and description from the DSL
            header, one `CardStage` per stage in execution order, and the
            provenance of the amendment

    Apply `compile-flow` for the compilation contract and the documents tuple.
    Apply `checkpoints` for the amendment delivery of the pipeline hooks
    zone.
    Apply `topic-paths` for the work identity resolution behind the
    amendment facts.
    Apply `convention` for docstring style and intra-package imports.

    Algorithm:
    1. Discover entries via `list_pipelines` and locate the matching name;
       on no match report the missing pipeline with a readable error
    2. Resolve the workflow via `resolve_workflow` with the pipeline name and
       the workflow flags
    3. Apply the `skip` names via `apply_skip_stages` onto the resolved
       workflow (None/empty — no skip)
    4. Resolve the amendment facts (the `PipelineIdentity` — the authored
       header name and description read via `parse_dsl` from the
       pipeline-file text; the `WorkflowDecision`; and the `WorkIdentity` —
       the current branch via `resolve_current_branch_name`, the literal
       "unknown" when it resolves None; the hosting topic slug and year via
       `resolve_topic_dir` when its directory exists, the branch-only form
       otherwise) and, unless the decision is disabled, deliver the
       amendment via the `PipelineHooks` checkpoint surface with the merged
       workflow — receiving the overlay result; a disabled decision delivers
       nothing and the overlay is the passthrough `WorkflowOverlay` of the
       merged workflow
    5. Compile the pipeline-file via `compile_flow` into a temporary flow-file
       located in a system temporary directory — outside the project
       directory and outside every runtime directory — with the overlay
       workflow, and receive the documents tuple
    6. Order the compiled stages via `order_stages`
    7. Build the card: name and description from the parsed pipeline document
       header; one `CardStage` per ordered stage — id from the `FlowStage`
       id, title from the `FlowStage` name (the display label); the card
       provenance from the overlay
    8. Discard the temporary flow-file and return the card

    Requirements:
    - The stage composition equals the composition a run of the same pipeline
      with the same workflow and skip flags would execute — the same
      amendment layer with the same precedence and the same compilation
      machine produce both
    - The card names the contributing tools — the provenance is empty when
      nothing contributed
    - An explicit workflow disable turns the layer off — no delivery
      happens and the raw authored DSL composes
    - A silent auto-match miss keeps the layer active — the delivery runs
      onto the empty base
    - Loop-expanded stage copies appear as separate stages
    - The CLI skip names apply through the same in-memory merge a run applies
      and are reflected in the stage list; workflow-file skip directives
      apply through the shared compilation machine — both compose exactly
      as a run with the same flags
    - The temporary flow-file lives outside the project and runtime
      directories and is removed afterwards
    - The card name and description are the authored DSL header values

    Constraints:
    - Do not launch afm and do not run any stage — the card is read-only
    - Do not emit run events in card form
    - Do not write into the project directory or any runtime directory
    - Do not re-parse the pipeline-file for the card fields — the card's
      name and description come from the documents tuple; the single early
      `parse_dsl` read of step 3 serves the amendment facts only
    - Do not reorder stages beyond `order_stages`

"run_pipeline(name: str, project_dir: Path, user_dir: Path, port: int, workflow: str | None = None, no_workflow: bool = False, skip: list[str] | None = None, parallel: int | None = None) -> exit_code: int":
  location: run_pipeline.py
  annotations: |
    Load and amend the project configuration, resolve a pipeline name to an
    absolute file path via `list_pipelines`, resolve an optional workflow via
    `resolve_workflow` from the CLI-provided decision, merge the CLI skip
    names via `apply_skip_stages`, deliver the workflow amendment through the
    pipeline hooks zone, compile the pipeline-file (extended by the effective
    workflow) into an afm flow-file at runtime via `compile_flow`, materialize
    the four agent prompt files (defaults plus inline overrides) into the
    runtime prompts directory, write the afm configuration file via
    `write_afm_config`, emit the run-creation facts, launch afm through
    `run_flow` with the composed launch environment layer, and emit the
    run-completion facts on its return. This is the run coordination routine
    — it performs the configuration load-and-amend, discovery, workflow
    resolution, path resolution, fact resolution, amendment delivery,
    compilation, prompt materialization, and afm configuration authorship;
    the actual subprocess execution lives in `run_flow`.

    `name`: pipeline name without extension
    `project_dir`: project-level pipelines directory (same meaning as in `list_pipelines`)
    `user_dir`: user-level pipelines directory (same meaning as in `list_pipelines`)
    `port`: TCP port forwarded to afm run --port via `run_flow`
            (allocated by the host-side caller)
    `workflow`: optional explicit workflow name (without the .yml extension) —
                the CLI decision from the -w flag; None with `no_workflow`
                unset resolves the basename auto-match
    `no_workflow`: when True, workflow application is disabled — the CLI
                   decision from the --no-workflow flag
    `skip`: stage names to skip — the CLI decision from the repeatable -s
            flags; None and an empty list both mean no skip
    `parallel`: optional cap on concurrently executing stages, forwarded to
                `run_flow` as its max_parallel argument. When None (default) —
                afm runs unbounded (run_flow omits --max-parallel). Read from
                the in-container CLI --parallel flag
    `exit_code`: 0 on success, non-zero on error (missing pipeline, missing
                 binary, afm failure, structural DSL error, workflow parse
                 error, materialization error, configuration load or delivery
                 failure). 127 means afm is not on PATH inside the container.

    Apply `convention` for error-handling style and docstring formatting.
    Apply `project-configuration` for the authored configuration load.
    Apply `checkpoints` for the workflow amendment delivery and the two
    emissions of the pipeline hooks zone.
    Apply `parse-workflow` for the workflow-file contract consumed through
    `resolve_workflow`.
    Apply `compile-flow` for the compilation step contract and the documents
    tuple.
    Apply `default_prompts` for resolving the packaged default prompt files.
    Apply `afm` for the written afm configuration-file contract.
    Apply `resolve-wrapper-path` for the agent command resolution inside
    `write_afm_config`.
    Apply `run-flow` for the subprocess launch contract and its environment
    layer parameter.
    Apply `extra-env-carriage` for the CLI entries carriage of the launch
    layer.
    Apply `topic-paths` for the work identity resolution and
    `topic-statuses` for the status facts of the run events.

    Algorithm:
    1. Load the authored project configuration via `load_project_config` —
       a load failure returns with a clean error before any work; no events
       fire
    2. Deliver the config amendment via the `ConfigHooks` checkpoint surface
       with the authored configuration — the first failing tool stops the
       run with a clean error naming the tool and the action (no events
       fire, afm never launches); print the amendment summary lines of the
       `ConfigOverlay` to stderr; every downstream step consumes the
       effective configuration of the `ConfigOverlay`
    3. Discover pipelines via `list_pipelines` and find the entry whose name
       matches
    4. If no match — report that the pipeline is missing and return a
       non-zero exit code
    5. Build the absolute pipeline path from the matching entry's source
       directory and the pipeline name
    6. Resolve the in-container runtime directory from the AFM_DIR
       environment variable; when unset raise a readable "AFM_DIR not set"
       error; resolve the value to an absolute path
    7. Compose the output flow path inside that directory
    8. Resolve the workflow via `resolve_workflow` with the pipeline name and
       the `workflow` / `no_workflow` parameters — the CLI decision arrives
       as explicit arguments, never from the environment
    9. Apply the `skip` names via `apply_skip_stages` onto the resolved
       workflow (None/empty — no skip)
    10. Resolve the amendment facts from the operation's own data: the
        `PipelineIdentity` (discovered name; authored header name and
        description read via `parse_dsl` from the pipeline-file text; entry
        source), the `WorkflowDecision` (disabled /
        explicit / auto-match / silent-miss with the resolved name), and the
        `WorkIdentity` (current branch via `resolve_current_branch_name`,
        the literal "unknown" when it resolves None; the hosting topic slug
        and year via `resolve_topic_dir` when its
        directory exists, the branch-only form otherwise)
    11. Unless the decision is disabled, deliver the amendment via the
        `PipelineHooks` checkpoint surface with the authored workflow after
        the skip merge — receiving the overlay result; a disabled decision
        delivers nothing (the layer is off) and the overlay is the
        passthrough `WorkflowOverlay` of the merged workflow
    12. Resolve the in-container project name via `resolve_project_name`
        (None when the git origin remote is unavailable). Compile via
        `compile_flow` with the overlay workflow (the resolved workflow when
        the layer is off), the in-container project root (Path.cwd()) as
        root_dir, and the project name; receive the documents tuple;
        structural errors propagate unchanged
    13. Materialize agent prompts atomically (validate-all, then wipe, then
        write): resolve the default prompts directory per `default_prompts`;
        for each overridable role (planner, executor, reviewer) require an
        inline override from the documents tuple or an existing default file
        (stem via `translate_role`); require the summary default; then reset
        <AFM_DIR>/prompts/ and write exactly four files — overrides where
        present, defaults otherwise, summary always from the default
    14. Write the afm configuration file via `write_afm_config` with the
        effective pipeline agent — the whole file at the fixed in-container
        home path, before the launch
    15. Order the compiled stages via `order_stages` and build one
        `CompositionStage` per ordered stage — id from the `FlowStage` id,
        title from the `FlowStage` name
    16. Resolve the work statuses — the maximal present statuses of the
        hosting topic via `assemble_status_scale` and `resolve_topic_status`;
        an empty list in the branch-only form
    17. Compose the afm launch environment layer per `extra-env-carriage`:
        decode the CLI entries payload via `decode_extra_env` from the
        dedicated engine variable; apply the decoded entries above the
        effective pipeline.env task env layer; drop the entries whose keys
        are engine variables — silently. A damaged payload is one clean
        error here — no events fire, afm never launches
    18. Emit the run-creation facts via the checkpoint surface immediately
        before the launch: the identity, the decision, the overlay, the
        composition, the work identity, the statuses, and the runtime dir as
        a posix string
    19. Launch afm via `run_flow` with the compiled flow-file path, `port`,
        max_parallel=`parallel`, and the composed layer as its env parameter
        — the process environment of the container stays untouched
    20. On every return of `run_flow` — zero, non-zero, and spawn failures
        alike — recompute the work statuses at the completion moment and
        emit the run-completion facts with the actual exit code
    21. Return the exit code returned by `run_flow`

    Requirements:
    - The environment reads are exactly two: AFM_DIR and the CLI entries
      payload engine variable — the workflow decision and the skip names
      arrive as parameters
    - Always pass the absolute pipeline path to `compile_flow` — never the
      bare name
    - Always pass the absolute compiled flow path to `run_flow` — never the
      bare name or the DSL path
    - Always forward `port` to `run_flow`; forward `parallel` (None
      propagates — no --max-parallel flag)
    - A configuration load failure and a configuration delivery failure fire
      no events — the return happens before the pipeline checkpoints; afm
      never launches on a delivery failure
    - The amendment summary lines of the config delivery print to stderr
      (nothing when empty)
    - Docker-level fields of the effective configuration (image, proxy,
      hosts) are consumed nowhere in-container — applied-but-unconsumed,
      silently, no warning
    - `no_workflow` takes precedence over `workflow` (the host-side launcher
      rejects the combined use; the precedence is defensive)
    - Workflow resolution and parsing go through `resolve_workflow` — one
      rule set shared with the card
    - The amendment delivery and its precedence are the same rule set the
      card applies — the same flags compose the same overlay in both forms
    - A missing workflow-file is a silent miss, not an error; the layer
      stays active onto the empty base
    - An explicit disable turns the layer off — no delivery happens and the
      raw authored DSL composes
    - Skip merges onto any resolved workflow and applies to a workflow-less
      pipeline; unknown skip names surface as the compiler's structural
      error
    - The branch, topic, and status facts resolve in the operation before
      the delivery — the checkpoints read nothing
    - The "run_created" action fires immediately before the runner launch;
      the "run_completed" action fires on every launch-attempt return path
      — no exit path skips the completion emission
    - A missing pipeline, a structural composition error, and a
      configuration load or delivery failure fire no events — the return
      happens before the checkpoints
    - A damaged CLI entries payload is one clean error before the
      run-creation facts — no events fire, afm never launches
    - The runtime dir fact is the resolved AFM_DIR path as a posix string
    - With no tool packages installed the overlay is the passthrough — the
      compiled workflow, the prompts, and the output behave exactly as with
      the amendment layer absent
    - <AFM_DIR>/prompts/ contains exactly four files after step 13 succeeds;
      validation precedes any wipe or write — atomicity guarantees no
      partial state on disk
    - The afm configuration file is written exactly once per run, before
      the launch, via `write_afm_config`
    - The launch layer applies to the afm subprocess only, through the env
      parameter of `run_flow` — engine-variable keys never enter the layer,
      and the container's process environment is never mutated
    - Inline prompt overrides come exclusively from the documents tuple
      header roles; the override is a full file replacement — no merge, no
      concatenation
    - Default prompt files resolve from the installed package location per
      `default_prompts` — never from AFM_DIR, CWD, or an environment
      variable
    - root_dir resolution is CWD-based (Path.cwd() resolves to /workspace
      inside the goga container); project_name resolves in-container via
      `resolve_project_name`
    - Do not mutate the documents tuple returned by `compile_flow` — read
      the inline prompt overrides as-is

    Constraints:
    - Do not load the configuration outside `load_project_config` followed
      by the checkpoint delivery — one load, one delivery, one effective
      configuration
    - Do not invoke afm directly outside `run_flow`
    - Do not invoke the compiler outside `compile_flow`
    - Do not invoke the workflow parser outside `parse_workflow`
    - Do not allocate the port — the caller allocates it
    - Do not default `parallel` — None means unbounded
    - Do not accept relative `project_dir` or `user_dir`
    - Do not mask or wrap exceptions from `compile_flow` or `parse_workflow`
      — structural errors propagate with their readable messages
    - Do not resolve git or topic facts inside a hook delivery — the
      operation owns the resolution
    - Do not skip the completion emission on any launch-attempt return path
    - Do not write prompts inside the project directory or /workspace —
      always <AFM_DIR>/prompts/
    - Do not write the afm configuration anywhere but the fixed in-container
      home path — never in the project directory, never in the afm state
      directory
    - Do not write inline prompt overrides into the compiled flow-file
    - Do not delete skipped stages or rewrite depends_on — `compile_flow`
      does both
    - Do not write or generate a workflow-file for skip — the merge is
      in-memory only
    - Do not mutate the process environment — composed layers travel only
      through the env parameter of `run_flow`
    - Do not print configuration values — summary lines carry the tool, the
      path, set or forced only

"write_afm_config(agent: str | None) -> config_path: Path":
  location: afm_config.py
  annotations: |
    Write the whole afm configuration file in-container before the afm
    launch: the agent client command resolved from the effective pipeline
    agent, plus the four static launcher-side fields.

    `agent`: the effective pipeline agent — the authored pipeline.agent as
             amended by the config delivery; None when no agent resolves
    `config_path`: the fixed in-container path of the written file

    Apply `convention` for docstring style and intra-package imports.
    Apply `afm` for the configuration-file contract — the field set, the
    fixed home path, and the per-stage override relationship.
    Apply `resolve-wrapper-path` when resolving the agent command.

    Algorithm:
    1. Resolve the absolute wrapper path via `resolve_wrapper_path` — only
       when `agent` is not None
    2. Compose the content: the client command carrying the resolved
       absolute wrapper path (never a bare agent name) — written only when
       an agent resolved, omitted otherwise so per-stage workflow agents or
       the binary's own defaults cover the absent global default; plus the
       four static fields — the dashboard theme, the disabled browser
       launch, the disabled internal proxy provider, and the in-container
       prompts directory
    3. Write the whole file to the fixed in-container home configuration
       path — the path is independent of the afm state directory variable
    4. Return the written path

    Requirements:
    - The whole file is rewritten on every run — a repeated invocation
      stays safe
    - The four static fields are constants — never configurable through
      the project configuration or the CLI
    - The content is serialized programmatically — never hand-escaped as a
      string
    - The file lands only at the fixed home configuration path — never in
      the project directory, never in the afm state directory

    Constraints:
    - Do not write a bare agent name as the client command — always the
      resolved absolute wrapper path
    - Do not derive the prompts directory value from an argument or a
      configuration field — the value is fixed
    - Do not create or manage the prompts directory — the run coordination
      materializes it
    - Do not print any part of the written content

"apply_skip_stages(workflow: WorkflowDocument | None, skip_stages: list[str]) -> workflow: WorkflowDocument | None":
  location: apply_skip_stages.py
  annotations: |
    Pure in-memory merge of CLI skip directives into a workflow document.
    Each name in `skip_stages` is applied as a `WorkflowStage` carrying
    skip=True over the workflow's stages map, so the downstream `compile_flow`
    removes those stages and transparently reconnects their dependents'
    depends_on. This routine does NOT delete stages or rewrite depends_on — it
    only prepares the declarative workflow that the compiler consumes.

    `workflow`: optional `WorkflowDocument` resolved by `run_pipeline` (parsed
                from a workflow-file via `parse_workflow`, or None when no
                workflow resolved)
    `skip_stages`: stage names to skip (the repeatable -s/--skip flag names
                   passed by run coordination and the card); an empty list
                   is a no-op
    `workflow`: the resulting `WorkflowDocument` carrying the skip directives,
                or the input unchanged when `skip_stages` is empty (None stays
                None — `compile_flow` then runs with no workflow)

    Algorithm:
    1. When `skip_stages` is empty — return `workflow` unchanged (None stays
       None; no skip applied)
    2. Build a new stages map: start from a copy of the stages map of `workflow`
       when `workflow` is not None, otherwise an empty dict
    3. For each name in `skip_stages` — set stages[name] = a `WorkflowStage`
       carrying skip=True (skip wins over any pre-existing entry for that name;
       the compiler removes the stage before applying overrides)
    4. Return a NEW `WorkflowDocument`: prompt = the prompt of `workflow` when
       `workflow` is not None else None; stages = the new map; extend = a copy
       of the extend map of `workflow` when `workflow` is not None else the
       default empty map; memory = the memory field of `workflow` when
       `workflow` is not None else None — carried verbatim. The input
       `workflow` and its maps are NOT mutated

    Requirements:
    - Empty `skip_stages` is a no-op — return the input unchanged
    - Skip always wins — a name present in both the workflow stages and
      `skip_stages` is replaced with a `WorkflowStage` carrying skip=True
    - Do not mutate the input `workflow` or its stages/extend maps — build a
      new `WorkflowDocument` and a new stages map
    - When `workflow` is None and `skip_stages` is non-empty — construct a
      `WorkflowDocument` whose stages map carries only the skip entries (prompt
      None, extend empty); skip applies to a workflow-less pipeline
    - Construct `WorkflowStage` with skip=True and all other fields at their
      defaults — notes stays None (the model field default)
    - The memory configuration of the input workflow survives the rebuild
      verbatim — a rebuild that drops it would silently disable memory
      participation for skip-driven runs
    - Stage-name validation is NOT performed here — the compiler's strict check
      raises a structural error on a name absent from the pipeline body; this
      routine stays declarative
    - Apply `convention` for code style and docstring formatting

    Constraints:
    - Do not delete stages or rewrite depends_on here — the compiler does both
    - Do not validate stage names against any pipeline here
    - Do not write, read, or generate any workflow-file — the merge operates
      purely on in-memory Python objects
    - Do not mutate the input `workflow` object or its maps
    - Do not interpret, validate, or rebuild the memory configuration — it is
      carried as an opaque value (per `memory`)

"pipeline_cli(argv: list[str]) -> exit_code: int":
  location: cli.py
  annotations: |
    In-container CLI implementation for python -m goga.pipeline. Parses argv
    via argparse and dispatches to one of four operations: the flat listing
    (`list_pipelines`), the overview (`describe_pipelines`), the card
    (`describe_pipeline`), or run coordination (`run_pipeline`). Invoked
    through the runpy entrypoint in __main__.py — never imported by Python
    from the host side (runtime docker boundary, no Imports).

    `argv`: argument list (typically the process argv minus the program name)
    `exit_code`: 0 on success, 2 on argparse error, non-zero on operation
                 failure

    Apply the `argparse` practice for parser construction.
    Apply the `cli_entrypoint` practice: this routine is defined in the cli
    module; __main__ only delegates to it.
    Apply `convention` for docstring style and intra-package imports.

    Algorithm:
    1. Build the parser with two subcommands:
       - list: an optional --info/-i flag
       - run: a required name positional; an optional --info/-i flag; optional
         --workflow/-w NAME and --no-workflow flags and a repeatable
         --skip/-s NAME flag (all three available with and without --info);
         a --port PORT integer required only when --info is absent; an
         optional --parallel N integer
    2. Parse argv; on argparse error exit with code 2
    3. Resolve the project pipelines directory (<cwd>/.goga/pipelines/) and
       the user pipelines directory (~/.goga/pipelines/)
    4. Dispatch:
       - list without --info — call `list_pipelines`; print the flat list:
         one bullet line per entry — the marker line "* <name>" (with the
         " (project)" suffix for project source entries); return 0
       - list with --info — call `describe_pipelines`; print the overview: one
         bullet block per pipeline — the marker line "* <name>" (with the
         " (project)" suffix for project source entries) followed by indented
         "name:" and "description:" field lines; return 0
       - run with --info — call `describe_pipeline` with the name, the parsed
         workflow flags, and the parsed skip names; print the card: "name:" and "description:"
         field lines, a blank line, a "---" separator, a blank line, then one
         bullet block per ordered stage — the marker line "* <id>:" and an
         indented "title:" field line; return 0
       - run without --info — call `run_pipeline` with name, port, the parsed
         workflow flags, the parsed skip names, and parallel; return its exit
         code
    5. Render an operation failure as a clean readable message to stderr (no
       traceback) and return a non-zero exit code

    Requirements:
    - The run subcommand accepts --info/-i, --workflow/-w NAME,
      --no-workflow, the repeatable --skip/-s NAME, and --parallel N; the
      workflow and skip flags bind in both info and run modes
    - --port is an integer option required only when --info is absent; a
      missing --port without --info is an argparse-style error (exit 2);
      --port is ignored in info mode
    - Long and short flag forms behave identically
    - The flat list template: one marker line "* <name>" per pipeline (with
      the " (project)" suffix for project-source entries) and no header line
      (example: "* deploy (project)" / "* rollback")
    - The overview template: the marker line "* <name>" (with the
      " (project)" suffix for project-source entries) followed by "name:"
      and "description:" field lines indented by four spaces; the name field
      carries the authored header name, the description field the header
      description (example: "* deploy (project)" / "    name: Deploy" /
      "    description: Deploy the service")
    - The card template: a "name:" line, a "description:" line, a blank line,
      a "---" separator, a blank line, then per ordered stage the marker line
      "* <id>:" and a "title:" line indented by four spaces; the separator
      block is printed even when the card carries no stages; when the card
      provenance is non-empty, one blank line and one "tools:" field line
      follow the stage blocks — the contributing tools comma-separated in
      provenance order; an empty provenance adds nothing — the output stays
      byte-identical to the provenance-free card
    - An empty discovery is not an error: the flat list prints nothing —
      zero discovered pipelines, zero lines; the overview likewise prints
      nothing; both return 0
    - Card stage bullet blocks follow execution order; loop-expanded copies
      appear as separate blocks
    - Overview and card output stays within terminal width — no horizontal
      scrolling

    Constraints:
    - Do not allocate a port inside this CLI — run mode receives it from the
      host-side launcher
    - Do not validate workflow flag combinations or skip stage names — the
      host-side launcher owns the flag-combination validation and the
      compiler owns the stage-name error
    - Do not print tracebacks for operation failures — clean stderr messages
    - Do not import this cell from the host side — invoke via docker only

---

Author: Goga
CreatedAt: 19/08/26
Description: |
  Cell that owns the pipeline workflow: discovery of *.yml pipeline files
  across the project and user pipeline directories, the pipeline-file entity
  model, the informational surface (overview and card), run coordination
  with the in-container run parameters — the configuration load-and-amend,
  the afm configuration authorship, and the launch environment composition —
  and the in-container CLI entrypoint pipeline_cli.
