feat: add Workshop config inventory generator (#320)
Adds a script that scans ~/.timmy/ and generates WORKSHOP_INVENTORY.md documenting every config file, env var, model route, and setting with annotations on who set each one (alex-set, hermes-set, default, timmy-chose). Timmy can run this to audit his Workshop and make each setting his own.
This commit is contained in:
254
scripts/generate_workshop_inventory.py
Normal file
254
scripts/generate_workshop_inventory.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user