mirror of
https://github.com/github/spec-kit.git
synced 2026-03-21 04:43:08 +00:00
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
This commit is contained in:
committed by
GitHub
parent
8b20d0b336
commit
3212309e7c
@@ -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 = [
|
||||
|
||||
@@ -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 <path> 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(
|
||||
|
||||
478
src/specify_cli/agent_pack.py
Normal file
478
src/specify_cli/agent_pack.py
Normal file
@@ -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/<id>/)
|
||||
2. Project-level (.specify/agents/<id>/)
|
||||
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 <agent>`` and
|
||||
``specify agent switch <agent>``.
|
||||
|
||||
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/<id>/``
|
||||
2. Project-level ``.specify/agents/<id>/``
|
||||
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 <path>' 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
|
||||
0
src/specify_cli/core_pack/agents/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/agy/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/agy/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/agy/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/agy/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/agy/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/agy/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/amp/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/amp/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/amp/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/amp/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/amp/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/amp/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/auggie/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/auggie/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/auggie/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/auggie/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/auggie/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/auggie/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/bob/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/bob/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/bob/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/bob/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/bob/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/bob/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/claude/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/claude/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/claude/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/claude/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/claude/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/claude/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/codebuddy/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/codebuddy/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/codex/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/codex/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/codex/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/codex/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/codex/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/codex/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/copilot/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/copilot/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/copilot/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/copilot/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py
Normal file
@@ -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)
|
||||
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/gemini/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/gemini/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/gemini/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/gemini/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/gemini/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/gemini/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/iflow/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/iflow/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/iflow/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/iflow/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/iflow/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/iflow/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/junie/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/junie/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/junie/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/junie/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/junie/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/junie/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/kilocode/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/kilocode/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/kimi/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/kimi/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/kimi/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/kimi/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/kimi/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/kimi/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/opencode/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/opencode/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/opencode/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/opencode/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/pi/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/pi/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/pi/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/pi/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/pi/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/pi/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/qodercli/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/qodercli/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/qwen/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/qwen/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/qwen/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/qwen/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/qwen/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/qwen/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/roo/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/roo/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/roo/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/roo/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/roo/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/roo/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/shai/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/shai/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/shai/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/shai/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/shai/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/shai/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/tabnine/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/tabnine/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/trae/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/trae/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/trae/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/trae/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/trae/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/trae/speckit-agent.yml
Normal file
@@ -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"
|
||||
0
src/specify_cli/core_pack/agents/vibe/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/vibe/__init__.py
Normal file
25
src/specify_cli/core_pack/agents/vibe/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/vibe/bootstrap.py
Normal file
@@ -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)
|
||||
25
src/specify_cli/core_pack/agents/vibe/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/vibe/speckit-agent.yml
Normal file
@@ -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"
|
||||
25
src/specify_cli/core_pack/agents/windsurf/bootstrap.py
Normal file
25
src/specify_cli/core_pack/agents/windsurf/bootstrap.py
Normal file
@@ -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)
|
||||
23
src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml
Normal file
@@ -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"
|
||||
520
tests/test_agent_pack.py
Normal file
520
tests/test_agent_pack.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user