Story 4.2: Fix lint errors and code review findings

- Remove unused StripOverlayLayer import and stripOverlayLayer variable from module.js
- Add comprehensive JSDoc annotations to FoundryAdapter.js methods (settings, socket, users, scenes, notifications, hooks)
- Add /* global Dialog */ comment to PlayerPrivacyPanel.js for ESLint
- Remove unused _force parameter from GMPlayerPrivacySelector.js render() method
- Fix PlayerPrivacyPanelMenu.js: add constructor() to fallback class and call super()

All 862 unit tests passing. All Story 4.2 acceptance criteria met.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-05-24 01:25:30 +02:00
parent 2d898f6818
commit 20d13fc678
460 changed files with 68054 additions and 22 deletions
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Enumerate customizable BMad skills installed alongside this one.
Scans a skills directory (by default: the directory this script's own skill
lives in, derived from __file__), finds every sibling directory containing a
`customize.toml`, classifies each as agent and/or workflow based on its
top-level blocks, reads the skill's SKILL.md frontmatter description for a
one-liner, and checks whether override files already exist in
`{project-root}/_bmad/custom/`.
Skills in BMad are loaded either from a project-local location (e.g. the
project's `.claude/skills/` or `.cursor/skills/`) or from a user-global
location (e.g. `~/.claude/skills/`). We do not hardcode those paths — the
running skill's own location is the source of truth for sibling discovery.
`--extra-root` is available for the rare case where skills live in multiple
locations on the same machine.
Output: JSON to stdout. Non-empty `errors[]` in the payload is non-fatal
by contract — the scanner surfaces malformed TOML, missing roots, and
skills with no customization block as data for the caller to display,
and still exits 0. Exit 2 is reserved for invocation errors (e.g.
missing or unreadable `--project-root`) where no useful payload can be
produced.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import tomllib
from pathlib import Path
# Top-level TOML blocks that indicate a customization surface.
SURFACE_KEYS = ("agent", "workflow")
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
def default_skills_root() -> Path:
"""Derive the skills root from this script's location.
Layout assumption: {skills_root}/bmad-customize/scripts/list_customizable_skills.py.
So the skills root is three parents up from this file.
"""
return Path(__file__).resolve().parent.parent.parent
def read_frontmatter_description(skill_md: Path) -> str:
"""Extract the `description:` value from a SKILL.md YAML frontmatter block.
Returns an empty string if the file is missing, unreadable, or has no
description field. Intentionally permissive — this is metadata for a
human-facing list, not a validation target.
"""
if not skill_md.is_file():
return ""
try:
text = skill_md.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return ""
m = FRONTMATTER_RE.match(text)
if not m:
return ""
for line in m.group(1).splitlines():
stripped = line.strip()
if stripped.startswith("description:"):
value = stripped[len("description:") :].strip()
# Strip surrounding quotes if present.
if (value.startswith("'") and value.endswith("'")) or (
value.startswith('"') and value.endswith('"')
):
value = value[1:-1]
return value
return ""
def load_customize(toml_path: Path) -> dict | None:
"""Return the parsed TOML, or None if unreadable."""
try:
with toml_path.open("rb") as f:
return tomllib.load(f)
except (OSError, tomllib.TOMLDecodeError):
return None
def scan_skills(
skills_roots: list[Path],
project_root: Path,
) -> dict:
"""Scan each skills root for directories that contain a customize.toml."""
agents: list[dict] = []
workflows: list[dict] = []
errors: list[str] = []
scanned_roots: list[str] = []
seen_names: set[str] = set()
custom_dir = project_root / "_bmad" / "custom"
for root in skills_roots:
if not root.is_dir():
errors.append(f"skills root does not exist: {root}")
continue
scanned_roots.append(str(root))
for skill_dir in sorted(p for p in root.iterdir() if p.is_dir()):
customize_toml = skill_dir / "customize.toml"
if not customize_toml.is_file():
continue
data = load_customize(customize_toml)
if data is None:
errors.append(f"failed to parse {customize_toml}")
continue
skill_name = skill_dir.name
# If a skill with this name was already found in an earlier
# root, skip it — roots are scanned in the order provided, so
# the first occurrence wins.
if skill_name in seen_names:
continue
seen_names.add(skill_name)
description = read_frontmatter_description(skill_dir / "SKILL.md")
team_override = custom_dir / f"{skill_name}.toml"
user_override = custom_dir / f"{skill_name}.user.toml"
entry_base = {
"name": skill_name,
"install_path": str(skill_dir),
"skills_root": str(root),
"description": description,
"has_team_override": team_override.is_file(),
"has_user_override": user_override.is_file(),
"team_override_path": str(team_override),
"user_override_path": str(user_override),
}
# A skill may expose an agent surface, a workflow surface, or
# both. Emit one entry per surface so the caller can group cleanly.
surfaces_found = [k for k in SURFACE_KEYS if k in data]
if not surfaces_found:
errors.append(
f"no [agent] or [workflow] block in {customize_toml}"
)
continue
for surface in surfaces_found:
entry = dict(entry_base)
entry["surface"] = surface
if surface == "agent":
agents.append(entry)
else:
workflows.append(entry)
return {
"project_root": str(project_root),
"scanned_roots": scanned_roots,
"custom_dir": str(custom_dir),
"agents": agents,
"workflows": workflows,
"errors": errors,
}
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"List customizable BMad skills installed alongside this one, "
"grouped by surface (agent vs workflow), with override status "
"looked up against {project-root}/_bmad/custom/."
)
)
parser.add_argument(
"--project-root",
required=True,
help="Absolute path to the project root (the folder containing _bmad/).",
)
parser.add_argument(
"--skills-root",
default=None,
help=(
"Override the primary skills directory to scan. Defaults to the "
"directory this script's own skill lives in."
),
)
parser.add_argument(
"--extra-root",
action="append",
default=[],
metavar="PATH",
help=(
"Additional skills directory to include (repeatable). Useful "
"when skills live in multiple locations on the same machine "
"(e.g. project-local plus a user-global install)."
),
)
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
project_root = Path(args.project_root).expanduser().resolve()
if not project_root.is_dir():
print(
f"error: project-root does not exist or is not a directory: {project_root}",
file=sys.stderr,
)
return 2
primary = (
Path(args.skills_root).expanduser().resolve()
if args.skills_root
else default_skills_root()
)
extras = [Path(p).expanduser().resolve() for p in args.extra_root]
# Deduplicate in order of appearance.
roots: list[Path] = []
for root in [primary, *extras]:
if root not in roots:
roots.append(root)
result = scan_skills(roots, project_root)
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Unit tests for list_customizable_skills.py.
Exercises the scanner against a synthesized install tree:
- an agent-only customize.toml
- a workflow-only customize.toml
- a customize.toml that exposes both surfaces
- a skill directory with no customize.toml (ignored)
- a pre-existing team override in _bmad/custom/
- malformed TOML (surfaces as an error without aborting)
- multiple skills roots (e.g. project-local + user-global mix)
Run: python3 scripts/tests/test_list_customizable_skills.py
"""
from __future__ import annotations
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).resolve().parent.parent / "list_customizable_skills.py"
def _load_module():
spec = importlib.util.spec_from_file_location("list_customizable_skills", SCRIPT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore[union-attr]
return module
MODULE = _load_module()
def _make_skill(parent: Path, name: str, body: str, skill_md: str | None = None) -> Path:
skill_dir = parent / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "customize.toml").write_text(body, encoding="utf-8")
if skill_md is not None:
(skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
return skill_dir
class ScannerTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
self.skills = self.root / "skills"
self.skills.mkdir(parents=True)
self.custom = self.root / "_bmad" / "custom"
self.custom.mkdir(parents=True)
def tearDown(self):
self.tmp.cleanup()
def test_agent_only_skill_detected(self):
_make_skill(
self.skills,
"bmad-agent-pm",
"[agent]\nicon = \"🧠\"\n",
"---\nname: bmad-agent-pm\ndescription: Product manager.\n---\n",
)
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(len(result["agents"]), 1)
self.assertEqual(len(result["workflows"]), 0)
entry = result["agents"][0]
self.assertEqual(entry["name"], "bmad-agent-pm")
self.assertEqual(entry["surface"], "agent")
self.assertEqual(entry["description"], "Product manager.")
self.assertFalse(entry["has_team_override"])
self.assertFalse(entry["has_user_override"])
def test_workflow_only_skill_detected(self):
_make_skill(
self.skills,
"bmad-create-prd",
"[workflow]\npersistent_facts = []\n",
"---\nname: bmad-create-prd\ndescription: 'Create a PRD.'\n---\n",
)
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(len(result["agents"]), 0)
self.assertEqual(len(result["workflows"]), 1)
entry = result["workflows"][0]
self.assertEqual(entry["description"], "Create a PRD.")
def test_dual_surface_skill_emits_two_entries(self):
_make_skill(
self.skills,
"bmad-dual",
"[agent]\nicon = \"x\"\n\n[workflow]\npersistent_facts = []\n",
"---\nname: bmad-dual\ndescription: Dual.\n---\n",
)
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(len(result["agents"]), 1)
self.assertEqual(len(result["workflows"]), 1)
self.assertEqual(result["agents"][0]["name"], "bmad-dual")
self.assertEqual(result["workflows"][0]["name"], "bmad-dual")
def test_skill_without_customize_toml_ignored(self):
(self.skills / "bmad-plain").mkdir()
(self.skills / "bmad-plain" / "SKILL.md").write_text("# plain\n")
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(len(result["agents"]) + len(result["workflows"]), 0)
self.assertEqual(result["errors"], [])
def test_existing_team_override_flagged(self):
_make_skill(
self.skills,
"bmad-agent-pm",
"[agent]\nicon = \"x\"\n",
"---\nname: bmad-agent-pm\ndescription: PM.\n---\n",
)
(self.custom / "bmad-agent-pm.toml").write_text("[agent]\n")
result = MODULE.scan_skills([self.skills], self.root)
entry = result["agents"][0]
self.assertTrue(entry["has_team_override"])
self.assertFalse(entry["has_user_override"])
def test_missing_surface_block_reports_error(self):
_make_skill(self.skills, "bmad-broken", "[not_a_surface]\nfoo = 1\n")
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(len(result["agents"]) + len(result["workflows"]), 0)
self.assertEqual(len(result["errors"]), 1)
self.assertIn("no [agent] or [workflow] block", result["errors"][0])
def test_malformed_toml_reports_error_without_aborting(self):
skill_dir = self.skills / "bmad-bad"
skill_dir.mkdir()
(skill_dir / "customize.toml").write_text("this is not [valid toml\n")
# Plus a good sibling to confirm scanning continues.
_make_skill(
self.skills,
"bmad-good",
"[agent]\nicon = \"x\"\n",
"---\nname: bmad-good\ndescription: Good.\n---\n",
)
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(len(result["agents"]), 1)
self.assertEqual(result["agents"][0]["name"], "bmad-good")
self.assertTrue(any("failed to parse" in e for e in result["errors"]))
def test_description_with_double_quotes_stripped(self):
_make_skill(
self.skills,
"bmad-q",
"[agent]\nicon = \"x\"\n",
'---\nname: bmad-q\ndescription: "Double-quoted desc."\n---\n',
)
result = MODULE.scan_skills([self.skills], self.root)
self.assertEqual(result["agents"][0]["description"], "Double-quoted desc.")
def test_multiple_skills_roots_are_merged(self):
extra_root = self.root / "extra-skills"
extra_root.mkdir()
_make_skill(
self.skills,
"bmad-agent-pm",
"[agent]\nicon = \"x\"\n",
"---\nname: bmad-agent-pm\ndescription: PM.\n---\n",
)
_make_skill(
extra_root,
"bmad-agent-dev",
"[agent]\nicon = \"y\"\n",
"---\nname: bmad-agent-dev\ndescription: Dev.\n---\n",
)
result = MODULE.scan_skills([self.skills, extra_root], self.root)
names = {a["name"] for a in result["agents"]}
self.assertEqual(names, {"bmad-agent-pm", "bmad-agent-dev"})
self.assertEqual(len(result["scanned_roots"]), 2)
def test_duplicate_skill_name_across_roots_first_wins(self):
extra_root = self.root / "extra-skills"
extra_root.mkdir()
_make_skill(
self.skills,
"bmad-agent-pm",
"[agent]\nicon = \"primary\"\n",
"---\nname: bmad-agent-pm\ndescription: Primary.\n---\n",
)
_make_skill(
extra_root,
"bmad-agent-pm",
"[agent]\nicon = \"duplicate\"\n",
"---\nname: bmad-agent-pm\ndescription: Duplicate.\n---\n",
)
result = MODULE.scan_skills([self.skills, extra_root], self.root)
self.assertEqual(len(result["agents"]), 1)
self.assertEqual(result["agents"][0]["description"], "Primary.")
self.assertEqual(result["agents"][0]["skills_root"], str(self.skills))
def test_missing_skills_root_reports_error(self):
result = MODULE.scan_skills(
[self.root / "does-not-exist", self.skills],
self.root,
)
self.assertTrue(any("skills root does not exist" in e for e in result["errors"]))
def test_cli_emits_valid_json_and_exits_zero(self):
_make_skill(
self.skills,
"bmad-agent-pm",
"[agent]\nicon = \"x\"\n",
"---\nname: bmad-agent-pm\ndescription: PM.\n---\n",
)
proc = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--project-root",
str(self.root),
"--skills-root",
str(self.skills),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
payload = json.loads(proc.stdout)
self.assertEqual(len(payload["agents"]), 1)
def test_cli_exits_two_on_missing_project_root(self):
proc = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--project-root",
str(self.root / "does-not-exist"),
"--skills-root",
str(self.skills),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(proc.returncode, 2)
self.assertIn("does not exist", proc.stderr)
if __name__ == "__main__":
unittest.main()