Wiki maintenance 2026-09-11: regenerate skills-index (339 skills), add freshness report, create wiki-maintain.sh automation script

This commit is contained in:
Tony0410
2026-09-11 03:05:21 +08:00
parent ae5724f657
commit 17283295d3
3 changed files with 785 additions and 390 deletions
+167
View File
@@ -0,0 +1,167 @@
#!/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"