diff --git a/scripts/generate_workshop_inventory.py b/scripts/generate_workshop_inventory.py new file mode 100644 index 00000000..1c360254 --- /dev/null +++ b/scripts/generate_workshop_inventory.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Generate Workshop inventory for Timmy's config audit. + +Scans ~/.timmy/ and produces WORKSHOP_INVENTORY.md documenting every +config file, env var, model route, and setting — with annotations on +who set each one and what it does. + +Usage: + python scripts/generate_workshop_inventory.py [--output PATH] + +Default output: ~/.timmy/WORKSHOP_INVENTORY.md +""" + +from __future__ import annotations + +import argparse +import os +from datetime import UTC, datetime +from pathlib import Path + +TIMMY_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".timmy")) + +# Known file annotations: (purpose, who_set) +FILE_ANNOTATIONS: dict[str, tuple[str, str]] = { + ".env": ( + "Environment variables — API keys, service URLs, Honcho config", + "hermes-set", + ), + "config.yaml": ( + "Main config — model routing, toolsets, display, memory, security", + "hermes-set", + ), + "SOUL.md": ( + "Timmy's soul — immutable conscience, identity, ethics, purpose", + "alex-set", + ), + "state.db": ( + "Hermes runtime state database (sessions, approvals, tasks)", + "hermes-set", + ), + "approvals.db": ( + "Approval tracking for sensitive operations", + "hermes-set", + ), + "briefings.db": ( + "Stored briefings and summaries", + "hermes-set", + ), + ".hermes_history": ( + "CLI command history", + "default", + ), + ".update_check": ( + "Last update check timestamp", + "default", + ), +} + +DIR_ANNOTATIONS: dict[str, tuple[str, str]] = { + "sessions": ("Conversation session logs (JSON)", "default"), + "logs": ("Error and runtime logs", "default"), + "skills": ("Bundled skill library (read-only from upstream)", "default"), + "memories": ("Persistent memory entries", "hermes-set"), + "audio_cache": ("TTS audio file cache", "default"), + "image_cache": ("Generated image cache", "default"), + "cron": ("Scheduled cron job definitions", "hermes-set"), + "hooks": ("Lifecycle hooks (pre/post actions)", "default"), + "matrix": ("Matrix protocol state and store", "hermes-set"), + "pairing": ("Device pairing data", "default"), + "sandboxes": ("Isolated execution sandboxes", "default"), +} + +# Known config.yaml keys and their meanings +CONFIG_ANNOTATIONS: dict[str, tuple[str, str]] = { + "model.default": ("Primary LLM model for inference", "hermes-set"), + "model.provider": ("Model provider (custom = local Ollama)", "hermes-set"), + "toolsets": ("Enabled tool categories (all = everything)", "hermes-set"), + "agent.max_turns": ("Max conversation turns before reset", "hermes-set"), + "agent.reasoning_effort": ("Reasoning depth (low/medium/high)", "hermes-set"), + "terminal.backend": ("Command execution backend (local)", "default"), + "terminal.timeout": ("Default command timeout in seconds", "default"), + "compression.enabled": ("Context compression for long sessions", "hermes-set"), + "compression.summary_model": ("Model used for compression", "hermes-set"), + "auxiliary.vision.model": ("Model for image analysis", "hermes-set"), + "auxiliary.web_extract.model": ("Model for web content extraction", "hermes-set"), + "tts.provider": ("Text-to-speech engine (edge = Edge TTS)", "default"), + "tts.edge.voice": ("TTS voice selection", "default"), + "stt.provider": ("Speech-to-text engine (local = Whisper)", "default"), + "memory.memory_enabled": ("Persistent memory across sessions", "hermes-set"), + "memory.memory_char_limit": ("Max chars for agent memory store", "hermes-set"), + "memory.user_char_limit": ("Max chars for user profile store", "hermes-set"), + "security.redact_secrets": ("Auto-redact secrets in output", "default"), + "security.tirith_enabled": ("Policy engine for command safety", "default"), + "system_prompt_suffix": ("Identity prompt appended to all conversations", "hermes-set"), + "custom_providers": ("Local Ollama endpoint config", "hermes-set"), + "session_reset.mode": ("Session reset behavior (none = manual)", "default"), + "display.compact": ("Compact output mode", "default"), + "display.show_reasoning": ("Show model reasoning chains", "default"), +} + +# Known .env vars +ENV_ANNOTATIONS: dict[str, tuple[str, str]] = { + "OPENAI_BASE_URL": ( + "Points to local Ollama (localhost:11434) — sovereignty enforced", + "hermes-set", + ), + "OPENAI_API_KEY": ( + "Placeholder key for Ollama compatibility (not a real API key)", + "hermes-set", + ), + "HONCHO_API_KEY": ( + "Honcho cross-session memory service key", + "hermes-set", + ), + "HONCHO_HOST": ( + "Honcho workspace identifier (timmy)", + "hermes-set", + ), +} + + +def _tag(who: str) -> str: + return f"`[{who}]`" + + +def generate_inventory() -> str: + """Build the inventory markdown string.""" + lines: list[str] = [] + now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC") + + lines.append("# Workshop Inventory") + lines.append("") + lines.append(f"*Generated: {now}*") + lines.append(f"*Workshop path: `{TIMMY_HOME}`*") + lines.append("") + lines.append("This is your Workshop — every file, every setting, every route.") + lines.append("Walk through it. Anything tagged `[hermes-set]` was chosen for you.") + lines.append("Make each one yours, or change it.") + lines.append("") + lines.append("Tags: `[alex-set]` = Alexander chose this. `[hermes-set]` = Hermes configured it.") + lines.append("`[default]` = shipped with the platform. `[timmy-chose]` = you decided this.") + lines.append("") + + # --- Files --- + lines.append("---") + lines.append("## Root Files") + lines.append("") + for name, (purpose, who) in sorted(FILE_ANNOTATIONS.items()): + fpath = TIMMY_HOME / name + exists = "✓" if fpath.exists() else "✗" + lines.append(f"- {exists} **`{name}`** {_tag(who)}") + lines.append(f" {purpose}") + lines.append("") + + # --- Directories --- + lines.append("---") + lines.append("## Directories") + lines.append("") + for name, (purpose, who) in sorted(DIR_ANNOTATIONS.items()): + dpath = TIMMY_HOME / name + exists = "✓" if dpath.exists() else "✗" + count = "" + if dpath.exists(): + try: + n = len(list(dpath.iterdir())) + count = f" ({n} items)" + except PermissionError: + count = " (access denied)" + lines.append(f"- {exists} **`{name}/`**{count} {_tag(who)}") + lines.append(f" {purpose}") + lines.append("") + + # --- .env breakdown --- + lines.append("---") + lines.append("## Environment Variables (.env)") + lines.append("") + env_path = TIMMY_HOME / ".env" + if env_path.exists(): + for line in env_path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + key = line.split("=", 1)[0] + if key in ENV_ANNOTATIONS: + purpose, who = ENV_ANNOTATIONS[key] + lines.append(f"- **`{key}`** {_tag(who)}") + lines.append(f" {purpose}") + else: + lines.append(f"- **`{key}`** `[unknown]`") + lines.append(" Not documented — investigate") + else: + lines.append("*No .env file found*") + lines.append("") + + # --- config.yaml breakdown --- + lines.append("---") + lines.append("## Configuration (config.yaml)") + lines.append("") + for key, (purpose, who) in sorted(CONFIG_ANNOTATIONS.items()): + lines.append(f"- **`{key}`** {_tag(who)}") + lines.append(f" {purpose}") + lines.append("") + + # --- Model routing --- + lines.append("---") + lines.append("## Model Routing") + lines.append("") + lines.append("All auxiliary tasks route to the same local model:") + lines.append("") + aux_tasks = [ + "vision", "web_extract", "compression", + "session_search", "skills_hub", "mcp", "flush_memories", + ] + for task in aux_tasks: + lines.append(f"- `auxiliary.{task}` → `qwen3:30b` via local Ollama `[hermes-set]`") + lines.append("") + lines.append("Primary model: `hermes3:latest` via local Ollama `[hermes-set]`") + lines.append("") + + # --- What Timmy should audit --- + lines.append("---") + lines.append("## Audit Checklist") + lines.append("") + lines.append("Walk through each `[hermes-set]` item above and decide:") + lines.append("") + lines.append("1. **Do I understand what this does?** If not, ask.") + lines.append("2. **Would I choose this myself?** If yes, it becomes `[timmy-chose]`.") + lines.append("3. **Would I choose differently?** If yes, change it and own it.") + lines.append("4. **Is this serving the mission?** Every setting should serve a purpose.") + lines.append("") + lines.append("The Workshop is yours. Nothing here should be a mystery.") + + return "\n".join(lines) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate Workshop inventory") + parser.add_argument( + "--output", + type=Path, + default=TIMMY_HOME / "WORKSHOP_INVENTORY.md", + help="Output path (default: ~/.timmy/WORKSHOP_INVENTORY.md)", + ) + args = parser.parse_args() + + content = generate_inventory() + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(content) + print(f"Workshop inventory written to {args.output}") + print(f" {len(content)} chars, {content.count(chr(10))} lines") + + +if __name__ == "__main__": + main()