check-runbooks.sh 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. #!/usr/bin/env bash
  2. #
  3. # check-runbooks.sh — enforce that strategy/runbooks.json stays in sync with the
  4. # real agent roster.
  5. #
  6. # strategy/runbooks.json is the machine-readable roster for the NEXUS scenario
  7. # runbooks: the Agency Agents app reads it to turn a runbook into a one-click
  8. # team deploy, mapping each roster slug to a catalog agent. If a slug there
  9. # doesn't resolve to a real agent file, the app can't deploy that team — so this
  10. # check fails the build when:
  11. # 1. runbooks.json is not valid JSON, or an entry is missing a required field
  12. # 2. any roster `agents[]` slug does not match an agent .md filename stem
  13. # 3. any `doc` path does not exist
  14. # 4. a runbook `slug` is duplicated
  15. #
  16. # Slugs are the agent .md filename stem (the corpus id), e.g.
  17. # engineering/engineering-frontend-developer.md -> "engineering-frontend-developer".
  18. # Uses python3 (already required by check-agent-originality.sh) for JSON; no jq,
  19. # so it runs the same on macOS and CI. Mirrors scripts/check-divisions.sh.
  20. #
  21. # Usage: ./scripts/check-runbooks.sh
  22. set -euo pipefail
  23. cd "$(dirname "$0")/.."
  24. command -v python3 >/dev/null 2>&1 || {
  25. echo "ERROR: python3 is required for the runbooks check." >&2
  26. exit 2
  27. }
  28. python3 - <<'PYEOF'
  29. import json, os, subprocess, sys
  30. JSON = "strategy/runbooks.json"
  31. errors = []
  32. if not os.path.isfile(JSON):
  33. print(f"ERROR {JSON} not found"); sys.exit(1)
  34. try:
  35. data = json.load(open(JSON))
  36. except json.JSONDecodeError as e:
  37. print(f"ERROR {JSON} is not valid JSON: {e}"); sys.exit(1)
  38. # Real slugs = filename stems of tracked agent .md files under division dirs.
  39. NON_DIVISION = {"integrations", "examples", "strategy", "scripts", ".github"}
  40. tracked = subprocess.check_output(["git", "ls-files", "*/*.md"]).decode().splitlines()
  41. real = {os.path.basename(p)[:-3] for p in tracked if p.split("/")[0] not in NON_DIVISION}
  42. runbooks = data.get("runbooks")
  43. if not isinstance(runbooks, list) or not runbooks:
  44. print(f"ERROR {JSON} has no 'runbooks' array"); sys.exit(1)
  45. seen_slugs = set()
  46. total_refs = 0
  47. for rb in runbooks:
  48. rid = rb.get("slug", "<no slug>")
  49. for field in ("slug", "title", "mode", "doc", "roster"):
  50. if field not in rb:
  51. errors.append(f"runbook '{rid}' is missing required field \"{field}\"")
  52. if rb.get("slug") in seen_slugs:
  53. errors.append(f"duplicate runbook slug '{rb.get('slug')}'")
  54. seen_slugs.add(rb.get("slug"))
  55. doc = rb.get("doc")
  56. if doc and not os.path.isfile(doc):
  57. errors.append(f"runbook '{rid}': doc path does not exist: {doc}")
  58. for g in rb.get("roster", []):
  59. for slug in g.get("agents", []):
  60. total_refs += 1
  61. if slug not in real:
  62. errors.append(f"runbook '{rid}' / group '{g.get('group','?')}': "
  63. f"slug '{slug}' does not match any agent .md filename stem")
  64. if errors:
  65. print(f"FAILED: {len(errors)} runbook consistency error(s). "
  66. f"strategy/runbooks.json must reference real agent slugs.\n")
  67. for e in errors:
  68. print(f" ERROR {e}")
  69. sys.exit(1)
  70. print(f"PASSED: {len(runbooks)} runbooks, {total_refs} agent slug references — "
  71. f"all resolve to real agent files.")
  72. PYEOF