generated from coulomb/repo-seed
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).
136 lines
4.7 KiB
Python
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)) |