Files
sar/.claude/skills/bmad-eval-runner/scripts/utils.py
julian 17c08e6392 chore: initial monorepo scaffold + WDS Phase 1+2 artifacts
- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24)
- apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004)
- apps/web: React 19 + Vite 8 (ESM)
- libs/shared/api-interface: Zod contract base
- Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit
- WDS artifacts:
  - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs)
  - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact)
- Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md
- AGENTS.md + README.md como entrada para devs/agentes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:34:20 +00:00

261 lines
7.9 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""Shared helpers for the eval runner."""
from __future__ import annotations
import json
import re
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
"""Return (name, description, body) from the skill's SKILL.md frontmatter."""
text = (skill_path / "SKILL.md").read_text(encoding="utf-8")
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", text, re.DOTALL)
if not fm_match:
raise ValueError(f"SKILL.md at {skill_path} is missing frontmatter")
frontmatter, body = fm_match.group(1), fm_match.group(2)
name = None
description_lines: list[str] = []
in_description = False
for line in frontmatter.splitlines():
if line.startswith("name:"):
name = line.split(":", 1)[1].strip()
in_description = False
elif line.startswith("description:"):
value = line.split(":", 1)[1].strip()
if value in ("|", ">"):
in_description = True
else:
description_lines = [value]
in_description = False
elif in_description and line.startswith((" ", "\t")):
description_lines.append(line.strip())
elif in_description:
in_description = False
if not name:
raise ValueError(f"SKILL.md at {skill_path} is missing a name")
return name, " ".join(description_lines).strip(), body
def discover_project_root(skill_path: Path) -> Path:
"""Walk up from the skill looking for _bmad/ or .git; default to skill's grandparent."""
for parent in [skill_path, *skill_path.parents]:
if (parent / "_bmad").is_dir() or (parent / ".git").exists():
return parent
return skill_path.parent.parent
def discover_evals(
skill_path: Path,
project_root: Path,
explicit: Path | None,
) -> dict[str, Path]:
"""Locate evals.json and triggers.json. Return dict with keys 'evals' and/or 'triggers'."""
found: dict[str, Path] = {}
def check_dir(d: Path) -> None:
if not d.is_dir():
return
for key, fname in (("evals", "evals.json"), ("triggers", "triggers.json")):
candidate = d / fname
if candidate.is_file() and key not in found:
found[key] = candidate
if explicit is not None:
explicit = explicit.resolve()
if explicit.is_file():
if explicit.name == "evals.json":
found["evals"] = explicit
elif explicit.name == "triggers.json":
found["triggers"] = explicit
elif explicit.is_dir():
check_dir(explicit)
return found
skill_name = skill_path.name
candidates: list[Path] = [
skill_path / "evals",
skill_path.parent.parent / "evals" / skill_name,
project_root / "evals" / skill_name,
]
for d in candidates:
check_dir(d)
if found:
break
if not found:
evals_root = project_root / "evals"
if evals_root.is_dir():
for sub in evals_root.rglob(skill_name):
if sub.is_dir():
check_dir(sub)
if found:
break
return found
def utc_now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def new_run_id(skill_name: str) -> str:
return f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{skill_name}"
def have_docker() -> bool:
if shutil.which("docker") is None:
return False
try:
result = subprocess.run(
["docker", "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=5,
)
return result.returncode == 0
except Exception:
return False
def docker_image_present(image: str = "bmad-eval-runner:latest") -> bool:
if not have_docker():
return False
try:
result = subprocess.run(
["docker", "image", "inspect", image],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=10,
)
return result.returncode == 0
except Exception:
return False
def read_macos_keychain_credentials() -> str | None:
"""Read the Claude Code OAuth credentials JSON from the macOS Keychain.
Returns the raw JSON string stored under service "Claude Code-credentials",
or None if unavailable (non-macOS, entry missing, or access denied).
Called in the parent process — which owns the Keychain ACL — so the credential
can be staged into each isolated workspace's `.claude/.credentials.json` before
`claude -p` is launched. Without this, an isolated subprocess with HOME pointed
at an empty dir has no auth and every eval fails with "Not logged in."
"""
if sys.platform != "darwin":
return None
try:
result = subprocess.run(
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
capture_output=True,
timeout=5,
)
if result.returncode != 0:
return None
val = result.stdout.decode("utf-8", errors="replace").strip()
return val if val else None
except Exception:
return None
def stage_credentials(claude_dir: Path, credentials_json: str | None) -> None:
"""Write credentials_json to <claude_dir>/.credentials.json. No-op if None."""
if not credentials_json:
return
claude_dir.mkdir(parents=True, exist_ok=True)
(claude_dir / ".credentials.json").write_text(credentials_json, encoding="utf-8")
def write_json(path: Path, data: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def read_json(path: Path) -> object:
return json.loads(path.read_text(encoding="utf-8"))
def parse_skill_dependencies(skill_path: Path) -> list[str]:
"""Return skill names declared under 'dependencies:' in SKILL.md frontmatter."""
try:
text = (skill_path / "SKILL.md").read_text(encoding="utf-8")
except (FileNotFoundError, OSError):
return []
fm = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
if not fm:
return []
deps: list[str] = []
in_deps = False
for line in fm.group(1).splitlines():
if re.match(r"^dependencies\s*:", line):
in_deps = True
elif in_deps:
m = re.match(r"^\s+-\s+(\S+)", line)
if m:
deps.append(m.group(1))
elif not line.startswith((" ", "\t")):
break
return deps
def discover_setup_dirs(evals_file: Path, eval_id: str | None = None) -> list[Path]:
"""Return ordered list of setup overlay dirs that exist.
base: <evals_dir>/setup/
per-eval: <evals_dir>/<eval_id>/setup/
Applied base-first so per-eval overlays win on conflict.
"""
evals_dir = evals_file.parent
dirs: list[Path] = []
base = evals_dir / "setup"
if base.is_dir():
dirs.append(base)
if eval_id:
per_eval = evals_dir / eval_id / "setup"
if per_eval.is_dir():
dirs.append(per_eval)
return dirs
def apply_setup_overlay(setup_dirs: list[Path], dest: Path) -> None:
"""Rsync each setup dir onto dest in order (base first, per-eval last)."""
dest.mkdir(parents=True, exist_ok=True)
for src in setup_dirs:
if not src.is_dir():
continue
subprocess.run(
["rsync", "-a", f"{src}/", f"{dest}/"],
check=False,
)
__all__ = [
"parse_skill_md",
"discover_project_root",
"discover_evals",
"utc_now_iso",
"new_run_id",
"have_docker",
"docker_image_present",
"read_macos_keychain_credentials",
"stage_credentials",
"write_json",
"read_json",
"parse_skill_dependencies",
"discover_setup_dirs",
"apply_setup_overlay",
]