ADR-117: Sensor Crate Extraction and Feature Flags¶
Context¶
attend (ADR-113) ships with four built-in sensors: context, git, peers, and processes. These are compiled directly into the attend binary as modules under src/sensors/. The ScriptSensor runner (shipped in PR #4) provides an extensibility path for shell-script sensors, but built-in sensors have no separation from the orchestrator.
This creates several problems:
- No isolation — sensor code shares the same module tree as the tick loop, state management, and CLI. Changes to one sensor can break another through shared internal interfaces.
- No selective compilation — a user who only wants git and context awareness still compiles peer discovery and process scanning. On constrained systems or custom builds, this matters.
- No clear trait contract — the
Sensortrait lives insensors/mod.rsalongside theSensorSlotruntime scaffolding. The boundary between "what a sensor must implement" and "how the orchestrator runs it" is blurred. - Future daemon model — if attend becomes long-running (paralleling the direction noted for ways-cli), hot-reloading sensors or dynamically enabling them requires a cleaner separation than in-binary modules.
Meanwhile, the workspace already demonstrates the crate extraction pattern: agent-fmt was extracted from ways-cli (PR #3) and is consumed by both tools. The same pattern applies here.
Decision¶
Extract each built-in sensor into its own workspace crate. Introduce a sensor-trait crate that defines the Sensor trait, Focus struct, and supporting types. Wire sensors into attend via Cargo feature flags.
Workspace Structure¶
tools/
agent-fmt/ # shared terminal formatting
sensor-trait/ # Sensor trait, Focus, SensorSlot
sensor-git/ # GitSensor
sensor-context/ # ContextSensor
sensor-peers/ # PeerSensor
sensor-processes/ # ProcessSensor
attend/ # orchestrator — depends on sensor crates via features
ways-cli/ # ways CLI
Trait Crate¶
sensor-trait defines the contract between attend and any sensor:
pub trait Sensor: Send {
fn name(&self) -> &str;
fn poll(&mut self, focus: &Focus) -> Vec<(f64, String)>;
fn emission_threshold(&self) -> f64;
fn base_interval(&self) -> Duration;
fn min_interval(&self) -> Duration;
fn export_state(&self) -> Vec<(String, String)> { Vec::new() }
fn import_state(&mut self, _state: &[(String, String)]) {}
}
The Send bound prepares for the daemon model where sensors may run on separate threads. Focus and SensorSlot also move to this crate since they define how the orchestrator interacts with sensors.
Feature Flags¶
attend's Cargo.toml:
[features]
default = ["sensor-git", "sensor-context", "sensor-peers", "sensor-processes"]
sensor-git = ["dep:sensor-git"]
sensor-context = ["dep:sensor-context"]
sensor-peers = ["dep:sensor-peers"]
sensor-processes = ["dep:sensor-processes"]
The orchestrator uses #[cfg(feature = "sensor-git")] guards around sensor registration. A minimal build with --no-default-features compiles only the orchestrator + script sensor runner.
Two Sensor Paths¶
This ADR formalizes the two-path model that emerged from PR #4:
| Path | Implementation | Performance | Extensibility | Compilation |
|---|---|---|---|---|
| Crate sensors | Rust, compiled in | Native | Requires recompile | Feature-flagged |
| Script sensors | Shell, external | Process fork per poll | No recompile | Config-driven |
Both paths are controlled by the same declarative config (ADR-115). A crate sensor and a script sensor with the same name: the crate sensor wins (compiled-in takes precedence). This allows a script sensor to prototype behavior that later graduates to a crate sensor.
Config as Control Plane¶
The attend.yaml config (ADR-115) controls all sensors uniformly:
sensors:
git:
interval: 30
threshold: 2.0
-processes: # disable a built-in sensor
+disk-pressure: # add a script sensor
script: scripts/check-disk.sh
interval: 120
Disabling a crate sensor via config (-processes) means it's never instantiated at runtime even though the code is compiled in. Disabling via feature flag (--no-default-features) means the code isn't compiled at all. Config is the user-facing control plane; features are the build-time control plane.
Consequences¶
Positive¶
- Clear contracts —
sensor-traitis the documented interface. Any crate implementingSensorcan be wired into attend. - Selective builds — custom attend binaries for constrained environments.
- Isolation — sensor bugs don't leak across module boundaries. Each sensor has its own dependency tree.
- Testability — sensors can be unit-tested in isolation against a mock
Focus. - Graduation path — script sensor → crate sensor is a defined workflow: prototype in shell, promote to Rust when performance matters.
- Daemon-ready —
Sendbound and crate isolation prepare for concurrent sensor polling.
Negative¶
- More crates — workspace goes from 3 to 7 members. Cargo handles this well but it's more manifests to maintain.
- Cross-crate changes — modifying the
Sensortrait requires updating all sensor crates. Mitigated by keeping the trait stable and small. - Initial migration effort — moving existing sensor code is mechanical but touches every sensor file.
Neutral¶
- No behavioral change — the default feature set compiles all sensors, reproducing current behavior exactly.
- ScriptSensor stays in attend — it's part of the orchestrator (runs any script), not a specific sensor implementation.
- Config format unchanged — ADR-115's config works identically before and after extraction.
Implementation Plan¶
- Create
sensor-traitcrate withSensor,Focus,SensorSlot,AdaptiveInterval,DeltaAccumulator - Create
sensor-gitcrate, movesensors/git.rscontent, depend onsensor-trait - Create
sensor-contextcrate, movesensors/context.rs - Create
sensor-peerscrate, movesensors/peer.rs(largest, has signal reading) - Create
sensor-processescrate, movesensors/process.rs - Update attend
Cargo.tomlwith feature flags,#[cfg]guards in sensor registration - Update
sensors/mod.rsto re-export from crates (compatibility shim, removable later) - Verify
make lint,make attend-rebuild, all existing behavior preserved - Test minimal build:
cargo build -p attend --no-default-features - Update CI workflow to test both default and minimal feature sets
Addendum, 2026-10-01: no minimal-feature build¶
Appended after acceptance; nothing above is changed. #701 (ADR-505) folded sensor-git, sensor-context and sensor-disclosure into attend as modules, which are always compiled, and removed their features. A --no-default-features build of attend did not compile before that change either, because the keepwarm command imports sensor_keepwarm unconditionally, and neither CI nor the Makefile builds one. The minimal build this record describes no longer exists. Config remains the control plane for turning a sensor off.