#!/usr/bin/env python3 """Surgically propagate credential-routing instructions to local repos. Writes `.claude/rules/credential-routing.md`, inserts the section into AGENTS.md (if missing), and adds the CLAUDE.md @include — without overwriting other custom rules or repo-specific AGENTS.md extensions below REPO-AGENTS-EXTENSIONS. Usage: python3 scripts/propagate_credential_routing.py # all /home/worsch repos with AGENTS.md python3 scripts/propagate_credential_routing.py activity-core issue-core ops-warden """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent TEMPLATE_DIR = ROOT / "scripts" / "project_rules" WORKSPACE = Path("/home/worsch") EXTENSION_MARKER = "" SECTION_HEADING = "## Credential and access routing" CLAUDE_INCLUDE = "@.claude/rules/credential-routing.md" def render(template: str, values: dict[str, str]) -> str: for key, value in values.items(): template = template.replace("{" + key + "}", value) return template def repo_values(repo_path: Path) -> dict[str, str]: slug = repo_path.name prefix = slug.split("-", 1)[0].upper() return { "PROJECT_NAME": slug, "PROJECT_DESCRIPTION": slug, "DOMAIN": "", "TOPIC_ID": "(none)", "REPO_SLUG": slug, "WP_PREFIX": prefix, } def insert_credential_section(agents_text: str, cred_section: str) -> str: """Insert general credential routing before repo-specific or workplan sections.""" if SECTION_HEADING in agents_text: return agents_text block = "\n---\n\n" + cred_section.strip() for anchor in ( "\n---\n\n## Issue-core emission", "\n---\n\n## REST ingestion API key", "\n---\n\n## Workplan Convention", "\n## Workplan Convention", ): if anchor in agents_text: return agents_text.replace(anchor, block + anchor, 1) return agents_text.rstrip() + block + "\n" def patch_claude_md(claude_path: Path) -> bool: if not claude_path.exists(): return False text = claude_path.read_text(encoding="utf-8") if CLAUDE_INCLUDE in text: return False if "@.claude/rules/agents.md" in text: text = text.replace( "@.claude/rules/agents.md", f"{CLAUDE_INCLUDE}\n@.claude/rules/agents.md", ) else: text = text.rstrip() + "\n" + CLAUDE_INCLUDE + "\n" claude_path.write_text(text, encoding="utf-8") return True def discover_repos(slugs: list[str] | None) -> list[Path]: if slugs: return [WORKSPACE / slug for slug in slugs if (WORKSPACE / slug).is_dir()] return sorted( p.parent for p in WORKSPACE.glob("*/AGENTS.md") if p.parent.is_dir() and not p.parent.name.startswith(".") ) def main(argv: list[str]) -> int: slugs = argv[1:] if len(argv) > 1 else None cred_template = (TEMPLATE_DIR / "credential-routing.template").read_text(encoding="utf-8") agents_template = (TEMPLATE_DIR / "agents-codex.template").read_text(encoding="utf-8") cred_rule_template = ( "# Credential and access routing\n\n" + cred_template.lstrip().removeprefix("## Credential and access routing\n\n") ) updated: list[str] = [] for repo_path in discover_repos(slugs): values = repo_values(repo_path) cred_section = render(cred_template, values) rules_dir = repo_path / ".claude" / "rules" rules_dir.mkdir(parents=True, exist_ok=True) (rules_dir / "credential-routing.md").write_text( render(cred_rule_template, values), encoding="utf-8" ) agents_path = repo_path / "AGENTS.md" if agents_path.exists(): agents_text = agents_path.read_text(encoding="utf-8") if SECTION_HEADING not in agents_text: agents_path.write_text( insert_credential_section(agents_text, cred_section), encoding="utf-8", ) updated.append(f"{repo_path.name}\tAGENTS.md (insert)") else: body = render(agents_template, {**values, "CREDENTIAL_ROUTING": cred_section}) agents_path.write_text(body, encoding="utf-8") updated.append(f"{repo_path.name}\tAGENTS.md (new)") if patch_claude_md(repo_path / "CLAUDE.md"): updated.append(f"{repo_path.name}\tCLAUDE.md") if f"{repo_path.name}\tcredential-routing.md" not in updated: updated.append(f"{repo_path.name}\tcredential-routing.md") print(f"Propagated to {len(discover_repos(slugs))} repo(s):") for line in updated: print(f" {line}") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))