Status: Implemented
Author: Mohamed Ameen
Date: 2026-04-17
Last Updated: 2026-05-09
Reviewers: --
Package: src/config/
Entry Point: N/A (library-only, consumed by CLI and orchestrator)
Version: v0.21.1
The configuration system defines, validates, loads, and persists AutoDev's runtime configuration. It provides a Pydantic v2 schema (AutodevConfig) that enforces strict validation (extra="forbid") on all nested models, a loader that converts .autodev/config.json into validated Python objects, and a factory that produces sensible defaults with platform-dependent model resolution. The schema serves as the single source of truth for all tunable parameters across the pipeline.
In scope:
AutodevConfigroot model and all nested sub-models.load_config()/save_config()for JSON file I/O.default_config()factory with platform-dependent model resolution.expand_paths()for user-home path resolution.REQUIRED_AGENT_ROLEStuple (all 14 roles).require_all_roles()validation.
Out of scope:
- Environment variable overrides (planned but not yet implemented).
- CLI flag-level overrides (handled by Click at the CLI layer).
- Config migration between schema versions.
- Runtime config mutation after load.
The configuration system sits at the foundation of the AutoDev pipeline. Every subsystem -- adapters, orchestrator, tournament engine, QA gates, guardrails, and knowledge -- reads its operational parameters from AutodevConfig. The config file (.autodev/config.json) is loaded once at CLI entry, validated against the Pydantic schema, and threaded through the pipeline as an immutable object.
flowchart TB
CLI["CLI Entry<br/>(Click)"] -->|path| Loader["load_config(path)"]
Loader -->|raw JSON| Validator["AutodevConfig.model_validate_json()"]
Validator -->|ConfigError| UserError["Error to user"]
Validator -->|valid| RoleCheck["require_all_roles()"]
RoleCheck -->|missing roles| UserError
RoleCheck -->|all present| Expand["expand_paths()"]
Expand --> Config["AutodevConfig"]
Config --> Orchestrator
Config --> Tournament["Tournament Engine"]
Config --> QA["QA Gates"]
Config --> Guardrails
Config --> Knowledge["Knowledge System"]
Config --> Adapters
Factory["default_config(platform)"] -->|init| SaveConfig["save_config(cfg, path)"]
- FR-1: Define a strict Pydantic v2 schema for
.autodev/config.jsonthat rejects unknown fields at every nesting level (extra="forbid"). - FR-2: Validate that all 14 required agent roles are present in the
agentsdict at load time. - FR-3: Load config from a JSON file, raising
ConfigErroron file-not-found, read errors, malformed JSON, or validation failures. - FR-4: Save config as pretty-printed JSON with a trailing newline.
- FR-5: Provide a
default_config(platform)factory that generates sensible defaults with platform-dependent model resolution (Claude Code vs Cursor). - FR-6: Support a
schema_versionfield (Literal["1.0.0"]) for forward-compatibility. - FR-7: Expand user-home paths (e.g.,
~/.local/share/...) inHiveConfig.path.
- Crash-safety:
save_config()creates parent directories as needed. JSON writing is a singlewrite_text()call (not atomic via tempfile -- acceptable for config files that are written infrequently and only by user action). - Pydantic v2 strict validation: Every model in the schema uses
ConfigDict(extra="forbid"). This catches typos in config keys and prevents silent misconfiguration. - Deterministic reproducibility: Given the same config file content,
load_config()always produces the sameAutodevConfigobject. - Maintainability: Schema changes are versioned via
schema_version. All models use clear field names with defaults documented.
- Must run on Python 3.11+.
- Config format is JSON (not YAML, TOML, or INI). Chosen for ubiquity and Pydantic's built-in
model_validate_json(). - Must not introduce dependencies beyond
pydanticand stdlib. schema_versionis aLiteral["1.0.0"]-- the schema must be updated (new literal variant) before any breaking changes.
flowchart TB
subgraph "src/config/"
schema["schema.py<br/>AutodevConfig + nested models"]
loader["loader.py<br/>load_config / save_config / expand_paths"]
defaults["defaults.py<br/>default_config / resolve_model"]
end
subgraph "Dependencies"
pydantic["pydantic v2"]
errors["src/errors.py<br/>ConfigError"]
end
schema --> pydantic
loader --> schema
loader --> errors
defaults --> schema
| File | Element | Responsibility |
|---|---|---|
src/config/schema.py |
AutodevConfig |
Root Pydantic model for .autodev/config.json |
src/config/schema.py |
AgentConfig |
Per-agent model/disabled/effort/max_turns configuration |
src/config/schema.py |
BranchConfig |
Per-branch overrides for heterogeneous-model multi-branch tournaments (v0.14.0) |
src/config/schema.py |
TournamentsConfig |
Tournament parameters (plan + impl + phase_review phases) |
src/config/schema.py |
TournamentPhaseConfig |
Per-phase tournament tuning (judges, rounds, convergence, runaway/plateau detectors, voting strategy) |
src/config/schema.py |
QAGatesConfig |
Boolean toggles + tuning for each QA gate (incl. baselines, mutation thresholds) |
src/config/schema.py |
GuardrailsConfig |
Per-task safety caps |
src/config/schema.py |
PRMConfig |
Strategy + thresholds for the trajectory PRM (v0.20.0 A1) |
src/config/schema.py |
PlateauDetectorConfig |
Strategy + slope threshold for plateau detection (v0.20.0 A2) |
src/config/schema.py |
TaskOverridesConfig |
Per-bucket huge-repo max_turns multipliers (v0.20.0 D1) |
src/config/schema.py |
DecayCurveConfig |
Per-event-type confidence-decay curves (v0.20.0 B1) |
src/config/schema.py |
HiveConfig |
File-level settings for cross-project knowledge |
src/config/schema.py |
KnowledgeConfig |
Behavioral config for the two-tier knowledge system |
src/config/schema.py |
REQUIRED_AGENT_ROLES |
Tuple of all 14 mandatory agent role names |
src/config/loader.py |
load_config() |
Load and validate a config file |
src/config/loader.py |
save_config() |
Write config as JSON |
src/config/loader.py |
expand_paths() |
Resolve ~ in file paths |
src/config/defaults.py |
default_config() |
Factory producing sensible defaults |
src/config/defaults.py |
resolve_model() |
Platform-dependent LLM model selection per role |
class AutodevConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
schema_version: Literal["1.0.0"] = "1.0.0"
platform: Literal["claude_code", "cursor", "inline", "auto"] = "auto"
agents: dict[str, AgentConfig]
tournaments: TournamentsConfig
qa_gates: QAGatesConfig = Field(default_factory=QAGatesConfig)
qa_retry_limit: int = 3
user_complexity: Literal["low", "medium", "high", "max"] = "medium"
guardrails: GuardrailsConfig = Field(default_factory=GuardrailsConfig)
task_overrides: TaskOverridesConfig = Field(default_factory=TaskOverridesConfig)
prm: PRMConfig = Field(default_factory=PRMConfig)
plateau_detector: PlateauDetectorConfig = Field(default_factory=PlateauDetectorConfig)
hive: HiveConfig
knowledge: KnowledgeConfig = Field(default_factory=KnowledgeConfig)
hallucination_guard: bool = True
repeated_hypothesis_threshold: float = Field(default=0.6, ge=0.0, le=1.0)
web_search_enabled: bool = False
worktree_sparse_checkout_enabled: bool = False
worktree_pool_enabled: bool = False # v0.21.0 A1
cross_phase_parallelism_enabled: bool = False # v0.21.0 B1
speculative_execution_enabled: bool = False # v0.21.0 B2
def require_all_roles(self) -> None:
missing = [r for r in REQUIRED_AGENT_ROLES if r not in self.agents]
if missing:
raise ValueError(f"missing required agent roles: {missing}")Top-level toggles added since v0.6.0:
| Field | Type | Default | Read by |
|---|---|---|---|
user_complexity |
Literal["low","medium","high","max"] |
"medium" |
tournament.effort.resolve_role_effort() — feeds the architect-effort floor and combines with Plan.complexity to derive per-role --effort |
hallucination_guard |
bool |
True |
qa.hallucination_guard — when False, the gate is skipped entirely |
repeated_hypothesis_threshold |
float ∈ [0,1] |
0.6 |
Multi-branch hypothesis dedup detector (v0.17.0 S4) — bigram-Jaccard threshold; advisory only |
web_search_enabled |
bool |
False |
Executor stuck-recovery ladder (v0.17.0 S2) — opt-in WEB_CONTEXT: block in critic_sounding_board prompts |
worktree_sparse_checkout_enabled |
bool |
False |
Per-task worktree creation (v0.17.0 S6) — narrows checkout to declared edit_scope |
worktree_pool_enabled |
bool |
False |
Execute-phase warm-start worktree pool (v0.21.0 A1) |
cross_phase_parallelism_enabled |
bool |
False |
Execute dispatcher (v0.21.0 B1) — allows file-disjoint phase N+1 tasks while phase N tail finishes |
speculative_execution_enabled |
bool |
False |
Execute dispatcher (v0.21.0 B2) — pre-runs single-parent child tasks; rollback on parent failure |
class AgentConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
model: str | None = None # LLM model alias; None = use platform default
disabled: bool = False # Disable this agent role entirely
max_turns: int | None = None # None = use role default
# v0.13.0 / 0.17.0: per-role override for Claude Code's --effort flag.
# None inherits from the effort resolver (plan + user complexity).
effort: Literal["low", "medium", "high", "xhigh", "max"] | None = Noneeffort is consumed by tournament.effort.resolve_role_effort() (highest priority — explicit per-role override wins over the architect floor and the matrix in tournament/effort.py). The adapter then forwards it via LLMInvocation.effort to claude -p --effort (adapters/claude_code.py) or to the Cursor adapter.
class BranchConfig(BaseModel):
"""Per-branch overrides for heterogeneous-model multi-branch
plan tournaments."""
model_config = ConfigDict(extra="forbid")
model_overrides: dict[str, str] = Field(default_factory=dict)
lane: Literal[
"distant-scout",
"local-tweak",
"architectural",
"constraint-removal",
"incumbent-confirmation",
] = "local-tweak"
risk: Literal["low", "medium", "high"] = "medium"
family: str | None = None| Field | Type | Default | Read by |
|---|---|---|---|
model_overrides |
dict[str, str] |
{} |
Plan-tournament runner — first lookup before falling through to cfg.agents[role].model then resolve_model() |
lane |
Literal[...] |
"local-tweak" |
Multi-branch dispatcher — suffixes the per-branch artifact dir (branch-{i}-{lane}/) and stamps ledger metadata; "distant-scout" is the lane the plateau detector forces |
risk |
Literal["low","medium","high"] |
"medium" |
Advisory only in v0.14.0 — reserved for future risk-cohort gating |
family |
str | None |
None |
Plateau detector — siblings sharing a family are tracked together for per-family plateau detection |
BranchConfig is referenced from TournamentPhaseConfig.branches. Empty list and combination with num_branches > 1 are rejected by _validate_branches.
class TournamentPhaseConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled: bool # Whether this tournament phase runs
num_judges: int # Number of judges per round
convergence_k: int # Consecutive stable-winner rounds to converge
max_rounds: int # Hard cap on tournament rounds
# Runaway / plateau detectors --------------------------------------------
score_stability_window: int | None = None
score_stability_max_delta: int | None = None
winner_stability_window: int | None = None # v0.6.0 / Issue 4
max_plan_lines_growth_ratio: float | None = None # v0.6.2 / Issue 5B
complex_plan_num_judges_override: int | None = None # v0.7.0 / Issue 5C
# Multi-branch / heterogeneity -------------------------------------------
num_branches: int = Field(default=1, ge=1, le=5) # v0.12.0
branches: list[BranchConfig] | None = None # v0.14.0
# Promotion / drift ------------------------------------------------------
promotion_grade_enabled: bool = False # v0.16.0
holdout_evaluation_enabled: bool = False # v0.19.0 C1
drift_verifier_enabled: bool = True # v0.16.0 / 0.17.0
explorer_enabled: bool = False # v0.17.0 S3
# Voting -----------------------------------------------------------------
voting_strategy: Literal["borda", "veto"] = "borda" # v0.18.0 C1
judge_roles: list[str] | None = None # v0.18.0 C3
judge_role_weights: dict[str, float] | None = None # v0.18.0 C3
# Plateau detection ------------------------------------------------------
plateau_detection_enabled: bool = False # v0.18.0 B2
plateau_window: int = 4
cross_family_plateau_enabled: bool = False
cross_family_plateau_window: int = 10
class TournamentsConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
plan: TournamentPhaseConfig
impl: TournamentPhaseConfig
phase_review: TournamentPhaseConfig = Field(default_factory=_default_phase_review_cfg)
max_parallel_subprocesses: int | None = None # v0.10.0 widened to int|None
execute_max_parallel_tasks: int | None = None # v0.11.0
auto_disable_for_models: list[str] = Field(default_factory=lambda: ["opus"])| Field | Type | Default | Read by |
|---|---|---|---|
score_stability_window |
int | None |
None |
tournament.core.Tournament.run — early-terminate when the trailing-window Borda scores barely change |
score_stability_max_delta |
int | None |
None |
Same — paired with the window; sum of |Δscore| across A/B/AB ≤ this triggers early break |
winner_stability_window |
int | None |
None |
tournament.core.Tournament.run — halts when the trailing window's effective_winner is stable on a non-A label (catches the QNX [AB,AB,AB] pattern) |
max_plan_lines_growth_ratio |
float | None |
None (plan default 1.5) |
tournament.core — demotes oversize AB winners (markdown line count > ratio × incumbent lines) to next-best Borda winner |
complex_plan_num_judges_override |
int | None |
None (plan default 7) |
orchestrator.plan_tournament_runner — substitutes num_judges with this value when Plan.complexity == "complex" |
num_branches |
int ∈ [1,5] |
1 |
orchestrator.plan_phase — fans out N independent RNG-seeded branch tournaments via multi_branch_tournament (mutually exclusive with branches) |
branches |
list[BranchConfig] | None |
None |
Multi-branch dispatcher — heterogeneous-model branches; list length wins over num_branches |
promotion_grade_enabled |
bool |
False |
tournament.promotion.decide — drives the on-disk grade rung ladder (dev_best → repeated → promotion_eligible) |
holdout_evaluation_enabled |
bool |
False |
Tournament loop — invokes the holdout runner against baseline tests/ snapshot before promoting repeated → eligible |
drift_verifier_enabled |
bool |
True |
orchestrator.drift_verifier.run_drift_verifier — final-defense gate after A-winner outcome |
explorer_enabled |
bool |
False |
Tournament loop — dispatches an Explorer specialist judge whose FINDINGS: block becomes discard-grade lessons |
voting_strategy |
Literal["borda","veto"] |
"borda" |
tournament.voting — BordaAggregator (default) vs VetoAggregator; impl-tournament runner is the current consumer |
judge_roles |
list[str] | None |
None |
tournament.core (line 1158) — list of specialist judge roles; length wins over num_judges |
judge_role_weights |
dict[str, float] | None |
None |
tournament.core (line 1025) — per-role Borda weighting; missing roles default to weight 1.0 |
plateau_detection_enabled |
bool |
False |
multi_branch_tournament — per-family plateau detection; fires force_distant_scout() |
plateau_window |
int |
4 |
Per-family plateau window count (rule-based strategy) |
cross_family_plateau_enabled |
bool |
False |
multi_branch_tournament — cross-family plateau detection across all families |
cross_family_plateau_window |
int |
10 |
Cross-family plateau window count |
See plateau_detection_design.md and prm.md for the dedicated detector / strategy designs.
class QAGatesConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
syntax_check: bool = True
lint: bool = True
build_check: bool = True
test_runner: bool = True
secretscan: bool = True
secretscan_baseline_enabled: bool = False # v0.19.0
secretscan_per_extension_thresholds: dict[str, float] | None = None # v0.19.0
sast_scan: bool = False # planning-time advisory only
mutation_test: bool = False # planning-time advisory only
mutation_test_enabled: bool = False # v0.19.0 — actual gate dispatch
mutation_test_threshold: float = 0.7| Field | Type | Default | Read by |
|---|---|---|---|
secretscan_baseline_enabled |
bool |
False |
qa.secretscan.run_secretscan — diff-filters findings against .autodev/secretscan-baseline.json; baseline managed via autodev secretscan baseline |
secretscan_per_extension_thresholds |
dict[str, float] | None |
None |
qa.secretscan — per-extension entropy override; None falls back to the module's _DEFAULT_PER_EXTENSION_ENTROPY curve |
mutation_test_enabled |
bool |
False |
QA gate dispatcher — when True, runs qa.mutation_test.run_mutation_test. (Distinct from the legacy mutation_test field which is consumed only by agent prompts for security-tier routing) |
mutation_test_threshold |
float |
0.7 |
qa.mutation_test (passed as kill_rate_threshold in execute_phase.py line 2905) — minimum mutation kill-rate required to pass the gate |
class GuardrailsConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
max_invocations_per_task: int = 60 # round-trip cap (pre_invocation enforced)
max_tool_calls_per_task: int = 60 # cumulative tool-call cap (stream-json)
max_duration_s_per_task: int = 900 # 15 minutes
max_diff_bytes: int = 5_242_880 # 5 MB
cost_budget_usd_per_plan: float | None = None # Reservedclass PRMConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
strategy: Literal["rules", "rules+ml"] = "rules"
ml_threshold: float = Field(default=0.7, ge=0.0, le=1.0)
ml_min_events: int = Field(default=3, ge=1)| Field | Type | Default | Read by |
|---|---|---|---|
strategy |
Literal["rules","rules+ml"] |
"rules" |
orchestrator.execute_phase (line 2483) — chooses between rule-based-only (legacy) and rules + LLM trajectory classifier augmentation |
ml_threshold |
float ∈ [0,1] |
0.7 |
LLMTrajectoryClassifier — confidence cutoff for the LLM classifier output |
ml_min_events |
int ≥ 1 |
3 |
LLMTrajectoryClassifier — cold-start guard; skip the LLM call when the trajectory has fewer events than this |
See prm.md for the full PRM design.
class PlateauDetectorConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
strategy: Literal["rules", "regression"] = "rules"
regression_window: int = Field(default=10, ge=3)
plateau_slope_threshold: float = Field(default=0.1, ge=0.0)| Field | Type | Default | Read by |
|---|---|---|---|
strategy |
Literal["rules","regression"] |
"rules" |
orchestrator.multi_branch_tournament (line 691) — chooses between v0.18.0 rule-based and v0.20.0 OLS-regression detection |
regression_window |
int ≥ 3 |
10 |
Sliding-window size for the cumulative-winner_promoted regression |
plateau_slope_threshold |
float ≥ 0 |
0.1 |
OLS slope below this → plateau flagged → forced lane change |
See plateau_detection_design.md for the full plateau detection design.
class TaskOverridesConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
huge_repo_multipliers: dict[str, float] | None = Nonehuge_repo_multipliers overrides per-bucket multipliers in runtime.repo_probe._HUGE_BUCKET_MULTIPLIERS (defaults: simple 3.0×, medium 2.0×, complex 1.5×). None (default) preserves the baked-in curve. Operator overrides merge per-bucket — missing buckets fall through to the default. Read by tournament.task_overrides and the max_turns resolver.
class HiveConfig(BaseModel):
"""File-level settings for cross-project knowledge."""
model_config = ConfigDict(extra="forbid")
enabled: bool = True
path: Path # e.g. ~/.local/share/autodev/shared-learnings.jsonl
class DecayCurveConfig(BaseModel):
"""v0.20.0 B1: per-event-type confidence decay."""
model_config = ConfigDict(extra="forbid")
half_life_days: float = Field(default=15.0, gt=0.0)
floor: float = Field(default=0.5, ge=0.0, le=1.0)
class KnowledgeConfig(BaseModel):
"""Behavioral config for the two-tier knowledge system."""
model_config = ConfigDict(extra="forbid")
enabled: bool = True
swarm_max_entries: int = 100
hive_max_entries: int = 200
dedup_threshold: float = 0.6
max_inject_count: int = 5
hive_enabled: bool = True
promotion_min_confirmations: int = 3
promotion_min_confidence: float = 0.7
denylist_roles: list[str] = Field(
default_factory=lambda: [
"explorer", "judge", "critic_t",
"architect_b", "synthesizer",
]
)
lane_aware_injection_enabled: bool = True # v0.18.0 B1
decay_curves: dict[str, DecayCurveConfig] | None = None # v0.20.0 B1lane_aware_injection_enabled is consumed by KnowledgeStore.inject_block — when True (default), lessons are filtered by branch lane when a lane= argument is supplied. decay_curves lets state.knowledge._recency_factor apply per-event-type half-life curves (e.g. winner_promoted decays slower than soft_blocker).
REQUIRED_AGENT_ROLES: tuple[str, ...] = (
"architect", "explorer", "domain_expert", "developer",
"reviewer", "test_engineer", "critic_sounding_board",
"critic_drift_verifier", "docs", "designer",
"critic_t", "architect_b", "synthesizer", "judge",
)classDiagram
class AutodevConfig {
schema_version: Literal["1.0.0"]
platform: Literal[...]
user_complexity: Literal[...]
agents: dict[str, AgentConfig]
tournaments: TournamentsConfig
qa_gates: QAGatesConfig
guardrails: GuardrailsConfig
task_overrides: TaskOverridesConfig
prm: PRMConfig
plateau_detector: PlateauDetectorConfig
hive: HiveConfig
knowledge: KnowledgeConfig
hallucination_guard: bool
repeated_hypothesis_threshold: float
web_search_enabled: bool
worktree_sparse_checkout_enabled: bool
worktree_pool_enabled: bool
cross_phase_parallelism_enabled: bool
speculative_execution_enabled: bool
require_all_roles()
}
class AgentConfig {
model: str | None
disabled: bool
max_turns: int | None
effort: Literal[...] | None
}
class BranchConfig {
model_overrides: dict[str, str]
lane: Literal[...]
risk: Literal[...]
family: str | None
}
class TournamentsConfig {
plan: TournamentPhaseConfig
impl: TournamentPhaseConfig
phase_review: TournamentPhaseConfig
max_parallel_subprocesses: int | None
execute_max_parallel_tasks: int | None
auto_disable_for_models: list[str]
}
class TournamentPhaseConfig {
enabled: bool
num_judges: int
convergence_k: int
max_rounds: int
score_stability_window: int | None
score_stability_max_delta: int | None
winner_stability_window: int | None
max_plan_lines_growth_ratio: float | None
complex_plan_num_judges_override: int | None
num_branches: int
branches: list[BranchConfig] | None
promotion_grade_enabled: bool
holdout_evaluation_enabled: bool
drift_verifier_enabled: bool
explorer_enabled: bool
voting_strategy: Literal["borda","veto"]
judge_roles: list[str] | None
judge_role_weights: dict[str, float] | None
plateau_detection_enabled: bool
plateau_window: int
cross_family_plateau_enabled: bool
cross_family_plateau_window: int
}
class QAGatesConfig {
syntax_check: bool
lint: bool
build_check: bool
test_runner: bool
secretscan: bool
secretscan_baseline_enabled: bool
secretscan_per_extension_thresholds: dict | None
sast_scan: bool
mutation_test: bool
mutation_test_enabled: bool
mutation_test_threshold: float
}
class GuardrailsConfig {
max_invocations_per_task: int
max_tool_calls_per_task: int
max_duration_s_per_task: int
max_diff_bytes: int
cost_budget_usd_per_plan: float | None
}
class PRMConfig {
strategy: Literal["rules","rules+ml"]
ml_threshold: float
ml_min_events: int
}
class PlateauDetectorConfig {
strategy: Literal["rules","regression"]
regression_window: int
plateau_slope_threshold: float
}
class TaskOverridesConfig {
huge_repo_multipliers: dict[str, float] | None
}
class HiveConfig {
enabled: bool
path: Path
}
class DecayCurveConfig {
half_life_days: float
floor: float
}
class KnowledgeConfig {
enabled: bool
swarm_max_entries: int
hive_max_entries: int
dedup_threshold: float
max_inject_count: int
hive_enabled: bool
promotion_min_confirmations: int
promotion_min_confidence: float
denylist_roles: list[str]
lane_aware_injection_enabled: bool
decay_curves: dict[str, DecayCurveConfig] | None
}
AutodevConfig --> AgentConfig : agents
AutodevConfig --> TournamentsConfig : tournaments
AutodevConfig --> QAGatesConfig : qa_gates
AutodevConfig --> GuardrailsConfig : guardrails
AutodevConfig --> TaskOverridesConfig : task_overrides
AutodevConfig --> PRMConfig : prm
AutodevConfig --> PlateauDetectorConfig : plateau_detector
AutodevConfig --> HiveConfig : hive
AutodevConfig --> KnowledgeConfig : knowledge
TournamentsConfig --> TournamentPhaseConfig : plan
TournamentsConfig --> TournamentPhaseConfig : impl
TournamentsConfig --> TournamentPhaseConfig : phase_review
TournamentPhaseConfig --> BranchConfig : branches[*]
KnowledgeConfig --> DecayCurveConfig : decay_curves[*]
The config system does not define protocols. It provides data models consumed by all other subsystems.
load_config(path: Path) -> AutodevConfig
Load and validate a config file. Raises ConfigError on any failure (file not found, read error, malformed JSON, validation error, missing roles).
save_config(cfg: AutodevConfig, path: Path) -> None
Write config as pretty-printed JSON. Creates parent directories as needed.
expand_paths(cfg: AutodevConfig) -> AutodevConfig
Return a deep copy with user-home paths resolved (currently hive.path).
default_config(platform: str = "auto") -> AutodevConfig
Return the shipped default configuration with platform-dependent model resolution.
resolve_model(model: str | None, role: str, platform: str) -> str
Resolve an LLM model alias for a given role and platform. Returns the explicit model if set; otherwise applies platform-specific defaults.
| Decision | Rationale | Alternatives Considered |
|---|---|---|
extra="forbid" on all models |
Catches typos and prevents silent misconfiguration. A mistyped key like max_too_calls is rejected immediately rather than silently ignored. |
extra="ignore" (lenient), extra="allow" (permissive) |
| JSON format (not YAML/TOML) | Pydantic v2 has native model_validate_json(). No additional parser dependency. JSON is universally understood. |
YAML (more human-friendly but needs PyYAML), TOML (PEP 680 but less expressive for nested structures) |
schema_version as Literal["1.0.0"] |
Enables forward-compatibility checking. Adding "2.0.0" to the Literal union lets the loader detect and handle both versions. |
Semver string with runtime parsing, integer version |
HiveConfig vs KnowledgeConfig separation |
HiveConfig governs the on-disk file (path + master switch). KnowledgeConfig governs behavioral tuning (ranking, dedup, caps). Operators can disable the hive file entirely without touching behavioral knobs, or vice versa. |
Single KnowledgeConfig with all fields, nested HiveConfig inside KnowledgeConfig |
Platform-dependent model resolution in defaults.py |
Different platforms have different model availability and cost profiles. Cursor's auto model selector behaves differently from Claude Code's alias resolution. Centralizing this logic avoids scattered conditionals. |
Per-platform config files, model aliases resolved at adapter layer |
require_all_roles() as explicit validation |
Ensures the config file has all 14 required agent roles. Failing fast at load time prevents cryptic errors deep in the pipeline when a role is referenced but missing. | Lazy validation on first access, optional roles with fallback |
- Strictness vs. extensibility:
extra="forbid"means users cannot add custom keys to the config. This is intentional -- unknown keys are almost always typos. Custom data should go in separate files. - Required roles enforcement: All 14 roles must be present even if
disabled: true. This ensures the config is always complete. The cost is verbosity for users who want a minimal config. - JSON vs. YAML: JSON lacks comments and is less human-friendly for editing. Mitigated by
autodev initgenerating the default config file. - No env var overrides (yet): The three-layer precedence (env var -> config file -> code defaults) is planned but only the latter two are implemented. Users must edit the config file for all non-default values.
load_config pipeline:
flowchart TB
A["Path exists?"] -->|no| E1["ConfigError: not found"]
A -->|yes| B["read_text(encoding='utf-8')"]
B -->|OSError| E2["ConfigError: could not read"]
B -->|success| C["AutodevConfig.model_validate_json(raw)"]
C -->|ValidationError| E3["ConfigError: invalid config"]
C -->|JSONDecodeError| E4["ConfigError: malformed JSON"]
C -->|success| D["cfg.require_all_roles()"]
D -->|ValueError| E5["ConfigError: missing roles"]
D -->|success| F["Return AutodevConfig"]
resolve_model logic:
flowchart TB
A{model is not None?}
A -->|yes| B["Return model as-is"]
A -->|no| C{platform == cursor?}
C -->|yes| D{role in architect, architect_b?}
D -->|yes| E["opus"]
D -->|no| F{role in reviewer, judge, critic_*, etc?}
F -->|yes| G["sonnet"]
F -->|no| H["auto"]
C -->|no| I{role == architect?}
I -->|yes| J["opus"]
I -->|no| K{role == explorer?}
K -->|yes| L["haiku"]
K -->|no| M["sonnet"]
Platform model resolution defaults:
| Role | Claude Code | Cursor |
|---|---|---|
| architect | opus | opus |
| architect_b | sonnet | opus |
| explorer | haiku | auto |
| developer | sonnet | auto |
| test_engineer | sonnet | auto |
| reviewer | sonnet | sonnet |
| judge | sonnet | sonnet |
| critic_t | sonnet | sonnet |
| critic_sounding_board | sonnet | sonnet |
| critic_drift_verifier | sonnet | sonnet |
| synthesizer | sonnet | sonnet |
| docs | sonnet | sonnet |
| designer | sonnet | sonnet |
| domain_expert | sonnet | sonnet |
The config system is fully synchronous. Config is loaded once at startup and passed immutably through the pipeline. No concurrency concerns.
Not applicable. Config loading does not spawn subprocesses.
save_config() uses a single path.write_text() call. This is not strictly atomic (no tempfile + os.replace()) but is acceptable for config files that are:
- Written only during
autodev initor explicit user action. - Never written concurrently.
- Small (< 10 KB).
All failures in load_config() are wrapped in ConfigError (subclass of AutodevError) with descriptive messages:
| Failure | Exception Chain | Message |
|---|---|---|
| File not found | ConfigError |
config file not found: {path} |
| Read error | ConfigError from OSError |
could not read {path}: {exc} |
| Validation error | ConfigError from ValidationError |
invalid config at {path}: {exc} |
| Malformed JSON | ConfigError from JSONDecodeError |
malformed JSON at {path}: {exc} |
| Missing roles | ConfigError from ValueError |
missing required agent roles: [...] |
- pydantic v2:
BaseModel,ConfigDict,Field,ValidationError-- the core of the schema system. - stdlib:
json,pathlib.Path,typing.Literal - Internal:
errors.ConfigError
The config system is self-referential -- it is the configuration mechanism for AutoDev. The config file location defaults to .autodev/config.json relative to the project root and can be overridden via the --config CLI flag.
Default config file structure (v0.21.1 schema dump):
The block below is generated verbatim by:
uv run python -c "from config.schema import AutodevConfig, AgentConfig, HiveConfig, REQUIRED_AGENT_ROLES, TournamentPhaseConfig, TournamentsConfig; from pathlib import Path; cfg = AutodevConfig(agents={r: AgentConfig() for r in REQUIRED_AGENT_ROLES}, tournaments=TournamentsConfig(plan=TournamentPhaseConfig(enabled=True, num_judges=3, convergence_k=2, max_rounds=15), impl=TournamentPhaseConfig(enabled=True, num_judges=1, convergence_k=1, max_rounds=3)), hive=HiveConfig(path=Path('~/.local/share/autodev/shared-learnings.jsonl'))); print(cfg.model_dump_json(indent=2))"{
"schema_version": "1.0.0",
"platform": "auto",
"agents": {
"architect": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"explorer": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"domain_expert": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"developer": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"reviewer": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"test_engineer": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"critic_sounding_board": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"critic_drift_verifier": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"docs": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"designer": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"critic_t": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"architect_b": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"synthesizer": { "model": null, "disabled": false, "max_turns": null, "effort": null },
"judge": { "model": null, "disabled": false, "max_turns": null, "effort": null }
},
"tournaments": {
"plan": {
"enabled": true, "num_judges": 3, "convergence_k": 2, "max_rounds": 15,
"score_stability_window": null, "score_stability_max_delta": null,
"winner_stability_window": null,
"max_plan_lines_growth_ratio": null,
"complex_plan_num_judges_override": null,
"num_branches": 1, "branches": null,
"promotion_grade_enabled": false, "holdout_evaluation_enabled": false,
"drift_verifier_enabled": true, "explorer_enabled": false,
"voting_strategy": "borda",
"judge_roles": null, "judge_role_weights": null,
"plateau_detection_enabled": false, "plateau_window": 4,
"cross_family_plateau_enabled": false, "cross_family_plateau_window": 10
},
"impl": {
"enabled": true, "num_judges": 1, "convergence_k": 1, "max_rounds": 3,
"score_stability_window": null, "score_stability_max_delta": null,
"winner_stability_window": null,
"max_plan_lines_growth_ratio": null,
"complex_plan_num_judges_override": null,
"num_branches": 1, "branches": null,
"promotion_grade_enabled": false, "holdout_evaluation_enabled": false,
"drift_verifier_enabled": true, "explorer_enabled": false,
"voting_strategy": "borda",
"judge_roles": null, "judge_role_weights": null,
"plateau_detection_enabled": false, "plateau_window": 4,
"cross_family_plateau_enabled": false, "cross_family_plateau_window": 10
},
"phase_review": {
"enabled": true, "num_judges": 3, "convergence_k": 1, "max_rounds": 2,
"score_stability_window": null, "score_stability_max_delta": null,
"winner_stability_window": null,
"max_plan_lines_growth_ratio": null,
"complex_plan_num_judges_override": null,
"num_branches": 1, "branches": null,
"promotion_grade_enabled": false, "holdout_evaluation_enabled": false,
"drift_verifier_enabled": true, "explorer_enabled": false,
"voting_strategy": "borda",
"judge_roles": null, "judge_role_weights": null,
"plateau_detection_enabled": false, "plateau_window": 4,
"cross_family_plateau_enabled": false, "cross_family_plateau_window": 10
},
"max_parallel_subprocesses": null,
"execute_max_parallel_tasks": null,
"auto_disable_for_models": ["opus"]
},
"qa_gates": {
"syntax_check": true, "lint": true, "build_check": true,
"test_runner": true, "secretscan": true,
"secretscan_baseline_enabled": false,
"secretscan_per_extension_thresholds": null,
"sast_scan": false, "mutation_test": false,
"mutation_test_enabled": false, "mutation_test_threshold": 0.7
},
"qa_retry_limit": 3,
"user_complexity": "medium",
"guardrails": {
"max_invocations_per_task": 60,
"max_tool_calls_per_task": 60,
"max_duration_s_per_task": 900,
"max_diff_bytes": 5242880,
"cost_budget_usd_per_plan": null
},
"task_overrides": { "huge_repo_multipliers": null },
"prm": { "strategy": "rules", "ml_threshold": 0.7, "ml_min_events": 3 },
"plateau_detector": {
"strategy": "rules",
"regression_window": 10,
"plateau_slope_threshold": 0.1
},
"hive": {
"enabled": true,
"path": "~/.local/share/autodev/shared-learnings.jsonl"
},
"knowledge": {
"enabled": true,
"swarm_max_entries": 100,
"hive_max_entries": 200,
"dedup_threshold": 0.6,
"max_inject_count": 5,
"hive_enabled": true,
"promotion_min_confirmations": 3,
"promotion_min_confidence": 0.7,
"denylist_roles": ["explorer", "judge", "critic_t", "architect_b", "synthesizer"],
"lane_aware_injection_enabled": true,
"decay_curves": null
},
"hallucination_guard": true,
"repeated_hypothesis_threshold": 0.6,
"web_search_enabled": false,
"worktree_sparse_checkout_enabled": false,
"worktree_pool_enabled": false,
"cross_phase_parallelism_enabled": false,
"speculative_execution_enabled": false
}errors.ConfigError: The sole exception type raised by the loader.
Not applicable. The config system does not consume adapter protocols. It provides AgentConfig.model which adapters use to select the LLM model.
The config system does not write ledger events.
Every major AutoDev subsystem reads from AutodevConfig:
| Consumer | Config Section | Usage |
|---|---|---|
| Orchestrator | platform, agents, qa_retry_limit |
Platform selection, agent roster, retry policy |
| Tournament engine | tournaments |
Phase enable/disable, judge count, convergence, rounds, voting strategy, judge specialists |
| Multi-branch dispatcher | tournaments.plan.{num_branches,branches,plateau_detection_enabled,cross_family_plateau_enabled}, plateau_detector |
Branch fan-out, plateau detection strategy, lane forcing |
| Plan tournament runner | tournaments.plan.complex_plan_num_judges_override |
Judge escalation on complex plans |
| Effort resolver | agents[role].effort, user_complexity |
Per-role --effort derivation |
| Execute phase | prm, worktree_*, cross_phase_parallelism_enabled, speculative_execution_enabled |
PRM strategy/classifier, worktree pooling, scheduler modes |
| QA phase engine | qa_gates |
Which gates to run; mutation/secretscan baselines + thresholds |
| Guardrails enforcer | guardrails |
Per-task safety caps |
| Knowledge system | knowledge, hive |
Behavioral tuning, decay curves, lane-aware injection |
| Adapter factory | agents[role].model, platform |
Model selection per agent role |
max_turns resolver |
task_overrides.huge_repo_multipliers |
Per-bucket huge-repo multipliers |
| Workspace initializer | agents |
Agent definitions and disabled roles |
- Filesystem:
.autodev/config.jsonfor config persistence,~/.local/share/autodev/for hive path.
- Schema validation: Round-trip
AutodevConfigthroughmodel_dump_json()/model_validate_json(). Verify all fields survive. - extra="forbid": Verify that unknown keys raise
ValidationErrorat every nesting level. - REQUIRED_AGENT_ROLES: Verify
require_all_roles()raisesValueErrorfor each missing role. - resolve_model: Test all branches for both
cursorandclaude_codeplatforms, with and without explicit model overrides. - default_config: Verify the factory produces a valid
AutodevConfigthat passesrequire_all_roles(). - load_config error paths: File not found, read error (mocked), malformed JSON, schema violation, missing roles.
- save_config: Verify file is written, parent dirs created, JSON is valid and parseable.
- expand_paths: Verify
~is expanded inhive.path.
- Full round-trip:
default_config()->save_config()->load_config()-> assert equality. - Config modification: Load config, modify a field via
model_copy(), save, reload, verify change persists.
- Hypothesis for AgentConfig: Generate random model strings and disabled booleans. Verify round-trip serialization.
- Hypothesis for GuardrailsConfig: Generate random positive integer caps. Verify schema accepts them.
- Hypothesis for KnowledgeConfig: Generate random thresholds in [0, 1]. Verify schema accepts them.
- Valid config JSON files with all 14 roles.
- Invalid config JSON files: missing roles, extra keys, wrong types, malformed JSON.
- Platform variants:
"cursor","claude_code","auto".
- Input validation:
extra="forbid"at every level rejects unexpected fields. Pydantic enforces types strictly. - Path traversal:
hive.pathis user-supplied and expanded withexpanduser(). The knowledge system should validate the resolved path is within expected directories. - No secrets in config: API keys and credentials must not be stored in
.autodev/config.json. They should use environment variables or platform-specific credential stores.
- Load time: Pydantic v2's
model_validate_json()is implemented in Rust and parses a typical config file in < 1ms. - Memory: The full
AutodevConfigobject is < 10 KB in memory. - No hot path: Config is loaded once at startup. No performance-sensitive code paths.
The config module is part of the core autodev wheel (src/config/). No separate entry point needed.
# Initialize a project with default config
autodev init
# Run with explicit config path
autodev --config path/to/config.json runThe autodev init command calls default_config() -> save_config() to create .autodev/config.json.
The config module does not emit structured log events. Errors are raised as ConfigError and logged by the CLI layer.
The config file itself (.autodev/config.json) serves as the audit artifact. It is checked into version control along with the project.
autodev status may display:
- Config file path and schema version.
- Current platform.
- Number of enabled/disabled agents.
- Tournament phase settings.
- Active QA gates.
- Guardrail caps.
The config system makes zero LLM calls. However, the configuration it defines directly controls cost:
| Config Parameter | Cost Impact |
|---|---|
agents[role].model |
Model pricing varies 10-50x between haiku/sonnet/opus |
tournaments.plan.max_rounds (default: 15) |
Each round = N judge calls |
tournaments.plan.num_judges (default: 3) |
Multiplied by rounds |
tournaments.impl.max_rounds (default: 3) |
Implementation refinement rounds |
tournaments.auto_disable_for_models: ["opus"] |
Auto-disables tournaments for expensive models |
guardrails.max_tool_calls_per_task (default: 60) |
Hard cap on tool calls per task |
guardrails.cost_budget_usd_per_plan |
Per-plan USD budget (reserved) |
qa_retry_limit (default: 3) |
QA failures trigger retries, each retry = more LLM calls |
- Environment variable overrides: Support
AUTODEV_GUARDRAILS__MAX_TOOL_CALLS=100style overrides (three-layer precedence: env var -> config file -> code defaults). - Schema migration: When
schema_versionadvances to"2.0.0", implement a migration function that upgrades"1.0.0"configs automatically. - Partial config: Allow config files to specify only overrides, merging with
default_config()for unspecified fields. - Config validation CLI command:
autodev config validateto check a config file without running the pipeline. - Per-task config overrides: Allow individual tasks to override guardrails or model selection.
- Dynamic model resolution: Integrate with platform APIs to detect available models at runtime rather than relying on static defaults.
- Should environment variable overrides use nested double-underscore notation (
AUTODEV_GUARDRAILS__MAX_TOOL_CALLS) or flat names? - Should
schema_versionbe enforced as a migration check or just a compatibility marker? - Should
HiveConfig.pathsupport environment variable expansion (e.g.,$XDG_DATA_HOME) in addition to~? - Should config support YAML as an alternative format for human-friendliness?
- Should
REQUIRED_AGENT_ROLESbe derived dynamically from the agent spec registry rather than hardcoded?
- ADR-009: Pydantic v2 strict validation (mandates
extra="forbid"on all boundary models)
src/config/schema.py-- AutodevConfig and all nested model definitionssrc/config/loader.py-- load_config, save_config, expand_pathssrc/config/defaults.py-- default_config, resolve_modelsrc/errors.py-- ConfigError definitionprm.md-- PRM (Process Reward Model) strategy + LLM trajectory classifier designplateau_detection_design.md-- Plateau detector design (rules + regression strategies)- Pydantic v2 documentation
- Python typing.Literal
| Date | Author | Changes |
|---|---|---|
| 2026-04-17 | Mohamed Ameen | Initial draft |
| 2026-05-09 | Mohamed Ameen | Refresh for v0.21.1: documented BranchConfig, runaway/plateau/winner-stability detectors, voting strategies (borda/veto), specialist judge_roles + weights, complex-plan judge escalation, multi-branch (num_branches/branches), QA additions (secretscan_baseline_enabled, per-extension thresholds, mutation_test_enabled/threshold), per-agent effort, new top-level models (PRMConfig, PlateauDetectorConfig, TaskOverridesConfig, DecayCurveConfig), v0.21.0 worktree-pool / cross-phase / speculative toggles. Regenerated example config JSON via current schema. Linked new prm.md and plateau_detection_design.md. |