Imports:
  - Types:
      - WorkflowDocument
      - WorkflowStage
      - WorkflowExtendStage
    Usages:
      - parse-workflow
    From: goga/pipeline/workflow

Usages:
  convention: .goga/usages/conventions.md
  beautiful_yaml: .goga/usages/cooks/beautiful_yaml.md

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 is a pure transformer: it reads a pipeline-file written in goga
  DSL (phases-list or stages-map) and writes an equivalent afm flow-file
  (flat YAML with top-level prompt (when present), top-level root_dir (when
  supplied by the caller), name, description, and a stages list). It performs
  no I/O beyond the two paths it receives and the optional workflow-file
  instruction source, performs no network or subprocess calls, and performs
  no environment-variable reads — the caller supplies the root_dir value
  explicitly.

  The cell imports two types from goga/pipeline/workflow: `WorkflowDocument`
  (consumed as the optional workflow argument of `compile_flow`) and
  `WorkflowStage` (the value type of the workflow stages map, referenced in
  the workflow reconstruction algorithm). The import is one-directional —
  this cell consumes the declarative workflow instructions; the workflow cell
  never imports from this cell. The cell does NOT call parse_workflow itself
  — the consumer (the run_pipeline routine in goga/pipeline) parses the
  workflow-file and passes the resulting `WorkflowDocument` to
  `compile_flow` via its optional workflow parameter. When workflow is None,
  no workflow is applied and the output carries no top-level prompt and no
  per-stage overrides.

  Use `beautiful_yaml` for the YAML serialization parameters applied by
  `serialize_flow`; a custom yaml.SafeDumper subclass extends these
  defaults to enforce the canonical key order, the flow-style for agents,
  the block-style for skills and depends_on, and the block-literal style for
  the top-level prompt.

  Use `convention` for code style, dataclass usage, and test layout.

  The input pipeline-file is segmented by a literal three-dash line: the
  segment before is the header (name, description, optional roles), the
  segment after is the body (either a YAML list for phases or a YAML dict
  for stages). The output flow-file is NOT segmented — it is a single flat
  YAML document with optional prompt, name, description, and stages.

  `BodyFormat` detection is structural: a list body yields PHASES
  (auto-generates depends_on by position), a dict body yields STAGES (passes
  depends_on through as-is, then rewrites after workflow loop-expansion when
  a workflow is applied). Any other body shape is a structural error.

  Stage-body field translation: the afm interactive field is authored as the
  communication key in a stage body (pipeline-file stage AND embedded
  extend-stage) and translated to the output interactive key in its canonical
  position; an authoring interactive key is a structural error "interactive key
  is forbidden in stage body; use communication". The output canonical key order
  keeps interactive (afm contract stable). All other step content fields
  (command, prompt, description, agents, skills, and any extra fields) are passed
  through verbatim — no validation of their contents, no schema enforcement.
  References in depends_on (dangling ids, cycles, duplicates) are afm's
  responsibility, not this cell's.

  Canonical `FlowStage` fields key order: interactive, auto_approve, command,
  prompt, description, agents, supervisor, supervisor_prompt, skills,
  script_before, script, script_after, then alphabetically-sorted unknown
  keys. command is populated from the agent
  field of `WorkflowStage` (composed as /home/goga/bin/AGENT-as-claude.sh);
  description is populated from the prompt field of `WorkflowStage`. Both
  are independent channels — pipeline-file prompt and workflow-file prompt
  coexist as separate fields in the same stage when both are present.
  supervisor and supervisor_prompt sit between agents and skills so an
  authored supervisor block reads as a continuation of the agents block;
  they appear in their canonical slots ONLY when the source step body
  authors them — they are NO LONGER populated by default injection.

  Default stage-field injection: the input stage-body field for the afm
  agents list is roles (an authoring agents key in a stage body is a
  structural error, raised by `compile_flow`). When a step body has no
  usable roles value (key absent, explicit null, or an empty list), the
  compiler injects
  a SINGLE default into the assembled `FlowStage` fields —
  "agents=[\"auto\"]". auto is a sentinel agent mode: goga emits the
  literal string auto verbatim and does NOT interpret it — the actual
  agent selection is performed on the afm side. supervisor and
  supervisor_prompt are NO LONGER default-injected; they remain valid
  authored fields that pass through to their canonical slots when the
  source step body carries them. An authored non-empty roles value (any
  list with at least one entry) always wins and disables injection
  entirely; its entries are translated to the output agents values via
  `translate_role` (known aliases mapped, all other values passed through
  verbatim).
  The injection lives in `FlowStage` assembly only — the
  `PipelineDocument` body returned to consumers is never affected (it
  stays a faithful mirror of the source pipeline-file). The injection runs
  uniformly on the non-workflow path and on the workflow path (after
  per-stage overrides and loop-expansion).

  The header segment additionally supports an optional roles block
  with three fixed string keys: planner, executor, reviewer. Each
  value is an inline prompt text that fully replaces (not merges with)
  the corresponding default prompt file during pipeline run
  materialization (the planner/executor/reviewer keys map to the
  planning/implementation/review prompt-file stems via `translate_role`).
  summary is NOT an overridable role. An agents key is rejected
  with a structural error by `parse_dsl`. Unknown keys, non-str values,
  and empty mappings are handled by `parse_dsl` per its contract; the
  roles data is carried in `PipelineHeader` (its roles field, of type
  `PipelineRoles`) and never enters the compiled afm flow-file — it is a
  goga-side artifact surfaced to the consumer via `PipelineDocument` (its
  header field) from `compile_flow` return value.

  `compile_flow` returns a tuple of `PipelineDocument` and `FlowDocument`: the
  input representation (`PipelineDocument`, aggregating header+format+body —
  the ORIGINAL parsed body, not the workflow-reconstructed body) and the
  output representation (`FlowDocument`, the afm flow-file model — including
  workflow-applied overrides, loop-expansion, rewritten depends_on, and the
  top-level prompt when a workflow supplied one). The text flow-file is
  written to flow_path as a side effect — the return value is part of the
  signature, not an additional side effect. Consumers access inline
  prompt overrides through `PipelineDocument` header, never through
  `FlowDocument`.

  Workflow application (when `compile_flow` is called with a non-None
  workflow argument): the cell reconstructs the body per the workflow
  instructions BEFORE building the `FlowDocument`. The reconstruction is:
  (0) extend-stages from the workflow's extend map embedded into the body
  and positioned via before/after — BEFORE per-stage overrides,
  loop-expansion, skip-removal, and reference rewriting, so the existing
  machinery applies to extend-stages uniformly. STAGES positions an
  extend-stage by deriving its explicit depends_on from before/after; PHASES
  positions an extend-stage by list insertion (no depends_on field on
  `PhaseStep`).
  (0.45) STRICT VALIDATION of workflow.extend.<name>.before/.after refs
  against the FULL name set (every step name/id present in the ORIGINAL body,
  plus every extend-stage name in workflow.extend): for each before/after ref,
  an unknown ref (naming no original step and no extend-stage) raises a
  structural error "unknown stage name in workflow.extend.<name>.before: <ref>"
  (or .after). The check runs BEFORE the (0) embed (extend names come from
  workflow.extend keys, so cross-references between extend-stages resolve
  without embedding) and BEFORE any skip removal, so a ref to a stage that
  exists in the original body — even if also marked skip: true — is NOT flagged
  (referencing a skipped stage is not a dangling ref). Existence only is
  checked — cycles, self-references, and duplicate refs remain afm's
  responsibility.
  (0.5) STRICT VALIDATION of workflow.stages names against the FULL name set
  (every step name/id present in the ORIGINAL body, plus every extend-stage
  name embedded at (0)): for each name in workflow.stages, an unknown name
  (absent from both the original body and the extend-stages) raises a
  structural error "unknown stage name in workflow.stages: <name>". The check
  runs on the FULL set
  BEFORE any skip removal, so a stage that exists in the original body —
  even if also marked skip: true — is NOT flagged as unknown. Extend-stage
  names are valid here (embedded before this check). Strictness over
  extend.<name>.before/.after refs is enforced separately at (0.45).
  (0.6) SKIP REMOVAL + transparent depends_on reconnection: stages whose
  workflow.stages[name].skip is True (and which exist) are removed from
  the working body. STAGES: each remaining stage's depends_on is
  transparently reconnected — a reference to a removed stage S is replaced
  with resolve(S), the transitive set of non-skipped predecessors of S
  (chains resolved; no dangling references or duplicates). PHASES: removed
  steps drop from the body list and depends_on re-derives by list position
  in step 5 (automatic collapse). skip wins over agent/prompt/loop/skills
  overrides on the same entry (removal runs before (a)). If the
  reconstructed body becomes empty (every stage skipped), raise a
  structural error "empty body".
  (a) per-stage overrides applied in-place to the body of `PhaseStep` /
  `StageStep`, matching embedded extend-stages by name. A stage
  not found here can only be an intentionally skipped one (removed at (0.6)),
  so the lookup is SILENT; unknown names are errors at (0.5). The agent
  override FALLS BACK to an extend-entry's inline agent, and a skills merge
  is applied;
  (b) loop-expansion producing N copies with ids NAME-1..N and chain-style
  internal depends_on, expanding extend-stages when the effective loop for
  that name is >= 2 (stages-block loop OR the extend-entry's inline loop);
  (c) external depends_on references rewritten via the expanded_ids map,
  rewriting before/after-derived refs as well: both after and before
  → LAST expanded id — any reference to a loop-expanded chain points to its
  completion). Unknown stage names in workflow.stages and dangling refs in
  extend before/after are STRUCTURAL ERRORS (validated on the full
  original∪extend name set before skip removal). The agent instruction is composed into the
  in-container wrapper path directly — the cell does NOT call any host-side
  resolver.

  Inline extend fields and override priority: an extend-entry may carry
  inline agent/loop (extracted by parse_workflow into
  `WorkflowExtendStage`). These inline fields provide DEFAULT override
  values for the extend-stage. An explicit stages-block entry for the
  same name takes precedence PER FIELD — the effective value for a field is
  the stages-block value when that entry provides it (not None), else the
  extend-entry's inline value (when the stage originated from an
  extend-entry carrying it), else None (agent) / 1 (loop). Thus a
  stages-block agent → command composition wins over an inline agent;
  a stages-block loop wins over an inline loop. Realized by treating
  each extend-stage's inline agent/loop as a virtual stages-block
  override merged UNDER the explicit one, so passes 1 and 2 operate on the
  effective override set with no new pass.

  Skills merge: when a stages-block entry for a name carries a non-None
  skills list, the compiler merges it with the step body's existing
  skills (from the pipeline-file) — pipeline skills first, then the
  workflow skills, deduplicated by value (first occurrence keeps its
  position). An empty pipeline-skills side yields the workflow list; an
  empty workflow-skills side leaves the pipeline list unchanged; both empty
  yields no skills key. Extend-entry skills are NOT merged — they pass
  through verbatim as the new stage's skills (a new stage has no pipeline
  skills to merge with).

  approve translation: for a stage whose effective approve is one of
  "auto"/"plan"/"dialog", the compiler reads the trigger fields from that
  stage's body (pipeline-file body for an existing stage; the extend-stage body
  for an extend-stage) and applies up to two INDEPENDENT effects —
  communication: true ⇒ SUPPRESS interactive (no interactive key), and roles
  containing planner ⇒ emit auto_approve: true. Each value drives a subset of
  the two effects: "auto" drives BOTH; "plan" drives ONLY the interactive
  suppression (communication effect); "dialog" drives ONLY the auto_approve
  emission (roles effect). Each effect fires on its own trigger; baseline
  no-op when neither trigger (or neither directive subset) applies; uniform
  across every loop-expanded copy.

  Stage script directives: before_script, script, after_script are string
  stage-body directives translated to script_before, script, script_after (the
  authoring keys are consumed, not passed through). A stage body that carries
  script together with prompt and/or skills is a structural error "script is
  mutually exclusive with prompt/skills in stage <name>"; before_script /
  after_script are compatible with prompt / skills / script (no error).

---

"BodyFormat()":
  location: body_format.py
  annotations: |
    str-backed Enum declaring the structural format of a pipeline-file body.

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

  properties:
    "PHASES = \"phases\"": |
      Body is a YAML list (sorted list of steps with a dash-prefixed name
      item). depends_on is auto-generated by position during compilation.
    "STAGES = \"stages\"": |
      Body is a YAML dict (map keyed by step id). User-supplied depends_on
      is passed through as-is during compilation.

"PipelineRoles(planner: str | None = None, executor: str | None = None, reviewer: str | None = None)":
  location: pipeline_roles.py
  annotations: |
    Data model of the header-level roles directive — three optional
    inline prompt overrides, one per fixed role key. Constructed by
    `parse_dsl` when the header segment contains a non-empty roles
    block; carried verbatim inside `PipelineHeader` (its roles field)
    and surfaced to the consumer through `PipelineDocument`.

    `planner`: inline prompt text overriding
               goga/assets/afm/prompts/planning.md, or None when not
               specified
    `executor`: inline prompt text overriding
                goga/assets/afm/prompts/implementation.md, or None
    `reviewer`: inline prompt text overriding
                goga/assets/afm/prompts/review.md, or None

    The planner/executor/reviewer field names map to the planning/
    implementation/review prompt-file stems via `translate_role`. summary
    is NOT an overridable role — its prompt file is always materialized
    from the default.

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - All three fields default to None (backwards-compat with pipeline-files
      that do not override every role)
    - Field order is fixed: planner, executor, reviewer — matches the
      canonical order of the overridable prompt files in
      goga/assets/afm/prompts/

    Constraints:
    - Do not validate the contents of prompt text — pass it through
      verbatim to the consumer (run_pipeline writes it to the prompt file
      unchanged)
    - Do not merge or concatenate with default prompt text — the consumer
      replaces the file wholesale
    - Do not carry a summary field — summary is never overridable from the
      DSL

  properties:
    "planner -> str | None": |
      Inline prompt text overriding goga/assets/afm/prompts/planning.md
      (stem resolved via `translate_role`), or None when not specified in
      the header.roles block.
    "executor -> str | None": |
      Inline prompt text overriding
      goga/assets/afm/prompts/implementation.md, or None when not
      specified.
    "reviewer -> str | None": |
      Inline prompt text overriding goga/assets/afm/prompts/review.md,
      or None when not specified.

"PipelineDocument(header: PipelineHeader, format: BodyFormat, body: PhasesBody | StagesBody)":
  location: pipeline_document.py
  annotations: |
    Aggregated pipeline-file document — the parsed representation of a
    pipeline-file as a single value, combining the header, body format,
    and body in one dataclass. Built by `compile_flow` from `parse_dsl`'s
    3-tuple output and returned to consumers as the first element of the
    documents tuple. Exists so that consumers (run_pipeline) can obtain
    the parsed representation — including the roles field of `header` — from a single
    return value without re-invoking parse_dsl.

    `header`: parsed `PipelineHeader` (name, description, optional roles)
    `format`: detected `BodyFormat` — PHASES or STAGES
    `body`: parsed body — `PhasesBody` when format is PHASES,
            `StagesBody` when format is STAGES. The body reflects the
            ORIGINAL parsed body; workflow-applied reconstruction lives
            only in `FlowStage` instances inside `FlowDocument`, never in
            this field.

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - All three fields are required (no defaults) — a PipelineDocument is
      always complete

    Constraints:
    - Do not validate the body format against the body type — the caller
      (`compile_flow`) constructs PipelineDocument from `parse_dsl`'s
      consistent output

  properties:
    "header -> PipelineHeader": |
      Parsed pipeline-file header (name, description, optional roles).
    "format -> BodyFormat": |
      Detected body format — PHASES or STAGES.
    "body -> PhasesBody | StagesBody": |
      Parsed body. Type matches the format property above: PhasesBody
      when PHASES, StagesBody otherwise. The ORIGINAL parsed body —
      workflow-reconstructed stages live only in `FlowDocument`.

"PipelineHeader(name: str, description: str, roles: PipelineRoles | None = None)":
  location: pipeline_header.py
  annotations: |
    Header of an input pipeline-file — top-level name and description
    appearing before the three-dash separator, plus an optional roles
    field (of type `PipelineRoles`) carrying inline prompt overrides.
    Parsed by `parse_dsl` from the header segment of the pipeline-file
    text. Carried 1:1 into the `PipelineDocument` (its header field) by
    `compile_flow`. The roles field does not enter the compiled
    `FlowDocument` — it is a goga-side
    artifact surfaced to the consumer through `PipelineDocument`.

    `name`: pipeline name (e.g. "Goga feature")
    `description`: short pipeline description (e.g. "Feature implementation")
    `roles`: optional `PipelineRoles` instance parsed from the
              header-level roles block, or None when the block is
              absent or empty

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - `name` and `description` are required (no defaults)
    - roles defaults to None (backwards-compat with pipeline-files
      without a roles block)

  properties:
    "name -> str": |
      Pipeline name.
    "description -> str": |
      Short pipeline description.
    "roles -> PipelineRoles | None": |
      Inline prompt overrides from the header-level roles block, or
      None when the block is absent or empty. Built by `parse_dsl`;
      carried verbatim through `PipelineDocument` to the consumer. Never
      enters the compiled afm flow-file.

"PhaseStep(name: str, title: str, body: dict[str, Any])":
  location: phase_step.py
  annotations: |
    One element of a phases-DSL body — a single dash-prefixed name list item.

    `name`: step id (the value of the name field inside the list item)
    `title`: display label (the value of title inside the item)
    `body`: verbatim copy of every other field in the item (e.g. roles,
            prompt, skills, interactive, and any extra fields),
            excluding name and title

    Does not carry depends_on — the compiler generates it from list
    position when building `FlowStage`.

    The body field is intentionally typed as dict[str, Any]: the cell does
    not validate or know the schema of step fields. Build the data model with
    the standard library dataclasses module (NOT pydantic, per `convention`).
    Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - The body field excludes name and title (those are separate fields)

    Constraints:
    - Do not validate the contents of body — pass it through verbatim
    - Do not normalize or rename keys in body

  properties:
    "name -> str": |
      Step id.
    "title -> str": |
      Display label.
    "body -> dict[str, Any]": |
      Verbatim extra fields of the step.

"StageStep(name: str, title: str, depends_on: list[str] | None, body: dict[str, Any])":
  location: stage_step.py
  annotations: |
    One entry of a stages-DSL body — a value in the body map, keyed by step id.

    `name`: step id (the map key)
    `title`: display label (the value of title inside the value)
    `depends_on`: list of predecessor step ids, or None when the field is
                  absent from the source value. None means "no depends_on
                  written" — the compiler writes no depends_on key in the
                  output. An empty list means "explicit empty dependency"
                  and is written as depends_on [].
    `body`: verbatim copy of every other field in the value (excluding
            title and depends_on)

    Carries `depends_on` from the source; the compiler passes it through
    unchanged when building `FlowStage` (rewriting external references
    to loop-expanded base-names when a workflow is applied).

    The body field is intentionally typed as dict[str, Any]: the cell does
    not validate or know the schema of step fields. Build the data model with
    the standard library dataclasses module (NOT pydantic, per `convention`).
    Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - The body field excludes title and depends_on (those are separate
      fields); name is the map key, also not part of the body
    - `depends_on` distinguishes None (absent) from an empty list (explicit empty)

    Constraints:
    - Do not validate the contents of `body` — pass it through verbatim
    - Do not normalize or rename keys in `body`
    - Do not validate `depends_on` references (dangling ids, cycles,
      duplicates) — afm's responsibility

  properties:
    "name -> str": |
      Step id.
    "title -> str": |
      Display label.
    "depends_on -> list[str] | None": |
      Predecessor step ids, or None when absent.
    "body -> dict[str, Any]": |
      Verbatim extra fields of the step.

"PhasesBody(steps: list[PhaseStep])":
  location: phases_body.py
  annotations: |
    Body of a phases-DSL pipeline-file — an ordered list of `PhaseStep`
    items, in the source order. The order carries semantic meaning: the
    compiler auto-generates depends_on from list position (first step
    gets none, each subsequent step depends on the previous one).

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - Preserve insertion order of `steps`

  properties:
    "steps -> list[PhaseStep]": |
      Ordered list of phase steps.

"StagesBody(steps: list[StageStep])":
  location: stages_body.py
  annotations: |
    Body of a stages-DSL pipeline-file — an ordered list of `StageStep`
    items, in the iteration order of the source map (Python 3.7+ preserves
    dict insertion order). The order carries no dependency meaning
    (depends_on is user-supplied per step) but determines the order of
    stages in the output flow-file.

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - Preserve insertion order of `steps`

  properties:
    "steps -> list[StageStep]": |
      Ordered list of stage steps.

"FlowStage(id: str, name: str, depends_on: list[str] | None, fields: dict[str, Any])":
  location: flow_stage.py
  annotations: |
    One stage of an output afm flow-file — a single dash-prefixed id list
    item under the stages key. Both `PhaseStep` and `StageStep` converge
    into this type during compilation.

    `id`: step identifier (output as a dash-prefixed id item)
    `name`: display label (output as the name field)
    `depends_on`: predecessor step ids, or None. None produces no depends_on
                  key in output; an empty list produces depends_on [].
    `fields`: extra step fields in canonical key order (interactive,
              auto_approve, command, prompt, description, agents, supervisor,
              supervisor_prompt, skills, script_before, script, script_after,
              then alphabetically-sorted unknown keys). Insertion order of
              this dict IS the output order — the
              serializer iterates it as-is. command is populated from the
              agent field of `WorkflowStage` (composed as the in-container
              wrapper path) when a workflow supplies one; description is
              populated from the prompt field of `WorkflowStage` when a
              workflow supplies one. Both are independent channels —
              pipeline-file prompt and workflow-file prompt may coexist as
              separate fields in the same stage. agents is populated by the
              default-injection rule of `compile_flow` (to the single value
              ["auto"]) when the source step body has no usable agents value
              (missing key, explicit null, or empty list); supervisor and
              supervisor_prompt appear ONLY when the source step body authors
              them (no longer default-injected).

    The description slot in `fields` is a distinct channel from
    `FlowStage` name: the name field carries the display label sourced
    from the pipeline-file step (the title field of `PhaseStep` or
    `StageStep`), while the description field inside `fields` carries
    the workflow-file per-stage prompt override. Both can coexist in the same
    `FlowStage` instance without collision — they live in different slots
    and serialize as separate YAML keys (name vs description inside the
    stage item).

    The compiler builds `fields` with the canonical key order when
    constructing the `FlowStage` instance; the serializer does not reorder.

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - `fields` keys must be in canonical order when constructed
    - `depends_on` distinguishes None (absent) from an empty list (explicit empty)

  properties:
    "id -> str": |
      Step identifier.
    "name -> str": |
      Display label.
    "depends_on -> list[str] | None": |
      Predecessor step ids, or None when absent.
    "fields -> dict[str, Any]": |
      Extra fields in canonical key order: interactive, auto_approve, command,
      prompt, description, agents, supervisor, supervisor_prompt, skills,
      script_before, script, script_after, then alphabetically-sorted unknown
      keys. auto_approve (bool) is present only when the effective approve
      directive drives the roles effect ("auto"/"dialog") + planner-in-roles
      fired; script_before/script/script_after (str) are present
      only when the corresponding stage directive was authored. Insertion order
      IS the output order — the serializer iterates as-is. command/description
      from WorkflowStage; agents default-injected to ["auto"]; supervisor/
      supervisor_prompt only when authored.

"FlowDocument(prompt: str | None = None, root_dir: str | None = None, name: str, description: str, stages: list[FlowStage])":
  location: flow_document.py
  annotations: |
    Output afm flow-file — a single flat YAML document with up to five
    top-level keys (prompt (when present), root_dir (when supplied),
    name, description, stages). No segmentation, no header sub-object:
    the format is flat, and this type mirrors that flatness.

    `prompt`: top-level prompt value emitted as the FIRST top-level key of
              the flow-file when not None. Populated from the prompt field
              of `WorkflowDocument` when a workflow supplies one;
              None otherwise (omitted from output — no top-level prompt
              key in the flow-file).
    `root_dir`: top-level afm root_dir directive value emitted as the
              SECOND top-level key of the flow-file (immediately after
              the prompt key when present, before the name key) when not
              None. Populated by the caller (the run_pipeline routine in
              goga/pipeline) from the in-container project root
              (Path.cwd() resolves to /workspace inside the goga container);
              None when the caller did not supply one (omitted from output
              — no top-level root_dir key in the flow-file). The compiler
              itself performs no environment-variable reads.
    `name`: top-level name value (carried 1:1 from `PipelineHeader` name)
    `description`: top-level description value (carried 1:1 from
                   `PipelineHeader` description)
    `stages`: ordered list of `FlowStage` items, output as the stages list.
              When a workflow supplied loop-expansion or per-stage
              overrides, the list reflects the reconstructed stages; when
              no workflow was applied, the list reflects the original
              parsed body.

    The only object `serialize_flow` accepts as input.

    Build the data model with the standard library dataclasses module
    (NOT pydantic, per `convention`). Use @dataclass(kw_only=True).

    Requirements:
    - Use @dataclass(kw_only=True) (per `convention`)
    - `prompt` defaults to None — the flow-file omits the top-level prompt
      key when no workflow supplies one
    - `root_dir` defaults to None — the flow-file omits the top-level
      root_dir key when the caller did not supply one
    - Field order is fixed: prompt, root_dir, name, description, stages —
      matches the canonical emission order in `serialize_flow`

  properties:
    "prompt -> str | None": |
      Top-level flow prompt, or None when no workflow supplied one. Emitted
      as the first top-level key when not None.
    "root_dir -> str | None": |
      Top-level afm root_dir directive, or None when the caller did not
      supply one. Emitted as the second top-level key (after prompt when
      present, before name) when not None.
    "name -> str": |
      Top-level flow name.
    "description -> str": |
      Top-level flow description.
    "stages -> list[FlowStage]": |
      Ordered list of flow stages.

"parse_dsl(text: str) -> header: PipelineHeader, format: BodyFormat, body: PhasesBody | StagesBody":
  location: parse_dsl.py
  annotations: |
    Structurally parse a pipeline-file into a header, a body format, and a
    typed body. No content validation of step fields; no depends_on rule
    application.

    `text`: full pipeline-file text (must contain a three-dash separator line)
    `header`: parsed `PipelineHeader` (name, description from the header
              segment; plus an optional roles field of type
              `PipelineRoles` or None, carrying inline prompt overrides
              from the header-level roles block — None when the block is
              absent or an empty mapping, a `PipelineRoles` instance
              otherwise)
    The format output is the detected `BodyFormat` — PHASES if the body is
    a YAML list, STAGES if the body is a YAML dict.
    The body output is the parsed body — `PhasesBody` when format is PHASES,
    `StagesBody` when format is STAGES.

    Algorithm:
    1. Split `text` into a header segment and a body segment on the
       three-dash separator. If no such separator exists — raise a
       structural error "missing body separator"
    2. Parse the header segment; extract name and description. If either
       is missing or not a str — raise a structural error
       "header missing name/description". Then extract the optional
       roles block from the header segment. If the block is absent or
       an empty mapping — set roles to None. If the block is present but
       its value is not a mapping (e.g. a scalar, a string, a list) —
       raise a structural error "non-mapping roles block in header".
       Otherwise validate every key against the fixed set {planner,
       executor, reviewer}: an unknown key (including summary) raises
       a structural error "unknown role in header.roles: <key>; valid
       keys: planner, executor, reviewer"; a non-str value raises a
       structural error "non-str value in header.roles.<key>". Build
       `PipelineRoles` from the validated entries. Build
       `PipelineHeader` with name, description, and roles.
       ADDITIONALLY, if the header segment carries the agents
       key — raise a structural error "agents key is forbidden in
       header; use roles".
    3. Parse the body segment
    4. Detect format by the structure of the parsed body:
       - a list → `BodyFormat` PHASES; build a `PhasesBody` of `PhaseStep`
         items (one per list entry). Each list item must carry string name
         and title fields; otherwise raise a structural error
         "phase item missing name/title"
       - a dict → `BodyFormat` STAGES; build a `StagesBody` of `StageStep`
         items (one per map entry). Each map value must carry a string
         title field; otherwise raise a structural error
         "stage value missing title"
       - any other shape — raise a structural error "unsupported body format"
    5. Return (header, format, body)

    Apply `convention` for code style and exception message formatting.

    Requirements:
    - The three-dash separator is matched as a line of exactly three dashes
      (per YAML document separator convention), not as a substring
    - An empty body (`PhasesBody` with zero steps or `StagesBody` with zero
      steps) is NOT a structural error here — `compile_flow` checks for
      emptiness
    - `PhaseStep` and `StageStep` carry deep copies of the parsed body
      dicts, so subsequent mutation does not affect the source
    - The fixed role key set is exactly {planner, executor, reviewer} —
      no other keys accepted (summary is not a valid role)
    - An absent roles block and an empty roles mapping are treated
      identically: roles is set to None
    - The agents key in the header is a structural error
    - A non-str value for a known key is a structural error (not silently
      coerced)
    - A non-mapping value for the roles block itself (scalar, string,
      list) is a structural error — the block must be a mapping or absent

    Constraints:
    - Do not apply depends_on rules — that is `compile_flow`'s job
    - Do not validate the contents of step fields
    - Do not resolve or validate depends_on references — afm's job
    - Do not handle a pipeline-file without a three-dash separator
      (already-afm format) — it is an error by design
    - Do not validate or merge inline prompt text — `parse_dsl` passes
      the roles values through verbatim; the consumer (the run_pipeline
      routine in goga/pipeline)
      decides how to materialize them

"serialize_flow(doc: FlowDocument) -> text: str":
  location: serialize_flow.py
  annotations: |
    Serialize a `FlowDocument` into a byte-exact string in the canonical afm
    flow-file format. Uses the `beautiful_yaml` parameters plus custom
    rules for canonical key order and flow-style for agents.

    `doc`: the `FlowDocument` to serialize (fields must already be in
           canonical key order — `compile_flow` enforces this when building;
           top-level prompt and root_dir may each be None or a str)
    `text`: the resulting YAML string, byte-equivalent to flow.yml for
            equivalent content

    Algorithm:
    1. Build a top-level representation in fixed order:
       - when `doc` prompt is not None — emit prompt as the FIRST
         top-level key, in block-literal scalar style (multi-line prompt
         text). When `doc` prompt is None — omit the key entirely
         (no top-level prompt key in the output)
       - when `doc` root_dir is not None — emit root_dir as the SECOND
         top-level key (after prompt when present, before name), as a
         plain scalar. When `doc` root_dir is None — omit the key entirely
         (no top-level root_dir key in the output)
       - name, then description, then stages (per step 2)
    2. For each `FlowStage` in `doc`, build a representation in canonical
       key order: id, name, then the stage fields as-is (already in
       canonical order — interactive, auto_approve, command, prompt,
       description, agents, supervisor, supervisor_prompt, skills,
       script_before, script, script_after, then alphabetically-sorted
       unknown keys), then depends_on when it is not None. The serializer
       does NOT reorder — canonical order is fixed at `FlowStage` assembly
    3. Serialize the representation to YAML per `beautiful_yaml`, with the
       flow-style applied to agents and block-style applied to skills,
       depends_on, and the top-level prompt; auto_approve as a plain bool
       scalar; script_before / script / script_after as plain scalars when
       single-line and block-literal scalars when multi-line
    4. Ensure the result ends with a single trailing newline
    5. Return the string

    Apply `beautiful_yaml` for the YAML serialization parameters.
    Apply `convention` for code style and docstring formatting.

    Requirements:
    - Output is canonical afm flow-file YAML: optional prompt (first,
      when present), optional root_dir (second, when supplied), then
      fixed top-level key order (name, description, stages), canonical
      per-stage key order (including auto_approve, command,
      description, and script_before/script/script_after), flow-style for
      agents, block-style for skills and depends_on, auto_approve as a
      plain bool scalar, single trailing newline
    - When `doc` prompt is None — the output omits the prompt key
      entirely (no top-level prompt key in the output)
    - When `doc` root_dir is None — the output omits the root_dir key
      entirely (no top-level root_dir key in the output)
    - root_dir is emitted as a plain scalar (NOT block-literal style like
      the top-level prompt)
    - agents is emitted in flow-style; skills, depends_on, and the
      top-level prompt are emitted in block-style
    - Empty list values (e.g. an explicit empty depends_on) are written
      explicitly, not omitted
    - All other key-value pairs follow `beautiful_yaml` defaults

    Constraints:
    - Do not reorder keys — the fields order is the caller's responsibility
    - Do not validate `doc` — assume it is well-formed (`compile_flow`
      constructed it)
    - Do not write to disk — return the string; the caller writes it

"compile_flow(pipeline_path: Path, flow_path: Path, workflow: WorkflowDocument | None = None, root_dir: str | None = None, project_name: str | None = None) -> documents: tuple[PipelineDocument, FlowDocument]":
  location: compile_flow.py
  annotations: |
    Entry point of the cell: compile a pipeline-file (goga DSL) into an afm
    flow-file at the given paths. Reads `pipeline_path`, parses and detects
    format via `parse_dsl`, applies per-format depends_on rules and canonical
    key order, builds a `FlowDocument`, serializes it via `serialize_flow`,
    writes to `flow_path`, and returns the documents tuple. When `workflow`
    is supplied (a `WorkflowDocument`), the cell reconstructs the parsed
    body per the workflow instructions BEFORE building the FlowDocument;
    when `workflow` is None, no workflow is applied and the output carries
    no top-level prompt and no per-stage overrides.

    `pipeline_path`: absolute path to the input pipeline-file (DSL format)
    `flow_path`: absolute path to the output flow-file (afm format). The
                 parent directory must already exist — `compile_flow` does
                 not create it
    `workflow`: optional `WorkflowDocument` carrying declarative
                instructions for extending the pipeline (top-level prompt,
                per-stage agent/prompt/loop overrides, AND new stages via
                extend with before/after positioning). When None — no
                workflow is applied, the output carries no top-level prompt
                and no per-stage overrides. Supplied by the consumer
                (the run_pipeline routine in goga/pipeline), parsed from a
                project workflow-file via parse_workflow (per the
                `parse-workflow` practice).
    `root_dir`: optional top-level afm root_dir directive carried into the
                `FlowDocument` and emitted by `serialize_flow` as the
                top-level root_dir key immediately after the prompt key
                (when present) and before the name key. When None — the
                root_dir key is omitted entirely (back-compat with
                flow-files that carry no root_dir). Supplied by the
                consumer (the run_pipeline routine in goga/pipeline),
                which resolves the value from the in-container project
                root (Path.cwd() == /workspace inside the goga container —
                the single source of truth mirroring the host-side mount
                decision).
    `project_name`: optional project name (basename of the git origin remote URL,
                minus a trailing .git) used as the description prefix in the
                compiled flow-file. When not None — the `FlowDocument` description
                becomes f"[{project_name}] {header.description}"; when None — the
                description is the header description unchanged. OUTPUT-only: the
                `PipelineDocument` description stays the faithful mirror (like
                `root_dir`). Supplied by the consumer (the run_pipeline routine in
                goga/pipeline), which resolves it in-container via
                resolve_project_name; the compiler performs no environment /
                subprocess reads to derive it.
    `documents`: a tuple of `PipelineDocument` and `FlowDocument` — the
                 parsed input representation (carrying header roles when
                 present; the ORIGINAL parsed body, NOT the
                 workflow-reconstructed body) and the output flow-file model
                 (including workflow-applied overrides, loop-expansion,
                 rewritten depends_on, the top-level prompt when a
                 workflow supplied one, and the top-level root_dir when
                 the caller supplied one). The text flow-file is also
                 written to `flow_path` as a side effect.

    Algorithm:
    1. Read `pipeline_path` as text
    2. Call `parse_dsl` to obtain (header, format, body)
    3. If the body has zero steps — raise a structural error "empty body"
    4. When `workflow` is not None — reconstruct the body per the `workflow`
       instructions (BEFORE building FlowStages):
       4a0. Embed extend-stages and position them via before/after — STAGES
            derives their explicit depends_on, PHASES inserts them into the
            body list (only when the workflow's extend map is non-empty):
            1. Build the set of known names: every step name/id present in
               the body, plus every extend-stage name. The set resolves
               cross-references between extend-stages.
            2. For each extend-stage name and its `WorkflowExtendStage` in
               the workflow's extend map (resolve cross-references via the
               known-names set; a second pass or deferred resolution handles
               extend-stages referencing not-yet-embedded extend-stages):
               - STAGES branch (explicit depends_on via the StageStep field):
                 a. Construct a `StageStep` whose name is the extend-stage
                    name, whose title comes from the extend-stage body,
                    whose depends_on is initialized from the extend-stage
                    after (None when after is None), and whose body is the
                    extend-stage body minus title.
                 b. For each name in the extend-stage before (when present):
                    append the extend-stage name to the depends_on of the
                    existing body step whose name/id equals that name
                    (initialize to an empty list when it had no depends_on).
               - PHASES branch (positional insertion — `PhaseStep` carries
                 NO depends_on field; depends_on is derived by list position
                 in step 5):
                 a. Construct a `PhaseStep` whose name is the extend-stage
                    name and whose title and body come from the extend-stage
                    body.
                 b. Compute the insertion index from the positioning fields:
                    immediately AFTER the step whose name/id equals the LAST
                    resolvable after name, and/or immediately BEFORE the
                    step whose name/id equals the FIRST resolvable before
                    name. When both after and before are present, the index
                    must satisfy both (after-targets precede
                    before-targets); when inconsistent, fall back to the
                    after-based index and emit a WARNING.
                 c. Insert the `PhaseStep` at the computed index; subsequent
                    steps shift down. Step 5's positional derivation then
                    yields the correct depends_on: the extend-stage depends
                    on its positional predecessor (the after-target, or the
                    after-target's last loop-expanded copy once 4b runs),
                    and each before-target gains the extend-stage as its
                    positional predecessor. Multi-target before/after in
                    PHASES reduces to a single positional predecessor per
                    step, but the transitive execution order still respects
                    every target (an extend-stage after [A, B] is placed
                    after the last of A and B, so it runs after both via the
                    positional chain).
               - Dangling reference (a before/after name absent from the
                 known-names set) — cannot occur at this step: step (0.45)
                 strict-validates every before/after ref BEFORE the embed and
                 raises a structural error "unknown stage name in
                 workflow.extend.<name>.before/.after: <ref>". The embed thus
                 runs as a pure transform that may assume every ref resolves.
            3. Place the constructed extend-steps into the body sequence:
               - STAGES: add the `StageStep`s to the body map, keyed by
                 name (their explicit depends_on encodes before/after).
               - PHASES: the `PhaseStep`s are already inserted at their
                 computed positions; depends_on is derived purely by list
                 position in step 5 (no explicit depends_on is carried).

       4a0-pre. Strict-validation of workflow.extend.<name>.before/.after refs:
                build the valid-name set = { every step name/id in the
                ORIGINAL body } ∪ { every extend-stage name in workflow.extend }.
                For each before/after ref: if the ref is NOT in the valid-name
                set — raise a structural error "unknown stage name in
                workflow.extend.<name>.before/.after: <ref>". Runs BEFORE the 4a0
                embed (extend names come from workflow.extend keys, so cross-
                references between extend-stages resolve without embedding) and
                BEFORE any skip removal, so a really-existing skipped stage is
                NOT flagged (referencing a skipped stage is not a dangling ref).
                Existence only — cycles, self-references, and duplicate refs
                remain afm's responsibility.
       4pre. Strict-validation of workflow.stages names: build the valid-name
             set = { every step name/id in the ORIGINAL body } ∪ { every
             extend-stage name embedded at 4a0 }. For each name in
             workflow.stages: if the name is NOT in the valid-name set — raise
             a structural error "unknown stage name in workflow.stages: <name>".
             The check runs on the FULL set BEFORE any skip removal, so a
             really-existing skipped stage is NOT flagged as unknown. Extend-
             stage names are valid here (embedded before this check). Strictness
             over extend.<name>.before/.after refs is enforced at 4a0-pre
             (above); a workflow must not carry a dangling extend ref.
       4skip. Skip removal + transparent depends_on reconnection: determine the
             skipped-name set = { name : workflow.stages[name].skip is True
             AND name exists in the working body }. Then:
             - STAGES branch (explicit depends_on reconnection):
               1. For each remaining `StageStep` D with a non-None depends_on,
                  rebuild depends_on as: for each ref in D.depends_on, if ref
                  is a skipped name S — replace with resolve(S); else keep
                  {ref}. resolve(S) = union of resolve(P) for P in
                  S.depends_on (a skipped P recurses; a non-skipped P
                  contributes {P}); a skipped S whose depends_on is None or
                  resolves to nothing contributes {}. resolve is evaluated
                  with a visited set so a depends_on cycle among skipped
                  stages terminates (cycles remain afm's concern — the
                  compiler does not validate them, but must not crash).
                  Deduplicate the rebuilt list preserving first-occurrence
                  order; a resulting empty list is written as depends_on []
                  (explicit empty). References to non-skipped names are
                  unchanged.
               2. Remove every `StageStep` whose name is a skipped name from
                  the body map.
             - PHASES branch (position-derived depends_on): remove every
               `PhaseStep` whose name is a skipped name from the body list.
               The position-derived depends_on in step 5 then chains the
               remaining steps correctly (a removed step is simply absent
               from the positional chain — automatic collapse, no explicit
               reconnection).
             skip wins over agent/prompt/loop/skills overrides on the same
             entry — removal runs BEFORE 4a, so the skipped stage's overrides
             are never applied (they target a stage already gone).
             Guard: after removal, if the reconstructed body has zero steps —
             raise a structural error "empty body" (consistent with the
             existing empty-body guard on the ORIGINAL body at step 3). This
             covers the edge-case where skip removes every stage of the
             pipeline.

       4a. Apply per-stage overrides (in-place mutation of the body's step
           bodies) — unchanged; now also matches embedded extend-stages by
           name; the agent override now falls back to an extend-entry's
           inline agent; a skills merge is applied. A name not found in the
           body is SILENTLY skipped (it can only be an intentionally skipped
           stage removed at 4skip; unknown names already errored at 4pre).
           Compute the EFFECTIVE
           override for each stage name by merging, per field, the explicit
           stages entry (when present and providing the field) OVER the
           extend-entry's inline agent/loop/approve (treated as defaults):
          - For each stage name that has EITHER an explicit stages-block
            entry OR (originated from an extend-entry carrying inline agent,
            loop, or approve), determine the effective values:
            * Find the step in the body whose name/id equals the stage name
            * If not found — silent (it can only be an intentionally skipped
              stage removed at 4skip; unknown names already errored at 4pre)
            * If found:
              - effective agent = the stages-block agent when the entry
                provides it (not None), else the extend-entry's inline
                agent when this step originated from an extend-entry
                carrying one, else leave the step's existing command
                unchanged. When the effective agent is not None — assign the
                wrapper path /home/goga/bin/<agent>-as-claude.sh to the
                command slot of the step body (the wrapper path is composed
                directly — do NOT call any host-side wrapper resolver)
              - when the stages-block prompt for this name is not None —
                assign that prompt text to the description slot of the step
                body (prompt has no inline extend equivalent — an
                extend-entry's prompt already lives in its body)
              - when the stages-block skills for this name is not None —
                merge skills: set the step body's skills to
                dedup(pipeline_skills ++ workflow_skills), where
                pipeline_skills is the step body's existing skills (or []),
                workflow_skills is the stages-block skills list, ++
                concatenates (pipeline first), and dedup drops later
                duplicates by value preserving first-occurrence position.
                Pipeline-only or workflow-only sides are harmless; both
                empty yields no skills key
              - effective approve = the stages-block approve when the entry
                provides it (not None), else the extend-entry's inline
                approve when this step originated from an extend-entry
                carrying one, else None. Thread the effective approve into
                the per-stage body for step 5 (it drives the interactive-
                suppress / auto_approve effects there)
       4b. Build a new ordered list of steps with loop-expansion applied —
           unchanged; now also expands an extend-stage when the effective
           loop for that name is >= 2 (producing NAME-1..N):
          - Maintain an expanded-ids map from each base-name to the list
            of ids produced for it
          - For each step in the body (in source order):
            * Determine loop_count: effective loop for the step's name =
              the stages-block loop when the entry provides it (not
              None), else the extend-entry's inline loop when this step
              originated from an extend-entry carrying one, else 1
            * When loop_count == 1 — append the step unchanged (id stays
              as-is); record the base-name mapped to a single-element list
              containing only the base-name
            * When loop_count >= 2 — append N copies with id
              NAME-1, NAME-2, ..., NAME-N. The FIRST copy inherits the
              original step's external depends_on (rewritten in step 4c);
              each subsequent copy gets an internal depends_on pointing to
              the previous copy (chain). Record the base-name mapped to
              the list [NAME-1, ..., NAME-N]
          - For PHASES format (position-derived depends_on): each expanded
            copy becomes its own PhaseStep in the list; the position in the
            new list determines depends_on automatically during step 5
            (the original step's position is inherited by the first copy;
            subsequent copies depend on their predecessor copy by list
            position; the next ORIGINAL step after the expanded one
            naturally depends on the LAST copy via list position)
       4c. Rewrite external depends_on references — unchanged mechanism,
           now also rewriting before/after-derived refs (symmetric — both
           point to the chain's completion). STAGES only — PHASES format is
           handled by position in step 5:
          - For each StageStep in the new sequence with non-None
            depends_on: for each ref in depends_on:
            * When ref matches a base-name whose loop_count >= 2 — replace
              with the LAST id from that base-name's expanded-ids list
            * When ref matches a base-name whose loop_count == 1 — keep
              the ref as-is; it points to the single unexpanded copy whose
              id equals the base-name (see step 4b)
            * Otherwise (ref does not match any base-name in the body) —
              keep ref as-is and let afm surface a dangling-reference
              error if applicable
          - The same LAST-expanded-id rewrite applies to
            before/after-derived refs: a ref equal to an after name whose
            loop_count >= 2, and a before-target's ref to the new
            extend-stage whose loop_count >= 2, both resolve to the LAST
            expanded id (depends_on means "runs after", so any reference
            to a loop-expanded chain points to its LAST copy). PHASES
            extend-stages were positionally inserted at 4a0; loop-expansion
            at 4b chains their copies in place, so each extend-stage's
            positional predecessor is the after-target's last expanded
            copy — no explicit rewrite needed for PHASES.
       4d. Replace the body's step list with the new (embedded + expanded +
           rewritten) sequence.
    5. Build FlowStages from the (possibly reconstructed) body:
       - PHASES: for each `PhaseStep`, build a `FlowStage` with depends_on
         derived from list position (first step has none; each subsequent
         step depends on the previous one — including expanded copies,
         which chain naturally via list position)
       - STAGES: for each `StageStep`, build a `FlowStage` with depends_on
         passed through from the (possibly rewritten) source step
       - Body source: pipeline-file step body for an existing stage; the
         extend-stage body (the body field of `WorkflowExtendStage`) for an
         extend-stage
       - In both branches, BEFORE assembling fields, check script exclusivity:
         if the (effective) body carries script together with prompt and/or
         skills — raise a structural error "script is mutually exclusive with
         prompt/skills in stage <name>" (every stage, incl. loop-expanded
         copies; same pass as the agents/interactive-forbidden checks).
         before_script/after_script do NOT trigger the error
       - In both branches, BEFORE canonical ordering, inject default stage
         fields when the source step body has no usable roles value
         (missing key, explicit null, or empty list): set agents to
         ["auto"]. The input stage-body field for the afm agents list is
         roles; an authored non-empty roles value is translated entry-
         by-entry via `translate_role` (known aliases mapped, all other
         values passed through verbatim) into the output agents value.
         An authoring agents key in a stage body — pipeline-file stage OR
         embedded extend-stage — is a structural error "agents key is
         forbidden in stage body; use roles". auto is a sentinel agent
         mode emitted verbatim — goga does not interpret it (afm resolves
         it). supervisor and supervisor_prompt are NOT default-injected
         (they appear only when the source step body authors them). An
         authored non-empty roles value always wins and disables
         injection. The ORIGINAL parsed body is never mutated by this
         injection — defaults live in FlowStage assembly only
       - Stage script directives: translate present body keys
         before_script → script_before, script → script, after_script →
         script_after (the authoring keys are consumed, not passed through
         to output)
       - approve effects: for a stage whose EFFECTIVE approve is "auto"
         (threaded in at step 4a), read the trigger fields from THAT STAGE'S
         body and apply two INDEPENDENT effects:
         * interactive translation: communication → interactive, EXCEPT
           when the effective approve directive drives the communication
           effect ("auto"/"plan") AND the body has communication: true —
           SUPPRESS (emit no interactive key). communication: false ⇒
           interactive: false as usual. The "interactive key is
           forbidden in stage body; use communication" error applies to an
           authoring interactive key
         * auto_approve: the effective approve directive drives the roles
           effect ("auto"/"dialog") AND the body roles (the raw list,
           before `translate_role`) contains literal "planner" — emit
           auto_approve: true (canonical slot next to interactive).
           Otherwise absent
         The two effects are independent — each fires on its own trigger
         AND its own directive subset; baseline no-op when the effective
         approve is None or neither trigger applies; uniform across every
         loop-expanded copy
       - In both branches, assemble the fields of each `FlowStage` in the
         EXTENDED canonical key order (interactive, auto_approve, command,
         prompt, description, agents, supervisor, supervisor_prompt, skills,
         script_before, script, script_after, then alphabetically-sorted
         unknown keys), copying from the (defaults-injected) step body.
         auto_approve (bool) is present only when the effective approve
         directive drives the roles effect ("auto"/"dialog") + planner-in-roles
         fired; script_before / script / script_after
         (str) are present only when the corresponding stage directive was
         authored. Stage-body field translation: the input communication key
         maps to the output interactive key in its canonical slot; an input
         interactive key (pipeline-file stage OR embedded extend-stage) is a
         structural error "interactive key is forbidden in stage body; use
         communication". The output canonical key order keeps interactive
    6. Build `FlowDocument`:
       - prompt = `workflow` prompt when `workflow` is not None, else None
       - root_dir = the `root_dir` parameter passed by the caller (forwarded
         verbatim — None when the caller did not supply one; the compiler
         performs no environment-variable reads to derive it)
       - description = f"[{project_name}] {header.description}" when
         `project_name` is not None, else the header description unchanged
         (OUTPUT-only — PipelineDocument.description stays the faithful mirror,
         like root_dir)
       - name from the header; stages from the assembled FlowStages
    7. Build `PipelineDocument` from (header, format, ORIGINAL body) — the
       parsed representation carried alongside the FlowDocument for the
       consumer. The body field reflects the ORIGINAL parsed body, NOT the
       workflow-reconstructed one — PipelineDocument is a faithful mirror
       of the input pipeline-file
    8. Call `serialize_flow` on the FlowDocument
    9. Write the resulting text to `flow_path`
    10. Return (PipelineDocument, FlowDocument) as the documents tuple

    Apply `convention` for code style, docstring formatting, and warning
    emission for unknown-stage-name warnings.
    Apply `parse-workflow` for the contract of the
    `WorkflowDocument` value supplied by the consumer.

    Requirements:
    - `pipeline_path` and `flow_path` must be absolute paths
    - `flow_path` parent must already exist (caller's guarantee)
    - Structural errors raise with a readable message; OSError (file
      missing, permission denied) propagates unchanged
    - Idempotent: repeated calls with the same inputs overwrite `flow_path`
    - Output is the canonical afm flow-file format as produced by
      `serialize_flow`
    - When `workflow` is None — no workflow is applied (the reconstruction
      step is skipped entirely, no top-level prompt is emitted, no per-stage
      overrides are injected)
    - Return value is a 2-tuple of `PipelineDocument` then `FlowDocument` —
      input representation first, output representation second
    - `PipelineDocument` body reflects the ORIGINAL parsed body —
      workflow-reconstructed stages live only in `FlowDocument` stages
    - The agent instruction of a `WorkflowStage` is composed into the
      in-container wrapper path /home/goga/bin/AGENT-as-claude.sh inline
      — the cell does NOT call any host-side wrapper resolver
    - `FlowStage` fields are assembled directly from the step body dict
      at step 5 — workflow overrides (command, description) are
      injected INTO the body at step 4a before this assembly, so a single
      body dict is the sole source for fields. Pipeline-file fields
      and workflow-injected fields coexist in the same dict without
      merging or collision
    - Unknown stage names in workflow.stages (names absent from both the
      original body and the extend-stages) raise a structural error
      "unknown stage name in workflow.stages: <name>";
      a really-existing skipped stage is NOT flagged (validation runs on the
      full original∪extend set before removal)
    - Dangling refs in workflow.extend.<name>.before/.after (refs naming no
      original step and no extend-stage) raise a structural error "unknown
      stage name in workflow.extend.<name>.before/.after: <ref>" (not a verbatim
      pass-through); a ref to a really-existing skipped
      stage is NOT flagged (validation runs before skip removal); cross-references
      between extend-stages resolve (extend names are valid targets)
    - stages.<name>.skip: true for an existing stage → the stage is removed
      from the compiled flow-file; dependents' depends_on are transparently
      reconnected (STAGES via resolve, PHASES positional collapse); chains
      resolved transitively; no dangling references or duplicates
    - skip wins over agent/prompt/loop/skills overrides on the same entry
      (removal runs before the override pass)
    - If the reconstructed body is empty after skip removal (every stage
      skipped), raise a structural error "empty body"
    - An extend-stage name referenced by a stages override is valid (extend
      embedded before validation)
    - Loop-expansion for STAGES format rewrites external depends_on
      references to the LAST expanded id (e.g. a stage that depended on
      "review" gets rewritten to depend on "review-2" when review has
      loop=2)
    - Loop-expansion for PHASES format relies on list position to chain
      expanded copies and to make the next original step depend on the last
      copy — no explicit external rewrite pass is needed
    - When the workflow's extend map is non-empty — extend-stages are
      embedded into the body BEFORE per-stage overrides and loop-expansion;
      their depends_on is derived from before/after (after → the new stage's
      depends_on; before → the new stage is added to the depends_on of each
      named existing stage), without losing the prior dependencies of those
      stages
    - Dangling refs in before/after (names in neither the body nor the
      extend-stages) raise a structural error "unknown stage name in
      workflow.extend.<name>.before/.after: <ref>" — strict validation at
      4a0-pre (a workflow must not carry a dangling extend ref; symmetric
      with workflow.stages strictness)
    - Cross-references between extend-stages (one extend-stage's
      before/after names another extend-stage) are resolved
    - After loop-expansion, before/after-derived references are rewritten
      with the existing expanded_ids map: both after and before → LAST
      expanded id (any reference to a loop-expanded chain points to its
      completion, since depends_on means "runs after")
    - In PHASES format, extend-stages carry NO explicit depends_on (PhaseStep
      has no such field); instead they are POSITIONALLY inserted into the
      body list (after their after-targets, before their before-targets),
      and depends_on is derived purely by list position in step 5.
      Multi-target before/after reduces to a single positional predecessor
      per step; transitive execution order still respects every target
    - extend-stages compile into ordinary `FlowStage` instances — no change
      to `FlowDocument`, `FlowStage`, or `serialize_flow`
    - When `workflow` is None, step 4a0 is skipped entirely (as is the whole
      reconstruction)
    - Consumers must NOT re-invoke `parse_dsl` to obtain the roles — they
      read header roles from `PipelineDocument` returned by this routine
    - Default stage-field injection: a step body with no usable roles
      value (missing key, explicit null, or empty list) yields a
      `FlowStage` carrying a single injected default — agents=["auto"].
      supervisor and supervisor_prompt are NOT default-injected (authored
      values pass through). auto is emitted verbatim; goga does not
      resolve it.
      An authored non-empty roles value always wins and disables
      injection. The injection runs uniformly on the non-workflow path
      and on the workflow path (after per-stage overrides and
      loop-expansion)
    - The input stage-body field for the afm agents list is roles; the
      agents key in a stage body (pipeline-file stage or embedded
      extend-stage) is a structural error raised by `compile_flow`. The afm
      interactive field is authored as the communication key in a stage body
      and translated to the output interactive key; an authoring interactive key
      is a structural error raised by `compile_flow`
    - Authored roles values are translated to the output agents values
      via `translate_role`; known aliases (planner/executor/reviewer) map
      to planning/implementation/review, every other value passes through
      verbatim (goga does not validate the open afm agent namespace)
    - The output field name agents, the default ["auto"] injection, the
      canonical key order, and the flow-style serialization are unchanged
      (afm contract stable)
    - Override priority (inline extend vs stages block): for a stage that
      originated from an extend-entry, the extend-entry's inline agent /
      loop are DEFAULT override values; an explicit stages-block entry
      for the same name wins PER FIELD (effective value = stages value when
      provided, else inline value, else None/1)
    - Skills merge: when a stages-block entry carries non-None skills,
      the compiler merges them with the step's pipeline-file skills
      (pipeline first, then workflow, deduplicated by value). Extend-entry
      skills are NOT merged (verbatim — a new stage has no pipeline side)
    - Inline agent / loop of an extend-entry do NOT appear in the
      compiled flow-file as stage fields — they are extracted into the
      model by parse_workflow and consumed only as override defaults
    - Default injection is local to `FlowStage` fields assembly — the
      `PipelineDocument` body carried alongside the `FlowDocument` is
      never affected (it stays a faithful mirror of the source
      pipeline-file, with whatever roles value — or absence — the
      source authored)
    - The optional `root_dir` parameter is carried verbatim into the
      `FlowDocument` — the compiler performs no environment-variable
      reads (no Path.cwd(), no AFM_DIR inspection) to derive it. The
      caller (the run_pipeline routine in goga/pipeline) computes the
      value from the in-container project root (Path.cwd() inside the
      goga container resolves to /workspace, mirroring the host-side
      mount decision). When the caller does not supply `root_dir`, the
      compiled flow-file carries no top-level root_dir key (back-compat).
    - The optional `project_name` parameter prefixes the compiled flow-file
      description: when not None, the `FlowDocument` description becomes
      f"[{project_name}] {header.description}"; when None, the description is
      the header description unchanged. OUTPUT-only — the `PipelineDocument`
      description stays the faithful mirror (like `root_dir`). The compiler
      performs no environment / subprocess reads to derive it; the caller
      (the run_pipeline routine in goga/pipeline) resolves it in-container via
      resolve_project_name.
    - approve effects read triggers from the stage's own body and the
      directive from the workflow (effective approve, one of
      auto/plan/dialog); each value drives a subset of the two effects —
      suppress = omission (not interactive:false) for a communication-effect
      directive (auto/plan) + communication:true; auto_approve only for a
      roles-effect directive (auto/dialog) + planner-in-roles; baseline no-op
      when effective approve is None or neither trigger applies; uniform
      across loop copies
    - script together with prompt and/or skills is a structural error
      "script is mutually exclusive with prompt/skills in stage <name>";
      before_script/after_script are compatible (no error)
    - auto_approve, script_before, script, and script_after appear only when
      their source directive is present; flow-files without those directives
      carry none of these keys

    Constraints:
    - Do not read AFM_DIR or any environment variable — `flow_path` is
      the only source of the output location
    - Do not create `flow_path` parent — caller's responsibility
    - Do not validate depends_on references (dangling, cycles,
      duplicates) — afm's job
    - Do not validate step content beyond structural presence of
      name/title (already enforced by `parse_dsl`)
    - Do not bypass exceptions from `parse_dsl`, `serialize_flow`, or
      the file read/write calls
    - Do not mutate the (header, format, body) tuple returned by `parse_dsl`
      BEFORE building `PipelineDocument` — `PipelineDocument` must reflect
      the original parsed body. Mutations during workflow reconstruction
      operate on a separate copy or a separately constructed sequence
    - Do not include inline prompt overrides in the `FlowDocument` — they
      are goga-side artifacts, not part of the afm flow-file
    - Do not call any host-side wrapper path resolver — the
      `WorkflowStage` agent value is composed into the wrapper path directly
    - Do not validate the `WorkflowStage` agent value against a known agent set —
      absence of the wrapper file is surfaced by afm at invocation time
    - Do not silently skip unknown workflow.stages names — strict validation
      raises a structural error (a workflow does not silently cover multiple
      pipelines); dangling extend.<name>.before/.after refs are likewise a
      structural error at 4a0-pre (symmetric strictness, no silent WARNING+skip)
    - Do not validate before/after refs for cycles, self-references, or
      duplicates — existence only is checked at 4a0-pre; ordering/cycle concerns
      remain afm's responsibility
    - Do not leave dangling depends_on after a skip — dependents are
      transparently reconnected to the skipped stage's predecessors
    - Do not leak skip-removed stages into `PipelineDocument` body — it stays
      the original parsed body; reconstruction operates on a copy/sequence
    - Do not leak injected defaults into `PipelineDocument` body — the
      default agents=["auto"] value is an output-side
      artifact of `FlowStage` assembly, never a property of the parsed
      representation. The injection helper must operate on a copy of the
      step body so the original body stays clean
    - Do not let an extend-entry's inline agent / loop reach the
      flow-file as stage fields — they are model fields consumed as override
      defaults, not body keys
    - Do not let an inline extend agent / loop override an explicit
      stages-block value for the same name — the stages block wins per
      field (inline fields are defaults only)
    - Do not merge extend-entry skills with pipeline skills — extend
      skills are verbatim (a new stage has no pipeline side)
    - Do not inject defaults when the source step body carries a non-empty
      roles value — authored roles always wins; the compiler must not
      second-guess the user's choice of roles
    - Do not embed extend-stages after loop-expansion — the order
      4a0 → 4a → 4b → 4c is mandatory (correctness of the symmetric
      before/after → LAST expanded-id rewriting in STAGES, and of PHASES
      positional insertion preceding loop-expansion, depends on it)
    - Do not add a depends_on field to PhaseStep or carry explicit depends_on
      for PHASES extend-stages — PHASES positioning is achieved by list
      insertion only (positional derivation in step 5)
    - Do not validate before/after refs for cycles, self-references, or
      duplicates after the existence check — existence IS validated at 4a0-pre
      (a dangling ref is a structural error); only cycles, self-references, and
      duplicates remain afm's responsibility (as for ordinary depends_on)
    - Do not change the stages block contract — overrides/loop/silent-skip
      of unknown names are unchanged
    - Do not leak embedded extend-stages into `PipelineDocument` body — it
      stays a faithful mirror of the original parsed body (as for the current
      workflow-reconstructed stages)
    - Do not perform any approve action for a stage without a workflow entry
      (effective approve is None) — approve is workflow-driven, never inferred
      from the body
    - Do not emit interactive: true for a communication-effect directive
      (auto/plan) + communication:true — suppress means omission, not
      interactive:false
    - Do not emit auto_approve unless a roles-effect directive (auto/dialog)
      AND planner-in-roles
    - Do not let authoring script keys (before_script / script / after_script)
      pass through to output as unknown keys — they are consumed and
      translated to script_before / script / script_after
    - Canonical key order is fixed at `FlowStage` assembly — the full
      order (interactive, auto_approve, command, prompt, description, agents,
      supervisor, supervisor_prompt, skills, script_before, script,
      script_after, then alphabetically-sorted unknown keys)
    - Do not leak auto_approve or script_* into `PipelineDocument` — they
      are output-side `FlowStage` fields only

"translate_role(role: str) -> name: str":
  location: compile_flow.py
  annotations: |
    Translate a single role alias from the input DSL vocabulary into the
    canonical afm agent name, which is identical to the corresponding
    default prompt-file stem. This is the single source of truth for the
    role <-> {agent-name, prompt-file-stem} bijection — used by the
    stage-list translation in `compile_flow` (role values -> output
    agents values) and imported by the prompt-materialization consumer
    in goga/pipeline (role override fields -> <stem>.md files).

    `role`: a role value/field-name from the input DSL (planner, executor,
            reviewer, or any other string)
    `name`: the canonical afm agent name == prompt-file stem

    Algorithm:
    1. Map the known aliases: planner -> planning, executor -> implementation,
       reviewer -> review
    2. For every other value (including summary, auto, and any other afm
       agent name) — return the input unchanged (verbatim); goga does not
       interpret the open afm agent namespace

    Requirements:
    - The mapping is exactly {planner: planning, executor: implementation,
      reviewer: review}; no other alias is translated
    - Non-alias values pass through verbatim without validation

    Constraints:
    - Do not validate role values against a known set — the afm agent
      namespace is open (summary, auto, and arbitrary agent names pass
      through)
    - Do not inject auto — auto is a compiler-side default for stages
      with no usable roles value, never a role value authored in the DSL

---

Author: Goga
CreatedAt: 15/07/26
Description: |
  Pure transformer cell that compiles goga DSL pipeline-files (phases-list
  or stages-map) into afm flow-files (flat YAML with optional prompt, name,
  description, and stages). Optionally applies declarative workflow
  instructions to reconstruct the parsed body before serialization.
