Skip to content

Agent Catalog -- Self-Bootstrapping Agent Packs with Catalog Override #1924

@mnriem

Description

@mnriem

Summary

Replace the monolithic AGENT_CONFIG dictionary and per-agent case/switch logic with self-bootstrapping agent packs. Each agent becomes a self-contained artifact with a declarative manifest (speckit-agent.yml) and a Python bootstrap module (bootstrap.py) implementing setup() and teardown(). The core CLI becomes a thin orchestrator that resolves packs and invokes their bootstrap.

The 25 official agent packs ship embedded in the pip wheel alongside the core template pack (#1711), providing zero-config offline functionality. A new agent catalog system -- inspired by the extension (#1707) and preset (#1708) catalog patterns but purpose-built for agent lifecycle semantics -- serves as an override layer. Organizations, teams, and users can publish customized or alternative agent packs that take priority over the embedded defaults. Community agents distribute exclusively via catalogs.

Because packs own both install and uninstall, agent switching becomes a first-class operation via specify agent switch <agent>.

Takes inspiration from #1707 (extension multi-catalog) and #1708 (pluggable presets), and builds directly on #1711 (embedded core pack + Python command generation).

Problem

Adding a new agent today requires changes to 10+ files across 5 languages:

File Language
src/specify_cli/__init__.py Python (AGENT_CONFIG + --ai help text)
README.md Markdown (supported agents table)
AGENTS.md Markdown (integration guide)
scripts/bash/update-agent-context.sh Bash (path variable + case block)
scripts/powershell/update-agent-context.ps1 PowerShell (path variable + switch case)
.github/workflows/scripts/create-release-packages.sh Bash (ALL_AGENTS + case statement)
.github/workflows/scripts/create-github-release.sh Bash (release artifact)
.devcontainer/devcontainer.json or post-create.sh JSON/Bash
pyproject.toml TOML (version bump)
CHANGELOG.md Markdown

Consequences:

The shipped --ai generic --ai-commands-dir <path> (#1639) proved that agent behavior is fully parameterized -- directory, format, argument token. This issue makes that parameterization the default architecture.

Dependencies

Inspirations (not direct dependencies)

Backward Compatibility: --ai Must Work Throughout Migration

specify init --ai claude must continue to work identically at every point during the agent catalog rollout. While the agent catalog system is being brought online, the existing AGENT_CONFIG-based resolution remains the active code path for --ai. The new pack-based resolution is opt-in, similar to how --offline is opt-in today. The migration is incremental:

  1. Before agent catalog ships: --ai claude uses the current AGENT_CONFIG + case/switch logic. No change.
  2. During migration: --ai claude still uses AGENT_CONFIG by default. The new pack resolution is available via an opt-in flag (e.g., --agent-catalog or similar). This allows testing and validation of the pack system without affecting existing users.
  3. After full validation: The opt-in flag becomes the default. AGENT_CONFIG is removed. --ai claude resolves entirely through the pack resolution order.

At no point should a user experience a regression from specify init --ai <agent>. The --ai flag is the primary interface; the catalog system is the implementation change behind it.

Proposed Solution

Architectural Shift

Inversion of control: push bootstrapping logic from core's case/switch blocks outward into self-contained agent packs -- but keep the official packs embedded in the wheel for zero-config operation.

TODAY:
  Release script (case/switch) --> 50 pre-baked ZIPs
  Core CLI downloads ZIP --> expands into filesystem
  Intelligence lives in: release script + core CLI

PROPOSED:
  Official Agent Packs embedded in pip wheel (lowest priority)
  Catalog packs override embedded defaults (higher priority)
  User/project packs override everything (highest priority)
  Core CLI resolves pack by priority --> invokes pack's bootstrap
  Intelligence lives in: the agent pack itself

Agent Pack Structure

Each agent pack is a directory with a standard layout:

claude/
    speckit-agent.yml       # Manifest: identity, metadata, requirements
    bootstrap.py            # Bootstrap module: setup() + teardown()
    content/                # Optional: agent-specific static files
        CLAUDE.md

Manifest (speckit-agent.yml) -- declarative metadata for catalog discovery and compatibility:

schema_version: "1.0"

agent:
  id: "claude"
  name: "Claude Code"
  version: "1.0.0"
  description: "Anthropic's Claude Code CLI for AI-assisted development"
  author: "github"
  license: "MIT"

runtime:
  requires_cli: true
  install_url: "https://docs.anthropic.com/en/docs/claude-code/setup"

requires:
  speckit_version: ">=0.1.0"

tags: ["cli", "anthropic", "claude"]

Bootstrap module (bootstrap.py) -- per-agent intelligence:

class Claude(AgentBootstrap):
    AGENT_DIR = ".claude"
    COMMANDS_SUBDIR = "commands"
    CONTEXT_FILE = "CLAUDE.md"

    def setup(self, project_path, script_type, options):
        commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
        commands_dir.mkdir(parents=True, exist_ok=True)
        for template in core.get_command_templates():
            content = core.render_command(
                template, script_type=script_type,
                format="md", arg_token="$ARGUMENTS",
                frontmatter={"description": template.description},
            )
            (commands_dir / template.filename).write_text(content)
        core.copy_pack_content(self.pack_path / "content", project_path)
        core.install_scripts(project_path, script_type)
        core.install_templates(project_path)

    def teardown(self, project_path):
        core.remove_dir(project_path / self.AGENT_DIR)
        core.remove_file(project_path / self.CONTEXT_FILE)

Core Bootstrap API (speckit_agent_api)

Stable utilities that all agent packs import:

  • core.get_command_templates() -- shared command templates
  • core.render_command(template, script_type, format, arg_token, frontmatter) -- render to target format (Markdown, TOML, agent.md)
  • core.install_scripts(project_path, script_type) -- copy scripts to .specify/scripts/
  • core.install_templates(project_path) -- copy artifact templates
  • core.copy_pack_content(source, dest) -- copy agent-specific static files
  • core.get_template(name) -- resolve template via feat(cli): Embed core template pack in CLI package for air-gapped deployment #1711 resolution stack

Interaction with Extension Custom Commands

Today, the extension system's CommandRegistrar reads AGENT_CONFIG to determine each agent's directory, command format (Markdown/TOML/SKILL.md), and argument placeholder ($ARGUMENTS / {{args}}). When an extension is installed via specify extension add, CommandRegistrar.register_commands_for_all_agents() transforms the extension's universal Markdown commands into agent-specific formats and writes them to each detected agent's command directory.

This creates a hard coupling between extensions and agent metadata. When AGENT_CONFIG is removed in favor of agent packs, the extension system must still be able to query agent metadata. Agent packs must expose the same information that CommandRegistrar currently reads from AGENT_CONFIG:

  • Command directory: Where to write commands (e.g., .claude/commands/, .windsurf/workflows/)
  • Command format: Markdown, TOML, or skills-based
  • Argument placeholder: $ARGUMENTS or {{args}}
  • Skills directory overrides: For agents like Codex (.agents/skills/) and Kimi (.kimi/skills/)
  • Special naming conventions: Copilot's .agent.md/.prompt.md, Codex's speckit-{cmd}/SKILL.md

Design requirement: Each agent pack's manifest (speckit-agent.yml) or bootstrap class must declare enough metadata for CommandRegistrar to render extension commands without importing the bootstrap module. This keeps the extension system decoupled from agent lifecycle logic:

# speckit-agent.yml additions for extension command support
command_registration:
  commands_dir: ".claude/commands"     # Where extension commands are written
  format: "markdown"                    # markdown | toml | skill
  arg_placeholder: "$ARGUMENTS"         # Argument token for this agent
  file_extension: ".md"                 # Output file extension
  # Optional overrides for skills-based agents:
  # skills_dir: ".agents/skills"
  # skill_name_pattern: "speckit-{cmd}"

Impact on agent switching: When specify agent switch removes the old agent's directory (which contains extension-registered commands), those commands are lost. After the new agent's setup() runs, the switch operation must re-register all installed extension commands for the new agent using the extension registry (.specify/extensions/.registry) and CommandRegistrar. This is automatic — no user intervention required.

Impact on CommandRegistrar: The registrar currently reads from the AGENT_CONFIG dictionary. It must be updated to read agent metadata from the resolved pack's manifest instead. The AGENT_CONFIGS lookup table inside CommandRegistrar should be built dynamically from available packs rather than hardcoded.

Catalog Code Sharing

The agent catalog system should take inspiration from the extension (#1707) and preset (#1708) catalog patterns -- catalog stack resolution, priority ordering, download/caching, install_allowed enforcement, and JSON schema validation are common concerns. However, no shared catalog library exists today: the preset system independently reimplemented these patterns from the extension system rather than extracting shared code.

The agent catalog should follow the same approach: implement its own catalog infrastructure informed by the established patterns, but purpose-built for agent semantics. Agents operate at a fundamentally different level than extensions or presets -- they are installed and uninstalled, have setup/teardown lifecycles, support switching between agents in an existing project, and require CLI tool checks. These lifecycle differences mean the Extension and Preset classes cannot be reused directly.

A future refactoring could extract a shared catalog library across all three systems (extensions, presets, agents), but that is not a prerequisite for shipping the agent catalog.

Embedded Agents with Catalog Override

The 25 official agent packs ship inside the pip wheel, following the same pattern as the core template pack (#1711):

src/specify_cli/
    core_pack/
        templates/...              # (from #1711)
        commands/...               # (from #1711) -- shared command templates
        scripts/...                # (from #1711)
        agents/                    # Official agent packs -- embedded in wheel
            claude/
                speckit-agent.yml
                bootstrap.py
                content/
            gemini/
                speckit-agent.yml
                bootstrap.py
                content/
            copilot/
                ...
            # ... all 25 official agents

This means pip install specify-cli gives you a fully functional toolkit -- every official agent works immediately, no network required, no catalog fetch on first use.

Resolution Order

When specify init --ai <agent> or specify agent switch <agent> is invoked, packs resolve by priority -- embedded agents are the lowest priority fallback:

Priority 1 (highest): ~/.specify/agents/<id>/     <-- User-level custom/override packs
Priority 2:           .specify/agents/<id>/        <-- Project-level custom/override packs
Priority 3:           Installed from catalog       <-- From `specify agent add` or prior catalog downloads
Priority 4 (lowest):  Embedded in wheel            <-- Official packs bundled in pip package

Why this order matters:

  • Default experience is zero-config. pip install specify-cli && specify init --ai claude works offline, immediately. The embedded Claude pack bootstraps the project.
  • Catalogs are the override layer. An org publishes a customized "claude" pack to their internal catalog with adjusted conventions. When a developer runs specify agent add claude (pulling from the org catalog), that version takes Priority 3 and shadows the embedded default.
  • Project-level packs win over catalogs. A specific repo needs a one-off agent tweak -- drop a pack in .specify/agents/claude/ and it shadows everything below it.
  • User-level packs win over everything. A developer's personal preference always takes priority.

Community agents (not embedded) distribute exclusively via catalogs. When specify init --ai aider is invoked and no local/cached pack exists, the CLI fetches it from the active agent catalogs. If the catalog is unreachable and no cached version exists, init fails with a clear message:

Agent 'aider' not found locally or in any active catalog.
Run 'specify agent search' to browse available agents, or
'specify agent add aider --from <path>' for offline install.

Override semantics: When a higher-priority pack has the same agent.id as a lower-priority one, the higher-priority pack is used entirely -- no merging, no partial override. This keeps resolution deterministic and debuggable. specify agent info claude shows which pack is active and why:

Agent: Claude Code (claude)
Source: catalog (org) -- ~/.specify/agent-cache/claude/
  Overrides: embedded v1.0.0
Version: 1.2.0

Development and Testing Workflow

The resolution order makes agent development fast and rebuild-free:

Testing changes to an official agent:

# 1. Copy the embedded pack to a project-level override
specify agent export claude --to .specify/agents/claude/

# 2. Edit bootstrap.py, content files, manifest -- any changes
$EDITOR .specify/agents/claude/bootstrap.py

# 3. Re-run init or switch -- picks up local changes immediately, no rebuild
specify init --ai claude
# or
specify agent switch claude

# 4. When done, remove the override -- embedded version is back
rm -rf .specify/agents/claude/

Developing a new community agent from scratch:

# 1. Create a pack directory anywhere
mkdir -p ~/dev/my-agent-pack
# Write speckit-agent.yml and bootstrap.py

# 2. Install from local path -- no catalog needed
specify agent add myagent --from ~/dev/my-agent-pack/

# 3. Test it
specify init --ai myagent

# 4. Iterate -- edit files, re-add, re-init. No wheel rebuild.
specify agent add myagent --from ~/dev/my-agent-pack/
specify agent switch myagent

A/B testing agent variations:

# Drop a modified pack into the project
cp -r $(specify agent export claude --print-path) .specify/agents/claude/
# Edit .specify/agents/claude/bootstrap.py with experimental changes
specify agent switch claude    # Uses the project-level override

# Compare output, then clean up
rm -rf .specify/agents/claude/
specify agent switch claude    # Back to embedded default

Because higher-priority packs shadow lower ones without any registration step, the edit-test cycle is: save file, run command, see result. No pip install -e ., no wheel rebuild, no cache invalidation.

Agent Switching

specify agent switch <agent> invokes the current pack's teardown(), then the new pack's setup(), then re-registers extension commands.

Preserved: specs, plans, tasks, constitution, memory, templates, scripts, extensions, extension registry, git history.

Removed and recreated: agent directory (including extension commands within it), context file, agent-specific configs.

Extension command re-registration: After setup() creates the new agent's command directory, the switch operation must re-register all installed extension commands for the new agent. It reads the extension registry (.specify/extensions/.registry) to discover which extensions have commands, then invokes CommandRegistrar to transform each extension's universal commands into the new agent's format and write them to the new agent's directory. This ensures extensions survive agent switches without requiring manual specify extension add re-runs.

CLI Commands

specify agent search [query] [--tag TAG]     # Search across active catalogs
specify agent add <id>                        # Download + cache from catalog (overrides embedded)
specify agent add --from <url-or-path>        # Install from local source (overrides embedded)
specify agent list [--installed]              # List available agents (embedded + catalog + local)
specify agent remove <id>                     # Remove cached/override pack (falls back to embedded if official)
specify agent info <id>                       # Show active pack, source, and override chain
specify agent export <id> --to <path>         # Export active pack to a directory for editing
specify agent validate <path>                 # Validate pack structure (for contributors)
specify agent switch <id>                      # Switch active agent in existing project

Note on specify agent remove: Removing a cached catalog version of an official agent (e.g., specify agent remove claude) does not break anything -- the embedded version becomes active again. Removing a community agent that has no embedded fallback removes it entirely.

Community Contribution (After)

For official agent fixes/improvements: Submit a PR to the core repo -- the pack ships in the next wheel release.

For community agents (new agents not in the official 25):

1. Create speckit-agent.yml (5-7 lines of YAML)
2. Create bootstrap.py (~15-40 lines of Python)
3. Add optional content files
4. Submit catalog entry (PR to catalog.community.json -- 1 JSON block)

No core code changes. No cross-file coordination. No cross-platform script expertise required.

What This Resolves

Community Requests

Request Signal Resolution
Multi-agent support (#1065) 32 reactions specify agent switch with setup/teardown
8 community agent PRs 8 PRs Resubmit as self-contained packs
Agent switching (#1372, #1395, #1463) 7+ reactions First-class switch command
Niche agent requests (7 low-signal) <3 reactions each Zero-cost for maintainers; community-solvable
Offline agent provisioning (#1513, #1499) -- Works out of the box for official agents; specify agent add --from <path> for community agents
Agent discovery gap -- specify agent search/list

Dev Roadmap Items Subsumed

Item Status
Phase 1: Manifest + scaffolding Fully subsumed
Phase 2: Universal release packages Fully subsumed
Phase 3: Remove legacy case/switch Fully subsumed
Phase 5: Local agent workflow + switching Fully subsumed
Phase 9: Custom agents Fully subsumed
Phase 10: Agent contribution workflow Fully subsumed
can-wait #1-3: Manifest, universal release, case/switch Fully subsumed
nice-to-have #4: Multi-agent (32 reactions) Resolved

Combined with #1711 (phases 6-8), this chain resolves 10 of 12 planned phases.

Codebase Impact

Area Change
src/specify_cli/__init__.py AGENT_CONFIG (~90 lines) removed; per-agent branching in init() replaced by resolve_agent_pack() + bootstrap.setup()
src/specify_cli/core_pack/agents/ (new) 25 official agent packs embedded in wheel
src/specify_cli/agent_api.py (new) AgentBootstrap base class + core utilities (~100 lines)
update-agent-context.sh ~80 lines of case/switch --> ~15 lines of pack-driven loop
update-agent-context.ps1 ~70 lines of switch --> ~15 lines of pack-driven loop
create-release-packages.sh ALL_AGENTS array + ~90-line case statement eliminated; release builds the wheel with embedded packs
agents/catalog.json (new) Official agent catalog (for version-ahead overrides)
agents/catalog.community.json (new) Community agent catalog
Release artifacts 50 ZIPs --> 0 agent ZIPs (official agents in wheel; community agents via catalogs)

Embedded vs. Catalog-First Trade-offs

Concern Embedded (this design) Catalog-first (alternative)
Offline / air-gapped Works out of the box for all 25 official agents Requires pre-provisioning via specify agent add
First-run experience pip install && specify init --ai claude -- instant pip install && specify init --ai claude -- network fetch on first use
Pip package size Larger (25 packs in wheel) Smaller (no agents bundled)
Agent update independence Official agents update with CLI releases Agents update independently via catalogs
Override path Catalog/local packs shadow embedded defaults N/A -- catalogs are the only source
Dev/test cycle Edit local override, re-run command -- no rebuild Same (catalog-installed packs are also local)

What This Does NOT Address

Acceptance Criteria

  • speckit-agent.yml manifest schema defined and validated
  • AgentBootstrap base class with setup()/teardown() contract implemented
  • speckit_agent_api.core utility module implemented (render_command, install_scripts, install_templates, copy_pack_content)
  • All 25 official agents restructured as self-contained packs under src/specify_cli/core_pack/agents/
  • Official agent packs included in pip wheel via pyproject.toml package data
  • Each pack's setup() produces identical output to the current release script for that agent
  • Each pack's teardown() cleanly removes agent-specific files while preserving shared infrastructure
  • AGENT_CONFIG dictionary removed from __init__.py
  • Pack resolution order implemented: user-level > project-level > catalog-installed > embedded (lowest priority)
  • specify init --ai <agent> resolves pack by priority, invokes bootstrap.setup() -- works offline for all 25 official agents
  • specify init --ai <community-agent> fetches from catalog when no local version exists
  • specify agent switch <agent> tears down current agent and sets up new one, preserving specs/plans/tasks/constitution/memory
  • specify agent search, add, list, remove, info, export, validate CLI commands implemented
  • specify agent info shows active pack source and override chain
  • specify agent export copies active pack to a target directory for editing
  • specify agent remove on an official agent falls back to embedded version
  • Agent catalogs (agents/catalog.json, agents/catalog.community.json) created with multi-catalog schema
  • Agent catalogs implement catalog stack resolution with install_allowed enforcement (following patterns established by extension and preset catalogs)
  • Catalog-installed packs override embedded packs of the same agent.id
  • update-agent-context.sh case/switch replaced with pack-driven queries
  • update-agent-context.ps1 switch blocks replaced with pack-driven queries
  • create-release-packages.sh agent case statement eliminated -- wheel contains embedded packs
  • User-level (~/.specify/agents/) and project-level (.specify/agents/) custom packs supported
  • --ai generic --ai-commands-dir <path> backward compatibility preserved
  • specify init --ai <agent> works identically at every stage of migration -- no user-facing regression
  • During migration, pack-based resolution is opt-in (flag-gated); AGENT_CONFIG remains the default until pack system is fully validated
  • Air-gapped install for community agents: specify agent add <id> --from <path>
  • Edit-test cycle works without wheel rebuild: save local override, run command, see result
  • AGENTS.md rewritten as Agent Pack Development Guide
  • CommandRegistrar updated to read agent metadata from pack manifests instead of AGENT_CONFIG
  • Agent pack manifests (speckit-agent.yml) include command registration metadata (directory, format, arg placeholder, file extension) sufficient for CommandRegistrar to render extension commands
  • specify agent switch re-registers all installed extension commands for the new agent after setup() completes
  • Extension commands survive agent switching without requiring manual specify extension add re-runs
  • Skills-based agent packs (Codex, Kimi) correctly declare skills directory and naming conventions for extension command registration
  • Unit tests: manifest validation, bootstrap API contract, resolution order (including embedded fallback), catalog override semantics, CLI commands, switch flow, extension command re-registration, output equivalence for all 25 agents

Metadata

Metadata

Assignees

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