Load a plugin's source tree from `plugins/ /`. Plugin names with `__` are rejected: the adapter framework uses ` __ ` as a namespace separator across Codex/OpenCode/Gemini. A plugin name containing `__` would break stale-detection and produce ambiguous reverse-mappi
(plugin_name: str)
| 312 | |
| 313 | |
| 314 | def load_plugin(plugin_name: str) -> PluginSource | None: |
| 315 | """Load a plugin's source tree from `plugins/<name>/`. |
| 316 | |
| 317 | Plugin names with `__` are rejected: the adapter framework uses |
| 318 | `<plugin>__<leaf>` as a namespace separator across Codex/OpenCode/Gemini. |
| 319 | A plugin name containing `__` would break stale-detection and produce |
| 320 | ambiguous reverse-mappings in doc_gardener. |
| 321 | """ |
| 322 | if "__" in plugin_name: |
| 323 | # We use stderr-style print here only at the load site (no logger in this module). |
| 324 | import sys |
| 325 | |
| 326 | print( |
| 327 | f"warning: skipping plugin `{plugin_name}` — plugin names must not contain " |
| 328 | "`__` (the adapter namespace separator).", |
| 329 | file=sys.stderr, |
| 330 | ) |
| 331 | return None |
| 332 | plugin_dir = PLUGINS_DIR / plugin_name |
| 333 | if not plugin_dir.is_dir(): |
| 334 | return None |
| 335 | |
| 336 | plugin_json = read_plugin_json(plugin_dir) |
| 337 | plugin = PluginSource(name=plugin_name, dir=plugin_dir, plugin_json=plugin_json) |
| 338 | |
| 339 | agents_dir = plugin_dir / "agents" |
| 340 | if agents_dir.is_dir(): |
| 341 | for md in sorted(agents_dir.glob("*.md")): |
| 342 | fm, body = parse_frontmatter(read_file(md)) |
| 343 | plugin.agents.append( |
| 344 | AgentSource(plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body) |
| 345 | ) |
| 346 | |
| 347 | skills_dir = plugin_dir / "skills" |
| 348 | if skills_dir.is_dir(): |
| 349 | for sd in sorted(skills_dir.iterdir()): |
| 350 | skill_file = sd / "SKILL.md" |
| 351 | if not (sd.is_dir() and skill_file.is_file()): |
| 352 | continue |
| 353 | fm, body = parse_frontmatter(read_file(skill_file)) |
| 354 | plugin.skills.append( |
| 355 | SkillSource(plugin=plugin_name, name=sd.name, dir=sd, frontmatter=fm, body=body) |
| 356 | ) |
| 357 | |
| 358 | commands_dir = plugin_dir / "commands" |
| 359 | if commands_dir.is_dir(): |
| 360 | for md in sorted(commands_dir.glob("*.md")): |
| 361 | fm, body = parse_frontmatter(read_file(md)) |
| 362 | plugin.commands.append( |
| 363 | CommandSource(plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body) |
| 364 | ) |
| 365 | |
| 366 | return plugin |
| 367 | |
| 368 | |
| 369 | def list_plugins() -> list[str]: |