Files
Aetheel/docs/project/features-guide.md
tanmay11k 82c2640481 feat: openclaw-style secrets (env.vars + \) and per-task model routing
- Replace python-dotenv with config.json env.vars block + \ substitution
- Add models section for per-task model routing (heartbeat, subagent, default)
- Heartbeat/subagent tasks can use different models/providers than main chat
- Remove python-dotenv from dependencies
- Update all docs to reflect new config approach
- Reorganize docs into project/ and research/ subdirectories
2026-02-20 23:49:05 -05:00

980 lines
24 KiB
Markdown

# Aetheel Features Guide
Complete reference for all Aetheel features, how to use them, and how to test them.
---
## Table of Contents
1. [Dual AI Runtimes](#1-dual-ai-runtimes)
2. [Tool Enablement & MCP Servers](#2-tool-enablement--mcp-servers)
3. [Multi-Channel Adapters](#3-multi-channel-adapters)
4. [WebChat Browser Interface](#4-webchat-browser-interface)
5. [Persistent Memory System](#5-persistent-memory-system)
6. [Skills System](#6-skills-system)
7. [Scheduler & Action Tags](#7-scheduler--action-tags)
8. [Heartbeat / Proactive System](#8-heartbeat--proactive-system)
9. [Subagents & Agent-to-Agent Communication](#9-subagents--agent-to-agent-communication)
10. [Self-Modification](#10-self-modification)
11. [Lifecycle Hooks](#11-lifecycle-hooks)
12. [Webhooks (External Event Receiver)](#12-webhooks-external-event-receiver)
13. [CLI Interface](#13-cli-interface)
14. [Configuration](#14-configuration)
15. [Running Tests](#15-running-tests)
13. [Running Tests](#13-running-tests)
---
## 1. Dual AI Runtimes
Aetheel supports two AI backends that share the same `AgentResponse` interface. Switch between them with a single flag.
### OpenCode (default)
Uses the [OpenCode](https://opencode.ai) CLI. Supports CLI mode (subprocess per request) and SDK mode (persistent server connection).
```bash
# CLI mode (default)
python main.py
# SDK mode (requires `opencode serve` running)
python main.py --sdk
# Custom model
python main.py --model anthropic/claude-sonnet-4-20250514
```
### OpenCode Advanced Features
Aetheel exposes several OpenCode CLI features via chat commands and config:
| Feature | Chat Command | Config Key |
|---------|-------------|------------|
| Agent selection | `agents` (list), config to set | `runtime.agent` |
| Attach to server | — | `runtime.attach` |
| Model discovery | `models`, `models <provider>` | — |
| Usage stats | `stats`, `stats <days>` | — |
| File attachments | Passed from chat adapters | — |
| Session forking | Internal (subagent branching) | — |
| Session titles | Auto-set from first message | — |
Setting `runtime.attach` to a running `opencode serve` URL (e.g. `"http://localhost:4096"`) makes CLI mode attach to that server instead of spawning a fresh process per request. This avoids MCP server cold boot times and is significantly faster.
```json
{
"runtime": {
"agent": "researcher",
"attach": "http://localhost:4096"
}
}
```
### Claude Code
Uses the [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI with native `--system-prompt` support.
```bash
python main.py --claude
python main.py --claude --model claude-sonnet-4-20250514
```
### Key differences
| Feature | OpenCode | Claude Code |
|---------|----------|-------------|
| System prompt | XML-injected into message | Native `--system-prompt` flag |
| Session continuity | `--continue --session <id>` | `--continue --session-id <id>` |
| Tool access | Enabled by default | Controlled via `--allowedTools` |
| Output format | JSONL events | JSON result object |
### How to test
```bash
# Echo mode (no AI runtime needed)
python main.py --test
# Verify runtime detection
python cli.py doctor
```
---
## 2. Tool Enablement & MCP Servers
### Tool access
Claude Code runtime now has tools enabled by default. The `allowed_tools` list controls which tools the agent can use:
```
Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch,
Task, TaskOutput, TaskStop, Skill, TeamCreate, TeamDelete, SendMessage
```
To disable tools (pure conversation mode), set in `~/.aetheel/config.json`:
```json
{
"claude": {
"no_tools": true
}
}
```
To customize the tool list:
```json
{
"claude": {
"no_tools": false,
"allowed_tools": ["Bash", "Read", "Write", "WebSearch"]
}
}
```
### MCP server configuration
Add external tool servers via config. Aetheel writes the appropriate config file (`.mcp.json` for Claude, `opencode.json` for OpenCode) to the workspace before launching the runtime.
```json
{
"mcp": {
"servers": {
"my-tool": {
"command": "uvx",
"args": ["my-mcp-server@latest"],
"env": { "API_KEY": "..." }
}
}
}
}
```
### How to test
```bash
# Run MCP config writer tests
cd Aetheel
python -m pytest tests/test_mcp_config.py -v
```
---
## 3. Multi-Channel Adapters
Aetheel connects to messaging platforms via adapters. Each adapter converts platform events into a channel-agnostic `IncomingMessage` and routes responses back.
### Slack (default)
Requires `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` in `config.json``env.vars`. Starts automatically when tokens are present.
```bash
python main.py
```
### Telegram
```bash
# Set TELEGRAM_BOT_TOKEN in config.json env.vars first
python main.py --telegram
```
### Discord
```bash
# Set DISCORD_BOT_TOKEN in config.json env.vars first
python main.py --discord
```
### Multiple adapters
```bash
python main.py --telegram --discord --webchat
```
When multiple adapters run, all but the last start in background threads. The last one runs blocking.
### How to test
```bash
python -m pytest tests/test_base_adapter.py -v
```
---
## 4. WebChat Browser Interface
A browser-based chat UI served via aiohttp with WebSocket support. No Slack/Discord/Telegram needed.
### Starting WebChat
```bash
# Via CLI flag
python main.py --webchat
# Or enable in config permanently
# Set "webchat": {"enabled": true} in ~/.aetheel/config.json
```
Then open `http://127.0.0.1:8080` in your browser.
### Configuration
```json
{
"webchat": {
"enabled": false,
"port": 8080,
"host": "127.0.0.1"
}
}
```
### Features
- Dark-themed chat UI at `GET /`
- WebSocket endpoint at `/ws` for real-time messaging
- Per-connection session isolation (each browser tab gets its own conversation)
- Auto-reconnect on disconnect (3 second delay)
- Basic markdown rendering (bold, code blocks, bullet points)
- Connection status indicator (green/red/gray)
### Architecture
```
Browser <--> WebSocket (/ws) <--> WebChatAdapter <--> ai_handler <--> Runtime
|
GET / serves static/chat.html
```
### How to test
```bash
# Start with echo handler + webchat
python main.py --test --webchat
# Open http://127.0.0.1:8080 and send a message
# You should see the echo response with message metadata
```
---
## 5. Persistent Memory System
Aetheel maintains persistent memory across sessions using local embeddings and SQLite.
### Identity files
Located in `~/.aetheel/workspace/`:
| File | Purpose |
|------|---------|
| `SOUL.md` | Personality, values, communication style |
| `USER.md` | User preferences, context, background |
| `MEMORY.md` | Long-term notes, facts to remember |
Edit these files directly. Changes are picked up automatically via file watching.
### How it works
1. All `.md` files in the workspace are chunked and embedded using `fastembed` (BAAI/bge-small-en-v1.5, 384-dim, runs locally)
2. Chunks are stored in SQLite with FTS5 full-text search
3. On each message, Aetheel searches memory using hybrid scoring (0.7 vector + 0.3 BM25)
4. Relevant results are injected into the system prompt as context
5. Conversations are logged to `daily/YYYY-MM-DD.md`
### CLI memory commands
```bash
# Search memory
python cli.py memory search "python projects"
# Force re-index
python cli.py memory sync
```
---
## 6. Skills System
Skills are markdown files that teach the agent how to handle specific types of requests. They're loaded at startup and injected into the system prompt when trigger words match.
### Creating a skill
Create `~/.aetheel/workspace/skills/<name>/SKILL.md`:
```markdown
---
name: weather
description: Check weather for any city
triggers: [weather, forecast, temperature, rain]
---
# Weather Skill
When the user asks about weather, use web search to find current conditions...
```
### How it works
1. Skills are discovered from `~/.aetheel/workspace/skills/*/SKILL.md`
2. YAML frontmatter defines `name`, `description`, and `triggers`
3. When a message contains a trigger word, the skill's body is injected into the system prompt
4. A summary of all available skills is always included
### Hot reload
Send `reload` or `/reload` in chat to reload skills without restarting.
### How to test
```bash
python -m pytest tests/test_skills.py -v
```
---
## 7. Scheduler & Action Tags
APScheduler-based system with SQLite persistence for one-shot and recurring jobs.
### Action tags
The AI can include action tags in its responses. The system strips them from the visible reply and executes the action.
| Tag | Effect |
|-----|--------|
| `[ACTION:remind\|5\|Drink water!]` | Sends "Drink water!" to the channel in 5 minutes |
| `[ACTION:cron\|0 9 * * *\|Good morning!]` | Sends "Good morning!" every day at 9 AM |
| `[ACTION:spawn\|Research Python 3.14]` | Spawns a background subagent for the task |
### Managing jobs
In chat:
- `/cron list` — list all scheduled jobs
- `/cron remove <id>` — remove a job
Via CLI:
```bash
python cli.py cron list
python cli.py cron remove <job_id>
```
### How to test
```bash
# Scheduler tests require apscheduler installed
python -m pytest tests/test_scheduler.py -v
```
---
## 8. Heartbeat / Proactive System
The heartbeat system runs periodic tasks automatically by parsing a user-editable `HEARTBEAT.md` file.
### How it works
1. At startup, `HeartbeatRunner` reads `~/.aetheel/workspace/HEARTBEAT.md`
2. Each `## ` heading defines a schedule (natural language)
3. Bullet points under each heading are task prompts
4. Tasks are registered as cron jobs with the Scheduler
5. When a task fires, it creates a synthetic message routed through `ai_handler`
### HEARTBEAT.md format
```markdown
# Heartbeat Tasks
## Every 30 minutes
- Check if any scheduled reminders need attention
- Review recent session logs for anything worth remembering
## Every morning (9:00 AM)
- Summarize yesterday's conversations
- Check for any pending follow-ups in MEMORY.md
## Every evening (6:00 PM)
- Update MEMORY.md with today's key learnings
```
### Supported schedule patterns
| Pattern | Cron equivalent |
|---------|----------------|
| `Every 30 minutes` | `*/30 * * * *` |
| `Every hour` | `0 * * * *` |
| `Every 2 hours` | `0 */2 * * *` |
| `Every morning (9:00 AM)` | `0 9 * * *` |
| `Every evening (6:00 PM)` | `0 18 * * *` |
### Configuration
```json
{
"heartbeat": {
"enabled": true,
"default_channel": "slack",
"default_channel_id": "C123456",
"silent": false
}
}
```
Set `enabled` to `false` to disable heartbeat entirely. If `HEARTBEAT.md` doesn't exist, a default one is created automatically.
### Model routing for heartbeat
Heartbeat tasks can use a cheaper/local model to save costs. Configure in the `models` section:
```json
{
"models": {
"heartbeat": {
"engine": "opencode",
"model": "ollama/llama3.2",
"provider": "ollama"
}
}
}
```
When set, heartbeat jobs use a dedicated runtime instance with the specified model instead of the global default. Regular chat messages are unaffected.
### How to test
```bash
# Verify heartbeat parsing works
python -c "
from heartbeat.heartbeat import HeartbeatRunner
print(HeartbeatRunner._parse_schedule_header('Every 30 minutes')) # */30 * * * *
print(HeartbeatRunner._parse_schedule_header('Every morning (9:00 AM)')) # 0 9 * * *
print(HeartbeatRunner._parse_schedule_header('Every evening (6:00 PM)')) # 0 18 * * *
"
```
---
## 9. Subagents & Agent-to-Agent Communication
### Subagent spawning
The AI can spawn background tasks that run in separate threads with their own runtime instances.
```
[ACTION:spawn|Research the latest Python 3.14 features and summarize them]
```
The subagent runs asynchronously and sends results back to the originating channel when done.
### Managing subagents
In chat:
- `/subagents` — list active subagent tasks with IDs and status
### SubagentBus
A thread-safe pub/sub message bus for inter-subagent communication:
```python
from agent.subagent import SubagentManager
mgr = SubagentManager(runtime_factory=..., send_fn=...)
# Subscribe to a channel
mgr.bus.subscribe("results", lambda msg, sender: print(f"{sender}: {msg}"))
# Publish from a subagent
mgr.bus.publish("results", "Task complete!", "subagent-abc123")
```
### Claude Code team tools
When using Claude Code runtime, the agent has access to team coordination tools:
- `TeamCreate`, `TeamDelete` — create/delete agent teams
- `SendMessage` — send messages between agents in a team
- `Task`, `TaskOutput`, `TaskStop` — spawn and manage subagent tasks
### How to test
```bash
python -m pytest tests/test_subagent_bus.py -v
```
---
## 10. Self-Modification
The AI agent knows it can modify its own files. The system prompt tells it about:
- `~/.aetheel/config.json` — edit configuration
- `~/.aetheel/workspace/skills/<name>/SKILL.md` — create new skills
- `~/.aetheel/workspace/SOUL.md` — update personality
- `~/.aetheel/workspace/USER.md` — update user profile
- `~/.aetheel/workspace/MEMORY.md` — update long-term memory
- `~/.aetheel/workspace/HEARTBEAT.md` — modify periodic tasks
### Hot reload
After the agent edits config or skills, send `reload` or `/reload` in chat to apply changes without restarting:
```
You: /reload
Aetheel: 🔄 Config and skills reloaded.
```
### How to test
```bash
# Verify the system prompt contains self-modification instructions
python -c "
from agent.opencode_runtime import build_aetheel_system_prompt
prompt = build_aetheel_system_prompt()
assert 'Self-Modification' in prompt
assert 'config.json' in prompt
assert '/reload' in prompt
print('Self-modification prompt sections present ✅')
"
```
---
## 11. Lifecycle Hooks
Event-driven lifecycle hooks inspired by OpenClaw's internal hook system. Hooks fire on gateway/agent lifecycle events and let you run custom Python code at those moments.
### Supported events
| Event | When it fires |
|---|---|
| `gateway:startup` | Gateway process starts (after adapters connect) |
| `gateway:shutdown` | Gateway process is shutting down |
| `command:reload` | User sends `/reload` |
| `command:new` | User starts a fresh session |
| `agent:bootstrap` | Before workspace files are injected into context |
| `agent:response` | After the agent produces a response |
### Creating a hook
Create a directory in `~/.aetheel/workspace/hooks/<name>/` with two files:
`HOOK.md` — metadata in YAML frontmatter:
```markdown
---
name: session-logger
description: Log session starts to a file
events: [gateway:startup, command:reload]
enabled: true
---
# Session Logger Hook
Logs gateway lifecycle events for debugging.
```
`handler.py` — Python handler with a `handle(event)` function:
```python
def handle(event):
"""Called when a matching event fires."""
print(f"Hook fired: {event.event_key}")
# Push messages back to the user
event.messages.append("Hook executed!")
```
### Hook discovery locations
Hooks are discovered from two directories (in order):
1. `~/.aetheel/workspace/hooks/<name>/HOOK.md` — workspace hooks (per-project)
2. `~/.aetheel/hooks/<name>/HOOK.md` — managed hooks (shared across workspaces)
### Programmatic hooks
You can also register hooks in Python code:
```python
from hooks import HookManager, HookEvent
mgr = HookManager(workspace_dir="~/.aetheel/workspace")
mgr.register("gateway:startup", lambda e: print("Gateway started!"))
mgr.trigger(HookEvent(type="gateway", action="startup"))
```
### Configuration
```json
{
"hooks": {
"enabled": true
}
}
```
Set `enabled` to `false` to disable all hook discovery and execution.
### How to test
```bash
python -m pytest tests/test_hooks.py -v
```
---
## 12. Webhooks (External Event Receiver)
HTTP endpoints that accept POST requests from external systems (GitHub, Jira, email services, custom scripts) and route them through the AI handler as synthetic messages. Inspired by OpenClaw's `/hooks/*` gateway endpoints.
### Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
| `/hooks/health` | GET | No | Health check |
| `/hooks/wake` | POST | Yes | Wake the agent with a text prompt |
| `/hooks/agent` | POST | Yes | Send a message to a specific agent session |
### Enabling webhooks
```json
{
"webhooks": {
"enabled": true,
"port": 8090,
"host": "127.0.0.1",
"token": "your-secret-token"
}
}
```
The webhook server starts automatically when `webhooks.enabled` is `true`.
### POST /hooks/wake
Wake the agent with a text prompt. The agent processes it and returns the response.
```bash
curl -X POST http://127.0.0.1:8090/hooks/wake \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"text": "Check my email for urgent items"}'
```
Response:
```json
{
"status": "ok",
"response": "I checked your inbox and found 2 urgent items..."
}
```
Optionally deliver the response to a messaging channel:
```bash
curl -X POST http://127.0.0.1:8090/hooks/wake \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{
"text": "Summarize today'\''s calendar",
"channel": "slack",
"channel_id": "C123456"
}'
```
### POST /hooks/agent
Send a message to a specific agent session with channel delivery:
```bash
curl -X POST http://127.0.0.1:8090/hooks/agent \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{
"message": "New GitHub issue: Fix login bug #42",
"channel": "slack",
"channel_id": "C123456",
"sender": "GitHub"
}'
```
### Use cases
- GitHub webhook → POST to `/hooks/agent` → agent triages the issue
- Email service → POST to `/hooks/wake` → agent summarizes new emails
- Cron script → POST to `/hooks/wake` → agent runs a daily report
- IoT sensor → POST to `/hooks/agent` → agent alerts on anomalies
### Authentication
All POST endpoints require a bearer token. Pass it via:
- `Authorization: Bearer <token>` header
- `?token=<token>` query parameter (fallback)
If no token is configured (`"token": ""`), endpoints are open (dev mode only).
### How to test
```bash
python -m pytest tests/test_webhooks.py -v
```
---
## 13. CLI Interface
Aetheel includes a Click-based CLI with subcommands for all major operations.
### Installation
After installing with `pip install -e .` or `uv sync`, the `aetheel` command is available:
```bash
aetheel # Same as `aetheel start`
aetheel start # Start with default adapters
aetheel --help # Show all commands
```
Or run directly:
```bash
python cli.py start --discord --webchat
python cli.py chat "What is Python?"
python cli.py doctor
```
### Commands
| Command | Description |
|---------|-------------|
| `aetheel` / `aetheel start` | Start with configured adapters |
| `aetheel start --discord` | Start with Discord adapter |
| `aetheel start --telegram` | Start with Telegram adapter |
| `aetheel start --webchat` | Start with WebChat adapter |
| `aetheel start --claude` | Use Claude Code runtime |
| `aetheel start --test` | Echo handler (no AI) |
| `aetheel chat "message"` | One-shot AI query (prints to stdout) |
| `aetheel status` | Show runtime status |
| `aetheel doctor` | Run diagnostics (check runtimes, tokens, workspace) |
| `aetheel config show` | Print current config.json |
| `aetheel config edit` | Open config in $EDITOR |
| `aetheel config init` | Reset config to defaults |
| `aetheel cron list` | List scheduled jobs |
| `aetheel cron remove <id>` | Remove a scheduled job |
| `aetheel memory search "query"` | Search memory |
| `aetheel memory sync` | Force memory re-index |
### How to test
```bash
# Verify CLI structure
python cli.py --help
python cli.py start --help
python cli.py config --help
python cli.py cron --help
python cli.py memory --help
# Run diagnostics
python cli.py doctor
```
---
## 14. Configuration
All configuration lives in `~/.aetheel/config.json`, including secrets (in the `env.vars` block).
### Config hierarchy (highest priority wins)
1. CLI arguments (`--model`, `--claude`, etc.)
2. Process environment variables
3. `env.vars` block in config.json
4. `${VAR}` substitution in config values
5. `~/.aetheel/config.json` static values
6. Dataclass defaults
### Full config.json example
```json
{
"env": {
"vars": {
"SLACK_BOT_TOKEN": "xoxb-...",
"SLACK_APP_TOKEN": "xapp-...",
"TELEGRAM_BOT_TOKEN": "",
"DISCORD_BOT_TOKEN": "",
"ANTHROPIC_API_KEY": ""
}
},
"log_level": "INFO",
"runtime": {
"mode": "cli",
"model": null,
"timeout_seconds": 120,
"server_url": "http://localhost:4096",
"format": "json",
"agent": null,
"attach": null
},
"claude": {
"model": null,
"timeout_seconds": 120,
"max_turns": 3,
"no_tools": false,
"allowed_tools": [
"Bash", "Read", "Write", "Edit", "Glob", "Grep",
"WebSearch", "WebFetch",
"Task", "TaskOutput", "TaskStop", "Skill",
"TeamCreate", "TeamDelete", "SendMessage"
]
},
"slack": {
"enabled": true,
"bot_token": "${SLACK_BOT_TOKEN}",
"app_token": "${SLACK_APP_TOKEN}"
},
"telegram": {
"enabled": false,
"bot_token": "${TELEGRAM_BOT_TOKEN}"
},
"discord": {
"enabled": false,
"bot_token": "${DISCORD_BOT_TOKEN}",
"listen_channels": []
},
"memory": {
"workspace": "~/.aetheel/workspace",
"db_path": "~/.aetheel/memory.db"
},
"scheduler": {
"db_path": "~/.aetheel/scheduler.db"
},
"heartbeat": {
"enabled": true,
"default_channel": "slack",
"default_channel_id": "",
"silent": false
},
"webchat": {
"enabled": false,
"port": 8080,
"host": "127.0.0.1"
},
"mcp": {
"servers": {}
},
"models": {
"heartbeat": null,
"subagent": null,
"default": null
},
"hooks": {
"enabled": true
},
"webhooks": {
"enabled": false,
"port": 8090,
"host": "127.0.0.1",
"token": ""
}
}
```
### Process environment variable overrides
Process env vars still override everything. Useful for CI, Docker, or systemd:
```bash
OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
CLAUDE_MODEL=claude-sonnet-4-20250514
LOG_LEVEL=DEBUG
```
---
## 15. Running Tests
### Prerequisites
```bash
cd Aetheel
pip install -e ".[test]"
# or
uv sync --extra test
```
### Run all tests
```bash
python -m pytest tests/ -v --ignore=tests/test_scheduler.py
```
> Note: `test_scheduler.py` requires `apscheduler` installed. If you have it, run the full suite:
> ```bash
> python -m pytest tests/ -v
> ```
### Run specific test files
```bash
# Base adapter tests
python -m pytest tests/test_base_adapter.py -v
# Skills system tests
python -m pytest tests/test_skills.py -v
# MCP config writer tests
python -m pytest tests/test_mcp_config.py -v
# SubagentBus pub/sub tests
python -m pytest tests/test_subagent_bus.py -v
# Scheduler tests (requires apscheduler)
python -m pytest tests/test_scheduler.py -v
# Hook system tests
python -m pytest tests/test_hooks.py -v
# Webhook receiver tests
python -m pytest tests/test_webhooks.py -v
```
### Test summary
| Test file | What it covers | Count |
|-----------|---------------|-------|
| `test_base_adapter.py` | BaseAdapter dispatch, handler registration, error handling | 9 |
| `test_skills.py` | Skill loading, trigger matching, frontmatter parsing, context building | 21 |
| `test_mcp_config.py` | MCP config writer (Claude/OpenCode formats, round-trip, edge cases) | 8 |
| `test_subagent_bus.py` | SubagentBus subscribe/publish, isolation, error resilience, thread safety | 10 + 2 |
| `test_hooks.py` | Hook discovery, trigger, programmatic hooks, error resilience, messages | 14 |
| `test_webhooks.py` | Webhook endpoints (wake, agent, health), auth, channel delivery | 10 |
| `test_scheduler.py` | Scheduler one-shot/cron jobs, persistence, removal | varies |
### Quick smoke tests
```bash
# Verify config loads correctly
python -c "from config import load_config; c = load_config(); print(f'Tools enabled: {not c.claude.no_tools}, Tools: {len(c.claude.allowed_tools)}')"
# Verify system prompt has new sections
python -c "
from agent.opencode_runtime import build_aetheel_system_prompt
p = build_aetheel_system_prompt()
for section in ['Your Tools', 'Self-Modification', 'Subagents & Teams']:
assert section in p, f'Missing: {section}'
print(f' ✅ {section}')
"
# Verify heartbeat parser
python -c "
from heartbeat.heartbeat import HeartbeatRunner
tests = [('Every 30 minutes', '*/30 * * * *'), ('Every morning (9:00 AM)', '0 9 * * *'), ('Every evening (6:00 PM)', '0 18 * * *')]
for header, expected in tests:
result = HeartbeatRunner._parse_schedule_header(header)
assert result == expected, f'{header}: got {result}, expected {expected}'
print(f' ✅ {header} -> {result}')
"
# Verify CLI commands exist
python cli.py --help
```