From 3212309e7caee1fed5124f5fcb873e813f0f1291 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:01:16 +0000 Subject: [PATCH] Add agent pack infrastructure with embedded packs, manifest validation, resolution, and CLI commands - Create src/specify_cli/agent_pack.py with AgentBootstrap base class, AgentManifest schema/validation, pack resolution (user > project > catalog > embedded) - Generate all 25 official agent packs under src/specify_cli/core_pack/agents/ with speckit-agent.yml manifests and bootstrap.py modules - Add 'specify agent' CLI subcommands: list, info, validate, export, switch, search, add, remove - Update pyproject.toml to bundle agent packs in the wheel - Add comprehensive tests (39 tests): manifest validation, bootstrap API, resolution order, discovery, consistency with AGENT_CONFIG Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/spec-kit/sessions/ef8b4682-7f1a-4b04-a112-df0878236b6b --- pyproject.toml | 2 + src/specify_cli/__init__.py | 451 +++++++++++++++ src/specify_cli/agent_pack.py | 478 ++++++++++++++++ src/specify_cli/core_pack/agents/__init__.py | 0 .../core_pack/agents/agy/__init__.py | 0 .../core_pack/agents/agy/bootstrap.py | 25 + .../core_pack/agents/agy/speckit-agent.yml | 23 + .../core_pack/agents/amp/__init__.py | 0 .../core_pack/agents/amp/bootstrap.py | 25 + .../core_pack/agents/amp/speckit-agent.yml | 25 + .../core_pack/agents/auggie/__init__.py | 0 .../core_pack/agents/auggie/bootstrap.py | 25 + .../core_pack/agents/auggie/speckit-agent.yml | 25 + .../core_pack/agents/bob/__init__.py | 0 .../core_pack/agents/bob/bootstrap.py | 25 + .../core_pack/agents/bob/speckit-agent.yml | 23 + .../core_pack/agents/claude/__init__.py | 0 .../core_pack/agents/claude/bootstrap.py | 25 + .../core_pack/agents/claude/speckit-agent.yml | 25 + .../core_pack/agents/codebuddy/__init__.py | 0 .../core_pack/agents/codebuddy/bootstrap.py | 25 + .../agents/codebuddy/speckit-agent.yml | 25 + .../core_pack/agents/codex/__init__.py | 0 .../core_pack/agents/codex/bootstrap.py | 25 + .../core_pack/agents/codex/speckit-agent.yml | 25 + .../core_pack/agents/copilot/__init__.py | 0 .../core_pack/agents/copilot/bootstrap.py | 25 + .../agents/copilot/speckit-agent.yml | 23 + .../core_pack/agents/cursor-agent/__init__.py | 0 .../agents/cursor-agent/bootstrap.py | 25 + .../agents/cursor-agent/speckit-agent.yml | 23 + .../core_pack/agents/gemini/__init__.py | 0 .../core_pack/agents/gemini/bootstrap.py | 25 + .../core_pack/agents/gemini/speckit-agent.yml | 25 + .../core_pack/agents/iflow/__init__.py | 0 .../core_pack/agents/iflow/bootstrap.py | 25 + .../core_pack/agents/iflow/speckit-agent.yml | 25 + .../core_pack/agents/junie/__init__.py | 0 .../core_pack/agents/junie/bootstrap.py | 25 + .../core_pack/agents/junie/speckit-agent.yml | 25 + .../core_pack/agents/kilocode/__init__.py | 0 .../core_pack/agents/kilocode/bootstrap.py | 25 + .../agents/kilocode/speckit-agent.yml | 23 + .../core_pack/agents/kimi/__init__.py | 0 .../core_pack/agents/kimi/bootstrap.py | 25 + .../core_pack/agents/kimi/speckit-agent.yml | 25 + .../core_pack/agents/kiro-cli/__init__.py | 0 .../core_pack/agents/kiro-cli/bootstrap.py | 25 + .../agents/kiro-cli/speckit-agent.yml | 25 + .../core_pack/agents/opencode/__init__.py | 0 .../core_pack/agents/opencode/bootstrap.py | 25 + .../agents/opencode/speckit-agent.yml | 25 + .../core_pack/agents/pi/__init__.py | 0 .../core_pack/agents/pi/bootstrap.py | 25 + .../core_pack/agents/pi/speckit-agent.yml | 25 + .../core_pack/agents/qodercli/__init__.py | 0 .../core_pack/agents/qodercli/bootstrap.py | 25 + .../agents/qodercli/speckit-agent.yml | 25 + .../core_pack/agents/qwen/__init__.py | 0 .../core_pack/agents/qwen/bootstrap.py | 25 + .../core_pack/agents/qwen/speckit-agent.yml | 25 + .../core_pack/agents/roo/__init__.py | 0 .../core_pack/agents/roo/bootstrap.py | 25 + .../core_pack/agents/roo/speckit-agent.yml | 23 + .../core_pack/agents/shai/__init__.py | 0 .../core_pack/agents/shai/bootstrap.py | 25 + .../core_pack/agents/shai/speckit-agent.yml | 25 + .../core_pack/agents/tabnine/__init__.py | 0 .../core_pack/agents/tabnine/bootstrap.py | 25 + .../agents/tabnine/speckit-agent.yml | 25 + .../core_pack/agents/trae/__init__.py | 0 .../core_pack/agents/trae/bootstrap.py | 25 + .../core_pack/agents/trae/speckit-agent.yml | 23 + .../core_pack/agents/vibe/__init__.py | 0 .../core_pack/agents/vibe/bootstrap.py | 25 + .../core_pack/agents/vibe/speckit-agent.yml | 25 + .../core_pack/agents/windsurf/__init__.py | 0 .../core_pack/agents/windsurf/bootstrap.py | 25 + .../agents/windsurf/speckit-agent.yml | 23 + tests/test_agent_pack.py | 520 ++++++++++++++++++ 80 files changed, 2685 insertions(+) create mode 100644 src/specify_cli/agent_pack.py create mode 100644 src/specify_cli/core_pack/agents/__init__.py create mode 100644 src/specify_cli/core_pack/agents/agy/__init__.py create mode 100644 src/specify_cli/core_pack/agents/agy/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/agy/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/amp/__init__.py create mode 100644 src/specify_cli/core_pack/agents/amp/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/amp/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/auggie/__init__.py create mode 100644 src/specify_cli/core_pack/agents/auggie/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/auggie/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/bob/__init__.py create mode 100644 src/specify_cli/core_pack/agents/bob/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/bob/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/claude/__init__.py create mode 100644 src/specify_cli/core_pack/agents/claude/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/claude/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/codebuddy/__init__.py create mode 100644 src/specify_cli/core_pack/agents/codebuddy/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/codex/__init__.py create mode 100644 src/specify_cli/core_pack/agents/codex/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/codex/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/copilot/__init__.py create mode 100644 src/specify_cli/core_pack/agents/copilot/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/copilot/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/cursor-agent/__init__.py create mode 100644 src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/cursor-agent/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/gemini/__init__.py create mode 100644 src/specify_cli/core_pack/agents/gemini/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/gemini/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/iflow/__init__.py create mode 100644 src/specify_cli/core_pack/agents/iflow/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/iflow/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/junie/__init__.py create mode 100644 src/specify_cli/core_pack/agents/junie/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/junie/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/kilocode/__init__.py create mode 100644 src/specify_cli/core_pack/agents/kilocode/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/kimi/__init__.py create mode 100644 src/specify_cli/core_pack/agents/kimi/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/kimi/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/kiro-cli/__init__.py create mode 100644 src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/opencode/__init__.py create mode 100644 src/specify_cli/core_pack/agents/opencode/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/opencode/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/pi/__init__.py create mode 100644 src/specify_cli/core_pack/agents/pi/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/pi/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/qodercli/__init__.py create mode 100644 src/specify_cli/core_pack/agents/qodercli/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/qwen/__init__.py create mode 100644 src/specify_cli/core_pack/agents/qwen/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/qwen/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/roo/__init__.py create mode 100644 src/specify_cli/core_pack/agents/roo/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/roo/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/shai/__init__.py create mode 100644 src/specify_cli/core_pack/agents/shai/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/shai/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/tabnine/__init__.py create mode 100644 src/specify_cli/core_pack/agents/tabnine/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/trae/__init__.py create mode 100644 src/specify_cli/core_pack/agents/trae/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/trae/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/vibe/__init__.py create mode 100644 src/specify_cli/core_pack/agents/vibe/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/vibe/speckit-agent.yml create mode 100644 src/specify_cli/core_pack/agents/windsurf/__init__.py create mode 100644 src/specify_cli/core_pack/agents/windsurf/bootstrap.py create mode 100644 src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml create mode 100644 tests/test_agent_pack.py diff --git a/pyproject.toml b/pyproject.toml index f3ca76dd..a00a28b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ packages = ["src/specify_cli"] "scripts/powershell" = "specify_cli/core_pack/scripts/powershell" ".github/workflows/scripts/create-release-packages.sh" = "specify_cli/core_pack/release_scripts/create-release-packages.sh" ".github/workflows/scripts/create-release-packages.ps1" = "specify_cli/core_pack/release_scripts/create-release-packages.ps1" +# Official agent packs (embedded in wheel for zero-config offline operation) +"src/specify_cli/core_pack/agents" = "specify_cli/core_pack/agents" [project.optional-dependencies] test = [ diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index d2bf63ee..8623ed80 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -2366,6 +2366,457 @@ def version(): console.print() +# ===== Agent Commands ===== + +agent_app = typer.Typer( + name="agent", + help="Manage agent packs for AI assistants", + add_completion=False, +) +app.add_typer(agent_app, name="agent") + + +@agent_app.command("list") +def agent_list( + installed: bool = typer.Option(False, "--installed", help="Only show agents with local presence in the current project"), +): + """List available agent packs.""" + from .agent_pack import list_all_agents, list_embedded_agents + + show_banner() + + project_path = Path.cwd() + agents = list_all_agents(project_path=project_path if installed else None) + if not agents and not installed: + agents_from_embedded = list_embedded_agents() + if not agents_from_embedded: + console.print("[yellow]No agent packs found.[/yellow]") + console.print("[dim]Agent packs are embedded in the specify-cli wheel.[/dim]") + raise typer.Exit(0) + + table = Table(title="Available Agent Packs", show_lines=False) + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Name", style="white") + table.add_column("Version", style="dim") + table.add_column("Source", style="green") + table.add_column("CLI Required", style="yellow", justify="center") + + for resolved in agents: + m = resolved.manifest + cli_marker = "✓" if m.requires_cli else "—" + source_display = resolved.source + if resolved.overrides: + source_display += f" (overrides {resolved.overrides})" + table.add_row(m.id, m.name, m.version, source_display, cli_marker) + + console.print(table) + console.print(f"\n[dim]{len(agents)} agent(s) available[/dim]") + + +@agent_app.command("info") +def agent_info( + agent_id: str = typer.Argument(..., help="Agent pack ID (e.g. 'claude', 'gemini')"), +): + """Show detailed information about an agent pack.""" + from .agent_pack import resolve_agent_pack, PackResolutionError + + show_banner() + + try: + resolved = resolve_agent_pack(agent_id, project_path=Path.cwd()) + except PackResolutionError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(1) + + m = resolved.manifest + + info_table = Table(show_header=False, box=None, padding=(0, 2)) + info_table.add_column("Key", style="cyan", justify="right") + info_table.add_column("Value", style="white") + + info_table.add_row("Agent", f"{m.name} ({m.id})") + info_table.add_row("Version", m.version) + info_table.add_row("Description", m.description or "—") + info_table.add_row("Author", m.author or "—") + info_table.add_row("License", m.license or "—") + info_table.add_row("", "") + + source_display = resolved.source + if resolved.source == "catalog": + source_display = f"catalog — {resolved.path}" + elif resolved.source == "embedded": + source_display = f"embedded (bundled in specify-cli wheel)" + + info_table.add_row("Source", source_display) + if resolved.overrides: + info_table.add_row("Overrides", resolved.overrides) + info_table.add_row("Pack Path", str(resolved.path)) + info_table.add_row("", "") + + info_table.add_row("Requires CLI", "Yes" if m.requires_cli else "No") + if m.install_url: + info_table.add_row("Install URL", m.install_url) + if m.cli_tool: + info_table.add_row("CLI Tool", m.cli_tool) + info_table.add_row("", "") + + info_table.add_row("Commands Dir", m.commands_dir or "—") + info_table.add_row("Format", m.command_format) + info_table.add_row("Arg Placeholder", m.arg_placeholder) + info_table.add_row("File Extension", m.file_extension) + info_table.add_row("", "") + + info_table.add_row("Tags", ", ".join(m.tags) if m.tags else "—") + info_table.add_row("Speckit Version", m.speckit_version) + + panel = Panel( + info_table, + title=f"[bold cyan]Agent: {m.name}[/bold cyan]", + border_style="cyan", + padding=(1, 2), + ) + console.print(panel) + + +@agent_app.command("validate") +def agent_validate( + pack_path: str = typer.Argument(..., help="Path to the agent pack directory to validate"), +): + """Validate an agent pack's structure and manifest.""" + from .agent_pack import validate_pack, ManifestValidationError, AgentManifest, MANIFEST_FILENAME + + show_banner() + + path = Path(pack_path).resolve() + if not path.is_dir(): + console.print(f"[red]Error:[/red] Not a directory: {path}") + raise typer.Exit(1) + + try: + warnings = validate_pack(path) + except ManifestValidationError as exc: + console.print(f"[red]Validation failed:[/red] {exc}") + raise typer.Exit(1) + + manifest = AgentManifest.from_yaml(path / MANIFEST_FILENAME) + console.print(f"[green]✓[/green] Pack '{manifest.id}' ({manifest.name}) is valid") + + if warnings: + console.print(f"\n[yellow]Warnings ({len(warnings)}):[/yellow]") + for w in warnings: + console.print(f" [yellow]⚠[/yellow] {w}") + else: + console.print("[green]No warnings.[/green]") + + +@agent_app.command("export") +def agent_export( + agent_id: str = typer.Argument(..., help="Agent pack ID to export"), + to: str = typer.Option(..., "--to", help="Destination directory for the exported pack"), +): + """Export the active agent pack to a directory for editing.""" + from .agent_pack import export_pack, PackResolutionError + + show_banner() + + dest = Path(to).resolve() + try: + result = export_pack(agent_id, dest, project_path=Path.cwd()) + except PackResolutionError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(1) + + console.print(f"[green]✓[/green] Exported '{agent_id}' pack to {result}") + console.print(f"[dim]Edit files in {result} and use as a project-level override.[/dim]") + + +@agent_app.command("switch") +def agent_switch( + agent_id: str = typer.Argument(..., help="Agent pack ID to switch to"), +): + """Switch the active AI agent in the current project. + + Tears down the current agent and sets up the new one. + Preserves specs, plans, tasks, constitution, memory, templates, and scripts. + """ + from .agent_pack import ( + resolve_agent_pack, + load_bootstrap, + PackResolutionError, + AgentPackError, + ) + + show_banner() + + project_path = Path.cwd() + init_options_file = project_path / ".specify" / "init-options.json" + + if not init_options_file.exists(): + console.print("[red]Error:[/red] Not a Specify project (missing .specify/init-options.json)") + console.print("[yellow]Hint:[/yellow] Run 'specify init --here' first.") + raise typer.Exit(1) + + # Load current project options + try: + options = json.loads(init_options_file.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + console.print(f"[red]Error reading init options:[/red] {exc}") + raise typer.Exit(1) + + current_agent = options.get("ai") + script_type = options.get("script", "sh") + + # Resolve the new agent pack + try: + resolved = resolve_agent_pack(agent_id, project_path=project_path) + except PackResolutionError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(1) + + console.print(f"[bold]Switching agent: {current_agent or '(none)'} → {agent_id}[/bold]") + + # Teardown current agent (best effort — may have been set up with old system) + if current_agent: + try: + current_resolved = resolve_agent_pack(current_agent, project_path=project_path) + current_bootstrap = load_bootstrap(current_resolved.path, current_resolved.manifest) + console.print(f" [dim]Tearing down {current_agent}...[/dim]") + current_bootstrap.teardown(project_path) + console.print(f" [green]✓[/green] {current_agent} removed") + except AgentPackError: + # If pack-based teardown fails, try legacy cleanup via AGENT_CONFIG + agent_config = AGENT_CONFIG.get(current_agent, {}) + agent_folder = agent_config.get("folder") + if agent_folder: + agent_dir = project_path / agent_folder.rstrip("/") + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) + console.print(f" [green]✓[/green] {current_agent} directory removed (legacy)") + + # Setup new agent + try: + new_bootstrap = load_bootstrap(resolved.path, resolved.manifest) + console.print(f" [dim]Setting up {agent_id}...[/dim]") + new_bootstrap.setup(project_path, script_type, options) + console.print(f" [green]✓[/green] {agent_id} installed") + except AgentPackError as exc: + console.print(f"[red]Error setting up {agent_id}:[/red] {exc}") + raise typer.Exit(1) + + # Update init options + options["ai"] = agent_id + init_options_file.write_text(json.dumps(options, indent=2), encoding="utf-8") + console.print(f"\n[bold green]Successfully switched to {resolved.manifest.name}[/bold green]") + + # Re-register extension commands for the new agent + _reregister_extension_commands(project_path, agent_id) + + +def _reregister_extension_commands(project_path: Path, agent_id: str) -> None: + """Re-register all installed extension commands for a new agent after switching.""" + registry_file = project_path / ".specify" / "extensions" / ".registry" + if not registry_file.is_file(): + return + + try: + registry_data = json.loads(registry_file.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return + + extensions = registry_data.get("extensions", {}) + if not extensions: + return + + try: + from .agents import CommandRegistrar + registrar = CommandRegistrar() + except ImportError: + return + + reregistered = 0 + for ext_id, ext_data in extensions.items(): + commands = ext_data.get("registered_commands", {}) + if not commands: + continue + + ext_dir = project_path / ".specify" / "extensions" / ext_id + if not ext_dir.is_dir(): + continue + + # Get the command list from the manifest + manifest_file = ext_dir / "extension.yml" + if not manifest_file.is_file(): + continue + + try: + from .extensions import ExtensionManifest + manifest = ExtensionManifest(manifest_file) + if manifest.commands: + registered = registrar.register_commands( + agent_id, manifest.commands, ext_id, ext_dir / "commands", project_path + ) + if registered: + reregistered += len(registered) + except Exception: + continue + + if reregistered: + console.print(f" [green]✓[/green] Re-registered {reregistered} extension command(s)") + + +@agent_app.command("search") +def agent_search( + query: str = typer.Argument(None, help="Search query (matches agent ID, name, or tags)"), + tag: str = typer.Option(None, "--tag", help="Filter by tag"), +): + """Search for agent packs across embedded and catalog sources.""" + from .agent_pack import list_all_agents + + show_banner() + + all_agents = list_all_agents(project_path=Path.cwd()) + + if query: + query_lower = query.lower() + all_agents = [ + a for a in all_agents + if query_lower in a.manifest.id.lower() + or query_lower in a.manifest.name.lower() + or query_lower in a.manifest.description.lower() + or any(query_lower in t.lower() for t in a.manifest.tags) + ] + + if tag: + tag_lower = tag.lower() + all_agents = [ + a for a in all_agents + if any(tag_lower == t.lower() for t in a.manifest.tags) + ] + + if not all_agents: + console.print("[yellow]No agents found matching your search.[/yellow]") + raise typer.Exit(0) + + table = Table(title="Search Results", show_lines=False) + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Name", style="white") + table.add_column("Description", style="dim") + table.add_column("Tags", style="green") + table.add_column("Source", style="yellow") + + for resolved in all_agents: + m = resolved.manifest + table.add_row( + m.id, m.name, + (m.description[:50] + "...") if len(m.description) > 53 else m.description, + ", ".join(m.tags), + resolved.source, + ) + + console.print(table) + console.print(f"\n[dim]{len(all_agents)} result(s)[/dim]") + + +@agent_app.command("add") +def agent_add( + agent_id: str = typer.Argument(..., help="Agent pack ID to install"), + from_path: str = typer.Option(None, "--from", help="Install from a local path instead of a catalog"), +): + """Install an agent pack from a catalog or local path.""" + from .agent_pack import ( + _catalog_agents_dir, + AgentManifest, + ManifestValidationError, + MANIFEST_FILENAME, + ) + + show_banner() + + if from_path: + source = Path(from_path).resolve() + if not source.is_dir(): + console.print(f"[red]Error:[/red] Not a directory: {source}") + raise typer.Exit(1) + + manifest_file = source / MANIFEST_FILENAME + if not manifest_file.is_file(): + console.print(f"[red]Error:[/red] No {MANIFEST_FILENAME} found in {source}") + raise typer.Exit(1) + + try: + manifest = AgentManifest.from_yaml(manifest_file) + except ManifestValidationError as exc: + console.print(f"[red]Validation failed:[/red] {exc}") + raise typer.Exit(1) + + dest = _catalog_agents_dir() / manifest.id + dest.mkdir(parents=True, exist_ok=True) + shutil.copytree(source, dest, dirs_exist_ok=True) + console.print(f"[green]✓[/green] Installed '{manifest.id}' ({manifest.name}) from {source}") + else: + # Catalog fetch — placeholder for future catalog integration + console.print(f"[yellow]Catalog fetch not yet implemented.[/yellow]") + console.print(f"[dim]Use --from to install from a local directory.[/dim]") + raise typer.Exit(1) + + +@agent_app.command("remove") +def agent_remove( + agent_id: str = typer.Argument(..., help="Agent pack ID to remove"), +): + """Remove a cached/override agent pack. + + If the agent is an official embedded agent, removing the override + falls back to the embedded version. + """ + from .agent_pack import ( + _catalog_agents_dir, + _user_agents_dir, + _embedded_agents_dir, + MANIFEST_FILENAME, + ) + + show_banner() + + removed = False + + # Check user-level + user_pack = _user_agents_dir() / agent_id + if user_pack.is_dir(): + shutil.rmtree(user_pack) + console.print(f"[green]✓[/green] Removed user-level override for '{agent_id}'") + removed = True + + # Check project-level + project_pack = Path.cwd() / ".specify" / "agents" / agent_id + if project_pack.is_dir(): + shutil.rmtree(project_pack) + console.print(f"[green]✓[/green] Removed project-level override for '{agent_id}'") + removed = True + + # Check catalog cache + catalog_pack = _catalog_agents_dir() / agent_id + if catalog_pack.is_dir(): + shutil.rmtree(catalog_pack) + console.print(f"[green]✓[/green] Removed catalog-cached version of '{agent_id}'") + removed = True + + if not removed: + # Check if it's an embedded agent + embedded_pack = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME + if embedded_pack.is_file(): + console.print(f"[yellow]'{agent_id}' is an embedded official agent and cannot be removed.[/yellow]") + console.print("[dim]It has no overrides to remove.[/dim]") + else: + console.print(f"[red]Error:[/red] Agent '{agent_id}' not found.") + raise typer.Exit(1) + else: + # Check for embedded fallback + embedded_pack = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME + if embedded_pack.is_file(): + console.print(f"[dim]Embedded version of '{agent_id}' is now active.[/dim]") + + # ===== Extension Commands ===== extension_app = typer.Typer( diff --git a/src/specify_cli/agent_pack.py b/src/specify_cli/agent_pack.py new file mode 100644 index 00000000..f3802136 --- /dev/null +++ b/src/specify_cli/agent_pack.py @@ -0,0 +1,478 @@ +""" +Agent Pack Manager for Spec Kit + +Implements self-bootstrapping agent packs with declarative manifests +(speckit-agent.yml) and Python bootstrap modules (bootstrap.py). + +Agent packs resolve by priority: + 1. User-level (~/.specify/agents//) + 2. Project-level (.specify/agents//) + 3. Catalog-installed (downloaded via `specify agent add`) + 4. Embedded in wheel (official packs under core_pack/agents/) + +The embedded packs ship inside the pip wheel so that +`pip install specify-cli && specify init --ai claude` works offline. +""" + +import importlib.util +import shutil +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + +import yaml +from platformdirs import user_data_path + + +# --------------------------------------------------------------------------- +# Manifest schema +# --------------------------------------------------------------------------- + +MANIFEST_FILENAME = "speckit-agent.yml" +BOOTSTRAP_FILENAME = "bootstrap.py" + +MANIFEST_SCHEMA_VERSION = "1.0" + +# Required top-level keys +_REQUIRED_TOP_KEYS = {"schema_version", "agent"} + +# Required keys within the ``agent`` block +_REQUIRED_AGENT_KEYS = {"id", "name", "version"} + + +class AgentPackError(Exception): + """Base exception for agent-pack operations.""" + + +class ManifestValidationError(AgentPackError): + """Raised when a speckit-agent.yml file is invalid.""" + + +class PackResolutionError(AgentPackError): + """Raised when no pack can be found for the requested agent id.""" + + +# --------------------------------------------------------------------------- +# Manifest +# --------------------------------------------------------------------------- + +@dataclass +class AgentManifest: + """Parsed and validated representation of a speckit-agent.yml file.""" + + # identity + id: str + name: str + version: str + description: str = "" + author: str = "" + license: str = "" + + # runtime + requires_cli: bool = False + install_url: Optional[str] = None + cli_tool: Optional[str] = None + + # compatibility + speckit_version: str = ">=0.1.0" + + # discovery + tags: List[str] = field(default_factory=list) + + # command registration metadata (used by CommandRegistrar / extensions) + commands_dir: str = "" + command_format: str = "markdown" + arg_placeholder: str = "$ARGUMENTS" + file_extension: str = ".md" + + # raw data for anything else + raw: Dict[str, Any] = field(default_factory=dict, repr=False) + + # filesystem path to the pack directory that produced this manifest + pack_path: Optional[Path] = field(default=None, repr=False) + + @classmethod + def from_yaml(cls, path: Path) -> "AgentManifest": + """Load and validate a manifest from *path*. + + Raises ``ManifestValidationError`` on structural problems. + """ + try: + text = path.read_text(encoding="utf-8") + data = yaml.safe_load(text) or {} + except yaml.YAMLError as exc: + raise ManifestValidationError(f"Invalid YAML in {path}: {exc}") + except FileNotFoundError: + raise ManifestValidationError(f"Manifest not found: {path}") + + return cls.from_dict(data, pack_path=path.parent) + + @classmethod + def from_dict(cls, data: dict, *, pack_path: Optional[Path] = None) -> "AgentManifest": + """Build a manifest from a raw dictionary.""" + if not isinstance(data, dict): + raise ManifestValidationError("Manifest must be a YAML mapping") + + missing_top = _REQUIRED_TOP_KEYS - set(data) + if missing_top: + raise ManifestValidationError( + f"Missing required top-level key(s): {', '.join(sorted(missing_top))}" + ) + + if data.get("schema_version") != MANIFEST_SCHEMA_VERSION: + raise ManifestValidationError( + f"Unsupported schema_version: {data.get('schema_version')!r} " + f"(expected {MANIFEST_SCHEMA_VERSION!r})" + ) + + agent_block = data.get("agent") + if not isinstance(agent_block, dict): + raise ManifestValidationError("'agent' must be a mapping") + + missing_agent = _REQUIRED_AGENT_KEYS - set(agent_block) + if missing_agent: + raise ManifestValidationError( + f"Missing required agent key(s): {', '.join(sorted(missing_agent))}" + ) + + runtime = data.get("runtime") or {} + requires = data.get("requires") or {} + tags = data.get("tags") or [] + cmd_reg = data.get("command_registration") or {} + + return cls( + id=str(agent_block["id"]), + name=str(agent_block["name"]), + version=str(agent_block["version"]), + description=str(agent_block.get("description", "")), + author=str(agent_block.get("author", "")), + license=str(agent_block.get("license", "")), + requires_cli=bool(runtime.get("requires_cli", False)), + install_url=runtime.get("install_url"), + cli_tool=runtime.get("cli_tool"), + speckit_version=str(requires.get("speckit_version", ">=0.1.0")), + tags=[str(t) for t in tags] if isinstance(tags, list) else [], + commands_dir=str(cmd_reg.get("commands_dir", "")), + command_format=str(cmd_reg.get("format", "markdown")), + arg_placeholder=str(cmd_reg.get("arg_placeholder", "$ARGUMENTS")), + file_extension=str(cmd_reg.get("file_extension", ".md")), + raw=data, + pack_path=pack_path, + ) + + +# --------------------------------------------------------------------------- +# Bootstrap base class +# --------------------------------------------------------------------------- + +class AgentBootstrap: + """Base class that every agent pack's ``bootstrap.py`` must subclass. + + Subclasses override :meth:`setup` and :meth:`teardown` to define + agent-specific lifecycle operations. + """ + + def __init__(self, manifest: AgentManifest): + self.manifest = manifest + self.pack_path = manifest.pack_path + + # -- lifecycle ----------------------------------------------------------- + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install agent files into *project_path*. + + This is invoked by ``specify init --ai `` and + ``specify agent switch ``. + + Args: + project_path: Target project directory. + script_type: ``"sh"`` or ``"ps"``. + options: Arbitrary key/value options forwarded from the CLI. + """ + raise NotImplementedError + + def teardown(self, project_path: Path) -> None: + """Remove agent-specific files from *project_path*. + + Invoked by ``specify agent switch`` (for the *old* agent) and + ``specify agent remove`` when the user explicitly uninstalls. + Must preserve shared infrastructure (specs, plans, tasks, etc.). + + Args: + project_path: Project directory to clean up. + """ + raise NotImplementedError + + # -- helpers available to subclasses ------------------------------------ + + def agent_dir(self, project_path: Path) -> Path: + """Return the agent's top-level directory inside the project.""" + return project_path / self.manifest.commands_dir.split("/")[0] + + +# --------------------------------------------------------------------------- +# Pack resolution +# --------------------------------------------------------------------------- + +def _embedded_agents_dir() -> Path: + """Return the path to the embedded agent packs inside the wheel.""" + return Path(__file__).parent / "core_pack" / "agents" + + +def _user_agents_dir() -> Path: + """Return the user-level agent overrides directory.""" + return user_data_path("specify", "github") / "agents" + + +def _project_agents_dir(project_path: Path) -> Path: + """Return the project-level agent overrides directory.""" + return project_path / ".specify" / "agents" + + +def _catalog_agents_dir() -> Path: + """Return the catalog-installed agent cache directory.""" + return user_data_path("specify", "github") / "agent-cache" + + +@dataclass +class ResolvedPack: + """Result of resolving an agent pack through the priority stack.""" + manifest: AgentManifest + source: str # "user", "project", "catalog", "embedded" + path: Path + overrides: Optional[str] = None # version of the pack being overridden + + +def resolve_agent_pack( + agent_id: str, + project_path: Optional[Path] = None, +) -> ResolvedPack: + """Resolve an agent pack through the priority stack. + + Priority (highest first): + 1. User-level ``~/.specify/agents//`` + 2. Project-level ``.specify/agents//`` + 3. Catalog-installed cache + 4. Embedded in wheel + + Raises ``PackResolutionError`` when no pack is found at any level. + """ + candidates: List[tuple[str, Path]] = [] + + # Priority 1 — user level + user_dir = _user_agents_dir() / agent_id + candidates.append(("user", user_dir)) + + # Priority 2 — project level + if project_path is not None: + proj_dir = _project_agents_dir(project_path) / agent_id + candidates.append(("project", proj_dir)) + + # Priority 3 — catalog cache + catalog_dir = _catalog_agents_dir() / agent_id + candidates.append(("catalog", catalog_dir)) + + # Priority 4 — embedded + embedded_dir = _embedded_agents_dir() / agent_id + candidates.append(("embedded", embedded_dir)) + + embedded_manifest: Optional[AgentManifest] = None + + for source, pack_dir in candidates: + manifest_file = pack_dir / MANIFEST_FILENAME + if manifest_file.is_file(): + manifest = AgentManifest.from_yaml(manifest_file) + if source == "embedded": + embedded_manifest = manifest + + overrides = None + if source != "embedded" and embedded_manifest is None: + # Try loading embedded to record what it overrides + emb_file = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME + if emb_file.is_file(): + try: + emb = AgentManifest.from_yaml(emb_file) + overrides = f"embedded v{emb.version}" + except AgentPackError: + pass + + return ResolvedPack( + manifest=manifest, + source=source, + path=pack_dir, + overrides=overrides, + ) + + raise PackResolutionError( + f"Agent '{agent_id}' not found locally or in any active catalog.\n" + f"Run 'specify agent search' to browse available agents, or\n" + f"'specify agent add {agent_id} --from ' for offline install." + ) + + +# --------------------------------------------------------------------------- +# Pack discovery helpers +# --------------------------------------------------------------------------- + +def list_embedded_agents() -> List[AgentManifest]: + """Return manifests for all agent packs embedded in the wheel.""" + agents_dir = _embedded_agents_dir() + if not agents_dir.is_dir(): + return [] + + manifests: List[AgentManifest] = [] + for child in sorted(agents_dir.iterdir()): + manifest_file = child / MANIFEST_FILENAME + if child.is_dir() and manifest_file.is_file(): + try: + manifests.append(AgentManifest.from_yaml(manifest_file)) + except AgentPackError: + continue + return manifests + + +def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]: + """List all available agents, resolved through the priority stack. + + Each agent id appears at most once, at its highest-priority source. + """ + seen: dict[str, ResolvedPack] = {} + + # Start from lowest priority (embedded) so higher priorities overwrite + for manifest in list_embedded_agents(): + seen[manifest.id] = ResolvedPack( + manifest=manifest, + source="embedded", + path=manifest.pack_path or _embedded_agents_dir() / manifest.id, + ) + + # Catalog cache + catalog_dir = _catalog_agents_dir() + if catalog_dir.is_dir(): + for child in sorted(catalog_dir.iterdir()): + mf = child / MANIFEST_FILENAME + if child.is_dir() and mf.is_file(): + try: + m = AgentManifest.from_yaml(mf) + overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None + seen[m.id] = ResolvedPack(manifest=m, source="catalog", path=child, overrides=overrides) + except AgentPackError: + continue + + # Project-level + if project_path is not None: + proj_dir = _project_agents_dir(project_path) + if proj_dir.is_dir(): + for child in sorted(proj_dir.iterdir()): + mf = child / MANIFEST_FILENAME + if child.is_dir() and mf.is_file(): + try: + m = AgentManifest.from_yaml(mf) + overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None + seen[m.id] = ResolvedPack(manifest=m, source="project", path=child, overrides=overrides) + except AgentPackError: + continue + + # User-level + user_dir = _user_agents_dir() + if user_dir.is_dir(): + for child in sorted(user_dir.iterdir()): + mf = child / MANIFEST_FILENAME + if child.is_dir() and mf.is_file(): + try: + m = AgentManifest.from_yaml(mf) + overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None + seen[m.id] = ResolvedPack(manifest=m, source="user", path=child, overrides=overrides) + except AgentPackError: + continue + + return sorted(seen.values(), key=lambda r: r.manifest.id) + + +def load_bootstrap(pack_path: Path, manifest: AgentManifest) -> AgentBootstrap: + """Import ``bootstrap.py`` from *pack_path* and return the bootstrap instance. + + The bootstrap module must define exactly one public subclass of + ``AgentBootstrap``. That class is instantiated with *manifest* and + returned. + """ + bootstrap_file = pack_path / BOOTSTRAP_FILENAME + if not bootstrap_file.is_file(): + raise AgentPackError( + f"Bootstrap module not found: {bootstrap_file}" + ) + + spec = importlib.util.spec_from_file_location( + f"speckit_agent_{manifest.id}_bootstrap", bootstrap_file + ) + if spec is None or spec.loader is None: + raise AgentPackError(f"Cannot load bootstrap module: {bootstrap_file}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find the AgentBootstrap subclass + candidates = [ + obj + for name, obj in vars(module).items() + if ( + isinstance(obj, type) + and issubclass(obj, AgentBootstrap) + and obj is not AgentBootstrap + and not name.startswith("_") + ) + ] + if not candidates: + raise AgentPackError( + f"No AgentBootstrap subclass found in {bootstrap_file}" + ) + if len(candidates) > 1: + raise AgentPackError( + f"Multiple AgentBootstrap subclasses in {bootstrap_file}: " + f"{[c.__name__ for c in candidates]}" + ) + + return candidates[0](manifest) + + +def validate_pack(pack_path: Path) -> List[str]: + """Validate a pack directory structure and return a list of warnings. + + Returns an empty list when the pack is fully valid. + Raises ``ManifestValidationError`` on hard errors. + """ + warnings: List[str] = [] + manifest_file = pack_path / MANIFEST_FILENAME + + if not manifest_file.is_file(): + raise ManifestValidationError( + f"Missing {MANIFEST_FILENAME} in {pack_path}" + ) + + manifest = AgentManifest.from_yaml(manifest_file) + + bootstrap_file = pack_path / BOOTSTRAP_FILENAME + if not bootstrap_file.is_file(): + warnings.append(f"Missing {BOOTSTRAP_FILENAME} (pack cannot be bootstrapped)") + + if not manifest.commands_dir: + warnings.append("command_registration.commands_dir not set in manifest") + + if not manifest.description: + warnings.append("agent.description is empty") + + if not manifest.tags: + warnings.append("No tags specified (reduces discoverability)") + + return warnings + + +def export_pack(agent_id: str, dest: Path, project_path: Optional[Path] = None) -> Path: + """Export the active pack for *agent_id* to *dest*. + + Returns the path to the exported pack directory. + """ + resolved = resolve_agent_pack(agent_id, project_path=project_path) + dest.mkdir(parents=True, exist_ok=True) + shutil.copytree(resolved.path, dest, dirs_exist_ok=True) + return dest diff --git a/src/specify_cli/core_pack/agents/__init__.py b/src/specify_cli/core_pack/agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/agy/__init__.py b/src/specify_cli/core_pack/agents/agy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/agy/bootstrap.py b/src/specify_cli/core_pack/agents/agy/bootstrap.py new file mode 100644 index 00000000..4f0dd5a7 --- /dev/null +++ b/src/specify_cli/core_pack/agents/agy/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Antigravity agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Agy(AgentBootstrap): + """Bootstrap for Antigravity.""" + + AGENT_DIR = ".agent" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Antigravity agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Antigravity agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/agy/speckit-agent.yml b/src/specify_cli/core_pack/agents/agy/speckit-agent.yml new file mode 100644 index 00000000..754afaa1 --- /dev/null +++ b/src/specify_cli/core_pack/agents/agy/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "agy" + name: "Antigravity" + version: "1.0.0" + description: "Antigravity IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'antigravity'] + +command_registration: + commands_dir: ".agent/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/amp/__init__.py b/src/specify_cli/core_pack/agents/amp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/amp/bootstrap.py b/src/specify_cli/core_pack/agents/amp/bootstrap.py new file mode 100644 index 00000000..51b676bf --- /dev/null +++ b/src/specify_cli/core_pack/agents/amp/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Amp agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Amp(AgentBootstrap): + """Bootstrap for Amp.""" + + AGENT_DIR = ".agents" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Amp agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Amp agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/amp/speckit-agent.yml b/src/specify_cli/core_pack/agents/amp/speckit-agent.yml new file mode 100644 index 00000000..eaca7fa3 --- /dev/null +++ b/src/specify_cli/core_pack/agents/amp/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "amp" + name: "Amp" + version: "1.0.0" + description: "Amp CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://ampcode.com/manual#install" + cli_tool: "amp" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'amp'] + +command_registration: + commands_dir: ".agents/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/auggie/__init__.py b/src/specify_cli/core_pack/agents/auggie/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/auggie/bootstrap.py b/src/specify_cli/core_pack/agents/auggie/bootstrap.py new file mode 100644 index 00000000..7ff391b9 --- /dev/null +++ b/src/specify_cli/core_pack/agents/auggie/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Auggie CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Auggie(AgentBootstrap): + """Bootstrap for Auggie CLI.""" + + AGENT_DIR = ".augment" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Auggie CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Auggie CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/auggie/speckit-agent.yml b/src/specify_cli/core_pack/agents/auggie/speckit-agent.yml new file mode 100644 index 00000000..d44bae65 --- /dev/null +++ b/src/specify_cli/core_pack/agents/auggie/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "auggie" + name: "Auggie CLI" + version: "1.0.0" + description: "Auggie CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli" + cli_tool: "auggie" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'augment', 'auggie'] + +command_registration: + commands_dir: ".augment/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/bob/__init__.py b/src/specify_cli/core_pack/agents/bob/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/bob/bootstrap.py b/src/specify_cli/core_pack/agents/bob/bootstrap.py new file mode 100644 index 00000000..ab4052a8 --- /dev/null +++ b/src/specify_cli/core_pack/agents/bob/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for IBM Bob agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Bob(AgentBootstrap): + """Bootstrap for IBM Bob.""" + + AGENT_DIR = ".bob" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install IBM Bob agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove IBM Bob agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/bob/speckit-agent.yml b/src/specify_cli/core_pack/agents/bob/speckit-agent.yml new file mode 100644 index 00000000..5716f0ce --- /dev/null +++ b/src/specify_cli/core_pack/agents/bob/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "bob" + name: "IBM Bob" + version: "1.0.0" + description: "IBM Bob IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'ibm', 'bob'] + +command_registration: + commands_dir: ".bob/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/claude/__init__.py b/src/specify_cli/core_pack/agents/claude/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/claude/bootstrap.py b/src/specify_cli/core_pack/agents/claude/bootstrap.py new file mode 100644 index 00000000..a2a515ee --- /dev/null +++ b/src/specify_cli/core_pack/agents/claude/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Claude Code agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Claude(AgentBootstrap): + """Bootstrap for Claude Code.""" + + AGENT_DIR = ".claude" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Claude Code agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Claude Code agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/claude/speckit-agent.yml b/src/specify_cli/core_pack/agents/claude/speckit-agent.yml new file mode 100644 index 00000000..b8073b95 --- /dev/null +++ b/src/specify_cli/core_pack/agents/claude/speckit-agent.yml @@ -0,0 +1,25 @@ +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" + cli_tool: "claude" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'anthropic', 'claude'] + +command_registration: + commands_dir: ".claude/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/codebuddy/__init__.py b/src/specify_cli/core_pack/agents/codebuddy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/codebuddy/bootstrap.py b/src/specify_cli/core_pack/agents/codebuddy/bootstrap.py new file mode 100644 index 00000000..a6f061ba --- /dev/null +++ b/src/specify_cli/core_pack/agents/codebuddy/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for CodeBuddy agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Codebuddy(AgentBootstrap): + """Bootstrap for CodeBuddy.""" + + AGENT_DIR = ".codebuddy" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install CodeBuddy agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove CodeBuddy agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml b/src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml new file mode 100644 index 00000000..d12fe608 --- /dev/null +++ b/src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "codebuddy" + name: "CodeBuddy" + version: "1.0.0" + description: "CodeBuddy CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://www.codebuddy.ai/cli" + cli_tool: "codebuddy" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'codebuddy'] + +command_registration: + commands_dir: ".codebuddy/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/codex/__init__.py b/src/specify_cli/core_pack/agents/codex/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/codex/bootstrap.py b/src/specify_cli/core_pack/agents/codex/bootstrap.py new file mode 100644 index 00000000..8f9a60a9 --- /dev/null +++ b/src/specify_cli/core_pack/agents/codex/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Codex CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Codex(AgentBootstrap): + """Bootstrap for Codex CLI.""" + + AGENT_DIR = ".agents" + COMMANDS_SUBDIR = "skills" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Codex CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Codex CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/codex/speckit-agent.yml b/src/specify_cli/core_pack/agents/codex/speckit-agent.yml new file mode 100644 index 00000000..0bff60cf --- /dev/null +++ b/src/specify_cli/core_pack/agents/codex/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "codex" + name: "Codex CLI" + version: "1.0.0" + description: "OpenAI Codex CLI with project skills support" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://github.com/openai/codex" + cli_tool: "codex" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'openai', 'codex', 'skills'] + +command_registration: + commands_dir: ".agents/skills" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: "/SKILL.md" diff --git a/src/specify_cli/core_pack/agents/copilot/__init__.py b/src/specify_cli/core_pack/agents/copilot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/copilot/bootstrap.py b/src/specify_cli/core_pack/agents/copilot/bootstrap.py new file mode 100644 index 00000000..44a23e1f --- /dev/null +++ b/src/specify_cli/core_pack/agents/copilot/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for GitHub Copilot agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Copilot(AgentBootstrap): + """Bootstrap for GitHub Copilot.""" + + AGENT_DIR = ".github" + COMMANDS_SUBDIR = "agents" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install GitHub Copilot agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove GitHub Copilot agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/copilot/speckit-agent.yml b/src/specify_cli/core_pack/agents/copilot/speckit-agent.yml new file mode 100644 index 00000000..a5430ea7 --- /dev/null +++ b/src/specify_cli/core_pack/agents/copilot/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "copilot" + name: "GitHub Copilot" + version: "1.0.0" + description: "GitHub Copilot for AI-assisted development in VS Code" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'github', 'copilot'] + +command_registration: + commands_dir: ".github/agents" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".agent.md" diff --git a/src/specify_cli/core_pack/agents/cursor-agent/__init__.py b/src/specify_cli/core_pack/agents/cursor-agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py b/src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py new file mode 100644 index 00000000..0af4d914 --- /dev/null +++ b/src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Cursor agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class CursorAgent(AgentBootstrap): + """Bootstrap for Cursor.""" + + AGENT_DIR = ".cursor" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Cursor agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Cursor agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/cursor-agent/speckit-agent.yml b/src/specify_cli/core_pack/agents/cursor-agent/speckit-agent.yml new file mode 100644 index 00000000..871658c2 --- /dev/null +++ b/src/specify_cli/core_pack/agents/cursor-agent/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "cursor-agent" + name: "Cursor" + version: "1.0.0" + description: "Cursor IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'cursor'] + +command_registration: + commands_dir: ".cursor/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/gemini/__init__.py b/src/specify_cli/core_pack/agents/gemini/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/gemini/bootstrap.py b/src/specify_cli/core_pack/agents/gemini/bootstrap.py new file mode 100644 index 00000000..8e18e5a7 --- /dev/null +++ b/src/specify_cli/core_pack/agents/gemini/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Gemini CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Gemini(AgentBootstrap): + """Bootstrap for Gemini CLI.""" + + AGENT_DIR = ".gemini" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Gemini CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Gemini CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/gemini/speckit-agent.yml b/src/specify_cli/core_pack/agents/gemini/speckit-agent.yml new file mode 100644 index 00000000..23864abf --- /dev/null +++ b/src/specify_cli/core_pack/agents/gemini/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "gemini" + name: "Gemini CLI" + version: "1.0.0" + description: "Google's Gemini CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://github.com/google-gemini/gemini-cli" + cli_tool: "gemini" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'google', 'gemini'] + +command_registration: + commands_dir: ".gemini/commands" + format: "toml" + arg_placeholder: "{{args}}" + file_extension: ".toml" diff --git a/src/specify_cli/core_pack/agents/iflow/__init__.py b/src/specify_cli/core_pack/agents/iflow/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/iflow/bootstrap.py b/src/specify_cli/core_pack/agents/iflow/bootstrap.py new file mode 100644 index 00000000..d421924d --- /dev/null +++ b/src/specify_cli/core_pack/agents/iflow/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for iFlow CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Iflow(AgentBootstrap): + """Bootstrap for iFlow CLI.""" + + AGENT_DIR = ".iflow" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install iFlow CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove iFlow CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/iflow/speckit-agent.yml b/src/specify_cli/core_pack/agents/iflow/speckit-agent.yml new file mode 100644 index 00000000..d148bc23 --- /dev/null +++ b/src/specify_cli/core_pack/agents/iflow/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "iflow" + name: "iFlow CLI" + version: "1.0.0" + description: "iFlow CLI by iflow-ai for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://docs.iflow.cn/en/cli/quickstart" + cli_tool: "iflow" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'iflow'] + +command_registration: + commands_dir: ".iflow/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/junie/__init__.py b/src/specify_cli/core_pack/agents/junie/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/junie/bootstrap.py b/src/specify_cli/core_pack/agents/junie/bootstrap.py new file mode 100644 index 00000000..6748ec7d --- /dev/null +++ b/src/specify_cli/core_pack/agents/junie/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Junie agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Junie(AgentBootstrap): + """Bootstrap for Junie.""" + + AGENT_DIR = ".junie" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Junie agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Junie agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/junie/speckit-agent.yml b/src/specify_cli/core_pack/agents/junie/speckit-agent.yml new file mode 100644 index 00000000..65ea20ca --- /dev/null +++ b/src/specify_cli/core_pack/agents/junie/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "junie" + name: "Junie" + version: "1.0.0" + description: "Junie by JetBrains for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://junie.jetbrains.com/" + cli_tool: "junie" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'jetbrains', 'junie'] + +command_registration: + commands_dir: ".junie/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/kilocode/__init__.py b/src/specify_cli/core_pack/agents/kilocode/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/kilocode/bootstrap.py b/src/specify_cli/core_pack/agents/kilocode/bootstrap.py new file mode 100644 index 00000000..f88f00f4 --- /dev/null +++ b/src/specify_cli/core_pack/agents/kilocode/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Kilo Code agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Kilocode(AgentBootstrap): + """Bootstrap for Kilo Code.""" + + AGENT_DIR = ".kilocode" + COMMANDS_SUBDIR = "workflows" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Kilo Code agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Kilo Code agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml b/src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml new file mode 100644 index 00000000..1b4519f4 --- /dev/null +++ b/src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "kilocode" + name: "Kilo Code" + version: "1.0.0" + description: "Kilo Code IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'kilocode'] + +command_registration: + commands_dir: ".kilocode/workflows" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/kimi/__init__.py b/src/specify_cli/core_pack/agents/kimi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/kimi/bootstrap.py b/src/specify_cli/core_pack/agents/kimi/bootstrap.py new file mode 100644 index 00000000..50b8ca29 --- /dev/null +++ b/src/specify_cli/core_pack/agents/kimi/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Kimi Code agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Kimi(AgentBootstrap): + """Bootstrap for Kimi Code.""" + + AGENT_DIR = ".kimi" + COMMANDS_SUBDIR = "skills" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Kimi Code agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Kimi Code agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/kimi/speckit-agent.yml b/src/specify_cli/core_pack/agents/kimi/speckit-agent.yml new file mode 100644 index 00000000..b439289d --- /dev/null +++ b/src/specify_cli/core_pack/agents/kimi/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "kimi" + name: "Kimi Code" + version: "1.0.0" + description: "Kimi Code CLI by Moonshot AI with skills support" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://code.kimi.com/" + cli_tool: "kimi" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'moonshot', 'kimi', 'skills'] + +command_registration: + commands_dir: ".kimi/skills" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: "/SKILL.md" diff --git a/src/specify_cli/core_pack/agents/kiro-cli/__init__.py b/src/specify_cli/core_pack/agents/kiro-cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py b/src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py new file mode 100644 index 00000000..1f2e1c21 --- /dev/null +++ b/src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Kiro CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class KiroCli(AgentBootstrap): + """Bootstrap for Kiro CLI.""" + + AGENT_DIR = ".kiro" + COMMANDS_SUBDIR = "prompts" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Kiro CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Kiro CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml b/src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml new file mode 100644 index 00000000..80b23f3a --- /dev/null +++ b/src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "kiro-cli" + name: "Kiro CLI" + version: "1.0.0" + description: "Kiro CLI by Amazon for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://kiro.dev/docs/cli/" + cli_tool: "kiro-cli" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'amazon', 'kiro'] + +command_registration: + commands_dir: ".kiro/prompts" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/opencode/__init__.py b/src/specify_cli/core_pack/agents/opencode/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/opencode/bootstrap.py b/src/specify_cli/core_pack/agents/opencode/bootstrap.py new file mode 100644 index 00000000..b1cc30de --- /dev/null +++ b/src/specify_cli/core_pack/agents/opencode/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for opencode agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Opencode(AgentBootstrap): + """Bootstrap for opencode.""" + + AGENT_DIR = ".opencode" + COMMANDS_SUBDIR = "command" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install opencode agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove opencode agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/opencode/speckit-agent.yml b/src/specify_cli/core_pack/agents/opencode/speckit-agent.yml new file mode 100644 index 00000000..9720592d --- /dev/null +++ b/src/specify_cli/core_pack/agents/opencode/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "opencode" + name: "opencode" + version: "1.0.0" + description: "opencode CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://opencode.ai" + cli_tool: "opencode" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'opencode'] + +command_registration: + commands_dir: ".opencode/command" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/pi/__init__.py b/src/specify_cli/core_pack/agents/pi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/pi/bootstrap.py b/src/specify_cli/core_pack/agents/pi/bootstrap.py new file mode 100644 index 00000000..51b3cc7b --- /dev/null +++ b/src/specify_cli/core_pack/agents/pi/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Pi Coding Agent agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Pi(AgentBootstrap): + """Bootstrap for Pi Coding Agent.""" + + AGENT_DIR = ".pi" + COMMANDS_SUBDIR = "prompts" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Pi Coding Agent agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Pi Coding Agent agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/pi/speckit-agent.yml b/src/specify_cli/core_pack/agents/pi/speckit-agent.yml new file mode 100644 index 00000000..31d94f7b --- /dev/null +++ b/src/specify_cli/core_pack/agents/pi/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "pi" + name: "Pi Coding Agent" + version: "1.0.0" + description: "Pi terminal coding agent for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://www.npmjs.com/package/@mariozechner/pi-coding-agent" + cli_tool: "pi" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'pi'] + +command_registration: + commands_dir: ".pi/prompts" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/qodercli/__init__.py b/src/specify_cli/core_pack/agents/qodercli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/qodercli/bootstrap.py b/src/specify_cli/core_pack/agents/qodercli/bootstrap.py new file mode 100644 index 00000000..cbfb5c82 --- /dev/null +++ b/src/specify_cli/core_pack/agents/qodercli/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Qoder CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Qodercli(AgentBootstrap): + """Bootstrap for Qoder CLI.""" + + AGENT_DIR = ".qoder" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Qoder CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Qoder CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml b/src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml new file mode 100644 index 00000000..38893696 --- /dev/null +++ b/src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "qodercli" + name: "Qoder CLI" + version: "1.0.0" + description: "Qoder CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://qoder.com/cli" + cli_tool: "qodercli" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'qoder'] + +command_registration: + commands_dir: ".qoder/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/qwen/__init__.py b/src/specify_cli/core_pack/agents/qwen/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/qwen/bootstrap.py b/src/specify_cli/core_pack/agents/qwen/bootstrap.py new file mode 100644 index 00000000..186fe2ad --- /dev/null +++ b/src/specify_cli/core_pack/agents/qwen/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Qwen Code agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Qwen(AgentBootstrap): + """Bootstrap for Qwen Code.""" + + AGENT_DIR = ".qwen" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Qwen Code agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Qwen Code agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/qwen/speckit-agent.yml b/src/specify_cli/core_pack/agents/qwen/speckit-agent.yml new file mode 100644 index 00000000..fdf7261d --- /dev/null +++ b/src/specify_cli/core_pack/agents/qwen/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "qwen" + name: "Qwen Code" + version: "1.0.0" + description: "Alibaba's Qwen Code CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://github.com/QwenLM/qwen-code" + cli_tool: "qwen" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'alibaba', 'qwen'] + +command_registration: + commands_dir: ".qwen/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/roo/__init__.py b/src/specify_cli/core_pack/agents/roo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/roo/bootstrap.py b/src/specify_cli/core_pack/agents/roo/bootstrap.py new file mode 100644 index 00000000..f1509314 --- /dev/null +++ b/src/specify_cli/core_pack/agents/roo/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Roo Code agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Roo(AgentBootstrap): + """Bootstrap for Roo Code.""" + + AGENT_DIR = ".roo" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Roo Code agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Roo Code agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/roo/speckit-agent.yml b/src/specify_cli/core_pack/agents/roo/speckit-agent.yml new file mode 100644 index 00000000..44d80286 --- /dev/null +++ b/src/specify_cli/core_pack/agents/roo/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "roo" + name: "Roo Code" + version: "1.0.0" + description: "Roo Code IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'roo'] + +command_registration: + commands_dir: ".roo/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/shai/__init__.py b/src/specify_cli/core_pack/agents/shai/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/shai/bootstrap.py b/src/specify_cli/core_pack/agents/shai/bootstrap.py new file mode 100644 index 00000000..968618d1 --- /dev/null +++ b/src/specify_cli/core_pack/agents/shai/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for SHAI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Shai(AgentBootstrap): + """Bootstrap for SHAI.""" + + AGENT_DIR = ".shai" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install SHAI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove SHAI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/shai/speckit-agent.yml b/src/specify_cli/core_pack/agents/shai/speckit-agent.yml new file mode 100644 index 00000000..e1cf6676 --- /dev/null +++ b/src/specify_cli/core_pack/agents/shai/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "shai" + name: "SHAI" + version: "1.0.0" + description: "SHAI CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://github.com/ovh/shai" + cli_tool: "shai" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'ovh', 'shai'] + +command_registration: + commands_dir: ".shai/commands" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/tabnine/__init__.py b/src/specify_cli/core_pack/agents/tabnine/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/tabnine/bootstrap.py b/src/specify_cli/core_pack/agents/tabnine/bootstrap.py new file mode 100644 index 00000000..f04411f3 --- /dev/null +++ b/src/specify_cli/core_pack/agents/tabnine/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Tabnine CLI agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Tabnine(AgentBootstrap): + """Bootstrap for Tabnine CLI.""" + + AGENT_DIR = ".tabnine/agent" + COMMANDS_SUBDIR = "commands" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Tabnine CLI agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Tabnine CLI agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml b/src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml new file mode 100644 index 00000000..cb1dc5d0 --- /dev/null +++ b/src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "tabnine" + name: "Tabnine CLI" + version: "1.0.0" + description: "Tabnine CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://docs.tabnine.com/main/getting-started/tabnine-cli" + cli_tool: "tabnine" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'tabnine'] + +command_registration: + commands_dir: ".tabnine/agent/commands" + format: "toml" + arg_placeholder: "{{args}}" + file_extension: ".toml" diff --git a/src/specify_cli/core_pack/agents/trae/__init__.py b/src/specify_cli/core_pack/agents/trae/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/trae/bootstrap.py b/src/specify_cli/core_pack/agents/trae/bootstrap.py new file mode 100644 index 00000000..264be5b6 --- /dev/null +++ b/src/specify_cli/core_pack/agents/trae/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Trae agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Trae(AgentBootstrap): + """Bootstrap for Trae.""" + + AGENT_DIR = ".trae" + COMMANDS_SUBDIR = "rules" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Trae agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Trae agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/trae/speckit-agent.yml b/src/specify_cli/core_pack/agents/trae/speckit-agent.yml new file mode 100644 index 00000000..d551d860 --- /dev/null +++ b/src/specify_cli/core_pack/agents/trae/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "trae" + name: "Trae" + version: "1.0.0" + description: "Trae IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'trae'] + +command_registration: + commands_dir: ".trae/rules" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/vibe/__init__.py b/src/specify_cli/core_pack/agents/vibe/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/vibe/bootstrap.py b/src/specify_cli/core_pack/agents/vibe/bootstrap.py new file mode 100644 index 00000000..955dece0 --- /dev/null +++ b/src/specify_cli/core_pack/agents/vibe/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Mistral Vibe agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Vibe(AgentBootstrap): + """Bootstrap for Mistral Vibe.""" + + AGENT_DIR = ".vibe" + COMMANDS_SUBDIR = "prompts" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Mistral Vibe agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Mistral Vibe agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/vibe/speckit-agent.yml b/src/specify_cli/core_pack/agents/vibe/speckit-agent.yml new file mode 100644 index 00000000..ae82f0f5 --- /dev/null +++ b/src/specify_cli/core_pack/agents/vibe/speckit-agent.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +agent: + id: "vibe" + name: "Mistral Vibe" + version: "1.0.0" + description: "Mistral Vibe CLI for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: true + install_url: "https://github.com/mistralai/mistral-vibe" + cli_tool: "vibe" + +requires: + speckit_version: ">=0.1.0" + +tags: ['cli', 'mistral', 'vibe'] + +command_registration: + commands_dir: ".vibe/prompts" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/src/specify_cli/core_pack/agents/windsurf/__init__.py b/src/specify_cli/core_pack/agents/windsurf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specify_cli/core_pack/agents/windsurf/bootstrap.py b/src/specify_cli/core_pack/agents/windsurf/bootstrap.py new file mode 100644 index 00000000..13318618 --- /dev/null +++ b/src/specify_cli/core_pack/agents/windsurf/bootstrap.py @@ -0,0 +1,25 @@ +"""Bootstrap module for Windsurf agent pack.""" + +from pathlib import Path +from typing import Any, Dict + +from specify_cli.agent_pack import AgentBootstrap + + +class Windsurf(AgentBootstrap): + """Bootstrap for Windsurf.""" + + AGENT_DIR = ".windsurf" + COMMANDS_SUBDIR = "workflows" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + """Install Windsurf agent files into the project.""" + commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR + commands_dir.mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + """Remove Windsurf agent files from the project.""" + import shutil + agent_dir = project_path / self.AGENT_DIR + if agent_dir.is_dir(): + shutil.rmtree(agent_dir) diff --git a/src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml b/src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml new file mode 100644 index 00000000..9618a51c --- /dev/null +++ b/src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +agent: + id: "windsurf" + name: "Windsurf" + version: "1.0.0" + description: "Windsurf IDE for AI-assisted development" + author: "github" + license: "MIT" + +runtime: + requires_cli: false + +requires: + speckit_version: ">=0.1.0" + +tags: ['ide', 'windsurf'] + +command_registration: + commands_dir: ".windsurf/workflows" + format: "markdown" + arg_placeholder: "$ARGUMENTS" + file_extension: ".md" diff --git a/tests/test_agent_pack.py b/tests/test_agent_pack.py new file mode 100644 index 00000000..77b5a74d --- /dev/null +++ b/tests/test_agent_pack.py @@ -0,0 +1,520 @@ +"""Tests for the agent pack infrastructure. + +Covers manifest validation, bootstrap API contract, pack resolution order, +CLI commands, and consistency with AGENT_CONFIG / CommandRegistrar.AGENT_CONFIGS. +""" + +import json +import shutil +import textwrap +from pathlib import Path + +import pytest +import yaml + +from specify_cli.agent_pack import ( + BOOTSTRAP_FILENAME, + MANIFEST_FILENAME, + MANIFEST_SCHEMA_VERSION, + AgentBootstrap, + AgentManifest, + AgentPackError, + ManifestValidationError, + PackResolutionError, + ResolvedPack, + export_pack, + list_all_agents, + list_embedded_agents, + load_bootstrap, + resolve_agent_pack, + validate_pack, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _write_manifest(path: Path, data: dict) -> Path: + """Write a speckit-agent.yml to *path* and return the file path.""" + path.mkdir(parents=True, exist_ok=True) + manifest_file = path / MANIFEST_FILENAME + manifest_file.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8") + return manifest_file + + +def _minimal_manifest_dict(agent_id: str = "test-agent", **overrides) -> dict: + """Return a minimal valid manifest dict, with optional overrides.""" + data = { + "schema_version": MANIFEST_SCHEMA_VERSION, + "agent": { + "id": agent_id, + "name": "Test Agent", + "version": "0.1.0", + "description": "A test agent", + }, + "runtime": {"requires_cli": False}, + "requires": {"speckit_version": ">=0.1.0"}, + "tags": ["test"], + "command_registration": { + "commands_dir": f".{agent_id}/commands", + "format": "markdown", + "arg_placeholder": "$ARGUMENTS", + "file_extension": ".md", + }, + } + data.update(overrides) + return data + + +def _write_bootstrap(pack_dir: Path, class_name: str = "TestAgent", agent_dir: str = ".test-agent") -> Path: + """Write a minimal bootstrap.py to *pack_dir*.""" + pack_dir.mkdir(parents=True, exist_ok=True) + bootstrap_file = pack_dir / BOOTSTRAP_FILENAME + bootstrap_file.write_text(textwrap.dedent(f"""\ + from pathlib import Path + from typing import Any, Dict + from specify_cli.agent_pack import AgentBootstrap + + class {class_name}(AgentBootstrap): + AGENT_DIR = "{agent_dir}" + + def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None: + (project_path / self.AGENT_DIR / "commands").mkdir(parents=True, exist_ok=True) + + def teardown(self, project_path: Path) -> None: + import shutil + d = project_path / self.AGENT_DIR + if d.is_dir(): + shutil.rmtree(d) + """), encoding="utf-8") + return bootstrap_file + + +# =================================================================== +# Manifest validation +# =================================================================== + +class TestManifestValidation: + """Validate speckit-agent.yml parsing and error handling.""" + + def test_valid_manifest(self, tmp_path): + data = _minimal_manifest_dict() + _write_manifest(tmp_path, data) + m = AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + assert m.id == "test-agent" + assert m.name == "Test Agent" + assert m.version == "0.1.0" + assert m.command_format == "markdown" + + def test_missing_schema_version(self, tmp_path): + data = _minimal_manifest_dict() + del data["schema_version"] + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="Missing required top-level"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_wrong_schema_version(self, tmp_path): + data = _minimal_manifest_dict() + data["schema_version"] = "99.0" + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="Unsupported schema_version"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_missing_agent_block(self, tmp_path): + data = {"schema_version": MANIFEST_SCHEMA_VERSION} + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="Missing required top-level"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_missing_agent_id(self, tmp_path): + data = _minimal_manifest_dict() + del data["agent"]["id"] + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="Missing required agent key"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_missing_agent_name(self, tmp_path): + data = _minimal_manifest_dict() + del data["agent"]["name"] + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="Missing required agent key"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_missing_agent_version(self, tmp_path): + data = _minimal_manifest_dict() + del data["agent"]["version"] + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="Missing required agent key"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_agent_block_not_dict(self, tmp_path): + data = {"schema_version": MANIFEST_SCHEMA_VERSION, "agent": "not-a-dict"} + _write_manifest(tmp_path, data) + with pytest.raises(ManifestValidationError, match="must be a mapping"): + AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + + def test_missing_file(self, tmp_path): + with pytest.raises(ManifestValidationError, match="Manifest not found"): + AgentManifest.from_yaml(tmp_path / "nonexistent" / MANIFEST_FILENAME) + + def test_invalid_yaml(self, tmp_path): + tmp_path.mkdir(parents=True, exist_ok=True) + bad = tmp_path / MANIFEST_FILENAME + bad.write_text("{{{{bad yaml", encoding="utf-8") + with pytest.raises(ManifestValidationError, match="Invalid YAML"): + AgentManifest.from_yaml(bad) + + def test_runtime_fields(self, tmp_path): + data = _minimal_manifest_dict() + data["runtime"] = { + "requires_cli": True, + "install_url": "https://example.com", + "cli_tool": "myagent", + } + _write_manifest(tmp_path, data) + m = AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + assert m.requires_cli is True + assert m.install_url == "https://example.com" + assert m.cli_tool == "myagent" + + def test_command_registration_fields(self, tmp_path): + data = _minimal_manifest_dict() + data["command_registration"] = { + "commands_dir": ".test/commands", + "format": "toml", + "arg_placeholder": "{{args}}", + "file_extension": ".toml", + } + _write_manifest(tmp_path, data) + m = AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + assert m.commands_dir == ".test/commands" + assert m.command_format == "toml" + assert m.arg_placeholder == "{{args}}" + assert m.file_extension == ".toml" + + def test_tags_field(self, tmp_path): + data = _minimal_manifest_dict() + data["tags"] = ["cli", "test", "agent"] + _write_manifest(tmp_path, data) + m = AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + assert m.tags == ["cli", "test", "agent"] + + def test_optional_fields_default(self, tmp_path): + """Manifest with only required fields uses sensible defaults.""" + data = { + "schema_version": MANIFEST_SCHEMA_VERSION, + "agent": {"id": "bare", "name": "Bare Agent", "version": "0.0.1"}, + } + _write_manifest(tmp_path, data) + m = AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + assert m.requires_cli is False + assert m.install_url is None + assert m.command_format == "markdown" + assert m.arg_placeholder == "$ARGUMENTS" + assert m.tags == [] + + def test_from_dict(self): + data = _minimal_manifest_dict("dict-agent") + m = AgentManifest.from_dict(data) + assert m.id == "dict-agent" + assert m.pack_path is None + + def test_from_dict_not_dict(self): + with pytest.raises(ManifestValidationError, match="must be a YAML mapping"): + AgentManifest.from_dict("not-a-dict") + + +# =================================================================== +# Bootstrap API contract +# =================================================================== + +class TestBootstrapContract: + """Verify AgentBootstrap interface and load_bootstrap().""" + + def test_base_class_setup_raises(self, tmp_path): + m = AgentManifest.from_dict(_minimal_manifest_dict()) + b = AgentBootstrap(m) + with pytest.raises(NotImplementedError): + b.setup(tmp_path, "sh", {}) + + def test_base_class_teardown_raises(self, tmp_path): + m = AgentManifest.from_dict(_minimal_manifest_dict()) + b = AgentBootstrap(m) + with pytest.raises(NotImplementedError): + b.teardown(tmp_path) + + def test_load_bootstrap(self, tmp_path): + data = _minimal_manifest_dict() + _write_manifest(tmp_path, data) + _write_bootstrap(tmp_path) + m = AgentManifest.from_yaml(tmp_path / MANIFEST_FILENAME) + b = load_bootstrap(tmp_path, m) + assert isinstance(b, AgentBootstrap) + + def test_load_bootstrap_missing_file(self, tmp_path): + m = AgentManifest.from_dict(_minimal_manifest_dict()) + with pytest.raises(AgentPackError, match="Bootstrap module not found"): + load_bootstrap(tmp_path, m) + + def test_bootstrap_setup_and_teardown(self, tmp_path): + """Verify a loaded bootstrap can set up and tear down.""" + pack_dir = tmp_path / "pack" + data = _minimal_manifest_dict() + _write_manifest(pack_dir, data) + _write_bootstrap(pack_dir) + + m = AgentManifest.from_yaml(pack_dir / MANIFEST_FILENAME) + b = load_bootstrap(pack_dir, m) + + project = tmp_path / "project" + project.mkdir() + + b.setup(project, "sh", {}) + assert (project / ".test-agent" / "commands").is_dir() + + b.teardown(project) + assert not (project / ".test-agent").exists() + + def test_load_bootstrap_no_subclass(self, tmp_path): + """A bootstrap module without an AgentBootstrap subclass fails.""" + pack_dir = tmp_path / "pack" + pack_dir.mkdir(parents=True) + data = _minimal_manifest_dict() + _write_manifest(pack_dir, data) + (pack_dir / BOOTSTRAP_FILENAME).write_text("x = 1\n", encoding="utf-8") + m = AgentManifest.from_yaml(pack_dir / MANIFEST_FILENAME) + with pytest.raises(AgentPackError, match="No AgentBootstrap subclass"): + load_bootstrap(pack_dir, m) + + +# =================================================================== +# Pack resolution +# =================================================================== + +class TestResolutionOrder: + """Verify the 4-level priority resolution stack.""" + + def test_embedded_resolution(self): + """Embedded agents are resolvable (at least claude should exist).""" + resolved = resolve_agent_pack("claude") + assert resolved.source == "embedded" + assert resolved.manifest.id == "claude" + + def test_missing_agent_raises(self): + with pytest.raises(PackResolutionError, match="not found"): + resolve_agent_pack("nonexistent-agent-xyz") + + def test_project_level_overrides_embedded(self, tmp_path): + """A project-level pack shadows the embedded pack.""" + proj_agents = tmp_path / ".specify" / "agents" / "claude" + data = _minimal_manifest_dict("claude") + data["agent"]["version"] = "99.0.0" + _write_manifest(proj_agents, data) + _write_bootstrap(proj_agents, class_name="ClaudeOverride", agent_dir=".claude") + + resolved = resolve_agent_pack("claude", project_path=tmp_path) + assert resolved.source == "project" + assert resolved.manifest.version == "99.0.0" + + def test_user_level_overrides_everything(self, tmp_path, monkeypatch): + """A user-level pack has highest priority.""" + from specify_cli import agent_pack + + user_dir = tmp_path / "user_agents" + monkeypatch.setattr(agent_pack, "_user_agents_dir", lambda: user_dir) + + user_pack = user_dir / "claude" + data = _minimal_manifest_dict("claude") + data["agent"]["version"] = "999.0.0" + _write_manifest(user_pack, data) + + # Also create a project-level override + proj_agents = tmp_path / "project" / ".specify" / "agents" / "claude" + data2 = _minimal_manifest_dict("claude") + data2["agent"]["version"] = "50.0.0" + _write_manifest(proj_agents, data2) + + resolved = resolve_agent_pack("claude", project_path=tmp_path / "project") + assert resolved.source == "user" + assert resolved.manifest.version == "999.0.0" + + def test_catalog_overrides_embedded(self, tmp_path, monkeypatch): + """A catalog-cached pack overrides embedded.""" + from specify_cli import agent_pack + + cache_dir = tmp_path / "agent-cache" + monkeypatch.setattr(agent_pack, "_catalog_agents_dir", lambda: cache_dir) + + catalog_pack = cache_dir / "claude" + data = _minimal_manifest_dict("claude") + data["agent"]["version"] = "2.0.0" + _write_manifest(catalog_pack, data) + + resolved = resolve_agent_pack("claude") + assert resolved.source == "catalog" + assert resolved.manifest.version == "2.0.0" + + +# =================================================================== +# List and discovery +# =================================================================== + +class TestDiscovery: + """Verify list_embedded_agents() and list_all_agents().""" + + def test_list_embedded_agents_nonempty(self): + agents = list_embedded_agents() + assert len(agents) >= 25 + ids = {a.id for a in agents} + assert "claude" in ids + assert "gemini" in ids + assert "copilot" in ids + + def test_list_all_agents(self): + all_agents = list_all_agents() + assert len(all_agents) >= 25 + # Should be sorted by id + ids = [a.manifest.id for a in all_agents] + assert ids == sorted(ids) + + +# =================================================================== +# Validate pack +# =================================================================== + +class TestValidatePack: + + def test_valid_pack(self, tmp_path): + pack_dir = tmp_path / "pack" + data = _minimal_manifest_dict() + _write_manifest(pack_dir, data) + _write_bootstrap(pack_dir) + warnings = validate_pack(pack_dir) + assert warnings == [] # All fields present, no warnings + + def test_missing_manifest(self, tmp_path): + pack_dir = tmp_path / "pack" + pack_dir.mkdir(parents=True) + with pytest.raises(ManifestValidationError, match="Missing"): + validate_pack(pack_dir) + + def test_missing_bootstrap_warning(self, tmp_path): + pack_dir = tmp_path / "pack" + data = _minimal_manifest_dict() + _write_manifest(pack_dir, data) + warnings = validate_pack(pack_dir) + assert any("bootstrap.py" in w for w in warnings) + + def test_missing_description_warning(self, tmp_path): + pack_dir = tmp_path / "pack" + data = _minimal_manifest_dict() + data["agent"]["description"] = "" + _write_manifest(pack_dir, data) + _write_bootstrap(pack_dir) + warnings = validate_pack(pack_dir) + assert any("description" in w for w in warnings) + + def test_missing_tags_warning(self, tmp_path): + pack_dir = tmp_path / "pack" + data = _minimal_manifest_dict() + data["tags"] = [] + _write_manifest(pack_dir, data) + _write_bootstrap(pack_dir) + warnings = validate_pack(pack_dir) + assert any("tags" in w.lower() for w in warnings) + + +# =================================================================== +# Export pack +# =================================================================== + +class TestExportPack: + + def test_export_embedded(self, tmp_path): + dest = tmp_path / "export" + result = export_pack("claude", dest) + assert (result / MANIFEST_FILENAME).is_file() + assert (result / BOOTSTRAP_FILENAME).is_file() + + +# =================================================================== +# Embedded packs consistency with AGENT_CONFIG +# =================================================================== + +class TestEmbeddedPacksConsistency: + """Ensure embedded agent packs match the runtime AGENT_CONFIG.""" + + def test_all_agent_config_agents_have_embedded_packs(self): + """Every agent in AGENT_CONFIG (except 'generic') has an embedded pack.""" + from specify_cli import AGENT_CONFIG + + embedded = {m.id for m in list_embedded_agents()} + + for agent_key in AGENT_CONFIG: + if agent_key == "generic": + continue + assert agent_key in embedded, ( + f"Agent '{agent_key}' is in AGENT_CONFIG but has no embedded pack" + ) + + def test_embedded_packs_match_agent_config_metadata(self): + """Embedded pack manifests are consistent with AGENT_CONFIG fields.""" + from specify_cli import AGENT_CONFIG + + for manifest in list_embedded_agents(): + config = AGENT_CONFIG.get(manifest.id) + if config is None: + continue # Extra embedded packs are fine + + assert manifest.name == config["name"], ( + f"{manifest.id}: name mismatch: pack={manifest.name!r} config={config['name']!r}" + ) + assert manifest.requires_cli == config["requires_cli"], ( + f"{manifest.id}: requires_cli mismatch" + ) + + if config.get("install_url"): + assert manifest.install_url == config["install_url"], ( + f"{manifest.id}: install_url mismatch" + ) + + def test_embedded_packs_match_command_registrar(self): + """Embedded pack command_registration matches CommandRegistrar.AGENT_CONFIGS.""" + from specify_cli.agents import CommandRegistrar + + for manifest in list_embedded_agents(): + registrar_config = CommandRegistrar.AGENT_CONFIGS.get(manifest.id) + if registrar_config is None: + # Some agents in AGENT_CONFIG may not be in the registrar + # (e.g., agy, vibe — recently added) + continue + + assert manifest.commands_dir == registrar_config["dir"], ( + f"{manifest.id}: commands_dir mismatch: " + f"pack={manifest.commands_dir!r} registrar={registrar_config['dir']!r}" + ) + assert manifest.command_format == registrar_config["format"], ( + f"{manifest.id}: format mismatch" + ) + assert manifest.arg_placeholder == registrar_config["args"], ( + f"{manifest.id}: arg_placeholder mismatch" + ) + assert manifest.file_extension == registrar_config["extension"], ( + f"{manifest.id}: file_extension mismatch" + ) + + def test_each_embedded_pack_validates(self): + """Every embedded pack passes validate_pack().""" + from specify_cli.agent_pack import _embedded_agents_dir + + agents_dir = _embedded_agents_dir() + for child in sorted(agents_dir.iterdir()): + if not child.is_dir(): + continue + manifest_file = child / MANIFEST_FILENAME + if not manifest_file.is_file(): + continue + # Should not raise + warnings = validate_pack(child) + # Warnings are acceptable; hard errors are not