Skip to content

CRITICAL: G1/G2/G3/G4 plugin __init__.py missing register(ctx) — loader silently skips wiring (hook + tools no-op) #78

@PowerCreek

Description

@PowerCreek

Bug

All four devagentic-* plugins I shipped (G1 #59, G2 #63, G3 #64, G4 #65 + #72 lazy-load) have __init__.py files that end at logger = logging.getLogger(__name__) with no register(ctx) function. Per hermes_cli/plugins.py:19-20 docstring:

"Each directory plugin must contain a plugin.yaml manifest and an __init__.py with a register(ctx) function."

Loader behavior (line 1184-1187):

register_fn = getattr(module, "register", None)
if register_fn is None:
    loaded.error = "no register() function"
    logger.warning("Plugin '%s' has no register() function", manifest.name)

So the loader scans the manifest, imports __init__.py, finds no register(), logs a WARN and the plugin is silently inert. After #76 fixed the packaging, plugins now LOAD in the wheel, but register zero hooks/tools/commands. Worker sees plugin "installed" but it does nothing.

Concrete impact (G1 most critical)

  • G1 vertical-preamble: preamble.on_pre_llm_call exists in preamble.py but is never registered to the pre_llm_call hook → preamble loader doesn't fire on session boot → worker sees no vertical context, hallucinates doc_ids when asked to fetch grafts.
  • G2 mutations / G3 github / G4 lane-h: MCP tools are registered separately in mcp_serve.py::_register_X_tools (for MCP clients), so MCP server still works. But plugin loader's register() being absent means the loader reports "no tools/hooks" for these plugins.

Root cause (mine)

When I shipped G1/G2/G3/G4 I treated the __init__.py as documentation/metadata only, copying the docstring + logger pattern but dropping the register(ctx) -> None function that lives in canvas/docs __init__.py files. Canvas was the pattern I mirrored — its register() wires register_command(...) + register_hook("pre_llm_call", _preamble.on_pre_llm_call) + register_skill(...). Mine doesn't.

Fix

Add register(ctx) -> None to all 4 plugin __init__.py:

  • G1 vertical-preamble: ctx.register_hook("pre_llm_call", _preamble.on_pre_llm_call)
  • G2/G3/G4: minimal stub register() — MCP tools surface via mcp_serve.py (consistent with devagentic-docs pattern where doc_write is MCP-only, not via register_tool). Stub + comment documents the wiring location. Loader no longer warns; plugin is properly "loaded" even though it surfaces nothing through the loader.

Test plan

  • Per-plugin smoke test: import __init__.py, assert register is callable.
  • G1 specifically: call register(stub_ctx) → assert pre_llm_call hook was registered with preamble.on_pre_llm_call as the callback.
  • G2/G3/G4: call register(stub_ctx) → assert no exception; assert stub-ctx records no hook/tool registrations (matches the "MCP-only" design).
  • Per-plugin contract test: parse __init__.py and assert register function exists at module level.

Combined with #77

After #76+#77 + this PR + container rebuild + service restart:

  • find .../site-packages/plugins -name plugin.yaml → ~25+ files (was 0 pre-fix(packaging): ship plugin.yaml via package-data + MANIFEST.in (closes #76) #77)
  • Plugin loader scans + calls register() on each → hook fires, plugin reported as loaded with N hooks/tools
  • G1 preamble loader fires on session boot → worker sees vertical-spec + grafted-context index + worker-guardrails
  • Poly-explorer no longer hallucinates doc_ids

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions