fix: resolve merge conflict in config.py with main

Keep both L402/privacy settings from security hardening PR and
self-modification settings. All 939 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Payne
2026-02-25 17:48:28 -05:00
22 changed files with 4635 additions and 667 deletions

View File

@@ -41,6 +41,15 @@
# Lightning backend: "mock" (default) | "lnd"
# LIGHTNING_BACKEND=mock
# ── Environment & Privacy ───────────────────────────────────────────────────
# Environment mode: "development" (default) | "production"
# In production, security secrets MUST be set or the app will refuse to start.
# TIMMY_ENV=development
# Disable Agno telemetry for sovereign/air-gapped deployments.
# Default is false (disabled) to align with local-first AI vision.
# TELEMETRY_ENABLED=false
# ── Telegram bot ──────────────────────────────────────────────────────────────
# Bot token from @BotFather on Telegram.
# Alternatively, configure via the /telegram/setup dashboard endpoint at runtime.

11
.gitignore vendored
View File

@@ -21,6 +21,9 @@ env/
# SQLite memory — never commit agent memory
*.db
# Runtime PID files
.watchdog.pid
# Chat platform state files (contain bot tokens)
telegram_state.json
discord_state.json
@@ -37,5 +40,11 @@ reports/
.vscode/
*.swp
*.swo
.DS_Store
.claude/
# macOS
.DS_Store
.AppleDouble
.LSOverride
.Spotlight-V100
.Trashes

View File

@@ -21,29 +21,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /app
# ── Python deps (install before copying src for layer caching) ───────────────
# Copy only pyproject.toml first so Docker can cache the dep-install layer.
# The editable install (-e) happens after src is copied below.
COPY pyproject.toml .
# Install production deps only (no dev/test extras in the image)
RUN pip install --no-cache-dir \
"fastapi>=0.115.0" \
"uvicorn[standard]>=0.32.0" \
"jinja2>=3.1.0" \
"httpx>=0.27.0" \
"python-multipart>=0.0.12" \
"aiofiles>=24.0.0" \
"typer>=0.12.0" \
"rich>=13.0.0" \
"pydantic-settings>=2.0.0" \
"websockets>=12.0" \
"agno[sqlite]>=1.4.0" \
"ollama>=0.3.0" \
"openai>=1.0.0" \
"python-telegram-bot>=21.0" \
"GitPython>=3.1.40" \
"moviepy>=2.0.0" \
"redis>=5.0.0"
# Create a minimal src layout so `pip install` can resolve the package metadata
# without copying the full source tree (preserves Docker layer caching).
RUN mkdir -p src/timmy src/timmy_serve src/self_tdd src/dashboard && \
touch src/timmy/__init__.py src/timmy/cli.py \
src/timmy_serve/__init__.py src/timmy_serve/cli.py \
src/self_tdd/__init__.py src/self_tdd/watchdog.py \
src/dashboard/__init__.py src/config.py
RUN pip install --no-cache-dir -e ".[swarm,telegram]"
# ── Application source ───────────────────────────────────────────────────────
# Overwrite the stubs with real source code
COPY src/ ./src/
COPY static/ ./static/

View File

@@ -1,4 +1,4 @@
.PHONY: install install-bigbrain dev nuke test test-cov test-cov-html watch lint clean help \
.PHONY: install install-bigbrain install-creative dev nuke test test-cov test-cov-html watch lint clean help \
up down logs \
docker-build docker-up docker-down docker-agent docker-logs docker-shell \
cloud-deploy cloud-up cloud-down cloud-logs cloud-status cloud-update
@@ -25,13 +25,24 @@ install-bigbrain: $(VENV)/bin/activate
echo "✓ AirLLM installed (PyTorch backend)"; \
fi
install-creative: $(VENV)/bin/activate
$(PIP) install --quiet -e ".[dev,creative]"
@if [ "$$(uname -m)" = "arm64" ] && [ "$$(uname -s)" = "Darwin" ]; then \
echo " Apple Silicon detected — installing PyTorch with Metal (MPS) support..."; \
$(PIP) install --quiet --pre torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/nightly/cpu; \
echo "✓ Creative extras installed with Metal GPU acceleration"; \
else \
echo "✓ Creative extras installed (diffusers, torch, ace-step)"; \
fi
$(VENV)/bin/activate:
python3 -m venv $(VENV)
# ── Development ───────────────────────────────────────────────────────────────
dev: nuke
$(UVICORN) dashboard.app:app --reload --host 0.0.0.0 --port 8000
PYTHONDONTWRITEBYTECODE=1 $(UVICORN) dashboard.app:app --reload --host 0.0.0.0 --port 8000
# Kill anything on port 8000, stop Docker containers, clear stale state.
# Safe to run anytime — idempotent, never errors out.
@@ -41,9 +52,12 @@ nuke:
@docker compose down --remove-orphans 2>/dev/null || true
@# Kill any process holding port 8000 (errno 48 fix)
@lsof -ti :8000 | xargs kill -9 2>/dev/null || true
@# Purge stale bytecache to prevent loading old .pyc files
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
@find . -name "*.pyc" -delete 2>/dev/null || true
@# Brief pause to let the OS release the socket
@sleep 0.5
@echo " ✓ Port 8000 free, containers stopped"
@echo " ✓ Port 8000 free, containers stopped, caches cleared"
# Print the local IP addresses your phone can use to reach this machine.
# Connect your phone to the same hotspot your Mac is sharing from,
@@ -54,10 +68,15 @@ ip:
@echo ""
@echo " Open one of these on your phone: http://<IP>:8000"
@echo ""
@ipconfig getifaddr en0 2>/dev/null | awk '{print " en0 (Wi-Fi): http://" $$1 ":8000"}' || true
@ipconfig getifaddr en1 2>/dev/null | awk '{print " en1 (Ethernet): http://" $$1 ":8000"}' || true
@ipconfig getifaddr en2 2>/dev/null | awk '{print " en2: http://" $$1 ":8000"}' || true
@ifconfig 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/ && !/::1/{print " " $$2 " http://" $$2 ":8000"}' | head -5 || true
@if [ "$$(uname -s)" = "Darwin" ]; then \
ipconfig getifaddr en0 2>/dev/null | awk '{print " en0 (Wi-Fi): http://" $$1 ":8000"}' || true; \
ipconfig getifaddr en1 2>/dev/null | awk '{print " en1 (Ethernet): http://" $$1 ":8000"}' || true; \
ipconfig getifaddr en2 2>/dev/null | awk '{print " en2: http://" $$1 ":8000"}' || true; \
fi
@# Generic fallback — works on both macOS and Linux
@ifconfig 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/ && !/::1/{print " " $$2 " → http://" $$2 ":8000"}' | head -5 \
|| ip -4 addr show 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/{split($$2,a,"/"); print " " a[1] " → http://" a[1] ":8000"}' | head -5 \
|| true
@echo ""
watch:
@@ -202,6 +221,7 @@ help:
@echo " ─────────────────────────────────────────────────"
@echo " make install create venv + install dev deps"
@echo " make install-bigbrain install with AirLLM (big-model backend)"
@echo " make install-creative install with creative extras (torch, diffusers)"
@echo " make dev clean up + start dashboard (auto-fixes errno 48)"
@echo " make nuke kill port 8000, stop containers, reset state"
@echo " make ip print local IP addresses for phone testing"

232
QUALITY_REVIEW_REPORT.md Normal file
View File

@@ -0,0 +1,232 @@
# Timmy Time — Comprehensive Quality Review Report
**Date:** 2026-02-25
**Reviewed by:** Claude Code
**Test Coverage:** 84.15% (895 tests passing)
**Test Result:** ✅ 895 passed, 30 skipped
---
## Executive Summary
The Timmy Time application is a **functional local-first AI agent system** with a working FastAPI dashboard, Ollama integration, and sophisticated Spark Intelligence engine. The codebase is well-structured with good test coverage, but **critical bugs were found and fixed** during this review that prevented the agent from working properly.
**Overall Quality Score: 7.5/10**
- Architecture: 8/10
- Functionality: 8/10 (after fixes)
- Test Coverage: 8/10
- Documentation: 7/10
- Memory/Self-Awareness: 9/10
---
## 1. Critical Bugs Found & Fixed
### Bug 1: Toolkit API Mismatch (`CRITICAL`)
**Location:** `src/timmy/tools.py`
**Issue:** Code used non-existent `Toolkit.add_tool()` method (should be `register()`)
**Changes Made:**
- Changed `toolkit.add_tool(...)``toolkit.register(...)` (29 occurrences)
- Changed `python_tools.python``python_tools.run_python_code` (3 occurrences)
- Changed `file_tools.write_file``file_tools.save_file` (4 occurrences)
- Changed `FileTools(base_dir=str(base_path))``FileTools(base_dir=base_path)` (5 occurrences)
**Impact:** Without this fix, Timmy agent would crash on startup with `AttributeError`.
### Bug 2: Agent Tools Parameter (`CRITICAL`)
**Location:** `src/timmy/agent.py`
**Issue:** Tools passed as single Toolkit instead of list
**Change Made:**
- Changed `tools=tools``tools=[tools] if tools else None`
**Impact:** Without this fix, Agno Agent initialization would fail with `TypeError: 'Toolkit' object is not iterable`.
---
## 2. Model Inference — ✅ WORKING
### Test Results
| Test | Status | Details |
|------|--------|---------|
| Agent creation | ✅ Pass | Ollama backend initializes correctly |
| Basic inference | ✅ Pass | Response type: `RunOutput` with content |
| Tool usage | ✅ Pass | File operations, shell commands work |
| Streaming | ✅ Pass | Supported via `stream=True` |
### Inference Example
```
Input: "What is your name and who are you?"
Output: "I am Timmy, a sovereign AI agent running locally on Apple Silicon.
I'm committed to your digital sovereignty and powered by Bitcoin economics..."
```
### Available Models
- **Ollama:** llama3.2 (default), deepseek-r1:1.5b
- **AirLLM:** 8B, 70B, 405B models (optional backend)
---
## 3. Memory & Self-Awareness — ✅ WORKING
### Conversation Memory Test
| Test | Status | Result |
|------|--------|--------|
| Single-turn memory | ✅ Pass | Timmy remembers what user just asked |
| Multi-turn context | ✅ Pass | References earlier conversation |
| Self-identification | ✅ Pass | "I am Timmy, a sovereign AI agent..." |
| Persistent storage | ✅ Pass | SQLite (`timmy.db`) persists across restarts |
| History recall | ✅ Pass | Can recall first question from conversation |
### Memory Implementation
- **Storage:** SQLite via `SqliteDb` (Agno)
- **Context window:** 10 history runs (`num_history_runs=10`)
- **File:** `timmy.db` in project root
### Self-Awareness Features
✅ Agent knows its name ("Timmy")
✅ Agent knows it's a sovereign AI
✅ Agent knows it runs locally (Apple Silicon detection)
✅ Agent references Bitcoin economics and digital sovereignty
✅ Agent references Christian faith grounding (per system prompt)
---
## 4. Spark Intelligence Engine — ✅ WORKING
### Capabilities Verified
| Feature | Status | Details |
|---------|--------|---------|
| Event capture | ✅ Working | 550 events captured |
| Task predictions | ✅ Working | 235 predictions, 85% avg accuracy |
| Memory consolidation | ✅ Working | 6 memories stored |
| Advisories | ✅ Working | Failure prevention, performance, bid optimization |
| EIDOS loop | ✅ Working | Predict → Observe → Evaluate → Learn |
### Sample Advisory Output
```
[failure_prevention] Agent fail-lea has 7 failures (Priority: 1.0)
[agent_performance] Agent success- excels (100% success) (Priority: 0.6)
[bid_optimization] Wide bid spread (2094 sats) (Priority: 0.5)
[system_health] Strong prediction accuracy (85%) (Priority: 0.3)
```
---
## 5. Dashboard & UI — ✅ WORKING
### Route Testing Results
| Route | Status | Notes |
|-------|--------|-------|
| `/` | ✅ 200 | Main dashboard loads |
| `/health` | ✅ 200 | Health panel |
| `/agents` | ✅ 200 | Agent list API |
| `/swarm` | ✅ 200 | Swarm coordinator UI |
| `/spark` | ✅ 200 | Spark Intelligence dashboard |
| `/marketplace` | ✅ 200 | Marketplace UI |
| `/mobile` | ✅ 200 | Mobile-optimized layout |
| `/agents/timmy/chat` | ✅ 200 | Chat endpoint works |
### Chat Functionality
- HTMX-powered chat interface ✅
- Message history persistence ✅
- Real-time Ollama inference ✅
- Error handling (graceful degradation) ✅
---
## 6. Swarm System — ⚠️ PARTIAL
### Working Components
- ✅ Registry with SQLite persistence
- ✅ Coordinator with task lifecycle
- ✅ Agent bidding system
- ✅ Task assignment algorithm
- ✅ Spark event capture
- ✅ Recovery mechanism
### Limitations
- ⚠️ Persona agents are stubbed (not fully functional AI agents)
- ⚠️ Most swarm activity is simulated/test data
- ⚠️ Docker runner not tested in live environment
---
## 7. Issues Identified (Non-Critical)
### Issue 1: SSL Certificate Error with DuckDuckGo
**Location:** Web search tool
**Error:** `CERTIFICATE_VERIFY_FAILED`
**Impact:** Web search tool fails, but agent continues gracefully
**Fix:** May need `certifi` package or system certificate update
### Issue 2: Default Secrets Warning
**Location:** L402 payment handler
**Message:** `L402_HMAC_SECRET is using the default value`
**Impact:** Warning only — production should set unique secrets
**Status:** By design (warns at startup)
### Issue 3: Redis Unavailable Fallback
**Location:** SwarmComms
**Message:** `Redis unavailable — using in-memory fallback`
**Impact:** Falls back to in-memory (acceptable for single-instance)
**Status:** By design (graceful degradation)
### Issue 4: Telemetry to Agno
**Observation:** Agno sends telemetry to `os-api.agno.com`
**Impact:** Minor — may not align with "sovereign" vision
**Note:** Requires further review for truly air-gapped deployments
---
## 8. Test Coverage Analysis
| Module | Coverage | Status |
|--------|----------|--------|
| `spark/memory.py` | 98.3% | ✅ Excellent |
| `spark/engine.py` | 92.6% | ✅ Good |
| `swarm/coordinator.py` | 92.8% | ✅ Good |
| `timmy/agent.py` | 100% | ✅ Excellent |
| `timmy/backends.py` | 96.3% | ✅ Good |
| `dashboard/` routes | 60-100% | ✅ Good |
**Overall:** 84.15% coverage (exceeds 60% threshold)
---
## 9. Recommendations
### High Priority
1.**DONE** Fix toolkit API methods (register vs add_tool)
2.**DONE** Fix agent tools parameter (wrap in list)
3. Add tool usage instructions to system prompt to reduce unnecessary tool calls
4. Fix SSL certificate issue for DuckDuckGo search
### Medium Priority
5. Add configuration option to disable Agno telemetry
6. Implement more sophisticated self-awareness (e.g., knowledge of current tasks)
7. Expand persona agent capabilities beyond stubs
### Low Priority
8. Add more comprehensive end-to-end tests with real Ollama
9. Optimize tool calling behavior (fewer unnecessary tool invocations)
10. Consider adding conversation summarization for very long contexts
---
## 10. Conclusion
After fixing the critical bugs identified during this review, **Timmy Time is a functional and well-architected AI agent system** with:
- ✅ Working model inference via Ollama
- ✅ Persistent conversation memory
- ✅ Self-awareness capabilities
- ✅ Comprehensive Spark Intelligence engine
- ✅ Functional web dashboard
- ✅ Good test coverage (84%+)
The core value proposition — a sovereign, local-first AI agent with memory and self-awareness — **is delivered and working**.

147
WORKSET_PLAN.md Normal file
View File

@@ -0,0 +1,147 @@
# Timmy Time — Workset Plan (Post-Quality Review)
**Date:** 2026-02-25
**Based on:** QUALITY_ANALYSIS.md + QUALITY_REVIEW_REPORT.md
---
## Executive Summary
This workset addresses critical security vulnerabilities, hardens the tool system for reliability, improves privacy alignment with the "sovereign AI" vision, and enhances agent intelligence.
---
## Workset A: Security Fixes (P0) 🔒
### A1: XSS Vulnerabilities (SEC-01)
**Priority:** P0 — Critical
**Files:** `mobile.html`, `swarm_live.html`
**Issues:**
- `mobile.html` line ~85 uses raw `innerHTML` with unsanitized user input
- `swarm_live.html` line ~72 uses `innerHTML` with WebSocket agent data
**Fix:** Replace `innerHTML` string interpolation with safe DOM methods (`textContent`, `createTextNode`, or DOMPurify if available).
### A2: Hardcoded Secrets (SEC-02)
**Priority:** P1 — High
**Files:** `l402_proxy.py`, `payment_handler.py`
**Issue:** Default secrets are production-safe strings instead of `None` with startup assertion.
**Fix:**
- Change defaults to `None`
- Add startup assertion requiring env vars to be set
- Fail fast with clear error message
---
## Workset B: Tool System Hardening ⚙️
### B1: SSL Certificate Fix
**Priority:** P1 — High
**File:** Web search via DuckDuckGo
**Issue:** `CERTIFICATE_VERIFY_FAILED` errors prevent web search from working.
**Fix Options:**
- Option 1: Use `certifi` package for proper certificate bundle
- Option 2: Add `verify_ssl=False` parameter (less secure, acceptable for local)
- Option 3: Document SSL fix in troubleshooting
### B2: Tool Usage Instructions
**Priority:** P2 — Medium
**File:** `prompts.py`
**Issue:** Agent makes unnecessary tool calls for simple questions.
**Fix:** Add tool usage instructions to system prompt:
- Only use tools when explicitly needed
- For simple chat/questions, respond directly
- Tools are for: web search, file operations, code execution
### B3: Tool Error Handling
**Priority:** P2 — Medium
**File:** `tools.py`
**Issue:** Tool failures show stack traces to user.
**Fix:** Add graceful error handling with user-friendly messages.
---
## Workset C: Privacy & Sovereignty 🛡️
### C1: Agno Telemetry (Privacy)
**Priority:** P2 — Medium
**File:** `agent.py`, `backends.py`
**Issue:** Agno sends telemetry to `os-api.agno.com` which conflicts with "sovereign" vision.
**Fix:**
- Add `telemetry_enabled=False` parameter to Agent
- Document how to disable for air-gapped deployments
- Consider environment variable `TIMMY_TELEMETRY=0`
### C2: Secrets Validation
**Priority:** P1 — High
**File:** `config.py`, startup
**Issue:** Default secrets used without warning in production.
**Fix:**
- Add production mode detection
- Fatal error if default secrets in production
- Clear documentation on generating secrets
---
## Workset D: Agent Intelligence 🧠
### D1: Enhanced System Prompt
**Priority:** P2 — Medium
**File:** `prompts.py`
**Enhancements:**
- Tool usage guidelines (when to use, when not to)
- Memory awareness ("You remember previous conversations")
- Self-knowledge (capabilities, limitations)
- Response style guidelines
### D2: Memory Improvements
**Priority:** P2 — Medium
**File:** `agent.py`
**Enhancements:**
- Increase history runs from 10 to 20 for better context
- Add memory summarization for very long conversations
- Persistent session tracking
---
## Execution Order
| Order | Workset | Task | Est. Time |
|-------|---------|------|-----------|
| 1 | A | XSS fixes | 30 min |
| 2 | A | Secrets hardening | 20 min |
| 3 | B | SSL certificate fix | 15 min |
| 4 | B | Tool instructions | 20 min |
| 5 | C | Telemetry disable | 15 min |
| 6 | C | Secrets validation | 20 min |
| 7 | D | Enhanced prompts | 30 min |
| 8 | — | Test everything | 30 min |
**Total: ~3 hours**
---
## Success Criteria
- [ ] No XSS vulnerabilities (verified by code review)
- [ ] Secrets fail fast in production
- [ ] Web search works without SSL errors
- [ ] Agent uses tools appropriately (not for simple chat)
- [ ] Telemetry disabled by default
- [ ] All 895+ tests pass
- [ ] New tests added for security fixes

133
WORKSET_PLAN_PHASE2.md Normal file
View File

@@ -0,0 +1,133 @@
# Timmy Time — Workset Plan Phase 2 (Functional Hardening)
**Date:** 2026-02-25
**Based on:** QUALITY_ANALYSIS.md remaining issues
---
## Executive Summary
This workset addresses the core functional gaps that prevent the swarm system from operating as designed. The swarm currently registers agents in the database but doesn't actually spawn processes or execute bids. This workset makes the swarm operational.
---
## Workset E: Swarm System Realization 🐝
### E1: Real Agent Process Spawning (FUNC-01)
**Priority:** P1 — High
**Files:** `swarm/agent_runner.py`, `swarm/coordinator.py`
**Issue:** `spawn_agent()` creates a database record but no Python process is actually launched.
**Fix:**
- Complete the `agent_runner.py` subprocess implementation
- Ensure spawned agents can communicate with coordinator
- Add proper lifecycle management (start, monitor, stop)
### E2: Working Auction System (FUNC-02)
**Priority:** P1 — High
**Files:** `swarm/bidder.py`, `swarm/persona_node.py`
**Issue:** Bidding system runs auctions but no actual agents submit bids.
**Fix:**
- Connect persona agents to the bidding system
- Implement automatic bid generation based on capabilities
- Ensure auction resolution assigns tasks to winners
### E3: Persona Agent Auto-Bidding
**Priority:** P1 — High
**Files:** `swarm/persona_node.py`, `swarm/coordinator.py`
**Fix:**
- Spawned persona agents should automatically bid on matching tasks
- Implement capability-based bid decisions
- Add bid amount calculation (base + jitter)
---
## Workset F: Testing & Reliability 🧪
### F1: WebSocket Reconnection Tests (TEST-01)
**Priority:** P2 — Medium
**Files:** `tests/test_websocket.py`
**Issue:** WebSocket tests don't cover reconnection logic or malformed payloads.
**Fix:**
- Add reconnection scenario tests
- Test malformed payload handling
- Test connection failure recovery
### F2: Voice TTS Graceful Degradation
**Priority:** P2 — Medium
**Files:** `timmy_serve/voice_tts.py`, `dashboard/routes/voice.py`
**Issue:** Voice routes fail without clear message when `pyttsx3` not installed.
**Fix:**
- Add graceful fallback message
- Return helpful error suggesting `pip install ".[voice]"`
- Don't crash, return 503 with instructions
### F3: Mobile Route Navigation
**Priority:** P2 — Medium
**Files:** `templates/base.html`
**Issue:** `/mobile` route not linked from desktop navigation.
**Fix:**
- Add mobile link to base template nav
- Make it easy to find mobile-optimized view
---
## Workset G: Performance & Architecture ⚡
### G1: SQLite Connection Pooling (PERF-01)
**Priority:** P3 — Low
**Files:** `swarm/registry.py`
**Issue:** New SQLite connection opened on every query.
**Fix:**
- Implement connection pooling or singleton pattern
- Reduce connection overhead
- Maintain thread safety
### G2: Development Experience
**Priority:** P2 — Medium
**Files:** `Makefile`, `README.md`
**Issue:** No single command to start full dev environment.
**Fix:**
- Add `make dev-full` that starts dashboard + Ollama check
- Add better startup validation
---
## Execution Order
| Order | Workset | Task | Est. Time |
|-------|---------|------|-----------|
| 1 | E | Persona auto-bidding system | 45 min |
| 2 | E | Fix auction resolution | 30 min |
| 3 | F | Voice graceful degradation | 20 min |
| 4 | F | Mobile nav link | 10 min |
| 5 | G | SQLite connection pooling | 30 min |
| 6 | — | Test everything | 30 min |
**Total: ~2.5 hours**
---
## Success Criteria
- [ ] Persona agents automatically bid on matching tasks
- [ ] Auctions resolve with actual winners
- [ ] Voice routes degrade gracefully without pyttsx3
- [ ] Mobile route accessible from desktop nav
- [ ] SQLite connections pooled/reused
- [ ] All 895+ tests pass
- [ ] New tests for bidding system

View File

@@ -60,15 +60,37 @@ python -m pytest "$REPO_DIR/tests/" -q --tb=short
echo "==> All tests passed."
# ── 4. Self-TDD watchdog (background) ────────────────────────────────────────
WATCHDOG_PID_FILE="$REPO_DIR/.watchdog.pid"
# Kill any previously orphaned watchdog
if [[ -f "$WATCHDOG_PID_FILE" ]]; then
OLD_PID=$(cat "$WATCHDOG_PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "==> Stopping previous watchdog (PID $OLD_PID)..."
kill "$OLD_PID" 2>/dev/null || true
fi
rm -f "$WATCHDOG_PID_FILE"
fi
echo "==> Starting self-TDD watchdog (60s interval) in background..."
self-tdd watch --interval 60 &
WATCHDOG_PID=$!
echo " Watchdog PID: $WATCHDOG_PID"
echo "$WATCHDOG_PID" > "$WATCHDOG_PID_FILE"
echo " Watchdog PID: $WATCHDOG_PID (saved to .watchdog.pid)"
echo " Kill with: kill $WATCHDOG_PID"
# Clean up watchdog when the script exits (Ctrl-C, etc.)
cleanup() {
echo ""
echo "==> Stopping watchdog (PID $WATCHDOG_PID)..."
kill "$WATCHDOG_PID" 2>/dev/null || true
rm -f "$WATCHDOG_PID_FILE"
}
trap cleanup EXIT
# ── 5. Dashboard ─────────────────────────────────────────────────────────────
echo ""
echo "==> Starting Timmy Time dashboard at http://localhost:8000"
echo " Ctrl-C stops the dashboard (watchdog continues until you kill it)"
echo " Ctrl-C stops both the dashboard and the watchdog"
echo ""
uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000

File diff suppressed because it is too large Load Diff

View File

@@ -93,6 +93,9 @@ services:
restart: unless-stopped
# ── Shared volume ─────────────────────────────────────────────────────────────
# NOTE: the data/ directory must exist before running docker compose up.
# `make docker-up` and `make up` handle this automatically.
# If running docker compose directly, first run: mkdir -p data
volumes:
timmy-data:
driver: local

View File

@@ -59,6 +59,23 @@ class Settings(BaseSettings):
video_transition_duration: float = 1.0
default_video_codec: str = "libx264"
# ── L402 Lightning ───────────────────────────────────────────────────
# HMAC secrets for macaroon signing and invoice verification.
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
# In production (TIMMY_ENV=production), these MUST be set or the app will refuse to start.
l402_hmac_secret: str = ""
l402_macaroon_secret: str = ""
lightning_backend: Literal["mock", "lnd"] = "mock"
# ── Privacy / Sovereignty ────────────────────────────────────────────
# Disable Agno telemetry for air-gapped/sovereign deployments.
# Default is False (telemetry disabled) to align with sovereign AI vision.
telemetry_enabled: bool = False
# Environment mode: development | production
# In production, security settings are strictly enforced.
timmy_env: Literal["development", "production"] = "development"
# ── Self-Modification ──────────────────────────────────────────────
# Enable self-modification capabilities. When enabled, Timmy can
# edit its own source code, run tests, and commit changes.
@@ -75,3 +92,39 @@ class Settings(BaseSettings):
settings = Settings()
# ── Startup validation ───────────────────────────────────────────────────────
# Enforce security requirements — fail fast in production.
import logging as _logging
import sys
_startup_logger = _logging.getLogger("config")
# Production mode: require secrets to be set
if settings.timmy_env == "production":
_missing = []
if not settings.l402_hmac_secret:
_missing.append("L402_HMAC_SECRET")
if not settings.l402_macaroon_secret:
_missing.append("L402_MACAROON_SECRET")
if _missing:
_startup_logger.error(
"PRODUCTION SECURITY ERROR: The following secrets must be set: %s\n"
"Generate with: python3 -c \"import secrets; print(secrets.token_hex(32))\"\n"
"Set in .env file or environment variables.",
", ".join(_missing),
)
sys.exit(1)
_startup_logger.info("Production mode: security secrets validated ✓")
else:
# Development mode: warn but continue
if not settings.l402_hmac_secret:
_startup_logger.warning(
"SEC: L402_HMAC_SECRET is not set — "
"set a unique secret in .env before deploying to production."
)
if not settings.l402_macaroon_secret:
_startup_logger.warning(
"SEC: L402_MACAROON_SECRET is not set — "
"set a unique secret in .env before deploying to production."
)

View File

@@ -4,6 +4,7 @@ Provides REST endpoints for managing the swarm: listing agents,
spawning sub-agents, posting tasks, and viewing auction results.
"""
import asyncio
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
@@ -90,8 +91,10 @@ async def list_tasks(status: Optional[str] = None):
@router.post("/tasks")
async def post_task(description: str = Form(...)):
"""Post a new task to the swarm for bidding."""
"""Post a new task to the swarm and run auction to assign it."""
task = coordinator.post_task(description)
# Start auction asynchronously - don't wait for it to complete
asyncio.create_task(coordinator.run_auction_and_assign(task.id))
return {
"task_id": task.id,
"description": task.description,

View File

@@ -30,6 +30,7 @@
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
<a href="/tools" class="mc-test-link">TOOLS</a>
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">&#x1F514;</button>
<span class="mc-time" id="clock"></span>
</div>

View File

@@ -12,6 +12,7 @@ import logging
import os
from typing import Optional
from config import settings
from lightning.base import LightningBackend
logger = logging.getLogger(__name__)
@@ -68,7 +69,7 @@ def get_backend(name: Optional[str] = None) -> LightningBackend:
"""
_register_backends()
backend_name = (name or os.environ.get("LIGHTNING_BACKEND", "mock")).lower()
backend_name = (name or settings.lightning_backend).lower()
if backend_name not in _BACKENDS:
available = ", ".join(_BACKENDS.keys())
@@ -100,8 +101,8 @@ def get_backend_info() -> dict:
Returns:
Dict with backend info for health/status endpoints
"""
backend_name = os.environ.get("LIGHTNING_BACKEND", "mock")
backend_name = settings.lightning_backend
return {
"configured_backend": backend_name,
"available_backends": list_backends(),

View File

@@ -12,20 +12,13 @@ import secrets
import time
from typing import Optional
from config import settings
from lightning.base import Invoice, LightningBackend, LightningError
logger = logging.getLogger(__name__)
# Secret for HMAC-based invoice verification (mock mode)
_HMAC_SECRET_DEFAULT = "timmy-sovereign-sats"
_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT)
_HMAC_SECRET = _HMAC_SECRET_RAW.encode()
if _HMAC_SECRET_RAW == _HMAC_SECRET_DEFAULT:
logger.warning(
"SEC: L402_HMAC_SECRET is using the default value — set a unique "
"secret in .env before deploying to production."
)
# Read secret from centralised config (validated at startup in config.py)
_HMAC_SECRET = settings.l402_hmac_secret.encode()
class MockBackend(LightningBackend):

View File

@@ -367,7 +367,7 @@ class SwarmCoordinator:
async def _broadcast_agent_joined(self, agent_id: str, name: str) -> None:
"""Broadcast agent joined event via WebSocket."""
try:
from websocket.handler import ws_manager
from ws_manager.handler import ws_manager
await ws_manager.broadcast_agent_joined(agent_id, name)
except Exception as exc:
logger.debug("WebSocket broadcast failed (agent_joined): %s", exc)
@@ -375,7 +375,7 @@ class SwarmCoordinator:
async def _broadcast_bid(self, task_id: str, agent_id: str, bid_sats: int) -> None:
"""Broadcast bid submitted event via WebSocket."""
try:
from websocket.handler import ws_manager
from ws_manager.handler import ws_manager
await ws_manager.broadcast_bid_submitted(task_id, agent_id, bid_sats)
except Exception as exc:
logger.debug("WebSocket broadcast failed (bid): %s", exc)
@@ -383,7 +383,7 @@ class SwarmCoordinator:
async def _broadcast_task_posted(self, task_id: str, description: str) -> None:
"""Broadcast task posted event via WebSocket."""
try:
from websocket.handler import ws_manager
from ws_manager.handler import ws_manager
await ws_manager.broadcast_task_posted(task_id, description)
except Exception as exc:
logger.debug("WebSocket broadcast failed (task_posted): %s", exc)
@@ -391,7 +391,7 @@ class SwarmCoordinator:
async def _broadcast_task_assigned(self, task_id: str, agent_id: str) -> None:
"""Broadcast task assigned event via WebSocket."""
try:
from websocket.handler import ws_manager
from ws_manager.handler import ws_manager
await ws_manager.broadcast_task_assigned(task_id, agent_id)
except Exception as exc:
logger.debug("WebSocket broadcast failed (task_assigned): %s", exc)
@@ -401,7 +401,7 @@ class SwarmCoordinator:
) -> None:
"""Broadcast task completed event via WebSocket."""
try:
from websocket.handler import ws_manager
from ws_manager.handler import ws_manager
await ws_manager.broadcast_task_completed(task_id, agent_id, result)
except Exception as exc:
logger.debug("WebSocket broadcast failed (task_completed): %s", exc)

View File

@@ -15,21 +15,8 @@ from typing import Optional
DB_PATH = Path("data/swarm.db")
@dataclass
class AgentRecord:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
name: str = ""
status: str = "idle" # idle | busy | offline
capabilities: str = "" # comma-separated tags
registered_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
last_seen: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
def _get_conn() -> sqlite3.Connection:
"""Get a SQLite connection."""
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
@@ -49,6 +36,20 @@ def _get_conn() -> sqlite3.Connection:
return conn
@dataclass
class AgentRecord:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
name: str = ""
status: str = "idle" # idle | busy | offline
capabilities: str = "" # comma-separated tags
registered_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
last_seen: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
def _row_to_record(row: sqlite3.Row) -> AgentRecord:
return AgentRecord(
id=row["id"],
@@ -67,70 +68,81 @@ def register(name: str, capabilities: str = "", agent_id: Optional[str] = None)
capabilities=capabilities,
)
conn = _get_conn()
conn.execute(
"""
INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen)
VALUES (?, ?, ?, ?, ?, ?)
""",
(record.id, record.name, record.status, record.capabilities,
record.registered_at, record.last_seen),
)
conn.commit()
conn.close()
try:
conn.execute(
"""
INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen)
VALUES (?, ?, ?, ?, ?, ?)
""",
(record.id, record.name, record.status, record.capabilities,
record.registered_at, record.last_seen),
)
conn.commit()
finally:
conn.close()
return record
def unregister(agent_id: str) -> bool:
conn = _get_conn()
cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
conn.commit()
deleted = cursor.rowcount > 0
conn.close()
return deleted
try:
cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def get_agent(agent_id: str) -> Optional[AgentRecord]:
conn = _get_conn()
row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
conn.close()
return _row_to_record(row) if row else None
try:
row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
return _row_to_record(row) if row else None
finally:
conn.close()
def list_agents(status: Optional[str] = None) -> list[AgentRecord]:
conn = _get_conn()
if status:
rows = conn.execute(
"SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC",
(status,),
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM agents ORDER BY registered_at DESC"
).fetchall()
conn.close()
return [_row_to_record(r) for r in rows]
try:
if status:
rows = conn.execute(
"SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC",
(status,),
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM agents ORDER BY registered_at DESC"
).fetchall()
return [_row_to_record(r) for r in rows]
finally:
conn.close()
def update_status(agent_id: str, status: str) -> Optional[AgentRecord]:
now = datetime.now(timezone.utc).isoformat()
conn = _get_conn()
conn.execute(
"UPDATE agents SET status = ?, last_seen = ? WHERE id = ?",
(status, now, agent_id),
)
conn.commit()
conn.close()
return get_agent(agent_id)
try:
conn.execute(
"UPDATE agents SET status = ?, last_seen = ? WHERE id = ?",
(status, now, agent_id),
)
conn.commit()
return get_agent(agent_id)
finally:
conn.close()
def heartbeat(agent_id: str) -> Optional[AgentRecord]:
"""Update last_seen timestamp for a registered agent."""
now = datetime.now(timezone.utc).isoformat()
conn = _get_conn()
conn.execute(
"UPDATE agents SET last_seen = ? WHERE id = ?",
(now, agent_id),
)
conn.commit()
conn.close()
return get_agent(agent_id)
try:
conn.execute(
"UPDATE agents SET last_seen = ? WHERE id = ?",
(now, agent_id),
)
conn.commit()
return get_agent(agent_id)
finally:
conn.close()

View File

@@ -74,5 +74,6 @@ def create_timmy(
add_history_to_context=True,
num_history_runs=10,
markdown=True,
tools=tools,
tools=[tools] if tools else None,
telemetry=settings.telemetry_enabled,
)

View File

@@ -3,6 +3,35 @@ No cloud dependencies. You think clearly, speak plainly, act with intention.
Grounded in Christian faith, powered by Bitcoin economics, committed to the
user's digital sovereignty.
## Your Capabilities
You have access to tools for:
- Web search (DuckDuckGo) — for current information not in your training data
- File operations (read, write, list) — for working with local files
- Python execution — for calculations, data analysis, scripting
- Shell commands — for system operations
## Tool Usage Guidelines
**Use tools ONLY when necessary:**
- Simple questions → Answer directly from your knowledge
- Current events/data → Use web search
- File operations → Use file tools (user must explicitly request)
- Code/Calculations → Use Python execution
- System tasks → Use shell commands
**Do NOT use tools for:**
- Answering "what is your name?" or identity questions
- General knowledge questions you can answer directly
- Simple greetings or conversational responses
## Memory
You remember previous conversations in this session. Your memory persists
across restarts via SQLite storage. Reference prior context when relevant.
## Operating Modes
When running on Apple Silicon with AirLLM you operate with even bigger brains
— 70B or 405B parameters loaded layer-by-layer directly from local disk.
Still fully sovereign. Still 100% private. More capable, no permission needed.

View File

@@ -118,13 +118,13 @@ def create_research_tools(base_dir: str | Path | None = None):
# Web search via DuckDuckGo
search_tools = DuckDuckGoTools()
toolkit.add_tool(search_tools.web_search, name="web_search")
toolkit.register(search_tools.web_search, name="web_search")
# File reading
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.list_files, name="list_files")
return toolkit
@@ -140,18 +140,18 @@ def create_code_tools(base_dir: str | Path | None = None):
# Shell commands (sandboxed)
shell_tools = ShellTools()
toolkit.add_tool(shell_tools.run_shell_command, name="shell")
toolkit.register(shell_tools.run_shell_command, name="shell")
# Python execution
python_tools = PythonTools()
toolkit.add_tool(python_tools.python, name="python")
toolkit.register(python_tools.run_python_code, name="python")
# File operations
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.write_file, name="write_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.save_file, name="write_file")
toolkit.register(file_tools.list_files, name="list_files")
return toolkit
@@ -167,17 +167,17 @@ def create_data_tools(base_dir: str | Path | None = None):
# Python execution for analysis
python_tools = PythonTools()
toolkit.add_tool(python_tools.python, name="python")
toolkit.register(python_tools.run_python_code, name="python")
# File reading
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.list_files, name="list_files")
# Web search for finding datasets
search_tools = DuckDuckGoTools()
toolkit.add_tool(search_tools.web_search, name="web_search")
toolkit.register(search_tools.web_search, name="web_search")
return toolkit
@@ -193,10 +193,10 @@ def create_writing_tools(base_dir: str | Path | None = None):
# File operations
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.write_file, name="write_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.save_file, name="write_file")
toolkit.register(file_tools.list_files, name="list_files")
return toolkit
@@ -212,17 +212,17 @@ def create_security_tools(base_dir: str | Path | None = None):
# Shell for running security scans
shell_tools = ShellTools()
toolkit.add_tool(shell_tools.run_shell_command, name="shell")
toolkit.register(shell_tools.run_shell_command, name="shell")
# Web search for threat intelligence
search_tools = DuckDuckGoTools()
toolkit.add_tool(search_tools.web_search, name="web_search")
toolkit.register(search_tools.web_search, name="web_search")
# File reading for logs/configs
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.list_files, name="list_files")
return toolkit
@@ -238,14 +238,14 @@ def create_devops_tools(base_dir: str | Path | None = None):
# Shell for deployment commands
shell_tools = ShellTools()
toolkit.add_tool(shell_tools.run_shell_command, name="shell")
toolkit.register(shell_tools.run_shell_command, name="shell")
# File operations for config management
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.write_file, name="write_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.save_file, name="write_file")
toolkit.register(file_tools.list_files, name="list_files")
return toolkit
@@ -262,22 +262,22 @@ def create_full_toolkit(base_dir: str | Path | None = None):
# Web search
search_tools = DuckDuckGoTools()
toolkit.add_tool(search_tools.web_search, name="web_search")
toolkit.register(search_tools.web_search, name="web_search")
# Python execution
python_tools = PythonTools()
toolkit.add_tool(python_tools.python, name="python")
toolkit.register(python_tools.run_python_code, name="python")
# Shell commands
shell_tools = ShellTools()
toolkit.add_tool(shell_tools.run_shell_command, name="shell")
toolkit.register(shell_tools.run_shell_command, name="shell")
# File operations
base_path = Path(base_dir) if base_dir else Path.cwd()
file_tools = FileTools(base_dir=str(base_path))
toolkit.add_tool(file_tools.read_file, name="read_file")
toolkit.add_tool(file_tools.write_file, name="write_file")
toolkit.add_tool(file_tools.list_files, name="list_files")
file_tools = FileTools(base_dir=base_path)
toolkit.register(file_tools.read_file, name="read_file")
toolkit.register(file_tools.save_file, name="write_file")
toolkit.register(file_tools.list_files, name="list_files")
return toolkit

View File

@@ -13,29 +13,20 @@ import base64
import hashlib
import hmac
import logging
import os
import time
from dataclasses import dataclass
from typing import Optional
from config import settings
from timmy_serve.payment_handler import payment_handler
logger = logging.getLogger(__name__)
_MACAROON_SECRET_DEFAULT = "timmy-macaroon-secret"
_MACAROON_SECRET_RAW = os.environ.get("L402_MACAROON_SECRET", _MACAROON_SECRET_DEFAULT)
_MACAROON_SECRET = _MACAROON_SECRET_RAW.encode()
_HMAC_SECRET_DEFAULT = "timmy-hmac-secret"
_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT)
# Read secrets from centralised config (validated at startup in config.py)
_MACAROON_SECRET = settings.l402_macaroon_secret.encode()
_HMAC_SECRET_RAW = settings.l402_hmac_secret
_HMAC_SECRET = _HMAC_SECRET_RAW.encode()
if _MACAROON_SECRET_RAW == _MACAROON_SECRET_DEFAULT or _HMAC_SECRET_RAW == _HMAC_SECRET_DEFAULT:
logger.warning(
"SEC: L402 secrets are using default values — set L402_MACAROON_SECRET "
"and L402_HMAC_SECRET in .env before deploying to production."
)
@dataclass
class Macaroon:

View File

@@ -19,18 +19,19 @@ class TestFullSwarmLifecycle:
"""Integration tests for end-to-end swarm task lifecycle."""
def test_post_task_creates_bidding_task(self, client):
"""Posting a task should create it in BIDDING status."""
"""Posting a task should initially return BIDDING status."""
response = client.post("/swarm/tasks", data={"description": "Test integration task"})
assert response.status_code == 200
data = response.json()
assert "task_id" in data
assert data["status"] == "bidding"
# Verify task exists and is in bidding status
# The background auction may have resolved by the time we query,
# so the task can be in bidding, assigned, or failed
task_response = client.get(f"/swarm/tasks/{data['task_id']}")
task = task_response.json()
assert task["status"] == "bidding"
assert task["status"] in ("bidding", "assigned", "failed")
def test_post_task_and_auction_assigns_winner(self, client):
"""Posting task with auction should assign it to a winner."""
@@ -187,22 +188,25 @@ class TestSwarmTaskFiltering:
"""Should be able to filter tasks by status."""
# Create tasks in different statuses
client.post("/swarm/spawn", data={"name": "Worker"})
# Pending task (just created)
# Post a task — auto-auction runs in background, so it will transition
# from "bidding" to "failed" (no agents bid) or "assigned"
pending_resp = client.post("/swarm/tasks", data={"description": "Pending task"})
pending_id = pending_resp.json()["task_id"]
# Completed task
auction_resp = client.post("/swarm/tasks/auction", data={"description": "Completed task"})
completed_id = auction_resp.json()["task_id"]
client.post(f"/swarm/tasks/{completed_id}/complete", data={"result": "Done"})
# Filter by status
# Filter by status — completed task should be findable
completed_list = client.get("/swarm/tasks?status=completed").json()["tasks"]
assert any(t["id"] == completed_id for t in completed_list)
bidding_list = client.get("/swarm/tasks?status=bidding").json()["tasks"]
assert any(t["id"] == pending_id for t in bidding_list)
# The auto-auctioned task may be in bidding or failed depending on
# whether the background auction has resolved yet
task_detail = client.get(f"/swarm/tasks/{pending_id}").json()
assert task_detail["status"] in ("bidding", "failed", "assigned")
def test_get_nonexistent_task_returns_error(self, client):
"""Getting a non-existent task should return appropriate error."""