Skip to content

ADR-115: Declarative Configuration with Project-Scope Overlay

Context

attend (ADR-113) introduced a two-layer configuration pattern during implementation: a user-scope config at ~/.config/attend/config.yaml and a project-scope overlay at {project}/.claude/attend.yaml. The project overlay uses +/- syntax to add or remove sensors without rewriting the full config. This pattern proved clean enough to propose as the standard for the agent-ways workspace.

ways currently has no central configuration file. Its "config" is distributed across individual way files (frontmatter declares thresholds, vocabulary, triggers) and environment variables. This works for way authoring but leaves system-level tuning scattered: disclosure gate parameters live in the Rust source, embedding engine paths are hardcoded or env-var-driven, and there's no project-scope override for global behavior.

This ADR proposes adopting attend's config pattern for the workspace — a shared convention that both ways and attend (and future sibling tools) follow.

Decision

The pattern

Each tool in the agent-ways workspace may declare a config file at two scopes:

~/.config/{tool}/config.yaml          # user scope — always loaded
{project}/.claude/{tool}.yaml         # project scope — layered on top

User scope provides defaults. Project scope overrides or extends them. Tools load user scope first, then apply project scope on top. Missing files at either scope are a no-op (compiled defaults apply).

Config format

YAML subset — flat keys, nested sections, lists. No full YAML parser required; the minimal subset that covers key-value pairs and two-level nesting is sufficient. This keeps tools zero-dependency (no serde, no yaml crate).

Project-scope overlay syntax

For collection-type configs (sensors in attend, potentially way groups in ways), the project overlay uses +/- to modify the set:

# project/.claude/attend.yaml
sensors:
  +disk-pressure:              # add a project-local sensor
    script: .claude/sensors/check-disk.sh
    interval: 120
  -processes:                  # disable a user-scope sensor

The + prefix adds an entry that doesn't exist in user scope. The - prefix disables an entry from user scope. Unprefixed entries override properties of existing entries.

Trust model

Same as ways scoping:

  • User scope (~/.config/) is trusted — the user installed it.
  • Project scope ({project}/.claude/) has the same trust level as project-scope ways. Scripts declared in project config are code that runs on poll — same scrutiny as project-scope way macros.

CLI convention

Each tool provides a config subcommand:

{tool} config init     # write default config to user scope
{tool} config show     # display effective config (both layers merged)
{tool} config path     # show user and project config file paths

What this means for ways

ways could adopt this pattern for:

  • Engine configuration: embedding model path, corpus path, fallback behavior, forced engine selection — currently hardcoded or env-var-driven
  • Disclosure gate tuning: re-disclosure intervals, token-gated thresholds — currently compiled defaults in Rust
  • Per-project way groups: enable/disable way categories per project without removing files
  • Scoring overrides: per-project BM25/embedding threshold adjustments

Example:

# ~/.config/ways/config.yaml
engine:
  model: minilm-l6-v2.gguf
  fallback: bm25
  forced: auto

disclosure:
  redisclose_default: 10
  token_gate: 0.3

# project/.claude/ways.yaml
scoring:
  +softwaredev/code/security:
    threshold: 1.5              # lower threshold for security-sensitive project
  -ea/comms:                    # disable comms way in this project

XDG compliance

Config files follow XDG conventions: - Config: $XDG_CONFIG_HOME/{tool}/ (default ~/.config/{tool}/) - State: $XDG_CACHE_HOME/{tool}/ (default ~/.cache/{tool}/)

This is consistent with the existing XDG separation documented in project memory.

Consequences

Positive

  • Tuning without recompiling. Governor params, sensor intervals, scoring thresholds — all externalized. Edit a file, restart (or self-reload in attend's case).
  • Project-scope customization. A security-focused project can lower security way thresholds. A hardware project can add system sensors. A documentation project can disable code-focused ways.
  • Shared convention. All workspace tools follow the same pattern. Users learn it once.
  • Progressive adoption. Tools can adopt the pattern incrementally. Attend shipped it first; ways can adopt it when ready without changing attend's implementation.
  • Zero dependency. The minimal YAML parser handles the config subset without pulling in serde or yaml crates.

Negative

  • Two files to manage. Users must understand the layering. Mitigation: config show displays the effective merged result; config path shows where to look.
  • Minimal parser limitations. The YAML subset doesn't handle anchors, multi-line values, or complex nesting. Mitigation: the config format is intentionally simple; complex configuration belongs in way files or sensor scripts, not in config.yaml.

Neutral

  • attend already implements this. This ADR documents the pattern for workspace-wide adoption. No changes to attend's existing implementation.
  • ways adoption is deferred. This ADR defines the target; a follow-up implements it in ways when the need arises.

References

  • attend config implementation: tools/attend/src/config.rs
  • attend ADR: ADR-113 — config section documents attend's implementation
  • XDG separation: project memory xdg-separation.md

Note, 2026-10-02: the +name: and -name: grammar is retired

Appended after acceptance; nothing above is changed. attend's settings moved onto the typed registry (ADR-503, #698), and the hand-written parser this record describes is gone, so the "zero dependency" and "minimal parser" consequences above no longer hold: the files are YAML, read and checked through attend-config's schema.

The overlay's +name: (add a sensor) and -name: (switch one off) prefixes are retired with no compatibility reader (ADR-506). A sensor of your own is any name with a script, and name: {enabled: false} switches a sensor off in a project. The layering itself (user file, then the project's .claude/attend.yaml, over the defaults) is unchanged.

An old prefixed entry is a finding that closes its file's sensors: section: every built-in sensor and every sensor that file names reads off, and nothing else in the section is read, until the name is edited by hand. ways settings fix refuses to repair a name. This was the coordinator's decision in the review of PR #733: a -name: entry may be an off-switch the schema cannot read, so it fails closed, following the "whole file fails closed" rule in the ADR-503 addendum. The release notes in docs/migration-1.0.md say how to move each form by hand.

Note, 2026-10-03: where the release notes went

Appended; nothing above is changed. docs/migration-1.0.md was deleted, and the attend part of its release notes was never in the copy kept at the ways-v1.8.3 tag. attend's configuration page now says how each retired form reads and how to rewrite it.