Skip to content

Configuration Precedence Rules

Configuration Precedence Rules

Prodigy loads configuration from multiple sources with a clear precedence hierarchy. Understanding how configuration is merged helps you control which settings take effect.

Precedence Hierarchy

From highest to lowest priority:

  1. Project Config (.prodigy/config.yml) - Highest priority
  2. Project-specific settings in your repository
  3. Located at .prodigy/config.yml in your project directory
  4. Overrides all default values
  5. Committed to version control (be careful with secrets)

  6. Defaults (lowest priority)

  7. Built-in default values defined in the code
  8. Used when project config doesn't provide a value
  9. See source: src/config/mod.rs:88-100

Note: Global config (~/.prodigy/config.yml), environment variables (PRODIGY_*), and CLI flag overrides are defined in the code but not currently loaded in production. Only project-level configuration and defaults are active.

How Settings Are Loaded

When Prodigy starts, it builds the final configuration with this process:

  1. Initialize with defaults - Create GlobalConfig with built-in defaults (source: src/config/mod.rs:88-100)
  2. Load project config - Read .prodigy/config.yml from project directory (source: src/config/loader.rs:85-104)
  3. Merge at field level - Project config values override defaults on a per-field basis (source: src/config/mod.rs:133-154)

Project config overrides default values at the individual field level. Settings not specified in project config inherit from defaults.

Examples

Example 1: Using Defaults

# No .prodigy/config.yml file exists

Result: Prodigy uses all default values: - log_level: "info" - auto_commit: true - max_concurrent_specs: 1

(Source: src/config/mod.rs:88-100)

Example 2: Project Config Override

# .prodigy/config.yml (project config)
name: my-project
claude_api_key: "sk-project-key"
auto_commit: false

Result: - claude_api_key: "sk-project-key" (from project config) - auto_commit: false (from project config) - log_level: "info" (from defaults - not specified in project) - max_concurrent_specs: 1 (from defaults - not specified in project)

Field-level precedence is implemented via getter methods (source: src/config/mod.rs:133-154):

pub fn get_auto_commit(&self) -> bool {
    self.project
        .as_ref()
        .and_then(|p| p.auto_commit)
        .or(self.global.auto_commit)
        .unwrap_or(true)  // Default if neither provides value
}

Example 3: Partial Project Override

# .prodigy/config.yml
name: my-project
claude_api_key: "sk-abc123"
# Other fields not specified

Result: - claude_api_key: "sk-abc123" (from project config) - log_level: "info" (from defaults) - auto_commit: true (from defaults) - max_concurrent_specs: 1 (from defaults)

Field-Level Precedence

Precedence is applied per field, not per file. Each configuration field is resolved independently using the precedence rules.

# .prodigy/config.yml (project config)
name: my-project
auto_commit: false  # Only override auto_commit
# Other fields inherited from defaults

Precedence Logic (source: src/config/mod.rs:133-154): 1. Check if project config has the field → use it 2. Otherwise, check if global config has the field → use it 3. Otherwise, use the default value

This allows fine-grained configuration: override only what you need, inherit the rest.

Configuration Loading Implementation

The configuration loading happens in these steps (source: src/config/loader.rs):

Step 1: Initialize

// ConfigLoader::new() - line 23
let config = Config::new();  // Creates Config with GlobalConfig defaults

Step 2: Load Project Config (optional)

// ConfigLoader::load_project() - line 85
let config_path = project_path.join(".prodigy").join("config.yml");
if config_path.exists() {
    let content = fs::read_to_string(&config_path).await?;
    let project_config = parse_project_config(&content)?;
    *config = merge_project_config(config.clone(), project_config);
}

Step 3: Access with Precedence

// Config::get_claude_api_key() - line 133
self.project
    .as_ref()
    .and_then(|p| p.claude_api_key.as_deref())  // Try project first
    .or(self.global.claude_api_key.as_deref())   // Fall back to global

Default Values

Built-in defaults (source: src/config/mod.rs:88-100):

impl Default for GlobalConfig {
    fn default() -> Self {
        Self {
            prodigy_home: get_global_prodigy_dir()
                .unwrap_or_else(|_| PathBuf::from("~/.prodigy")),
            default_editor: None,
            log_level: Some("info".to_string()),
            claude_api_key: None,
            max_concurrent_specs: Some(1),
            auto_commit: Some(true),
            plugins: None,
        }
    }
}

Future: Global Config and Environment Variables

The codebase includes infrastructure for additional configuration sources, but these are not currently loaded in production:

Global Config (~/.prodigy/config.yml): - Mentioned in documentation (line 49: src/config/mod.rs) - No loader implementation yet - Would provide user-level defaults across all projects

Environment Variables: - Defined in Config::merge_env_vars() (lines 111-131: src/config/mod.rs) - Supports: PRODIGY_CLAUDE_API_KEY, PRODIGY_LOG_LEVEL, PRODIGY_EDITOR, PRODIGY_AUTO_COMMIT - Only called in tests, not in production code - Would override file-based configuration when implemented

CLI Flag Overrides: - No implementation yet - Would provide highest-priority overrides for individual runs

Test Coverage

Configuration precedence behavior is validated through comprehensive tests (source: src/config/loader.rs:113-334):

Test: Default Configuration

// Line 120: test_new_creates_default_config
// Verifies GlobalConfig defaults are set correctly
assert_eq!(config.global.log_level, Some("info".to_string()));
assert_eq!(config.global.max_concurrent_specs, Some(1));
assert_eq!(config.global.auto_commit, Some(true));

Test: Project Config Loading

// Line 230: test_load_project_config
// Verifies .prodigy/config.yml is loaded and merged
let project = config.project.unwrap();
assert_eq!(project.name, "test-project");
assert_eq!(project.claude_api_key, Some("test-key".to_string()));
assert_eq!(project.auto_commit, Some(false));

Test: Field-Level Override

// src/config/mod.rs:471 - test shows project overrides global
config.project = Some(ProjectConfig {
    name: "test".into(),
    claude_api_key: Some("project-key".into()),
    // ... other fields
});
assert_eq!(config.get_claude_api_key(), Some("project-key"));