Skip to main content

scripts-repo-docs-updater

#!/usr/bin/env python3 """ Repository Documentation Updater - K.5.9

Auto-updates AGENTS.md, README.md, and other repository documentation with discovered components, keeping docs in sync with code.

Usage: python3 scripts/repo-docs-updater.py [--dry-run] python3 scripts/repo-docs-updater.py --target README.md

Track: K (Workflow Automation) Agent: repo-docs-updater Command: /repo-docs-update """

import argparse import json import re import subprocess import sys from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any

@dataclass class ComponentInfo: """Information about a discovered component.""" name: str path: str component_type: str description: str status: str

def run_command(cmd: list[str]) -> tuple[int, str, str]: """Run a shell command.""" try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) return result.returncode, result.stdout, result.stderr except Exception as e: return 1, "", str(e)

def discover_agents(agents_dir: Path) -> list[ComponentInfo]: """Discover agents from the agents directory.""" agents = []

for md_file in agents_dir.glob("*.md"):
if md_file.name in ("README.md", "INDEX.md"):
continue

content = md_file.read_text(errors="ignore")

# Extract frontmatter
name = md_file.stem
description = ""
status = "active"

# Try to parse YAML frontmatter
if content.startswith("---"):
end = content.find("---", 3)
if end > 0:
frontmatter = content[3:end]
for line in frontmatter.split("\n"):
if line.startswith("summary:"):
description = line.split(":", 1)[1].strip().strip("'\"")
elif line.startswith("description:"):
description = description or line.split(":", 1)[1].strip().strip("'\"")
elif line.startswith("status:"):
status = line.split(":", 1)[1].strip()

# Fallback: try to get description from first paragraph
if not description:
lines = content.split("\n")
for line in lines[5:20]: # Skip frontmatter area
if line.strip() and not line.startswith("#") and not line.startswith("-"):
description = line.strip()[:100]
break

agents.append(ComponentInfo(
name=name,
path=str(md_file.relative_to(Path.cwd())),
component_type="agent",
description=description[:100] + "..." if len(description) > 100 else description,
status=status
))

return sorted(agents, key=lambda a: a.name)

def discover_skills(skills_dir: Path) -> list[ComponentInfo]: """Discover skills from the skills directory.""" skills = []

for skill_md in skills_dir.glob("*/SKILL.md"):
content = skill_md.read_text(errors="ignore")

name = skill_md.parent.name
description = ""
status = "active"

# Parse frontmatter
if content.startswith("---"):
end = content.find("---", 3)
if end > 0:
frontmatter = content[3:end]
for line in frontmatter.split("\n"):
if line.startswith("description:"):
description = line.split(":", 1)[1].strip().strip("'\"")
elif line.startswith("status:"):
status = line.split(":", 1)[1].strip()

skills.append(ComponentInfo(
name=name,
path=str(skill_md.relative_to(Path.cwd())),
component_type="skill",
description=description[:100] + "..." if len(description) > 100 else description,
status=status
))

return sorted(skills, key=lambda s: s.name)

def discover_commands(commands_dir: Path) -> list[ComponentInfo]: """Discover commands from the commands directory.""" commands = []

for md_file in commands_dir.glob("*.md"):
if md_file.name in ("README.md", "INDEX.md", "GUIDE.md"):
continue

content = md_file.read_text(errors="ignore")

name = md_file.stem
description = ""
status = "active"

# Parse frontmatter
if content.startswith("---"):
end = content.find("---", 3)
if end > 0:
frontmatter = content[3:end]
for line in frontmatter.split("\n"):
if line.startswith("summary:"):
description = line.split(":", 1)[1].strip().strip("'\"")
elif line.startswith("status:"):
status = line.split(":", 1)[1].strip()

commands.append(ComponentInfo(
name=f"/{name}",
path=str(md_file.relative_to(Path.cwd())),
component_type="command",
description=description[:100] + "..." if len(description) > 100 else description,
status=status
))

return sorted(commands, key=lambda c: c.name)

def discover_scripts(scripts_dir: Path) -> list[ComponentInfo]: """Discover Python scripts.""" scripts = []

for py_file in scripts_dir.glob("*.py"):
if py_file.name.startswith("__"):
continue

content = py_file.read_text(errors="ignore")

name = py_file.stem
description = ""

# Extract docstring
match = re.search(r'^"""(.*?)"""', content, re.DOTALL)
if match:
docstring = match.group(1).strip()
# Get first line as description
description = docstring.split("\n")[0].strip()

scripts.append(ComponentInfo(
name=name,
path=str(py_file.relative_to(Path.cwd())),
component_type="script",
description=description[:100] + "..." if len(description) > 100 else description,
status="active"
))

return sorted(scripts, key=lambda s: s.name)

def generate_agents_md(agents: list[ComponentInfo]) -> str: """Generate AGENTS.md content.""" lines = [ "# CODITECT Agents", "", f"Total Agents: {len(agents)}", f"Last Updated: {datetime.now().strftime('%Y-%m-%d')}", "", "---", "", "## Agent Index", "", "| Agent | Description | Status |", "|-------|-------------|--------|", ]

for agent in agents:
status_emoji = "" if agent.status == "active" else "" if agent.status == "draft" else ""
lines.append(f"| [{agent.name}]({agent.path}) | {agent.description} | {status_emoji} {agent.status} |")

lines.extend([
"",
"---",
"",
"*Auto-generated by CODITECT Repo Docs Updater*",
])

return "\n".join(lines)

def generate_skills_md(skills: list[ComponentInfo]) -> str: """Generate SKILLS.md content.""" lines = [ "# CODITECT Skills", "", f"Total Skills: {len(skills)}", f"Last Updated: {datetime.now().strftime('%Y-%m-%d')}", "", "---", "", "## Skill Index", "", "| Skill | Description | Status |", "|-------|-------------|--------|", ]

for skill in skills:
status_emoji = "" if skill.status == "active" else ""
lines.append(f"| [{skill.name}]({skill.path}) | {skill.description} | {status_emoji} {skill.status} |")

lines.extend([
"",
"---",
"",
"*Auto-generated by CODITECT Repo Docs Updater*",
])

return "\n".join(lines)

def update_readme_counts(readme_path: Path, counts: dict[str, int]) -> str | None: """Update component counts in README.md.""" if not readme_path.exists(): return None

content = readme_path.read_text()
original = content

# Update patterns like "**Agents:** 150" or "| Agents | 150 |"
for component_type, count in counts.items():
# Pattern 1: **Type:** N
pattern1 = rf"\*\*{component_type.title()}s?:\*\*\s*\d+"
replacement1 = f"**{component_type.title()}s:** {count}"
content = re.sub(pattern1, replacement1, content, flags=re.IGNORECASE)

# Pattern 2: | Type | N |
pattern2 = rf"\|\s*{component_type.title()}s?\s*\|\s*\d+\s*\|"
replacement2 = f"| {component_type.title()}s | {count} |"
content = re.sub(pattern2, replacement2, content, flags=re.IGNORECASE)

if content != original:
return content
return None

def generate_report( agents: list[ComponentInfo], skills: list[ComponentInfo], commands: list[ComponentInfo], scripts: list[ComponentInfo], updates: list[str], dry_run: bool ) -> str: """Generate update report.""" lines = [ "# Repository Documentation Update Report", "", f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", f"Mode: {'Dry Run' if dry_run else 'Applied'}", "", "---", "", "## Component Discovery", "", "| Type | Count | Active | Draft |", "|------|-------|--------|-------|", ]

for name, components in [
("Agents", agents),
("Skills", skills),
("Commands", commands),
("Scripts", scripts)
]:
active = sum(1 for c in components if c.status == "active")
draft = sum(1 for c in components if c.status == "draft")
lines.append(f"| {name} | {len(components)} | {active} | {draft} |")

lines.extend([
"",
"## Updates",
"",
])

if updates:
for update in updates:
lines.append(f"- {update}")
else:
lines.append("No updates needed.")

lines.extend([
"",
"---",
"*Generated by CODITECT Repo Docs Updater*",
])

return "\n".join(lines)

def main(): parser = argparse.ArgumentParser( description="Update repository documentation with discovered components" ) parser.add_argument( "--dry-run", "-n", action="store_true", help="Preview changes without writing files" ) parser.add_argument( "--target", "-t", type=str, action="append", help="Specific file to update (can specify multiple)" ) parser.add_argument( "--output", "-o", type=str, default=None, help="Output report file path" ) parser.add_argument( "--json", action="store_true", help="Output as JSON instead of Markdown" )

args = parser.parse_args()

# Discover components
agents = discover_agents(Path("agents")) if Path("agents").exists() else []
skills = discover_skills(Path("skills")) if Path("skills").exists() else []
commands = discover_commands(Path("commands")) if Path("commands").exists() else []
scripts = discover_scripts(Path("scripts")) if Path("scripts").exists() else []

print(f"Discovered: {len(agents)} agents, {len(skills)} skills, "
f"{len(commands)} commands, {len(scripts)} scripts", file=sys.stderr)

updates = []
targets = args.target or ["AGENTS.md", "README.md"]

for target in targets:
target_path = Path(target)

if target == "AGENTS.md" and agents:
content = generate_agents_md(agents)
if args.dry_run:
updates.append(f"Would update AGENTS.md ({len(agents)} agents)")
else:
target_path.write_text(content)
updates.append(f"Updated AGENTS.md ({len(agents)} agents)")

elif target == "SKILLS.md" and skills:
content = generate_skills_md(skills)
if args.dry_run:
updates.append(f"Would update SKILLS.md ({len(skills)} skills)")
else:
target_path.write_text(content)
updates.append(f"Updated SKILLS.md ({len(skills)} skills)")

elif target == "README.md":
counts = {
"agent": len(agents),
"skill": len(skills),
"command": len(commands),
"script": len(scripts)
}
updated_content = update_readme_counts(target_path, counts)
if updated_content:
if args.dry_run:
updates.append("Would update README.md component counts")
else:
target_path.write_text(updated_content)
updates.append("Updated README.md component counts")
else:
updates.append("README.md - no count patterns found to update")

if args.json:
output = json.dumps({
"agents": len(agents),
"skills": len(skills),
"commands": len(commands),
"scripts": len(scripts),
"updates": updates,
"dry_run": args.dry_run
}, indent=2)
else:
output = generate_report(agents, skills, commands, scripts, updates, args.dry_run)

if args.output:
Path(args.output).write_text(output)
print(f"Report written to: {args.output}", file=sys.stderr)
else:
print(output)

if name == "main": main()