multi-tool-support Design

Overview

This design implements extensible content generation for multiple AI development tools through a plugin-based renderer architecture. The system enables adding new AI tools (coders) without restructuring existing data sources by separating tool-agnostic data from tool-specific rendering logic.

Architecture

Component Organization

See documentation/architecture/filesystem.rst for the complete filesystem layout. Key modules for multi-tool support:

  • sources/agentsmgr/renderers/base.py: RendererBase abstract class, type definitions

  • sources/agentsmgr/renderers/{claude,codex,opencode}.py: Coder-specific renderer implementations

  • sources/agentsmgr/renderers/__init__.py: Re-exports and imports for renderer registration

  • sources/agentsmgr/generator.py: ContentGenerator with template rendering and fallback logic

  • sources/agentsmgr/sources/base.py: Plugin registration system for source handlers

  • sources/agentsmgr/population.py: Orchestrates content generation across multiple coders

Core Abstractions

RendererBase Protocol

Abstract base class defining the interface all coder renderers must implement:

class RendererBase(immut.Object):
    # Required class attributes
    name: str                                    # Coder identifier (e.g., 'claude')
    modes_available: frozenset[ExplicitTargetMode]  # Supported targeting modes
    mode_default: ExplicitTargetMode             # Default targeting mode
    memory_filename: str                         # Memory file name (e.g., 'CLAUDE.md')

    # Required methods
    def resolve_base_directory(
        self, mode: ExplicitTargetMode, target: Path,
        configuration: Mapping[str, Any], environment: Mapping[str, str]
    ) -> Path:
        """Resolves base output directory for this coder."""
        raise NotImplementedError

    # Optional overrides
    def calculate_directory_location(self, item_type: str) -> str:
        """Returns directory name for item type (default: item_type)."""
        return item_type

    def get_template_flavor(self, item_type: str) -> str:
        """Determines template format to use (default: 'claude')."""
        return 'claude'

    def provide_project_symlinks(self, target: Path) -> Sequence[tuple[Path, Path]]:
        """Provides (source, link_path) symlink tuples for per-project mode."""
        # Default: .auxiliary/configuration/coders/{name} -> .{name}

Plugin Registration Pattern

Renderers self-register by importing and adding themselves to the global RENDERERS registry defined in renderers/base.py. Each renderer module:

  1. Imports RENDERERS and RendererBase as private names (following VBL201)

  2. Defines its renderer class inheriting from RendererBase

  3. Registers an instance in the RENDERERS dictionary

  4. Gets imported by renderers/__init__.py to trigger registration

Design rationale: Self-registration pattern enables automatic discovery without maintaining central registry code. New renderers are added by creating the module and importing it in __init__.py.

Path Resolution Hierarchy

Each renderer implements a three-tier precedence system for resolving output directories:

  1. Environment variables (highest precedence)

    • Claude: CLAUDE_CONFIG_DIR

    • Codex: CODEX_HOME

    • OpenCode: OPENCODE_CONFIG

  2. Configuration file overrides (middle precedence)

    • Read from coders array in application configuration

    • Each coder entry may specify directory (or home for Codex)

  3. Coder defaults (fallback)

    • Claude: ~/.claude

    • Codex: ~/.codex

    • OpenCode: ~/.config/opencode

Per-project mode bypasses precedence and uses standardized paths:

  • .auxiliary/configuration/coders/{coder_name}/

Template Flavor System

The template flavor system enables content format sharing across compatible coders:

def get_template_flavor(self, item_type: str) -> str:
    """Returns pioneering coder name whose template format to use."""

Flavor mappings:

  • 'claude': Markdown format pioneered by Claude Code (used by Claude, Codex, OpenCode commands)

  • 'opencode': OpenCode-specific agent format (used by OpenCode agents only)

  • 'gemini': TOML format pioneered by Gemini (future use)

ContentGenerator uses the flavor system when resolving template paths, looking up the renderer’s preferred flavor and constructing the template path accordingly. This enables Claude and OpenCode to share command templates while using different agent formats.

Content Fallback Mechanism

ContentGenerator implements intelligent fallback for coder-specific content bodies. Configuration specifies fallback mappings (e.g., Claude ↔ OpenCode bidirectional fallback, Gemini isolated).

Resolution logic:

  1. Try contents/{coder}/{item_type}/{item_name}.md

  2. If missing and fallback configured, try contents/{fallback_coder}/{item_type}/{item_name}.md

  3. If still missing, raise ContentAbsence exception

Design rationale: Reduces duplication for coders with compatible markdown formats (Claude ↔ OpenCode) while maintaining isolation for incompatible formats (Gemini).

Targeting Mode Support

Each renderer declares supported targeting modes via its modes_available attribute (e.g., Claude, OpenCode, and Codex support both per-user and per-project).

The population module filters coders by matching their mode_default against the requested target mode. This ensures populate project only processes per-project coders and populate user only processes per-user coders, respecting each tool’s design constraints.

Module Contracts

RendererBase

Responsibilities:

  • Define interface for coder-specific path resolution and configuration

  • Provide default implementations for common behavior

  • Validate targeting mode support

Collaborators:

  • ContentGenerator: Uses renderers to determine output locations and template flavors

  • population.py: Uses renderers for symlink creation and mode filtering

Invariants:

  • mode_default must be in modes_available

  • name must be unique across all registered renderers

  • resolve_base_directory must return absolute paths

ContentGenerator

Responsibilities:

  • Orchestrate template rendering with metadata and content bodies

  • Implement content fallback logic for compatible coders

  • Manage Jinja2 template environment

Collaborators:

  • RENDERERS: Looks up renderer instances by coder name

  • Source handlers: Resolves data source locations

  • Template system: Loads and renders Jinja2 templates

Invariants:

  • Template paths follow pattern: templates/{flavor}/{item_type}/{item_name}.j2

  • Content bodies follow pattern: contents/{coder}/{item_type}/{item_name}.md

  • Metadata follows pattern: configurations/{item_type}/{item_name}.toml

RENDERERS Registry

Responsibilities:

  • Provide global singleton registry of all available renderers

  • Enable lookup by coder name

Collaborators:

  • Renderer modules: Self-register on import

  • ContentGenerator: Retrieves renderers for content generation

  • CLI commands: Validates coder names, iterates available coders

Invariants:

  • Registry is accretive (write-once, append-only)

  • Keys match renderer.name values

  • All values are RendererBase instances

Implementation Patterns

Adding a New Coder

To add support for a new AI tool:

  1. Create renderer module sources/agentsmgr/renderers/{coder}.py implementing the RendererBase interface with:

    • Class attributes: name, modes_available, mode_default, memory_filename

    • Required method: resolve_base_directory

    • Optional overrides: get_template_flavor, calculate_directory_location, provide_project_symlinks

    • Self-registration in the RENDERERS dictionary

  2. Import the new module in renderers/__init__.py to trigger registration

  3. Add content directory defaults/contents/{coder}/ with coder-specific content bodies

  4. Add or reuse templates in defaults/templates/{flavor}/ as appropriate

  5. Configure fallback (optional) in application configuration if compatible with existing coder formats

No changes required to existing data sources (configurations/, other contents/ directories).

Coder-Specific Directory Naming

Some coders use non-standard directory names (e.g., OpenCode uses singular forms like agent/ instead of agents/). Override calculate_directory_location to provide a mapping from generic item types to coder-specific directory names.

Environment-Specific Configuration

Path resolution for per-user installations follows the three-tier precedence hierarchy documented earlier: environment variables take precedence over configuration file overrides, which take precedence over coder defaults. Each coder implements this logic in its resolve_base_directory method.

Extension Points

Source Handler Plugin System

A similar plugin architecture exists for data source handlers in sources/base.py. Source handlers register themselves for specific URL schemes (e.g., git:, github:, gitlab:) and implement the AbstractSourceHandler protocol to resolve source specifications to local filesystem paths.

This enables extensibility for new source types (S3, HTTP archives, etc.) without modifying core logic.

Template Engine Abstraction

While currently Jinja2-specific, the template flavor system provides an abstraction layer that could support alternative template engines per flavor if needed.

Testing Considerations

Renderer Testing

Each renderer should verify:

  • Mode validation (raises TargetModeNoSupport for unsupported modes)

  • Path resolution precedence (environment > config > default)

  • Symlink generation (correct source/link pairs)

  • Template flavor selection (returns expected flavor for each item type)

Integration Testing

End-to-end tests should verify:

  • Content generation with fallback (missing content falls back correctly)

  • Multi-coder population (each coder gets appropriate content)

  • Mode filtering (per-project vs per-user coder segregation)

  • Template flavor resolution (correct templates loaded for each coder)

Mock Strategy

Tests should mock:

  • RENDERERS registry for isolated renderer testing

  • File system operations for symlink and path resolution testing

  • Environment variables for precedence testing

Performance Characteristics

  • Renderer lookup: O(1) dictionary access in RENDERERS registry

  • Content fallback: O(1) or O(2) filesystem checks (primary + fallback)

  • Template loading: Cached by Jinja2 environment, O(1) after first load

  • Mode filtering: O(n) where n = number of configured coders (typically < 5)

Scaling considerations: Plugin architecture keeps memory overhead low—renderers are lightweight objects with no state beyond configuration. Registry size grows linearly with number of supported coders.

Trade-offs and Alternatives

Plugin Registration vs Explicit Registry

Chosen approach: Self-registration via module imports

  • ✅ Automatic discovery of new renderers

  • ✅ No central registry maintenance

  • ❌ Import side effects (registration happens at import time)

  • ❌ No lazy loading (all renderers loaded even if unused)

Alternative: Explicit registry with manual registration

  • ❌ Requires updating central list when adding renderers

  • ✅ More explicit, easier to trace

  • ✅ Could support lazy loading

Decision rationale: Simplicity of adding new coders outweighs concerns about import side effects. All renderers are lightweight and lazy loading provides minimal benefit.

Template Flavor vs Per-Coder Templates

Chosen approach: Template flavor system with sharing

  • ✅ Reduces duplication for compatible formats

  • ✅ Makes format compatibility explicit

  • ❌ Additional indirection in template resolution

  • ❌ Requires coordination on flavor naming

Alternative: Per-coder template directories

  • ❌ Massive duplication (Claude/Codex/OpenCode use identical command format)

  • ✅ Simpler lookup (direct mapping coder → templates)

  • ❌ No explicit representation of format compatibility

Decision rationale: Duplication reduction and explicit compatibility modeling justify the indirection cost.

Content Fallback vs Duplication

Chosen approach: Configurable fallback mappings

  • ✅ Single source for compatible coders

  • ✅ Maintainability (one content file for Claude/OpenCode)

  • ❌ Configuration complexity (fallback mappings)

  • ❌ Debugging complexity (which content was actually used?)

Alternative: Duplicate content for each coder

  • ❌ Duplication burden (2x content for Claude/OpenCode)

  • ❌ Synchronization issues (content divergence)

  • ✅ Explicit, simple lookup

  • ✅ Easy debugging

Decision rationale: Maintenance burden of duplication outweighs configuration complexity. Claude and OpenCode share markdown format, so fallback is semantically correct.

Future Considerations

Dynamic Renderer Discovery

Future enhancement: Load renderers from entry points or plugin directories for third-party renderer packages.

Renderer Composition

Consider supporting renderer mixins or composition for shared behavior across renderer families (e.g., per-user-only renderers, cloud-based renderers).

Template Format Versioning

As AI tools evolve their formats, consider versioning within template flavors (e.g., claude-v1, claude-v2) to maintain compatibility across tool versions.

Configuration Schema Validation

Add formal schema validation for renderer configuration to catch errors early and provide better diagnostics.