Gemstack uses pluggy for deep extensibility. Plugins can register custom topologies, custom roles, custom validation checks, and intercept compilation and execution events — all without modifying Gemstack's core code.
The Plugin API is stable and backward-compatible through the entire 1.x release series. New hooks may be added, but existing signatures will not change.
Here's a minimal plugin that adds a custom topology in ~10 lines:
# gemstack_mobile/__init__.py
from gemstack.plugins.hooks import hookimpl
class MobilePlugin:
@hookimpl
def gemstack_register_topologies(self):
return [{"name": "mobile", "description": "iOS/Android", "content": "# Mobile\n\nOffline-first. Platform parity."}]
@hookimpl
def gemstack_post_init(self, project_root, profile):
(project_root / ".agent" / "MOBILE.md").write_text("# Mobile Context\n")Register it in your pyproject.toml:
[project.entry-points."gemstack"]
mobile = "gemstack_mobile:MobilePlugin"That's it. Gemstack discovers and loads it automatically. Read on for the full hook reference.
Install Gemstack with the plugins extra:
pipx install 'gemstack[plugins]'
# or
uv tool install 'gemstack[all]'This installs pluggy>=1.5 alongside Gemstack.
Plugins are standard Python packages that:
- Import the
hookimplmarker fromgemstack.plugins.hooks - Define a class with methods decorated by
@hookimpl - Register the class via the
[project.entry-points."gemstack"]section in theirpyproject.toml
Gemstack discovers and loads all registered plugins at startup using pluggy's entry-point discovery mechanism.
These hooks fire during project initialization and autonomous execution:
Called after gemstack init completes. Use this to:
- Add custom files to the
.agent/directory - Post-process the AI-generated context files
- Set up project-specific configuration
from gemstack.plugins.hooks import hookimpl
from pathlib import Path
class MyPlugin:
@hookimpl
def gemstack_post_init(self, project_root, profile):
"""Add a custom context file after initialization."""
custom_file = project_root / ".agent" / "CUSTOM.md"
custom_file.write_text(
f"# Custom Context\n\n"
f"Language: {profile.language}\n"
f"Framework: {profile.framework}\n"
)Called before gemstack run executes a workflow step. Use this for:
- Pre-execution validation
- Logging and telemetry
- Environment setup
@hookimpl
def gemstack_pre_run(self, step, feature):
"""Log execution start to an external system."""
print(f"Starting {step} for feature: {feature}")Called after gemstack run completes a step. The result parameter is an ExecutionResult instance containing:
step,feature,success,dry_runfiles_written,compiled_tokens,output_tokenscost_usd,duration_seconds,error,next_step
@hookimpl
def gemstack_post_run(self, step, result):
"""Send a Slack notification on step completion."""
if result.success:
send_slack(f"✅ {step} completed for '{result.feature}' (${result.cost_usd:.4f})")
else:
send_slack(f"❌ {step} failed: {result.error}")These hooks let you modify the compiled context before and after it's assembled:
Called before the compiler stitches sections together. You receive the full list of (section_name, content) tuples and must return the (possibly modified) list.
Use cases:
- Inject additional context sections (e.g., company-wide coding standards)
- Remove or filter sections based on custom logic
- Reorder sections for specific steps
@hookimpl
def gemstack_pre_compile(self, step, sections):
"""Inject company coding standards into every step."""
standards = Path("~/.company/coding-standards.md").expanduser().read_text()
sections.append(("Company Standards", standards))
return sectionsCalled after the full context string is assembled. You receive the compiled string and must return the (possibly modified) string.
@hookimpl
def gemstack_post_compile(self, step, compiled):
"""Append a reminder to every compiled context."""
return compiled + "\n\n## REMINDER: Follow the company coding standards above.\n"These hooks let you register new content types:
Register custom topology profiles. Each dict must have keys:
name— Topology identifier (e.g.,"mobile")description— Human-readable descriptioncontent— Full markdown content of the topology guardrails
@hookimpl
def gemstack_register_topologies(self):
return [{
"name": "mobile",
"description": "iOS and Android native development",
"content": (
"# Mobile Topology Guardrails\n\n"
"## Platform Parity\n"
"Every feature must work on both iOS and Android.\n\n"
"## Offline First\n"
"All core features must work without network connectivity.\n"
),
}]Register custom role definitions. Same dict structure as topologies:
@hookimpl
def gemstack_register_roles(self):
return [{
"name": "accessibility-engineer",
"description": "Accessibility specialist",
"content": "# Accessibility Engineer\n\nYou are an accessibility expert...",
}]Register custom validation checks for gemstack check. Each callable receives a project_root: Path and returns a list of error strings (empty if the check passes):
@hookimpl
def gemstack_register_checks(self):
def check_changelog(project_root):
if not (project_root / "CHANGELOG.md").exists():
return ["Missing CHANGELOG.md — required by company policy"]
return []
return [check_changelog]Package your plugin as a standard Python package and register it via entry points in your pyproject.toml:
[project]
name = "gemstack-mobile"
version = "0.1.0"
dependencies = ["gemstack"]
[project.entry-points."gemstack"]
mobile = "gemstack_mobile:MobilePlugin"Where gemstack_mobile/__init__.py contains:
from gemstack.plugins.hooks import hookimpl
class MobilePlugin:
@hookimpl
def gemstack_register_topologies(self):
return [{
"name": "mobile",
"description": "iOS/Android",
"content": "...",
}]
@hookimpl
def gemstack_post_init(self, project_root, profile):
(project_root / ".agent" / "MOBILE.md").write_text("# Mobile Context")After installing the plugin package (pip install gemstack-mobile), Gemstack will automatically discover and load it.
You can check the plugin API version for compatibility:
from gemstack.plugins.hooks import PLUGIN_API_VERSION
print(PLUGIN_API_VERSION) # "1.0"The PLUGIN_API_VERSION is "1.0" and is guaranteed stable through the 1.x release series. Third-party plugins can check this value to verify they're running against a compatible Gemstack installation.
If pluggy is not installed (i.e., the user installed gemstack without the plugins extra), all hook markers become no-ops. The gemstack.plugins.hooks module can still be imported — the @hookspec and @hookimpl decorators simply pass through without effect. This means:
- Plugin code can live in your project without causing import errors
- Users who don't need plugins pay no performance penalty
- The plugin system is entirely opt-in