Skip to content

fix(plugins): add register(ctx) to G1/G2/G3/G4 — loader was silently skipping (closes #78)#79

Merged
PowerCreek merged 1 commit into
mainfrom
issue-78-plugin-register-fns
May 24, 2026
Merged

fix(plugins): add register(ctx) to G1/G2/G3/G4 — loader was silently skipping (closes #78)#79
PowerCreek merged 1 commit into
mainfrom
issue-78-plugin-register-fns

Conversation

@PowerCreek

Copy link
Copy Markdown

Closes #78. My bug from the original G1/G2/G3/G4 ships — I treated __init__.py as docstring + logger metadata and omitted the register(ctx) function that hermes_cli/plugins.py requires.

Per the loader (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 after #77 fixed packaging, plugins now ship + load — but every devagentic-* plugin registers nothing, because there's nothing to register. The G1 preamble loader is the most painful symptom: preamble.on_pre_llm_call exists but is never wired to the pre_llm_call hook → worker boots with no vertical preamble → polynomial-explorer hallucinates doc_ids.

Fix

Plugin register() does what
G1 vertical-preamble ctx.register_hook("pre_llm_call", _preamble.on_pre_llm_call) — the actual fix. Wires the hook the plugin.yaml manifest declares.
G2 mutations Stub register() — MCP-only (silo_query / confer_run live in mcp_serve.py::_register_devagentic_mutation_tools). Matches devagentic-docs pattern where doc_write is MCP-only too.
G3 hermes-github Same: stub — file_issue in _register_github_tools.
G4 lane-h Same: stub — lane_h_list / lane_h_fetch / grafted_context_fetch in _register_lane_h_tools.

The stub register()s exist so the loader doesn't warn no register() and skip the plugin. The MCP-tool wiring is separate (server-side) and unchanged.

Test plan

  • 8 tests in tests/test_plugin_register_fns.py:
    • G1 specifically: register() wires pre_llm_call hook to a callback named on_pre_llm_call (the source-truth fix).
    • G2/G3/G4 parametrized: register() is callable, doesn't raise, registers nothing via the loader (MCP-only by design).
    • All 4 parametrized source check: __init__.py literally contains def register(ctx. Catches future regression where the function disappears.
  • Loads each plugin via importlib.spec_from_file_location with sibling-module pre-loading so the package's relative import (from . import preamble) resolves without requiring the whole hermes package install.

Deploy chain reminder

For polynomial-explorer to actually work end-to-end, the operator needs ALL of these landed + redeployed in the container:

  1. fix(packaging): ship plugin.yaml via package-data + MANIFEST.in (closes #76) #77 packaging (merged) — plugins ship in wheel
  2. CRITICAL: G1/G2/G3/G4 plugin __init__.py missing register(ctx) — loader silently skips wiring (hook + tools no-op) #78 register() (this PR) — loader actually wires the hook + reports tools/hooks > 0 per plugin
  3. devagentic#222 (merged) — /graphql reachable on port 6071 so the preamble loader's fetch returns the rollup
  4. Container rebuild from main + service restart

After all four:

  • find .../site-packages/plugins -name plugin.yaml → ~25+ files (fix(packaging): ship plugin.yaml via package-data + MANIFEST.in (closes #76) #77 effect)
  • hermes plugin list reports each devagentic-* plugin with 1 hook (G1) or 0 hooks 0 tools (MCP-only) (G2/G3/G4) — but the loader no longer WARNs
  • G1 preamble fires on session boot → worker sees the vertical-spec + 70-graft index + worker-guardrails
  • Worker can call grafted_context_fetch(graft_id=<real-id>) with actual ids from the index instead of hallucinating

Notes

  • I have NOT audited every plugin in the tree for this issue — only the four I shipped. Existing plugins (canvas, docs, etc.) have register() per my earlier inspection. If other plugin authors hit this same bug, that's a separate issue.
  • The MCP-only stub pattern for G2/G3/G4 is a deliberate scope choice — making those tools available via the plugin-loader registry (so they appear in agent.tools for the worker's chat completions) would require schema definitions + handler shims per tool, ~50 LOC × 6 tools. Different PR, different scope. Today, those tools serve MCP clients (Claude desktop, hermes-internal MCP-client bridges, etc.) — the worker can still reach them via that path.

@PowerCreek PowerCreek merged commit 1ad2415 into main May 24, 2026
@PowerCreek PowerCreek deleted the issue-78-plugin-register-fns branch May 24, 2026 07:18
PowerCreek added a commit that referenced this pull request May 24, 2026
Layer 2 of the #67 cascade: post-#79 (register-ctx) + #81 (mcp_serve
packaging), the bundled MCP server now ships and the G2/G3/G4 plugins
load — but no MCP server entry pointed at it, so workers booted with
zero MCP-side tools. This adds the missing wiring.

Three edits:

1. mcp_serve.py — `if __name__ == "__main__":` block so
   `python -m mcp_serve` runs the existing stdio server. Honors
   `--verbose`/`-v` for subprocess debugging.

2. hermes_cli/mcp_autowire.py (new) — ensure_internal_mcp_server()
   idempotently inserts a `hermes-internal` entry under `mcp_servers`
   in the active profile config. Uses sys.executable + [-m, mcp_serve]
   so the spawn survives venv-not-on-PATH installs (the typical
   container-deploy shape). Opt-out via HERMES_DISABLE_INTERNAL_MCP=1.

3. tui_gateway/entry.py — calls ensure_internal_mcp_server() BEFORE
   the read_raw_config()::mcp_servers gate, so the gate sees the
   freshly-written entry on the same boot and discover_mcp_tools()
   picks it up.

Issue body proposed `.mcp.json` (Claude-Code-style separate file);
the actual config file is ~/.hermes/config.yaml under the
`mcp_servers` key (the same location `tools/mcp_tool.py` reads via
`hermes_cli.config.load_config`). This patch targets the real config.

Tests (tests/test_internal_mcp_autowire.py — 20 cases):
- HERMES_DISABLE_INTERNAL_MCP truthy variants all opt out
- Idempotent: re-call is a no-op
- Existing operator MCP servers (Linear/Notion/...) preserved
- Creates `mcp_servers` key when absent
- Command is sys.executable (not literal "python")
- Args are [-m, mcp_serve] (catches the spawn-shape regression)
- Source-level: mcp_serve has __main__ guard invoking run_mcp_server
- Source-level: entry.py calls ensure_internal_mcp_server() BEFORE
  the read_raw_config gate (catches ordering regression)

Smoke verified: `timeout 3 python3 -m mcp_serve < /dev/null` → exit 0
(stdio handshake; closes loop cleanly on stdin EOF).

After container rebuild + worker spawn, poly-explorer should have
end-to-end tool access (final layer of the cascade started at #67).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant