Skip to content

80. config.yaml vs. agent env vars: where a config option belongs

Date: 2026-07-31

Amends: ADR 0049

Status

Accepted

Context

fullsend-ai/agents#567 added TRIAGE_AUTO_CODE as a harness env.runner default in harness/triage.yaml. A reviewer flagged that fullsend-ai/fullsend#1754, the issue that requested the knob, asked for it to "live in the per-repo/ per-org config surface" — i.e. .fullsend/config.yaml — not a harness default (see the review discussion). config.yaml already carries fields like create_issues.allow_targets that are unrelated to any single agent, per the governance and agent infrastructure problem docs. ADR 0049 defines how agent config env vars are named ({AGENT}_{SETTING_NAME}) and requires separate per-agent vars when the same concept is independently tunable per agent (e.g. CODE_MAX_FILE_SIZE vs REVIEW_MAX_FILE_SIZE), but it draws no line against config.yaml — nothing in it says when a knob should live there instead of as an env var, so there was no rule to check the PR against.

Decision

A config option belongs in exactly one of the two surfaces, based on whether its behavior is meaningful to more than one agent. This narrows ADR 0049's per-agent-vars rule (a setting that applies to multiple agents gets separate vars per agent) to the case where each agent needs its own independently tunable value; a single value meant to apply the same way across every agent is a different case, and belongs in config.yaml instead:

  • Pipeline/dispatch policy — governs whether or how agents run, or applies the same way across every agent, rather than tuning one agent's own inference-time logic: it is a config.yaml field. It gets a plain name with no {AGENT}_ prefix, and it is not also settable via environment variable — config.yaml (internal/config accessors) is the single source of truth. roles, kill_switch, and create_issues.allow_targets are existing examples.
  • Single-agent behavior tuning — adjusts how one specific agent does its own job: it is an {AGENT}_-prefixed env var per ADR 0049, delivered via that agent's env.runner/env.sandbox. The prefix matters even though the var lives in one agent's harness: .env files can be sourced together and runner_env/env.sandbox can share a host environment, so the agent name scopes the var and prevents collisions in those shared contexts (ADR 0049, Consequences). It is not also settable as a config.yaml field — overriding it per repo or org means overriding the harness (e.g. via base: composition, per ADR 0045), not adding a parallel field to config.yaml.

Override convention: env.runner/env.sandbox values are agent defaults. A per-repo or per-org override edits the harness (base: composition, per ADR 0045), not the CI workflow env: block. ADR 0081 reserves that block for infrastructure plumbing (credentials, project IDs, regions), not agent behavior knobs — overriding ADR 0049's "CI workflow injection" delivery mechanism for behavior knobs specifically; see that ADR for the full rule and its exceptions. This also means behavior defaults in env.runner/env.sandbox must be literals (e.g. TRIAGE_AUTO_CODE: "on"), not shell-style passthrough expressions (e.g. ${TRIAGE_AUTO_CODE:-on}) — env.runner/env.sandbox support ${VAR} host-variable expansion (see ADR 0055, § Runner behavior), not shell default-value syntax, so passthrough syntax is rejected at harness load: env validation treats TRIAGE_AUTO_CODE:-on as a host variable name and fails with host variable … is not set; even absent validation, os.Expand would resolve the whole reference to an empty string, not the intended default.

A knob only moves from one surface to the other by a deliberate migration, not by adding a second way to set the same value. Applying this rule to TRIAGE_AUTO_CODE is a boundary case: it decides whether the code agent runs next, which sounds like dispatch policy, but that decision is made inside triage's own post-script, as part of triage's inference-time behavior — not by a shared dispatch/CLI layer gating multiple agents uniformly. That keeps it single-agent behavior tuning today, so it correctly belongs in harness/triage.yaml env.runner, not config.yaml. If the check is ever lifted out of triage's post-script into a shared dispatch layer, it becomes pipeline/dispatch policy and should move to config.yaml as a deliberate migration, not before. fullsend-ai/fullsend#1754's "per-repo/per-org config surface" request is satisfied by documenting the existing harness override path (base: composition or an org/repo harness copy), not by adding a config.yaml field — the gap the reviewer found is a documentation gap, not a placement gap.

Consequences

  • Resolves the fullsend-ai/agents#567 ambiguity: TRIAGE_AUTO_CODE stays an env var; docs/agents/<agent>.md's Variables table must state how to override it (which harness layer to edit), matching the guidance ADR 0049 already expects.
  • config.yaml cannot accumulate {agent}_foo-style fields — any such field is a signal the knob was misplaced.
  • An env var cannot quietly gain a config.yaml mirror with its own precedence rules; there is one settable location per config option.
  • A knob whose scope grows from one agent to several requires a new decision (an ADR update or explicit review), not a silent field addition to config.yaml.
Content