diff --git a/AGENTS.md b/AGENTS.md index 19650917..a94037e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,8 @@ Plateforme d'analyse de combats WvW. Les contrats et conventions détaillés viv - Exécuter les outils Python via `uv run`; une invocation Python nue contourne l'environnement du workspace. - Itérer avec les tests ciblés; la suite Python complète impose une couverture globale de 90 % et peut nécessiter les services Docker pour les tests d'intégration. +- Les skills BMAD vivent dans `.agents/skills/bmad-*` (chargés via `skills.paths` d'`opencode.json`) et le framework dans `_bmad/`. Le noyau (`resolve_config.py`, `resolve_customization.py`, `render_skill.py`, `memlog.py`) ne dépend que de la stdlib — vérifiable via `tests/scripts/test_bmad_framework.py`. +- opencode ne substitue aucune variable dans les skills (à la différence de Claude Code) : remplacer manuellement `{project-root}` par la racine du repo (`/home/roddy/Work/Gw2Analytics`) et `{skill-root}` par le répertoire du skill invoqué (le bloc « Base directory » de l'outil skill). ## Conventions that differ from defaults diff --git a/opencode.json b/opencode.json index cc5a1ff8..87c79447 100644 --- a/opencode.json +++ b/opencode.json @@ -3,6 +3,9 @@ "plugin": ["@dietrichgebert/ponytail"], "instructions": [], "references": {}, + "skills": { + "paths": [".agents/skills"] + }, "mcp": { "postgres": { "type": "local", diff --git a/tests/scripts/test_bmad_framework.py b/tests/scripts/test_bmad_framework.py new file mode 100644 index 00000000..37ab9f78 --- /dev/null +++ b/tests/scripts/test_bmad_framework.py @@ -0,0 +1,60 @@ +# ruff: noqa: S603 +import json +import re +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[2] +BMAD_SCRIPTS = ROOT / "_bmad" / "scripts" +SKILLS_DIR = ROOT / ".agents" / "skills" + +FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\n?", re.DOTALL) + + +@pytest.mark.parametrize( + "script", + ["resolve_config.py", "resolve_customization.py", "render_skill.py", "memlog.py"], +) +def test_bmad_framework_script_is_invokable(script): + proc = subprocess.run( + [ + sys.executable, + str(BMAD_SCRIPTS / script), + "--help", + ], + check=False, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + + +def test_bmad_framework_resolve_config_merges_project(): + proc = subprocess.run( + [sys.executable, str(BMAD_SCRIPTS / "resolve_config.py"), "--project-root", str(ROOT)], + capture_output=True, + text=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + config = json.loads(proc.stdout) + assert config["core"]["project_name"] == "Gw2Analytics" + assert "bmad-agent-analyst" in config["agents"] + + +def test_bmad_skills_frontmatter_is_opencode_loadable(): + skills = sorted(dir_ for dir_ in SKILLS_DIR.iterdir() if dir_.is_dir()) + assert skills, "no skills under .agents/skills/" + for skill_dir in skills: + skill_md = skill_dir / "SKILL.md" + assert skill_md.is_file(), f"{skill_dir.name}/SKILL.md missing" + match = FRONTMATTER_RE.match(skill_md.read_text()) + assert match, f"{skill_dir.name}/SKILL.md has no frontmatter" + meta = yaml.safe_load(match.group(1)) + assert isinstance(meta, dict), f"{skill_dir.name}: frontmatter not a mapping" + assert meta.get("name") == skill_dir.name, f"{skill_dir.name}: name mismatch" + assert meta.get("description"), f"{skill_dir.name}: description missing"