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:
co-authored by
Claude Opus 4.6
parent
2435a70c79
commit
b3067ff45a
+17
-3
@@ -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="")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user