docs: update CLAUDE.md for Stage 6 daemon infrastructure
- Current Status: add Stage 6 daemon infrastructure in progress - Architecture table: expand daemon/__init__.py stub to all 5 daemon modules - Code Inventory: add daemon.core, daemon.pid, daemon.ipc, daemon.service sections with function signatures and purposes - Internal classes: add PluginSupervisor and PidFile; expand DaemonConfig Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@ a plugin/integration system (Stage 2+) and an encrypted vault (Stage 3+).
|
||||
## Current Status
|
||||
|
||||
**Stage 3 — Memory Database: complete** (2026-05-18)
|
||||
Next: Stage 4 — Vault Encryption
|
||||
**Stage 6 — Daemon infrastructure: in progress** (`feat/daemon` branch)
|
||||
Next: Stage 4 — Vault Encryption (skipped for now); messaging bots (Stage 6 remainder)
|
||||
|
||||
## Project Roadmap
|
||||
|
||||
@@ -19,11 +20,11 @@ memory in `~/.pyra/memory/`, and hard security boundaries around the vault.
|
||||
### Stage 2 — Plugin Framework ✅ COMPLETE
|
||||
- `src/pyra/plugins/` package: `base.py`, `loader.py`, `registry.py`, `executor.py`, `install.py`
|
||||
- `src/pyra/bundled_plugins/` — ships bundled plugin scripts with pyra
|
||||
- `src/pyra/daemon/` stub (CLI surface only)
|
||||
- `src/pyra/daemon/` stub (CLI surface only; daemon itself is Stage 6)
|
||||
- Config: `PluginConfig` + `DaemonConfig` added to `PyraConfig`
|
||||
- Bootstrap: `~/.pyra/plugins/` and `~/.pyra/logs/` created on startup
|
||||
- Chat session: AI tool-use loop (up to 10 iterations), approval gate, plugin slash commands
|
||||
- CLI: `pyra plugin list/install/enable/disable/setup`, `pyra daemon *` stubs
|
||||
- CLI: `pyra plugin list/install/enable/disable/setup`, `pyra daemon *` (stubs at Stage 2; implemented in Stage 6)
|
||||
|
||||
### Stage 3 — Memory Database ✅ COMPLETE
|
||||
- `src/pyra/memory/database.py`: SQLite + FTS5 via `memory_meta` + `memory_fts` tables
|
||||
@@ -117,7 +118,11 @@ the vault under namespaced keys (`plugin:{name}:{key}`).
|
||||
| `plugins/executor.py` | Approval gate: scan args → prompt → execute → scan result → log |
|
||||
| `plugins/install.py` | Copies bundled plugins to `~/.pyra/plugins/` |
|
||||
| `bundled_plugins/` | Standalone plugin scripts shipped with pyra (installed on demand) |
|
||||
| `daemon/__init__.py` | Daemon package stub (implementation in Stage 2.4) |
|
||||
| `daemon/pid.py` | Atomic PID file — write, read, stale detection (POSIX + Windows), context manager |
|
||||
| `daemon/ipc.py` | IPC transport — Unix socket chmod 600 + UID-check (Linux/macOS) or TCP loopback + port file (Windows); newline-delimited JSON protocol |
|
||||
| `daemon/service.py` | OS service file generation + install/uninstall — launchd plist (macOS), systemd user unit (Linux), schtasks XML (Windows) |
|
||||
| `daemon/core.py` | asyncio event loop entry point, `PluginSupervisor` (per-task restart, max 10×, 5s back-off, reload), IPC command dispatch, signal handling |
|
||||
| `daemon/__init__.py` | Public daemon API exports |
|
||||
|
||||
### Runtime: `~/.pyra/`
|
||||
|
||||
@@ -493,6 +498,40 @@ Dataclass: `MemoryFile(name, path, category, size_bytes, modified)`
|
||||
| `list_bundled_plugins` | `(bundled_dir: Path) -> list[str]` | Names of all bundled plugins that have a `manifest.json` |
|
||||
| `read_manifest` | `(plugin_dir: Path) -> dict` | Reads `manifest.json`; returns `{}` if missing |
|
||||
|
||||
#### `daemon.core`
|
||||
|
||||
| Function | Signature | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| `run_foreground` | `() -> None` | Entry point for `pyra daemon run` — loads config + plugins, writes PID file, runs asyncio loop |
|
||||
| `start_background` | `() -> None` | Spawns `pyra daemon run` as a detached subprocess (`start_new_session` on POSIX, `DETACHED_PROCESS` on Windows) |
|
||||
|
||||
#### `daemon.pid`
|
||||
|
||||
| Function | Signature | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| `resolve_pid_path` | `(cfg_path: str) -> Path` | Expand `~` and resolve to absolute Path |
|
||||
|
||||
#### `daemon.ipc`
|
||||
|
||||
| Function | Signature | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| `send_command` | `(address, msg, timeout=5.0) -> IpcResponse` | Synchronous CLI helper — `asyncio.run(IpcClient.send(...))` |
|
||||
| `get_socket_path` | `(cfg: str) -> Path` | Expand `~` and return Unix socket path |
|
||||
| `is_unix_socket` | `() -> bool` | True on Linux/macOS (`sys.platform != 'nt'`) |
|
||||
| `get_port_file_path` | `() -> Path` | Path to `~/.pyra/daemon.port` (Windows TCP port file) |
|
||||
|
||||
#### `daemon.service`
|
||||
|
||||
| Function | Signature | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| `detect_platform` | `() -> Literal["macos","linux","windows"]` | Detect current OS |
|
||||
| `find_pyra_executable` | `() -> str` | `shutil.which("pyra")` → sibling fallback → `sys.executable -m pyra` |
|
||||
| `install_service` | `() -> None` | Generate + register OS service (reads config for log/pid paths) |
|
||||
| `uninstall_service` | `() -> None` | Deregister OS service |
|
||||
| `render_launchd_plist` | `(exe, log_file, pid_file) -> str` | macOS plist template |
|
||||
| `render_systemd_unit` | `(exe, log_file) -> str` | Linux systemd unit template |
|
||||
| `render_schtasks_xml` | `(exe) -> str` | Windows Task Scheduler XML template (write as UTF-16) |
|
||||
|
||||
#### `chat.renderer` — rendering functions and shared `console`
|
||||
|
||||
Import `console` from here; do not create a second `rich.Console()` in new code.
|
||||
@@ -515,7 +554,7 @@ Import `console` from here; do not create a second `rich.Console()` in new code.
|
||||
| `GeneralConfig` | `config.schema` | `general:` block — `user_name`, `assistant_name` |
|
||||
| `ProviderConfig` | `config.schema` | `ai:` block — `provider_id`, `model`, `base_url` |
|
||||
| `PluginConfig` | `config.schema` | `plugins:` block — `enabled`, `require_approval`, `log_executions` |
|
||||
| `DaemonConfig` | `config.schema` | `daemon:` block |
|
||||
| `DaemonConfig` | `config.schema` | `daemon:` block — `enabled`, `socket_path`, `log_file`, `pid_file`, `ipc_port` |
|
||||
| `MemoryConfig` | `config.schema` | `memory:` block — `max_tokens_in_context`, `auto_load` |
|
||||
| `SecurityConfig` | `config.schema` | `security:` block — `injection_detection`, `log_injections` |
|
||||
| `ConversationHistory` | `chat.history` | Holds message list; builds API payload via `build_for_api()`; trims to token budget |
|
||||
@@ -526,3 +565,5 @@ Import `console` from here; do not create a second `rich.Console()` in new code.
|
||||
| `PyraPlugin` | `plugins.base` | `@runtime_checkable` Protocol — the plugin interface |
|
||||
| `BasePlugin` | `plugins.base` | Concrete base with no-op defaults; plugins should inherit this |
|
||||
| `TaskPlanner` | `chat.planner` | Multi-step plan runner; `make_tool_handler()` returns the callable wired into the chat session; presents plan for user approval, executes each step via litellm with up to 5 tool-use iterations, verifies output before proceeding |
|
||||
| `PluginSupervisor` | `daemon.core` | asyncio supervisor — `add_task(name, factory)`, `start()`, `stop()`, `reload()`, `status()`; restarts crashed tasks up to 10× with 5s back-off |
|
||||
| `PidFile` | `daemon.pid` | `write()` (atomic), `read()`, `is_stale()`, `remove()`, context manager; `PidFileError(OSError)` raised when live PID already exists |
|
||||
|
||||
Reference in New Issue
Block a user