#!/usr/bin/env bash # wiki-maintain.sh — Agent Estate Wiki maintenance entrypoint # Run daily at 03:00 AWST via cron (wiki-stocktake-maintenance job) # # Does three things: # 1. Regenerates systems/skills-index.md from live ~/.hermes/skills/ tree # 2. Audits EVERY page for staleness (>21 days since `updated` frontmatter) # 3. Rebuilds Quartz static site into public/ set -euo pipefail WIKI_DIR="/home/hermes/workspace/agent-estate-wiki" SKILLS_DIR="$HOME/.hermes/skills" STOCKTAKE_DIR="$WIKI_DIR/raw/stocktake" QUARTZ_CONTENT_DIR="$WIKI_DIR/public" TODAY=$(date +%Y%m%d) cd "$WIKI_DIR" echo "[wiki-maintain] Starting maintenance at $(date -Iseconds)" echo "" # ============================================================================= # [1/3] Regenerate skills-index.md from live skill tree # ============================================================================= echo "[1/3] Regenerating systems/skills-index.md from $SKILLS_DIR..." python3 << 'PYTHON_SCRIPT' import sys from pathlib import Path from datetime import date import subprocess skills_dir = Path("/home/hermes/.hermes/skills") output = Path("/home/hermes/workspace/agent-estate-wiki/systems/skills-index.md") # Use find to get all SKILL.md files, then group by their immediate parent category # This handles nested structures like context-engineering-collection/skills/foo/SKILL.md result = subprocess.run( ["find", str(skills_dir), "-name", "SKILL.md", "-type", "f"], capture_output=True, text=True ) skill_files = [Path(p) for p in result.stdout.strip().split('\n') if p] # Group by the top-level category (first directory under skills_dir) categories = {} for skill_md in skill_files: # Get relative path from skills_dir try: rel = skill_md.relative_to(skills_dir) except ValueError: continue # Skip if not under skills_dir (e.g., broken symlinks) # Top-level category is the first part of the path category = rel.parts[0] skill_name = skill_md.parent.name # The directory containing SKILL.md # Read the description from the skill file try: content = skill_md.read_text(errors='ignore')[:2000] lines = content.split('\n') desc = "" in_frontmatter = False for line in lines: if line.strip() == '---': in_frontmatter = not in_frontmatter continue if not in_frontmatter and line.strip(): desc = line.strip() break if len(desc) > 200: desc = desc[:197] + "..." except Exception as e: desc = f"[error reading: {e}]" if category not in categories: categories[category] = [] # Avoid duplicates (same skill_name in same category) if not any(name == skill_name for name, _ in categories[category]): categories[category].append((skill_name, desc)) # Sort skills within each category for cat in categories: categories[cat] = sorted(categories[cat], key=lambda x: x[0]) # Build the markdown today = date.today().isoformat() lines = [ "---", "title: Installed Skills", "type: system", "status: active", "created: 2026-07-22", f"updated: {today}", f"verified_on: {today}", "confidence: high", "tags: [skills, hermes]", "sources: []", "---", "", "# Installed Skills", "", f"**Total installed: {sum(len(v) for v in categories.values())} skills** in `~/.hermes/skills/`.", "", "This page is auto-regenerated from the live skill inventory. Do not hand-edit.", "", "## Stock-take notes", "- Category = directory structure under `~/.hermes/skills/` (`category/skill/SKILL.md`).", "- Skills directly under `~/.hermes/skills/` group under `uncategorized`.", "", ] # Write categories (sorted, excluding 'uncategorized' which goes last) for cat_name in sorted(categories.keys()): skills = categories[cat_name] # Format category name (capitalize first letter of each word) cat_display = cat_name.replace('-', ' ').title() lines.append(f"## {cat_display} ({len(skills)})") lines.append("") for skill_name, desc in skills: # Format skill name skill_display = skill_name.replace('-', ' ') if desc: lines.append(f"- `{skill_name}` — {desc}") else: lines.append(f"- `{skill_name}` — (no description)") lines.append("") output.write_text('\n'.join(lines)) print(f" Written {output} with {sum(len(v) for v in categories.values())} skills in {len(categories)} categories") PYTHON_SCRIPT echo "" # ============================================================================= # [2/3] Freshness audit — check every page's `updated` frontmatter # ============================================================================= REPORT="$STOCKTAKE_DIR/freshness-$TODAY.txt" echo "[2/3] Auditing wiki freshness (threshold: >21 days)..." python3 "$STOCKTAKE_DIR/_freshness_audit.py" "$WIKI_DIR" "$REPORT" 21 echo "" # ============================================================================= # [3/3] Rebuild Quartz static site # ============================================================================= echo "[3/3] Checking Quartz build..." # Quartz is already built into public/ — if there's a build script, run it # For now, the public/ dir is the Quartz output, regenerated when content changes # If Quartz CLI is available, we'd run: npx quartz build if command -v npx &> /dev/null && [ -f "$WIKI_DIR/quartz.config.ts" ]; then echo " Running Quartz build..." cd "$WIKI_DIR" npx quartz build 2>&1 | head -20 || echo " Quartz build had issues (non-fatal)" else echo " Quartz build skipped (no quartz.config.ts or npx not available)" echo " public/ directory contains the current static site" fi echo "" echo "[wiki-maintain] Maintenance complete at $(date -Iseconds)" echo " Freshness report: $REPORT" echo "" # Print summary head -5 "$REPORT" tail -3 "$REPORT"