Usages:
  convention: .goga/usages/conventions.md
  yaml: |
    Use yaml.safe_load() to parse .goga/config.yml.
    Requires the PyYAML library.

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

  All data model classes in this cell are immutable dataclasses
  (frozen=True, kw_only=True), per `convention`. Use the standard library
  dataclasses module — NOT pydantic (pydantic is treated as tech debt in this
  project).

  Use the `yaml` practice for parsing .goga/config.yml via yaml.safe_load().

  The cell enforces structural validation only; semantic validation is
  deferred to the owning consumers.

  The build section is two-part: the build root carries the tasks-pass
  settings source (agent, env, max_iterations, session_timeout, idle_timeout,
  wait, prompts_dir, agents_dir, proxy, hosts); the build.review key carries
  the review-pass settings source (skip, agent, env, roles, base_ref,
  strategy, finalize, additional, max_iterations, and the session knobs). The
  loader extracts
  known fields only — unknown keys are silently
  ignored. Inheritance (review from root, additional.agent from
  review.agent) belongs to the consuming cell — values are exposed verbatim
  with no default merging.

---

"load_project_config() -> config: ProjectConfig":
  location: loader.py
  annotations: |
    Parses .goga/config.yml from the project root and returns a ProjectConfig instance.

    Algorithm:
    1. Locate .goga/config.yml in the project root
    2. Parse the YAML via the `yaml` practice
    3. Validate the parsed document is a mapping; raise FileNotFoundError when
       the file is absent/empty, ValueError when it is not a mapping
    4. Extract and validate the top-level fields (language, image,
       dockerfile); the language key is required — a missing key raises
       KeyError
    5. Extract the pipeline block
    6. Extract the build block. Absent → build None; present but not a
       mapping → ValueError. From the mapping extract the tasks-pass root
       fields: agent (OPTIONAL — absent/YAML-null/empty/whitespace → None; a
       non-string value → ValueError), env (optional string mapping, default
       empty dict), max_iterations (OPTIONAL int — absent/YAML-null → None; a
       non-int value including a YAML boolean → ValueError), session_timeout,
       idle_timeout, wait (OPTIONAL strings — the agent emptiness pattern),
       prompts_dir and agents_dir (OPTIONAL strings), proxy (OPTIONAL string),
       hosts (optional string mapping, default empty dict). Unknown keys of
       the mapping are ignored — the loader extracts known fields only
    7. Extract the optional review sub-mapping of build: absent/YAML-null →
       review None; present but not a mapping → ValueError. Fields: skip
       (absent/null → None; non-bool → ValueError); agent (the agent
       pattern); env (the env pattern); roles (absent/null → None; non-list
       or a non-string element → ValueError; an empty list passes verbatim);
       base_ref (the agent pattern); strategy (absent/null/empty/whitespace →
       None; non-string → ValueError — structural typing only, the
       full|medium|short whitelist belongs to the consumer); finalize
       (absent/null/empty/whitespace → None; non-string → ValueError — the
       user-authored final review prompt, stored verbatim); additional
       (absent/null → None; present but not a mapping → ValueError; inside:
       agent — the agent pattern; patience — absent/null → None, non-int
       including a YAML boolean → ValueError; max_iterations — the patience
       pattern); the session knobs (the root pattern); max_iterations (the
       patience pattern — the review-pass iteration cap, stored verbatim).
       Construct a
       `ReviewConfig` from the resolved fields and an `AdditionalReviewConfig`
       from the additional mapping, and pass them into `BuildConfig`
    8. Extract the codemanifest, lint, commands, tools, and usages
       blocks
    9. Extract the topics block: absent/YAML-null → topics None;
       present but not a mapping → ValueError. base_ref — an optional
       string (absent/YAML-null/empty/whitespace → None; non-string →
       ValueError). The create, update, and propagate sub-mappings:
       absent/YAML-null → None; present but not a mapping → ValueError;
       inside each, strategy and commit are optional strings with the
       same pattern. Unknown keys are silently ignored — the loader
       extracts known fields only
    10. Construct and return the `ProjectConfig` from all assembled parts

    Requirements:
    - Top-level image is the Docker image (None is valid — consumers raise
      ClickException when they need a concrete image)
    - pipeline block is OPTIONAL (None when the key is absent;
      present-but-non-mapping → ValueError); WHEN present, pipeline.agent is
      OPTIONAL — absent/YAML-null/empty/whitespace → None; a non-string value →
      ValueError. The agent value is consumed by the in-container run
      coordination (goga/pipeline); no host surface guards it
    - pipeline.env is optional, defaults to an empty mapping
    - pipeline.proxy is optional, defaults to None
    - pipeline.hosts is optional, defaults to an empty mapping
    - build block is OPTIONAL (None when the key is absent;
      present-but-non-mapping → ValueError)
    - The two-part build model: root tasks-pass fields plus the optional
      review sub-mapping; values verbatim, no default merge — inheritance is
      the consumer's
    - The loader extracts known fields only: any unknown key is silently
      ignored — not an error, not stored
    - strategy and finalize are structural string checks — the whitelist and
      the prompt semantics belong to the consumer
    - build.proxy is optional, defaults to None
    - build.hosts is optional, defaults to an empty mapping
    - codemanifest is optional
    - tools is optional; values are stored verbatim with NO semantic validation
      — invalid forms (operator-prefixed, malformed numerics) pass through
      load_project_config. The loader is NOT the validation authority; the consumer
      that owns the version grammar surfaces invalid values as ValueError at
      resolution time
    - usages is optional; None when the key is absent; present-but-non-mapping
      → ValueError; each dep requires a non-empty git (KeyError when missing,
      ValueError when invalid); ref is optional and defaults to None
    - usages <dep>.root is optional; absent or empty/separator/whitespace-only → None
      (≡ clone root); a non-string value → ValueError; a non-empty string is structurally
      validated (no "..", no absolute path) at this boundary
    - usages <group>/<dep> keys are validated as filesystem path segments: a
      name that is empty, a traversal segment (""/"."/".."), or contains a path
      separator ("/" or "\") → ValueError. These keys flow verbatim into
      .goga/usages/<group>/<dep>/ in the downstream usages-sync consumer, so
      traversal names are rejected at the config boundary to keep deploys
      inside the target root
    - lint is optional (None when absent/YAML-null; present-but-non-mapping
      → ValueError); lint.ignore is optional (absent/YAML-null/empty →
      empty list; present-but-non-list or a non-string element → ValueError);
      no semantic validation of ignore contents at the loader level
    - Values are exposed as-is without default merge — consumers apply their own defaults
    - topics is optional (None when absent/YAML-null; present-but-non-mapping
      → ValueError); base_ref and the three sub-sections are structural
      string checks — the strategy whitelists, the template grammar, and
      the defaults belong to the consuming domain

    Constraints:
    - Do not default image — None is a valid value, surface it to the caller
    - Do NOT enforce presence of pipeline/build at the loader level — that
      validation is the consuming command's responsibility (the pipeline and
      build commands). The loader keeps the language requirement (KeyError) and
      the mapping/inner-field validation of any PRESENT section
    - Do NOT validate tools value semantics at the loader level — operator
      syntax, x-range grammar, and the 'latest' keyword belong to the consumer
      that owns the version grammar. The loader only enforces that each value
      is a string (structural type check)
    - Do NOT normalise YAML-null tools values — 'viewer:' (null) is a structural
      type error and raises ValueError; it is NOT silently coerced to 'latest'
    - Do NOT resolve or stat root against the filesystem here — the cloned repository is
      owned by the usages-sync consumer; only structural path safety is enforced at the
      config boundary
    - Do NOT treat empty-string root as an error — normalize it to None (clone root)
    - Do NOT validate ignore path semantics at the loader level — glob,
      traversal, and normalization belong to the consumer; the loader enforces
      only the structural type (list of strings)
    - Do NOT reject glob characters in ignore entries — they are stored
      verbatim; the consumer (lint/AST) documents them as unsupported
    - Do not validate review semantics (roles whitelist, env-requires-agent,
      base_ref resolvability, strategy whitelist, patience and max_iterations
      ranges) — consumer territory
    - Do not default-merge root values into review — the consumer applies
      inheritance
    - Do NOT validate strategy values or template semantics at the loader
      level; Do NOT treat an unknown key of a section as an
      error — it passes through verbatim with no warning and no effect
    - The final `ProjectConfig` assembly MUST include topics
    - The final `ProjectConfig` assembly MUST include lint

"ProjectConfig(language: str, image: str | None, dockerfile: str | None, build: BuildConfig | None, pipeline: PipelineConfig | None, commands: dict, codemanifest: CodemanifestConfig | None, tools: dict[str, str] | None, usages: dict[str, dict[str, DepConfig]] | None = None, lint: LintConfig | None = None, topics: TopicsConfig | None = None)":
  location: config.py
  annotations: |
    Root project configuration object. Constructed by load_project_config.

    `language`: project language directive
    `image`: top-level Docker image shared by build and pipeline; None is a valid value
    `dockerfile`: top-level path to a project Dockerfile; None is a valid value
    `build`: build configuration in the two-part form as a `BuildConfig`
             instance, or None when the build section is absent in
             .goga/config.yml
    `pipeline`: pipeline configuration as a `PipelineConfig` instance, or None
                when the pipeline section is absent in .goga/config.yml
    `commands`: command hooks — reserved for future prompt customization
    `codemanifest`: CODEMANIFEST configuration as a `CodemanifestConfig` instance
    `tools`: optional raw mapping of goga-tool version declarations; values are
             strings in the four-form grammar (1.0.x, 1.x, 1.0.1, latest) but
             the loader performs NO semantic validation — invalid values pass
             through verbatim and surface as ValueError at the consumer's
             resolution step
    `usages`: optional usages-sync declarations from the `usages` section, as a
              dict[str, dict[str, DepConfig]] or None; defaults to None at the
              dataclass level (kw_only), matching commands/codemanifest/tools
    `lint`: optional lint configuration; instance of `LintConfig` or None when
            the lint section is absent; defaults to None (kw_only); callers may
            omit lint=
    `topics`: optional fast-creation configuration; instance of
              `TopicsConfig` or None when the topics section is absent;
              defaults to None (kw_only), callers may omit topics=
  properties:
    "language -> str": |
      Project language. Sourced from the root language directive in .goga/config.yml.
    "image -> str | None": |
      Top-level Docker image shared by build and pipeline execution.
      Sourced from the top-level image field in .goga/config.yml.
      None is a valid value — consumers must handle the None case (e.g. raise ClickException).
    "dockerfile -> str | None": |
      Path to a project Dockerfile, relative to the project root. Shared by build
      and pipeline. None is a valid value — when set, --update builds locally
      (docker_update → DockerBuilder); when None, --update pulls (docker_pull).
    "build -> BuildConfig | None": |
      Build configuration from .goga/config.yml in the two-part form: the
      root tasks-pass settings and the optional review part. Instance of
      `BuildConfig`, or None when the build section is absent. Consumers that
      need it (goga/commands/build) guard the None case and raise
      ClickException before any field access.
    "pipeline -> PipelineConfig | None": |
      Pipeline configuration from .goga/config.yml. Instance of `PipelineConfig`,
      or None when the pipeline section is absent. Consumers that need it
      (goga/commands/pipeline) guard the None case and raise ClickException
      before any field access.
    "commands -> dict": |
      Command hooks from .goga/config.yml.
      Reserved for future prompt customization — currently unused.
      Defaults to an empty dict when the section is absent.
    "codemanifest -> CodemanifestConfig | None": |
      CODEMANIFEST configuration from .goga/config.yml.
      Instance of `CodemanifestConfig`.
      Returns None when the codemanifest section is absent.
    "tools -> dict[str, str] | None": |
      Raw mapping of goga-tool version declarations from .goga/config.yml.
      Keys are tool names (without goga-tool- / goga_tool_ prefix); values are
      version-form strings. Defaults to None when the tools section is absent
      or YAML-null. Returns an empty dict when the section is present but empty.
      The loader stores values verbatim — NO semantic validation of the four-form
      grammar (1.0.x, 1.x, 1.0.1, latest) is performed at the config layer.
      Invalid forms (operator-prefixed '==1.0', malformed '1.x.0') pass through
      load_project_config and surface as ValueError at the consumer's resolution step.
      YAML-null values (e.g. 'viewer:') are rejected by the
      loader as a structural type error before reaching this field.
    "usages -> dict[str, dict[str, DepConfig]] | None": |
      Optional usages-sync declarations from the usages section of .goga/config.yml.
      Structure: {group: {dep: DepConfig}}. None when the section is absent; empty dict
      when present but empty. Dynamic <group>/<dep> names are dict keys (NOT dataclass
      fields) so the goga config usages.<group>.<dep> dot-notation works.
      Defaults to None at the dataclass level (kw_only) — matches commands/codemanifest/tools.
    "lint -> LintConfig | None": |
      Optional lint configuration from .goga/config.yml. Instance of
      `LintConfig`, or None when the lint section is absent. Defaults to None
      (kw_only) so ProjectConfig(...) callers may omit lint=.
    "topics -> TopicsConfig | None": |
      The topics configuration of .goga/config.yml — the shared base and
      the operation sections. Instance of `TopicsConfig`, or None when
      the topics section is absent. Defaults to None (kw_only) so
      ProjectConfig(...) callers may omit topics=.

"BuildConfig(agent: str | None, env: dict[str, str], max_iterations: int | None, session_timeout: str | None, idle_timeout: str | None, wait: str | None, prompts_dir: str | None, agents_dir: str | None, proxy: str | None, hosts: dict[str, str], review: ReviewConfig | None)":
  location: config.py
  annotations: |
    Build execution configuration in the two-part form. Constructed by
    load_project_config from the build section of .goga/config.yml. The root
    fields are the tasks-pass settings source; the review part is the
    review-pass settings source. Inheritance from root into review belongs to
    the consumer.

    `agent`: tasks-pass executor agent name — None when unset
    `env`: tasks-pass environment layer — the review pass never receives it
    `max_iterations`: maximum task iterations
    `session_timeout`, `idle_timeout`, `wait`: session knobs (Go duration strings)
    `prompts_dir`, `agents_dir`: custom ralphex source directories
    `proxy`: optional HTTP/HTTPS proxy URL
    `hosts`: optional host→IP mapping for docker run --add-host
    `review`: the review-pass settings part, or None when absent

    Requirements:
    - All fields may be None; env and hosts default to empty dicts
    - Values stored verbatim, no inheritance applied here
  properties:
    "agent -> str | None": |
      Tasks-pass executor agent name matching the wrapper convention; None
      when unset. Resolution into a wrapper path belongs to the consumer.
    "env -> dict[str, str]": |
      Tasks-pass environment layer ({str: str}), verbatim. Applied as the
      env layer of the tasks pass only — the review pass never receives it.
      Empty dict when absent.
    "max_iterations -> int | None": |
      Maximum number of task iterations of the tasks pass. None when unset.
      Root-only: never inherits into the review part (the review cap is
      review.max_iterations).
    "session_timeout -> str | None": |
      Session timeout duration, Go duration format. None when unset.
    "idle_timeout -> str | None": |
      Session idle timeout duration, Go duration format. None when unset.
    "wait -> str | None": |
      Rate-limit retry wait duration, Go duration format. None when unset.
    "prompts_dir -> str | None": |
      Custom ralphex prompt directory path. None when unset.
    "agents_dir -> str | None": |
      Custom ralphex agent directory path. None when unset.
    "proxy -> str | None": |
      Optional HTTP/HTTPS proxy URL for the build container.
    "hosts -> dict[str, str]": |
      Optional host→IP mapping for docker run --add-host flags. Empty dict
      when absent.
    "review -> ReviewConfig | None": |
      The review-pass settings part. Instance of `ReviewConfig`, or None
      when the build.review key is absent. Structural validity is owned by
      `load_project_config`; semantics and inheritance belong to the consumer.

"ReviewConfig(skip: bool | None, agent: str | None, env: dict[str, str], roles: list[str] | None, base_ref: str | None, strategy: str | None, finalize: str | None, additional: AdditionalReviewConfig | None, session_timeout: str | None, idle_timeout: str | None, wait: str | None, max_iterations: int | None)":
  location: config.py
  annotations: |
    The build.review settings source — the review-pass part of the two-part
    build model. Constructed by load_project_config. Every field is stored
    verbatim; an unset field is None (or an empty dict for env) and means
    "inherit from the root" to the consumer — except max_iterations, which
    never inherits the root (the root value caps the tasks pass alone).

    Requirements:
    - Immutable frozen dataclass (frozen=True, kw_only=True), per `convention`
    - No normalization of emptiness, no whitelist; env resolves
      absent/YAML-null/{} to an empty dict

    Constraints:
    - Do not validate role names, agent names, strategy values, or env
      applicability at this level — structural typing only
  properties:
    "skip -> bool | None": |
      Tri-state source for skipping the review pass. None when the field is
      absent.
    "agent -> str | None": |
      Review-pass executor name matching the wrapper convention. None when
      unset — the consumer inherits the root agent.
    "env -> dict[str, str]": |
      Review-pass environment layer, verbatim. Empty dict when absent — the
      consumer inherits nothing for env (the root env is the tasks-pass
      layer and is never inherited).
    "roles -> list[str] | None": |
      Declared reviewer composition, verbatim. None when unset; an empty list
      stays an empty list (the full default set is the consumer-side meaning).
    "base_ref -> str | None": |
      Review diff base — a branch name or a commit hash, verbatim. None when
      unset (normalized by `load_project_config`).
    "strategy -> str | None": |
      Review strategy source — full, medium, or short; None when unset (the
      consumer applies the default medium). Structural typing only.
    "finalize -> str | None": |
      The user-authored final review prompt, verbatim. None when unset — the
      finalize step stays at the ralphex default (off).
    "additional -> AdditionalReviewConfig | None": |
      The external-review block source. Instance of `AdditionalReviewConfig`,
      or None when absent.
    "session_timeout -> str | None": |
      Review-pass session timeout; None inherits the root value.
    "idle_timeout -> str | None": |
      Review-pass idle timeout; None inherits the root value.
    "wait -> str | None": |
      Review-pass rate-limit wait; None inherits the root value.
    "max_iterations -> int | None": |
      Review-pass iteration cap, verbatim. None when unset — no inheritance:
      the root build.max_iterations caps the tasks pass only and never
      reaches the review part.

"AdditionalReviewConfig(agent: str | None, patience: int | None, max_iterations: int | None)":
  location: config.py
  annotations: |
    The build.review.additional external-review block source.

    `agent`: external review agent name — None when unset (the consumer
             inherits review.agent)
    `patience`: external-review stop threshold (0 = disabled)
    `max_iterations`: external review iteration cap (0 = ralphex auto)

    Requirements:
    - Immutable frozen dataclass (frozen=True, kw_only=True), per `convention`
    - Values stored verbatim; 0 is a meaningful value, not an unset marker

    Constraints:
    - Do not validate agent names or ranges at this level — structural typing
      only
  properties:
    "agent -> str | None": |
      External review agent name matching the wrapper convention. None when
      unset — the consumer inherits review.agent.
    "patience -> int | None": |
      External-review stop threshold — stop after N consecutive unchanged
      rounds; 0 = disabled. None when unset.
    "max_iterations -> int | None": |
      External review iteration cap; 0 = ralphex auto (max(3,
      max_iterations/5)). None when unset.

"PipelineConfig(agent: str | None, env: dict, proxy: str | None, hosts: dict[str, str])":
  location: config.py
  annotations: |
    Pipeline configuration block. Constructed by load_project_config from the pipeline section.
    Semantically distinct from `BuildConfig`: this `agent` drives the
    afm client.command inside the container during pipeline execution.

    `agent`: AI executor identifier used as afm client.command inside the container
             (optional — None when unset)
    `env`: environment variable dictionary
    `proxy`: optional HTTP/HTTPS proxy URL. Same semantics as BuildConfig.proxy,
             consumed by the pipeline container launcher. Defaults to None.
    `hosts`: optional host→IP mapping for docker run --add-host. Defaults to an
             empty dict.

    Requirements:
    - agent is OPTIONAL — absent/YAML-null/empty/whitespace in .goga/config.yml
      resolves to None; a non-string value raises ValueError. The value resolves
      in-container (goga/pipeline) into the afm client command — this cell
      performs no resolution, validation, or guarding
    - env is optional and defaults to an empty dict
    - proxy is optional and defaults to None
    - hosts is optional and defaults to an empty dict
  properties:
    "agent -> str | None": |
      AI executor identifier — agent name as declared in the goga Docker image
      (e.g. "claude", "codex", "opencode", or any other name matching the
      /home/goga/bin/<agent>-as-claude.sh wrapper convention). Resolved at
      runtime by the in-container consumer (goga/pipeline) into an absolute
      wrapper path written into the afm configuration file; this cell does
      no resolution or validation of the value.
      Optional — None when the agent is not configured in .goga/config.yml.
    "env -> dict": |
      Environment variable dictionary ({str: str}).
      Applied in-container as the afm launch env layer, above the inherited
      launch environment; it never travels through the docker launch
      env-file.
      Optional — defaults to an empty dict.
    "proxy -> str | None": |
      Optional HTTP/HTTPS proxy URL for the pipeline container.
      When non-None, the pipeline launcher adds HTTP_PROXY, HTTPS_PROXY, and
      NO_PROXY (fixed at "localhost,127.0.0.1") to the container env-file.
      Defaults to None.
    "hosts -> dict[str, str]": |
      Optional host→IP mapping for "docker run --add-host HOST:IP" flags.
      Defaults to an empty dict when absent in .goga/config.yml.

"CodemanifestConfig(usages: dict, annotations: str | None)":
  location: config.py
  annotations: |
    CODEMANIFEST section configuration from .goga/config.yml.
    Constructed by load_project_config from the codemanifest section.

    `usages`: usage name-to-path mapping ({usage_name: path/to/file.md})
    `annotations`: freeform annotations for the AI agent

    Requirements:
    - usages is required — defaults to an empty dict when absent
    - annotations is optional and defaults to None
  properties:
    "usages -> dict": |
      Usage name-to-path mapping ({usage_name: path/to/file.md}).
      Named practices referenced in project CODEMANIFEST files.
      Defaults to an empty dict when the usages section is absent.
    "annotations -> str | None": |
      Freeform annotations — instructions for the AI agent.
      Optional — defaults to None.

"LintConfig(ignore: list[str])":
  location: config.py
  annotations: |
    Configuration of the optional lint section of .goga/config.yml: the list
    of relative paths excluded from AST traversal during goga lint. Immutable
    frozen dataclass (frozen=True, kw_only=True), per `convention`.

    `ignore`: list of exact relative paths (e.g. .venv/, build/dist)
              excluded from traversal.

    Requirements:
    - `ignore` is stored verbatim; defaults to an empty list when lint.ignore
      is absent/YAML-null/empty
    - Structural validation (list of strings) is performed in
      `load_project_config`; LintConfig does not validate contents

    Constraints:
    - Do not normalize or semantically validate paths at the dataclass level —
      structural validity (list of str) belongs to `load_project_config`,
      semantics to the consumer
  properties:
    "ignore -> list[str]": |
      List of exact relative paths excluded from AST traversal during
      goga lint. Empty list when lint.ignore is absent/empty.

"TopicsConfig(base_ref: str | None, create: TopicsCreateConfig | None, update: TopicsUpdateConfig | None, propagate: TopicsPropagateConfig | None)":
  location: config.py
  annotations: |
    The topics section of .goga/config.yml — the shared base of the
    topic exchange and the per-operation knobs and message templates.

    `base_ref`: any revision string the topic exchange resolves from —
                verbatim, None when unset
    `create`: the topics.create section, or None when absent
    `update`: the topics.update section, or None when absent
    `propagate`: the topics.propagate section, or None when absent

    Requirements:
    - Immutable frozen dataclass (frozen=True, kw_only=True), per
      `convention`
    - Fields are stored verbatim — the empty-to-None normalization
      belongs to `load_project_config`; the strategy whitelists, the
      template grammar, and the defaults belong to the consumer

    Constraints:
    - Do not resolve the revision string, validate strategy values, or
      validate template grammar at this level — structural typing
      belongs to `load_project_config`, semantics to the consumer
  properties:
    "base_ref -> str | None": |
      The base revision of the topic exchange, verbatim from
      .goga/config.yml. None when the field is absent, YAML-null, or
      empty/whitespace-only (normalized by `load_project_config`).
    "create -> TopicsCreateConfig | None": |
      The topics.create section, or None when absent.
    "update -> TopicsUpdateConfig | None": |
      The topics.update section, or None when absent.
    "propagate -> TopicsPropagateConfig | None": |
      The topics.propagate section, or None when absent.

"TopicsCreateConfig(commit: str | None)":
  location: config.py
  annotations: |
    The topics.create section of .goga/config.yml — the commit message
    template of the creation todo commit.

    `commit`: the message template, verbatim — None when unset

    Requirements:
    - Immutable frozen dataclass (frozen=True, kw_only=True), per
      `convention`
    - Stored verbatim; the {slug}/{base} grammar and the default
      template belong to the consumer

    Constraints:
    - Do not validate the template grammar at this level
  properties:
    "commit -> str | None": |
      The commit message template of the creation, verbatim. None when
      the field is absent, YAML-null, or empty/whitespace-only.

"TopicsUpdateConfig(strategy: str | None, commit: str | None)":
  location: config.py
  annotations: |
    The topics.update section of .goga/config.yml — the update strategy
    and the commit message template.

    `strategy`: the strategy name, verbatim — None when unset (the
                consumer applies the default)
    `commit`: the message template, verbatim — None when unset

    Requirements:
    - Immutable frozen dataclass (frozen=True, kw_only=True), per
      `convention`
    - Stored verbatim; the whitelist and the defaults belong to the
      consumer

    Constraints:
    - Do not validate the strategy value or the template grammar at
      this level — an invalid value surfaces as a clean configuration
      error from the consumer, naming the key
  properties:
    "strategy -> str | None": |
      The update strategy source, verbatim. None when the field is
      absent, YAML-null, or empty/whitespace-only.
    "commit -> str | None": |
      The update commit message template, verbatim. None when unset.

"TopicsPropagateConfig(strategy: str | None, commit: str | None)":
  location: config.py
  annotations: |
    The topics.propagate section of .goga/config.yml — the propagation
    strategy and the commit message template.

    `strategy`: the strategy name, verbatim — None when unset (the
                consumer applies the default)
    `commit`: the message template, verbatim — None when unset

    Requirements:
    - Immutable frozen dataclass (frozen=True, kw_only=True), per
      `convention`
    - Stored verbatim; the whitelist and the defaults belong to the
      consumer

    Constraints:
    - Do not validate the strategy value or the template grammar at
      this level — an invalid value surfaces as a clean configuration
      error from the consumer, naming the key
  properties:
    "strategy -> str | None": |
      The propagate strategy source, verbatim. None when the field is
      absent, YAML-null, or empty/whitespace-only.
    "commit -> str | None": |
      The propagate commit message template, verbatim. None when unset.

"DepConfig(git: str, ref: str | None, root: str | None = None)":
  location: config.py
  annotations: |
    Value of a single <dep> declaration inside the usages section of .goga/config.yml.
    Immutable frozen dataclass (frozen=True, kw_only=True), per `convention`.

    `git`: git repository URL (required, non-empty)
    `ref`: optional git ref — branch, tag, or commit. None means "clone the default branch".
    `root`: optional subpath inside the cloned repository from which the usages-sync consumer
            walks .usages folders. None means "root not declared" — walk from the cloned
            repository root. The loader normalizes an explicit empty-string `root` to None
            before construction, so this field never stores "".

    Requirements:
    - git is required and must be a non-empty string
    - ref is optional and defaults to None (explicit absence — clone default branch)
    - root is optional and defaults to None (explicit absence — walk from clone root);
      structural validity of a non-None root is enforced by `load_project_config`

    Constraints:
    - Do not default git — empty/missing git is a structural error raised at load time
    - root is stored verbatim by the dataclass; normalization ("" → None) and path-safety
      validation belong to `load_project_config`, not the dataclass
  properties:
    "git -> str": |
      Git repository URL. Required, non-empty.
    "ref -> str | None": |
      Optional git ref (branch, tag, or commit hash). None means "clone the default branch".
    "root -> str | None": |
      Optional subpath inside the cloned repository from which the usages-sync consumer begins
      walking .usages folders. None means "root not declared" — the deploy consumer walks from
      the cloned repository root. The loader normalizes an explicit
      empty-string root input to None, so this field never holds "". Structural safety
      (no "..", no absolute paths) is enforced by `load_project_config`; resolving a non-None
      root to an existing directory inside the clone is the deploy consumer's responsibility.

---

Author: Goga
CreatedAt: 24/07/26

Description: |
  Project configuration model + loader for .goga/config.yml, with the
  two-part build section (the root tasks-pass settings plus the optional
  review part). Structural validation only; semantic validation is deferred
  to the owning consumers.
