-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbacklog.json
More file actions
374 lines (373 loc) · 120 KB
/
backlog.json
File metadata and controls
374 lines (373 loc) · 120 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
{
"labels": [
{"name": "type: feature", "color": "C2E0C6", "description": "New functionality"},
{"name": "type: bug", "color": "D73A4A", "description": "Something isn't working"},
{"name": "type: refactor", "color": "F9D0C4", "description": "Code cleanup, no feature changes"},
{"name": "type: spike", "color": "D4C5F9", "description": "Time-boxed exploratory research requiring an ADR deliverable"},
{"name": "type: infrastructure", "color": "0E8A16", "description": "Makefiles, CI/CD, Docker, K8s"},
{"name": "type: documentation", "color": "0075CA", "description": "Documentation improvements"},
{"name": "type: safety", "color": "B60205", "description": "Safety-critical constraints and circuit breakers"},
{"name": "type: epic", "color": "7E57C2", "description": "Tracking issue for an Epic — groups related sub-issues"},
{"name": "component: common", "color": "0052CC", "description": "platform/orpheus-common"},
{"name": "component: agent", "color": "5319E7", "description": "Any agent in agents/"},
{"name": "component: ui-dash", "color": "006B75", "description": "React UI or FastAPI backend"},
{"name": "component: infra", "color": "0E8A16", "description": "Build system, CI/CD, deployment tooling"},
{"name": "component: devops", "color": "1D76DB", "description": "Developer tooling and workflow automation"},
{"name": "domain: active-inference", "color": "B60205", "description": "Cognitive state space and VFE minimization"},
{"name": "domain: mcp", "color": "1D76DB", "description": "Model Context Protocol integration"},
{"name": "domain: hardware", "color": "E99695", "description": "Jetson, GPIO, ALSA, or physical sensors"},
{"name": "domain: mlops", "color": "FBCA04", "description": "Model evaluation and data pipelines"},
{"name": "domain: observability", "color": "FEF2C0", "description": "Telemetry, tracing, logging, and monitoring"},
{"name": "domain: event-bus", "color": "D4C5F9", "description": "MQTT, Redis Streams, message transport"},
{"name": "domain: privacy", "color": "B60205", "description": "Data privacy and ethical constraints"},
{"name": "domain: config", "color": "C5DEF5", "description": "Configuration management and versioning"},
{"name": "C4: System Context", "color": "000000", "description": "Ecosystem interactions (Wildlife, Weather, Humans)"},
{"name": "C4: Container", "color": "1D76DB", "description": "Deployable units (Agents, Broker, DB, React UI)"},
{"name": "C4: Component", "color": "5319E7", "description": "Internal logical boundaries (Classifiers, Managers)"},
{"name": "good first issue", "color": "7057FF", "description": "Good for newcomers"},
{"name": "help wanted", "color": "008672", "description": "Extra attention is needed"}
],
"milestones": [
{
"title": "Epic 1: The Cognitive Holarchy",
"description": "Expand the semantic state space, entity taxonomy, and cognitive reasoning capabilities of the agent network."
},
{
"title": "Epic 2: Universal Actuation (MCP)",
"description": "Standardize how the system safely touches the physical world using the Model Context Protocol."
},
{
"title": "Epic 3: Interspecies Interfaces",
"description": "Build human interaction surfaces — voice control, mobile apps, privacy filtering, and field displays."
},
{
"title": "Epic 4: Event Bus Evolution",
"description": "Abstract the message transport layer and migrate from ephemeral MQTT to durable, replayable streams."
},
{
"title": "Epic 5: Observability & Telemetry",
"description": "Integrate environmental data and migrate system observability to OpenTelemetry."
},
{
"title": "Epic 6: Infrastructure & Extensibility",
"description": "Improve the build system, shell testing, and package management for a seamless contributor experience."
},
{
"title": "Epic 7: Containerization & Simulation",
"description": "Build a complete Docker-based simulation environment so contributors can develop without hardware."
},
{
"title": "Epic 8: Configuration Management",
"description": "Evolve configuration from flat YAML to a versioned, database-backed system with environment overrides."
},
{
"title": "Epic 9: Edge Hardware Realities",
"description": "Survive the thermodynamics and physical constraints of a sealed enclosure in a Michigan wetland."
}
],
"issues": [
{
"title": "Epic 1: The Cognitive Holarchy",
"is_epic": true,
"body": "## Epic: The Cognitive Holarchy\n\nExpand the semantic state space, entity taxonomy, and cognitive reasoning capabilities of the agent network.\n\n### Goal\n\nTransform Orpheus from a single-species detector into a multi-entity cognitive system with temporal memory, self-awareness (corollary discharge), and behavioral validation.\n\n### Success Criteria\n\n- EntityEvent taxonomy supports Animals, Humans, and Plants with extensible subtypes.\n- Agents can query historical likelihoods for predictive behavior.\n- The system distinguishes self-generated audio from genuine wildlife detections.\n- Full BDD test coverage validates the cognitive loop end-to-end.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "domain: active-inference", "C4: Component"],
"milestone": "Epic 1: The Cognitive Holarchy"
},
{
"title": "Epic 2: Universal Actuation (MCP)",
"is_epic": true,
"body": "## Epic: Universal Actuation (MCP)\n\nStandardize how the system safely touches the physical world using the Model Context Protocol.\n\n### Goal\n\nAbstract all hardware actuation behind MCP servers with mandatory safety circuit breakers, enabling higher-level agents to command physical devices without knowing implementation details.\n\n### Success Criteria\n\n- Audio playback exposed as an MCP tool with full input validation.\n- Hardware circuit breakers enforce rate limits that cannot be overridden by application logic.\n- Ambient-adaptive volume calibration runs autonomously.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "domain: mcp", "domain: hardware", "C4: Container"],
"milestone": "Epic 2: Universal Actuation (MCP)"
},
{
"title": "Epic 3: Interspecies Interfaces",
"is_epic": true,
"body": "## Epic: Interspecies Interfaces\n\nBuild human interaction surfaces — voice control, mobile apps, privacy filtering, and field displays.\n\n### Goal\n\nMake the station interactive and accessible to humans while respecting privacy constraints. The system should be approachable in the field, alerting remotely, and safe from accidental data capture.\n\n### Success Criteria\n\n- On-device wake-word triggers a voice interaction loop.\n- Push notifications reach mobile devices for notable events.\n- Human speech is detected and redacted from stored audio.\n- A weatherproof field display shows real-time status.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "C4: System Context"],
"milestone": "Epic 3: Interspecies Interfaces"
},
{
"title": "Epic 4: Event Bus Evolution",
"is_epic": true,
"body": "## Epic: Event Bus Evolution\n\nAbstract the message transport layer and migrate from ephemeral MQTT to durable, replayable streams.\n\n### Goal\n\nDecouple agents from paho-mqtt, provide a pluggable EventBus interface, and add durable stream support for consumer groups, replay, and historical analysis.\n\n### Success Criteria\n\n- No agent imports paho-mqtt directly — all use the EventBus ABC.\n- A durable stream backend passes integration tests for persistence and consumer groups.\n- Historical events can be replayed at configurable speeds for offline analysis.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "domain: event-bus", "C4: Component"],
"milestone": "Epic 4: Event Bus Evolution"
},
{
"title": "Epic 5: Observability & Telemetry",
"is_epic": true,
"body": "## Epic: Observability & Telemetry\n\nIntegrate environmental data and migrate system observability to OpenTelemetry.\n\n### Goal\n\nCorrelate wildlife activity with weather data, and separate observability traffic (traces, metrics) from domain events (detections, actuations) using OpenTelemetry.\n\n### Success Criteria\n\n- Weather data from the Ecowitt station is ingested and available for correlation queries.\n- OTel SDK is integrated into orpheus-common with distributed tracing across agents.\n- MQTT bus carries only domain events after migration.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "domain: observability", "C4: Component"],
"milestone": "Epic 5: Observability & Telemetry"
},
{
"title": "Epic 6: Infrastructure & Extensibility",
"is_epic": true,
"body": "## Epic: Infrastructure & Extensibility\n\nImprove the build system, shell testing, and package management for a seamless contributor experience.\n\n### Goal\n\nConsolidate build infrastructure into a DRY Makefile architecture, add shell script testing, and evaluate modern Python package management.\n\n### Success Criteria\n\n- A single `Makefile.include` eliminates cross-component duplication.\n- Critical shell scripts have BATS test coverage.\n- Package management recommendation is documented in an ADR.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "component: infra", "C4: Container"],
"milestone": "Epic 6: Infrastructure & Extensibility"
},
{
"title": "Epic 7: Containerization & Simulation",
"is_epic": true,
"body": "## Epic: Containerization & Simulation\n\nBuild a complete Docker-based simulation environment so contributors can develop without hardware.\n\n### Goal\n\nEnable a `git clone` → `make dev-up` → working system experience in under 5 minutes, and auto-generate deployment manifests from the single source of truth (`orpheus.yaml`).\n\n### Success Criteria\n\n- Docker Compose dev environment runs the full stack with mock agents.\n- Deployment manifests (Docker Compose + systemd) are auto-generated from orpheus.yaml.\n- Multi-arch images build for amd64 and arm64.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "component: devops", "C4: Container"],
"milestone": "Epic 7: Containerization & Simulation"
},
{
"title": "Epic 8: Configuration Management",
"is_epic": true,
"body": "## Epic: Configuration Management\n\nEvolve configuration from flat YAML to a versioned, database-backed system with environment overrides.\n\n### Goal\n\nImplement a layered configuration system (YAML → DB → env vars) with full version history, hot-reload via MQTT, and backward compatibility with existing deployments.\n\n### Success Criteria\n\n- `ConfigStore` implements layered lookup with no breaking changes to `OrpheusConfig.get_instance()`.\n- Every configuration change is versioned in SQLite.\n- Agents hot-reload on MQTT config-change notifications.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "domain: config", "C4: Component"],
"milestone": "Epic 8: Configuration Management"
},
{
"title": "Epic 9: Edge Hardware Realities",
"is_epic": true,
"body": "## Epic: Edge Hardware Realities\n\nSurvive the thermodynamics and physical constraints of a sealed enclosure in a Michigan wetland.\n\n### Goal\n\nBuild autonomous thermal management and model regression testing so the system stays healthy in extreme conditions and model updates never silently degrade accuracy.\n\n### Success Criteria\n\n- Thermal manager autonomously sheds load at configurable temperature thresholds.\n- Golden Dataset CI pipeline blocks merges that regress species detection accuracy.\n- System recovers gracefully from thermal shedding events.\n\n### Sub-Issues\n\n_Populated automatically by sync script._",
"labels": ["type: epic", "domain: hardware", "C4: Container"],
"milestone": "Epic 9: Edge Hardware Realities"
},
{
"title": "[ARCH] Generalize the EntityEvent State Space Taxonomy",
"body": "### Context\n\nThe current scope of the Event Correlator is limited to Bird/Crow. Orpheus needs a rich, semantic taxonomy that scales to `Animals → (Birds, Critters)`, `Humans → (Known 2FA, Unknown)`, and `Plants`. Every entity type gets first-class representation in the `EntityEvent` models in `orpheus-common`.\n\nThis is the foundational issue for Epic 1 — most other cognitive holarchy work depends on this taxonomy being in place.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `EntityType` enum or registry defining the taxonomy tree: `Animal.Bird`, `Animal.Critter`, `Human.Known`, `Human.Unknown`, `Plant`.\n- `EntityEvent` Pydantic model with a `entity_type: EntityType` field replacing the current flat string.\n- Serialization contract: JSON output must be backward-compatible with existing MQTT consumers expecting `orpheus/entities/animal`.\n\n**Implementation (Internal Logic):**\n- Taxonomy tree loaded from a data file (`entity_taxonomy.yaml`) — extensible without code changes.\n- Migration utility that rewrites legacy SQLite detection rows to the new taxonomy format.\n- Topic routing logic in `orpheus-common` that maps `EntityType` → MQTT topic path.\n\n### Architectural Constraints\n\n- Must not break existing downstream consumers of the `orpheus/entities/animal` MQTT topic.\n- Must follow ADR 0006 for event hierarchy conventions.\n- Must be Python 3.9 compatible (no `match` statements, no `X | None` syntax).\n- The taxonomy tree should be extensible without code changes (data-driven where possible).\n\n### Acceptance Criteria\n\n```gherkin\nFeature: EntityEvent State Space Taxonomy\n\n Scenario: Classify a detection with the new taxonomy\n Given the taxonomy file defines \"Animal.Bird.Crow\" as a valid entity type\n When a BirdNET detection event is created for species \"Corvus brachyrhynchos\"\n Then the EntityEvent has entity_type \"Animal.Bird.Crow\"\n And the MQTT topic is \"orpheus/entities/animal/bird/crow\"\n\n Scenario: Backward compatibility with legacy consumers\n Given a legacy consumer subscribes to \"orpheus/entities/animal\"\n When a new-format EntityEvent is published for entity_type \"Animal.Bird.Hawk\"\n Then the legacy consumer receives the event on \"orpheus/entities/animal\"\n\n Scenario: Extend taxonomy without code changes\n Given the taxonomy file is updated to add \"Animal.Amphibian.Frog\"\n When the system reloads its taxonomy\n Then \"Animal.Amphibian.Frog\" is a valid entity_type\n And no Python code changes were required\n```\n\n### Definition of Done\n\n- [ ] Update `EntityEvent` Pydantic models in `orpheus-common/src/orpheus_common/events.py`.\n- [ ] Taxonomy tree loaded from `entity_taxonomy.yaml` data file.\n- [ ] Provide migration script for legacy SQLite detections.\n- [ ] 100% test coverage for the new taxonomy serialization.\n- [ ] Update ADR documenting the taxonomy design decision.",
"labels": ["type: feature", "component: common", "domain: active-inference", "C4: Component", "help wanted"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy"
},
{
"title": "[CORE] State Space Likelihood & Latent Memory",
"body": "### Context\n\nAgents currently react to events in real-time with no historical awareness. We need agents to query the state space for likelihoods: \"What's the probability of a coyote visit at 2am in March?\" Implement latent memory states so the system *remembers* temporal and spatial patterns rather than only responding to the present moment.\n\nThis enables predictive behavior — the system can pre-position attention (e.g., activate video at dusk when coyotes are historically likely).\n\n### Dependencies\n\n**Requires:** `[ARCH] Generalize the EntityEvent State Space Taxonomy`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `StateSpaceMemory` class with methods: `record_event(event: EntityEvent)`, `query_likelihood(entity_type: EntityType, time_window: TimeWindow) -> float`, `query_pattern(entity_type: EntityType) -> TemporalPattern`.\n- `TemporalPattern` dataclass describing hourly/daily/seasonal distributions.\n- `TimeWindow` dataclass with `hour_of_day`, `day_of_week`, `month` optional filters.\n\n**Implementation (Internal Logic):**\n- SQLite-backed persistence with hourly event count buckets per entity type.\n- Likelihood calculation using kernel density estimation over historical buckets.\n- LRU cache for hot queries with TTL-based invalidation.\n\n### Architectural Constraints\n\n- Memory states must persist across agent restarts (SQLite-backed, not in-memory).\n- Likelihood queries must complete in <100ms on the Jetson Orin NX.\n- Must be Python 3.9 compatible.\n- Must not introduce new heavyweight dependencies (no pandas, no scipy).\n\n### Acceptance Criteria\n\n```gherkin\nFeature: State Space Likelihood Queries\n\n Scenario: Query temporal likelihood for a known pattern\n Given 30 days of historical data show coyote detections between 22:00-04:00\n When an agent queries the likelihood of \"Animal.Critter.Coyote\" at 02:00\n Then the returned likelihood is greater than 0.7\n\n Scenario: Query returns low likelihood outside historical pattern\n Given 30 days of historical data show coyote detections between 22:00-04:00\n When an agent queries the likelihood of \"Animal.Critter.Coyote\" at 14:00\n Then the returned likelihood is less than 0.1\n\n Scenario: Memory persists across restarts\n Given 10 events have been recorded for \"Animal.Bird.Crow\"\n When the StateSpaceMemory is destroyed and re-instantiated\n Then query_likelihood returns a non-zero value for \"Animal.Bird.Crow\"\n```\n\n### Definition of Done\n\n- [ ] Implement `StateSpaceMemory` class in `orpheus-common` with temporal likelihood queries.\n- [ ] Integrate into `orpheus-agent-event-correlator` as an optional enrichment step.\n- [ ] Unit tests with synthetic temporal data proving likelihood calculations are correct.\n- [ ] Performance benchmark showing <100ms query time on representative dataset.",
"labels": ["type: feature", "component: common", "component: agent", "domain: active-inference", "C4: Component"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy",
"depends_on": ["[ARCH] Generalize the EntityEvent State Space Taxonomy"]
},
{
"title": "[CORE] Implement Corollary Discharge (The \"Echo\" Problem)",
"body": "### Context\n\nWhen the system plays a crow call through the speaker, its own microphone hears it and triggers a detection. The Event Correlator currently has no way to distinguish self-generated audio from genuine wildlife. We need a temporal blanking window in the Correlator that subscribes to playback events and explicitly tags detections overlapping with our own audio output.\n\nThis is a classic corollary discharge problem from neuroscience — the system must predict and cancel its own sensory consequences.\n\n### Dependencies\n\n**Requires:** `[ARCH] Generalize the EntityEvent State Space Taxonomy`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `CorollaryDischargeFilter` class with methods: `register_playback(start_time: float, duration: float, source: str)`, `is_self_generated(event: EntityEvent) -> bool`.\n- `EntityEvent` extended with `is_self_generated: bool` field (default `False`).\n- Configuration schema: `corollary_discharge.buffer_seconds` in `orpheus.yaml`.\n\n**Implementation (Internal Logic):**\n- Ring buffer of recent playback windows (max 100 entries, auto-evicting after 5 minutes).\n- Overlap detection: event timestamp falls within `[playback_start, playback_start + duration + buffer]`.\n- MQTT subscription to `orpheus/actuation/audio/playback` for automatic window registration.\n\n### Architectural Constraints\n\n- **DO NOT** modify the ML inference agents (BirdNET/AVES). Filtering MUST happen in `orpheus-agent-event-correlator`.\n- Do not drop self-generated events entirely. Tag them with `is_self_generated: true` in the database for future acoustic calibration analysis.\n- Blanking window duration must be configurable via `orpheus.yaml`.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Corollary Discharge Filtering\n\n Scenario: Tag detection during own playback as self-generated\n Given the system played a crow call at 14:00:00 for 5 seconds\n And the corollary discharge buffer is 2 seconds\n When a crow detection event arrives at 14:00:03\n Then the event is tagged with is_self_generated = true\n And the event is still stored in the database\n\n Scenario: Do not tag detection outside playback window\n Given the system played a crow call at 14:00:00 for 5 seconds\n And the corollary discharge buffer is 2 seconds\n When a crow detection event arrives at 14:00:10\n Then the event is tagged with is_self_generated = false\n\n Scenario: Multiple overlapping playback windows\n Given the system played audio at 14:00:00 for 3 seconds\n And the system played audio at 14:00:02 for 3 seconds\n When a detection event arrives at 14:00:04\n Then the event is tagged with is_self_generated = true\n```\n\n### Definition of Done\n\n- [ ] Correlator subscribes to the `orpheus/actuation/audio/playback` MQTT topic.\n- [ ] Temporal overlap logic implemented with configurable window (default: playback duration + 2s buffer).\n- [ ] `pytest-asyncio` test simulating simultaneous arrival of playback and detection events.\n- [ ] Tagged events visible in the dashboard's detection history.",
"labels": ["type: feature", "component: agent", "domain: active-inference", "C4: Component"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy",
"depends_on": ["[ARCH] Generalize the EntityEvent State Space Taxonomy"]
},
{
"title": "[TEST] Full BDD End-to-End Testing Pipeline",
"body": "### Context\n\nWe have unit tests and some integration tests, but no way to validate the entire cognitive loop: audio trigger → classification → correlation → actuation. Build a Cucumber/Behave BDD suite that simulates complete detection sequences using generated audio/video signals and asserts on the system's end-to-end behavior.\n\nThis is critical for regression testing as the cognitive holarchy grows in complexity.\n\n### Dependencies\n\n**Requires:** `[CORE] Implement Corollary Discharge (The \"Echo\" Problem)`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- Behave step definitions exposing: `given_audio_signal(frequency, duration)`, `when_detection_pipeline_runs()`, `then_event_correlated(entity_type, confidence_threshold)`.\n- Feature file naming convention: `tests/bdd/features/{domain}.feature`.\n- Step definition location: `tests/bdd/steps/{domain}_steps.py`.\n\n**Implementation (Internal Logic):**\n- Synthetic signal generator using `numpy` for sine waves and white noise.\n- In-process MQTT broker (or mock) for test isolation.\n- Fixture that boots the agent pipeline in a subprocess with test configuration.\n\n### Architectural Constraints\n\n- Use `behave` (Python BDD framework) to stay in the Python ecosystem.\n- Test signals must be generated (synthetic sine waves, test images), not recorded samples, to avoid copyright and storage issues.\n- Tests must be runnable in CI without hardware (mock MQTT broker, mock ALSA).\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: End-to-End BDD Testing Pipeline\n\n Scenario: Single species detection through full pipeline\n Given a synthetic audio signal matching \"American Crow\" frequency profile\n When the detection pipeline processes the audio chunk\n Then an EntityEvent is published with entity_type \"Animal.Bird.Crow\"\n And the event is stored in the SQLite database\n\n Scenario: Corollary discharge filtering in E2E\n Given the system is playing a crow call\n And a synthetic crow detection arrives during playback\n When the correlation pipeline processes the detection\n Then the event is tagged with is_self_generated = true\n\n Scenario: Multi-sensor correlated detection\n Given a synthetic audio detection for \"Coyote\" at timestamp T\n And a synthetic video motion event at timestamp T + 1s\n When the correlation pipeline processes both events\n Then a correlated multi-sensor EntityEvent is published\n```\n\n### Definition of Done\n\n- [ ] `behave` framework integrated with feature files in `tests/bdd/`.\n- [ ] At least 3 scenarios covering: single detection, correlated multi-sensor detection, and corollary discharge filtering.\n- [ ] CI pipeline step that runs BDD tests on every PR.\n- [ ] Documentation in `docs/TESTING.md` explaining how to write new scenarios.",
"labels": ["type: feature", "type: infrastructure", "domain: active-inference", "C4: System Context", "help wanted"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy",
"depends_on": ["[CORE] Implement Corollary Discharge (The \"Echo\" Problem)"]
},
{
"title": "[SPIKE] Isolate Individual Bird Calls from Overlapped Audio",
"body": "### Context\n\nDuring a dawn chorus, BirdNET receives audio chunks with 10-15 overlapping species. It classifies the dominant species but misses the rest. Can we use blind source separation (BSS), spectral masking, or other DSP techniques to extract individual bird songs from heavily overlapped recordings?\n\nThis is a research problem — there may not be a production-ready solution. The goal is to evaluate feasibility and prototype an approach.\n\n**Time-box: 5 days**\n\n### Architectural Constraints\n\n- Any solution must run on the Jetson Orin NX (limited GPU memory, no cloud round-trips).\n- Must not replace BirdNET — this is a pre-processing or post-processing step.\n- Output must be individual audio segments that can be re-fed into the classification pipeline.\n- Must be Python 3.9 compatible.\n\n### Definition of Done\n\n- [ ] **ADR Deliverable:** `docs/adr/XXXX-bss-bird-call-separation.md` documenting the evaluation, findings, and recommendation (viable/not viable for production).\n- [ ] Literature review of BSS/spectral masking techniques for birdsong (documented in `docs/research/`).\n- [ ] Prototype script that takes a multi-species audio chunk and outputs separated tracks.\n- [ ] Quantitative evaluation: re-feed separated tracks into BirdNET and measure classification improvement.\n- [ ] Time-box respected: if no viable approach found within 5 days, document findings and close.",
"labels": ["type: spike", "component: agent", "domain: active-inference", "domain: mlops", "C4: Component"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy"
},
{
"title": "[HARDWARE] Implement MCP Server for ALSA Audio Playback",
"body": "### Context\n\nWrap the existing `orpheus-agent-audio-playback` functionality in a formal Model Context Protocol (MCP) server. This exposes `play_audio(uri, volume)` as a standardized tool that any higher-level agent (e.g., the future Director) can invoke without knowing anything about ALSA, sound cards, or audio drivers.\n\nThis is the first step toward a fully abstracted actuation layer.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- MCP Tool: `play_audio(uri: str, volume: float) -> PlaybackResult`.\n- `PlaybackResult` schema: `{\"status\": \"playing\" | \"error\", \"duration_seconds\": float, \"started_at\": str}`.\n- Input validation: `uri` must match `file://` or `asset://` scheme; `volume` must be in `[0.0, 1.0]`.\n- MCP resource: `audio://playback/status` returning current playback state.\n\n**Implementation (Internal Logic):**\n- ALSA backend using `alsaaudio` or subprocess call to `aplay`.\n- Async playback via `asyncio.to_thread()` to avoid blocking the MCP event loop.\n- Playback event published to `orpheus/actuation/audio/playback` MQTT topic on start/stop.\n\n### Architectural Constraints\n\n- Must run cleanly via ALSA without PulseAudio (Jetson Nano/Orin NX constraint).\n- Must be strictly Python 3.9 compatible.\n- The MCP server must validate all inputs (URI format, volume range 0.0-1.0) before touching hardware.\n- Must not block the event loop — audio playback should be async or threaded.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: MCP Audio Playback Server\n\n Scenario: Play audio via MCP tool call\n Given the MCP server is running and connected to ALSA\n When a client calls play_audio with uri \"asset://crow-call.wav\" and volume 0.5\n Then the server returns status \"playing\" with the correct duration\n And an MQTT event is published to \"orpheus/actuation/audio/playback\"\n\n Scenario: Reject invalid volume\n Given the MCP server is running\n When a client calls play_audio with volume 1.5\n Then the server returns a validation error\n And no audio is played\n\n Scenario: Reject invalid URI scheme\n Given the MCP server is running\n When a client calls play_audio with uri \"http://example.com/audio.wav\"\n Then the server returns a validation error\n And no audio is played\n```\n\n### Definition of Done\n\n- [ ] MCP server initialized in `orpheus-agent-audio-playback` with `play_audio` tool exposed.\n- [ ] Input validation for URI format and volume range.\n- [ ] Tested via local MCP inspector and documented in the agent's README.\n- [ ] Integration test proving an MCP client can trigger playback end-to-end.",
"labels": ["type: feature", "component: agent", "domain: mcp", "domain: hardware", "C4: Container", "help wanted"],
"milestone": "Epic 2: Universal Actuation (MCP)",
"parent_epic": "Epic 2: Universal Actuation (MCP)"
},
{
"title": "[SAFETY] Hardware Circuit Breakers via MCP Middleware",
"body": "### Context\n\nIf the Director agent gets stuck in a positive feedback loop, it could play crow calls all night or burn out a relay. We need hard rate-limiting at the MCP middleware layer that *cannot* be overridden by higher-level agents. Think of it as a fuse box for the physical world.\n\nExamples: max 3 audio playbacks per hour, max 1 feeder drop per day, max 10 relay toggles per hour.\n\n### Dependencies\n\n**Requires:** `[HARDWARE] Implement MCP Server for ALSA Audio Playback`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `CircuitBreaker` class with methods: `check(action: str) -> bool`, `record(action: str)`, `reset(action: str)`, `get_status(action: str) -> BreakerStatus`.\n- `BreakerStatus` dataclass: `{\"action\": str, \"count\": int, \"limit\": int, \"window_seconds\": int, \"is_tripped\": bool, \"resets_at\": str}`.\n- Configuration schema in `orpheus.yaml`: `circuit_breakers.<action>.limit`, `circuit_breakers.<action>.window_seconds`.\n\n**Implementation (Internal Logic):**\n- SQLite-backed counter table with sliding window expiration.\n- MCP middleware hook: intercept tool calls, run `check()` before forwarding to the real handler.\n- MQTT publication to `orpheus/system/safety` on every trip event.\n\n### Architectural Constraints\n\n- Rate limits must be configurable via `orpheus.yaml` (not hardcoded).\n- State must persist across agent restarts — store counters in SQLite or filesystem, not in-memory.\n- Circuit breaker must log every trip to the `orpheus/system/safety` MQTT topic.\n- Must be implemented as a reusable utility in `orpheus-common`, not per-agent.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Hardware Circuit Breakers\n\n Scenario: Allow actions within rate limit\n Given the circuit breaker for \"audio_playback\" is configured with limit 3 per 3600 seconds\n When 2 playback actions have been recorded in the current window\n Then check(\"audio_playback\") returns true\n\n Scenario: Trip circuit breaker at limit\n Given the circuit breaker for \"audio_playback\" is configured with limit 3 per 3600 seconds\n When 3 playback actions have been recorded in the current window\n Then check(\"audio_playback\") returns false\n And an MQTT event is published to \"orpheus/system/safety\" with action \"audio_playback\"\n\n Scenario: Circuit breaker resets after window expires\n Given the circuit breaker for \"audio_playback\" tripped 3601 seconds ago\n When check(\"audio_playback\") is called\n Then it returns true\n And the counter is reset to 0\n```\n\n### Definition of Done\n\n- [ ] `CircuitBreaker` / `RateLimiter` utility added to `orpheus-common`.\n- [ ] Integrated into the audio MCP server and feeder relay agent.\n- [ ] Unit tests proving the circuit breaker trips at the limit and resets after the window.\n- [ ] Manual test documenting behavior when an agent exceeds the rate limit.",
"labels": ["type: safety", "type: feature", "component: common", "domain: hardware", "domain: mcp", "C4: Component", "good first issue"],
"milestone": "Epic 2: Universal Actuation (MCP)",
"parent_epic": "Epic 2: Universal Actuation (MCP)",
"depends_on": ["[HARDWARE] Implement MCP Server for ALSA Audio Playback"]
},
{
"title": "[FEATURE] Agentic Audio Auto-Tuning via MCP",
"body": "### Context\n\nThe current audio playback volume is hardcoded. But ambient noise levels change — wind, rain, traffic, dawn chorus intensity all affect whether a played crow call is audible or overwhelmingly loud. Let agents use MCP to play frequency sweeps and dynamically calibrate volume constraints based on real-time ambient noise measurements.\n\nThe system should tune itself to the environment rather than relying on a human to SSH in and tweak a config value.\n\n### Dependencies\n\n**Requires:** `[HARDWARE] Implement MCP Server for ALSA Audio Playback`\n**Requires:** `[SAFETY] Hardware Circuit Breakers via MCP Middleware`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `CalibrationRoutine` class with methods: `run_sweep() -> CalibrationResult`, `get_current_ceiling() -> float`.\n- `CalibrationResult` dataclass: `{\"ambient_db\": float, \"recommended_volume\": float, \"calibrated_at\": str}`.\n- MCP Tool: `calibrate_audio() -> CalibrationResult` (triggers a sweep and returns the result).\n- Configuration: `audio.auto_tune.sweep_frequency_hz`, `audio.auto_tune.quiet_period_minutes`.\n\n**Implementation (Internal Logic):**\n- Frequency sweep generator: play test tones at decreasing volumes, measure ambient response via microphone.\n- Quiet period detection: only run sweeps when no detections have occurred for N minutes.\n- Volume ceiling stored in SQLite `config_overrides` table and loaded by the audio MCP server.\n\n### Architectural Constraints\n\n- Auto-tuning sweeps must respect circuit breaker limits (they count as playback events).\n- Calibrated volume constraints must be stored persistently and loaded on restart.\n- Must not interfere with active detection — sweeps should only run during quiet periods.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Agentic Audio Auto-Tuning\n\n Scenario: Calibrate volume based on ambient noise\n Given the ambient noise level is 45 dB\n When the calibration routine runs a frequency sweep\n Then the recommended volume ceiling is set between 0.3 and 0.7\n And the result is persisted to the configuration store\n\n Scenario: Respect circuit breaker during calibration\n Given the audio_playback circuit breaker has 1 remaining action\n When the calibration routine attempts a 3-tone sweep\n Then only 1 tone is played\n And the calibration returns an incomplete result with a warning\n\n Scenario: Do not calibrate during active detections\n Given a bird detection occurred 2 minutes ago\n And the quiet period threshold is 5 minutes\n When calibration is requested\n Then the routine defers and returns status \"deferred_active_detections\"\n```\n\n### Definition of Done\n\n- [ ] Calibration agent or routine that plays test tones and measures ambient response.\n- [ ] Dynamic volume ceiling stored in config/DB and respected by the audio MCP server.\n- [ ] Integration test simulating a calibration cycle with mock audio.\n- [ ] Documentation explaining the calibration algorithm and its assumptions.",
"labels": ["type: feature", "component: agent", "domain: mcp", "domain: hardware", "C4: Container"],
"milestone": "Epic 2: Universal Actuation (MCP)",
"parent_epic": "Epic 2: Universal Actuation (MCP)",
"depends_on": [
"[HARDWARE] Implement MCP Server for ALSA Audio Playback",
"[SAFETY] Hardware Circuit Breakers via MCP Middleware"
]
},
{
"title": "[FEATURE] Human Wake-Word Integration",
"body": "### Context\n\nA human standing near the station should be able to say \"Orpheus, what birds are here?\" and get an audible or visual response. This needs to work entirely on-device — no cloud speech APIs, no network dependency. The Jetson has enough compute for a lightweight wake-word detector (e.g., OpenWakeWord, Porcupine) plus a small local ASR model.\n\nThis is the first step toward treating the station as an interactive artifact, not just a passive observer.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `WakeWordDetector` ABC with methods: `start_listening()`, `stop_listening()`, `on_wake_word(callback: Callable)`.\n- `VoiceCommandHandler` ABC with methods: `parse_command(audio: bytes) -> Command`, `execute(command: Command) -> Response`.\n- `Command` dataclass: `{\"intent\": str, \"entities\": dict, \"confidence\": float}`.\n- MQTT events: `orpheus/interaction/wake_word` on detection, `orpheus/interaction/command` on parsed command.\n\n**Implementation (Internal Logic):**\n- OpenWakeWord or Porcupine engine loaded from config-specified model path.\n- ASR via Whisper.cpp (C++ inference, Python bindings) for on-device transcription.\n- Command parser using keyword matching (v1) — no NLU framework needed initially.\n\n### Architectural Constraints\n\n- Must work offline on the Jetson Orin NX — zero cloud dependencies for wake-word and ASR.\n- Must not interfere with the primary BirdNET/AVES audio pipeline (separate audio stream or time-slicing).\n- Wake-word model must be swappable (configuration-driven, not hardcoded).\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Human Wake-Word Integration\n\n Scenario: Detect wake word and process command\n Given the wake-word detector is listening for \"Orpheus\"\n When a human says \"Orpheus, what birds are here?\"\n Then a wake_word event is published to MQTT\n And the ASR transcribes the command\n And the system responds with the list of recently detected species\n\n Scenario: Ignore non-wake-word speech\n Given the wake-word detector is listening for \"Orpheus\"\n When ambient human conversation occurs without the wake word\n Then no wake_word event is published\n And the BirdNET pipeline is not interrupted\n\n Scenario: Operate fully offline\n Given the Jetson has no network connectivity\n When a human says \"Orpheus, what birds are here?\"\n Then the system still detects the wake word and processes the command\n```\n\n### Definition of Done\n\n- [ ] Wake-word detection agent created (`orpheus-agent-wake-word` or integrated into existing agent).\n- [ ] At least one command implemented end-to-end (e.g., \"what birds are here?\").\n- [ ] Tested with recorded wake-word samples in CI.\n- [ ] ADR documenting the choice of wake-word engine and ASR model.",
"labels": ["type: feature", "component: agent", "domain: hardware", "C4: System Context"],
"milestone": "Epic 3: Interspecies Interfaces",
"parent_epic": "Epic 3: Interspecies Interfaces"
},
{
"title": "[FEATURE] Privacy & Human Anomaly Filtering",
"body": "### Context\n\nWhen the microphone picks up human conversation, we must detect it and strip it from saved recordings. This is both an ethical requirement (nobody wants their private chat stored on a wildlife station's SD card) and potentially a legal one depending on jurisdiction.\n\nThe system should detect speech segments, redact or mute them in stored audio, and log the filtering event without storing the content.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `SpeechFilter` ABC with methods: `detect_speech(audio_chunk: bytes) -> List[SpeechSegment]`, `redact(audio: bytes, segments: List[SpeechSegment]) -> bytes`.\n- `SpeechSegment` dataclass: `{\"start_ms\": int, \"end_ms\": int, \"confidence\": float}`.\n- `FilterAuditEntry` dataclass: `{\"timestamp\": str, \"segment_count\": int, \"total_duration_ms\": int}` — NO speech content.\n- MQTT event: `orpheus/privacy/speech_filtered` with audit entry payload.\n\n**Implementation (Internal Logic):**\n- Silero VAD model for speech detection (lightweight, runs on CPU).\n- Redaction by replacing detected segments with silence (zero-fill, preserving sample rate and channel count).\n- Audit log written to SQLite `privacy_audit` table.\n\n### Architectural Constraints\n\n- Speech detection must run on-device (no cloud APIs).\n- Must not interfere with the BirdNET classification pipeline — filter at the storage layer, not the detection layer.\n- Filtered segments should be replaced with silence (not deleted) to preserve timeline integrity.\n- Filtering metadata (timestamps, duration) must be logged for audit purposes, but speech content must NOT be logged.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Privacy & Human Anomaly Filtering\n\n Scenario: Detect and redact human speech from audio\n Given an audio chunk contains 3 seconds of human speech at offset 5s\n When the speech filter processes the chunk\n Then the output audio has silence from 5s to 8s\n And the original audio segment is not stored anywhere\n\n Scenario: Log filtering event without speech content\n Given human speech was detected and redacted\n When the audit log is written\n Then it contains the timestamp and duration of filtered segments\n And it does NOT contain any transcription or audio data\n\n Scenario: Preserve birdsong during speech filtering\n Given an audio chunk contains overlapping birdsong and human speech\n When the speech filter processes the chunk\n Then only the speech segments are replaced with silence\n And the birdsong portions are preserved intact\n```\n\n### Definition of Done\n\n- [ ] Speech detection model integrated (e.g., Silero VAD or similar lightweight model).\n- [ ] Audio storage pipeline filters human speech segments before writing to disk.\n- [ ] Audit log records filtering events with timestamps but no speech content.\n- [ ] Unit tests with synthetic speech + birdsong audio proving correct filtering.",
"labels": ["type: feature", "component: agent", "domain: privacy", "domain: hardware", "C4: System Context"],
"milestone": "Epic 3: Interspecies Interfaces",
"parent_epic": "Epic 3: Interspecies Interfaces"
},
{
"title": "[FEATURE] Personal Observer Mobile App (Push Alerts)",
"body": "### Context\n\nBuild a lightweight mobile interface that sends push notifications when interesting events happen at the station. \"A coyote was just detected.\" \"Dawn chorus peak — 12 species active.\" The goal is to Pavlov-condition human operators to pay attention to their local wildlife.\n\nThis could be a PWA (progressive web app) for maximum portability or a thin native shell around the existing React dashboard.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `NotificationService` ABC with methods: `send(event: NotifiableEvent) -> DeliveryResult`, `register_device(token: str)`, `unregister_device(token: str)`.\n- `NotifiableEvent` dataclass: `{\"title\": str, \"body\": str, \"category\": str, \"priority\": str, \"event_id\": str}`.\n- REST API endpoint: `POST /api/notifications/subscribe` with device token.\n- Configuration: `notifications.provider` (\"webpush\" | \"ntfy\"), `notifications.rate_limit_per_hour`.\n\n**Implementation (Internal Logic):**\n- Event filter in FastAPI backend subscribing to MQTT notable events.\n- Rate limiter (token bucket) to prevent alert fatigue.\n- Web Push (VAPID) as primary provider, ntfy.sh as lightweight alternative.\n\n### Architectural Constraints\n\n- Must work with the existing FastAPI backend — no new backend services.\n- Push notifications via web push (VAPID) or a lightweight service like ntfy.sh.\n- Must not require the mobile device to be on the same network as the station (cloud relay or push service required).\n- Notification frequency must be rate-limited to avoid alert fatigue.\n- Must be Python 3.9 compatible on the backend.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Personal Observer Mobile App\n\n Scenario: Receive push notification for notable detection\n Given a user has subscribed to push notifications\n When a coyote detection event is published to MQTT\n Then the user receives a push notification with title \"Coyote Detected\"\n And the notification body includes the timestamp and confidence\n\n Scenario: Rate limiting prevents alert fatigue\n Given the rate limit is set to 10 notifications per hour\n When 15 notable events occur within one hour\n Then only 10 push notifications are sent\n And the remaining 5 are silently dropped\n\n Scenario: Notification works off-network\n Given the mobile device is on a cellular network\n And the station is on a local network\n When a notable event occurs\n Then the push notification is delivered via the cloud relay\n```\n\n### Definition of Done\n\n- [ ] Push notification infrastructure chosen and documented (ADR).\n- [ ] Backend endpoint that publishes notable events to the push service.\n- [ ] Mobile-friendly UI (PWA or native shell) that receives and displays alerts.\n- [ ] Rate limiting on notifications (configurable, default: max 10/hour).",
"labels": ["type: feature", "component: ui-dash", "C4: Container", "help wanted"],
"milestone": "Epic 3: Interspecies Interfaces",
"parent_epic": "Epic 3: Interspecies Interfaces"
},
{
"title": "[FEATURE] Weatherproof Field Display for the Floating Tank",
"body": "### Context\n\nMount a ruggedized, weatherproof screen on the exterior of the Floating Tank that shows real-time status: recent detections, environmental data, system health, and a live spectrogram. Think of it as a kiosk for the forest — useful for field researchers, visitors, and debugging without opening the enclosure.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `DisplayFeed` ABC with methods: `get_frame() -> DisplayFrame`, `get_status() -> SystemStatus`.\n- `DisplayFrame` dataclass: `{\"detections\": List[Detection], \"weather\": WeatherData, \"health\": HealthStatus, \"spectrogram_uri\": str}`.\n- HTTP endpoint: `GET /api/display/frame` returning JSON for the current display state.\n- Configuration: `display.refresh_interval_seconds`, `display.renderer` (\"framebuffer\" | \"chromium-kiosk\").\n\n**Implementation (Internal Logic):**\n- FastAPI endpoint aggregating latest detections, weather, and health from SQLite.\n- Framebuffer renderer using Python `pillow` for e-ink/LCD output, or Chromium kiosk mode for HDMI displays.\n- Systemd service with `Restart=always` and watchdog timer.\n\n### Architectural Constraints\n\n- Display must be readable in direct sunlight (e-ink or high-brightness LCD).\n- Must connect via a simple interface (HDMI, SPI, or USB) — no WiFi display protocols.\n- Software must be a lightweight renderer (framebuffer or minimal browser), not a full desktop environment.\n- Must auto-start on boot and recover from crashes without manual intervention.\n- Must be Python 3.9 compatible for the data feed; display rendering can use other tools.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Weatherproof Field Display\n\n Scenario: Display shows recent detections\n Given 5 bird detections occurred in the last hour\n When the display refreshes\n Then the screen shows the 5 most recent detections with species and timestamps\n\n Scenario: Display recovers from crash\n Given the display renderer process crashes\n When systemd detects the failure\n Then the renderer is restarted within 10 seconds\n And the display resumes showing current data\n\n Scenario: Display readable in sunlight\n Given the display hardware is a high-brightness LCD or e-ink panel\n When mounted on the exterior of the Floating Tank\n Then the display content is legible in direct sunlight\n```\n\n### Definition of Done\n\n- [ ] Hardware recommendation documented (specific display model, mounting approach).\n- [ ] Data feed service that publishes display-formatted status to a local endpoint.\n- [ ] Rendering application that displays the feed on the connected screen.\n- [ ] Systemd service file with auto-restart and watchdog.",
"labels": ["type: feature", "component: infra", "domain: hardware", "C4: Container", "help wanted"],
"milestone": "Epic 3: Interspecies Interfaces",
"parent_epic": "Epic 3: Interspecies Interfaces"
},
{
"title": "[ARCH] Abstract paho-mqtt Behind a Generic EventBus Interface",
"body": "### Context\n\nMQTT is excellent for lightweight IoT pub/sub, but Active Inference requires durable, replayable event logs. Currently, agents import `paho-mqtt` directly, which tightly couples them to a specific transport. We need to refactor `orpheus_common.mqtt` into an abstract `EventBus` base class with dependency-injected backends.\n\nAfter this refactor, no agent should import `paho-mqtt` directly. The transport becomes a deployment decision, not an application decision.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `EventBus` ABC with methods:\n - `publish(topic: str, payload: dict, qos: int = 0) -> None`\n - `subscribe(topic: str, callback: Callable[[str, dict], None]) -> None`\n - `unsubscribe(topic: str) -> None`\n - `connect() -> None`\n - `disconnect() -> None`\n - `is_connected() -> bool`\n- Type alias: `EventCallback = Callable[[str, dict], None]`\n- Factory function: `create_event_bus(config: OrpheusConfig) -> EventBus` that reads `event_bus.backend` from config.\n\n**Implementation (Internal Logic):**\n- `MQTTBus(EventBus)` wrapping existing `paho-mqtt` behavior with identical semantics.\n- Connection lifecycle managed internally (auto-reconnect, backoff).\n- `create_event_bus()` factory using a registry pattern for backend discovery.\n\n### Architectural Constraints\n\n- Zero changes to agent business logic — only `orpheus-common` refactoring and dependency injection.\n- The `EventBus` ABC must support: `publish()`, `subscribe()`, `unsubscribe()`, and connection lifecycle.\n- Provide an `MQTTBus` implementation that wraps the existing paho-mqtt behavior identically.\n- Must maintain backward compatibility with existing MQTT topic structures.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: EventBus Abstraction\n\n Scenario: Publish and subscribe via EventBus interface\n Given an MQTTBus instance connected to a test broker\n When a message is published to \"orpheus/test/topic\"\n Then a subscriber on \"orpheus/test/topic\" receives the message\n\n Scenario: No agent imports paho-mqtt directly\n Given all agents have been updated to use EventBus\n When scanning all agent source files for \"import paho\"\n Then zero matches are found\n\n Scenario: Factory creates correct backend from config\n Given orpheus.yaml has event_bus.backend set to \"mqtt\"\n When create_event_bus() is called\n Then an MQTTBus instance is returned\n```\n\n### Definition of Done\n\n- [ ] `EventBus` abstract base class defined in `orpheus-common`.\n- [ ] `MQTTBus` implementation passes all existing MQTT tests.\n- [ ] All 11 agents updated to use `EventBus` via dependency injection (no direct paho imports).\n- [ ] ADR documenting the EventBus abstraction and migration path.",
"labels": ["type: refactor", "component: common", "domain: event-bus", "C4: Component"],
"milestone": "Epic 4: Event Bus Evolution",
"parent_epic": "Epic 4: Event Bus Evolution"
},
{
"title": "[SPIKE] Durable Event Backend Evaluation (Redis Streams vs NATS vs Kafka)",
"body": "### Context\n\nWith the `EventBus` abstraction in place, we need to choose a durable stream backend. Evaluate Redis Streams, NATS JetStream, and Kafka for edge viability (memory footprint, ARM compatibility, operational complexity). The winner becomes the second `EventBus` adapter.\n\nDurable streams unlock consumer groups (multiple agents processing the same events independently) and historical replay (critical for Active Inference training and debugging).\n\n**Time-box: 3 days**\n\n### Dependencies\n\n**Requires:** `[ARCH] Abstract paho-mqtt Behind a Generic EventBus Interface`\n\n### Architectural Constraints\n\n- Must run on the Jetson Orin NX (8GB RAM shared with GPU) — memory footprint matters.\n- Must support consumer groups for independent agent processing.\n- Must support message persistence across broker restarts.\n- Must be deployable via the existing systemd service management.\n- Must be Python 3.9 compatible.\n\n### Definition of Done\n\n- [ ] **ADR Deliverable:** `docs/adr/XXXX-durable-event-backend.md` documenting the evaluation matrix (memory, ARM compat, consumer groups, persistence, operational complexity) and final recommendation.\n- [ ] Evaluation document with benchmarks in `docs/research/durable-event-backend-eval.md`.\n- [ ] Proof-of-concept: chosen backend running on Jetson with a simple publish/subscribe test.\n- [ ] Performance benchmark on Jetson hardware (throughput, memory, latency).\n- [ ] Time-box respected: if evaluation inconclusive within 3 days, document findings and recommend next steps.",
"labels": ["type: spike", "component: common", "domain: event-bus", "C4: Container", "help wanted"],
"milestone": "Epic 4: Event Bus Evolution",
"parent_epic": "Epic 4: Event Bus Evolution",
"depends_on": ["[ARCH] Abstract paho-mqtt Behind a Generic EventBus Interface"]
},
{
"title": "[FEATURE] Historical Event Replay Tooling",
"body": "### Context\n\nBuild tools to inject historical `EntityEvents` back into the event bus. This unlocks three capabilities:\n\n1. **Offline analysis** — replay a week of events through a new agent to test its behavior.\n2. **Regression testing** — replay known scenarios to verify system behavior hasn't changed.\n3. **Counterfactual queries** — \"What would the system have done if the Director had been active last Tuesday?\"\n\n### Dependencies\n\n**Requires:** `[ARCH] Abstract paho-mqtt Behind a Generic EventBus Interface`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- CLI: `orpheus-replay --source <path> --speed <multiplier> --start <timestamp> --end <timestamp> --tag <label>`.\n- `ReplayEngine` class with methods: `load(source: str) -> int` (returns event count), `play(speed: float = 1.0, start: Optional[str] = None, end: Optional[str] = None)`.\n- Replayed events MUST include metadata: `{\"_replay\": true, \"_replay_tag\": str, \"_original_timestamp\": str}`.\n\n**Implementation (Internal Logic):**\n- SQLite reader that loads events from the detection database, sorted by timestamp.\n- Time-dilation engine: calculate inter-event delays, divide by speed multiplier.\n- EventBus publisher: works with both MQTT and durable stream backends via the ABC.\n\n### Architectural Constraints\n\n- Replay must preserve original event timestamps (not use current time).\n- Replay speed must be configurable: real-time (1x), fast-forward (10x, 100x), or as-fast-as-possible.\n- Replayed events must be clearly tagged to prevent confusion with live data.\n- Must work with both MQTT and durable stream backends.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Historical Event Replay\n\n Scenario: Replay events at real-time speed\n Given a SQLite database with 10 events spanning 60 seconds\n When orpheus-replay runs with --speed 1.0\n Then events are published with approximately 6-second intervals\n And each event has _replay = true in its metadata\n\n Scenario: Replay events at 10x speed\n Given a SQLite database with 10 events spanning 60 seconds\n When orpheus-replay runs with --speed 10.0\n Then all 10 events are published in approximately 6 seconds\n\n Scenario: Filter replay by time range\n Given a SQLite database with events from 2025-01-01 to 2025-01-07\n When orpheus-replay runs with --start 2025-01-03 --end 2025-01-04\n Then only events from January 3rd are replayed\n```\n\n### Definition of Done\n\n- [ ] CLI tool (`orpheus-replay`) that reads events from SQLite/export and publishes to the bus.\n- [ ] Support for speed control (1x, 10x, max) and time-range filtering.\n- [ ] Replayed events tagged with `replay: true` metadata.\n- [ ] Integration test replaying a synthetic event sequence and verifying agent responses.",
"labels": ["type: feature", "component: common", "domain: event-bus", "C4: Component"],
"milestone": "Epic 4: Event Bus Evolution",
"parent_epic": "Epic 4: Event Bus Evolution",
"depends_on": ["[ARCH] Abstract paho-mqtt Behind a Generic EventBus Interface"]
},
{
"title": "[FEATURE] Ecowitt Weather Station Integration (#153)",
"body": "### Context\n\nThere's a local Ecowitt weather station at the field site. Ingest its data — temperature, humidity, barometric pressure, wind speed/direction, rainfall — into `SpatiotemporalContext` so agents can correlate wildlife activity with weather patterns.\n\n\"Do crows visit more before a storm?\" \"Does coyote activity increase on cold nights?\" We can't answer these questions without environmental context.\n\n### Dependencies\n\n**Requires:** `[ARCH] Generalize the EntityEvent State Space Taxonomy`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `WeatherProvider` ABC with methods: `fetch() -> WeatherReading`, `get_latest() -> Optional[WeatherReading]`.\n- `WeatherReading` dataclass: `{\"temperature_c\": float, \"humidity_pct\": float, \"pressure_hpa\": float, \"wind_speed_mps\": float, \"wind_direction_deg\": int, \"rainfall_mm\": float, \"timestamp\": str}`.\n- `SpatiotemporalContext` model extended with optional `weather: WeatherReading` field.\n- MQTT publication: `orpheus/environment/weather` with WeatherReading payload.\n\n**Implementation (Internal Logic):**\n- `EcowittProvider(WeatherProvider)` polling the local HTTP API at configurable intervals.\n- SQLite storage: `weather_readings` table with all fields plus `recorded_at` timestamp.\n- Graceful degradation: if Ecowitt API unreachable, log warning and continue with `weather = None`.\n\n### Architectural Constraints\n\n- Ecowitt exposes data via a local HTTP API (no cloud dependency required).\n- Weather data must be stored in the same SQLite database as detection events for easy joins.\n- Polling interval must be configurable (default: 5 minutes).\n- Must handle Ecowitt API unavailability gracefully (log warning, continue without weather data).\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Ecowitt Weather Station Integration\n\n Scenario: Ingest weather data from Ecowitt\n Given the Ecowitt station is reachable at the configured URL\n When the weather agent polls the HTTP API\n Then a WeatherReading is stored in SQLite\n And an MQTT event is published to \"orpheus/environment/weather\"\n\n Scenario: Correlate detection with weather\n Given a crow detection event and a weather reading at the same timestamp\n When the SpatiotemporalContext is built for the detection\n Then the context includes weather data (temperature, humidity, wind)\n\n Scenario: Graceful degradation when Ecowitt offline\n Given the Ecowitt station is unreachable\n When the weather agent polls the HTTP API\n Then a warning is logged\n And the system continues operating with weather = None\n```\n\n### Definition of Done\n\n- [ ] Weather ingestion service or agent that polls Ecowitt HTTP API.\n- [ ] `SpatiotemporalContext` model updated with weather fields in `orpheus-common`.\n- [ ] Weather data stored in SQLite and available to agents via state space queries.\n- [ ] Dashboard widget showing current weather conditions alongside detection activity.",
"labels": ["type: feature", "component: agent", "component: common", "domain: observability", "C4: System Context", "good first issue"],
"milestone": "Epic 5: Observability & Telemetry",
"parent_epic": "Epic 5: Observability & Telemetry",
"depends_on": ["[ARCH] Generalize the EntityEvent State Space Taxonomy"]
},
{
"title": "[REFACTOR] OpenTelemetry (OTel) Migration (#95)",
"body": "### Context\n\nRight now, debug logging, health checks, and system metrics share the MQTT event bus with actual detection events. This pollutes the control plane and makes it hard to set up proper observability dashboards. Migrate the observability plane to OpenTelemetry with a Jaeger or Tempo backend.\n\nAfter this migration, MQTT carries only domain events (detections, actuations), while OTel carries traces, metrics, and health telemetry.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `setup_tracing(service_name: str, config: OrpheusConfig) -> TracerProvider` in `orpheus-common`.\n- `get_tracer(name: str) -> Tracer` mirroring the existing `get_logger()` pattern.\n- Trace context propagation: W3C `traceparent` header injected into MQTT message metadata.\n- Configuration: `telemetry.backend` (\"jaeger\" | \"tempo\" | \"console\"), `telemetry.sample_rate`.\n\n**Implementation (Internal Logic):**\n- `opentelemetry-sdk` with OTLP exporter to Jaeger/Tempo.\n- MQTT middleware that injects/extracts W3C trace context on publish/subscribe.\n- Sampling strategy: configurable rate (default 10%) to stay within CPU budget.\n\n### Architectural Constraints\n\n- Must use the `opentelemetry-api` and `opentelemetry-sdk` Python packages.\n- Traces must propagate across agent boundaries via MQTT message headers (W3C trace context).\n- Jaeger or Tempo backend must be deployable on the Jetson (Docker container or native binary).\n- Must not increase CPU overhead by more than 5% (sampling configuration required).\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: OpenTelemetry Migration\n\n Scenario: Trace spans detection-to-actuation pipeline\n Given OTel tracing is enabled with Jaeger backend\n When a bird detection flows through classification → correlation → actuation\n Then a single distributed trace appears in Jaeger with spans for each stage\n\n Scenario: MQTT bus free of observability traffic\n Given OTel migration is complete\n When monitoring the MQTT topic tree\n Then no health-check, debug, or metrics messages appear on MQTT topics\n And domain events (detections, actuations) continue flowing normally\n\n Scenario: Sampling respects CPU budget\n Given the sample rate is set to 0.1 (10%)\n When 1000 events are processed\n Then approximately 100 traces are exported to the backend\n And CPU overhead remains below 5% above baseline\n```\n\n### Definition of Done\n\n- [ ] OTel SDK integrated into `orpheus-common` with a `setup_tracing()` helper.\n- [ ] At least 3 agents instrumented with traces spanning detection → correlation → actuation.\n- [ ] Jaeger/Tempo backend added to the Docker Compose dev environment.\n- [ ] MQTT bus verified to be free of observability traffic after migration.",
"labels": ["type: refactor", "component: common", "domain: observability", "C4: Component", "help wanted"],
"milestone": "Epic 5: Observability & Telemetry",
"parent_epic": "Epic 5: Observability & Telemetry"
},
{
"title": "[INFRA] DRY Makefile Architecture with OS/Arch Detection",
"body": "### Context\n\nWhile the recent PR #193 standardized Makefiles across components, there's still duplicated logic. Consolidate into a centralized `Makefile.include` pattern with automatic OS/Arch detection (`uname -m`, `uname -s`) so the same targets work seamlessly on macOS (dev), Linux x86 (CI), and Linux ARM (Jetson).\n\nOne include file, many thin component Makefiles. No more copy-paste drift.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `Makefile.include` exposes standard targets: `test`, `lint`, `format`, `coverage`, `clean`, `install`, `build`.\n- Auto-detected variables: `OS` (darwin/linux), `ARCH` (x86_64/aarch64), `PYTHON` (resolved interpreter path).\n- Component Makefiles: `include ../../Makefile.include` + component-specific variable overrides.\n\n**Implementation (Internal Logic):**\n- OS/Arch detection via `uname -s` / `uname -m` with fallback defaults.\n- Conditional blocks for platform-specific flags (e.g., `--platform linux/arm64` for Docker on macOS).\n- Coverage threshold enforcement: `coverage report --fail-under=$(COVERAGE_MIN)`.\n\n### Architectural Constraints\n\n- Must work with GNU Make 3.81+ (macOS ships an old version).\n- OS detection must handle: macOS (Darwin), Linux x86_64, Linux aarch64 (Jetson).\n- Component Makefiles should be <20 lines, delegating all shared logic to the include.\n- Must not break any existing `make test` / `make lint` / `make coverage` targets.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: DRY Makefile Architecture\n\n Scenario: Component Makefile delegates to shared include\n Given a component Makefile includes Makefile.include\n When running \"make test\" in the component directory\n Then the shared test target executes with correct OS/Arch variables\n\n Scenario: OS detection on macOS\n Given the build runs on macOS (Darwin)\n When Makefile.include detects the OS\n Then OS is set to \"darwin\" and ARCH is set correctly\n\n Scenario: OS detection on Jetson\n Given the build runs on Linux aarch64\n When Makefile.include detects the OS\n Then OS is set to \"linux\" and ARCH is set to \"aarch64\"\n```\n\n### Definition of Done\n\n- [ ] `Makefile.include` created at the repo root with shared targets and OS/Arch detection.\n- [ ] All component Makefiles refactored to include it.\n- [ ] Verified on macOS, Linux x86, and Linux ARM (or cross-compilation simulation).\n- [ ] Documentation in `CONTRIBUTING.md` explaining the Makefile architecture.",
"labels": ["type: infrastructure", "component: infra", "C4: Container", "good first issue"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility"
},
{
"title": "[INFRA] Implement BATS (Bash Automated Testing System)",
"body": "### Context\n\nOrpheus has critical shell scripts — startup sequences, health checks, deployment automation, systemd helpers — with zero test coverage. If a bash script runs in production, it should have a test. Implement BATS to cover the shell infrastructure.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- Test file location: `tests/bats/*.bats`.\n- Each `.bats` file tests one script or one logical group of scripts.\n- Helper functions: `tests/bats/test_helper.bash` for shared setup/teardown.\n- Makefile target: `make test-bash` at the repo root.\n\n**Implementation (Internal Logic):**\n- `bats-core` with `bats-assert` and `bats-support` installed via git submodules in `tests/bats/lib/`.\n- System command mocking via PATH manipulation (prepend mock directory).\n- CI integration: new GitHub Actions step after Python tests.\n\n### Architectural Constraints\n\n- Use `bats-core` with `bats-assert` and `bats-support` helper libraries.\n- Tests must run in CI without requiring hardware or root access.\n- Tests should mock system commands (systemctl, jtop) where necessary.\n- Must not require any dependencies beyond bash and bats-core.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: BATS Shell Script Testing\n\n Scenario: Run shell tests via Makefile\n Given BATS is installed in the repository\n When running \"make test-bash\" at the repo root\n Then all .bats test files execute\n And the exit code reflects pass/fail status\n\n Scenario: Mock system commands in tests\n Given a test for the health-check script\n When the test mocks \"systemctl\" to return a failure\n Then the health-check script reports the correct service as down\n And no actual systemctl commands are executed\n```\n\n### Definition of Done\n\n- [ ] BATS installed and configured in CI pipeline.\n- [ ] Tests written for at least 5 critical shell scripts (startup, health-check, deploy, etc.).\n- [ ] `make test-bash` target added to the root Makefile.\n- [ ] Documentation in `docs/TESTING.md` explaining how to write BATS tests.",
"labels": ["type: infrastructure", "component: infra", "C4: Container", "good first issue", "help wanted"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility"
},
{
"title": "[SPIKE] Package Management Evaluation (uv vs Poetry)",
"body": "### Context\n\nThe current Python dependency management uses a mix of `pip` and `poetry`. Evaluate migrating to `uv` — it's significantly faster for dependency resolution, produces cleaner lockfiles, and handles virtual environments more predictably.\n\nThis is an evaluation task, not a full migration. Run a proof-of-concept on one agent, measure the results, and make a recommendation.\n\n**Time-box: 2 days**\n\n### Architectural Constraints\n\n- Must work on all target platforms: macOS, Linux x86, Linux aarch64 (Jetson).\n- Must support Python 3.9 (the project minimum).\n- Must not break the existing `make test` / `make lint` workflow.\n- If migration is recommended, provide a phased rollout plan (not big-bang).\n\n### Definition of Done\n\n- [ ] **ADR Deliverable:** `docs/adr/XXXX-python-package-management.md` documenting the evaluation and recommendation (migrate to uv or stay with poetry) with rationale.\n- [ ] One agent migrated to `uv` as a proof-of-concept (suggest: `orpheus-agent-audio-motion`).\n- [ ] Benchmarks comparing `uv` vs `poetry` for: install time, resolve time, lockfile size.\n- [ ] Compatibility verified on macOS and Linux ARM.\n- [ ] Time-box respected: if evaluation inconclusive within 2 days, document findings and close.",
"labels": ["type: spike", "component: infra", "C4: Container"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility"
},
{
"title": "[INFRA] Independent Component Versioning System",
"body": "### Context\n\nCurrently, `platform/orpheus-common` and every agent in `agents/` share an implicit monorepo version with no independent release cadence. A fix to `orpheus-common` forces consumers to track the repo HEAD; a breaking change to one agent has no formal version signal. Implement independent SemVer versioning for each component with an explicit dependency graph strategy.\n\nThis is the prerequisite for PyPI publishing — a component cannot be published as a package without a stable, independent version.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- Each component (`platform/orpheus-common`, each `agents/*`) carries its own `VERSION` file (e.g., `1.2.0`) at the package root.\n- `pyproject.toml` (or `setup.cfg`) `version` field reads from `VERSION` at build time.\n- Dependency declarations: agent `pyproject.toml` files declare `orpheus-common >= <X.Y.Z>` (minimum compatible version), not a pinned commit.\n- Version bump script: `scripts/bump-version.sh <component-path> <major|minor|patch>` updates the `VERSION` file and generates a CHANGELOG entry.\n\n**Implementation (Internal Logic):**\n- Version files: one `VERSION` file per component; CI reads this to tag releases.\n- Compatibility matrix: `docs/version-compat-matrix.md` tracking which agent versions are compatible with which `orpheus-common` versions.\n- Semantic constraints: breaking changes to a public API in `orpheus-common` require a major bump; new backwards-compatible features require a minor bump.\n- ADR documenting versioning policy: when to bump, who approves, how compatibility is declared.\n\n### Architectural Constraints\n\n- `orpheus-common` versions must use SemVer (`MAJOR.MINOR.PATCH`) strictly — no date-based or commit-hash versions.\n- Agents must declare a minimum `orpheus-common` version, not a maximum (open upper bound acceptable until a breaking change is detected).\n- The `VERSION` file must be the single source of truth — no version strings duplicated in `__init__.py` or `setup.cfg`.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Independent Component Versioning\n\n Scenario: Each component has an independent VERSION file\n Given the repository contains orpheus-common and 11 agents\n When scanning all component roots for a VERSION file\n Then every component has exactly one VERSION file with a valid SemVer string\n\n Scenario: Agent declares minimum orpheus-common version\n Given orpheus-agent-bird-detection requires orpheus-common >= 0.9.0\n When the agent's pyproject.toml is parsed\n Then the dependency declaration is \"orpheus-common >= 0.9.0\"\n And no upper-bound pin is present\n\n Scenario: Version bump script updates VERSION file\n Given orpheus-common VERSION is \"0.9.0\"\n When running \"scripts/bump-version.sh platform/orpheus-common minor\"\n Then the VERSION file contains \"0.10.0\"\n And a CHANGELOG entry is generated for the bump\n```\n\n### Definition of Done\n\n- [ ] `VERSION` file added to `platform/orpheus-common` and all agents in `agents/`.\n- [ ] Each `pyproject.toml` reads version from its `VERSION` file at build time.\n- [ ] `scripts/bump-version.sh` script that updates a component VERSION and prepends a CHANGELOG stub.\n- [ ] `docs/version-compat-matrix.md` created documenting the current compatibility baseline.\n- [ ] ADR: `docs/adr/XXXX-independent-component-versioning.md` documenting the versioning policy.",
"labels": ["type: infrastructure", "component: infra", "component: common", "C4: Container"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility"
},
{
"title": "[INFRA] PyPI Publishing Pipeline (Common + Agents)",
"body": "### Context\n\nOrpheus components are currently only consumable by cloning the repository. Publishing `orpheus-common` and each agent as independent PyPI packages enables third-party integrations, reuse in other wildlife monitoring projects, and a clean dependency model for contributors who only want to use a subset of the system.\n\nThe goal: `pip install orpheus-agent-bird-detection` should work.\n\n### Dependencies\n\n**Requires:** `[INFRA] Independent Component Versioning System`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- Each component has a `pyproject.toml` conforming to the PEP 517/518 build standard.\n- Package names follow the `orpheus-<component-type>-<name>` convention: `orpheus-common`, `orpheus-agent-bird-detection`, `orpheus-agent-audio-motion`, etc.\n- Extras: agents declare `extras_require` for optional hardware dependencies (e.g., `[jetson]` for ALSA/GPIO bindings).\n- Build backend: `hatchling` or `setuptools >= 61.0` (must support `dynamic = [\"version\"]` reading from `VERSION` file).\n\n**Implementation (Internal Logic):**\n- Template `pyproject.toml` for agent packages: project name, description, Python requires (>=3.9), dependencies, optional extras.\n- Template `pyproject.toml` for `orpheus-common`: classifiers, author, license, README long description.\n- Build validation: `python -m build --wheel` must succeed for every component in CI.\n- Test PyPI: publish to TestPyPI on every PR touching a component, publish to PyPI on version tag.\n\n### Architectural Constraints\n\n- Packages must declare `python_requires = \">=3.9\"` — no 3.10+ only packages.\n- Hardware-specific dependencies (ALSA, GPIO, CUDA) must be in optional `extras_require`, not required dependencies, so packages install cleanly on developer machines.\n- README files for each component must be suitable as PyPI long descriptions (no repo-relative image paths).\n- Must not require a monorepo install tool (pip-installable as standalone packages).\n\n### Acceptance Criteria\n\n```gherkin\nFeature: PyPI Publishing Pipeline\n\n Scenario: Install orpheus-common from PyPI\n Given orpheus-common has been published to PyPI\n When running \"pip install orpheus-common\" in a clean environment\n Then the package installs without errors on Python 3.9, 3.10, and 3.11\n\n Scenario: Install agent from PyPI\n Given orpheus-agent-bird-detection has been published to PyPI\n When running \"pip install orpheus-agent-bird-detection\"\n Then the package installs with orpheus-common as a transitive dependency\n\n Scenario: Hardware extras install cleanly on developer machine\n Given orpheus-agent-audio-motion is installed without extras\n When running \"pip install orpheus-agent-audio-motion\"\n Then the package installs without requiring ALSA or GPIO libraries\n And the [jetson] extra can be installed separately on Jetson hardware\n\n Scenario: Build wheel in CI\n Given a PR modifies any file in platform/orpheus-common\n When the CI pipeline runs\n Then \"python -m build --wheel\" succeeds for orpheus-common\n And the wheel is uploaded to TestPyPI as a pre-release artifact\n```\n\n### Definition of Done\n\n- [ ] `pyproject.toml` added to `platform/orpheus-common` and all agents using the approved template.\n- [ ] Template documents created: `docs/templates/pyproject-agent.toml.template` and `docs/templates/pyproject-common.toml.template`.\n- [ ] Hardware-specific dependencies moved to `[jetson]` extras across all relevant agents.\n- [ ] CI step: `python -m build --wheel` validates every component's package build on each PR.\n- [ ] TestPyPI publication configured for pre-release validation on tagged commits.\n- [ ] `CONTRIBUTING.md` updated with instructions for releasing a new package version.",
"labels": ["type: infrastructure", "component: infra", "component: common", "C4: Container"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility",
"depends_on": ["[INFRA] Independent Component Versioning System"]
},
{
"title": "[INFRA] Automated Release Workflows (Change Detection + PyPI Push)",
"body": "### Context\n\nWith independent versioned components and PyPI-ready packages, we need a GitHub Actions workflow that automates the release pipeline end-to-end: detect which components changed in a PR, run their specific test suites, and — upon a version tag — build and push only the affected components to PyPI. No manual release scripts, no full-repo rebuilds when one agent changes.\n\n### Dependencies\n\n**Requires:** `[INFRA] Independent Component Versioning System`\n**Requires:** `[INFRA] PyPI Publishing Pipeline (Common + Agents)`\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- Workflow file: `.github/workflows/release.yml` triggered on push to `main` and on version tags (`v*.*.*` or `<component>/v*.*.*`).\n- Change detection: `scripts/changed-components.sh` reads `git diff --name-only` and outputs the set of component paths that contain modified files.\n- Tag convention: `orpheus-common/v1.2.0` releases `orpheus-common`; `orpheus-agent-bird-detection/v0.5.0` releases that agent independently.\n- Matrix build: GitHub Actions matrix strategy over `changed-components.sh` output to parallelize per-component test and build jobs.\n\n**Implementation (Internal Logic):**\n- `changed-components.sh`: maps changed file paths to component roots (e.g., `platform/orpheus-common/src/...` → `platform/orpheus-common`).\n- PR workflow: for each changed component, run `make test && make lint && make coverage` as a matrix job.\n- Release workflow (tag push): for each tagged component, run `python -m build` and `twine upload` to PyPI using OIDC trusted publishing.\n- Dependency ordering: if a tag releases `orpheus-common`, trigger downstream agent builds in dependency order.\n\n### Architectural Constraints\n\n- Must use GitHub Actions OIDC trusted publishing to PyPI (no long-lived API tokens stored as secrets).\n- Change detection must correctly identify transitive dependencies — if `orpheus-common` changes, all agents' test suites run.\n- Release jobs must be idempotent — re-running a failed release job must not double-publish.\n- Must not run full-repo test suites on every PR if only one component changed (cost constraint: only run what changed).\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Automated Release Workflows\n\n Scenario: PR affecting one agent only runs that agent's tests\n Given a PR modifies files only in agents/orpheus-agent-bird-detection/\n When the CI pipeline runs\n Then only the orpheus-agent-bird-detection test suite executes\n And orpheus-common and other agents are not re-tested\n\n Scenario: PR affecting orpheus-common runs all downstream tests\n Given a PR modifies files in platform/orpheus-common/\n When the CI pipeline runs\n Then orpheus-common tests execute first\n And all 11 agent test suites execute as downstream dependents\n\n Scenario: Version tag triggers PyPI publish for that component\n Given a tag \"orpheus-agent-bird-detection/v0.5.0\" is pushed to main\n When the release workflow runs\n Then only orpheus-agent-bird-detection is built and published to PyPI\n And other components are not touched\n\n Scenario: OIDC trusted publishing used for PyPI upload\n Given the release workflow is triggered by a version tag\n When the PyPI upload step runs\n Then no API token secret is used — OIDC authentication is used\n And the upload is auditable via PyPI's trusted publisher log\n```\n\n### Definition of Done\n\n- [ ] `.github/workflows/ci.yml` updated with matrix-based per-component test jobs driven by `changed-components.sh`.\n- [ ] `.github/workflows/release.yml` created: detects tagged component, builds wheel, publishes to PyPI via OIDC.\n- [ ] `scripts/changed-components.sh` script: maps `git diff` output to component paths with correct transitive expansion for `orpheus-common` changes.\n- [ ] OIDC trusted publishing configured in PyPI project settings for each package.\n- [ ] Runbook in `docs/RELEASING.md` documenting the end-to-end release process and how to cut a hotfix.",
"labels": ["type: infrastructure", "component: infra", "component: devops", "C4: Container"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility",
"depends_on": [
"[INFRA] Independent Component Versioning System",
"[INFRA] PyPI Publishing Pipeline (Common + Agents)"
]
},
{
"title": "[INFRA] Contributor Guardrail Automation",
"body": "### Context\n\nOpen source repositories require specific GitHub settings — Branch Protection on `main`, required code reviews, community health files (CODE_OF_CONDUCT, CONTRIBUTING, SECURITY) — that are easy to misconfigure and hard to audit manually. We need a script or workflow that verifies these guardrails are in place and alerts maintainers when they drift from the expected state.\n\nThis automates the 'repository hygiene' checklist that every new maintainer has to verify manually.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- Script: `scripts/verify-repo-guardrails.sh` that uses the GitHub CLI (`gh`) to check all guardrail conditions.\n- GitHub Actions workflow: `.github/workflows/guardrail-audit.yml` running on a weekly cron schedule.\n- Exit codes: `0` = all guardrails passing; `1` = one or more guardrails failing (CI-safe).\n- Report output: human-readable checklist printed to stdout with `[PASS]`/`[FAIL]` per check.\n\n**Guardrails to Verify:**\n1. Branch Protection on `main`: enabled, requires PR before merging.\n2. Required approvals: at least 1 approval required on all PRs to `main`.\n3. Status checks required: CI must pass before merge.\n4. `CODE_OF_CONDUCT.md` present at repository root or `.github/`.\n5. `CONTRIBUTING.md` present at repository root.\n6. `SECURITY.md` present at repository root or `.github/`.\n7. Issue templates: at least one issue template in `.github/ISSUE_TEMPLATE/`.\n8. PR template: `.github/pull_request_template.md` exists.\n\n**Implementation (Internal Logic):**\n- Branch protection checks: `gh api repos/{owner}/{repo}/branches/main/protection` and parse JSON response.\n- Community health check: `gh api repos/{owner}/{repo}/community/profile` and verify `files` object.\n- Cron schedule: run weekly on Monday 09:00 UTC; post a GitHub Issue if any check fails.\n- `make verify-guardrails` target at the repo root for local maintainer use.\n\n### Architectural Constraints\n\n- Must use the GitHub CLI (`gh`) — no third-party Python libraries for GitHub API calls.\n- Must not require admin tokens in CI — use OIDC or the default `GITHUB_TOKEN` with repository scope.\n- The script must be runnable locally by any maintainer with `gh auth login`.\n- All checks must be idempotent — running the script twice has no side effects.\n- Must work with GNU bash 3.2+ (macOS ships an old version).\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Contributor Guardrail Automation\n\n Scenario: All guardrails passing\n Given all branch protection rules and community files are in place\n When verify-repo-guardrails.sh runs\n Then all checks show [PASS]\n And the script exits with code 0\n\n Scenario: Branch protection disabled — alert raised\n Given branch protection on main is disabled\n When verify-repo-guardrails.sh runs\n Then the branch protection check shows [FAIL]\n And the script exits with code 1\n\n Scenario: Weekly cron creates GitHub Issue on failure\n Given the guardrail audit workflow runs on its weekly schedule\n And one or more checks fail\n When the workflow completes\n Then a GitHub Issue is created titled \"[Guardrail Audit] <date> — N checks failing\"\n And the issue body lists the failing checks with remediation steps\n\n Scenario: Required approvals check passes\n Given branch protection requires at least 1 approval\n When verify-repo-guardrails.sh checks the approval requirement\n Then the required-approvals check shows [PASS]\n```\n\n### Definition of Done\n\n- [ ] `scripts/verify-repo-guardrails.sh` covering all 8 guardrail checks listed above.\n- [ ] `.github/workflows/guardrail-audit.yml` running weekly cron with GitHub Issue creation on failure.\n- [ ] `make verify-guardrails` target added to the root Makefile.\n- [ ] All community health files verified present and linked in the GitHub repository settings.\n- [ ] Documentation in `CONTRIBUTING.md` explaining the guardrail checks and how to fix failures.",
"labels": ["type: infrastructure", "component: devops", "C4: Container", "good first issue"],
"milestone": "Epic 6: Infrastructure & Extensibility",
"parent_epic": "Epic 6: Infrastructure & Extensibility"
},
{
"title": "[DEVOPS] Docker Compose \"Simulacrum\" Dev Environment (#152)",
"body": "### Context\n\nNot everyone has a Jetson. We need a `docker-compose.dev.yml` that spins up the entire Orpheus stack — Mosquitto broker, SQLite database, FastAPI backend, React UI, and mock agents that replay pre-recorded audio/video events from `artifacts/audio-samples/`.\n\nA new contributor should go from `git clone` to a working, interactive system in under 5 minutes.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `docker-compose.dev.yml` at the repo root defining all services.\n- Makefile targets: `make dev-up`, `make dev-down`, `make dev-logs`, `make dev-status`.\n- Mock agent configuration: `docker/mock-agents/config.yaml` specifying replay files and intervals.\n- Service naming convention: `orpheus-<component>` (e.g., `orpheus-broker`, `orpheus-api`, `orpheus-ui`).\n\n**Implementation (Internal Logic):**\n- Multi-stage Dockerfiles for Python agents (builder + runtime).\n- Mock agent: Python script replaying audio samples from mounted `artifacts/` volume.\n- Hot-reload: local `src/` directories mounted as volumes for FastAPI (uvicorn --reload) and React (Vite HMR).\n- Health checks: each service has a Docker HEALTHCHECK with appropriate interval.\n\n### Architectural Constraints\n\n- Must run seamlessly on macOS (ARM and x86) and Linux (ARM and x86).\n- Must mount local directories for hot-reloading (UI and backend code changes reflected immediately).\n- Mock agents must be clearly labeled in the UI so users don't confuse simulated data with real detections.\n- Must not require any API keys, cloud services, or external dependencies.\n- Docker images must be multi-arch (buildx with `linux/amd64,linux/arm64`).\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Docker Compose Simulacrum Dev Environment\n\n Scenario: New contributor quick start\n Given a fresh clone of the Orpheus repository\n When the contributor runs \"make dev-up\"\n Then all services start within 5 minutes\n And the React UI is accessible at http://localhost:3000\n And mock detection events appear in the dashboard\n\n Scenario: Hot reload backend code\n Given the dev environment is running\n When a developer modifies a FastAPI endpoint\n Then the change is reflected without restarting containers\n\n Scenario: Mock agents clearly labeled\n Given mock agents are replaying sample data\n When viewing detections in the dashboard\n Then each mock detection is tagged with source \"simulacrum\"\n```\n\n### Definition of Done\n\n- [ ] `docker-compose.dev.yml` added to the repository root.\n- [ ] Mock agent script(s) that replay audio samples and generate synthetic video events.\n- [ ] `make dev-up` and `make dev-down` targets in the root Makefile.\n- [ ] Documented in `CONTRIBUTING.md` with a \"Quick Start\" section.",
"labels": ["type: infrastructure", "component: devops", "C4: Container", "good first issue", "help wanted"],
"milestone": "Epic 7: Containerization & Simulation",
"parent_epic": "Epic 7: Containerization & Simulation"
},
{
"title": "[FEATURE] Auto-Generate Deployment Manifests from orpheus.yaml",
"body": "### Context\n\nWhen a new agent is added to `orpheus.yaml`, someone currently has to manually update Docker Compose files and/or systemd service files. Automate this: parse `orpheus.yaml` and auto-generate deployment manifests (Docker Compose, K8s/K3s, or systemd units) so the deployment topology stays in sync with the configuration.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- CLI: `orpheus-manifest-gen --config <path> --target <docker-compose|systemd|k3s> --output <dir>`.\n- `ManifestGenerator` ABC with methods: `generate(config: OrpheusConfig) -> str`, `validate(manifest: str) -> List[ValidationError]`.\n- `DockerComposeGenerator(ManifestGenerator)` and `SystemdGenerator(ManifestGenerator)` concrete implementations.\n- Output format: human-readable YAML (Docker Compose) or INI (systemd units).\n\n**Implementation (Internal Logic):**\n- Config parser reads `orpheus.yaml` agent list with per-agent settings (image, resources, restart policy).\n- Jinja2 templates for each target format, populated from parsed config.\n- Validation: Docker Compose schema validation via `docker compose config`, systemd via `systemd-analyze verify`.\n\n### Architectural Constraints\n\n- `orpheus.yaml` is the single source of truth for which agents/services are active.\n- Generated manifests must be human-readable (not minified) for debugging.\n- Must support at least two targets: Docker Compose (dev) and systemd (production Jetson).\n- Generation must be idempotent — running it twice produces the same output.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Auto-Generate Deployment Manifests\n\n Scenario: Generate Docker Compose from config\n Given orpheus.yaml defines 5 active agents\n When orpheus-manifest-gen runs with --target docker-compose\n Then a docker-compose.generated.yml is created with 5 service definitions\n And running it again produces byte-identical output\n\n Scenario: Generate systemd units from config\n Given orpheus.yaml defines 5 active agents\n When orpheus-manifest-gen runs with --target systemd\n Then 5 .service files are created in the output directory\n And each service file has Restart=always and appropriate dependencies\n\n Scenario: Validate generated manifests\n Given a generated Docker Compose file\n When validation is run\n Then no schema errors are reported\n```\n\n### Definition of Done\n\n- [ ] CLI tool (`orpheus-manifest-gen`) that reads `orpheus.yaml` and outputs deployment files.\n- [ ] Docker Compose and systemd generators implemented.\n- [ ] Integration test that parses a sample config and validates generated output.\n- [ ] `make generate-manifests` target in the root Makefile.",
"labels": ["type: feature", "component: infra", "component: devops", "C4: Container"],
"milestone": "Epic 7: Containerization & Simulation",
"parent_epic": "Epic 7: Containerization & Simulation"
},
{
"title": "[FEATURE] Database-Backed Configuration System with Versioning",
"body": "### Context\n\nOrpheus currently lives in a single `orpheus.yaml` file. That works for one station, but it doesn't support per-environment overrides, runtime tuning without restarts, or configuration versioning. Implement a layered configuration system:\n\n1. **Base:** `orpheus.yaml` (checked into git)\n2. **Overrides:** Database-stored settings (runtime-tunable)\n3. **Final:** Environment variables (deployment-specific, highest priority)\n\nThis enables a future multi-station deployment where each station has a base config with local overrides.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `ConfigStore` class with methods:\n - `get(key: str, default: Any = None) -> Any` — layered lookup (env → DB → YAML).\n - `set(key: str, value: Any, author: str = \"system\") -> ConfigVersion` — write to DB layer with version tracking.\n - `history(key: str, limit: int = 10) -> List[ConfigVersion]` — audit trail.\n - `subscribe(key_pattern: str, callback: Callable) -> None` — hot-reload notification.\n- `ConfigVersion` dataclass: `{\"key\": str, \"value\": Any, \"author\": str, \"changed_at\": str, \"version\": int}`.\n- Backward compatibility: `OrpheusConfig.get_instance()` delegates to `ConfigStore` internally.\n\n**Implementation (Internal Logic):**\n- Layered resolver: check env vars first, then SQLite `config_overrides` table, then YAML file.\n- SQLite schema: `config_versions` table with `key`, `value_json`, `author`, `changed_at`, `version` (auto-increment per key).\n- MQTT publication to `orpheus/system/config_changed` with key and new value on every `set()` call.\n\n### Architectural Constraints\n\n- Must be backward-compatible with existing `OrpheusConfig.get_instance()` usage.\n- Database schema must version every configuration change (who changed what, when).\n- Must not require a migration for existing deployments — if no DB config exists, fall back to YAML.\n- Configuration changes must be publishable to MQTT so agents can hot-reload without restart.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Database-Backed Configuration System\n\n Scenario: Layered config lookup (env > DB > YAML)\n Given orpheus.yaml sets \"audio.volume\" to 0.5\n And the DB override sets \"audio.volume\" to 0.7\n When config.get(\"audio.volume\") is called\n Then the returned value is 0.7\n\n Scenario: Environment variable takes highest priority\n Given orpheus.yaml sets \"audio.volume\" to 0.5\n And the DB override sets \"audio.volume\" to 0.7\n And ORPHEUS_AUDIO_VOLUME env var is set to 0.3\n When config.get(\"audio.volume\") is called\n Then the returned value is 0.3\n\n Scenario: Config change triggers MQTT notification\n Given an agent subscribes to \"orpheus/system/config_changed\"\n When config.set(\"audio.volume\", 0.8, author=\"admin\") is called\n Then the agent receives an MQTT message with key \"audio.volume\" and value 0.8\n And a new version record is created in the config_versions table\n\n Scenario: Backward compatibility with existing code\n Given existing code calls OrpheusConfig.get_instance()\n When the ConfigStore is initialized\n Then OrpheusConfig.get_instance() still works identically\n And no code changes are required in existing agents\n```\n\n### Definition of Done\n\n- [ ] `ConfigStore` class in `orpheus-common` implementing the layered lookup (YAML → DB → env).\n- [ ] SQLite schema for versioned configuration storage.\n- [ ] MQTT notification on config change so agents can subscribe and reload.\n- [ ] Migration guide documenting upgrade path for existing deployments.",
"labels": ["type: feature", "component: common", "domain: config", "C4: Component"],
"milestone": "Epic 8: Configuration Management",
"parent_epic": "Epic 8: Configuration Management"
},
{
"title": "[HARDWARE] Dynamic Thermal Throttling & Load Shedding",
"body": "### Context\n\nThe Floating Tank is a sealed enclosure in a Michigan wetland. In summer, internal temperatures can exceed safe operating limits for the Jetson. We need autonomous thermal management: monitor `jtop` thermal stats, and when thresholds are crossed, systematically shed load — kill video processing first (the heaviest workload), drop to audio-only mode, and log the thermal event.\n\nThe system should manage its own thermals without human intervention.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `ThermalManager` class with methods: `get_temperature() -> ThermalReading`, `get_shedding_state() -> SheddingLevel`, `check_and_shed() -> Optional[SheddingAction]`.\n- `ThermalReading` dataclass: `{\"cpu_temp_c\": float, \"gpu_temp_c\": float, \"timestamp\": str}`.\n- `SheddingLevel` enum: `NORMAL`, `VIDEO_SHED`, `NON_ESSENTIAL_SHED`, `EMERGENCY_SHUTDOWN`.\n- Configuration: `thermal.shed_video_at_c`, `thermal.shed_nonessential_at_c`, `thermal.emergency_shutdown_at_c`.\n- MQTT event: `orpheus/system/health` with thermal status and shedding actions.\n\n**Implementation (Internal Logic):**\n- `jtop` integration via the `jtop` Python library (or subprocess fallback).\n- Systemd integration: `systemctl stop/start` for managed agent services.\n- Hysteresis: resume services only after temperature drops 5°C below the shedding threshold.\n- Graceful fallback: if `jtop` unavailable (Docker dev), log warning and assume NORMAL.\n\n### Architectural Constraints\n\n- Must integrate with systemd to pause/resume agent services (not SIGKILL — graceful stop).\n- Must log thermal shedding events to the `orpheus/system/health` MQTT topic.\n- Thresholds must be configurable via `orpheus.yaml` (default: shed video at 85°C CPU, emergency shutdown at 95°C).\n- Must handle `jtop` being unavailable gracefully (e.g., in Docker dev environment).\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Dynamic Thermal Throttling\n\n Scenario: Shed video processing at threshold\n Given the CPU temperature reaches 85°C\n When the thermal manager runs its check cycle\n Then orpheus-agent-video-motion is stopped via systemd\n And an MQTT event is published with shedding_level \"VIDEO_SHED\"\n\n Scenario: Resume services after cooldown\n Given video processing was shed at 85°C\n When the CPU temperature drops to 79°C (below 85 - 5 hysteresis)\n Then orpheus-agent-video-motion is restarted via systemd\n And an MQTT event is published with shedding_level \"NORMAL\"\n\n Scenario: Emergency shutdown at critical temperature\n Given the CPU temperature reaches 95°C\n When the thermal manager runs its check cycle\n Then all non-essential services are stopped\n And an MQTT event is published with shedding_level \"EMERGENCY_SHUTDOWN\"\n\n Scenario: Graceful fallback without jtop\n Given jtop is not available (Docker dev environment)\n When the thermal manager initializes\n Then a warning is logged\n And the manager assumes NORMAL shedding level\n```\n\n### Definition of Done\n\n- [ ] `orpheus-thermal-manager` service/agent created with `jtop` monitoring.\n- [ ] Graduated shedding: video → non-essential agents → emergency shutdown.\n- [ ] Successfully stops and restarts `orpheus-agent-video-motion` based on CPU temp thresholds.\n- [ ] Unit tests with mocked `jtop` data simulating thermal ramp-up scenarios.",
"labels": ["type: feature", "component: infra", "domain: hardware", "C4: Container", "help wanted"],
"milestone": "Epic 9: Edge Hardware Realities",
"parent_epic": "Epic 9: Edge Hardware Realities"
},
{
"title": "[MLOPS] Automated Acoustic Regression Testing (Golden Dataset)",
"body": "### Context\n\nWhen we update BirdNET or AVES models, how do we know accuracy hasn't regressed? Build a CI pipeline step that tests new models against a \"Golden Dataset\" — a curated set of audio recordings with known-correct species labels. If accuracy drops below a threshold, the pipeline blocks the merge.\n\nNo regressions in production. Ever.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- CLI: `orpheus-model-eval --model <path> --dataset <path> --threshold <float> --output <report_path>`.\n- `ModelEvaluator` class with methods: `evaluate(model_path: str, dataset_path: str) -> EvaluationReport`.\n- `EvaluationReport` dataclass: `{\"overall_accuracy\": float, \"per_species\": Dict[str, SpeciesMetrics], \"passed\": bool, \"threshold\": float}`.\n- `SpeciesMetrics` dataclass: `{\"precision\": float, \"recall\": float, \"f1\": float, \"sample_count\": int}`.\n- CI output: Markdown report posted as PR comment.\n\n**Implementation (Internal Logic):**\n- Golden Dataset stored in Git LFS or DVC with manifest file listing audio paths and ground-truth labels.\n- Inference runner: load model, process each audio file, collect predictions.\n- Metric calculator: per-species precision/recall/F1 plus overall accuracy.\n- Regression gate: fail if any species F1 drops more than threshold (default 5%) vs. baseline.\n\n### Architectural Constraints\n\n- Golden Dataset must be stored in a reproducible location (Git LFS, S3, or DVC — not checked into the repo directly).\n- Evaluation metrics: precision, recall, F1 per species, plus overall accuracy.\n- Threshold for blocking must be configurable (default: no species drops more than 5% F1).\n- Pipeline must produce a human-readable report (Markdown or HTML) attached to the PR.\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Automated Acoustic Regression Testing\n\n Scenario: Model passes regression check\n Given a Golden Dataset with 50 labeled recordings\n And the baseline model achieves 0.85 F1 for American Crow\n When the new model achieves 0.87 F1 for American Crow\n Then the regression check passes\n And the PR report shows improvement for American Crow\n\n Scenario: Model fails regression check\n Given a Golden Dataset with 50 labeled recordings\n And the baseline model achieves 0.85 F1 for American Crow\n When the new model achieves 0.78 F1 for American Crow (> 5% drop)\n Then the regression check fails\n And the CI pipeline blocks the merge\n And the PR report highlights the regression\n\n Scenario: Generate human-readable report\n Given a completed model evaluation\n When the report is generated\n Then it contains per-species precision, recall, and F1 scores\n And it clearly marks regressions vs. improvements\n```\n\n### Definition of Done\n\n- [ ] Golden Dataset curated with at least 50 labeled recordings covering the top 10 detected species.\n- [ ] Evaluation script that runs inference and computes per-species metrics.\n- [ ] CI pipeline step that runs on model-update PRs and blocks on regression.\n- [ ] Report template showing before/after metrics in the PR comment.",
"labels": ["type: feature", "type: infrastructure", "domain: mlops", "domain: hardware", "C4: Component"],
"milestone": "Epic 9: Edge Hardware Realities",
"parent_epic": "Epic 9: Edge Hardware Realities"
},
{
"title": "[FEATURE] Audio Events Agent (PANNs AudioSet Classifier)",
"body": "### Context\n\nOrpheus classifies audio two ways today: BirdNET (species) and crow-tools (call type / age / intent for corvids). Everything else in the soundscape — dogs, humans, sirens, vehicles, power tools, footsteps — is invisible except for a handful of labels BirdNET happens to include and that the UI filters out via the hardcoded `NON_BIRD_SOUNDS` set in `services/orpheus_ui/backend/src/orpheus_ui/api/diagnostics.py`.\n\nThis issue adds a parallel audio-event classifier agent that runs on the same raw audio clips as BirdNET, producing AudioSet-style labels (527 classes). Results land in SQLite and flow through the event correlator. A new \"Audio Events\" page in the dashboard visualises them, reusing the filter / pagination / chart patterns already built for Birds and Crows.\n\n**Outcome:** a real noise channel answering \"what else is going on in the soundscape?\"\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- New agent `agents/orpheus-agent-audio-events/` cloned from the `orpheus-agent-crow-detection` template.\n- MQTT subscribe: `orpheus/audio/motion/events` (same raw-clip topic BirdNET consumes — runs in parallel, not downstream).\n- MQTT publish: `orpheus/detection/audio/events`.\n- One `Detection(detection_type=\"audio.classified\")` published per AudioSet label above confidence threshold. Top label populated as `species_common` / `species_code=\"audioset_<m_id>\"` so the existing Entity correlator keys work unchanged.\n- New backend endpoint `GET /api/data/audio-events/history` mirroring the bird-history contract (date range, time-of-day window, labels CSV, page / page_size).\n- New frontend page at `/audio-events` reusing `SpeciesFilter` (relabelled \"Labels\"), `DateRangeFilter`, and the existing chart components.\n\n**Implementation (Internal Logic):**\n- Model: PANNs MobileNetV2 (~25 MB, PyTorch native, 527 AudioSet classes). Fits the existing PyTorch stack without adding TensorFlow. `panns_inference` PyPI package drives inference.\n- Model weights distributed via Git LFS in `artifacts/models/panns_mobilenetv2.pth` (matches the pattern for AVES / mt_70.pt / birdnet.onnx). `make download-models` provides an HF fallback.\n- Confidence threshold ≥ 0.5 before publishing (PANNs emits probabilities over all 527 classes per clip; without a threshold we'd flood the DB).\n- No label blocklist in the agent — all labels above threshold are forwarded. The correlator alias map (see `[CORE] Correlator Alias Map for Cross-Classifier Merging`) is the correct place to de-dupe cross-classifier signals.\n- Entities page description broadens from \"correlated animal detection events\" to \"correlated animal and audio events\". Existing species filter picks up audio labels via the `all_species` response field with no extra wiring.\n\n### Architectural Constraints\n\n- Must run on Jetson Orin NX alongside BirdNET + crow-tools without saturating compute. Profile on Jetson before merge; if PANNs inference exceeds ~500 ms per clip, consider gating on audio-motion peak energy or downsampling input from 48 kHz to 32 kHz (PANNs's native rate).\n- Must not replace the hardcoded `NON_BIRD_SOUNDS` filter in this PR — the Birds page keeps its current filter.\n- PANNs and AudioSet are MIT / CC-BY — safe for open-source. We distribute the trained model, not the dataset.\n- Model weights addition must use Git LFS (already configured for `*.pth`).\n- Per-OS quickstart docs (Mac / Linux / Jetson / Windows) must be updated to mention the new agent in \"What Gets Installed\".\n- Must be Python 3.9 compatible.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Audio Events Agent\n\n Scenario: Classify a dog-barking clip\n Given the audio-events agent is running and subscribed to orpheus/audio/motion/events\n When an audio.motion event is published for a clip containing dog barks\n Then the agent publishes at least one Detection with detection_type=\"audio.classified\" and species_common=\"Dog\"\n And the detection persists to SQLite with the correct timestamp and clip path\n\n Scenario: Confidence threshold drops low-signal labels\n Given PANNs emits probabilities for all 527 classes on a clip\n When the highest probability is 0.3\n Then zero detections are published for that clip\n\n Scenario: Audio Events page renders aggregated data\n Given 100 audio.classified detections spanning the last 7 days\n When a user opens /audio-events with date range = 7 days\n Then the stat cards show the total count and unique-label count\n And the label distribution pie chart renders the top labels\n And the Labels filter dropdown lists every label present in the range\n```\n\n### Definition of Done\n\n- [ ] New `agents/orpheus-agent-audio-events/` created by cloning `orpheus-agent-crow-detection` (MQTT sub/pub, classifier wrapper, pydantic config, systemd unit, pytest fixtures, Makefile).\n- [ ] `panns_inference` wired into the classifier wrapper.\n- [ ] PANNs MobileNetV2 weights committed to `artifacts/models/panns_mobilenetv2.pth` via Git LFS.\n- [ ] Agent unit tests: confidence threshold, handles missing clip gracefully, publishes correct detection_type.\n- [ ] New `/api/data/audio-events/history` endpoint with the pagination + labels-filter contract (mirrors bird-history). Backend tests mirror `TestBirdHistoryEndpoint`.\n- [ ] New `services/orpheus_ui/frontend/src/pages/AudioEvents.tsx` + route + nav entry in Layout. Frontend test covers the page renders and the labels filter populates.\n- [ ] Entities page description updated to \"correlated animal and audio events from multiple sensors\".\n- [ ] Per-OS quickstart docs mention the new agent.\n- [ ] Jetson profile captured in a comment on this issue (inference time per clip) before merge.\n\n### Follow-ups (tracked here as bullets, not separate issues)\n\n- **Jetson performance profile**: if three parallel classifiers saturate compute, gate PANNs on audio-motion peak energy or downsample.\n- **NON_BIRD_SOUNDS deprecation**: once audio-events is producing real labels, retire the hardcoded filter in `diagnostics.py` in favor of filtering by `detection_type` only.",
"labels": ["type: feature", "component: agent", "component: ui-dash", "domain: mlops", "C4: Container"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy"
},
{
"title": "[CORE] Correlator Alias Map for Cross-Classifier Merging",
"body": "### Context\n\nThe event correlator keys clusters on the exact `species_code` string (`cluster_manager.py:134`). Today BirdNET emits `species_code=\"amecro\"` for an American Crow and crow-tools emits `species_code=\"american_crow\"` for the same real-world bird — these already fail to merge into one Entity, producing duplicates. Adding PANNs as a third classifier (see `[FEATURE] Audio Events Agent`) makes the problem impossible to ignore: a crow audio clip would produce one entity per classifier.\n\nThis is a surgical stepping-stone on the way to a proper taxonomic hierarchy (see `[ARCH] Generalize the EntityEvent State Space Taxonomy`, which remains the destination). We ship an alias map that maps known-duplicate species_codes to a canonical code before clustering. Non-aliased codes pass through unchanged, so no existing behavior changes except for the aliases we explicitly list.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- New module `platform/orpheus-common/src/orpheus_common/detection/aliases.py` exporting:\n - `SPECIES_CODE_ALIASES: dict[str, str]` — known-duplicate species_codes → canonical codes.\n - `canonicalize(species_code: str) -> str` — returns the alias if present, otherwise the input unchanged.\n- `TemporalCluster.species_code` becomes the canonical code. The emitted Entity's `species` field is the canonical code.\n- The evidence list on each Entity retains the original (pre-canonicalization) species_code so users can still see what each classifier actually said.\n\n**Implementation (Internal Logic):**\n- Seed alias map with explicit known equivalences across the three current classifiers:\n - `\"american_crow\" → \"crow\"` (crow-tools)\n - `\"amecro\" → \"crow\"` (BirdNET)\n - AudioSet crow/raven-family codes → `\"crow\"` (PANNs)\n - A handful of obvious multi-source duplicates as they surface (e.g., AudioSet dog → `\"dog\"`).\n- In `cluster_manager.py:process_observation`, replace `species = obs.species_code` with `species = canonicalize(obs.species_code)` before cluster lookup.\n- Add `\"audio.classified\"` to `PROCESSED_DETECTION_TYPES` in the correlator's `main.py` so PANNs detections flow through clustering.\n\n### Architectural Constraints\n\n- **Surgical de-duplication, not taxonomic collapse.** Only explicit known-equivalences across classifiers go in the map. We do NOT alias every BirdNET code to a generic category.\n- Must preserve existing behavior for all non-aliased codes (regression safety).\n- Must be backward-compatible with existing Entity consumers — `species` field still a string; callers don't need to know about canonicalization.\n- Must be Python 3.9 compatible.\n- Supersedes the \"small correlator upgrade\" discussion from the birds-page PR; the full taxonomy work remains owned by `[ARCH] Generalize the EntityEvent State Space Taxonomy`.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: Correlator Alias Map\n\n Scenario: Three classifiers agreeing on one crow collapse into one Entity\n Given observations arrive within the correlation window on the same sensor with species_codes \"amecro\", \"american_crow\", and \"audioset_crow\"\n When the correlator processes them\n Then a single Entity is emitted with species=\"crow\"\n And the Entity's evidence list contains all three source event_ids with their original species_codes\n\n Scenario: Unaliased species still produce separate Entities\n Given observations arrive for \"norcar\" (Northern Cardinal) and \"robin\" (American Robin)\n When the correlator processes them\n Then two Entities are emitted, one per species_code (no accidental merging)\n\n Scenario: Empty / unknown species_code does not crash\n Given an observation arrives with species_code=\"\"\n When canonicalize(\"\") is called\n Then it returns \"\" (the input is passed through unchanged)\n```\n\n### Definition of Done\n\n- [ ] `platform/orpheus-common/src/orpheus_common/detection/aliases.py` with `SPECIES_CODE_ALIASES` and `canonicalize()`.\n- [ ] Unit tests: alias hits, alias misses, empty-string handling.\n- [ ] `cluster_manager.py` uses `canonicalize()` before cluster key lookup.\n- [ ] Evidence list retains pre-canonicalization species_codes (unit test).\n- [ ] `audio.classified` added to `PROCESSED_DETECTION_TYPES`.\n- [ ] Regression test: three classifiers → one Entity with merged evidence (Gherkin scenario above).\n- [ ] Regression test: unaliased species_codes still produce separate Entities (Gherkin scenario above).\n\n### Follow-ups (tracked here, not separate issues)\n\n- **Automatic alias discovery**: when two classifiers consistently fire on the same audio clip (same `source_event_id`) with different species_codes, surface those as alias-map candidates for human review. Keeps the map curated without manual maintenance burden.",
"labels": ["type: feature", "component: common", "component: agent", "domain: active-inference", "C4: Component"],
"milestone": "Epic 1: The Cognitive Holarchy",
"parent_epic": "Epic 1: The Cognitive Holarchy"
},
{
"title": "[FEATURE] External Species Links on Birds Page",
"body": "### Context\n\nThe Birds page shows species by common name only. Users want to click through to authoritative info (photos, range maps, sounds) — All About Birds, eBird, Wikipedia. We can link reliably, but only if we persist the scientific name: our current `species_code` is a 6-char slug derived from the scientific name, NOT the eBird code, and common-name-only URLs are ambiguous across regions.\n\nGood news: BirdNET already parses the scientific name from `labels.json` (\"Scientific_Name\") and includes it in its prediction output — it is simply dropped before persistence. Wiring it end-to-end unlocks deterministic linking.\n\n**Outcome:** each detection row on the Birds page gains a small set of external-link icons (All About Birds, eBird, Wikipedia) that resolve deterministically for new rows. Historical rows without a scientific name fall back to common-name-based Wikipedia / All About Birds links where possible.\n\n### Architecturally Significant Requirements (ASRs)\n\n**Interface (Contract):**\n- `Detection` pydantic model (`platform/orpheus-common/src/orpheus_common/detection/models.py`) gains `species_scientific: Optional[str] = None`. `to_dict()` / `from_dict()` round-trip the field.\n- `/api/data/birds/history` response detection objects include `species_scientific` (nullable).\n- New `<SpeciesLinks />` React component rendering up to three compact icon links per detection row.\n\n**Implementation (Internal Logic):**\n- Additive SQLite migration: add `species_scientific TEXT` column via the existing `ensure_schema_updates()` hook (auto-runs on startup). No backfill.\n- BirdNET agent wiring: parser in `birdnet.py` already emits `species_scientific` (around line 160). Verify the agent `main` forwards it into the `Detection(...)` constructor; add a pass-through if missing.\n- Frontend slug helpers: lowercase, replace spaces with `-` (eBird) or `_` (Wikipedia); All About Birds uses its own common-name-based URL format (verify during implementation).\n- Fallback logic when `species_scientific` is null: render All About Birds + Wikipedia search links keyed on common name; omit the eBird link.\n\n### Architectural Constraints\n\n- **No backfill.** Historical rows keep `species_scientific = NULL`. Migration is additive and reversible by dropping the column.\n- Must not break existing `Detection` consumers — new field is optional with default `None`.\n- Must be Python 3.9 compatible.\n- Must not introduce new runtime dependencies on either side.\n- External URL formats should be verified against each site's canonical pattern during implementation; keep them in one place (the `<SpeciesLinks />` component) so they're easy to update if a site's URL scheme changes.\n\n### Acceptance Criteria\n\n```gherkin\nFeature: External Species Links on Birds Page\n\n Scenario: New detection has scientific name and all three links\n Given a BirdNET detection arrives with species_scientific=\"Corvus brachyrhynchos\" and species_common=\"American Crow\"\n When the detection persists and the Birds page row renders\n Then the row shows three icon links: All About Birds, eBird, and Wikipedia\n And clicking Wikipedia opens https://en.wikipedia.org/wiki/Corvus_brachyrhynchos\n\n Scenario: Historical detection without scientific name falls back gracefully\n Given a historical detection row has species_scientific=NULL and species_common=\"American Crow\"\n When the Birds page renders the row\n Then All About Birds and a Wikipedia search link are rendered\n And the eBird link is NOT rendered (it needs the scientific name)\n And the UI does not crash\n\n Scenario: Schema migration on startup is idempotent\n Given an existing SQLite detections database with no species_scientific column\n When the backend starts up\n Then ensure_schema_updates adds the species_scientific TEXT column\n And a second startup does not attempt to re-add the column\n And old rows read back with species_scientific = None\n```\n\n### Definition of Done\n\n- [ ] `Detection` model updated with `species_scientific: Optional[str] = None` and round-trip tests in `orpheus-common`.\n- [ ] SQLite schema migration via `ensure_schema_updates()`; verified idempotent on an existing DB.\n- [ ] BirdNET agent forwards `species_scientific` from the parser to `Detection(...)`. Agent test updated.\n- [ ] `/api/data/birds/history` returns `species_scientific` in the detection dicts. Backend test updated.\n- [ ] `<SpeciesLinks />` React component + integration into `Birds.tsx` detection rows. Frontend test verifying link URLs for the scientific-present and scientific-NULL cases.\n- [ ] End-to-end manual verification: new detection row shows 3 links; historical row shows 2 (fallback).",
"labels": ["type: feature", "component: common", "component: ui-dash", "component: agent", "C4: Component", "good first issue"],
"milestone": "Epic 3: Interspecies Interfaces",
"parent_epic": "Epic 3: Interspecies Interfaces"
}
]
}