Files
state-hub/scripts/propagate_credential_routing.py
tegwick af2972a460 Add shared credential-routing template and propagation tooling
Introduce credential-routing.template for Codex, Claude Code, Grok, and
llm-connect agents. Wire into agents-codex.template and claude-md.template.
Add propagate_credential_routing.py for surgical rollout without clobbering
repo-specific AGENTS.md extensions (REPO-AGENTS-EXTENSIONS marker).
2026-06-18 22:48:43 +02:00

136 lines
4.7 KiB
Python

#!/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 = "<!-- REPO-AGENTS-EXTENSIONS -->"
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))