feat(drone): add introspective pattern — no-args discovery vs --help

Implements the internal AIPass navigation pattern:
- `drone @module` (no args) → introspective: shows what's connected
- `drone @module --help` → help: shows usage documentation

Seedgo introspective lists all discovered plugins with descriptions.
Modules that don't implement get_introspective() fall back to help.

442 tests pass. 5 new introspective tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-03-02 23:06:28 -08:00
co-authored by Claude Opus 4.6
parent 2435a70c79
commit b3067ff45a
4 changed files with 158 additions and 6 deletions
+17 -3
View File
@@ -31,6 +31,7 @@ from aipass.drone.modules import (
is_module,
list_modules,
get_module_info,
get_module_introspective,
route_module_command,
get_module_help,
)
@@ -171,9 +172,22 @@ def _cmd_branch(args: list[str]) -> None:
def _cmd_module(name: str, args: list[str]) -> None:
"""Handle routing to an internal module (e.g. @seedgo)."""
# drone @module --help (or no args)
if not args or args == ["--help"]:
"""Handle routing to an internal module (e.g. @seedgo).
No args → introspective (discovery: what's connected).
--help → help text (usage documentation).
"""
# drone @module (no args) → introspective / discovery
if not args:
intro_text = get_module_introspective(name)
if intro_text:
print(intro_text, end="")
else:
print(f"No information available for @{name}.")
sys.exit(0)
# drone @module --help → usage documentation
if args == ["--help"]:
help_text = get_module_help(name)
if help_text:
print(help_text, end="")
+26
View File
@@ -95,6 +95,32 @@ def get_module_help(name: str, command: str | None = None) -> str:
return ""
def get_module_introspective(name: str) -> str:
"""Get introspective view from a module's drone adapter.
Introspective = discovery mode (no args): shows what's connected.
Falls back to help text if the adapter doesn't implement get_introspective().
Returns introspective string, or empty string if unavailable.
"""
adapter_path = _MODULE_REGISTRY.get(name)
if adapter_path is None:
return ""
try:
mod = importlib.import_module(adapter_path)
# Try introspective first, fall back to help
intro_fn = getattr(mod, "get_introspective", None)
if intro_fn is not None:
return intro_fn()
# Fallback: use help if no introspective defined
help_fn = getattr(mod, "get_help", None)
if help_fn is not None:
return help_fn(None)
return ""
except (ImportError, AttributeError):
return ""
def register_module(name: str, adapter_path: str) -> None:
"""Register a new module. Used for dynamic registration (e.g., plugins)."""
_MODULE_REGISTRY[name] = adapter_path
+72
View File
@@ -2,6 +2,12 @@
Provides the drone module interface so `drone @seedgo` commands work.
Delegates to the seedgo CLI via subprocess for clean stdout/stderr capture.
Drone adapter contract:
- DRONE_MODULE: dict with name, version, description
- handle_command(command, args) -> dict with stdout/stderr/exit_code
- get_help(command=None) -> str (usage documentation)
- get_introspective() -> str (discovery — what's connected)
"""
from __future__ import annotations
@@ -37,6 +43,72 @@ Examples:
"""
def get_introspective() -> str:
"""Return discovery view — what's connected to this module.
Called when user types `drone @seedgo` with no arguments.
Shows connected plugins and available commands at a glance.
"""
# Discover plugins by calling seedgo list and parsing output
try:
result = subprocess.run(
[sys.executable, "-m", "seedgo", "list"],
capture_output=True,
text=True,
timeout=10,
)
plugin_output = result.stdout if result.stdout else ""
except (subprocess.TimeoutExpired, FileNotFoundError):
plugin_output = ""
# Count plugins from the output
# seedgo list outputs lines like " plugin-name source file_types description"
plugin_lines = []
in_table = False
for line in plugin_output.splitlines():
stripped = line.strip()
if stripped.startswith("---"):
in_table = True
continue
if in_table and stripped:
parts = stripped.split()
if len(parts) >= 2:
name = parts[0]
# Find description — everything after the file types column
# The format is: NAME SOURCE FILE_TYPES DESCRIPTION
desc = ""
if len(parts) >= 4:
# Rejoin from 3rd column onwards as description
# Actually the columns are fixed-width, let's just grab the name
desc = " ".join(parts[3:]) if len(parts) > 3 else ""
plugin_lines.append((name, desc))
lines = []
lines.append(f"SEEDGO — Code Standards Framework (v{DRONE_MODULE['version']})")
lines.append("")
lines.append("Auto-discovered plugin orchestration")
lines.append("")
if plugin_lines:
lines.append(f"Discovered Plugins: {len(plugin_lines)}")
lines.append("")
# Find max name length for alignment
max_name = max(len(name) for name, _ in plugin_lines) if plugin_lines else 20
for name, desc in plugin_lines:
if desc:
lines.append(f" * {name:<{max_name}} {desc}")
else:
lines.append(f" * {name}")
else:
lines.append("Discovered Plugins: 0")
lines.append("")
lines.append("Run 'drone @seedgo --help' for usage information")
lines.append("")
return "\n".join(lines)
def handle_command(command: str, args: list[str] | None = None) -> dict:
"""Handle a command routed by drone.
+43 -3
View File
@@ -18,6 +18,7 @@ from aipass.drone.modules import (
ModuleInfo,
get_module_help,
get_module_info,
get_module_introspective,
is_module,
list_modules,
register_module,
@@ -179,6 +180,42 @@ class TestModuleHelp:
assert get_module_help("nonexistent") == ""
class TestModuleIntrospective:
"""Test module introspective (discovery) retrieval."""
def test_seedgo_introspective_shows_plugins(self):
"""get_module_introspective for seedgo lists discovered plugins."""
text = get_module_introspective("seedgo")
assert text != ""
assert "SEEDGO" in text
assert "Discovered Plugins" in text
# Should show at least some known plugins
assert "drone-compliance" in text or "no-bare-except" in text
def test_introspective_shows_version(self):
"""Introspective includes the module version."""
text = get_module_introspective("seedgo")
assert "1.0.0" in text
def test_introspective_points_to_help(self):
"""Introspective tells you how to get help."""
text = get_module_introspective("seedgo")
assert "--help" in text
def test_unknown_module_introspective_returns_empty(self):
"""get_module_introspective for unknown module returns empty string."""
assert get_module_introspective("nonexistent") == ""
def test_introspective_differs_from_help(self):
"""Introspective and help return different content."""
intro = get_module_introspective("seedgo")
help_text = get_module_help("seedgo")
# Both non-empty but different
assert intro != ""
assert help_text != ""
assert intro != help_text
# ---------------------------------------------------------------------------
# CLI integration — drone systems shows modules
# ---------------------------------------------------------------------------
@@ -227,11 +264,14 @@ class TestCLIModuleRouting:
assert "seedgo" in out
assert "check" in out
def test_seedgo_no_args_shows_help(self):
"""drone @seedgo with no command shows help."""
def test_seedgo_no_args_shows_introspective(self):
"""drone @seedgo with no command shows introspective (discovery)."""
code, out, _ = _run_cli("@seedgo")
assert code == 0
assert "seedgo" in out
assert "SEEDGO" in out
assert "Discovered Plugins" in out
# Should NOT be the help text (which has "Commands:" section)
assert "Examples:" not in out
def test_seedgo_list(self):
"""drone @seedgo list shows plugins."""