2026-09-22 11:46:40 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Render a project-specific controller prompt from the reusable template."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TEMPLATE = Path(__file__).resolve().parent.parent / "templates/ORCHESTRATOR-PROMPT.template.md"
|
|
|
|
|
PLACEHOLDER = re.compile(r"\{\{([A-Z][A-Z0-9_]*)\}\}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render(template: str, profile: dict[str, str]) -> str:
|
|
|
|
|
keys = set(PLACEHOLDER.findall(template))
|
|
|
|
|
missing = sorted(keys - profile.keys())
|
|
|
|
|
if missing:
|
|
|
|
|
raise ValueError("missing profile values: " + ", ".join(missing))
|
|
|
|
|
for key in keys:
|
|
|
|
|
if not isinstance(profile[key], str) or not profile[key].strip():
|
|
|
|
|
raise ValueError(f"{key} must be a nonempty string")
|
|
|
|
|
result = PLACEHOLDER.sub(lambda match: profile[match.group(1)], template)
|
|
|
|
|
if PLACEHOLDER.search(result):
|
|
|
|
|
raise ValueError("profile values must not contain template placeholders")
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument("profile", type=Path)
|
|
|
|
|
parser.add_argument("--output", type=Path)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
profile = json.loads(args.profile.read_text())
|
|
|
|
|
if not isinstance(profile, dict):
|
|
|
|
|
parser.error("profile must be a JSON object")
|
2026-09-22 21:22:02 -04:00
|
|
|
content = render(TEMPLATE.read_text(), profile)
|
2026-09-22 11:46:40 -04:00
|
|
|
if args.output:
|
|
|
|
|
args.output.write_text(content)
|
|
|
|
|
print(args.output)
|
|
|
|
|
else:
|
|
|
|
|
sys.stdout.write(content)
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|