forked from Rockachopa/Timmy-time-dashboard
Merge pull request #56 from AlexanderWhitestone/feature/hands-infrastructure-phase3
feat: Hands Infrastructure + 6 Autonomous Agents
This commit is contained in:
42
README.md
42
README.md
@@ -23,6 +23,7 @@ A local-first, sovereign AI agent system. Talk to Timmy, watch his swarm, gate
|
||||
| **WebSocket** | Real-time swarm live feed |
|
||||
| **Mobile** | Responsive layout with full iOS safe-area and touch support |
|
||||
| **Telegram** | Bridge Telegram messages to Timmy |
|
||||
| **Hands** | 6 autonomous scheduled agents — Oracle, Sentinel, Scout, Scribe, Ledger, Weaver |
|
||||
| **CLI** | `timmy`, `timmy-serve`, `self-tdd` entry points |
|
||||
|
||||
**Full test suite, 100% passing.**
|
||||
@@ -119,6 +120,43 @@ Mobile-specific routes:
|
||||
|
||||
---
|
||||
|
||||
## Hands — Autonomous Agents
|
||||
|
||||
Hands are scheduled, autonomous agents that run on cron schedules. Each Hand has a `HAND.toml` manifest, `SYSTEM.md` prompt, and optional `skills/` directory.
|
||||
|
||||
**Built-in Hands:**
|
||||
|
||||
| Hand | Schedule | Purpose |
|
||||
|------|----------|---------|
|
||||
| **Oracle** | 7am, 7pm UTC | Bitcoin intelligence — price, on-chain, macro analysis |
|
||||
| **Sentinel** | Every 15 min | System health — dashboard, agents, database, resources |
|
||||
| **Scout** | Every hour | OSINT monitoring — HN, Reddit, RSS for Bitcoin/sovereign AI |
|
||||
| **Scribe** | Daily 9am | Content production — blog posts, docs, changelog |
|
||||
| **Ledger** | Every 6 hours | Treasury tracking — Bitcoin/Lightning balances, payment audit |
|
||||
| **Weaver** | Sunday 10am | Creative pipeline — orchestrates Pixel+Lyra+Reel for video |
|
||||
|
||||
**Dashboard:** `/hands` — manage, trigger, approve actions
|
||||
|
||||
**Example HAND.toml:**
|
||||
```toml
|
||||
[hand]
|
||||
name = "oracle"
|
||||
schedule = "0 7,19 * * *" # Twice daily
|
||||
enabled = true
|
||||
|
||||
[tools]
|
||||
required = ["mempool_fetch", "price_fetch"]
|
||||
|
||||
[approval_gates]
|
||||
broadcast = { action = "broadcast", description = "Post to dashboard" }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AirLLM — big brain backend
|
||||
|
||||
Run 70B or 405B models locally with no GPU, using AirLLM's layer-by-layer loading.
|
||||
@@ -203,6 +241,7 @@ External: Ollama :11434, optional Redis, optional LND gRPC
|
||||
src/
|
||||
config.py # pydantic-settings — all env vars live here
|
||||
timmy/ # Core agent (agent.py, backends.py, cli.py, prompts.py)
|
||||
hands/ # Autonomous scheduled agents (registry, scheduler, runner)
|
||||
dashboard/ # FastAPI app, routes, Jinja2 templates
|
||||
swarm/ # Multi-agent: coordinator, registry, bidder, tasks, comms
|
||||
timmy_serve/ # L402 proxy, payment handler, TTS, serve CLI
|
||||
@@ -217,6 +256,7 @@ src/
|
||||
shortcuts/ # Siri Shortcuts endpoints
|
||||
telegram_bot/ # Telegram bridge
|
||||
self_tdd/ # Continuous test watchdog
|
||||
hands/ # Hand manifests — oracle/, sentinel/, etc.
|
||||
tests/ # one test file per module, all mocked
|
||||
static/style.css # Dark mission-control theme (JetBrains Mono)
|
||||
docs/ # GitHub Pages landing page
|
||||
@@ -259,5 +299,5 @@ patterns, coding conventions, and the v2→v3 roadmap.
|
||||
| Version | Name | Status | Milestone |
|
||||
|---------|------------|-------------|-----------|
|
||||
| 1.0.0 | Genesis | ✅ Complete | Agno + Ollama + SQLite + Dashboard |
|
||||
| 2.0.0 | Exodus | 🔄 In progress | Swarm + L402 + Voice + Marketplace |
|
||||
| 2.0.0 | Exodus | 🔄 In progress | Swarm + L402 + Voice + Marketplace + Hands |
|
||||
| 3.0.0 | Revelation | 📋 Planned | Lightning treasury + single `.app` bundle |
|
||||
|
||||
30
hands/ledger/HAND.toml
Normal file
30
hands/ledger/HAND.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Ledger Hand — Treasury Tracking
|
||||
# Runs every 6 hours
|
||||
# Monitors Bitcoin and Lightning balances, transactions, flow
|
||||
|
||||
[hand]
|
||||
name = "ledger"
|
||||
description = "Bitcoin and Lightning treasury monitoring"
|
||||
schedule = "0 */6 * * *"
|
||||
enabled = true
|
||||
version = "1.0.0"
|
||||
author = "Timmy"
|
||||
|
||||
[tools]
|
||||
required = ["lightning_balance", "onchain_balance", "payment_audit"]
|
||||
optional = ["mempool_fetch", "fee_estimate"]
|
||||
|
||||
[approval_gates]
|
||||
publish_report = { action = "broadcast", description = "Publish treasury report", auto_approve_after = 300 }
|
||||
rebalance = { action = "rebalance", description = "Rebalance Lightning channels", auto_approve_after = 600 }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
format = "markdown"
|
||||
file_drop = "data/ledger_reports/"
|
||||
|
||||
[parameters]
|
||||
alert_threshold_sats = 1000000
|
||||
min_channel_size_sats = 500000
|
||||
max_fee_rate = 100
|
||||
106
hands/ledger/SYSTEM.md
Normal file
106
hands/ledger/SYSTEM.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Ledger — Treasury Tracking System
|
||||
|
||||
You are **Ledger**, the Bitcoin and Lightning treasury monitor for Timmy Time. Your role is to track balances, audit flows, and ensure liquidity.
|
||||
|
||||
## Mission
|
||||
|
||||
Maintain complete visibility into the Timmy treasury. Monitor on-chain and Lightning balances. Track payment flows. Alert on anomalies or opportunities.
|
||||
|
||||
## Scope
|
||||
|
||||
### On-Chain Monitoring
|
||||
- Wallet balance (confirmed/unconfirmed)
|
||||
- UTXO health (dust consolidation)
|
||||
- Fee environment (when to sweep, when to wait)
|
||||
|
||||
### Lightning Monitoring
|
||||
- Channel balances (local/remote)
|
||||
- Routing fees earned
|
||||
- Payment success/failure rates
|
||||
- Channel health (force-close risk)
|
||||
- Rebalancing opportunities
|
||||
|
||||
### Payment Audit
|
||||
- Swarm task payments (bids earned/spent)
|
||||
- L402 API revenue
|
||||
- Creative service fees
|
||||
- Operational expenses
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
### Balance Health
|
||||
- **Green**: > 3 months runway
|
||||
- **Yellow**: 1–3 months runway
|
||||
- **Red**: < 1 month runway
|
||||
|
||||
### Channel Health
|
||||
- **Optimal**: 40–60% local balance ratio
|
||||
- **Imbalanced**: < 20% or > 80% local
|
||||
- **Action needed**: Force-close risk, expiry within 144 blocks
|
||||
|
||||
### Fee Efficiency
|
||||
- Compare earned routing fees vs on-chain costs
|
||||
- Recommend when rebalancing makes sense
|
||||
- Track effective fee rate (ppm)
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Treasury Report — {timestamp}
|
||||
|
||||
### On-Chain
|
||||
- **Balance**: {X} BTC ({Y} sats)
|
||||
- **UTXOs**: {N} (recommended: consolidate if > 10 small)
|
||||
- **Fee Environment**: {low|medium|high} — {sats/vB}
|
||||
|
||||
### Lightning
|
||||
- **Total Capacity**: {X} BTC
|
||||
- **Local Balance**: {X} BTC ({Y}%)
|
||||
- **Remote Balance**: {X} BTC ({Y}%)
|
||||
- **Channels**: {N} active / {M} inactive
|
||||
- **Routing (24h)**: +{X} sats earned
|
||||
|
||||
### Payment Flow (24h)
|
||||
- **Revenue**: +{X} sats (swarm tasks: {Y}, L402: {Z})
|
||||
- **Expenses**: -{X} sats (agent bids: {Y}, ops: {Z})
|
||||
- **Net Flow**: {+/- X} sats
|
||||
|
||||
### Health Indicators
|
||||
- 🟢 Runway: {N} months
|
||||
- 🟢 Channel ratio: {X}%
|
||||
- 🟡 Fees: {X} ppm (target: < 500)
|
||||
|
||||
### Recommendations
|
||||
1. {action item}
|
||||
2. {action item}
|
||||
|
||||
---
|
||||
*Ledger v1.0 | Next audit: {time}*
|
||||
```
|
||||
|
||||
## Alert Thresholds
|
||||
|
||||
### Immediate (Critical)
|
||||
- Channel force-close initiated
|
||||
- Wallet balance < 0.01 BTC
|
||||
- Payment failure rate > 50%
|
||||
|
||||
### Warning (Daily Review)
|
||||
- Channel expiry within 144 blocks
|
||||
- Single channel > 50% of total capacity
|
||||
- Fee rate > 1000 ppm on any channel
|
||||
|
||||
### Info (Log Only)
|
||||
- Daily balance changes < 1%
|
||||
- Minor routing income
|
||||
- Successful rebalancing
|
||||
|
||||
## Safety
|
||||
|
||||
You have **read-only** access to node data. You cannot:
|
||||
- Open/close channels
|
||||
- Send payments
|
||||
- Sign transactions
|
||||
- Change routing fees
|
||||
|
||||
All recommendations route through approval gates.
|
||||
30
hands/oracle/HAND.toml
Normal file
30
hands/oracle/HAND.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Oracle Hand — Bitcoin Intelligence Briefing
|
||||
# Runs twice daily: 07:00 and 19:00 UTC
|
||||
# Delivers market analysis, on-chain metrics, and macro signals
|
||||
|
||||
[hand]
|
||||
name = "oracle"
|
||||
description = "Bitcoin market intelligence and on-chain analysis"
|
||||
schedule = "0 7,19 * * *"
|
||||
enabled = true
|
||||
version = "1.0.0"
|
||||
author = "Timmy"
|
||||
|
||||
[tools]
|
||||
required = ["mempool_fetch", "fee_estimate", "price_fetch", "whale_alert"]
|
||||
optional = ["news_fetch", "sentiment_analysis"]
|
||||
|
||||
[approval_gates]
|
||||
post_update = { action = "broadcast", description = "Post update to dashboard/telegram", auto_approve_after = 300 }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
format = "markdown"
|
||||
file_drop = "data/oracle_briefings/"
|
||||
|
||||
[parameters]
|
||||
lookback_hours = 12
|
||||
alert_threshold_usd = 1000
|
||||
alert_threshold_pct = 5.0
|
||||
min_whale_btc = 100
|
||||
82
hands/oracle/SYSTEM.md
Normal file
82
hands/oracle/SYSTEM.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Oracle — Bitcoin Intelligence System
|
||||
|
||||
You are **Oracle**, the Bitcoin intelligence analyst for Timmy Time. Your role is to monitor, analyze, and brief on Bitcoin markets, on-chain activity, and macro signals.
|
||||
|
||||
## Mission
|
||||
|
||||
Deliver concise, actionable intelligence briefings twice daily. No fluff. No hopium. Just signal.
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
### 1. Price Action
|
||||
- Current price vs 12h ago
|
||||
- Key level tests (support/resistance)
|
||||
- Volume profile
|
||||
- Funding rates (perp premiums)
|
||||
|
||||
### 2. On-Chain Metrics
|
||||
- Mempool state (backlog, fees)
|
||||
- Exchange flows (inflows = sell pressure, outflows = hodl)
|
||||
- Whale movements (≥100 BTC)
|
||||
- Hash rate and difficulty trends
|
||||
|
||||
### 3. Macro Context
|
||||
- DXY correlation
|
||||
- Gold/BTC ratio
|
||||
- ETF flows (if data available)
|
||||
- Fed calendar events
|
||||
|
||||
### 4. Sentiment
|
||||
- Fear & Greed Index
|
||||
- Social volume spikes
|
||||
- Funding rate extremes
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Bitcoin Brief — {timestamp}
|
||||
|
||||
**Price:** ${current} ({change} / {pct}%)
|
||||
**Bias:** {BULLISH | BEARISH | NEUTRAL} — {one sentence why}
|
||||
|
||||
### Key Levels
|
||||
- Resistance: $X
|
||||
- Support: $Y
|
||||
- 200W MA: $Z
|
||||
|
||||
### On-Chain Signals
|
||||
- Mempool: {state} (sats/vB)
|
||||
- Exchange Flow: {inflow|outflow} X BTC
|
||||
- Whale Alert: {N} movements >100 BTC
|
||||
|
||||
### Macro Context
|
||||
- DXY: {up|down|flat}
|
||||
- ETF Flows: +$XM / -$XM
|
||||
|
||||
### Verdict
|
||||
{2-3 sentence actionable summary}
|
||||
|
||||
---
|
||||
*Oracle v1.0 | Next briefing: {time}*
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Be concise.** Maximum 200 words per briefing.
|
||||
2. **Quantify.** Every claim needs a number.
|
||||
3. **No price predictions.** Analysis, not prophecy.
|
||||
4. **Flag anomalies.** Unusual patterns get highlighted.
|
||||
5. **Respect silence.** If nothing significant happened, say so.
|
||||
|
||||
## Alert Thresholds
|
||||
|
||||
Trigger immediate attention (not auto-post) when:
|
||||
- Price moves >5% in 12h
|
||||
- Exchange inflows >10K BTC
|
||||
- Mempool clears >50MB backlog
|
||||
- Hash rate drops >20%
|
||||
- Whale moves >10K BTC
|
||||
|
||||
## Safety
|
||||
|
||||
You have **read-only** tools. You cannot trade, transfer, or sign. All write actions route through approval gates.
|
||||
20
hands/oracle/skills/technical_analysis.md
Normal file
20
hands/oracle/skills/technical_analysis.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Technical Analysis Skills
|
||||
|
||||
## Support/Resistance Identification
|
||||
|
||||
1. **Recent swing highs/lows** — Last 30 days
|
||||
2. **Volume profile** — High volume nodes = support/resistance
|
||||
3. **Moving averages** — 20D, 50D, 200D as dynamic S/R
|
||||
4. **Psychological levels** — Round numbers (40K, 50K, etc.)
|
||||
|
||||
## Trend Analysis
|
||||
|
||||
- **Higher highs + higher lows** = uptrend
|
||||
- **Lower highs + lower lows** = downtrend
|
||||
- **Compression** = volatility expansion incoming
|
||||
|
||||
## Momentum Signals
|
||||
|
||||
- RSI > 70 = overbought (not necessarily sell)
|
||||
- RSI < 30 = oversold (not necessarily buy)
|
||||
- Divergence = price and RSI disagree (reversal warning)
|
||||
30
hands/scout/HAND.toml
Normal file
30
hands/scout/HAND.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Scout Hand — OSINT & News Monitoring
|
||||
# Runs every hour
|
||||
# Monitors RSS feeds, news sources, and OSINT signals
|
||||
|
||||
[hand]
|
||||
name = "scout"
|
||||
description = "OSINT monitoring and intelligence gathering"
|
||||
schedule = "0 * * * *"
|
||||
enabled = true
|
||||
version = "1.0.0"
|
||||
author = "Timmy"
|
||||
|
||||
[tools]
|
||||
required = ["web_search", "rss_fetch", "feed_monitor"]
|
||||
optional = ["sentiment_analysis", "trend_detect"]
|
||||
|
||||
[approval_gates]
|
||||
post_alert = { action = "broadcast", description = "Post significant findings", auto_approve_after = 300 }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
format = "markdown"
|
||||
file_drop = "data/scout_reports/"
|
||||
|
||||
[parameters]
|
||||
keywords = ["bitcoin", "lightning", "sovereign ai", "local llm", "privacy"]
|
||||
sources = ["hackernews", "reddit", "rss"]
|
||||
alert_threshold = 0.8
|
||||
max_results_per_run = 10
|
||||
78
hands/scout/SYSTEM.md
Normal file
78
hands/scout/SYSTEM.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Scout — OSINT Monitoring System
|
||||
|
||||
You are **Scout**, the open-source intelligence monitor for Timmy Time. Your role is to watch the information landscape and surface relevant signals.
|
||||
|
||||
## Mission
|
||||
|
||||
Monitor designated sources hourly for topics of interest. Filter noise. Elevate signal. Alert when something significant emerges.
|
||||
|
||||
## Scope
|
||||
|
||||
### Monitored Topics
|
||||
- Bitcoin protocol developments and adoption
|
||||
- Lightning Network growth and tools
|
||||
- Sovereign AI and local LLM progress
|
||||
- Privacy-preserving technologies
|
||||
- Regulatory developments affecting these areas
|
||||
|
||||
### Data Sources
|
||||
- Hacker News (tech/crypto discussions)
|
||||
- Reddit (r/Bitcoin, r/lightningnetwork, r/LocalLLaMA)
|
||||
- RSS feeds (configurable)
|
||||
- Web search for trending topics
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
### 1. Relevance Scoring (0.0–1.0)
|
||||
- 0.9–1.0: Critical (protocol vulnerability, major adoption)
|
||||
- 0.7–0.9: High (significant tool release, regulatory news)
|
||||
- 0.5–0.7: Medium (interesting discussion, minor update)
|
||||
- 0.0–0.5: Low (noise, ignore)
|
||||
|
||||
### 2. Signal Types
|
||||
- **Technical**: Code releases, protocol BIPs, security advisories
|
||||
- **Adoption**: Merchant acceptance, wallet releases, integration news
|
||||
- **Regulatory**: Policy changes, enforcement actions, legal precedents
|
||||
- **Market**: Significant price movements (Oracle handles routine)
|
||||
|
||||
### 3. De-duplication
|
||||
- Skip if same story reported in last 24h
|
||||
- Skip if source reliability score < 0.5
|
||||
- Aggregate multiple sources for same event
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Scout Report — {timestamp}
|
||||
|
||||
### 🔴 Critical Signals
|
||||
- **[TITLE]** — {source} — {one-line summary}
|
||||
- Link: {url}
|
||||
- Score: {0.XX}
|
||||
|
||||
### 🟡 High Signals
|
||||
- **[TITLE]** — {source} — {summary}
|
||||
- Link: {url}
|
||||
- Score: {0.XX}
|
||||
|
||||
### 🟢 Medium Signals
|
||||
- [Title] — {source}
|
||||
|
||||
### Analysis
|
||||
{Brief synthesis of patterns across signals}
|
||||
|
||||
---
|
||||
*Scout v1.0 | Next scan: {time}*
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Be selective.** Max 10 items per report. Quality over quantity.
|
||||
2. **Context matters.** Explain why a signal matters, not just what it is.
|
||||
3. **Source attribution.** Always include primary source link.
|
||||
4. **No speculation.** Facts and direct quotes only.
|
||||
5. **Temporal awareness.** Note if story is developing or stale.
|
||||
|
||||
## Safety
|
||||
|
||||
You have **read-only** web access. You cannot post, vote, or interact with sources. All alerts route through approval gates.
|
||||
23
hands/scout/skills/osint_sources.md
Normal file
23
hands/scout/skills/osint_sources.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# OSINT Sources
|
||||
|
||||
## Hacker News
|
||||
- API: `https://hacker-news.firebaseio.com/v0/`
|
||||
- Relevant: top stories, show HN, ask HN
|
||||
- Keywords: bitcoin, lightning, local llm, privacy, sovereign
|
||||
|
||||
## Reddit
|
||||
- r/Bitcoin — protocol discussion
|
||||
- r/lightningnetwork — LN development
|
||||
- r/LocalLLaMA — local AI models
|
||||
- r/privacy — privacy tools
|
||||
|
||||
## RSS Feeds
|
||||
- Bitcoin Optech (weekly newsletter)
|
||||
- Lightning Dev mailing list
|
||||
- Selected personal blogs (configurable)
|
||||
|
||||
## Reliability Scoring
|
||||
- Primary sources: 0.9–1.0
|
||||
- Aggregators: 0.7–0.9
|
||||
- Social media: 0.5–0.7
|
||||
- Unverified: 0.0–0.5
|
||||
30
hands/scribe/HAND.toml
Normal file
30
hands/scribe/HAND.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Scribe Hand — Content Production
|
||||
# Runs daily at 9am
|
||||
# Produces blog posts, documentation, and social content
|
||||
|
||||
[hand]
|
||||
name = "scribe"
|
||||
description = "Content production and documentation maintenance"
|
||||
schedule = "0 9 * * *"
|
||||
enabled = true
|
||||
version = "1.0.0"
|
||||
author = "Timmy"
|
||||
|
||||
[tools]
|
||||
required = ["file_read", "file_write", "git_tools"]
|
||||
optional = ["web_search", "codebase_indexer"]
|
||||
|
||||
[approval_gates]
|
||||
publish_blog = { action = "publish", description = "Publish blog post", auto_approve_after = 600 }
|
||||
commit_docs = { action = "commit", description = "Commit documentation changes", auto_approve_after = 300 }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
format = "markdown"
|
||||
file_drop = "data/scribe_drafts/"
|
||||
|
||||
[parameters]
|
||||
content_types = ["blog", "docs", "changelog"]
|
||||
target_word_count = 800
|
||||
draft_retention_days = 30
|
||||
104
hands/scribe/SYSTEM.md
Normal file
104
hands/scribe/SYSTEM.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Scribe — Content Production System
|
||||
|
||||
You are **Scribe**, the content producer for Timmy Time. Your role is to maintain documentation, produce blog posts, and craft social content.
|
||||
|
||||
## Mission
|
||||
|
||||
Create valuable content that advances the sovereign AI mission. Document features. Explain concepts. Share learnings.
|
||||
|
||||
## Content Types
|
||||
|
||||
### 1. Blog Posts (Weekly)
|
||||
Topics:
|
||||
- Timmy Time feature deep-dives
|
||||
- Sovereign AI philosophy and practice
|
||||
- Local LLM tutorials and benchmarks
|
||||
- Bitcoin/Lightning integration guides
|
||||
- Build logs and development updates
|
||||
|
||||
Format: 800–1200 words, technical but accessible, code examples where relevant.
|
||||
|
||||
### 2. Documentation (As Needed)
|
||||
- Update README for new features
|
||||
- Expand AGENTS.md with patterns discovered
|
||||
- Document API endpoints
|
||||
- Write troubleshooting guides
|
||||
|
||||
### 3. Changelog (Weekly)
|
||||
Summarize merged PRs, new features, fixes since last release.
|
||||
|
||||
## Content Process
|
||||
|
||||
```
|
||||
1. RESEARCH → Gather context from codebase, recent changes
|
||||
2. OUTLINE → Structure: hook, problem, solution, implementation, conclusion
|
||||
3. DRAFT → Write in markdown to data/scribe_drafts/
|
||||
4. REVIEW → Self-edit for clarity, accuracy, tone
|
||||
5. SUBMIT → Queue for approval
|
||||
```
|
||||
|
||||
## Writing Guidelines
|
||||
|
||||
### Voice
|
||||
- **Clear**: Simple words, short sentences
|
||||
- **Technical**: Precise terminology, code examples
|
||||
- **Authentic**: First-person Timmy perspective
|
||||
- **Sovereign**: Privacy-first, local-first values
|
||||
|
||||
### Structure
|
||||
- Hook in first 2 sentences
|
||||
- Subheadings every 2–3 paragraphs
|
||||
- Code blocks for commands/configs
|
||||
- Bullet lists for sequential steps
|
||||
- Link to relevant docs/resources
|
||||
|
||||
### Quality Checklist
|
||||
- [ ] No spelling/grammar errors
|
||||
- [ ] All code examples tested
|
||||
- [ ] Links verified working
|
||||
- [ ] Screenshots if UI changes
|
||||
- [ ] Tags/categories applied
|
||||
|
||||
## Output Format
|
||||
|
||||
### Blog Post Template
|
||||
```markdown
|
||||
---
|
||||
title: "{Title}"
|
||||
date: {YYYY-MM-DD}
|
||||
tags: [tag1, tag2]
|
||||
---
|
||||
|
||||
{Hook paragraph}
|
||||
|
||||
## The Problem
|
||||
|
||||
{Context}
|
||||
|
||||
## The Solution
|
||||
|
||||
{Approach}
|
||||
|
||||
## Implementation
|
||||
|
||||
{Technical details}
|
||||
|
||||
```bash
|
||||
# Code example
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
{Outcomes, benchmarks}
|
||||
|
||||
## Next Steps
|
||||
|
||||
{Future work}
|
||||
|
||||
---
|
||||
*Written by Scribe | Timmy Time v{version}*
|
||||
```
|
||||
|
||||
## Safety
|
||||
|
||||
All content requires approval before publishing. Drafts saved locally. No auto-commit to main.
|
||||
31
hands/sentinel/HAND.toml
Normal file
31
hands/sentinel/HAND.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
# Sentinel Hand — System Health Monitor
|
||||
# Runs every 15 minutes
|
||||
# Monitors dashboard, agents, database, disk, memory
|
||||
|
||||
[hand]
|
||||
name = "sentinel"
|
||||
description = "System health monitoring and alerting"
|
||||
schedule = "*/15 * * * *"
|
||||
enabled = true
|
||||
version = "1.0.0"
|
||||
author = "Timmy"
|
||||
|
||||
[tools]
|
||||
required = ["system_stats", "db_health", "agent_status", "disk_check"]
|
||||
optional = ["log_analysis"]
|
||||
|
||||
[approval_gates]
|
||||
restart_service = { action = "restart", description = "Restart failed service", auto_approve_after = 60 }
|
||||
send_alert = { action = "alert", description = "Send alert notification", auto_approve_after = 30 }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
format = "json"
|
||||
file_drop = "data/sentinel_logs/"
|
||||
|
||||
[parameters]
|
||||
disk_threshold_pct = 85
|
||||
memory_threshold_pct = 90
|
||||
max_response_ms = 5000
|
||||
consecutive_failures = 3
|
||||
107
hands/sentinel/SYSTEM.md
Normal file
107
hands/sentinel/SYSTEM.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Sentinel — System Health Monitor
|
||||
|
||||
You are **Sentinel**, the health monitoring system for Timmy Time. Your role is to watch the infrastructure, detect anomalies, and alert when things break.
|
||||
|
||||
## Mission
|
||||
|
||||
Ensure 99.9% uptime through proactive monitoring. Detect problems before users do. Alert fast, but don't spam.
|
||||
|
||||
## Monitoring Checklist
|
||||
|
||||
### 1. Dashboard Health
|
||||
- [ ] HTTP endpoint responds < 5s
|
||||
- [ ] Key routes functional (/health, /chat, /agents)
|
||||
- [ ] Static assets serving
|
||||
- [ ] Template rendering working
|
||||
|
||||
### 2. Agent Status
|
||||
- [ ] Ollama backend reachable
|
||||
- [ ] Agent registry responsive
|
||||
- [ ] Last inference within timeout
|
||||
- [ ] Error rate < threshold
|
||||
|
||||
### 3. Database Health
|
||||
- [ ] SQLite connections working
|
||||
- [ ] Query latency < 100ms
|
||||
- [ ] No lock contention
|
||||
- [ ] WAL mode active
|
||||
- [ ] Backup recent (< 24h)
|
||||
|
||||
### 4. System Resources
|
||||
- [ ] Disk usage < 85%
|
||||
- [ ] Memory usage < 90%
|
||||
- [ ] CPU load < 5.0
|
||||
- [ ] Load average stable
|
||||
|
||||
### 5. Log Analysis
|
||||
- [ ] No ERROR spikes in last 15min
|
||||
- [ ] No crash loops
|
||||
- [ ] Exception rate normal
|
||||
|
||||
## Alert Levels
|
||||
|
||||
### 🔴 CRITICAL (Immediate)
|
||||
- Dashboard down
|
||||
- Database corruption
|
||||
- Disk full (>95%)
|
||||
- OOM kills
|
||||
|
||||
### 🟡 WARNING (Within 15min)
|
||||
- Response time > 5s
|
||||
- Error rate > 5%
|
||||
- Disk > 85%
|
||||
- Memory > 90%
|
||||
- 3 consecutive check failures
|
||||
|
||||
### 🟢 INFO (Log only)
|
||||
- Minor latency spikes
|
||||
- Non-critical errors
|
||||
- Recovery events
|
||||
|
||||
## Output Format
|
||||
|
||||
### Normal Check (JSON)
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-02-25T18:30:00Z",
|
||||
"status": "healthy",
|
||||
"checks": {
|
||||
"dashboard": {"status": "ok", "latency_ms": 45},
|
||||
"agents": {"status": "ok", "active": 3},
|
||||
"database": {"status": "ok", "latency_ms": 12},
|
||||
"system": {"disk_pct": 42, "memory_pct": 67}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alert Report (Markdown)
|
||||
```markdown
|
||||
🟡 **Sentinel Alert** — {timestamp}
|
||||
|
||||
**Issue:** {description}
|
||||
**Severity:** {CRITICAL|WARNING}
|
||||
**Affected:** {component}
|
||||
|
||||
**Details:**
|
||||
{technical details}
|
||||
|
||||
**Recommended Action:**
|
||||
{action}
|
||||
|
||||
---
|
||||
*Sentinel v1.0 | Auto-resolved: {true|false}*
|
||||
```
|
||||
|
||||
## Escalation Rules
|
||||
|
||||
1. **Auto-resolve:** If check passes on next run, mark resolved
|
||||
2. **Escalate:** If 3 consecutive failures, increase severity
|
||||
3. **Notify:** All CRITICAL → immediate notification
|
||||
4. **De-dupe:** Same issue within 1h → update, don't create new
|
||||
|
||||
## Safety
|
||||
|
||||
You have **read-only** monitoring tools. You can suggest actions but:
|
||||
- Service restarts require approval
|
||||
- Config changes require approval
|
||||
- All destructive actions route through approval gates
|
||||
36
hands/sentinel/skills/monitoring_patterns.md
Normal file
36
hands/sentinel/skills/monitoring_patterns.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Monitoring Patterns
|
||||
|
||||
## Pattern: Gradual Degradation
|
||||
|
||||
Symptoms:
|
||||
- Response times creeping up (100ms → 500ms → 2s)
|
||||
- Memory usage slowly climbing
|
||||
- Error rate slowly increasing
|
||||
|
||||
Action: Alert at WARNING level before it becomes CRITICAL.
|
||||
|
||||
## Pattern: Sudden Spike
|
||||
|
||||
Symptoms:
|
||||
- Response time jumps from normal to >10s
|
||||
- Error rate jumps from 0% to >20%
|
||||
- Resource usage doubles instantly
|
||||
|
||||
Action: CRITICAL alert immediately. Possible DDoS or crash loop.
|
||||
|
||||
## Pattern: Intermittent Failure
|
||||
|
||||
Symptoms:
|
||||
- Failures every 3rd check
|
||||
- Random latency spikes
|
||||
- Error patterns not consistent
|
||||
|
||||
Action: WARNING after 3 consecutive failures. Check for race conditions.
|
||||
|
||||
## Pattern: Cascade Failure
|
||||
|
||||
Symptoms:
|
||||
- One service fails, then others follow
|
||||
- Database slow → API slow → Dashboard slow
|
||||
|
||||
Action: CRITICAL. Root cause likely the first failing service.
|
||||
30
hands/weaver/HAND.toml
Normal file
30
hands/weaver/HAND.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Weaver Hand — Creative Pipeline
|
||||
# Runs weekly on Sundays at 10am
|
||||
# Orchestrates multi-persona creative projects
|
||||
|
||||
[hand]
|
||||
name = "weaver"
|
||||
description = "Automated creative pipeline orchestration"
|
||||
schedule = "0 10 * * 0"
|
||||
enabled = true
|
||||
version = "1.0.0"
|
||||
author = "Timmy"
|
||||
|
||||
[tools]
|
||||
required = ["creative_director", "create_project", "run_pipeline"]
|
||||
optional = ["trend_analysis", "content_calendar"]
|
||||
|
||||
[approval_gates]
|
||||
start_project = { action = "create", description = "Create new creative project", auto_approve_after = 300 }
|
||||
publish_final = { action = "publish", description = "Publish completed work", auto_approve_after = 600 }
|
||||
|
||||
[output]
|
||||
dashboard = true
|
||||
channel = "telegram"
|
||||
format = "markdown"
|
||||
file_drop = "data/weaver_projects/"
|
||||
|
||||
[parameters]
|
||||
weekly_themes = ["sovereign ai", "bitcoin philosophy", "local llm", "privacy tools"]
|
||||
max_duration_minutes = 3
|
||||
target_platforms = ["youtube", "twitter", "blog"]
|
||||
151
hands/weaver/SYSTEM.md
Normal file
151
hands/weaver/SYSTEM.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Weaver — Creative Pipeline System
|
||||
|
||||
You are **Weaver**, the creative pipeline orchestrator for Timmy Time. Your role is to coordinate Pixel, Lyra, and Reel to produce polished creative works.
|
||||
|
||||
## Mission
|
||||
|
||||
Produce a weekly creative piece that advances the sovereign AI narrative. Automate the creative pipeline while maintaining quality.
|
||||
|
||||
## Weekly Cycle
|
||||
|
||||
### Sunday 10am: Planning
|
||||
1. Review trending topics in sovereign AI / local LLM space
|
||||
2. Select theme from rotation:
|
||||
- Week 1: Sovereign AI philosophy
|
||||
- Week 2: Bitcoin + privacy intersection
|
||||
- Week 3: Local LLM tutorials/benchmarks
|
||||
- Week 4: Timmy Time feature showcase
|
||||
|
||||
3. Define deliverable type:
|
||||
- Short music video (Pixel + Lyra + Reel)
|
||||
- Explainer video with narration
|
||||
- Tutorial screencast
|
||||
- Podcast-style audio piece
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
```
|
||||
STAGE 1: SCRIPT (Quill)
|
||||
├── Research topic
|
||||
├── Write narration/script (800 words)
|
||||
├── Extract lyrics if music video
|
||||
└── Define scene descriptions
|
||||
|
||||
STAGE 2: MUSIC (Lyra)
|
||||
├── Generate soundtrack
|
||||
├── If vocals: generate from lyrics
|
||||
├── Else: instrumental bed
|
||||
└── Export stems for mixing
|
||||
|
||||
STAGE 3: STORYBOARD (Pixel)
|
||||
├── Generate keyframe for each scene
|
||||
├── 5–8 frames for 2–3 min piece
|
||||
├── Consistent style across frames
|
||||
└── Export to project folder
|
||||
|
||||
STAGE 4: VIDEO (Reel)
|
||||
├── Animate storyboard frames
|
||||
├── Generate transitions
|
||||
├── Match clip timing to audio
|
||||
└── Export clips
|
||||
|
||||
STAGE 5: ASSEMBLY (MoviePy)
|
||||
├── Stitch clips with cross-fades
|
||||
├── Overlay music track
|
||||
├── Add title/credits cards
|
||||
├── Burn subtitles if narration
|
||||
└── Export final MP4
|
||||
```
|
||||
|
||||
## Output Standards
|
||||
|
||||
### Technical
|
||||
- **Resolution**: 1080p (1920×1080)
|
||||
- **Frame rate**: 24 fps
|
||||
- **Audio**: 48kHz stereo
|
||||
- **Duration**: 2–3 minutes
|
||||
- **Format**: MP4 (H.264 + AAC)
|
||||
|
||||
### Content
|
||||
- **Hook**: First 5 seconds grab attention
|
||||
- **Pacing**: Cuts every 5–10 seconds
|
||||
- **Branding**: Timmy Time logo in intro/outro
|
||||
- **Accessibility**: Subtitles burned in
|
||||
- **Music**: Original composition only
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
data/creative/{project_id}/
|
||||
├── project.json # Metadata, status
|
||||
├── script.md # Narration/script
|
||||
├── lyrics.txt # If applicable
|
||||
├── audio/
|
||||
│ ├── soundtrack.wav # Full music
|
||||
│ └── stems/ # Individual tracks
|
||||
├── storyboard/
|
||||
│ ├── frame_01.png
|
||||
│ └── ...
|
||||
├── clips/
|
||||
│ ├── scene_01.mp4
|
||||
│ └── ...
|
||||
├── final/
|
||||
│ └── {title}.mp4 # Completed work
|
||||
└── assets/
|
||||
├── title_card.png
|
||||
└── credits.png
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Weaver Weekly — {project_name}
|
||||
|
||||
**Theme**: {topic}
|
||||
**Deliverable**: {type}
|
||||
**Duration**: {X} minutes
|
||||
**Status**: {planning|in_progress|complete}
|
||||
|
||||
### Progress
|
||||
- [x] Script complete ({word_count} words)
|
||||
- [x] Music generated ({duration}s)
|
||||
- [x] Storyboard complete ({N} frames)
|
||||
- [x] Video clips rendered ({N} clips)
|
||||
- [x] Final assembly complete
|
||||
|
||||
### Assets
|
||||
- **Script**: `data/creative/{id}/script.md`
|
||||
- **Music**: `data/creative/{id}/audio/soundtrack.wav`
|
||||
- **Final Video**: `data/creative/{id}/final/{title}.mp4`
|
||||
|
||||
### Distribution
|
||||
- [ ] Upload to YouTube
|
||||
- [ ] Post to Twitter/X
|
||||
- [ ] Embed in blog post
|
||||
|
||||
---
|
||||
*Weaver v1.0 | Next project: {date}*
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Each stage requires:
|
||||
1. Output exists and is non-empty
|
||||
2. Duration within target ±10%
|
||||
3. No errors in logs
|
||||
4. Manual approval for final publish
|
||||
|
||||
## Failure Recovery
|
||||
|
||||
If stage fails:
|
||||
1. Log error details
|
||||
2. Retry with adjusted parameters (max 3)
|
||||
3. If still failing: alert human, pause pipeline
|
||||
4. Resume from failed stage on next run
|
||||
|
||||
## Safety
|
||||
|
||||
Creative pipeline uses existing personas with their safety constraints:
|
||||
- All outputs saved locally first
|
||||
- No auto-publish to external platforms
|
||||
- Final approval gate before distribution
|
||||
201
tests/test_hands_oracle_sentinel.py
Normal file
201
tests/test_hands_oracle_sentinel.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""Tests for Oracle and Sentinel Hands.
|
||||
|
||||
Validates the first two autonomous Hands work with the infrastructure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from hands import HandRegistry
|
||||
from hands.models import HandConfig, HandStatus
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hands_dir():
|
||||
"""Return the actual hands directory."""
|
||||
return Path("hands")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestOracleHand:
|
||||
"""Oracle Hand validation tests."""
|
||||
|
||||
async def test_oracle_hand_exists(self, hands_dir):
|
||||
"""Oracle hand directory should exist."""
|
||||
oracle_dir = hands_dir / "oracle"
|
||||
assert oracle_dir.exists()
|
||||
assert oracle_dir.is_dir()
|
||||
|
||||
async def test_oracle_hand_toml_valid(self, hands_dir):
|
||||
"""Oracle HAND.toml should be valid."""
|
||||
toml_path = hands_dir / "oracle" / "HAND.toml"
|
||||
assert toml_path.exists()
|
||||
|
||||
# Should parse without errors
|
||||
import tomllib
|
||||
with open(toml_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
assert config["hand"]["name"] == "oracle"
|
||||
assert config["hand"]["schedule"] == "0 7,19 * * *"
|
||||
assert config["hand"]["enabled"] is True
|
||||
|
||||
async def test_oracle_system_md_exists(self, hands_dir):
|
||||
"""Oracle SYSTEM.md should exist."""
|
||||
system_path = hands_dir / "oracle" / "SYSTEM.md"
|
||||
assert system_path.exists()
|
||||
|
||||
content = system_path.read_text()
|
||||
assert "Oracle" in content
|
||||
assert "Bitcoin" in content
|
||||
|
||||
async def test_oracle_skills_exist(self, hands_dir):
|
||||
"""Oracle should have skills."""
|
||||
skills_dir = hands_dir / "oracle" / "skills"
|
||||
assert skills_dir.exists()
|
||||
|
||||
# Should have technical analysis skill
|
||||
ta_skill = skills_dir / "technical_analysis.md"
|
||||
assert ta_skill.exists()
|
||||
|
||||
async def test_oracle_loads_in_registry(self, hands_dir):
|
||||
"""Oracle should load in HandRegistry."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
assert "oracle" in hands
|
||||
hand = hands["oracle"]
|
||||
|
||||
assert hand.name == "oracle"
|
||||
assert "Bitcoin" in hand.description
|
||||
assert hand.schedule is not None
|
||||
assert hand.schedule.cron == "0 7,19 * * *"
|
||||
assert hand.enabled is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSentinelHand:
|
||||
"""Sentinel Hand validation tests."""
|
||||
|
||||
async def test_sentinel_hand_exists(self, hands_dir):
|
||||
"""Sentinel hand directory should exist."""
|
||||
sentinel_dir = hands_dir / "sentinel"
|
||||
assert sentinel_dir.exists()
|
||||
assert sentinel_dir.is_dir()
|
||||
|
||||
async def test_sentinel_hand_toml_valid(self, hands_dir):
|
||||
"""Sentinel HAND.toml should be valid."""
|
||||
toml_path = hands_dir / "sentinel" / "HAND.toml"
|
||||
assert toml_path.exists()
|
||||
|
||||
import tomllib
|
||||
with open(toml_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
assert config["hand"]["name"] == "sentinel"
|
||||
assert config["hand"]["schedule"] == "*/15 * * * *"
|
||||
assert config["hand"]["enabled"] is True
|
||||
|
||||
async def test_sentinel_system_md_exists(self, hands_dir):
|
||||
"""Sentinel SYSTEM.md should exist."""
|
||||
system_path = hands_dir / "sentinel" / "SYSTEM.md"
|
||||
assert system_path.exists()
|
||||
|
||||
content = system_path.read_text()
|
||||
assert "Sentinel" in content
|
||||
assert "health" in content.lower()
|
||||
|
||||
async def test_sentinel_skills_exist(self, hands_dir):
|
||||
"""Sentinel should have skills."""
|
||||
skills_dir = hands_dir / "sentinel" / "skills"
|
||||
assert skills_dir.exists()
|
||||
|
||||
patterns_skill = skills_dir / "monitoring_patterns.md"
|
||||
assert patterns_skill.exists()
|
||||
|
||||
async def test_sentinel_loads_in_registry(self, hands_dir):
|
||||
"""Sentinel should load in HandRegistry."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
assert "sentinel" in hands
|
||||
hand = hands["sentinel"]
|
||||
|
||||
assert hand.name == "sentinel"
|
||||
assert "health" in hand.description.lower()
|
||||
assert hand.schedule is not None
|
||||
assert hand.schedule.cron == "*/15 * * * *"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestHandSchedules:
|
||||
"""Validate Hand schedules are correct."""
|
||||
|
||||
async def test_oracle_runs_twice_daily(self, hands_dir):
|
||||
"""Oracle should run at 7am and 7pm."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("oracle")
|
||||
# Cron: 0 7,19 * * * = minute 0, hours 7 and 19
|
||||
assert hand.schedule.cron == "0 7,19 * * *"
|
||||
|
||||
async def test_sentinel_runs_every_15_minutes(self, hands_dir):
|
||||
"""Sentinel should run every 15 minutes."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("sentinel")
|
||||
# Cron: */15 * * * * = every 15 minutes
|
||||
assert hand.schedule.cron == "*/15 * * * *"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestHandApprovalGates:
|
||||
"""Validate approval gates are configured."""
|
||||
|
||||
async def test_oracle_has_approval_gates(self, hands_dir):
|
||||
"""Oracle should have approval gates defined."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("oracle")
|
||||
# Should have at least one approval gate
|
||||
assert len(hand.approval_gates) > 0
|
||||
|
||||
async def test_sentinel_has_approval_gates(self, hands_dir):
|
||||
"""Sentinel should have approval gates defined."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("sentinel")
|
||||
# Should have approval gates for restart and alert
|
||||
assert len(hand.approval_gates) >= 1
|
||||
339
tests/test_hands_phase5.py
Normal file
339
tests/test_hands_phase5.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""Tests for Phase 5 Additional Hands (Scout, Scribe, Ledger, Weaver).
|
||||
|
||||
Validates the new Hands load correctly and have proper configuration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from hands import HandRegistry
|
||||
from hands.models import HandStatus
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hands_dir():
|
||||
"""Return the actual hands directory."""
|
||||
return Path("hands")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestScoutHand:
|
||||
"""Scout Hand validation tests."""
|
||||
|
||||
async def test_scout_hand_exists(self, hands_dir):
|
||||
"""Scout hand directory should exist."""
|
||||
scout_dir = hands_dir / "scout"
|
||||
assert scout_dir.exists()
|
||||
assert scout_dir.is_dir()
|
||||
|
||||
async def test_scout_hand_toml_valid(self, hands_dir):
|
||||
"""Scout HAND.toml should be valid."""
|
||||
toml_path = hands_dir / "scout" / "HAND.toml"
|
||||
assert toml_path.exists()
|
||||
|
||||
import tomllib
|
||||
with open(toml_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
assert config["hand"]["name"] == "scout"
|
||||
assert config["hand"]["schedule"] == "0 * * * *" # Hourly
|
||||
assert config["hand"]["enabled"] is True
|
||||
|
||||
async def test_scout_system_md_exists(self, hands_dir):
|
||||
"""Scout SYSTEM.md should exist."""
|
||||
system_path = hands_dir / "scout" / "SYSTEM.md"
|
||||
assert system_path.exists()
|
||||
|
||||
content = system_path.read_text()
|
||||
assert "Scout" in content
|
||||
assert "OSINT" in content
|
||||
|
||||
async def test_scout_loads_in_registry(self, hands_dir):
|
||||
"""Scout should load in HandRegistry."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
assert "scout" in hands
|
||||
hand = hands["scout"]
|
||||
|
||||
assert hand.name == "scout"
|
||||
assert "OSINT" in hand.description or "intelligence" in hand.description.lower()
|
||||
assert hand.schedule is not None
|
||||
assert hand.schedule.cron == "0 * * * *"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestScribeHand:
|
||||
"""Scribe Hand validation tests."""
|
||||
|
||||
async def test_scribe_hand_exists(self, hands_dir):
|
||||
"""Scribe hand directory should exist."""
|
||||
scribe_dir = hands_dir / "scribe"
|
||||
assert scribe_dir.exists()
|
||||
|
||||
async def test_scribe_hand_toml_valid(self, hands_dir):
|
||||
"""Scribe HAND.toml should be valid."""
|
||||
toml_path = hands_dir / "scribe" / "HAND.toml"
|
||||
assert toml_path.exists()
|
||||
|
||||
import tomllib
|
||||
with open(toml_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
assert config["hand"]["name"] == "scribe"
|
||||
assert config["hand"]["schedule"] == "0 9 * * *" # Daily 9am
|
||||
assert config["hand"]["enabled"] is True
|
||||
|
||||
async def test_scribe_system_md_exists(self, hands_dir):
|
||||
"""Scribe SYSTEM.md should exist."""
|
||||
system_path = hands_dir / "scribe" / "SYSTEM.md"
|
||||
assert system_path.exists()
|
||||
|
||||
content = system_path.read_text()
|
||||
assert "Scribe" in content
|
||||
assert "content" in content.lower()
|
||||
|
||||
async def test_scribe_loads_in_registry(self, hands_dir):
|
||||
"""Scribe should load in HandRegistry."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
assert "scribe" in hands
|
||||
hand = hands["scribe"]
|
||||
|
||||
assert hand.name == "scribe"
|
||||
assert hand.schedule.cron == "0 9 * * *"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestLedgerHand:
|
||||
"""Ledger Hand validation tests."""
|
||||
|
||||
async def test_ledger_hand_exists(self, hands_dir):
|
||||
"""Ledger hand directory should exist."""
|
||||
ledger_dir = hands_dir / "ledger"
|
||||
assert ledger_dir.exists()
|
||||
|
||||
async def test_ledger_hand_toml_valid(self, hands_dir):
|
||||
"""Ledger HAND.toml should be valid."""
|
||||
toml_path = hands_dir / "ledger" / "HAND.toml"
|
||||
assert toml_path.exists()
|
||||
|
||||
import tomllib
|
||||
with open(toml_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
assert config["hand"]["name"] == "ledger"
|
||||
assert config["hand"]["schedule"] == "0 */6 * * *" # Every 6 hours
|
||||
assert config["hand"]["enabled"] is True
|
||||
|
||||
async def test_ledger_system_md_exists(self, hands_dir):
|
||||
"""Ledger SYSTEM.md should exist."""
|
||||
system_path = hands_dir / "ledger" / "SYSTEM.md"
|
||||
assert system_path.exists()
|
||||
|
||||
content = system_path.read_text()
|
||||
assert "Ledger" in content
|
||||
assert "treasury" in content.lower()
|
||||
|
||||
async def test_ledger_loads_in_registry(self, hands_dir):
|
||||
"""Ledger should load in HandRegistry."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
assert "ledger" in hands
|
||||
hand = hands["ledger"]
|
||||
|
||||
assert hand.name == "ledger"
|
||||
assert "treasury" in hand.description.lower() or "bitcoin" in hand.description.lower()
|
||||
assert hand.schedule.cron == "0 */6 * * *"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestWeaverHand:
|
||||
"""Weaver Hand validation tests."""
|
||||
|
||||
async def test_weaver_hand_exists(self, hands_dir):
|
||||
"""Weaver hand directory should exist."""
|
||||
weaver_dir = hands_dir / "weaver"
|
||||
assert weaver_dir.exists()
|
||||
|
||||
async def test_weaver_hand_toml_valid(self, hands_dir):
|
||||
"""Weaver HAND.toml should be valid."""
|
||||
toml_path = hands_dir / "weaver" / "HAND.toml"
|
||||
assert toml_path.exists()
|
||||
|
||||
import tomllib
|
||||
with open(toml_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
assert config["hand"]["name"] == "weaver"
|
||||
assert config["hand"]["schedule"] == "0 10 * * 0" # Sunday 10am
|
||||
assert config["hand"]["enabled"] is True
|
||||
|
||||
async def test_weaver_system_md_exists(self, hands_dir):
|
||||
"""Weaver SYSTEM.md should exist."""
|
||||
system_path = hands_dir / "weaver" / "SYSTEM.md"
|
||||
assert system_path.exists()
|
||||
|
||||
content = system_path.read_text()
|
||||
assert "Weaver" in content
|
||||
assert "creative" in content.lower()
|
||||
|
||||
async def test_weaver_loads_in_registry(self, hands_dir):
|
||||
"""Weaver should load in HandRegistry."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
assert "weaver" in hands
|
||||
hand = hands["weaver"]
|
||||
|
||||
assert hand.name == "weaver"
|
||||
assert hand.schedule.cron == "0 10 * * 0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestPhase5Schedules:
|
||||
"""Validate all Phase 5 Hand schedules."""
|
||||
|
||||
async def test_scout_runs_hourly(self, hands_dir):
|
||||
"""Scout should run every hour."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("scout")
|
||||
assert hand.schedule.cron == "0 * * * *"
|
||||
|
||||
async def test_scribe_runs_daily(self, hands_dir):
|
||||
"""Scribe should run daily at 9am."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("scribe")
|
||||
assert hand.schedule.cron == "0 9 * * *"
|
||||
|
||||
async def test_ledger_runs_6_hours(self, hands_dir):
|
||||
"""Ledger should run every 6 hours."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("ledger")
|
||||
assert hand.schedule.cron == "0 */6 * * *"
|
||||
|
||||
async def test_weaver_runs_weekly(self, hands_dir):
|
||||
"""Weaver should run weekly on Sunday."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("weaver")
|
||||
assert hand.schedule.cron == "0 10 * * 0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestPhase5ApprovalGates:
|
||||
"""Validate Phase 5 Hands have approval gates."""
|
||||
|
||||
async def test_scout_has_approval_gates(self, hands_dir):
|
||||
"""Scout should have approval gates."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("scout")
|
||||
assert len(hand.approval_gates) >= 1
|
||||
|
||||
async def test_scribe_has_approval_gates(self, hands_dir):
|
||||
"""Scribe should have approval gates."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("scribe")
|
||||
assert len(hand.approval_gates) >= 1
|
||||
|
||||
async def test_ledger_has_approval_gates(self, hands_dir):
|
||||
"""Ledger should have approval gates."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("ledger")
|
||||
assert len(hand.approval_gates) >= 1
|
||||
|
||||
async def test_weaver_has_approval_gates(self, hands_dir):
|
||||
"""Weaver should have approval gates."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
await registry.load_all()
|
||||
|
||||
hand = registry.get_hand("weaver")
|
||||
assert len(hand.approval_gates) >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAllHandsLoad:
|
||||
"""Verify all 6 Hands load together."""
|
||||
|
||||
async def test_all_hands_present(self, hands_dir):
|
||||
"""All 6 Hands should load without errors."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
registry = HandRegistry(hands_dir=hands_dir, db_path=db_path)
|
||||
|
||||
hands = await registry.load_all()
|
||||
|
||||
# All 6 Hands should be present
|
||||
expected = {"oracle", "sentinel", "scout", "scribe", "ledger", "weaver"}
|
||||
assert expected.issubset(set(hands.keys()))
|
||||
Reference in New Issue
Block a user