Skip to main content

agent_profile/adapter/
aider.rs

1// Copyright (c) 2026 Costas Kirgoussios
2//
3// This software is licensed under the PolyForm Noncommercial License 1.0.0.
4// You may obtain a copy of the License at:
5// https://polyformproject.org/licenses/noncommercial/1.0.0/
6
7//! Aider: `--config <profile file>` (SP2 design §5.3).
8
9use std::path::PathBuf;
10
11use super::{
12    Adapter, AdapterEvidence, AdapterMetadata, Capability, CapabilityClaim, CapabilityState,
13    ConflictOption, Mechanism, PathKind, PlanContext, PlannedLaunch, SupportLevel, profile_dir,
14};
15use crate::config::AppRoot;
16use crate::error::Result;
17use crate::name::ProfileName;
18
19const FLAG: &str = "--config";
20const FILE_NAME: &str = ".aider.conf.yml";
21
22/// Every spelling of the option Aider launches with; `--config` first, so it is the one reported.
23const CONFIG_OPTION: ConflictOption =
24    ConflictOption { long: &[FLAG, "--confi", "--conf", "--con"], short: Some('c') };
25
26/// An empty YAML mapping: Aider rejects an empty or comment-only file, and `{}` sets no option (design D5).
27pub const INITIAL_CONFIG: &[u8] = b"{}\n";
28
29/// Shown on every launch.
30pub const LAYERING_NOTE: &str =
31    "--config is layered over .aider.conf.yml in the working directory, git root and home";
32
33static METADATA: AdapterMetadata = AdapterMetadata {
34    id: "aider",
35    executable: "aider",
36    mechanism: Mechanism::FlagFile(CONFIG_OPTION),
37    support: SupportLevel::Proven,
38    evidence: AdapterEvidence {
39        mechanism_id: "aider-config-file-v1",
40        verified_at: "2026-09-15",
41        upstream_version: "0.86.2",
42        source_url: "measured",
43        notes: "measured in a sandbox: --config layers over default .aider.conf.yml files; -c, --con, --conf and \
44                --confi select the config file; a missing, empty or comment-only file exits 2; {} is accepted",
45    },
46    capabilities: &[
47        CapabilityClaim {
48            capability: Capability::ConfigIsolation,
49            state: CapabilityState::NotGuaranteed,
50            basis: "measured: the user-level ~/.aider.conf.yml, repository and cwd config files, .env \
51                    files and AIDER_* variables still apply",
52        },
53        CapabilityClaim {
54            capability: Capability::CredentialIsolation,
55            state: CapabilityState::NotSupported,
56            basis: "measured: API keys come from the environment, .env files and config files",
57        },
58        CapabilityClaim {
59            capability: Capability::StateIsolation,
60            state: CapabilityState::NotSupported,
61            basis: "measured: history files are written in the working directory",
62        },
63    ],
64    env: &[],
65    // Empty on purpose. `--config` is the mechanism's own option, and `check_conflicts` scans that
66    // alongside this list, so naming it here again refused nothing extra — it MASKED the chain: with the
67    // duplicate present, deleting `.chain(metadata.mechanism.conflict_option())` left the whole suite
68    // green. Measured both ways. `conflicts` is for options an adapter refuses BESIDES its mechanism's.
69    conflicts: &[],
70    credential_env: &[],
71};
72
73/// The Aider adapter.
74#[derive(Debug, Clone, Copy)]
75pub struct Aider;
76
77impl Adapter for Aider {
78    fn metadata(&self) -> &'static AdapterMetadata {
79        &METADATA
80    }
81
82    fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
83        let dir = profile_dir(root, profile, METADATA.id);
84        let file = dir.join(FILE_NAME);
85        vec![(dir, PathKind::Dir), (file, PathKind::File { contents: INITIAL_CONFIG })]
86    }
87
88    fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
89        let mut planned = METADATA.mechanism.plan(self, ctx)?;
90        planned.notes.push(LAYERING_NOTE.to_owned());
91        Ok(planned)
92    }
93}