Compare commits

..

11 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
978addc390 refactor: simplify finalize_setup scan to agent_root only, improve comments
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/054690bb-c048-41e0-b553-377d5cb36b78
2026-03-20 22:32:36 +00:00
copilot-swe-agent[bot]
9b580a536b feat: setup() owns scaffolding and returns actual installed files
- AgentBootstrap._scaffold_project() calls scaffold_from_core_pack,
  snapshots before/after, returns all new files
- finalize_setup() filters agent_files to only track files under the
  agent's own directory tree (shared .specify/ files not tracked)
- All 25 bootstrap setup() methods call _scaffold_project() and return
  the actual file list instead of []
- --agent init flow routes through setup() for scaffolding instead of
  calling scaffold_from_core_pack directly
- 100 new tests (TestSetupReturnsFiles): verify every agent's setup()
  returns non-empty, existing, absolute paths including agent-dir files
- Parity tests use CliRunner to invoke the real init command
- finalize_setup bug fix: skills-migrated agents (agy) now have their
  skills directory scanned correctly
- 1262 tests pass (452 in test_agent_pack.py alone)

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/054690bb-c048-41e0-b553-377d5cb36b78
2026-03-20 22:29:33 +00:00
copilot-swe-agent[bot]
d6016ab9db style: simplify --agent help text, normalize comment spelling
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/930d8c4d-ce42-41fb-a40f-561fb1468e81
2026-03-20 21:54:56 +00:00
copilot-swe-agent[bot]
c2227a7ffd feat: add --agent flag to init for pack-based flow with file tracking
- `specify init --agent claude` resolves through the pack system and
  records all installed files in .specify/agent-manifest-<id>.json via
  finalize_setup() after the init pipeline finishes
- --agent and --ai are mutually exclusive; --agent additionally enables
  tracked teardown/switch
- init-options.json gains "agent_pack" key when --agent is used
- 4 new parity tests verify: pack resolution matches AGENT_CONFIG,
  commands_dir parity, finalize_setup records pipeline-created files,
  pack metadata matches CommandRegistrar configuration

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/930d8c4d-ce42-41fb-a40f-561fb1468e81
2026-03-20 21:53:03 +00:00
copilot-swe-agent[bot]
c3efd1fb71 style: fix f-string formatting in _reregister_extension_commands
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/32e470fc-6bf5-453c-bf6c-79a8521efa56
2026-03-20 21:37:27 +00:00
copilot-swe-agent[bot]
e190116d13 refactor: setup reports files, CLI checks modifications before teardown, categorised manifest
- setup() returns List[Path] of installed files so CLI can record them
- finalize_setup() accepts agent_files + extension_files for combined tracking
- Install manifest categorises files: agent_files and extension_files
- get_tracked_files() returns (agent_files, extension_files) split
- remove_tracked_files() accepts explicit files dict for CLI-driven teardown
- agent_switch checks for modifications BEFORE teardown and prompts user
- _reregister_extension_commands() returns List[Path] of created files
- teardown() accepts files parameter to receive explicit file lists
- All 25 bootstraps updated with new signatures
- 5 new tests: categorised manifest, get_tracked_files, explicit file teardown,
  extension file modification detection

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/32e470fc-6bf5-453c-bf6c-79a8521efa56
2026-03-20 21:34:59 +00:00
copilot-swe-agent[bot]
a63c248c80 Move file recording to finalize_setup() — called after init pipeline writes files
Address code review: setup() now only creates directories, while
finalize_setup() (on base class) scans the agent's commands_dir
for all files and records them. This ensures files are tracked
after the full init pipeline has written them, not before.

- Add AgentBootstrap.finalize_setup() that scans commands_dir
- Remove premature record_installed_files() from all 25 setup() methods
- agent_switch calls finalize_setup() after setup() completes
- Update test helper to match new pattern

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/779eabf6-21d5-428b-9f01-dd363df4c84a
2026-03-20 21:20:22 +00:00
copilot-swe-agent[bot]
b5a5e3fc35 Add installed-file tracking with SHA-256 hashes for safe agent teardown
Setup records installed files and their SHA-256 hashes in
.specify/agent-manifest-<agent_id>.json. Teardown uses the manifest
to remove only individual files (never directories). If any tracked
file was modified since installation, teardown requires --force.

- Add record_installed_files(), check_modified_files(), remove_tracked_files()
  and AgentFileModifiedError to agent_pack.py
- Update all 25 bootstrap modules to use file-tracked setup/teardown
- Add --force flag to 'specify agent switch'
- Add 11 new tests for file tracking (record, check, remove, force,
  directory preservation, deleted-file handling, manifest structure)

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/779eabf6-21d5-428b-9f01-dd363df4c84a
2026-03-20 21:15:48 +00:00
copilot-swe-agent[bot]
ec5471af61 Fix code review issues: safe teardown for shared dirs, less brittle test assertions
- Copilot: only remove .github/agents/ (preserves workflows, templates)
- Tabnine: only remove .tabnine/agent/ (preserves other config)
- Amp/Codex: only remove respective subdirs (commands/skills)
  to avoid deleting each other's files in shared .agents/ dir
- Tests: use flexible assertions instead of hardcoded >= 25 counts

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
2026-03-20 21:03:34 +00:00
copilot-swe-agent[bot]
3212309e7c 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
2026-03-20 21:01:16 +00:00
copilot-swe-agent[bot]
8b20d0b336 Initial plan 2026-03-20 20:46:50 +00:00
80 changed files with 4066 additions and 4 deletions

View File

@@ -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 = [

View File

@@ -36,7 +36,7 @@ import json5
import stat
import yaml
from pathlib import Path
from typing import Any, Optional, Tuple
from typing import Any, List, Optional, Tuple
import typer
import httpx
@@ -1715,6 +1715,7 @@ def _handle_agent_skills_migration(console: Console, agent_key: str) -> None:
def init(
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
ai_assistant: str = typer.Option(None, "--ai", help=AI_ASSISTANT_HELP),
agent: str = typer.Option(None, "--agent", help="AI agent to use (enables file tracking for clean teardown when switching agents). Accepts the same agent IDs as --ai."),
ai_commands_dir: str = typer.Option(None, "--ai-commands-dir", help="Directory for agent command files (required with --ai generic, e.g. .myagent/commands/)"),
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
@@ -1753,6 +1754,7 @@ def init(
Examples:
specify init my-project
specify init my-project --ai claude
specify init my-project --agent claude # Pack-based flow (with file tracking)
specify init my-project --ai copilot --no-git
specify init --ignore-agent-tools my-project
specify init . --ai claude # Initialize in current directory
@@ -1765,6 +1767,7 @@ def init(
specify init --here --force # Skip confirmation when current directory not empty
specify init my-project --ai claude --ai-skills # Install agent skills
specify init --here --ai gemini --ai-skills
specify init my-project --agent claude --ai-skills # Pack-based flow with skills
specify init my-project --ai generic --ai-commands-dir .myagent/commands/ # Unsupported agent
specify init my-project --offline # Use bundled assets (no network access)
specify init my-project --ai claude --preset healthcare-compliance # With preset
@@ -1772,6 +1775,17 @@ def init(
show_banner()
# --agent and --ai are interchangeable for agent selection, but --agent
# additionally opts into the pack-based flow (file tracking via
# finalize_setup for tracked teardown/switch).
use_agent_pack = False
if agent:
if ai_assistant:
console.print("[red]Error:[/red] --agent and --ai cannot both be specified. Use one or the other.")
raise typer.Exit(1)
ai_assistant = agent
use_agent_pack = True
# Detect when option values are likely misinterpreted flags (parameter ordering issue)
if ai_assistant and ai_assistant.startswith("--"):
console.print(f"[red]Error:[/red] Invalid value for --ai: '{ai_assistant}'")
@@ -1802,7 +1816,7 @@ def init(
raise typer.Exit(1)
if ai_skills and not ai_assistant:
console.print("[red]Error:[/red] --ai-skills requires --ai to be specified")
console.print("[red]Error:[/red] --ai-skills requires --ai or --agent to be specified")
console.print("[yellow]Usage:[/yellow] specify init <project> --ai <agent> --ai-skills")
raise typer.Exit(1)
@@ -1854,6 +1868,19 @@ def init(
"copilot"
)
# When --agent is used, validate that the agent resolves through the pack
# system and prepare the bootstrap for post-init file tracking.
agent_bootstrap = None
if use_agent_pack:
from .agent_pack import resolve_agent_pack, load_bootstrap, PackResolutionError, AgentPackError
try:
resolved = resolve_agent_pack(selected_ai)
agent_bootstrap = load_bootstrap(resolved.path, resolved.manifest)
console.print(f"[dim]Pack-based flow: {resolved.manifest.name} ({resolved.source})[/dim]")
except (PackResolutionError, AgentPackError) as exc:
console.print(f"[red]Error resolving agent pack:[/red] {exc}")
raise typer.Exit(1)
# Agents that have moved from explicit commands/prompts to agent skills.
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
# If selected interactively (no --ai provided), automatically enable
@@ -1957,7 +1984,10 @@ def init(
"This will become the default in v0.6.0."
)
if use_github:
if use_agent_pack:
# Pack-based flow: setup() owns scaffolding, always uses bundled assets.
tracker.add("scaffold", "Apply bundled assets")
elif use_github:
for key, label in [
("fetch", "Fetch latest release"),
("download", "Download template"),
@@ -1992,7 +2022,26 @@ def init(
verify = not skip_tls
local_ssl_context = ssl_context if verify else False
if use_github:
# -- scaffolding ------------------------------------------------
# Pack-based flow (--agent): setup() owns scaffolding and
# returns every file it created. Legacy flow (--ai): scaffold
# directly or download from GitHub.
agent_setup_files: list[Path] = []
if use_agent_pack and agent_bootstrap is not None:
tracker.start("scaffold")
try:
agent_setup_files = agent_bootstrap.setup(
project_path, selected_script, {"here": here})
tracker.complete(
"scaffold",
f"{selected_ai} ({len(agent_setup_files)} files)")
except Exception as exc:
tracker.error("scaffold", str(exc))
if not here and project_path.exists():
shutil.rmtree(project_path)
raise typer.Exit(1)
elif use_github:
with httpx.Client(verify=local_ssl_context) as local_client:
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
else:
@@ -2090,6 +2139,7 @@ def init(
"ai": selected_ai,
"ai_skills": ai_skills,
"ai_commands_dir": ai_commands_dir,
"agent_pack": use_agent_pack,
"branch_numbering": branch_numbering or "sequential",
"here": here,
"preset": preset,
@@ -2133,6 +2183,16 @@ def init(
if not use_github:
tracker.skip("cleanup", "not needed (no download)")
# When --agent is used, record all installed agent files for
# tracked teardown. setup() already returned the files it
# created; pass them to finalize_setup so the manifest is
# accurate. finalize_setup also scans the agent directory
# to catch any additional files created by later pipeline
# steps (skills, extensions, presets).
if use_agent_pack and agent_bootstrap is not None:
agent_bootstrap.finalize_setup(
project_path, agent_files=agent_setup_files)
tracker.complete("final", "project ready")
except (typer.Exit, SystemExit):
raise
@@ -2366,6 +2426,513 @@ 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"),
force: bool = typer.Option(False, "--force", help="Remove agent files even if they were modified since installation"),
):
"""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,
check_modified_files,
get_tracked_files,
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)
# Check for modified files BEFORE teardown and prompt for confirmation
modified = check_modified_files(project_path, current_agent)
if modified and not force:
console.print("[yellow]The following files have been modified since installation:[/yellow]")
for f in modified:
console.print(f" {f}")
if not typer.confirm("Remove these modified files?"):
console.print("[dim]Aborted. Use --force to skip this check.[/dim]")
raise typer.Exit(0)
# Retrieve tracked file lists and feed them into teardown
agent_files, extension_files = get_tracked_files(project_path, current_agent)
all_files = {**agent_files, **extension_files}
console.print(f" [dim]Tearing down {current_agent}...[/dim]")
current_bootstrap.teardown(
project_path,
force=True, # already confirmed above
files=all_files if all_files else None,
)
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]")
agent_files = 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")
# Re-register extension commands for the new agent
extension_files = _reregister_extension_commands(project_path, agent_id)
# Record all installed files (agent + extensions) for tracked teardown
new_bootstrap.finalize_setup(
project_path,
agent_files=agent_files,
extension_files=extension_files,
)
console.print(f"\n[bold green]Successfully switched to {resolved.manifest.name}[/bold green]")
def _reregister_extension_commands(project_path: Path, agent_id: str) -> List[Path]:
"""Re-register all installed extension commands for a new agent after switching.
Returns:
List of absolute file paths created by extension registration.
"""
created_files: List[Path] = []
registry_file = project_path / ".specify" / "extensions" / ".registry"
if not registry_file.is_file():
return created_files
try:
registry_data = json.loads(registry_file.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return created_files
extensions = registry_data.get("extensions", {})
if not extensions:
return created_files
try:
from .agents import CommandRegistrar
registrar = CommandRegistrar()
except ImportError:
return created_files
# Snapshot the commands directory before registration so we can
# detect which files were created by extension commands.
agent_config = registrar.AGENT_CONFIGS.get(agent_id)
if agent_config:
commands_dir = project_path / agent_config["dir"]
pre_existing = set(commands_dir.rglob("*")) if commands_dir.is_dir() else set()
else:
pre_existing = set()
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
# Collect files created by extension registration
if agent_config:
commands_dir = project_path / agent_config["dir"]
if commands_dir.is_dir():
for p in commands_dir.rglob("*"):
if p.is_file() and p not in pre_existing:
created_files.append(p)
if reregistered:
console.print(
f" [green]✓[/green] Re-registered {reregistered} extension command(s) ({len(created_files)} file(s))"
)
return created_files
@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(

View File

@@ -0,0 +1,849 @@
"""
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 hashlib
import importlib.util
import json
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."""
class AgentFileModifiedError(AgentPackError):
"""Raised when teardown finds user-modified files and ``--force`` is not set."""
# ---------------------------------------------------------------------------
# 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]) -> List[Path]:
"""Install agent files into *project_path*.
This is invoked by ``specify init --ai <agent>`` and
``specify agent switch <agent>``.
Implementations **must** return every file they create so that the
CLI can record both agent-installed files and extension-installed
files in a single install manifest.
Args:
project_path: Target project directory.
script_type: ``"sh"`` or ``"ps"``.
options: Arbitrary key/value options forwarded from the CLI.
Returns:
List of absolute paths of files created during setup.
"""
raise NotImplementedError
def teardown(
self,
project_path: Path,
*,
force: bool = False,
files: Optional[Dict[str, str]] = None,
) -> List[str]:
"""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.).
Only individual files are removed — directories are **never**
deleted.
The caller (CLI) is expected to check for user-modified files
**before** invoking teardown and prompt for confirmation. If
*files* is provided, exactly those files are removed (values are
ignored but kept for forward compatibility). Otherwise the
install manifest is read.
Args:
project_path: Project directory to clean up.
force: When ``True``, remove files even if they were modified
after installation.
files: Mapping of project-relative path → SHA-256 hash.
When supplied, only these files are removed and the
install manifest is not consulted.
Returns:
List of project-relative paths that were actually deleted.
"""
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]
def collect_installed_files(self, project_path: Path) -> List[Path]:
"""Return every file under the agent's directory tree.
Subclasses should call this at the end of :meth:`setup` to build
the return list. Any files present in the agent directory at
that point — whether created by ``setup()`` itself, by the
scaffold pipeline, or by a preceding step — are reported.
"""
root = self.agent_dir(project_path)
if not root.is_dir():
return []
return sorted(p for p in root.rglob("*") if p.is_file())
def _scaffold_project(
self,
project_path: Path,
script_type: str,
is_current_dir: bool = False,
) -> List[Path]:
"""Run the shared scaffolding pipeline and return new files.
Calls ``scaffold_from_core_pack`` for this agent and then
collects every file that was created. Subclasses should call
this from :meth:`setup` when they want to use the shared
scaffolding rather than creating files manually.
Returns:
List of absolute paths of **all** files created by the
scaffold (agent-specific commands, shared scripts,
templates, etc.).
"""
# Lazy import to avoid circular dependency (agent_pack is
# imported by specify_cli.__init__).
from specify_cli import scaffold_from_core_pack
# Snapshot existing files
before: set[Path] = set()
if project_path.exists():
before = {p for p in project_path.rglob("*") if p.is_file()}
ok = scaffold_from_core_pack(
project_path, self.manifest.id, script_type, is_current_dir,
)
if not ok:
raise AgentPackError(
f"Scaffolding failed for agent '{self.manifest.id}'")
# Collect every new file
after = {p for p in project_path.rglob("*") if p.is_file()}
return sorted(after - before)
def finalize_setup(
self,
project_path: Path,
agent_files: Optional[List[Path]] = None,
extension_files: Optional[List[Path]] = None,
) -> None:
"""Record installed files for tracked teardown.
This must be called **after** the full init pipeline has finished
writing files (commands, context files, extensions) into the
project. It combines the files reported by :meth:`setup` with
any extra files (e.g. from extension registration), scans the
agent's directory tree for anything additional, and writes the
install manifest.
``setup()`` may return *all* files created by the shared
scaffolding (including shared project files in ``.specify/``).
Only files under the agent's own directory tree are recorded as
``agent_files`` — shared project infrastructure is not tracked
per-agent and will not be removed during teardown.
Args:
agent_files: Files reported by :meth:`setup`.
extension_files: Files created by extension registration.
"""
all_extension = list(extension_files or [])
# Filter agent_files: only keep files under the agent's directory
# tree. setup() returns *all* scaffolded files (including shared
# project infrastructure in .specify/) but only agent-owned files
# should be tracked per-agent — shared files are not removed
# during teardown/switch.
agent_root = self.agent_dir(project_path)
agent_root_resolved = agent_root.resolve()
all_agent: List[Path] = []
for p in (agent_files or []):
try:
p.resolve().relative_to(agent_root_resolved)
all_agent.append(p)
except ValueError:
pass
# Scan the agent's directory tree for files created by later
# init pipeline steps (skills, presets, extensions) that
# setup() did not report. We scan the agent root directory
# (e.g. .claude/) so we catch both commands and skills
# directories (skills-migrated agents replace the commands
# directory with a sibling skills directory during init).
if self.manifest.commands_dir:
agent_root = self.agent_dir(project_path)
if agent_root.is_dir():
agent_set = {p.resolve() for p in all_agent}
for p in agent_root.rglob("*"):
if p.is_file() and p.resolve() not in agent_set:
all_agent.append(p)
agent_set.add(p.resolve())
record_installed_files(
project_path,
self.manifest.id,
agent_files=all_agent,
extension_files=all_extension,
)
# ---------------------------------------------------------------------------
# Installed-file tracking
# ---------------------------------------------------------------------------
def _manifest_path(project_path: Path, agent_id: str) -> Path:
"""Return the path to the install manifest for *agent_id*."""
return project_path / ".specify" / f"agent-manifest-{agent_id}.json"
def _sha256(path: Path) -> str:
"""Return the hex SHA-256 of a file."""
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
def _hash_file_list(
project_path: Path,
files: List[Path],
) -> Dict[str, str]:
"""Build a {relative_path: sha256} dict from a list of file paths."""
entries: Dict[str, str] = {}
for file_path in files:
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
if abs_path.is_file():
rel = str(abs_path.relative_to(project_path))
entries[rel] = _sha256(abs_path)
return entries
def record_installed_files(
project_path: Path,
agent_id: str,
agent_files: Optional[List[Path]] = None,
extension_files: Optional[List[Path]] = None,
) -> Path:
"""Record the installed files and their SHA-256 hashes.
Writes ``.specify/agent-manifest-<agent_id>.json`` containing
categorised mappings of project-relative paths to SHA-256 digests.
Args:
project_path: Project root directory.
agent_id: Agent identifier.
agent_files: Files created by the agent's ``setup()`` and the
init pipeline (core commands / templates).
extension_files: Files created by extension registration.
Returns:
Path to the written manifest file.
"""
agent_entries = _hash_file_list(project_path, agent_files or [])
extension_entries = _hash_file_list(project_path, extension_files or [])
manifest_file = _manifest_path(project_path, agent_id)
manifest_file.parent.mkdir(parents=True, exist_ok=True)
manifest_file.write_text(
json.dumps(
{
"agent_id": agent_id,
"agent_files": agent_entries,
"extension_files": extension_entries,
},
indent=2,
),
encoding="utf-8",
)
return manifest_file
def _all_tracked_entries(data: dict) -> Dict[str, str]:
"""Return the combined file → hash mapping from a manifest dict.
Supports both the new categorised layout (``agent_files`` +
``extension_files``) and the legacy flat ``files`` key.
"""
combined: Dict[str, str] = {}
# Legacy flat format
if "files" in data and isinstance(data["files"], dict):
combined.update(data["files"])
# New categorised format
if "agent_files" in data and isinstance(data["agent_files"], dict):
combined.update(data["agent_files"])
if "extension_files" in data and isinstance(data["extension_files"], dict):
combined.update(data["extension_files"])
return combined
def get_tracked_files(
project_path: Path,
agent_id: str,
) -> tuple[Dict[str, str], Dict[str, str]]:
"""Return the tracked file hashes split by source.
Returns:
A tuple ``(agent_files, extension_files)`` where each is a
``{relative_path: sha256}`` dict. Returns two empty dicts
when no install manifest exists.
"""
manifest_file = _manifest_path(project_path, agent_id)
if not manifest_file.is_file():
return {}, {}
try:
data = json.loads(manifest_file.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}, {}
# Support legacy flat format
if "files" in data and "agent_files" not in data:
return dict(data["files"]), {}
agent_entries = data.get("agent_files", {})
ext_entries = data.get("extension_files", {})
return dict(agent_entries), dict(ext_entries)
def check_modified_files(
project_path: Path,
agent_id: str,
) -> List[str]:
"""Return project-relative paths of files modified since installation.
Returns an empty list when no install manifest exists or when every
tracked file still has its original hash.
"""
manifest_file = _manifest_path(project_path, agent_id)
if not manifest_file.is_file():
return []
try:
data = json.loads(manifest_file.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return []
entries = _all_tracked_entries(data)
modified: List[str] = []
for rel_path, original_hash in entries.items():
abs_path = project_path / rel_path
if abs_path.is_file():
if _sha256(abs_path) != original_hash:
modified.append(rel_path)
# If the file was deleted by the user, treat it as not needing
# removal — skip rather than flag as modified.
return modified
def remove_tracked_files(
project_path: Path,
agent_id: str,
*,
force: bool = False,
files: Optional[Dict[str, str]] = None,
) -> List[str]:
"""Remove individual tracked files.
If *files* is provided, exactly those files are removed (the values
are ignored but accepted for forward compatibility). Otherwise the
install manifest for *agent_id* is read.
Raises :class:`AgentFileModifiedError` if any tracked file was
modified and *force* is ``False`` (only when reading from the
manifest — callers that pass *files* are expected to have already
prompted the user).
Directories are **never** deleted — only individual files.
Args:
project_path: Project root directory.
agent_id: Agent identifier.
force: When ``True``, delete even modified files.
files: Explicit mapping of project-relative path → hash. When
supplied, the install manifest is not consulted.
Returns:
List of project-relative paths that were removed.
"""
manifest_file = _manifest_path(project_path, agent_id)
if files is not None:
entries = files
else:
if not manifest_file.is_file():
return []
try:
data = json.loads(manifest_file.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return []
entries = _all_tracked_entries(data)
if not entries:
manifest_file.unlink(missing_ok=True)
return []
if not force:
modified = check_modified_files(project_path, agent_id)
if modified:
raise AgentFileModifiedError(
f"The following agent files have been modified since installation:\n"
+ "\n".join(f" {p}" for p in modified)
+ "\nUse --force to remove them anyway."
)
removed: List[str] = []
for rel_path in entries:
abs_path = project_path / rel_path
if abs_path.is_file():
abs_path.unlink()
removed.append(rel_path)
# Clean up the install manifest itself
if manifest_file.is_file():
manifest_file.unlink(missing_ok=True)
return removed
# ---------------------------------------------------------------------------
# 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

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Antigravity agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Antigravity agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Amp agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Amp agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Auggie CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Auggie CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for IBM Bob agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove IBM Bob agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Claude Code agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Claude Code agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for CodeBuddy agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove CodeBuddy agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Codex CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Codex CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for GitHub Copilot agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove GitHub Copilot agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Cursor agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Cursor agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View File

@@ -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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Gemini CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Gemini CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for iFlow CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove iFlow CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Junie agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Junie agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Kilo Code agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Kilo Code agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Kimi Code agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Kimi Code agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Kiro CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Kiro CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for opencode agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove opencode agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Pi Coding Agent agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Pi Coding Agent agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Qoder CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Qoder CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Qwen Code agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Qwen Code agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Roo Code agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Roo Code agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for SHAI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove SHAI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Tabnine CLI agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Tabnine CLI agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Trae agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Trae agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Mistral Vibe agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Mistral Vibe agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

View File

@@ -0,0 +1,30 @@
"""Bootstrap module for Windsurf agent pack."""
from pathlib import Path
from typing import Any, Dict, List, Optional
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
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]) -> List[Path]:
"""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)
return self._scaffold_project(project_path, script_type)
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
"""Remove Windsurf agent files from the project.
Only removes individual tracked files — directories are never
deleted. When *files* is provided, exactly those files are
removed. Otherwise the install manifest is consulted and
``AgentFileModifiedError`` is raised if any tracked file was
modified and *force* is ``False``.
"""
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)

View 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"

1285
tests/test_agent_pack.py Normal file

File diff suppressed because it is too large Load Diff