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: for the usages root directive,
  path-safety (no "..", no absolute paths, non-string rejected) is enforced at this
  config boundary; resolving root to an existing directory inside the clone is a
  semantic check deferred to the usages-sync deploy consumer, consistent with this
  cell's "structural validation only; semantic validation deferred to the owning
  consumer" stance.

  The cell enforces structural validation only. The optional lint section
  follows the same stance: the loader enforces that lint.ignore is a list of
  strings; glob interpretation, path normalization, and existence are deferred
  to the owning consumer.

---

"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 required top-level fields, raising KeyError on
       missing fields and ValueError on invalid values:
       - lang from the language directive
       - image (top-level Docker image, optional — None is a valid value)
       - dockerfile (top-level path to a project Dockerfile, optional — None is a
         valid value; a non-string value raises ValueError), parsed like image
    5. Extract the pipeline block. When the pipeline key is absent → set
       pipeline to None (the section is optional at the loader level). When the
       key is present → validate it is a mapping (raise ValueError otherwise)
       and construct a `PipelineConfig` from its fields: agent (OPTIONAL —
       absent/YAML-null/empty/whitespace → None; a non-string value raises
       ValueError), env (optional, default empty dict), proxy (optional, default
       None), hosts (optional, default empty dict)
    6. Extract the build block. When the build key is absent → set build to
       None. When the key is present → validate it is a mapping (raise
       ValueError otherwise), then validate the required task_executor
       sub-block (raise KeyError when missing), construct a
       `TaskExecutorConfig` from agent (OPTIONAL — absent/YAML-null/empty/
       whitespace → None; a non-string value raises ValueError) and env (optional,
       default empty dict), and finally construct a `BuildConfig` from
       task_executor, proxy (optional, default None), hosts (optional, default
       empty dict), plus the remaining optional build fields
    7. Extract the optional codemanifest block; when present, construct a
       `CodemanifestConfig` from its usages and annotations fields, otherwise None
    8. Extract the optional lint block. When the lint key is absent or
       YAML-null → lint=None. When present but not a mapping → raise
       ValueError. When a mapping → extract the optional ignore:
       absent/YAML-null/empty → empty list; present → must be a list whose every
       element is a string (otherwise ValueError). Perform NO semantic validation
       of path contents (glob, existence, normalization) — only the structural
       "list of strings" check. Construct a `LintConfig` from the resolved ignore list.
    9. Extract the optional commands mapping (defaults to empty)
    10. Extract the optional tools mapping via the `yaml` practice. When the key
        is absent or YAML-null → set tools to None. When the key is present but
        the value is not a mapping → raise ValueError. When the value is a
        mapping → validate structurally that every key is a string and every
        value is a string (raise ValueError on non-string keys or values —
        YAML-null values like 'viewer:' are rejected). Perform NO semantic
        validation of value contents: operator-prefixed forms ('==1.0',
        '>=1.0'), malformed forms ('1.x.0', '1.0.0a1'), and any other
        non-grammar strings pass through the loader verbatim. The loader is NOT
        the validation authority for the version grammar — that responsibility
        belongs to the consumer.
    11. Extract the optional usages block. When the usages key is absent → set
        usages to None. When present but not a mapping → raise ValueError. When a mapping
        → for each group (str key → mapping) and each dep (str key → mapping):
        - require dep.git (non-empty str) — KeyError when missing, ValueError when invalid
        - dep.ref optional (str or absent) → None when absent
        - dep.root optional (str or absent): when absent → None; when present and not a str
          → raise ValueError; when present and empty/separator/whitespace-only → normalize
          to None (≡ "no root", not an error); otherwise validate the root path structurally
          — reject path-escape (".." as any segment) and absolute paths (leading "/" or a
          UNC root "//host/share") with ValueError; a trailing separator is insignificant (normalized)
        - validate each <group>/<dep> key as a plain path segment: reject empty, "." / "..",
          or any name containing "/" or "\" (ValueError) — these keys become
          .goga/usages/<group>/<dep>/ segments in the downstream usages-sync consumer
        - construct a `DepConfig`(git, ref, root) per dep, preserving group/dep as dict keys
        Build usages as dict[str, dict[str, DepConfig]]; empty mapping when present-but-empty
    12. Construct and return a `ProjectConfig` from all assembled parts, including
        dockerfile, tools, usages, and lint

    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 pipeline command raises a clean ClickException when it needs an agent
    - 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); WHEN present, build.task_executor
      sub-block is required (KeyError when missing), but build.task_executor.agent is
      OPTIONAL — absent/YAML-null/empty/whitespace → None; a non-string value →
      ValueError. the build command raises a clean ClickException when it needs an agent
    - 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

    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
    - The final `ProjectConfig` assembly MUST include lint

"ProjectConfig(lang: 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)":
  location: config.py
  annotations: |
    Root project configuration object. Constructed by load_project_config.

    `lang`: 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 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,
              so existing ProjectConfig(...) call sites that omit usages= remain
              valid
    `lint`: optional lint configuration; instance of `LintConfig` or None when
            the lint section is absent; defaults to None (kw_only); callers may
            omit lint=
  properties:
    "lang -> 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. 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,
      so existing ProjectConfig(...) call sites that omit usages= remain valid.
    "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=.

"BuildConfig(task_executor: TaskExecutorConfig, worktree: bool | None, skip_finalize: bool | None, session_timeout: str | None, idle_timeout: str | None, wait: str | None, max_iterations: int | None, review_patience: int | None, prompts_dir: str | None, agents_dir: str | None, codex_review: bool | None, proxy: str | None, hosts: dict[str, str])":
  location: config.py
  annotations: |
    Build execution configuration. Constructed by load_project_config from the build section of .goga/config.yml.

    `task_executor`: AI agent configuration. Required.
    `proxy`: optional HTTP/HTTPS proxy URL (e.g. "http://corp:3128"). When non-None,
            consumers write HTTP_PROXY/HTTPS_PROXY/NO_PROXY into the container env-file.
            Defaults to None.
    `hosts`: optional host→IP mapping for "docker run --add-host HOST:IP" flags. Empty dict
             when the section is absent. Consumers merge CLI --add-host flags on top.
    All remaining fields are optional and default to None.

    Requirements:
    - task_executor is required
    - All other fields may be None; hosts defaults to an empty dict
  properties:
    "task_executor -> TaskExecutorConfig": |
      AI agent configuration. `TaskExecutorConfig` instance.
      Required field.
    "worktree -> bool | None": |
      Enable isolated git worktree execution.
    "skip_finalize -> bool | None": |
      Skip the ralphex finalization step.
    "session_timeout -> str | None": |
      Session timeout duration. Go duration format ("30m", "1h").
    "idle_timeout -> str | None": |
      Session idle timeout duration. Go duration format.
    "wait -> str | None": |
      Rate-limit retry wait duration. Go duration format.
    "max_iterations -> int | None": |
      Maximum number of task iterations.
    "review_patience -> int | None": |
      Stop review after N consecutive rounds with no changes.
    "prompts_dir -> str | None": |
      Custom ralphex prompt directory path.
    "agents_dir -> str | None": |
      Custom ralphex agent directory path.
    "codex_review -> bool | None": |
      Enable external codex review.
    "proxy -> str | None": |
      Optional HTTP/HTTPS proxy URL for the build container.
      When non-None, consumers add HTTP_PROXY, HTTPS_PROXY, and NO_PROXY to the
      container env-file (NO_PROXY is fixed at "localhost,127.0.0.1").
      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.

"TaskExecutorConfig(agent: str | None, env: dict)":
  location: config.py
  annotations: |
    AI agent configuration for task execution.
    Constructed by load_project_config from the task_executor section.

    `agent`: AI executor identifier (optional — None when unset)
    `env`: environment variable dictionary

    Requirements:
    - agent is OPTIONAL — absent/YAML-null/empty/whitespace in .goga/config.yml
      resolves to None; a non-string value raises ValueError. the build command raises a
      clean ClickException when it needs an agent.
    - env 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 consumer (goga/build) into an absolute wrapper path;
      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}).
      Passed to the AI executor at launch to configure models and endpoints.
      Optional — defaults to an empty dict.

"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 TaskExecutorConfig: 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 pipeline command raises a
      clean ClickException when it needs an agent.
    - 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 consumer (goga/commands/pipeline) into an absolute
      wrapper path written into the afm-config tmpfile; 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}).
      Passed to the container at pipeline run time.
      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.

"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 (back-compat default). 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. Structural
  validation only; semantic validation of version-grammar values and of the
  `lint.ignore` paths is deferred to the owning consumer.
