From 66ffca4cba55095183605e18cc0b61b9e549eeed Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Thu, 12 Feb 2026 18:39:08 +0100 Subject: [PATCH 1/9] Update package-lock.json and pyproject.toml, enhance type hints in aam-cli commands - Added license information to package-lock.json. - Updated pyproject.toml to ignore additional unused argument warnings. - Improved type hints in various aam-cli command functions and adapters for better clarity and type safety. - Refactored function signatures to specify return types and argument types more accurately across multiple files. --- apps/aam-cli/pyproject.toml | 2 ++ apps/aam-cli/src/aam_cli/adapters/claude.py | 2 +- apps/aam-cli/src/aam_cli/adapters/codex.py | 2 +- apps/aam-cli/src/aam_cli/adapters/copilot.py | 2 +- .../src/aam_cli/commands/create_package.py | 11 +++++---- apps/aam-cli/src/aam_cli/commands/diff.py | 5 ++-- .../src/aam_cli/commands/init_package.py | 5 ++-- apps/aam-cli/src/aam_cli/commands/install.py | 16 ++++++------- .../src/aam_cli/commands/list_packages.py | 21 ++++++++++------- apps/aam-cli/src/aam_cli/commands/outdated.py | 9 ++++---- .../src/aam_cli/commands/show_package.py | 5 ++-- apps/aam-cli/src/aam_cli/commands/source.py | 3 ++- .../aam-cli/src/aam_cli/commands/uninstall.py | 1 - apps/aam-cli/src/aam_cli/commands/upgrade.py | 8 +++---- apps/aam-cli/src/aam_cli/commands/verify.py | 5 ++-- apps/aam-cli/src/aam_cli/core/workspace.py | 19 +++++++++++++++ apps/aam-cli/src/aam_cli/main.py | 4 ++-- apps/aam-cli/src/aam_cli/mcp/resources.py | 9 +++++--- apps/aam-cli/src/aam_cli/mcp/tools_read.py | 10 ++++---- .../aam_cli/services/client_init_service.py | 4 ++-- .../src/aam_cli/services/install_service.py | 2 ++ .../src/aam_cli/services/recommend_service.py | 2 +- .../src/aam_cli/services/source_service.py | 14 ++++++----- .../tests/unit/test_adapters_factory.py | 1 - .../tests/unit/test_commands_search.py | 6 ++--- apps/aam-cli/tests/unit/test_mcp_resources.py | 3 +-- .../tests/unit/test_mcp_tools_write.py | 7 +++--- .../tests/unit/test_services_config.py | 8 +++---- .../tests/unit/test_services_doctor.py | 2 -- .../tests/unit/test_services_package.py | 23 +++++++++---------- .../tests/unit/test_services_recommend.py | 1 - .../tests/unit/test_services_search.py | 4 +--- .../unit/test_unit_install_from_source.py | 1 - .../tests/unit/test_unit_mcp_init_tools.py | 1 - apps/aam-cli/tests/unit/test_unit_outdated.py | 2 +- apps/aam-cli/tests/unit/test_unit_upgrade.py | 2 -- package-lock.json | 1 + 37 files changed, 124 insertions(+), 99 deletions(-) diff --git a/apps/aam-cli/pyproject.toml b/apps/aam-cli/pyproject.toml index a9d23ec..3d01ff1 100644 --- a/apps/aam-cli/pyproject.toml +++ b/apps/aam-cli/pyproject.toml @@ -79,6 +79,8 @@ select = [ ignore = [ "E501", # line too long (handled by formatter) "B008", # do not perform function calls in argument defaults + "ARG001", # unused function arguments (false positives from API contracts) + "ARG002", # unused method arguments (false positives from @patch decorators, overrides) ] [tool.ruff.lint.isort] diff --git a/apps/aam-cli/src/aam_cli/adapters/claude.py b/apps/aam-cli/src/aam_cli/adapters/claude.py index ce25e38..73d9260 100644 --- a/apps/aam-cli/src/aam_cli/adapters/claude.py +++ b/apps/aam-cli/src/aam_cli/adapters/claude.py @@ -396,7 +396,7 @@ def _read_agent_content(self, agent_path: Path, agent_ref: ArtifactRef) -> str: from aam_cli.utils.yaml_utils import load_yaml agent_data = load_yaml(agent_yaml_path) - prompt_file = agent_data.get("system_prompt", "system-prompt.md") + prompt_file: str = agent_data.get("system_prompt", "system-prompt.md") alt_path = agent_path / prompt_file if alt_path.is_file(): return alt_path.read_text(encoding="utf-8") diff --git a/apps/aam-cli/src/aam_cli/adapters/codex.py b/apps/aam-cli/src/aam_cli/adapters/codex.py index 289a599..cac3379 100644 --- a/apps/aam-cli/src/aam_cli/adapters/codex.py +++ b/apps/aam-cli/src/aam_cli/adapters/codex.py @@ -398,7 +398,7 @@ def _read_agent_content(self, agent_path: Path, agent_ref: ArtifactRef) -> str: from aam_cli.utils.yaml_utils import load_yaml agent_data = load_yaml(agent_yaml_path) - prompt_file = agent_data.get("system_prompt", "system-prompt.md") + prompt_file: str = agent_data.get("system_prompt", "system-prompt.md") alt_path = agent_path / prompt_file if alt_path.is_file(): return alt_path.read_text(encoding="utf-8") diff --git a/apps/aam-cli/src/aam_cli/adapters/copilot.py b/apps/aam-cli/src/aam_cli/adapters/copilot.py index 4bde7aa..2c5ae4a 100644 --- a/apps/aam-cli/src/aam_cli/adapters/copilot.py +++ b/apps/aam-cli/src/aam_cli/adapters/copilot.py @@ -419,7 +419,7 @@ def _read_agent_content(self, agent_path: Path, agent_ref: ArtifactRef) -> str: from aam_cli.utils.yaml_utils import load_yaml agent_data = load_yaml(agent_yaml_path) - prompt_file = agent_data.get("system_prompt", "system-prompt.md") + prompt_file: str = agent_data.get("system_prompt", "system-prompt.md") alt_path = agent_path / prompt_file if alt_path.is_file(): return alt_path.read_text(encoding="utf-8") diff --git a/apps/aam-cli/src/aam_cli/commands/create_package.py b/apps/aam-cli/src/aam_cli/commands/create_package.py index f8ff730..2f23bc6 100644 --- a/apps/aam-cli/src/aam_cli/commands/create_package.py +++ b/apps/aam-cli/src/aam_cli/commands/create_package.py @@ -22,6 +22,7 @@ import shutil from datetime import UTC, datetime from pathlib import Path +from typing import Any import click from rich.console import Console @@ -291,9 +292,9 @@ def _generate_manifest_dict( license_str: str, artifacts: list[DetectedArtifact], organize: str, -) -> dict: +) -> dict[str, Any]: """Generate the aam.yaml manifest data as a dict.""" - grouped: dict[str, list[dict]] = { + grouped: dict[str, list[dict[str, str]]] = { "agents": [], "skills": [], "prompts": [], @@ -309,7 +310,7 @@ def _generate_manifest_dict( } grouped[art.type + "s"].append(ref) - data: dict = { + data: dict[str, Any] = { "name": name, "version": version, "description": description, @@ -559,7 +560,7 @@ def _create_from_source( # ----- # Step 7: Generate manifest with provenance # ----- - grouped: dict[str, list[dict]] = { + grouped: dict[str, list[dict[str, str]]] = { "agents": [], "skills": [], "prompts": [], @@ -580,7 +581,7 @@ def _create_from_source( "description": art.get("description") or f"{art_type.capitalize()} {art['name']}", }) - manifest_data: dict = { + manifest_data: dict[str, Any] = { "name": name, "version": version, "description": description, diff --git a/apps/aam-cli/src/aam_cli/commands/diff.py b/apps/aam-cli/src/aam_cli/commands/diff.py index 5c7a1ff..56e25d1 100644 --- a/apps/aam-cli/src/aam_cli/commands/diff.py +++ b/apps/aam-cli/src/aam_cli/commands/diff.py @@ -18,6 +18,7 @@ import logging import sys from pathlib import Path +from typing import Any import click from rich.console import Console @@ -55,7 +56,7 @@ def diff_package( package_name: str, project_dir: Path | None = None, -) -> dict: +) -> dict[str, Any]: """Compute diffs for modified files in an installed package. First runs verification to identify modified files, then generates @@ -109,7 +110,7 @@ def diff_package( # ----- packages_dir = get_packages_dir(project_dir) package_dir = packages_dir / package_name - diffs: list[dict] = [] + diffs: list[dict[str, Any]] = [] for rel_path in verify_result["modified_files"]: file_path = package_dir / rel_path diff --git a/apps/aam-cli/src/aam_cli/commands/init_package.py b/apps/aam-cli/src/aam_cli/commands/init_package.py index cb91cd7..0078b40 100644 --- a/apps/aam-cli/src/aam_cli/commands/init_package.py +++ b/apps/aam-cli/src/aam_cli/commands/init_package.py @@ -11,6 +11,7 @@ import logging from pathlib import Path +from typing import Any import click from rich.console import Console @@ -113,7 +114,7 @@ def init_package(ctx: click.Context, name: str | None) -> None: # ----- # Step 5: Generate aam.yaml # ----- - manifest_data: dict = { + manifest_data: dict[str, Any] = { "name": pkg_name, "version": version, "description": description, @@ -128,7 +129,7 @@ def init_package(ctx: click.Context, name: str | None) -> None: manifest_data["dependencies"] = {} - platforms_config: dict = {} + platforms_config: dict[str, Any] = {} if "cursor" in selected_platforms: platforms_config["cursor"] = { "skill_scope": "project", diff --git a/apps/aam-cli/src/aam_cli/commands/install.py b/apps/aam-cli/src/aam_cli/commands/install.py index 4ce7709..74843ca 100644 --- a/apps/aam-cli/src/aam_cli/commands/install.py +++ b/apps/aam-cli/src/aam_cli/commands/install.py @@ -19,11 +19,12 @@ from rich.console import Console from aam_cli.adapters.factory import create_adapter, is_supported_platform -from aam_cli.core.config import load_config +from aam_cli.core.config import AamConfig, load_config from aam_cli.core.installer import install_packages from aam_cli.core.manifest import load_manifest from aam_cli.core.resolver import resolve_dependencies from aam_cli.core.workspace import ( + FileChecksums, LockedPackage, ensure_workspace, get_packages_dir, @@ -190,7 +191,7 @@ def _handle_upgrade_warning( def _read_file_checksums_from_package( package_dir: Path, -) -> "object | None": +) -> FileChecksums | None: """Read file checksums from a package's aam.yaml if present. The ``file_checksums`` section is written by ``aam pack`` and @@ -204,7 +205,6 @@ def _read_file_checksums_from_package( A :class:`FileChecksums` instance or ``None`` if the package does not contain per-file checksums. """ - from aam_cli.core.workspace import FileChecksums manifest_path = package_dir / "aam.yaml" if not manifest_path.is_file(): @@ -351,7 +351,7 @@ def install( ################################################################################ -def _collect_available_names(config: "AamConfig") -> list[str]: # noqa: F821 +def _collect_available_names(config: AamConfig) -> list[str]: """Collect all known package names from registries and sources. Used for "Did you mean?" suggestions when the user supplies an @@ -403,7 +403,7 @@ def _collect_available_names(config: "AamConfig") -> list[str]: # noqa: F821 def _show_name_suggestions( console: Console, invalid_input: str, - config: "AamConfig", # noqa: F821 + config: AamConfig, ) -> None: """Show "Did you mean?" suggestions for an invalid package name. @@ -437,7 +437,7 @@ def _install_from_registry_or_source( console: Console, package_spec: str, project_dir: Path, - config: "AamConfig", # noqa: F821 + config: AamConfig, platform_name: str, no_deploy: bool, force: bool, @@ -609,7 +609,7 @@ def _try_install_from_source( console: Console, artifact_name: str, project_dir: Path, - config: "AamConfig", # noqa: F821 + config: AamConfig, platform_name: str, no_deploy: bool, force: bool, @@ -623,11 +623,11 @@ def _try_install_from_source( Raises: ValueError: If the artifact cannot be found in sources. """ + from aam_cli.services.install_service import install_from_source from aam_cli.services.source_service import ( build_source_index, resolve_artifact, ) - from aam_cli.services.install_service import install_from_source console.print(f"Searching sources for '{artifact_name}'...") diff --git a/apps/aam-cli/src/aam_cli/commands/list_packages.py b/apps/aam-cli/src/aam_cli/commands/list_packages.py index 22e846d..74b0155 100644 --- a/apps/aam-cli/src/aam_cli/commands/list_packages.py +++ b/apps/aam-cli/src/aam_cli/commands/list_packages.py @@ -3,14 +3,16 @@ Lists installed packages from the lock file and ``.aam/packages/``. """ +from __future__ import annotations + ################################################################################ # # # IMPORTS & DEPENDENCIES # # # ################################################################################ - import logging from pathlib import Path +from typing import TYPE_CHECKING import click from rich.console import Console @@ -18,10 +20,13 @@ from rich.tree import Tree from aam_cli.core.manifest import load_manifest -from aam_cli.core.workspace import get_packages_dir, read_lock_file +from aam_cli.core.workspace import LockedPackage, LockFile, get_packages_dir, read_lock_file from aam_cli.utils.naming import parse_package_name, to_filesystem_name from aam_cli.utils.paths import resolve_project_dir +if TYPE_CHECKING: + from aam_cli.services.source_service import VirtualPackage + ################################################################################ # # # LOGGING # @@ -94,7 +99,7 @@ def list_packages(ctx: click.Context, tree: bool, available: bool, is_global: bo _show_table(console, lock, project_dir) -def _format_source(locked: "LockedPackage") -> str: # noqa: F821 +def _format_source(locked: LockedPackage) -> str: """Build a human-readable source label from a locked package. Prefers the git source name (for example, ``google-gemini/gemini-skills``) @@ -114,7 +119,7 @@ def _format_source(locked: "LockedPackage") -> str: # noqa: F821 def _show_table( console: Console, - lock: "LockFile", # noqa: F821 + lock: LockFile, project_dir: Path, ) -> None: """Display packages as a flat table.""" @@ -161,7 +166,7 @@ def _show_table( def _show_tree( console: Console, - lock: "LockFile", # noqa: F821 + lock: LockFile, _project_dir: Path, ) -> None: """Display packages as a dependency tree.""" @@ -188,8 +193,8 @@ def _show_tree( def _add_deps_to_tree( parent: Tree, - locked: "LockedPackage", # noqa: F821 - lock: "LockFile", # noqa: F821 + locked: LockedPackage, + lock: LockFile, ) -> None: """Recursively add dependencies to a Rich Tree.""" for dep_name in locked.dependencies: @@ -229,7 +234,7 @@ def _show_available(console: Console) -> None: # ----- # Group artifacts by source # ----- - by_source: dict[str, list] = {} + by_source: dict[str, list[VirtualPackage]] = {} for vp in index.by_qualified_name.values(): by_source.setdefault(vp.source_name, []).append(vp) diff --git a/apps/aam-cli/src/aam_cli/commands/outdated.py b/apps/aam-cli/src/aam_cli/commands/outdated.py index 50dcc66..3d66888 100644 --- a/apps/aam-cli/src/aam_cli/commands/outdated.py +++ b/apps/aam-cli/src/aam_cli/commands/outdated.py @@ -14,14 +14,13 @@ import json import logging -from pathlib import Path import click from rich.console import Console from rich.table import Table -from aam_cli.core.config import load_config -from aam_cli.core.workspace import read_lock_file +from aam_cli.core.config import AamConfig, load_config +from aam_cli.core.workspace import LockFile, read_lock_file from aam_cli.services.upgrade_service import OutdatedPackage, OutdatedResult from aam_cli.utils.paths import resolve_project_dir @@ -168,8 +167,8 @@ def outdated(ctx: click.Context, output_json: bool, is_global: bool) -> None: def check_outdated( - lock: "LockFile", # noqa: F821 - config: "AamConfig", # noqa: F821 + lock: LockFile, + config: AamConfig, ) -> OutdatedResult: """Compare installed source packages against source HEAD commits. diff --git a/apps/aam-cli/src/aam_cli/commands/show_package.py b/apps/aam-cli/src/aam_cli/commands/show_package.py index 516819b..3037211 100644 --- a/apps/aam-cli/src/aam_cli/commands/show_package.py +++ b/apps/aam-cli/src/aam_cli/commands/show_package.py @@ -15,6 +15,7 @@ import logging from pathlib import Path +from typing import Any import click import yaml @@ -145,7 +146,7 @@ def _resolve_artifact_file( return None -def _extract_frontmatter(file_path: Path) -> dict | None: +def _extract_frontmatter(file_path: Path) -> dict[str, Any] | None: """Parse YAML frontmatter delimited by ``---`` from a file. Reads only the frontmatter block at the top of the file (between @@ -188,7 +189,7 @@ def _extract_frontmatter(file_path: Path) -> dict | None: def _render_frontmatter( console: Console, - frontmatter: dict, + frontmatter: dict[str, Any], ) -> None: """Render YAML frontmatter as a Rich panel. diff --git a/apps/aam-cli/src/aam_cli/commands/source.py b/apps/aam-cli/src/aam_cli/commands/source.py index aff8e89..0567ebc 100644 --- a/apps/aam-cli/src/aam_cli/commands/source.py +++ b/apps/aam-cli/src/aam_cli/commands/source.py @@ -20,6 +20,7 @@ import json import logging +from typing import Any import click from rich.console import Console @@ -566,7 +567,7 @@ def candidates( # ----- # Group by source # ----- - by_source: dict[str, list[dict]] = {} + by_source: dict[str, list[dict[str, Any]]] = {} for c in candidates_list: src = c.get("source_name", "unknown") by_source.setdefault(src, []).append(c) diff --git a/apps/aam-cli/src/aam_cli/commands/uninstall.py b/apps/aam-cli/src/aam_cli/commands/uninstall.py index 0f0f412..3cbda4f 100644 --- a/apps/aam-cli/src/aam_cli/commands/uninstall.py +++ b/apps/aam-cli/src/aam_cli/commands/uninstall.py @@ -11,7 +11,6 @@ import logging import shutil -from pathlib import Path import click from rich.console import Console diff --git a/apps/aam-cli/src/aam_cli/commands/upgrade.py b/apps/aam-cli/src/aam_cli/commands/upgrade.py index 1f35059..efa9e4b 100644 --- a/apps/aam-cli/src/aam_cli/commands/upgrade.py +++ b/apps/aam-cli/src/aam_cli/commands/upgrade.py @@ -19,9 +19,9 @@ from rich.console import Console from aam_cli.commands.outdated import check_outdated -from aam_cli.core.config import load_config +from aam_cli.core.config import AamConfig, load_config from aam_cli.core.workspace import read_lock_file -from aam_cli.services.upgrade_service import UpgradeResult +from aam_cli.services.upgrade_service import OutdatedPackage, UpgradeResult from aam_cli.utils.paths import resolve_project_dir ################################################################################ @@ -181,8 +181,8 @@ def upgrade( def upgrade_packages( - targets: list, - config: "AamConfig", # noqa: F821 + targets: list["OutdatedPackage"], + config: AamConfig, project_dir: Path, force: bool, dry_run: bool, diff --git a/apps/aam-cli/src/aam_cli/commands/verify.py b/apps/aam-cli/src/aam_cli/commands/verify.py index 584d123..d4b59b2 100644 --- a/apps/aam-cli/src/aam_cli/commands/verify.py +++ b/apps/aam-cli/src/aam_cli/commands/verify.py @@ -15,6 +15,7 @@ import json import logging import sys +from typing import Any import click from rich.console import Console @@ -98,7 +99,7 @@ def verify( _display_single_result(result) -def _display_single_result(result: dict) -> None: +def _display_single_result(result: dict[str, Any]) -> None: """Display verification result for a single package.""" console.print() name = result["package_name"] @@ -141,7 +142,7 @@ def _display_single_result(result: dict) -> None: console.print() -def _display_all_results(result: dict) -> None: +def _display_all_results(result: dict[str, Any]) -> None: """Display verification results for all packages.""" console.print() diff --git a/apps/aam-cli/src/aam_cli/core/workspace.py b/apps/aam-cli/src/aam_cli/core/workspace.py index 2bc9a8c..1e4bc94 100644 --- a/apps/aam-cli/src/aam_cli/core/workspace.py +++ b/apps/aam-cli/src/aam_cli/core/workspace.py @@ -24,6 +24,25 @@ ) from aam_cli.utils.yaml_utils import dump_yaml, load_yaml_optional +################################################################################ +# # +# PUBLIC API # +# # +################################################################################ + +__all__ = [ + "FileChecksums", + "LockedPackage", + "LockFile", + "ensure_workspace", + "get_packages_dir", + "get_workspace_path", + "read_lock_file", + "write_lock_file", + "get_installed_packages", + "is_package_installed", +] + ################################################################################ # # # LOGGING # diff --git a/apps/aam-cli/src/aam_cli/main.py b/apps/aam-cli/src/aam_cli/main.py index b2b1b80..cb8e6e6 100644 --- a/apps/aam-cli/src/aam_cli/main.py +++ b/apps/aam-cli/src/aam_cli/main.py @@ -76,7 +76,7 @@ def format_commands( formatter: Click help formatter. """ for section_name, cmd_names in self.SECTIONS.items(): - commands: list[tuple[str, click.BaseCommand]] = [] + commands: list[tuple[str, click.Command]] = [] for name in cmd_names: cmd = self.commands.get(name) if cmd and not cmd.hidden: @@ -257,7 +257,7 @@ def cli(ctx: click.Context, verbose: bool) -> None: @click.option("--from-source", "from_source", default=None) @click.option("--artifacts", "artifact_names", multiple=True) @click.pass_context -def deprecated_create_package(ctx: click.Context, **kwargs) -> None: # type: ignore[no-untyped-def] +def deprecated_create_package(ctx: click.Context, /, **kwargs: object) -> None: """(Deprecated) Use 'aam pkg create' instead.""" print_deprecation_warning("aam create-package", "aam pkg create") ctx.invoke(create_package.create_package, **kwargs) diff --git a/apps/aam-cli/src/aam_cli/mcp/resources.py b/apps/aam-cli/src/aam_cli/mcp/resources.py index 43c9646..3d4cb3c 100644 --- a/apps/aam-cli/src/aam_cli/mcp/resources.py +++ b/apps/aam-cli/src/aam_cli/mcp/resources.py @@ -64,7 +64,8 @@ def resource_config() -> dict[str, Any]: """ logger.debug("MCP resource aam://config accessed") result = get_config(key=None) - return result["value"] + value: dict[str, Any] = result["value"] + return value @mcp.resource("aam://packages/installed") def resource_packages_installed() -> list[dict[str, Any]]: @@ -160,7 +161,8 @@ def resource_sources() -> list[dict[str, Any]]: """ logger.debug("MCP resource aam://sources accessed") result = list_sources() - return result.get("sources", []) + sources: list[dict[str, Any]] = result.get("sources", []) + return sources @mcp.resource("aam://sources/{source_id}") def resource_source_detail(source_id: str) -> dict[str, Any] | None: @@ -209,7 +211,8 @@ def resource_source_candidates(source_id: str) -> list[dict[str, Any]]: ) try: result = list_candidates(source_filter=source_name) - return result.get("candidates", []) + candidates: list[dict[str, Any]] = result.get("candidates", []) + return candidates except ValueError: logger.debug(f"Source not found for candidates: {source_name}") return [] diff --git a/apps/aam-cli/src/aam_cli/mcp/tools_read.py b/apps/aam-cli/src/aam_cli/mcp/tools_read.py index 46cf798..4cc2f44 100644 --- a/apps/aam-cli/src/aam_cli/mcp/tools_read.py +++ b/apps/aam-cli/src/aam_cli/mcp/tools_read.py @@ -26,8 +26,8 @@ get_package_info, list_installed_packages, ) -from aam_cli.services.registry_service import list_registries from aam_cli.services.recommend_service import recommend_skills_for_repo +from aam_cli.services.registry_service import list_registries from aam_cli.services.search_service import search_packages from aam_cli.services.source_service import ( list_candidates, @@ -220,7 +220,8 @@ def aam_source_list() -> list[dict[str, Any]]: """ logger.info("MCP tool aam_source_list") result = list_sources() - return result.get("sources", []) + sources: list[dict[str, Any]] = result.get("sources", []) + return sources @mcp.tool(tags={"read"}) def aam_source_scan( @@ -289,7 +290,8 @@ def aam_source_candidates( source_filter=source_name, type_filter=type_filter, ) - return result.get("candidates", []) + candidates: list[dict[str, Any]] = result.get("candidates", []) + return candidates @mcp.tool(tags={"read"}) def aam_source_diff(source_name: str) -> dict[str, Any]: @@ -433,7 +435,7 @@ def aam_available() -> dict[str, Any]: # Group by source for structured output # ----- by_source: dict[str, list[dict[str, Any]]] = {} - for qname, vp in index.by_qualified_name.items(): + for _qname, vp in index.by_qualified_name.items(): source_group = by_source.setdefault(vp.source_name, []) source_group.append({ "name": vp.name, diff --git a/apps/aam-cli/src/aam_cli/services/client_init_service.py b/apps/aam-cli/src/aam_cli/services/client_init_service.py index 98c76f5..5fff99f 100644 --- a/apps/aam-cli/src/aam_cli/services/client_init_service.py +++ b/apps/aam-cli/src/aam_cli/services/client_init_service.py @@ -18,7 +18,6 @@ from pathlib import Path from aam_cli.core.config import ( - AamConfig, load_config, save_global_config, ) @@ -133,7 +132,8 @@ def setup_default_sources() -> list[str]: logger.info("Setting up default sources") result = register_default_sources() - return result.get("registered", []) + registered: list[str] = result.get("registered", []) + return registered ################################################################################ diff --git a/apps/aam-cli/src/aam_cli/services/install_service.py b/apps/aam-cli/src/aam_cli/services/install_service.py index b42da91..fd9b4f3 100644 --- a/apps/aam-cli/src/aam_cli/services/install_service.py +++ b/apps/aam-cli/src/aam_cli/services/install_service.py @@ -23,6 +23,8 @@ from aam_cli.core.config import AamConfig from aam_cli.core.installer import ( _deploy_package, +) +from aam_cli.core.installer import ( install_packages as core_install_packages, ) from aam_cli.core.resolver import resolve_dependencies diff --git a/apps/aam-cli/src/aam_cli/services/recommend_service.py b/apps/aam-cli/src/aam_cli/services/recommend_service.py index 41067ed..f343438 100644 --- a/apps/aam-cli/src/aam_cli/services/recommend_service.py +++ b/apps/aam-cli/src/aam_cli/services/recommend_service.py @@ -243,7 +243,7 @@ def recommend_skills( keyword_set = {k.lower() for k in repo_context.keywords} scored: list[SkillRecommendation] = [] - for qname, vp in index.by_qualified_name.items(): + for _qname, vp in index.by_qualified_name.items(): score, rationale = _score_artifact(vp, keyword_set, repo_context) if score > 0: scored.append( diff --git a/apps/aam-cli/src/aam_cli/services/source_service.py b/apps/aam-cli/src/aam_cli/services/source_service.py index e9edc6d..71dd0ca 100644 --- a/apps/aam-cli/src/aam_cli/services/source_service.py +++ b/apps/aam-cli/src/aam_cli/services/source_service.py @@ -16,11 +16,16 @@ # # ################################################################################ +from __future__ import annotations + import logging from dataclasses import dataclass, field from datetime import UTC, datetime from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from aam_cli.registry.local import LocalRegistry from aam_cli.core.config import ( AamConfig, @@ -1127,12 +1132,9 @@ def materialize_source_packages( Dict with ``packages_created``, ``errors``, ``registry_path``. """ import shutil - import tempfile from aam_cli.registry.local import LocalRegistry - from aam_cli.utils.archive import create_archive - from aam_cli.utils.paths import get_sources_registry_dir, to_file_url - from aam_cli.utils.yaml_utils import dump_yaml + from aam_cli.utils.paths import get_sources_registry_dir logger.info("Materializing source artifacts into local registry") @@ -1196,7 +1198,7 @@ def materialize_source_packages( def _publish_virtual_package_to_registry( - registry: "LocalRegistry", + registry: LocalRegistry, vp: VirtualPackage, registry_dir: Path, ) -> None: diff --git a/apps/aam-cli/tests/unit/test_adapters_factory.py b/apps/aam-cli/tests/unit/test_adapters_factory.py index 00f6471..983c8f8 100644 --- a/apps/aam-cli/tests/unit/test_adapters_factory.py +++ b/apps/aam-cli/tests/unit/test_adapters_factory.py @@ -8,7 +8,6 @@ import logging from pathlib import Path -from unittest.mock import MagicMock import pytest diff --git a/apps/aam-cli/tests/unit/test_commands_search.py b/apps/aam-cli/tests/unit/test_commands_search.py index 8a42bc3..cc1f49d 100644 --- a/apps/aam-cli/tests/unit/test_commands_search.py +++ b/apps/aam-cli/tests/unit/test_commands_search.py @@ -16,11 +16,10 @@ import logging from unittest.mock import MagicMock, patch -import pytest from click.testing import CliRunner from aam_cli.commands.search import _collect_installed_names, search -from aam_cli.core.workspace import LockFile, LockedPackage +from aam_cli.core.workspace import LockedPackage, LockFile from aam_cli.services.search_service import SearchResponse, SearchResult ################################################################################ @@ -228,9 +227,10 @@ def test_unit_search_json_includes_installed_field( mock_installed: MagicMock, ) -> None: """JSON output includes 'installed' field for each result.""" - from rich.console import Console from io import StringIO + from rich.console import Console + mock_config.return_value = MagicMock() mock_search.return_value = _make_search_response( results=[ diff --git a/apps/aam-cli/tests/unit/test_mcp_resources.py b/apps/aam-cli/tests/unit/test_mcp_resources.py index e26eef7..e610344 100644 --- a/apps/aam-cli/tests/unit/test_mcp_resources.py +++ b/apps/aam-cli/tests/unit/test_mcp_resources.py @@ -3,10 +3,9 @@ import logging from unittest.mock import patch -import pytest +from fastmcp import Client from aam_cli.mcp.server import create_mcp_server -from fastmcp import Client logger = logging.getLogger(__name__) diff --git a/apps/aam-cli/tests/unit/test_mcp_tools_write.py b/apps/aam-cli/tests/unit/test_mcp_tools_write.py index 02dce5a..faf834d 100644 --- a/apps/aam-cli/tests/unit/test_mcp_tools_write.py +++ b/apps/aam-cli/tests/unit/test_mcp_tools_write.py @@ -3,10 +3,9 @@ import logging from unittest.mock import patch -import pytest +from fastmcp import Client from aam_cli.mcp.server import create_mcp_server -from fastmcp import Client logger = logging.getLogger(__name__) @@ -26,8 +25,8 @@ def test_unit_aam_install_success(self) -> None: "failed": [], "dependencies_resolved": 1 } - with patch("aam_cli.mcp.tools_write.install_packages", return_value=mock_result): - with patch("aam_cli.mcp.tools_write.load_config"): + with patch("aam_cli.mcp.tools_write.install_packages", return_value=mock_result), \ + patch("aam_cli.mcp.tools_write.load_config"): server = create_mcp_server(allow_write=True) async def check(): async with Client(server) as client: diff --git a/apps/aam-cli/tests/unit/test_services_config.py b/apps/aam-cli/tests/unit/test_services_config.py index 1c3bb81..a4f572c 100644 --- a/apps/aam-cli/tests/unit/test_services_config.py +++ b/apps/aam-cli/tests/unit/test_services_config.py @@ -3,9 +3,7 @@ import logging from unittest.mock import MagicMock, patch -import pytest - -from aam_cli.services.config_service import get_config, set_config, list_config +from aam_cli.services.config_service import get_config, set_config logger = logging.getLogger(__name__) @@ -32,8 +30,8 @@ def test_unit_get_config_key(self) -> None: def test_unit_set_config(self) -> None: mock_cfg = MagicMock() mock_cfg.default_platform = "cursor" - with patch("aam_cli.services.config_service.load_config", return_value=mock_cfg): - with patch("aam_cli.services.config_service.save_global_config"): + with patch("aam_cli.services.config_service.load_config", return_value=mock_cfg), \ + patch("aam_cli.services.config_service.save_global_config"): result = set_config(key="default_platform", value="vscode") assert result["key"] == "default_platform" assert result["value"] == "vscode" diff --git a/apps/aam-cli/tests/unit/test_services_doctor.py b/apps/aam-cli/tests/unit/test_services_doctor.py index 33e9a3a..f35ea58 100644 --- a/apps/aam-cli/tests/unit/test_services_doctor.py +++ b/apps/aam-cli/tests/unit/test_services_doctor.py @@ -7,11 +7,9 @@ ################################################################################ import logging -import sys from pathlib import Path from unittest.mock import MagicMock, patch -import pytest import yaml from aam_cli.services.doctor_service import ( diff --git a/apps/aam-cli/tests/unit/test_services_package.py b/apps/aam-cli/tests/unit/test_services_package.py index 4bcb8e5..740dde4 100644 --- a/apps/aam-cli/tests/unit/test_services_package.py +++ b/apps/aam-cli/tests/unit/test_services_package.py @@ -7,6 +7,7 @@ ################################################################################ import logging +from pathlib import Path from unittest.mock import MagicMock, patch import pytest @@ -46,7 +47,7 @@ def test_unit_list_packages_empty(self) -> None: result = list_installed_packages() assert result == [] - def test_unit_list_packages_with_installed(self, tmp_path: "Path") -> None: + def test_unit_list_packages_with_installed(self, tmp_path: Path) -> None: """Verify list structure when packages are installed.""" mock_locked = MagicMock() mock_locked.version = "1.0.0" @@ -62,15 +63,14 @@ def test_unit_list_packages_with_installed(self, tmp_path: "Path") -> None: with patch( "aam_cli.services.package_service.read_lock_file", return_value=mock_lock, + ), patch( + "aam_cli.services.package_service.get_packages_dir", + return_value=packages_dir, ): - with patch( - "aam_cli.services.package_service.get_packages_dir", - return_value=packages_dir, - ): - result = list_installed_packages(tmp_path) - assert len(result) == 1 - assert result[0]["name"] == "test-pkg" - assert result[0]["version"] == "1.0.0" + result = list_installed_packages(tmp_path) + assert len(result) == 1 + assert result[0]["name"] == "test-pkg" + assert result[0]["version"] == "1.0.0" def test_unit_get_package_info_not_found(self) -> None: """Verify error when package not installed.""" @@ -80,6 +80,5 @@ def test_unit_get_package_info_not_found(self) -> None: with patch( "aam_cli.services.package_service.read_lock_file", return_value=mock_lock, - ): - with pytest.raises(ValueError, match="AAM_PACKAGE_NOT_FOUND"): - get_package_info("nonexistent") + ), pytest.raises(ValueError, match="AAM_PACKAGE_NOT_FOUND"): + get_package_info("nonexistent") diff --git a/apps/aam-cli/tests/unit/test_services_recommend.py b/apps/aam-cli/tests/unit/test_services_recommend.py index 8f9ec2a..391fd75 100644 --- a/apps/aam-cli/tests/unit/test_services_recommend.py +++ b/apps/aam-cli/tests/unit/test_services_recommend.py @@ -18,7 +18,6 @@ from aam_cli.services.recommend_service import ( RepoContext, - SkillRecommendation, analyze_repository, recommend_skills, recommend_skills_for_repo, diff --git a/apps/aam-cli/tests/unit/test_services_search.py b/apps/aam-cli/tests/unit/test_services_search.py index e6cec77..1182c8f 100644 --- a/apps/aam-cli/tests/unit/test_services_search.py +++ b/apps/aam-cli/tests/unit/test_services_search.py @@ -12,6 +12,7 @@ ################################################################################ import logging +import time from unittest.mock import MagicMock, patch import pytest @@ -31,9 +32,6 @@ logger = logging.getLogger(__name__) - -import time - ################################################################################ # # # HELPERS # diff --git a/apps/aam-cli/tests/unit/test_unit_install_from_source.py b/apps/aam-cli/tests/unit/test_unit_install_from_source.py index 40b4f4f..beeacef 100644 --- a/apps/aam-cli/tests/unit/test_unit_install_from_source.py +++ b/apps/aam-cli/tests/unit/test_unit_install_from_source.py @@ -14,7 +14,6 @@ ################################################################################ import logging -from unittest.mock import MagicMock, patch import pytest diff --git a/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py b/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py index 1b609e6..788ec61 100644 --- a/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py +++ b/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py @@ -12,7 +12,6 @@ # # ################################################################################ -from dataclasses import dataclass, field from pathlib import Path from unittest.mock import MagicMock, patch diff --git a/apps/aam-cli/tests/unit/test_unit_outdated.py b/apps/aam-cli/tests/unit/test_unit_outdated.py index 23410c4..b4a6b9d 100644 --- a/apps/aam-cli/tests/unit/test_unit_outdated.py +++ b/apps/aam-cli/tests/unit/test_unit_outdated.py @@ -16,7 +16,7 @@ import pytest -from aam_cli.core.workspace import LockFile, LockedPackage +from aam_cli.core.workspace import LockedPackage from aam_cli.services.upgrade_service import OutdatedPackage, OutdatedResult ################################################################################ diff --git a/apps/aam-cli/tests/unit/test_unit_upgrade.py b/apps/aam-cli/tests/unit/test_unit_upgrade.py index 824865d..f3ee861 100644 --- a/apps/aam-cli/tests/unit/test_unit_upgrade.py +++ b/apps/aam-cli/tests/unit/test_unit_upgrade.py @@ -13,8 +13,6 @@ import logging -import pytest - from aam_cli.services.upgrade_service import UpgradeResult ################################################################################ diff --git a/package-lock.json b/package-lock.json index 0379487..56f65d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@aam/monorepo", "version": "0.1.0", + "license": "Apache-2.0", "workspaces": [ "apps/*", "libs/*" From 1263f3610ef62ba03c5cbdad730da6579269ca45 Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Thu, 12 Feb 2026 18:49:29 +0100 Subject: [PATCH 2/9] Update tests to reflect changes in tool counts and command registrations - Modified expected command list in `test_main.py` to include new commands and remove outdated ones. - Updated assertions in `test_mcp_integration.py` to reflect the correct number of tools (29) based on the latest specifications. - Adjusted assertions in `test_mcp_server.py` to verify the correct number of tools (17 for read-only and 29 for writable) in accordance with updated specifications. --- .../tests/integration/test_mcp_integration.py | 4 ++-- apps/aam-cli/tests/test_main.py | 13 ++++++++----- apps/aam-cli/tests/unit/test_mcp_server.py | 15 ++++++++------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/aam-cli/tests/integration/test_mcp_integration.py b/apps/aam-cli/tests/integration/test_mcp_integration.py index d64bddb..0ad1a28 100644 --- a/apps/aam-cli/tests/integration/test_mcp_integration.py +++ b/apps/aam-cli/tests/integration/test_mcp_integration.py @@ -81,13 +81,13 @@ async def check() -> None: _run_async(check()) def test_integration_full_access_has_all_tools(self) -> None: - """Full-access server should list all 23 tools (spec 002 + 003).""" + """Full-access server should list all 29 tools (spec 002–005).""" server = create_mcp_server(allow_write=True) async def check() -> None: async with Client(server) as client: tools = await client.list_tools() - assert len(tools) == 23 + assert len(tools) == 29 _run_async(check()) diff --git a/apps/aam-cli/tests/test_main.py b/apps/aam-cli/tests/test_main.py index fc4d084..489790b 100644 --- a/apps/aam-cli/tests/test_main.py +++ b/apps/aam-cli/tests/test_main.py @@ -78,19 +78,22 @@ class TestCommandRegistration: """Test that all expected commands are registered.""" EXPECTED_COMMANDS = [ - "build", "config", - "create-package", + "diff", + "doctor", "info", "init", "install", "list", - "pack", - "publish", + "mcp", + "outdated", + "pkg", "registry", "search", + "source", "uninstall", - "validate", + "upgrade", + "verify", ] def setup_method(self) -> None: diff --git a/apps/aam-cli/tests/unit/test_mcp_server.py b/apps/aam-cli/tests/unit/test_mcp_server.py index 2b885de..ed5be82 100644 --- a/apps/aam-cli/tests/unit/test_mcp_server.py +++ b/apps/aam-cli/tests/unit/test_mcp_server.py @@ -43,11 +43,12 @@ def test_unit_create_server_default(self) -> None: @pytest.mark.asyncio async def test_unit_create_server_read_only(self) -> None: - """Verify only 16 read tools listed when allow_write=False. + """Verify only 17 read tools listed when allow_write=False. 7 spec-002 read tools + 6 spec-003 read tools + 2 spec-004 read tools (outdated, available) - + 1 spec-004 init info tool = 16. + + 1 spec-004 init info tool + + 1 spec-005 recommend tool = 17. """ server = create_mcp_server(allow_write=False) # ----- @@ -56,7 +57,7 @@ async def test_unit_create_server_read_only(self) -> None: async with Client(server) as client: tools = await client.list_tools() tool_names = [t.name for t in tools] - assert len(tool_names) == 16 + assert len(tool_names) == 17 # ----- # Spec 002 read-only tools # ----- @@ -93,16 +94,16 @@ async def test_unit_create_server_read_only(self) -> None: @pytest.mark.asyncio async def test_unit_create_server_allow_write(self) -> None: - """Verify all 28 tools listed when allow_write=True. + """Verify all 29 tools listed when allow_write=True. - 16 read tools + 7 spec-002 write + 3 spec-003 write - + 1 spec-004 upgrade + 1 spec-004 init = 28. + 17 read tools + 7 spec-002 write + 3 spec-003 write + + 1 spec-004 upgrade + 1 spec-004 init = 29. """ server = create_mcp_server(allow_write=True) async with Client(server) as client: tools = await client.list_tools() tool_names = [t.name for t in tools] - assert len(tool_names) == 28 + assert len(tool_names) == 29 # ----- # Check spec 002 write tools present # ----- From abb50158785368af53cbb6af3a2c8deef107fdf0 Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Thu, 12 Feb 2026 18:53:51 +0100 Subject: [PATCH 3/9] Update CI workflow to enable verbose output for aam-cli linting and testing --- .github/workflows/publish-pypi-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-pypi-test.yml b/.github/workflows/publish-pypi-test.yml index 12688b3..8a877b3 100644 --- a/.github/workflows/publish-pypi-test.yml +++ b/.github/workflows/publish-pypi-test.yml @@ -58,7 +58,7 @@ jobs: working-directory: apps/aam-cli - name: Lint and test aam-cli - run: npx nx run-many -t lint,test -p aam-cli + run: npx nx run-many -t lint,test -p aam-cli --verbose # ========================================================================== # BUILD & PUBLISH — Build wheels/sdists and upload to Test PyPI From dd5206f3235cd1e562ae07b6e6bb31d0182c93a8 Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Thu, 12 Feb 2026 18:56:57 +0100 Subject: [PATCH 4/9] Add type hints for PyYAML in development dependencies and update test assertions - Included `types-PyYAML` version 6.0.0 in the development dependencies of `pyproject.toml`. - Updated the assertion in `test_main.py` to check for the existence of the archive file on disk instead of checking the output directly. --- apps/aam-cli/pyproject.toml | 1 + apps/aam-cli/tests/test_main.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/aam-cli/pyproject.toml b/apps/aam-cli/pyproject.toml index 3d01ff1..e46f724 100644 --- a/apps/aam-cli/pyproject.toml +++ b/apps/aam-cli/pyproject.toml @@ -42,6 +42,7 @@ dev = [ "mypy>=1.11.0", "ruff>=0.6.0", "black>=24.0.0", + "types-PyYAML>=6.0.0", ] [project.scripts] diff --git a/apps/aam-cli/tests/test_main.py b/apps/aam-cli/tests/test_main.py index 489790b..dde87f6 100644 --- a/apps/aam-cli/tests/test_main.py +++ b/apps/aam-cli/tests/test_main.py @@ -467,7 +467,8 @@ def test_unit_pack_valid_package(self) -> None: result = self.runner.invoke(cli, ["pack"]) assert result.exit_code == 0 - assert "test-pkg-1.0.0.aam" in result.output + # Archive file should be created on disk + assert Path("test-pkg-1.0.0.aam").exists() ################################################################################ From 7d310415fba0a38e68673f5b6e4e7815164f872b Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Sun, 15 Feb 2026 14:17:40 +0100 Subject: [PATCH 5/9] Update README and CLI documentation to reflect MCP server integration and default sources changes - Added detailed documentation for the built-in Model Context Protocol (MCP) server, including usage instructions and available tools. - Updated the README and CLI documentation to reflect the removal of the `cursor/community-skills` source, now listing 4 curated community repositories. - Enhanced user guidance on package name validation and error messaging in the CLI commands. - Adjusted test cases to ensure consistency with the updated default sources and MCP functionalities. --- README.md | 62 +- apps/aam-cli/README.md | 133 +++ apps/aam-cli/src/aam_cli/commands/convert.py | 223 +++++ .../src/aam_cli/commands/create_package.py | 17 +- .../src/aam_cli/commands/init_package.py | 8 +- .../src/aam_cli/commands/pkg/create.py | 2 +- apps/aam-cli/src/aam_cli/commands/source.py | 2 +- .../src/aam_cli/converters/__init__.py | 1 + .../src/aam_cli/converters/frontmatter.py | 92 ++ .../src/aam_cli/converters/mappings.py | 235 +++++ apps/aam-cli/src/aam_cli/main.py | 4 +- .../src/aam_cli/services/convert_service.py | 871 ++++++++++++++++++ .../src/aam_cli/services/doctor_service.py | 13 +- .../src/aam_cli/services/init_service.py | 5 +- .../src/aam_cli/services/source_service.py | 15 +- apps/aam-cli/src/aam_cli/utils/naming.py | 59 ++ apps/aam-cli/tests/test_main.py | 30 + apps/aam-cli/tests/unit/test_convert.py | 594 ++++++++++++ .../tests/unit/test_services_doctor.py | 12 +- .../tests/unit/test_services_search.py | 10 +- .../tests/unit/test_unit_client_init.py | 6 +- .../tests/unit/test_unit_default_sources.py | 30 +- .../tests/unit/test_unit_mcp_init_tools.py | 4 +- docs/DESIGN.md | 66 +- docs/USER_GUIDE.md | 99 +- docs/spec_convert_1.md | 480 ++++++++++ docs/specs/SPEC-convert-command.md | 372 ++++++++ docs/user_docs/docs/cli/convert.md | 160 ++++ docs/user_docs/docs/cli/index.md | 7 + docs/user_docs/docs/cli/list.md | 2 +- .../docs/cli/source-enable-defaults.md | 18 +- docs/user_docs/docs/concepts/git-sources.md | 4 +- docs/user_docs/docs/index.md | 189 +++- docs/user_docs/docs/mcp/index.md | 256 +++++ docs/user_docs/docs/platforms/index.md | 22 + .../docs/troubleshooting/migration.md | 44 +- docs/user_docs/docs/tutorials/index.md | 26 +- .../docs/tutorials/install-from-sources.md | 542 +++++++++++ .../docs/tutorials/skill-consolidation.md | 606 ++++++++++++ docs/user_docs/mkdocs.yml | 5 + 40 files changed, 5216 insertions(+), 110 deletions(-) create mode 100644 apps/aam-cli/src/aam_cli/commands/convert.py create mode 100644 apps/aam-cli/src/aam_cli/converters/__init__.py create mode 100644 apps/aam-cli/src/aam_cli/converters/frontmatter.py create mode 100644 apps/aam-cli/src/aam_cli/converters/mappings.py create mode 100644 apps/aam-cli/src/aam_cli/services/convert_service.py create mode 100644 apps/aam-cli/tests/unit/test_convert.py create mode 100644 docs/spec_convert_1.md create mode 100644 docs/specs/SPEC-convert-command.md create mode 100644 docs/user_docs/docs/cli/convert.md create mode 100644 docs/user_docs/docs/mcp/index.md create mode 100644 docs/user_docs/docs/tutorials/install-from-sources.md create mode 100644 docs/user_docs/docs/tutorials/skill-consolidation.md diff --git a/README.md b/README.md index 2351b18..90dfb6d 100644 --- a/README.md +++ b/README.md @@ -148,13 +148,12 @@ aam pkg publish # Upload to registry ## Default Skill Sources -On first run or when you run `aam source enable-defaults`, AAM registers 5 curated community repositories: +On first run or when you run `aam source enable-defaults`, AAM registers 4 curated community repositories: | Source | Repository | Path | |--------|------------|------| | `github/awesome-copilot` | [github/awesome-copilot](https://github.com/github/awesome-copilot) | `skills` | | `openai/skills:.curated` | [openai/skills](https://github.com/openai/skills) | `skills/.curated` | -| `cursor/community-skills` | [cursor/community-skills](https://github.com/cursor/community-skills) | `skills` | | `anthropics/skills` | [anthropics/skills](https://github.com/anthropics/skills) | `skills` | | `microsoft/skills` | [microsoft/skills](https://github.com/microsoft/skills) | `.github/skills` | @@ -168,14 +167,13 @@ flowchart LR direction TB A["github/awesome-copilot"] B["openai/skills"] - C["cursor/community-skills"] - D["anthropics/skills"] - E["microsoft/skills"] + C["anthropics/skills"] + D["microsoft/skills"] Custom["+ Your own
aam source add <url>"] end subgraph AAM["🔧 AAM"] - Cache["~/.aam/sources-cache/"] + Cache["~/.aam/cache/git/"] Scan["Scan & Index"] Cache --> Scan end @@ -188,7 +186,6 @@ flowchart LR B -->|clone| Cache C -->|clone| Cache D -->|clone| Cache - E -->|clone| Cache Custom -->|clone| Cache Scan -->|install| Cursor @@ -203,12 +200,60 @@ flowchart LR --- +## MCP Server + +AAM includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) server, allowing IDE agents to interact with AAM directly — search packages, manage sources, install skills, and get recommendations without leaving the editor. + +```bash +# Start read-only server (default, safe for any IDE) +aam mcp serve + +# Enable write operations (install, publish, source management) +aam mcp serve --allow-write +``` + +**IDE configuration (Cursor, Claude Desktop, Windsurf, VS Code):** + +```json +{ + "mcpServers": { + "aam": { + "command": "aam", + "args": ["mcp", "serve"] + } + } +} +``` + +The server exposes **29 tools** and **9 resources**: + +| Category | Read-only tools | Write tools (opt-in) | +|----------|----------------|---------------------| +| **Packages** | `search`, `list`, `info`, `validate` | `install`, `uninstall`, `publish`, `create_package` | +| **Sources** | `source_list`, `source_scan`, `source_candidates`, `source_diff` | `source_add`, `source_remove`, `source_update` | +| **Config** | `config_get`, `doctor`, `registry_list` | `config_set`, `registry_add`, `init` | +| **Discovery** | `recommend_skills`, `available`, `outdated` | `upgrade`, `init_package` | +| **Integrity** | `verify`, `diff` | — | + +**Options:** + +| Flag | Description | +|------|-------------| +| `--transport` | `stdio` (default) or `http` | +| `--port PORT` | HTTP port (default: `8000`, HTTP transport only) | +| `--allow-write` | Enable write tools (install, publish, source management). Without this flag, only read-only tools are available. | +| `--log-file PATH` | Log to file instead of stderr | +| `--log-level` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | + +--- + ## Features - **One package, all platforms** — Write once, deploy to Cursor, Claude, GitHub Copilot, and Codex - **Dependency management** — Declare dependencies, AAM resolves them automatically - **Local & centralized registries** — Work offline or share with the community - **Package signing** — Sigstore (keyless) and GPG signature support +- **MCP server** — IDE agents can use AAM tools directly via Model Context Protocol - **Simple CLI** — Intuitive commands: `init`, `install`, `pkg publish`, `source` --- @@ -232,6 +277,9 @@ flowchart LR | `aam source` | Manage git artifact sources (add, list, update, remove) | | `aam registry` | Manage registries (init, add, list) | | `aam config` | Manage configuration | +| `aam mcp serve` | Start MCP server for IDE agent integration | +| `aam doctor` | Run environment diagnostics | +| `aam convert` | Convert configs between platforms (Cursor, Copilot, Claude, Codex) | --- diff --git a/apps/aam-cli/README.md b/apps/aam-cli/README.md index 9efc450..c0a5560 100644 --- a/apps/aam-cli/README.md +++ b/apps/aam-cli/README.md @@ -323,6 +323,139 @@ aam -v publish --dry-run --- +## MCP Server + +AAM includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) server that exposes AAM capabilities as tools for IDE agents. This lets AI assistants in Cursor, Claude Desktop, Windsurf, and VS Code search for packages, manage sources, install skills, and get recommendations directly. + +### Starting the Server + +```bash +# Read-only mode (default — safe for any IDE) +aam mcp serve + +# Enable write operations (install, publish, source add/remove, etc.) +aam mcp serve --allow-write + +# HTTP/SSE transport instead of stdio +aam mcp serve --transport http --port 8000 + +# With logging +aam mcp serve --log-file /tmp/aam-mcp.log --log-level DEBUG +``` + +### Options + +| Flag | Description | +|------|-------------| +| `--transport` | `stdio` (default) or `http` | +| `--port PORT` | HTTP port (default: `8000`, only for `--transport http`) | +| `--allow-write` | Enable write tools — install, uninstall, publish, source management, config changes. **Without this flag, only read-only tools are available.** | +| `--log-file PATH` | Log to file instead of stderr | +| `--log-level` | `DEBUG`, `INFO` (default), `WARNING`, `ERROR` | + +### Safety Model + +By default the server starts in **read-only mode** — only 17 safe, non-destructive tools are available. When `--allow-write` is passed, 12 additional write tools are enabled (29 total), allowing the agent to install/uninstall packages, add/remove sources, modify configuration, and publish packages. + +### Available Tools + +**Read-only tools (always available):** + +| Tool | Description | +|------|-------------| +| `aam_search` | Search registries and sources for packages | +| `aam_list` | List installed packages | +| `aam_info` | Get detailed package metadata | +| `aam_validate` | Validate package manifest and artifacts | +| `aam_config_get` | Get configuration values | +| `aam_registry_list` | List configured registries | +| `aam_doctor` | Run environment diagnostics | +| `aam_source_list` | List configured git sources | +| `aam_source_scan` | Scan a source for artifacts | +| `aam_source_candidates` | List unpackaged artifact candidates | +| `aam_source_diff` | Preview upstream changes | +| `aam_verify` | Verify package file integrity | +| `aam_diff` | Show file differences in packages | +| `aam_outdated` | Check for outdated source-installed packages | +| `aam_available` | List all available artifacts from sources | +| `aam_init_info` | Get client initialization information | +| `aam_recommend_skills` | Recommend skills based on repo analysis | + +**Write tools (requires `--allow-write`):** + +| Tool | Description | +|------|-------------| +| `aam_install` | Install packages from registries or sources | +| `aam_uninstall` | Uninstall packages | +| `aam_publish` | Publish packages to registry | +| `aam_create_package` | Create a package manifest | +| `aam_config_set` | Set configuration values | +| `aam_registry_add` | Add a new registry | +| `aam_source_add` | Add a git source | +| `aam_source_remove` | Remove a git source | +| `aam_source_update` | Update git sources | +| `aam_upgrade` | Upgrade outdated packages | +| `aam_init` | Initialize client for a platform | +| `aam_init_package` | Scaffold a new package | + +### MCP Resources + +The server also exposes 9 read-only resources for passive data access: + +| URI | Description | +|-----|-------------| +| `aam://config` | Current merged configuration | +| `aam://packages/installed` | List of installed packages | +| `aam://packages/{name}` | Details for a specific package | +| `aam://registries` | Configured registries | +| `aam://manifest` | Read `aam.yaml` from current directory | +| `aam://sources` | List of git sources | +| `aam://sources/{source_id}` | Source details with artifacts | +| `aam://sources/{source_id}/candidates` | Candidate artifacts from a source | +| `aam://init_status` | Client initialization status | + +### IDE Configuration + +**Cursor / Claude Desktop / Windsurf** (stdio): + +```json +{ + "mcpServers": { + "aam": { + "command": "aam", + "args": ["mcp", "serve"] + } + } +} +``` + +**With write access enabled:** + +```json +{ + "mcpServers": { + "aam": { + "command": "aam", + "args": ["mcp", "serve", "--allow-write"] + } + } +} +``` + +**HTTP transport:** + +```json +{ + "mcpServers": { + "aam": { + "url": "http://localhost:8000" + } + } +} +``` + +--- + ## Development ```bash diff --git a/apps/aam-cli/src/aam_cli/commands/convert.py b/apps/aam-cli/src/aam_cli/commands/convert.py new file mode 100644 index 0000000..c8914ac --- /dev/null +++ b/apps/aam-cli/src/aam_cli/commands/convert.py @@ -0,0 +1,223 @@ +"""Convert command for AAM CLI. + +Provides ``aam convert`` to convert AI agent configurations between +platforms (Cursor, Copilot, Claude, Codex). + +Reference: docs/specs/SPEC-convert-command.md +""" + +################################################################################ +# # +# IMPORTS & DEPENDENCIES # +# # +################################################################################ + +import logging +from pathlib import Path + +import click +from rich.console import Console + +from aam_cli.converters.mappings import PLATFORMS, VERBOSE_WORKAROUNDS +from aam_cli.services.convert_service import run_conversion + +################################################################################ +# # +# LOGGING # +# # +################################################################################ + +# Initialize logger for this module +logger = logging.getLogger(__name__) + +################################################################################ +# # +# CONSTANTS # +# # +################################################################################ + +ARTIFACT_TYPES = ("instruction", "agent", "prompt", "skill") + +################################################################################ +# # +# COMMAND # +# # +################################################################################ + + +@click.command("convert") +@click.option( + "--source-platform", "-s", + required=True, + type=click.Choice(PLATFORMS, case_sensitive=False), + help="Source platform to convert from.", +) +@click.option( + "--target-platform", "-t", + required=True, + type=click.Choice(PLATFORMS, case_sensitive=False), + help="Target platform to convert to.", +) +@click.option( + "--type", + "artifact_type", + type=click.Choice(ARTIFACT_TYPES, case_sensitive=False), + default=None, + help="Filter by artifact type.", +) +@click.option( + "--dry-run", + is_flag=True, + default=False, + help="Show what would be converted without writing files.", +) +@click.option( + "--force", + is_flag=True, + default=False, + help="Overwrite existing target files (creates .bak backup).", +) +@click.option( + "--verbose", + is_flag=True, + default=False, + help="Show detailed conversion notes and workarounds.", +) +@click.pass_context +def convert( + ctx: click.Context, + source_platform: str, + target_platform: str, + artifact_type: str | None, + dry_run: bool, + force: bool, + verbose: bool, +) -> None: + """Convert AI agent configurations between platforms. + + Reads artifacts from one platform's format and writes them in + another's format. Supports Cursor, Copilot, Claude, and Codex. + + Examples:: + + aam convert -s cursor -t copilot + aam convert -s copilot -t claude --type instruction --dry-run + aam convert -s codex -t cursor --force + """ + console: Console = ctx.obj["console"] + project_dir = Path.cwd() + + # ----- + # Validate platforms are different + # ----- + if source_platform.lower() == target_platform.lower(): + console.print( + "[red]Error:[/red] Source and target platform cannot be the same." + ) + ctx.exit(1) + return + + # ----- + # Display header + # ----- + prefix = "[DRY RUN] " if dry_run else "" + console.print( + f"\n{prefix}Converting [bold]{source_platform.title()}[/bold] " + f"→ [bold]{target_platform.title()}[/bold]...\n" + ) + + # ----- + # Run conversion + # ----- + report = run_conversion( + project_root=project_dir, + source_platform=source_platform.lower(), + target_platform=target_platform.lower(), + artifact_type=artifact_type.lower() if artifact_type else None, + dry_run=dry_run, + force=force, + ) + + # ----- + # Display results grouped by type + # ----- + if not report.results: + console.print( + f" No {source_platform} artifacts found to convert." + ) + console.print() + return + + # Group results by artifact type + grouped: dict[str, list] = {} + for result in report.results: + grouped.setdefault(result.artifact_type.upper() + "S", []).append(result) + + for section_name, results in grouped.items(): + console.print(f"[bold]{section_name}:[/bold]") + + for result in results: + if result.error: + console.print( + f" [red]✗[/red] {result.source_path}" + ) + console.print(f" [red]{result.error}[/red]") + elif result.skipped: + console.print( + f" [yellow]⊘[/yellow] {result.source_path} → {result.target_path}" + ) + for warning in result.warnings: + console.print(f" [yellow]⚠ {warning}[/yellow]") + else: + console.print( + f" [green]✓[/green] {result.source_path} → {result.target_path}" + ) + for warning in result.warnings: + console.print(f" [yellow]⚠ {warning}[/yellow]") + if verbose: + _print_verbose_workaround(console, warning) + + console.print() + + # ----- + # Display summary + # ----- + summary_parts = [ + f"{report.converted_count} converted", + f"{report.failed_count} failed", + f"{report.warning_count} warnings", + ] + if report.skipped_count: + summary_parts.append(f"{report.skipped_count} skipped") + + console.print(f"[bold]SUMMARY:[/bold] {', '.join(summary_parts)}") + + if report.warning_count and not verbose: + console.print( + "\nWarnings indicate metadata that could not be converted." + ) + console.print( + "Run with [bold]--verbose[/bold] for detailed workaround instructions." + ) + + console.print() + + if report.failed_count: + ctx.exit(1) + + +################################################################################ +# # +# HELPERS # +# # +################################################################################ + + +def _print_verbose_workaround(console: Console, warning: str) -> None: + """Print verbose workaround text for a warning if available.""" + warning_lower = warning.lower() + + for key, workaround in VERBOSE_WORKAROUNDS.items(): + if key.replace("_", " ").replace("removed", "").strip() in warning_lower: + console.print(f" [dim]{workaround}[/dim]") + return diff --git a/apps/aam-cli/src/aam_cli/commands/create_package.py b/apps/aam-cli/src/aam_cli/commands/create_package.py index 2f23bc6..7ca933a 100644 --- a/apps/aam-cli/src/aam_cli/commands/create_package.py +++ b/apps/aam-cli/src/aam_cli/commands/create_package.py @@ -35,7 +35,7 @@ DetectedArtifact, scan_project, ) -from aam_cli.utils.naming import validate_package_name +from aam_cli.utils.naming import format_invalid_package_name_message, validate_package_name from aam_cli.utils.yaml_utils import dump_yaml ################################################################################ @@ -529,15 +529,12 @@ def _create_from_source( while True: name = Prompt.ask( - "Package name", + "Package name (e.g. my-pkg, @scope/my-pkg)", default=pkg_name or default_name, ) if validate_package_name(name): break - console.print( - "[red]Invalid package name.[/red] " - "Use lowercase, hyphens, optional @scope/ prefix." - ) + console.print(f"[red]{format_invalid_package_name_message(name)}[/red]") version = Prompt.ask("Version", default=pkg_version or "1.0.0") description = Prompt.ask("Description", default=pkg_description or "") @@ -796,7 +793,7 @@ def _create_from_source( default=None, help="Artifact type for --include", ) -@click.option("--name", "pkg_name", default=None, help="Package name") +@click.option("--name", "pkg_name", default=None, help="Package name (e.g. my-pkg, @scope/my-pkg)") @click.option("--scope", "pkg_scope", default=None, help="Scope prefix") @click.option("--version", "pkg_version", default=None, help="Package version") @click.option("--description", "pkg_description", default=None, help="Package description") @@ -973,14 +970,12 @@ def create_package( while True: name = Prompt.ask( - "Package name", + "Package name (e.g. my-pkg, @scope/my-pkg)", default=pkg_name or default_name, ) if validate_package_name(name): break - console.print( - "[red]Invalid package name.[/red] Use lowercase, hyphens, optional @scope/ prefix." - ) + console.print(f"[red]{format_invalid_package_name_message(name)}[/red]") version = Prompt.ask("Version", default=pkg_version or "1.0.0") description = Prompt.ask("Description", default=pkg_description or "") diff --git a/apps/aam-cli/src/aam_cli/commands/init_package.py b/apps/aam-cli/src/aam_cli/commands/init_package.py index 0078b40..6d29ddf 100644 --- a/apps/aam-cli/src/aam_cli/commands/init_package.py +++ b/apps/aam-cli/src/aam_cli/commands/init_package.py @@ -17,7 +17,7 @@ from rich.console import Console from rich.prompt import Confirm, Prompt -from aam_cli.utils.naming import validate_package_name +from aam_cli.utils.naming import format_invalid_package_name_message, validate_package_name from aam_cli.utils.yaml_utils import dump_yaml ################################################################################ @@ -67,12 +67,10 @@ def init_package(ctx: click.Context, name: str | None) -> None: default_name = name or Path.cwd().name while True: - pkg_name = Prompt.ask("Package name", default=default_name) + pkg_name = Prompt.ask("Package name (e.g. my-pkg, @scope/my-pkg)", default=default_name) if validate_package_name(pkg_name): break - console.print( - "[red]Invalid package name.[/red] Use lowercase, hyphens, optional @scope/ prefix." - ) + console.print(f"[red]{format_invalid_package_name_message(pkg_name)}[/red]") version = Prompt.ask("Version", default="1.0.0") description = Prompt.ask("Description", default="") diff --git a/apps/aam-cli/src/aam_cli/commands/pkg/create.py b/apps/aam-cli/src/aam_cli/commands/pkg/create.py index 46d325e..a7da30a 100644 --- a/apps/aam-cli/src/aam_cli/commands/pkg/create.py +++ b/apps/aam-cli/src/aam_cli/commands/pkg/create.py @@ -72,7 +72,7 @@ default=None, help="Artifact type for --include", ) -@click.option("--name", "pkg_name", default=None, help="Package name") +@click.option("--name", "pkg_name", default=None, help="Package name (e.g. my-pkg, @scope/my-pkg)") @click.option("--scope", "pkg_scope", default=None, help="Scope prefix") @click.option("--version", "pkg_version", default=None, help="Package version") @click.option("--description", "pkg_description", default=None, help="Package description") diff --git a/apps/aam-cli/src/aam_cli/commands/source.py b/apps/aam-cli/src/aam_cli/commands/source.py index 0567ebc..7c82ba8 100644 --- a/apps/aam-cli/src/aam_cli/commands/source.py +++ b/apps/aam-cli/src/aam_cli/commands/source.py @@ -606,7 +606,7 @@ def candidates( def enable_defaults(ctx: click.Context, output_json: bool) -> None: """Enable all default community skill sources. - Registers the 5 curated default skill sources shipped with AAM. + Registers the 4 curated default skill sources shipped with AAM. If any were previously removed, they are re-enabled. Sources that are already configured are skipped. diff --git a/apps/aam-cli/src/aam_cli/converters/__init__.py b/apps/aam-cli/src/aam_cli/converters/__init__.py new file mode 100644 index 0000000..3a6f419 --- /dev/null +++ b/apps/aam-cli/src/aam_cli/converters/__init__.py @@ -0,0 +1 @@ +"""Converter utilities for cross-platform artifact conversion.""" diff --git a/apps/aam-cli/src/aam_cli/converters/frontmatter.py b/apps/aam-cli/src/aam_cli/converters/frontmatter.py new file mode 100644 index 0000000..c6e126e --- /dev/null +++ b/apps/aam-cli/src/aam_cli/converters/frontmatter.py @@ -0,0 +1,92 @@ +"""YAML frontmatter parsing and generation utilities. + +Handles reading and writing YAML frontmatter delimited by ``---`` markers +in markdown files, as used by Cursor (.mdc), Copilot (.instructions.md, +.agent.md, .prompt.md), and Claude/Cursor subagent files. +""" + +################################################################################ +# # +# IMPORTS & DEPENDENCIES # +# # +################################################################################ + +import logging +from typing import Any + +import yaml + +################################################################################ +# # +# LOGGING # +# # +################################################################################ + +# Initialize logger for this module +logger = logging.getLogger(__name__) + +################################################################################ +# # +# FRONTMATTER PARSING # +# # +################################################################################ + + +def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]: + """Parse YAML frontmatter from a markdown/MDC string. + + Expects content in the form:: + + --- + key: value + --- + body content here + + Args: + text: Full file content. + + Returns: + Tuple of (frontmatter_dict, body_content). If no frontmatter + is found, returns an empty dict and the original text. + """ + stripped = text.lstrip("\n") + if not stripped.startswith("---"): + return {}, text + + # Find the closing --- + end_idx = stripped.find("---", 3) + if end_idx == -1: + return {}, text + + yaml_block = stripped[3:end_idx].strip() + body = stripped[end_idx + 3:].lstrip("\n") + + try: + frontmatter = yaml.safe_load(yaml_block) + if not isinstance(frontmatter, dict): + frontmatter = {} + except yaml.YAMLError: + logger.warning("Failed to parse YAML frontmatter") + return {}, text + + return frontmatter, body + + +def generate_frontmatter(metadata: dict[str, Any], body: str) -> str: + """Generate a markdown string with YAML frontmatter. + + Args: + metadata: Dictionary of frontmatter fields. + body: Markdown body content. + + Returns: + Combined string with ``---`` delimited frontmatter and body. + """ + if not metadata: + return body + + yaml_str = yaml.safe_dump( + metadata, default_flow_style=False, sort_keys=False + ).rstrip("\n") + + return f"---\n{yaml_str}\n---\n{body}" diff --git a/apps/aam-cli/src/aam_cli/converters/mappings.py b/apps/aam-cli/src/aam_cli/converters/mappings.py new file mode 100644 index 0000000..8428990 --- /dev/null +++ b/apps/aam-cli/src/aam_cli/converters/mappings.py @@ -0,0 +1,235 @@ +"""Platform-to-platform field mapping tables and conversion constants. + +Defines which fields are supported per platform per artifact type, +and how fields map between platforms during conversion. +""" + +################################################################################ +# # +# IMPORTS & DEPENDENCIES # +# # +################################################################################ + +import logging + +################################################################################ +# # +# LOGGING # +# # +################################################################################ + +# Initialize logger for this module +logger = logging.getLogger(__name__) + +################################################################################ +# # +# PLATFORM IDENTIFIERS # +# # +################################################################################ + +PLATFORMS = ("cursor", "copilot", "claude", "codex") + +################################################################################ +# # +# INSTRUCTION FILE PATHS # +# # +################################################################################ + +# Platform instruction file paths (source patterns for scanning) +INSTRUCTION_PATHS: dict[str, list[str]] = { + "cursor": [".cursor/rules/*.mdc", ".cursorrules"], + "copilot": [ + ".github/copilot-instructions.md", + ".github/instructions/*.instructions.md", + ], + "claude": ["CLAUDE.md", ".claude/CLAUDE.md"], + "codex": ["AGENTS.md", "AGENTS.override.md"], +} + +# Single-file instruction targets (always-on, no per-file frontmatter) +SINGLE_FILE_INSTRUCTION_TARGETS: dict[str, str] = { + "claude": "CLAUDE.md", + "codex": "AGENTS.md", +} + +################################################################################ +# # +# AGENT FILE PATHS # +# # +################################################################################ + +AGENT_PATHS: dict[str, list[str]] = { + "cursor": [".cursor/rules/agent-*.mdc", ".cursor/agents/*.md"], + "copilot": [".github/agents/*.agent.md"], + "claude": [".claude/agents/*.md"], + "codex": [], # Codex uses sections within AGENTS.md +} + +AGENT_TARGET_DIRS: dict[str, str] = { + "cursor": ".cursor/agents", + "copilot": ".github/agents", + "claude": ".claude/agents", +} + +AGENT_TARGET_EXTENSIONS: dict[str, str] = { + "cursor": ".md", + "copilot": ".agent.md", + "claude": ".md", +} + +################################################################################ +# # +# PROMPT FILE PATHS # +# # +################################################################################ + +PROMPT_PATHS: dict[str, list[str]] = { + "cursor": [".cursor/prompts/*.md", ".cursor/commands/*.md"], + "copilot": [".github/prompts/*.prompt.md"], + "claude": [".claude/prompts/*.md"], + "codex": [], # Codex has no prompt file concept +} + +PROMPT_TARGET_DIRS: dict[str, str] = { + "cursor": ".cursor/commands", + "copilot": ".github/prompts", + "claude": ".claude/prompts", +} + +PROMPT_TARGET_EXTENSIONS: dict[str, str] = { + "cursor": ".md", + "copilot": ".prompt.md", + "claude": ".md", +} + +################################################################################ +# # +# SKILL FILE PATHS # +# # +################################################################################ + +SKILL_PATHS: dict[str, list[str]] = { + "cursor": [".cursor/skills/*/SKILL.md"], + "copilot": [".github/skills/*/SKILL.md"], + "claude": [".claude/skills/*/SKILL.md"], + "codex": [".agents/skills/*/SKILL.md"], +} + +SKILL_TARGET_DIRS: dict[str, str] = { + "cursor": ".cursor/skills", + "copilot": ".github/skills", + "claude": ".claude/skills", + "codex": ".agents/skills", +} + +################################################################################ +# # +# AGENT FIELD SUPPORT # +# # +################################################################################ + +# Fields supported per platform for agent files +AGENT_SUPPORTED_FIELDS: dict[str, set[str]] = { + "cursor": {"name", "description", "model", "readonly", "is_background"}, + "copilot": { + "name", "description", "tools", "model", "agents", + "handoffs", "user-invokable", "target", + }, + "claude": {"name", "description"}, + "codex": set(), # Codex agents are just markdown sections +} + +################################################################################ +# # +# PROMPT FIELD SUPPORT # +# # +################################################################################ + +# Fields supported per platform for prompt files +PROMPT_SUPPORTED_FIELDS: dict[str, set[str]] = { + "cursor": set(), # Cursor prompts/commands are plain markdown + "copilot": {"description", "name", "agent", "model", "tools", "argument-hint"}, + "claude": set(), # Claude prompts are plain markdown + "codex": set(), +} + +################################################################################ +# # +# INSTRUCTION FIELD SUPPORT # +# # +################################################################################ + +# Fields supported per platform for instruction files +INSTRUCTION_SUPPORTED_FIELDS: dict[str, set[str]] = { + "cursor": {"description", "alwaysApply", "globs"}, + "copilot": {"name", "description", "applyTo"}, + "claude": set(), # Claude instructions are plain markdown + "codex": set(), # Codex instructions are plain markdown +} + +################################################################################ +# # +# GLOB FIELD MAPPING # +# # +################################################################################ + +# Mapping between glob/scope field names across platforms +GLOB_FIELD_MAP: dict[str, str] = { + "cursor": "globs", # list of globs + "copilot": "applyTo", # single glob string +} + +################################################################################ +# # +# WARNING MESSAGES # +# # +################################################################################ + +# Verbose workaround messages for lossy conversions +VERBOSE_WORKAROUNDS: dict[str, str] = { + "alwaysApply": ( + "The alwaysApply field controls whether a Cursor rule is always active. " + "Other platforms do not have this concept. If the rule should be " + "conditional, add context in the instruction text." + ), + "globs_lost": ( + "The target platform does not support file-scoped instructions. " + "The instruction will apply globally. Consider adding file-path " + "references in the instruction text to indicate intended scope." + ), + "tools_removed": ( + "Tool bindings are platform-specific. Configure tools manually " + "on the target platform." + ), + "handoffs_removed": ( + "Handoff workflows are a Copilot-specific feature. Implement " + "equivalent logic manually on the target platform." + ), + "model_removed": ( + "Model identifiers differ between platforms. Set the model " + "manually on the target platform." + ), + "readonly_removed": ( + "The readonly flag is not supported on the target platform. " + "Enforce read-only behavior via instruction text." + ), + "is_background_removed": ( + "The is_background flag is not supported on the target platform." + ), + "user_invokable_removed": ( + "The user-invokable flag is a Copilot-specific visibility control. " + "Not applicable on the target platform." + ), + "target_removed": ( + "The target field (vscode/github-copilot) is Copilot-specific. " + "Not applicable on the target platform." + ), + "agent_binding_removed": ( + "Prompt agent bindings are Copilot-specific. Bind the prompt " + "to an agent manually on the target platform." + ), + "mcp_servers_removed": ( + "MCP server configuration must be configured separately. " + "See target platform documentation." + ), +} diff --git a/apps/aam-cli/src/aam_cli/main.py b/apps/aam-cli/src/aam_cli/main.py index cb8e6e6..4496492 100644 --- a/apps/aam-cli/src/aam_cli/main.py +++ b/apps/aam-cli/src/aam_cli/main.py @@ -61,7 +61,7 @@ class OrderedGroup(click.Group): "Package Authoring": ["pkg"], "Source Management": ["source"], "Configuration": ["config", "registry"], - "Utilities": ["mcp", "doctor"], + "Utilities": ["mcp", "doctor", "convert"], } def format_commands( @@ -146,6 +146,7 @@ def cli(ctx: click.Context, verbose: bool) -> None: search, ) from aam_cli.commands.client_init import client_init # noqa: E402 +from aam_cli.commands.convert import convert # noqa: E402 from aam_cli.commands.diff import diff_cmd # noqa: E402 from aam_cli.commands.doctor import doctor # noqa: E402 from aam_cli.commands.list_packages import list_packages # noqa: E402 @@ -224,6 +225,7 @@ def cli(ctx: click.Context, verbose: bool) -> None: cli.add_command(mcp) cli.add_command(doctor) +cli.add_command(convert) ################################################################################ # # diff --git a/apps/aam-cli/src/aam_cli/services/convert_service.py b/apps/aam-cli/src/aam_cli/services/convert_service.py new file mode 100644 index 0000000..c61fc79 --- /dev/null +++ b/apps/aam-cli/src/aam_cli/services/convert_service.py @@ -0,0 +1,871 @@ +"""Core conversion logic for cross-platform artifact conversion. + +Reads artifacts from one platform's format and writes them in another's, +tracking warnings for lossy conversions and errors for incompatible ones. + +Reference: docs/specs/SPEC-convert-command.md +""" + +################################################################################ +# # +# IMPORTS & DEPENDENCIES # +# # +################################################################################ + +import logging +import shutil +from dataclasses import dataclass, field +from pathlib import Path + +from aam_cli.converters.frontmatter import generate_frontmatter, parse_frontmatter +from aam_cli.converters.mappings import ( + AGENT_SUPPORTED_FIELDS, + AGENT_TARGET_DIRS, + AGENT_TARGET_EXTENSIONS, + INSTRUCTION_SUPPORTED_FIELDS, + PROMPT_SUPPORTED_FIELDS, + PROMPT_TARGET_DIRS, + PROMPT_TARGET_EXTENSIONS, + SINGLE_FILE_INSTRUCTION_TARGETS, + SKILL_TARGET_DIRS, +) + +################################################################################ +# # +# LOGGING # +# # +################################################################################ + +# Initialize logger for this module +logger = logging.getLogger(__name__) + +################################################################################ +# # +# DATA MODELS # +# # +################################################################################ + + +@dataclass +class ConversionResult: + """Result of a single artifact conversion.""" + + source_path: str + target_path: str + artifact_type: str + warnings: list[str] = field(default_factory=list) + error: str | None = None + skipped: bool = False + + +@dataclass +class ConversionReport: + """Summary report of a full conversion run.""" + + source_platform: str + target_platform: str + results: list[ConversionResult] = field(default_factory=list) + + @property + def converted_count(self) -> int: + return sum( + 1 for r in self.results if not r.skipped and r.error is None + ) + + @property + def failed_count(self) -> int: + return sum(1 for r in self.results if r.error is not None) + + @property + def skipped_count(self) -> int: + return sum(1 for r in self.results if r.skipped) + + @property + def warning_count(self) -> int: + return sum(len(r.warnings) for r in self.results) + + +################################################################################ +# # +# MARKER SECTION HELPERS # +# # +################################################################################ + +BEGIN_MARKER_TEMPLATE = "" +END_MARKER_TEMPLATE = "" + + +def _upsert_marker_section(file_path: Path, name: str, content: str) -> None: + """Insert or replace a marker-delimited section in a file. + + Args: + file_path: Path to the target file. + name: Section identifier used in markers. + content: Markdown body to place between the markers. + """ + file_path.parent.mkdir(parents=True, exist_ok=True) + + begin_marker = BEGIN_MARKER_TEMPLATE.format(name=name) + end_marker = END_MARKER_TEMPLATE.format(name=name) + + section_block = f"{begin_marker}\n{content.rstrip()}\n{end_marker}\n" + + if file_path.is_file(): + existing = file_path.read_text(encoding="utf-8") + + if begin_marker in existing and end_marker in existing: + before = existing[: existing.index(begin_marker)] + after = existing[existing.index(end_marker) + len(end_marker) :] + after = after.lstrip("\n") + new_content = before.rstrip("\n") + "\n\n" + section_block + if after.strip(): + new_content += "\n" + after + else: + new_content = existing.rstrip("\n") + "\n\n" + section_block + else: + new_content = section_block + + file_path.write_text(new_content, encoding="utf-8") + + +################################################################################ +# # +# SOURCE SCANNING # +# # +################################################################################ + + +def _find_cursor_instructions(root: Path) -> list[Path]: + """Find Cursor instruction files (.mdc rules and .cursorrules).""" + files: list[Path] = [] + + # .cursor/rules/*.mdc (non-agent) + rules_dir = root / ".cursor" / "rules" + if rules_dir.is_dir(): + for mdc in rules_dir.glob("*.mdc"): + if not mdc.name.startswith("agent-"): + files.append(mdc) + + # Legacy .cursorrules + cursorrules = root / ".cursorrules" + if cursorrules.is_file(): + files.append(cursorrules) + + return files + + +def _find_copilot_instructions(root: Path) -> list[Path]: + """Find Copilot instruction files.""" + files: list[Path] = [] + + main = root / ".github" / "copilot-instructions.md" + if main.is_file(): + files.append(main) + + instr_dir = root / ".github" / "instructions" + if instr_dir.is_dir(): + for f in instr_dir.glob("*.instructions.md"): + files.append(f) + + return files + + +def _find_claude_instructions(root: Path) -> list[Path]: + """Find Claude instruction files.""" + files: list[Path] = [] + + for name in ("CLAUDE.md", ".claude/CLAUDE.md"): + p = root / name + if p.is_file(): + files.append(p) + + return files + + +def _find_codex_instructions(root: Path) -> list[Path]: + """Find Codex instruction files.""" + files: list[Path] = [] + + for name in ("AGENTS.md", "AGENTS.override.md"): + p = root / name + if p.is_file(): + files.append(p) + + return files + + +def _find_instructions(root: Path, platform: str) -> list[Path]: + """Find instruction files for a given source platform.""" + finders = { + "cursor": _find_cursor_instructions, + "copilot": _find_copilot_instructions, + "claude": _find_claude_instructions, + "codex": _find_codex_instructions, + } + return finders[platform](root) + + +def _find_agents(root: Path, platform: str) -> list[Path]: + """Find agent files for a given source platform.""" + files: list[Path] = [] + + if platform == "cursor": + # .cursor/rules/agent-*.mdc + rules_dir = root / ".cursor" / "rules" + if rules_dir.is_dir(): + files.extend(rules_dir.glob("agent-*.mdc")) + # .cursor/agents/*.md + agents_dir = root / ".cursor" / "agents" + if agents_dir.is_dir(): + files.extend(agents_dir.glob("*.md")) + + elif platform == "copilot": + agents_dir = root / ".github" / "agents" + if agents_dir.is_dir(): + files.extend(agents_dir.glob("*.agent.md")) + + elif platform == "claude": + agents_dir = root / ".claude" / "agents" + if agents_dir.is_dir(): + files.extend(agents_dir.glob("*.md")) + + # codex: no discrete agent files + + return files + + +def _find_prompts(root: Path, platform: str) -> list[Path]: + """Find prompt files for a given source platform.""" + files: list[Path] = [] + + if platform == "cursor": + for subdir in ("prompts", "commands"): + d = root / ".cursor" / subdir + if d.is_dir(): + files.extend(d.glob("*.md")) + + elif platform == "copilot": + d = root / ".github" / "prompts" + if d.is_dir(): + files.extend(d.glob("*.prompt.md")) + + elif platform == "claude": + d = root / ".claude" / "prompts" + if d.is_dir(): + files.extend(d.glob("*.md")) + + # codex: no prompt files + + return files + + +def _find_skills(root: Path, platform: str) -> list[Path]: + """Find skill directories for a given source platform. + + Returns paths to skill directories (parent of SKILL.md). + """ + dirs: list[Path] = [] + base_map = { + "cursor": ".cursor/skills", + "copilot": ".github/skills", + "claude": ".claude/skills", + "codex": ".agents/skills", + } + + base = root / base_map.get(platform, "") + if base.is_dir(): + for skill_dir in base.iterdir(): + if skill_dir.is_dir() and (skill_dir / "SKILL.md").is_file(): + dirs.append(skill_dir) + + return dirs + + +################################################################################ +# # +# CONVERSION FUNCTIONS # +# # +################################################################################ + + +def _derive_name(file_path: Path, platform: str) -> str: + """Derive an artifact name from a file path.""" + stem = file_path.stem + + # Strip platform-specific extensions + if stem.endswith(".instructions"): + stem = stem.removesuffix(".instructions") + elif stem.endswith(".agent"): + stem = stem.removesuffix(".agent") + elif stem.endswith(".prompt"): + stem = stem.removesuffix(".prompt") + + # Strip agent- prefix from Cursor agent rules + if platform == "cursor" and stem.startswith("agent-"): + stem = stem.removeprefix("agent-") + + return stem + + +def _convert_instruction( + source_file: Path, + root: Path, + source_platform: str, + target_platform: str, + force: bool, + dry_run: bool, +) -> ConversionResult: + """Convert a single instruction file.""" + name = _derive_name(source_file, source_platform) + rel_source = str(source_file.relative_to(root)) + + # Parse source content + raw_content = source_file.read_text(encoding="utf-8") + frontmatter, body = parse_frontmatter(raw_content) + + warnings: list[str] = [] + target_path_str = "" + + # ----- + # Determine target path and content + # ----- + if target_platform in SINGLE_FILE_INSTRUCTION_TARGETS: + # Claude / Codex: append to single file with markers + target_file = root / SINGLE_FILE_INSTRUCTION_TARGETS[target_platform] + target_path_str = str(target_file.relative_to(root)) + + # Warn about lost fields + if frontmatter.get("globs"): + globs = frontmatter["globs"] + warnings.append( + f"Glob-scoped instruction converted to always-on. " + f"Original globs: {globs}" + ) + if frontmatter.get("applyTo"): + apply_to = frontmatter["applyTo"] + warnings.append( + f"Conditional instruction converted to always-on. " + f"Original applyTo: {apply_to}" + ) + if "alwaysApply" in frontmatter: + warnings.append("alwaysApply metadata not supported on target platform.") + + # Build section content with scope hint + scope_hint = "" + if frontmatter.get("globs"): + scope_hint = f" (applies to: {frontmatter['globs']})" + elif frontmatter.get("applyTo"): + scope_hint = f" (applies to: {frontmatter['applyTo']})" + + section_content = body + if scope_hint: + # Prepend scope hint to first heading or as a comment + section_content = f"## {name}{scope_hint}\n\n{body}" + + if not dry_run: + if not force and target_file.is_file(): + # Check if section already exists + existing = target_file.read_text(encoding="utf-8") + begin_marker = BEGIN_MARKER_TEMPLATE.format(name=name) + if begin_marker not in existing: + # Safe to append + pass + _upsert_marker_section(target_file, name, section_content) + + return ConversionResult( + source_path=rel_source, + target_path=f"{target_path_str} (appended)", + artifact_type="instruction", + warnings=warnings, + ) + + elif target_platform == "copilot": + # Copilot: scoped instructions or main file + if frontmatter.get("globs") or frontmatter.get("alwaysApply") is False: + # Convert to scoped .instructions.md + target_dir = root / ".github" / "instructions" + target_file = target_dir / f"{name}.instructions.md" + target_path_str = str(target_file.relative_to(root)) + + new_meta: dict[str, object] = {"name": name} + if frontmatter.get("description"): + new_meta["description"] = frontmatter["description"] + if frontmatter.get("globs"): + globs = frontmatter["globs"] + # Convert list to single glob (take first if list) + if isinstance(globs, list): + new_meta["applyTo"] = globs[0] if len(globs) == 1 else ", ".join(globs) + else: + new_meta["applyTo"] = globs + + if "alwaysApply" in frontmatter: + warnings.append("alwaysApply field dropped (not supported in Copilot instructions.md)") + + content = generate_frontmatter(new_meta, body) + + if not dry_run: + if target_file.exists() and not force: + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="instruction", + skipped=True, + warnings=["Target exists, use --force to overwrite"], + ) + if target_file.exists() and force: + _backup_file(target_file) + target_file.parent.mkdir(parents=True, exist_ok=True) + target_file.write_text(content, encoding="utf-8") + + else: + # Always-on instruction -> copilot-instructions.md + target_file = root / ".github" / "copilot-instructions.md" + target_path_str = f"{target_file.relative_to(root)} (appended)" + + if "alwaysApply" in frontmatter: + warnings.append("alwaysApply field dropped (not supported in Copilot)") + + if not dry_run: + _upsert_marker_section(target_file, name, body) + + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="instruction", + warnings=warnings, + ) + + elif target_platform == "cursor": + # Convert to .cursor/rules/*.mdc + target_dir = root / ".cursor" / "rules" + target_file = target_dir / f"{name}.mdc" + target_path_str = str(target_file.relative_to(root)) + + new_meta: dict[str, object] = {} + new_meta["description"] = frontmatter.get("description", "Converted instruction") + + if frontmatter.get("applyTo"): + new_meta["alwaysApply"] = False + apply_to = frontmatter["applyTo"] + new_meta["globs"] = [apply_to] if isinstance(apply_to, str) else apply_to + else: + new_meta["alwaysApply"] = True + + content = generate_frontmatter(new_meta, body) + + if not dry_run: + if target_file.exists() and not force: + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="instruction", + skipped=True, + warnings=["Target exists, use --force to overwrite"], + ) + if target_file.exists() and force: + _backup_file(target_file) + target_file.parent.mkdir(parents=True, exist_ok=True) + target_file.write_text(content, encoding="utf-8") + + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="instruction", + warnings=warnings, + ) + + return ConversionResult( + source_path=rel_source, + target_path="", + artifact_type="instruction", + error=f"Unsupported target platform: {target_platform}", + ) + + +def _convert_agent( + source_file: Path, + root: Path, + source_platform: str, + target_platform: str, + force: bool, + dry_run: bool, +) -> ConversionResult: + """Convert a single agent file.""" + name = _derive_name(source_file, source_platform) + rel_source = str(source_file.relative_to(root)) + + raw_content = source_file.read_text(encoding="utf-8") + frontmatter, body = parse_frontmatter(raw_content) + + warnings: list[str] = [] + + # ----- + # Codex target: append to AGENTS.md as a section + # ----- + if target_platform == "codex": + target_file = root / "AGENTS.md" + target_path_str = f"{target_file.relative_to(root)} (appended)" + + warnings.append( + "Codex does not support discrete agent files. " + "Agent content appended as a section in AGENTS.md." + ) + + section = f"## Agent: {name}\n\n{body}" + if not dry_run: + _upsert_marker_section(target_file, f"agent-{name}", section) + + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="agent", + warnings=warnings, + ) + + # ----- + # Other platforms: create discrete agent file + # ----- + target_dir_name = AGENT_TARGET_DIRS.get(target_platform) + target_ext = AGENT_TARGET_EXTENSIONS.get(target_platform) + + if not target_dir_name or not target_ext: + return ConversionResult( + source_path=rel_source, + target_path="", + artifact_type="agent", + error=f"Unsupported agent target platform: {target_platform}", + ) + + target_file = root / target_dir_name / f"{name}{target_ext}" + target_path_str = str(target_file.relative_to(root)) + + # Build target frontmatter with only supported fields + supported = AGENT_SUPPORTED_FIELDS.get(target_platform, set()) + new_meta: dict[str, object] = {} + + if "name" in supported: + new_meta["name"] = frontmatter.get("name", name) + if "description" in supported and frontmatter.get("description"): + new_meta["description"] = frontmatter["description"] + + # Warn about dropped fields + for field_name in ("tools", "handoffs", "model", "readonly", "is_background", + "user-invokable", "target", "mcp-servers"): + if field_name in frontmatter and field_name not in supported: + value = frontmatter[field_name] + if field_name == "tools": + warnings.append(f"Tools {value} are {source_platform}-specific and were removed.") + elif field_name == "handoffs": + warnings.append(f"Handoffs are {source_platform}-specific and were removed: {value}.") + elif field_name == "model": + if target_platform in ("cursor", "claude"): + # Cursor/Claude subagents support model but map differently + if "model" in supported: + new_meta["model"] = "inherit" + warnings.append(f"Model '{value}' is {source_platform}-specific, set to 'inherit'.") + else: + warnings.append(f"Model '{value}' is {source_platform}-specific, removed.") + else: + warnings.append(f"Model '{value}' is {source_platform}-specific. Set model manually.") + elif field_name == "readonly" and value: + warnings.append("readonly=true not supported on target. Enforce via instruction text.") + elif field_name == "is_background" and value: + warnings.append("is_background not supported on target.") + elif field_name == "user-invokable": + warnings.append("user-invokable flag removed.") + elif field_name == "target": + warnings.append("target field removed — not applicable on target platform.") + elif field_name == "mcp-servers": + warnings.append( + f"MCP server configuration {value} must be configured separately." + ) + + # Handle model field for Cursor target (supports model) + if "model" in supported and "model" in frontmatter and "model" not in new_meta: + # Copy model if source and target both support it + new_meta["model"] = frontmatter["model"] + + content = generate_frontmatter(new_meta, body) + + if not dry_run: + if target_file.exists() and not force: + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="agent", + skipped=True, + warnings=["Target exists, use --force to overwrite"], + ) + if target_file.exists() and force: + _backup_file(target_file) + target_file.parent.mkdir(parents=True, exist_ok=True) + target_file.write_text(content, encoding="utf-8") + + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="agent", + warnings=warnings, + ) + + +def _convert_prompt( + source_file: Path, + root: Path, + source_platform: str, + target_platform: str, + force: bool, + dry_run: bool, +) -> ConversionResult: + """Convert a single prompt file.""" + name = _derive_name(source_file, source_platform) + rel_source = str(source_file.relative_to(root)) + + raw_content = source_file.read_text(encoding="utf-8") + frontmatter, body = parse_frontmatter(raw_content) + + warnings: list[str] = [] + + # ----- + # Codex target: append to AGENTS.md + # ----- + if target_platform == "codex": + target_file = root / "AGENTS.md" + target_path_str = f"{target_file.relative_to(root)} (appended)" + + warnings.append( + "Codex does not support prompt files. " + "Prompt content appended to AGENTS.md as a reference section." + ) + + section = f"## Prompt: {name}\n\n{body}" + if not dry_run: + _upsert_marker_section(target_file, f"prompt-{name}", section) + + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="prompt", + warnings=warnings, + ) + + # ----- + # Other platforms + # ----- + target_dir_name = PROMPT_TARGET_DIRS.get(target_platform) + target_ext = PROMPT_TARGET_EXTENSIONS.get(target_platform) + + if not target_dir_name or not target_ext: + return ConversionResult( + source_path=rel_source, + target_path="", + artifact_type="prompt", + error=f"Unsupported prompt target platform: {target_platform}", + ) + + target_file = root / target_dir_name / f"{name}{target_ext}" + target_path_str = str(target_file.relative_to(root)) + + # Build target frontmatter + supported = PROMPT_SUPPORTED_FIELDS.get(target_platform, set()) + new_meta: dict[str, object] = {} + + if target_platform == "copilot": + # Copilot prompts can have frontmatter + if "name" in supported: + new_meta["name"] = name + if frontmatter.get("description") and "description" in supported: + new_meta["description"] = frontmatter["description"] + + # Warn about dropped fields + for field_name in ("agent", "model", "tools", "argument-hint"): + if field_name in frontmatter and field_name not in supported: + value = frontmatter[field_name] + if field_name == "agent": + warnings.append( + f"Prompt was bound to agent '{value}'. Bind manually on target platform." + ) + elif field_name == "model": + warnings.append(f"Model '{value}' removed — target platform doesn't specify models in prompts.") + elif field_name == "tools": + warnings.append(f"Prompt tools {value} removed.") + elif field_name == "argument-hint": + warnings.append(f"argument-hint '{value}' removed.") + + content = generate_frontmatter(new_meta, body) if new_meta else body + + if not dry_run: + if target_file.exists() and not force: + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="prompt", + skipped=True, + warnings=["Target exists, use --force to overwrite"], + ) + if target_file.exists() and force: + _backup_file(target_file) + target_file.parent.mkdir(parents=True, exist_ok=True) + target_file.write_text(content, encoding="utf-8") + + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="prompt", + warnings=warnings, + ) + + +def _convert_skill( + source_dir: Path, + root: Path, + source_platform: str, + target_platform: str, + force: bool, + dry_run: bool, +) -> ConversionResult: + """Convert a skill directory (direct copy).""" + name = source_dir.name + rel_source = str(source_dir.relative_to(root)) + + target_base = SKILL_TARGET_DIRS.get(target_platform) + if not target_base: + return ConversionResult( + source_path=rel_source, + target_path="", + artifact_type="skill", + error=f"Unsupported skill target platform: {target_platform}", + ) + + target_dir = root / target_base / name + target_path_str = str(target_dir.relative_to(root)) + + if not dry_run: + if target_dir.exists() and not force: + return ConversionResult( + source_path=rel_source, + target_path=target_path_str, + artifact_type="skill", + skipped=True, + warnings=["Target exists, use --force to overwrite"], + ) + if target_dir.exists() and force: + # Backup by renaming + backup_dir = target_dir.with_suffix(".bak") + if backup_dir.exists(): + shutil.rmtree(backup_dir) + target_dir.rename(backup_dir) + + target_dir.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(source_dir, target_dir) + + return ConversionResult( + source_path=rel_source, + target_path=f"{target_path_str} (direct copy)", + artifact_type="skill", + ) + + +################################################################################ +# # +# BACKUP HELPER # +# # +################################################################################ + + +def _backup_file(file_path: Path) -> Path: + """Create a .bak backup of a file. + + Args: + file_path: File to back up. + + Returns: + Path to the backup file. + """ + backup = file_path.with_suffix(file_path.suffix + ".bak") + shutil.copy2(file_path, backup) + logger.info(f"Backed up {file_path} -> {backup}") + return backup + + +################################################################################ +# # +# PUBLIC API # +# # +################################################################################ + + +def run_conversion( + project_root: Path, + source_platform: str, + target_platform: str, + artifact_type: str | None = None, + dry_run: bool = False, + force: bool = False, +) -> ConversionReport: + """Run a full cross-platform conversion. + + Args: + project_root: Root directory of the project. + source_platform: Source platform identifier. + target_platform: Target platform identifier. + artifact_type: Optional filter (instruction, agent, prompt, skill). + dry_run: If True, do not write any files. + force: If True, overwrite existing target files (with backup). + + Returns: + ConversionReport with results for each artifact. + """ + report = ConversionReport( + source_platform=source_platform, + target_platform=target_platform, + ) + + root = project_root.resolve() + + types_to_convert = ( + [artifact_type] if artifact_type + else ["instruction", "agent", "prompt", "skill"] + ) + + # ----- + # Convert instructions + # ----- + if "instruction" in types_to_convert: + for src_file in _find_instructions(root, source_platform): + result = _convert_instruction( + src_file, root, source_platform, target_platform, force, dry_run + ) + report.results.append(result) + + # ----- + # Convert agents + # ----- + if "agent" in types_to_convert: + for src_file in _find_agents(root, source_platform): + result = _convert_agent( + src_file, root, source_platform, target_platform, force, dry_run + ) + report.results.append(result) + + # ----- + # Convert prompts + # ----- + if "prompt" in types_to_convert: + for src_file in _find_prompts(root, source_platform): + result = _convert_prompt( + src_file, root, source_platform, target_platform, force, dry_run + ) + report.results.append(result) + + # ----- + # Convert skills + # ----- + if "skill" in types_to_convert: + for src_dir in _find_skills(root, source_platform): + result = _convert_skill( + src_dir, root, source_platform, target_platform, force, dry_run + ) + report.results.append(result) + + return report diff --git a/apps/aam-cli/src/aam_cli/services/doctor_service.py b/apps/aam-cli/src/aam_cli/services/doctor_service.py index 7f3d055..d1d4ee4 100644 --- a/apps/aam-cli/src/aam_cli/services/doctor_service.py +++ b/apps/aam-cli/src/aam_cli/services/doctor_service.py @@ -184,13 +184,15 @@ def _check_config_files(project_dir: Path) -> list[dict[str, Any]]: if not config_path.exists(): # ----- - # File does not exist — perfectly fine, defaults will be used + # File does not exist — defaults will be used; warn for project config # ----- logger.debug(f"{label} not found: path='{config_path}'") + # Project config missing is a warning (user may want to initialize) + is_project = "project" in check_name.lower() checks.append( { "name": check_name, - "status": "pass", + "status": "warn" if is_project else "pass", "message": f"{label}: {config_path} (not found, using defaults)", "suggestion": None, } @@ -279,6 +281,11 @@ def _check_config_valid(project_dir: Path) -> dict[str, Any]: try: config = load_config(project_dir) reg_count = len(config.registries) + suggestion = ( + "Run 'aam init' to set up AAM and add default sources." + if reg_count == 0 + else None + ) return { "name": "config_valid", "status": "pass", @@ -286,7 +293,7 @@ def _check_config_valid(project_dir: Path) -> dict[str, Any]: f"Configuration loaded ({reg_count} " f"registr{'y' if reg_count == 1 else 'ies'} configured)" ), - "suggestion": None, + "suggestion": suggestion, } except Exception as exc: return { diff --git a/apps/aam-cli/src/aam_cli/services/init_service.py b/apps/aam-cli/src/aam_cli/services/init_service.py index 2664f14..51dc432 100644 --- a/apps/aam-cli/src/aam_cli/services/init_service.py +++ b/apps/aam-cli/src/aam_cli/services/init_service.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import Any -from aam_cli.utils.naming import validate_package_name +from aam_cli.utils.naming import format_invalid_package_name_message, validate_package_name from aam_cli.utils.yaml_utils import dump_yaml ################################################################################ @@ -102,8 +102,7 @@ def init_package( # ----- if not validate_package_name(name): raise ValueError( - f"[AAM_INVALID_ARGUMENT] Invalid package name '{name}'. " - "Use lowercase letters, digits, and hyphens, with optional @scope/ prefix." + f"[AAM_INVALID_ARGUMENT] {format_invalid_package_name_message(name)}" ) # ----- diff --git a/apps/aam-cli/src/aam_cli/services/source_service.py b/apps/aam-cli/src/aam_cli/services/source_service.py index 71dd0ca..27d93f5 100644 --- a/apps/aam-cli/src/aam_cli/services/source_service.py +++ b/apps/aam-cli/src/aam_cli/services/source_service.py @@ -204,34 +204,29 @@ class ArtifactIndex: ################################################################################ # Default sources registered on first init (per spec 003 FR-040) +# URLs use canonical HTTPS format (no .git suffix) to match git_url.parse() output DEFAULT_SOURCES: list[dict[str, str]] = [ { "name": "github/awesome-copilot", - "url": "https://github.com/github/awesome-copilot.git", + "url": "https://github.com/github/awesome-copilot", "ref": "main", "path": "skills", }, { "name": "openai/skills:.curated", - "url": "https://github.com/openai/skills.git", + "url": "https://github.com/openai/skills", "ref": "main", "path": "skills/.curated", }, - { - "name": "cursor/community-skills", - "url": "https://github.com/cursor/community-skills.git", - "ref": "main", - "path": "skills", - }, { "name": "anthropics/skills", - "url": "https://github.com/anthropics/skills.git", + "url": "https://github.com/anthropics/skills", "ref": "main", "path": "skills", }, { "name": "microsoft/skills", - "url": "https://github.com/microsoft/skills.git", + "url": "https://github.com/microsoft/skills", "ref": "main", "path": ".github/skills", }, diff --git a/apps/aam-cli/src/aam_cli/utils/naming.py b/apps/aam-cli/src/aam_cli/utils/naming.py index aceac2d..ad681b5 100644 --- a/apps/aam-cli/src/aam_cli/utils/naming.py +++ b/apps/aam-cli/src/aam_cli/utils/naming.py @@ -59,6 +59,65 @@ # Example: @author/asvc-report -> author--asvc-report FS_SEPARATOR: str = "--" +# ----- +# User-facing validation hints +# ----- +PACKAGE_NAME_HINT: str = ( + "Use lowercase letters, digits, and hyphens only (no underscores). " + "Examples: my-pkg, @scope/my-pkg" +) + + +def suggest_package_name(invalid_name: str) -> str | None: + """Suggest a valid package name by replacing underscores with hyphens. + + Only suggests when the fix is straightforward (underscores in the name + part). Returns None if the name has other issues (uppercase, etc.). + + Args: + invalid_name: The invalid name the user entered. + + Returns: + A suggested valid name, or None if no simple fix applies. + """ + if not invalid_name: + return None + # If it's just underscores in the name part, suggest hyphen replacement + if invalid_name.startswith("@"): + slash = invalid_name.find("/") + if slash == -1: + return None + scope = invalid_name[1:slash] + name_part = invalid_name[slash + 1 :] + if "_" in name_part and _SCOPE_PATTERN.match(scope): + suggested = f"@{scope}/{name_part.replace('_', '-')}" + if validate_package_name(suggested): + return suggested + else: + if "_" in invalid_name: + suggested = invalid_name.replace("_", "-") + if validate_package_name(suggested): + return suggested + return None + + +def format_invalid_package_name_message(invalid_name: str) -> str: + """Build a user-friendly error message for an invalid package name. + + Args: + invalid_name: The invalid name the user entered. + + Returns: + A complete error message, optionally with a suggested fix. + """ + suggestion = suggest_package_name(invalid_name) + if suggestion: + return ( + f"Invalid package name '{invalid_name}'. {PACKAGE_NAME_HINT} " + f"Did you mean: {suggestion}?" + ) + return f"Invalid package name '{invalid_name}'. {PACKAGE_NAME_HINT}" + ################################################################################ # # # FUNCTIONS # diff --git a/apps/aam-cli/tests/test_main.py b/apps/aam-cli/tests/test_main.py index dde87f6..33226f9 100644 --- a/apps/aam-cli/tests/test_main.py +++ b/apps/aam-cli/tests/test_main.py @@ -19,9 +19,11 @@ from aam_cli.main import cli from aam_cli.utils.naming import ( + format_invalid_package_name_message, format_package_name, parse_package_name, parse_package_spec, + suggest_package_name, to_filesystem_name, validate_package_name, ) @@ -595,6 +597,34 @@ def test_unit_invalid_names(self) -> None: assert validate_package_name("@/name") is False +class TestNamingSuggest: + """Test suggest_package_name and format_invalid_package_name_message.""" + + def test_unit_suggest_underscores_unscoped(self) -> None: + """Underscores in unscoped name -> suggest hyphens.""" + assert suggest_package_name("rb_1_prompt") == "rb-1-prompt" + + def test_unit_suggest_underscores_scoped(self) -> None: + """Underscores in scoped name part -> suggest hyphens.""" + assert suggest_package_name("@rma/rb_1_prompt") == "@rma/rb-1-prompt" + + def test_unit_suggest_no_fix_uppercase(self) -> None: + """Uppercase has no simple fix.""" + assert suggest_package_name("My-Package") is None + + def test_unit_format_message_with_suggestion(self) -> None: + """Message includes suggestion when available.""" + msg = format_invalid_package_name_message("rb_1_prompt") + assert "rb-1-prompt" in msg + assert "Did you mean" in msg + + def test_unit_format_message_without_suggestion(self) -> None: + """Message has hint but no suggestion when fix not obvious.""" + msg = format_invalid_package_name_message("UPPERCASE") + assert "UPPERCASE" in msg + assert "no underscores" in msg + + class TestNamingFormat: """Test format_package_name and to_filesystem_name utilities.""" diff --git a/apps/aam-cli/tests/unit/test_convert.py b/apps/aam-cli/tests/unit/test_convert.py new file mode 100644 index 0000000..29bbc06 --- /dev/null +++ b/apps/aam-cli/tests/unit/test_convert.py @@ -0,0 +1,594 @@ +"""Unit tests for the convert command and conversion service.""" + +################################################################################ +# # +# IMPORTS & DEPENDENCIES # +# # +################################################################################ + +import logging +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from aam_cli.converters.frontmatter import generate_frontmatter, parse_frontmatter +from aam_cli.main import cli +from aam_cli.services.convert_service import ( + ConversionReport, + ConversionResult, + run_conversion, +) + +################################################################################ +# # +# LOGGING # +# # +################################################################################ + +logger = logging.getLogger(__name__) + +################################################################################ +# # +# HELPERS # +# # +################################################################################ + + +def _write_file(path: Path, content: str) -> None: + """Write a file, creating parent directories.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +################################################################################ +# # +# FRONTMATTER TESTS # +# # +################################################################################ + + +class TestParseFrontmatter: + """Tests for YAML frontmatter parsing.""" + + def test_parse_with_frontmatter(self) -> None: + text = '---\ndescription: "test"\nalwaysApply: true\n---\n# Body\nContent here' + meta, body = parse_frontmatter(text) + assert meta["description"] == "test" + assert meta["alwaysApply"] is True + assert "# Body" in body + assert "Content here" in body + + def test_parse_without_frontmatter(self) -> None: + text = "# Just markdown\nNo frontmatter here." + meta, body = parse_frontmatter(text) + assert meta == {} + assert body == text + + def test_parse_with_list_field(self) -> None: + text = '---\nglobs:\n - "**/*.py"\n - "**/*.pyi"\n---\nBody' + meta, body = parse_frontmatter(text) + assert meta["globs"] == ["**/*.py", "**/*.pyi"] + assert body == "Body" + + def test_parse_empty_frontmatter(self) -> None: + text = "---\n---\nBody content" + meta, body = parse_frontmatter(text) + assert meta == {} + assert "Body content" in body + + def test_parse_invalid_yaml(self) -> None: + text = "---\n: invalid: yaml: [[\n---\nBody" + meta, body = parse_frontmatter(text) + # Should return empty dict and original text on parse error + assert meta == {} + + +class TestGenerateFrontmatter: + """Tests for YAML frontmatter generation.""" + + def test_generate_with_metadata(self) -> None: + result = generate_frontmatter( + {"name": "test", "description": "A test"}, + "# Body\nContent", + ) + assert result.startswith("---\n") + assert "name: test" in result + assert "description: A test" in result + assert result.endswith("# Body\nContent") + + def test_generate_without_metadata(self) -> None: + result = generate_frontmatter({}, "# Body\nContent") + assert result == "# Body\nContent" + + def test_roundtrip(self) -> None: + original_meta = {"description": "test", "alwaysApply": False} + original_body = "# Instructions\nDo things." + text = generate_frontmatter(original_meta, original_body) + parsed_meta, parsed_body = parse_frontmatter(text) + assert parsed_meta["description"] == "test" + assert parsed_meta["alwaysApply"] is False + assert "# Instructions" in parsed_body + + +################################################################################ +# # +# CONVERSION SERVICE TESTS # +# # +################################################################################ + + +class TestConversionReport: + """Tests for ConversionReport data model.""" + + def test_counts(self) -> None: + report = ConversionReport( + source_platform="cursor", + target_platform="copilot", + results=[ + ConversionResult("a", "b", "instruction"), + ConversionResult("c", "d", "instruction", warnings=["w1"]), + ConversionResult("e", "f", "agent", error="failed"), + ConversionResult("g", "h", "skill", skipped=True), + ], + ) + assert report.converted_count == 2 + assert report.failed_count == 1 + assert report.skipped_count == 1 + assert report.warning_count == 1 + + +class TestInstructionConversion: + """Tests for instruction file conversion.""" + + def test_cursor_mdc_to_copilot_instructions(self, tmp_path: Path) -> None: + """Cursor .mdc with globs → Copilot .instructions.md.""" + _write_file( + tmp_path / ".cursor" / "rules" / "python-style.mdc", + '---\ndescription: "Python coding standards"\nalwaysApply: false\n' + 'globs:\n - "**/*.py"\n---\n# Python Standards\nUse type hints.', + ) + + report = run_conversion(tmp_path, "cursor", "copilot") + + assert report.converted_count == 1 + result = report.results[0] + assert result.artifact_type == "instruction" + assert ".github/instructions/python-style.instructions.md" in result.target_path + + # Check output file + target = tmp_path / ".github" / "instructions" / "python-style.instructions.md" + assert target.is_file() + content = target.read_text() + assert "applyTo" in content + assert "**/*.py" in content + assert "# Python Standards" in content + + # Should warn about alwaysApply dropped + assert any("alwaysApply" in w for w in result.warnings) + + def test_cursor_mdc_always_apply_to_copilot(self, tmp_path: Path) -> None: + """Cursor .mdc with alwaysApply=true → Copilot copilot-instructions.md.""" + _write_file( + tmp_path / ".cursor" / "rules" / "general.mdc", + '---\ndescription: "General rules"\nalwaysApply: true\n---\n' + "Always follow these rules.", + ) + + report = run_conversion(tmp_path, "cursor", "copilot") + + assert report.converted_count == 1 + result = report.results[0] + assert "copilot-instructions.md" in result.target_path + + def test_cursor_mdc_to_claude(self, tmp_path: Path) -> None: + """Cursor .mdc → Claude CLAUDE.md (appended with markers).""" + _write_file( + tmp_path / ".cursor" / "rules" / "style.mdc", + '---\nglobs:\n - "**/*.py"\n---\nUse Black formatter.', + ) + + report = run_conversion(tmp_path, "cursor", "claude") + + assert report.converted_count == 1 + target = tmp_path / "CLAUDE.md" + assert target.is_file() + content = target.read_text() + assert "" in content + assert "" in content + assert "Use Black formatter." in content + + # Should warn about globs lost + result = report.results[0] + assert any("globs" in w.lower() or "always-on" in w.lower() for w in result.warnings) + + def test_cursorrules_to_claude(self, tmp_path: Path) -> None: + """Legacy .cursorrules → Claude CLAUDE.md.""" + _write_file(tmp_path / ".cursorrules", "Be helpful and concise.") + + report = run_conversion(tmp_path, "cursor", "claude") + + assert report.converted_count == 1 + target = tmp_path / "CLAUDE.md" + assert target.is_file() + assert "Be helpful and concise." in target.read_text() + + def test_copilot_instructions_to_cursor(self, tmp_path: Path) -> None: + """Copilot .instructions.md with applyTo → Cursor .mdc.""" + _write_file( + tmp_path / ".github" / "instructions" / "react.instructions.md", + '---\napplyTo: "**/*.tsx"\n---\nUse functional components.', + ) + + report = run_conversion(tmp_path, "copilot", "cursor") + + assert report.converted_count == 1 + target = tmp_path / ".cursor" / "rules" / "react.mdc" + assert target.is_file() + content = target.read_text() + meta, body = parse_frontmatter(content) + assert meta["globs"] == ["**/*.tsx"] + assert meta["alwaysApply"] is False + assert "Use functional components." in body + + def test_copilot_main_to_codex(self, tmp_path: Path) -> None: + """Copilot copilot-instructions.md → Codex AGENTS.md.""" + _write_file( + tmp_path / ".github" / "copilot-instructions.md", + "Follow coding standards.", + ) + + report = run_conversion(tmp_path, "copilot", "codex") + + assert report.converted_count == 1 + target = tmp_path / "AGENTS.md" + assert target.is_file() + assert "Follow coding standards." in target.read_text() + + def test_claude_to_codex(self, tmp_path: Path) -> None: + """Claude CLAUDE.md → Codex AGENTS.md.""" + _write_file(tmp_path / "CLAUDE.md", "Always write tests.") + + report = run_conversion(tmp_path, "claude", "codex") + + assert report.converted_count == 1 + target = tmp_path / "AGENTS.md" + assert target.is_file() + assert "Always write tests." in target.read_text() + + def test_no_source_files(self, tmp_path: Path) -> None: + """No source files → empty report.""" + report = run_conversion(tmp_path, "cursor", "copilot") + assert report.converted_count == 0 + assert len(report.results) == 0 + + +class TestAgentConversion: + """Tests for agent file conversion.""" + + def test_copilot_agent_to_cursor(self, tmp_path: Path) -> None: + """Copilot .agent.md → Cursor subagent.""" + _write_file( + tmp_path / ".github" / "agents" / "reviewer.agent.md", + '---\ndescription: "Code review specialist"\n' + 'tools:\n - "github/*"\n - "terminal"\n' + 'model: "gpt-4o"\n---\nReview code for bugs.', + ) + + report = run_conversion(tmp_path, "copilot", "cursor") + + # Find agent result + agent_results = [r for r in report.results if r.artifact_type == "agent"] + assert len(agent_results) == 1 + result = agent_results[0] + + target = tmp_path / ".cursor" / "agents" / "reviewer.md" + assert target.is_file() + content = target.read_text() + meta, body = parse_frontmatter(content) + + assert meta["name"] == "reviewer" + assert meta["description"] == "Code review specialist" + assert "Review code for bugs." in body + + # Should warn about tools and model + assert any("tools" in w.lower() for w in result.warnings) + + def test_cursor_agent_to_claude(self, tmp_path: Path) -> None: + """Cursor subagent → Claude subagent (drops unsupported fields).""" + _write_file( + tmp_path / ".cursor" / "agents" / "helper.md", + '---\nname: helper\ndescription: "Helper subagent"\n' + "model: fast\nreadonly: true\nis_background: true\n---\n" + "Help with tasks.", + ) + + report = run_conversion(tmp_path, "cursor", "claude") + + agent_results = [r for r in report.results if r.artifact_type == "agent"] + assert len(agent_results) == 1 + result = agent_results[0] + + target = tmp_path / ".claude" / "agents" / "helper.md" + assert target.is_file() + meta, body = parse_frontmatter(target.read_text()) + + assert meta["name"] == "helper" + assert meta["description"] == "Helper subagent" + assert "model" not in meta + assert "readonly" not in meta + assert "is_background" not in meta + + # Check warnings + assert any("model" in w.lower() for w in result.warnings) + assert any("readonly" in w.lower() for w in result.warnings) + assert any("is_background" in w.lower() for w in result.warnings) + + def test_agent_to_codex(self, tmp_path: Path) -> None: + """Any agent → Codex appends to AGENTS.md.""" + _write_file( + tmp_path / ".cursor" / "agents" / "tester.md", + '---\nname: tester\ndescription: "Test agent"\n---\n' + "Run all tests.", + ) + + report = run_conversion(tmp_path, "cursor", "codex") + + agent_results = [r for r in report.results if r.artifact_type == "agent"] + assert len(agent_results) == 1 + + target = tmp_path / "AGENTS.md" + assert target.is_file() + content = target.read_text() + assert "## Agent: tester" in content + assert "Run all tests." in content + + +class TestPromptConversion: + """Tests for prompt file conversion.""" + + def test_copilot_prompt_to_cursor(self, tmp_path: Path) -> None: + """Copilot .prompt.md → Cursor command (drops frontmatter).""" + _write_file( + tmp_path / ".github" / "prompts" / "review.prompt.md", + '---\ndescription: "Review changes"\nagent: "agent"\n' + 'model: "gpt-4o"\ntools:\n - "terminal"\n---\n' + "Review all staged changes.", + ) + + report = run_conversion(tmp_path, "copilot", "cursor") + + prompt_results = [r for r in report.results if r.artifact_type == "prompt"] + assert len(prompt_results) == 1 + result = prompt_results[0] + + target = tmp_path / ".cursor" / "commands" / "review.md" + assert target.is_file() + content = target.read_text() + + # Should be plain markdown (no frontmatter) + assert not content.startswith("---") + assert "Review all staged changes." in content + + # Should warn about dropped fields + assert any("agent" in w.lower() for w in result.warnings) + assert any("model" in w.lower() for w in result.warnings) + assert any("tools" in w.lower() for w in result.warnings) + + def test_cursor_prompt_to_copilot(self, tmp_path: Path) -> None: + """Cursor prompt → Copilot .prompt.md (adds extension).""" + _write_file( + tmp_path / ".cursor" / "commands" / "deploy.md", + "Deploy the application to production.", + ) + + report = run_conversion(tmp_path, "cursor", "copilot") + + prompt_results = [r for r in report.results if r.artifact_type == "prompt"] + assert len(prompt_results) == 1 + + target = tmp_path / ".github" / "prompts" / "deploy.prompt.md" + assert target.is_file() + + def test_prompt_to_codex(self, tmp_path: Path) -> None: + """Prompt → Codex appends to AGENTS.md.""" + _write_file( + tmp_path / ".cursor" / "prompts" / "test.md", + "Run all unit tests.", + ) + + report = run_conversion(tmp_path, "cursor", "codex") + + prompt_results = [r for r in report.results if r.artifact_type == "prompt"] + assert len(prompt_results) == 1 + + target = tmp_path / "AGENTS.md" + assert target.is_file() + assert "## Prompt: test" in target.read_text() + + def test_cursor_prompt_to_claude(self, tmp_path: Path) -> None: + """Cursor prompt → Claude prompt (direct copy).""" + _write_file( + tmp_path / ".cursor" / "prompts" / "review.md", + "Review the code changes.", + ) + + report = run_conversion(tmp_path, "cursor", "claude") + + prompt_results = [r for r in report.results if r.artifact_type == "prompt"] + assert len(prompt_results) == 1 + + target = tmp_path / ".claude" / "prompts" / "review.md" + assert target.is_file() + assert "Review the code changes." in target.read_text() + + +class TestSkillConversion: + """Tests for skill directory conversion.""" + + def test_cursor_skill_to_copilot(self, tmp_path: Path) -> None: + """Cursor skill → Copilot skill (direct copy).""" + skill_dir = tmp_path / ".cursor" / "skills" / "code-review" + _write_file(skill_dir / "SKILL.md", "# Code Review Skill") + _write_file(skill_dir / "config.yaml", "timeout: 30") + + report = run_conversion(tmp_path, "cursor", "copilot") + + skill_results = [r for r in report.results if r.artifact_type == "skill"] + assert len(skill_results) == 1 + result = skill_results[0] + assert "direct copy" in result.target_path + + target = tmp_path / ".github" / "skills" / "code-review" + assert target.is_dir() + assert (target / "SKILL.md").is_file() + assert (target / "config.yaml").is_file() + + +class TestConflictHandling: + """Tests for conflict resolution behavior.""" + + def test_skip_existing_without_force(self, tmp_path: Path) -> None: + """Target exists + no --force → skip with warning.""" + _write_file( + tmp_path / ".cursor" / "agents" / "helper.md", + '---\nname: helper\n---\nOriginal.', + ) + # Pre-create target + _write_file( + tmp_path / ".claude" / "agents" / "helper.md", + "Existing content.", + ) + + report = run_conversion(tmp_path, "cursor", "claude", force=False) + + agent_results = [r for r in report.results if r.artifact_type == "agent"] + assert len(agent_results) == 1 + assert agent_results[0].skipped is True + assert any("force" in w.lower() for w in agent_results[0].warnings) + + # Original content preserved + assert "Existing content." in ( + tmp_path / ".claude" / "agents" / "helper.md" + ).read_text() + + def test_overwrite_with_force(self, tmp_path: Path) -> None: + """Target exists + --force → overwrite with .bak backup.""" + _write_file( + tmp_path / ".cursor" / "agents" / "helper.md", + '---\nname: helper\n---\nNew content.', + ) + target = tmp_path / ".claude" / "agents" / "helper.md" + _write_file(target, "Old content.") + + report = run_conversion(tmp_path, "cursor", "claude", force=True) + + agent_results = [r for r in report.results if r.artifact_type == "agent"] + assert len(agent_results) == 1 + assert not agent_results[0].skipped + + # Backup created + assert (tmp_path / ".claude" / "agents" / "helper.md.bak").is_file() + assert "Old content." in ( + tmp_path / ".claude" / "agents" / "helper.md.bak" + ).read_text() + + # New content written + assert "New content." in target.read_text() + + def test_same_platform_error(self) -> None: + """Same source and target → error.""" + runner = CliRunner() + result = runner.invoke( + cli, + ["convert", "-s", "cursor", "-t", "cursor"], + catch_exceptions=False, + ) + assert "cannot be the same" in result.output + + +class TestDryRun: + """Tests for dry-run mode.""" + + def test_dry_run_no_files_written(self, tmp_path: Path) -> None: + """Dry run should not create any files.""" + _write_file( + tmp_path / ".cursor" / "rules" / "test.mdc", + '---\ndescription: "Test"\nalwaysApply: true\n---\nTest content.', + ) + + report = run_conversion( + tmp_path, "cursor", "copilot", dry_run=True + ) + + assert report.converted_count == 1 + + # No target files created + assert not (tmp_path / ".github").exists() + + +class TestTypeFilter: + """Tests for --type filtering.""" + + def test_filter_by_type(self, tmp_path: Path) -> None: + """Only convert artifacts of the specified type.""" + _write_file( + tmp_path / ".cursor" / "rules" / "style.mdc", + '---\nalwaysApply: true\n---\nStyle rules.', + ) + _write_file( + tmp_path / ".cursor" / "agents" / "helper.md", + '---\nname: helper\n---\nHelp.', + ) + + # Only convert instructions + report = run_conversion( + tmp_path, "cursor", "copilot", artifact_type="instruction" + ) + + assert report.converted_count == 1 + assert all(r.artifact_type == "instruction" for r in report.results) + + +class TestCLICommand: + """Tests for the Click CLI command.""" + + def test_convert_help(self) -> None: + runner = CliRunner() + result = runner.invoke(cli, ["convert", "--help"]) + assert result.exit_code == 0 + assert "--source-platform" in result.output + assert "--target-platform" in result.output + assert "--dry-run" in result.output + assert "--force" in result.output + + def test_convert_dry_run_output(self, tmp_path: Path) -> None: + """CLI dry-run shows [DRY RUN] prefix.""" + _write_file( + tmp_path / ".cursor" / "rules" / "test.mdc", + '---\nalwaysApply: true\n---\nTest.', + ) + + runner = CliRunner() + import os + with runner.isolated_filesystem(temp_dir=tmp_path): + os.chdir(tmp_path) + result = runner.invoke( + cli, + ["convert", "-s", "cursor", "-t", "copilot", "--dry-run"], + catch_exceptions=False, + ) + assert "DRY RUN" in result.output + + def test_convert_no_artifacts_found(self, tmp_path: Path) -> None: + """CLI shows message when no artifacts found.""" + runner = CliRunner() + import os + with runner.isolated_filesystem(temp_dir=tmp_path): + os.chdir(tmp_path) + result = runner.invoke( + cli, + ["convert", "-s", "cursor", "-t", "copilot"], + catch_exceptions=False, + ) + assert "No cursor artifacts found" in result.output diff --git a/apps/aam-cli/tests/unit/test_services_doctor.py b/apps/aam-cli/tests/unit/test_services_doctor.py index f35ea58..9f46d9b 100644 --- a/apps/aam-cli/tests/unit/test_services_doctor.py +++ b/apps/aam-cli/tests/unit/test_services_doctor.py @@ -206,7 +206,7 @@ def test_unit_doctor_config_files_global_only(self, tmp_path) -> None: assert "(valid)" in gc["message"] pc = checks[1] - assert pc["status"] == "pass" + assert pc["status"] == "warn" assert "not found, using defaults" in pc["message"] assert str(project_cfg) in pc["message"] @@ -234,7 +234,7 @@ def test_unit_doctor_config_files_project_only(self, tmp_path) -> None: assert "(valid)" in pc["message"] def test_unit_doctor_config_files_neither_exists(self, tmp_path) -> None: - """Neither config file exists; both report not found with pass.""" + """Neither config file exists; global passes, project warns.""" global_cfg = tmp_path / "global" / "config.yaml" project_cfg = tmp_path / "project" / ".aam" / "config.yaml" @@ -246,9 +246,11 @@ def test_unit_doctor_config_files_neither_exists(self, tmp_path) -> None: assert len(checks) == 2 - for check in checks: - assert check["status"] == "pass" - assert "not found, using defaults" in check["message"] + gc, pc = checks[0], checks[1] + assert gc["status"] == "pass" + assert "not found, using defaults" in gc["message"] + assert pc["status"] == "warn" + assert "not found, using defaults" in pc["message"] def test_unit_doctor_config_files_invalid_yaml(self, tmp_path) -> None: """Config file with broken YAML reports fail status.""" diff --git a/apps/aam-cli/tests/unit/test_services_search.py b/apps/aam-cli/tests/unit/test_services_search.py index 1182c8f..4ff644e 100644 --- a/apps/aam-cli/tests/unit/test_services_search.py +++ b/apps/aam-cli/tests/unit/test_services_search.py @@ -1202,14 +1202,14 @@ def test_unit_search_same_name_multiple_sources_both_shown( ) vp2 = _make_virtual_package( "docs-writer", - source_name="cursor/community-skills", - description="Write docs (Cursor)", + source_name="anthropics/skills", + description="Write docs (Anthropic)", commit_sha="fff9999aaa1111", ) mock_index = MagicMock() mock_index.by_qualified_name = { "google-gemini/gemini-skills/docs-writer": vp1, - "cursor/community-skills/docs-writer": vp2, + "anthropics/skills/docs-writer": vp2, } mock_build_index.return_value = mock_index @@ -1217,7 +1217,7 @@ def test_unit_search_same_name_multiple_sources_both_shown( registries=[], sources=[ _make_source("google-gemini/gemini-skills"), - _make_source("cursor/community-skills"), + _make_source("anthropics/skills"), ], ) response = search_packages("docs", config) @@ -1228,7 +1228,7 @@ def test_unit_search_same_name_multiple_sources_both_shown( assert len(response.results) == 2 origins = {r.origin for r in response.results} assert "google-gemini/gemini-skills" in origins - assert "cursor/community-skills" in origins + assert "anthropics/skills" in origins # Both should have distinct version hashes versions = {r.version for r in response.results} diff --git a/apps/aam-cli/tests/unit/test_unit_client_init.py b/apps/aam-cli/tests/unit/test_unit_client_init.py index 706bec0..3e3220b 100644 --- a/apps/aam-cli/tests/unit/test_unit_client_init.py +++ b/apps/aam-cli/tests/unit/test_unit_client_init.py @@ -122,7 +122,7 @@ def test_full_result(self) -> None: platform="copilot", registry_created=True, registry_name="my-registry", - sources_added=["community-skills", "awesome-prompts"], + sources_added=["anthropics/skills", "awesome-prompts"], config_path=Path("/tmp/test/config.yaml"), is_reconfigure=True, ) @@ -178,12 +178,12 @@ def test_basic_init( """Orchestrate init saves config and registers sources.""" mock_aam_dir.return_value = tmp_path mock_load.return_value = MagicMock(default_platform="cursor") - mock_sources.return_value = ["community-skills"] + mock_sources.return_value = ["anthropics/skills"] result = orchestrate_init(platform="copilot") assert result.platform == "copilot" - assert result.sources_added == ["community-skills"] + assert result.sources_added == ["anthropics/skills"] assert result.is_reconfigure is False mock_save.assert_called_once() diff --git a/apps/aam-cli/tests/unit/test_unit_default_sources.py b/apps/aam-cli/tests/unit/test_unit_default_sources.py index 7c9bd56..99994ea 100644 --- a/apps/aam-cli/tests/unit/test_unit_default_sources.py +++ b/apps/aam-cli/tests/unit/test_unit_default_sources.py @@ -120,9 +120,9 @@ def test_unit_no_save_when_nothing_registered( assert len(result["skipped"]) == len(DEFAULT_SOURCES) mock_save.assert_not_called() - def test_unit_default_sources_constant_has_five_entries(self) -> None: - """DEFAULT_SOURCES list contains exactly 5 curated sources.""" - assert len(DEFAULT_SOURCES) == 5 + def test_unit_default_sources_constant_has_four_entries(self) -> None: + """DEFAULT_SOURCES list contains exactly 4 curated sources.""" + assert len(DEFAULT_SOURCES) == 4 def test_unit_default_sources_constant_valid(self) -> None: """DEFAULT_SOURCES list has required fields.""" @@ -154,22 +154,22 @@ def test_unit_enables_all_defaults_on_empty_config( mock_load: MagicMock, mock_save: MagicMock, ) -> None: - """Enables all 5 defaults when config has no sources.""" + """Enables all 4 defaults when config has no sources.""" mock_load.return_value = AamConfig() result = enable_default_sources() - assert len(result["registered"]) == 5 + assert len(result["registered"]) == 4 assert len(result["skipped"]) == 0 assert len(result["re_enabled"]) == 0 - assert result["total"] == 5 + assert result["total"] == 4 mock_save.assert_called_once() # ----- # Verify all sources were persisted with default=True # ----- saved_config = mock_save.call_args[0][0] - assert len(saved_config.sources) == 5 + assert len(saved_config.sources) == 4 for source in saved_config.sources: assert source.default is True @@ -192,8 +192,8 @@ def test_unit_skips_already_configured_sources( result = enable_default_sources() assert DEFAULT_SOURCES[0]["name"] in result["skipped"] - assert len(result["registered"]) == 4 - assert result["total"] == 5 + assert len(result["registered"]) == 3 + assert result["total"] == 4 mock_save.assert_called_once() @patch("aam_cli.services.source_service.save_global_config") @@ -215,7 +215,7 @@ def test_unit_re_enables_previously_removed_defaults( # ----- assert removed_name in result["re_enabled"] assert removed_name in result["registered"] - assert len(result["registered"]) == 5 + assert len(result["registered"]) == 4 # ----- # removed_defaults should be cleared for this source @@ -230,7 +230,7 @@ def test_unit_no_save_when_all_already_present( mock_load: MagicMock, mock_save: MagicMock, ) -> None: - """Does not save when all 5 defaults are already configured.""" + """Does not save when all 4 defaults are already configured.""" existing_sources = [ SourceEntry( name=d["name"], @@ -247,8 +247,8 @@ def test_unit_no_save_when_all_already_present( result = enable_default_sources() assert len(result["registered"]) == 0 - assert len(result["skipped"]) == 5 - assert result["total"] == 5 + assert len(result["skipped"]) == 4 + assert result["total"] == 4 mock_save.assert_not_called() @patch("aam_cli.services.source_service.save_global_config") @@ -265,8 +265,8 @@ def test_unit_re_enables_all_removed_defaults( result = enable_default_sources() - assert len(result["registered"]) == 5 - assert len(result["re_enabled"]) == 5 + assert len(result["registered"]) == 4 + assert len(result["re_enabled"]) == 4 assert len(result["skipped"]) == 0 mock_save.assert_called_once() diff --git a/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py b/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py index 788ec61..28523ad 100644 --- a/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py +++ b/apps/aam-cli/tests/unit/test_unit_mcp_init_tools.py @@ -81,7 +81,7 @@ def test_unit_init_valid_platform(self, mock_config: MagicMock, mock_mcp: MagicM platform="cursor", registry_created=False, registry_name=None, - sources_added=["community-skills"], + sources_added=["anthropics/skills"], config_path=Path("/home/user/.aam/config.yaml"), is_reconfigure=False, ) @@ -97,7 +97,7 @@ def test_unit_init_valid_platform(self, mock_config: MagicMock, mock_mcp: MagicM result = tool_fn(platform="cursor", skip_sources=False) assert result["platform"] == "cursor" - assert result["sources_added"] == ["community-skills"] + assert result["sources_added"] == ["anthropics/skills"] assert result["is_reconfigure"] is False mock_orchestrate.assert_called_once_with( platform="cursor", diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 091bd82..521b8f0 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -668,6 +668,7 @@ Configuration: Utilities: mcp serve Start MCP stdio server for IDE integration doctor Check environment and diagnose issues + convert Convert configs between platforms ``` ### 5.2 Command Details @@ -1057,6 +1058,39 @@ asvc-auditor@1.0.0 report-templates ^2.0.0 (installed: 2.1.0) ``` +#### `aam convert` + +Converts AI agent configurations between platforms (Cursor, Copilot, Claude, Codex). +Reads artifacts from one platform's format and writes them in another's format, +with warnings about metadata that cannot be directly converted. + +```bash +# Convert all Cursor configs to Copilot format +aam convert -s cursor -t copilot + +# Convert only instructions, dry-run +aam convert -s copilot -t claude --type instruction --dry-run + +# Force overwrite existing target files (with .bak backup) +aam convert -s codex -t cursor --force +``` + +**Options:** +- `--source-platform` / `-s` (required): Source platform +- `--target-platform` / `-t` (required): Target platform +- `--type`: Filter by artifact type (`instruction`, `agent`, `prompt`, `skill`) +- `--dry-run`: Preview conversions without writing files +- `--force`: Overwrite existing targets (creates `.bak` backup) +- `--verbose`: Show detailed workaround instructions for lossy conversions + +**Conversion behavior:** +- Skills: Direct copy (universal `SKILL.md` format) +- Instructions: Field mapping (e.g. Cursor `globs` → Copilot `applyTo`) +- Agents: Metadata filtering (platform-specific fields dropped with warnings) +- Prompts: Frontmatter stripping or addition as needed + +See `docs/specs/SPEC-convert-command.md` for the full conversion matrix. + ### 5.3 MCP Server Mode AAM exposes its CLI capabilities as an **MCP (Model Context Protocol) server**, allowing AI agents inside IDEs (Cursor, VS Code, Windsurf, etc.) to invoke AAM operations programmatically. The server communicates over **stdio** (JSON-RPC 2.0) — the standard transport for locally-embedded MCP servers. @@ -1142,6 +1176,16 @@ CLI commands are exposed as MCP tools via `@mcp.tool` decorators. Each tool is t | `aam_config_get` | `aam config get` | Get configuration value(s) | | `aam_registry_list` | `aam registry list` | List configured registries | | `aam_doctor` | `aam doctor` | Check environment and diagnose issues | +| `aam_source_list` | `aam source list` | List configured artifact sources | +| `aam_source_scan` | `aam source scan` | Scan a source for available artifacts | +| `aam_source_candidates` | `aam source candidates` | List unpackaged candidates from a source | +| `aam_source_diff` | `aam source diff` | Show diff between source and installed version | +| `aam_verify` | `aam verify` | Verify installed package integrity | +| `aam_diff` | `aam diff` | Show diff between installed and source versions | +| `aam_outdated` | `aam outdated` | Check for outdated source-installed packages | +| `aam_available` | `aam available` | List all available artifacts from configured sources | +| `aam_recommend_skills` | `aam recommend-skills` | Recommend skills based on repository analysis | +| `aam_init_info` | `aam init --info` | Get client initialization status and detected platform | **Write tools** (require `--allow-write`): @@ -1153,6 +1197,12 @@ CLI commands are exposed as MCP tools via `@mcp.tool` decorators. Each tool is t | `aam_create_package` | `aam pkg create` | Create AAM package from existing project | | `aam_config_set` | `aam config set` | Set a configuration value | | `aam_registry_add` | `aam registry add` | Add a new registry source | +| `aam_init_package` | `aam pkg init` | Scaffold a new package with directories and manifest | +| `aam_source_add` | `aam source add` | Add a remote git repository as an artifact source | +| `aam_source_remove` | `aam source remove` | Remove a configured source | +| `aam_source_update` | `aam source update` | Fetch upstream changes for one or all sources | +| `aam_upgrade` | `aam upgrade` | Upgrade outdated source-installed packages | +| `aam_init` | `aam init` | Initialize the AAM client for a specific AI platform | > **Tool naming:** All tools are prefixed with `aam_` to avoid collisions when multiple > MCP servers are active in an IDE. @@ -1259,20 +1309,20 @@ def aam_doctor() -> dict: @mcp.tool(tags={"write"}) def aam_install( packages: list[str], - dev: bool = False, - force: bool = False, platform: str | None = None, + force: bool = False, + no_deploy: bool = False, ) -> dict: - """Install one or more AAM packages and their dependencies. + """Install AAM packages from registries or local sources. Args: - packages: Package specifiers (e.g. ['asvc-auditor', '@author/pkg@1.2.0']). - dev: Install as development dependency. - force: Force reinstall even if already installed. - platform: Deploy to specific platform only (default: all configured). + packages: List of package specs (e.g. ['my-pkg', 'other@1.0']). + platform: Target platform override (default: from config). + force: Reinstall even if already present. + no_deploy: Download only, skip deployment. Returns: - Installation summary with installed packages, versions, and deployment paths. + Install result with lists of installed, already_installed, and failed packages. """ ... diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index b21578d..0b8097d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -22,6 +22,7 @@ This guide walks you through creating, publishing, and installing AAM packages w 11. [Portable Bundles](#11-portable-bundles) 12. [MCP Server Integration](#12-mcp-server-integration) 13. [Environment Diagnostics (aam doctor)](#13-environment-diagnostics-aam-doctor) +14. [Cross-Platform Conversion (aam convert)](#14-cross-platform-conversion-aam-convert) --- @@ -1856,7 +1857,7 @@ packages: $ aam outdated Package Current Latest Source Status -code-analysis 1a2b3c4 9f8e7d6 community-skills outdated +code-analysis 1a2b3c4 9f8e7d6 anthropics/skills outdated common-prompts 5e6f7a8 b1c2d3e awesome-prompts outdated (modified) 2 outdated, 1 up to date, 0 from registry @@ -3308,8 +3309,8 @@ AAM ships with 4 curated community skill sources: - `github/awesome-copilot` - GitHub Copilot skills - `openai/skills:.curated` - OpenAI curated skills -- `cursor/community-skills` - Cursor community skills -- `anthropic/claude-prompts` - Anthropic Claude prompts +- `anthropics/skills` - Anthropic Claude skills +- `microsoft/skills` - Microsoft skills These are registered automatically when you run `aam init`. You can also enable them at any time: @@ -3442,7 +3443,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) | `aam_recommend_skills` | Recommend skills based on repository analysis | | `aam_init_info` | Get client initialization status and detected platform | -**Write tools** (require `--allow-write`, 10 tools): +**Write tools** (require `--allow-write`, 12 tools): | Tool | Description | |------|-------------| @@ -3456,6 +3457,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) | `aam_source_add` | Add a remote git repository as an artifact source | | `aam_source_remove` | Remove a configured source (with optional cache purge) | | `aam_source_update` | Fetch upstream changes for one or all sources | +| `aam_upgrade` | Upgrade outdated source-installed packages (supports `dry_run` and `force`) | +| `aam_init` | Initialize the AAM client for a specific AI platform (`platform`, `skip_sources`) | **Skill recommendation (`aam_recommend_skills`):** @@ -3485,6 +3488,7 @@ Resources provide passive data access — agents can read them without invoking | `aam://sources` | List of all configured remote git sources | | `aam://sources/{id}` | Source details with artifact list (use `--` for `/` in names) | | `aam://sources/{id}/candidates` | Unpackaged candidates from a source | +| `aam://init_status` | Client initialization status and detected platform | ### 13.6 Example Agent Conversations @@ -3566,6 +3570,93 @@ AAM Environment Diagnostics --- +## 14. Cross-Platform Conversion (aam convert) + +The `aam convert` command converts AI agent configurations between platforms, +making it easy to migrate or maintain configs across Cursor, Copilot, Claude, and Codex. + +### 14.1 Basic Usage + +```bash +# Convert all Cursor configs to Copilot format +aam convert -s cursor -t copilot + +# Convert only instructions from Copilot to Claude +aam convert -s copilot -t claude --type instruction + +# Preview what would happen without writing files +aam convert -s cursor -t copilot --dry-run +``` + +### 14.2 What Gets Converted + +| Artifact Type | Behavior | +|---------------|----------| +| **Skills** | Direct copy — universal `SKILL.md` format works everywhere | +| **Instructions** | Field mapping (e.g. Cursor `globs` ↔ Copilot `applyTo`). Platform-only fields generate warnings | +| **Agents** | Metadata filtered to target platform's supported fields. Codex gets AGENTS.md sections | +| **Prompts** | Frontmatter stripped or added as needed. Codex gets AGENTS.md sections | + +### 14.3 Handling Lossy Conversions + +Some metadata cannot be converted between platforms. The convert command warns +you about each lost field and suggests workarounds: + +```bash +$ aam convert -s cursor -t claude --verbose + +INSTRUCTIONS: + ✓ .cursor/rules/python-style.mdc → CLAUDE.md (appended) + ⚠ Glob-scoped instruction converted to always-on. Original globs: **/*.py + The target platform does not support file-scoped instructions. + The instruction will apply globally. Consider adding file-path + references in the instruction text to indicate intended scope. +``` + +### 14.4 Force Overwrite with Backup + +By default, existing target files are skipped. Use `--force` to overwrite +(a `.bak` backup is created automatically): + +```bash +aam convert -s codex -t cursor --force +``` + +### 14.5 Appending to Single-File Targets + +Claude (`CLAUDE.md`) and Codex (`AGENTS.md`) use single files for instructions. +When multiple source files convert to the same target, they are appended with +section markers: + +```markdown + +## Python Standards (applies to: **/*.py) +Use type hints... + +``` + +Re-running the conversion updates existing sections in place. + +### 14.6 Common Conversion Workflows + +**Migrating from Cursor to Copilot:** +```bash +aam convert -s cursor -t copilot --dry-run # Preview first +aam convert -s cursor -t copilot # Run conversion +``` + +**Setting up Claude Code alongside Cursor:** +```bash +aam convert -s cursor -t claude +``` + +**Converting Codex AGENTS.md to Cursor rules:** +```bash +aam convert -s codex -t cursor --force +``` + +--- + For more details, see: - [DESIGN.md](./DESIGN.md) — Architecture and concepts - [HTTP_REGISTRY_SPEC.md](./HTTP_REGISTRY_SPEC.md) — Registry API specification diff --git a/docs/spec_convert_1.md b/docs/spec_convert_1.md new file mode 100644 index 0000000..991028b --- /dev/null +++ b/docs/spec_convert_1.md @@ -0,0 +1,480 @@ +Feature Specification: aam convert Command │ +│ │ +│ Context │ +│ │ +│ Teams often migrate between AI coding agents (Cursor, VS Code Copilot, Claude Code, Codex) or use multiple platforms simultaneously. Each platform uses different file paths, formats, and frontmatter conventions for the same concepts │ +│ (instructions, agents, prompts, skills). Currently there is no automated way to convert configurations between platforms. The aam convert command solves this by reading artifacts from one platform's format and writing them in another's format, │ +│ with clear warnings about what cannot be directly converted. │ +│ │ +│ Spec location: docs/specs/SPEC-convert-command.md │ +│ │ +│ --- │ +│ Command Interface │ +│ │ +│ aam convert --source-platform --target-platform [OPTIONS] │ +│ │ +│ Arguments │ +│ │ +│ ┌────────────────────────┬──────────┬────────────────────────────────────────────────────────────┐ │ +│ │ Flag │ Required │ Description │ │ +│ ├────────────────────────┼──────────┼────────────────────────────────────────────────────────────┤ │ +│ │ --source-platform / -s │ Yes │ Source platform: cursor, copilot, claude, codex │ │ +│ ├────────────────────────┼──────────┼────────────────────────────────────────────────────────────┤ │ +│ │ --target-platform / -t │ Yes │ Target platform: cursor, copilot, claude, codex │ │ +│ ├────────────────────────┼──────────┼────────────────────────────────────────────────────────────┤ │ +│ │ --type │ No │ Filter by artifact type: instruction, agent, prompt, skill │ │ +│ ├────────────────────────┼──────────┼────────────────────────────────────────────────────────────┤ │ +│ │ --dry-run │ No │ Show what would be converted without writing files │ │ +│ ├────────────────────────┼──────────┼────────────────────────────────────────────────────────────┤ │ +│ │ --force │ No │ Overwrite existing target files │ │ +│ ├────────────────────────┼──────────┼────────────────────────────────────────────────────────────┤ │ +│ │ --verbose │ No │ Show detailed conversion notes and warnings │ │ +│ └────────────────────────┴──────────┴────────────────────────────────────────────────────────────┘ │ +│ │ +│ Example Usage │ +│ │ +│ # Convert all Cursor configs to Copilot format │ +│ aam convert -s cursor -t copilot │ +│ │ +│ # Convert only instructions, dry-run │ +│ aam convert -s copilot -t claude --type instruction --dry-run │ +│ │ +│ # Convert Codex AGENTS.md to Cursor rules │ +│ aam convert -s codex -t cursor --force │ +│ │ +│ --- │ +│ Platform Configuration Reference │ +│ │ +│ Instructions │ +│ │ +│ ┌─────────────────┬────────────────────────────────────────┬─────────────────────────────┬───────────────────────────────────┐ │ +│ │ Platform │ File(s) │ Format │ Metadata │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Cursor │ .cursor/rules/*.mdc │ MDC with YAML frontmatter │ description, alwaysApply, globs │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Cursor (legacy) │ .cursorrules │ Plain markdown │ None │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Copilot │ .github/copilot-instructions.md │ Markdown │ None (always-on) │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Copilot │ .github/instructions/*.instructions.md │ Markdown + YAML frontmatter │ name, description, applyTo (glob) │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Claude │ CLAUDE.md or .claude/CLAUDE.md │ Plain markdown │ None (always-on) │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Codex │ AGENTS.md │ Plain markdown │ None (always-on) │ │ +│ ├─────────────────┼────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────────┤ │ +│ │ Codex │ AGENTS.override.md │ Plain markdown │ None (takes priority) │ │ +│ └─────────────────┴────────────────────────────────────────┴─────────────────────────────┴───────────────────────────────────┘ │ +│ │ +│ Agents / Subagents │ +│ │ +│ ┌──────────┬─────────────────────────────────┬─────────────────────────────┬───────────────────────────────────────────────────────────────────────────┐ │ +│ │ Platform │ File(s) │ Format │ Metadata │ │ +│ ├──────────┼─────────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor │ .cursor/rules/agent-*.mdc │ MDC + YAML frontmatter │ description, alwaysApply │ │ +│ ├──────────┼─────────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor │ .cursor/agents/*.md (subagents) │ Markdown + YAML frontmatter │ name, description, model, readonly, is_background │ │ +│ ├──────────┼─────────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤ │ +│ │ Copilot │ .github/agents/*.agent.md │ Markdown + YAML frontmatter │ description, name, tools, model, agents, handoffs, user-invokable, target │ │ +│ ├──────────┼─────────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤ │ +│ │ Claude │ .claude/agents/*.md (subagents) │ Markdown + YAML frontmatter │ Same fields as Cursor subagents │ │ +│ ├──────────┼─────────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────────────────────────┤ │ +│ │ Codex │ Sections within AGENTS.md │ Plain markdown │ None │ │ +│ └──────────┴─────────────────────────────────┴─────────────────────────────┴───────────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Prompts / Commands │ +│ │ +│ ┌──────────┬─────────────────────────────┬─────────────────────────────┬───────────────────────────────────────────────────────┐ │ +│ │ Platform │ File(s) │ Format │ Metadata │ │ +│ ├──────────┼─────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────┤ │ +│ │ Cursor │ .cursor/prompts/*.md │ Plain markdown │ None │ │ +│ ├──────────┼─────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────┤ │ +│ │ Cursor │ .cursor/commands/*.md │ Plain markdown │ None (invoked via /name) │ │ +│ ├──────────┼─────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────┤ │ +│ │ Copilot │ .github/prompts/*.prompt.md │ Markdown + YAML frontmatter │ description, name, agent, model, tools, argument-hint │ │ +│ ├──────────┼─────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────┤ │ +│ │ Claude │ .claude/prompts/*.md │ Plain markdown │ None │ │ +│ ├──────────┼─────────────────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────┤ │ +│ │ Codex │ N/A │ N/A │ No dedicated prompt files │ │ +│ └──────────┴─────────────────────────────┴─────────────────────────────┴───────────────────────────────────────────────────────┘ │ +│ │ +│ Skills │ +│ │ +│ ┌──────────┬─────────────────────────────────┬────────────────────────────────────────┬─────────────────────────────────┐ │ +│ │ Platform │ File(s) │ Format │ Metadata │ │ +│ ├──────────┼─────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ │ +│ │ Cursor │ .cursor/skills/*/SKILL.md │ Markdown │ None │ │ +│ ├──────────┼─────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ │ +│ │ Copilot │ Follows agentskills.io standard │ SKILL.md + optional metadata │ Via agent integration │ │ +│ ├──────────┼─────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ │ +│ │ Claude │ .claude/skills/*/ │ Directory with files │ None │ │ +│ ├──────────┼─────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ │ +│ │ Codex │ .agents/skills/*/SKILL.md │ Markdown + optional agents/openai.yaml │ interface, policy, dependencies │ │ +│ └──────────┴─────────────────────────────────┴────────────────────────────────────────┴─────────────────────────────────┘ │ +│ │ +│ --- │ +│ Conversion Matrix │ +│ │ +│ Direct Conversions (lossless or near-lossless) │ +│ │ +│ ┌─────────────────────────────────┬─────────────────────────────────┬──────────────────┬────────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Source │ Target │ Artifact │ Conversion │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Any │ Any │ Skill (SKILL.md) │ Direct copy — universal format across all platforms. Only deployment path changes. │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor .cursorrules │ Claude CLAUDE.md │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor .cursorrules │ Codex AGENTS.md │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor .cursorrules │ Copilot copilot-instructions.md │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Copilot copilot-instructions.md │ Claude CLAUDE.md │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Copilot copilot-instructions.md │ Codex AGENTS.md │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Copilot copilot-instructions.md │ Cursor .cursorrules │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Claude CLAUDE.md │ Codex AGENTS.md │ instruction │ Direct content copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor prompts │ Cursor commands │ prompt │ Direct copy (same format) │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor prompts │ Claude prompts │ prompt │ Direct copy │ │ +│ ├─────────────────────────────────┼─────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────────────────────────────────┤ │ +│ │ Cursor commands │ Copilot prompts │ prompt │ Add .prompt.md extension, can add frontmatter │ │ +│ └─────────────────────────────────┴─────────────────────────────────┴──────────────────┴────────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Lossy Conversions (data loss — user notified) │ +│ │ +│ ┌──────────────────────────────────┬──────────────────────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┐ │ +│ │ Source │ Target │ What's Lost │ Workaround │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Cursor .mdc with globs │ Claude / Codex │ globs field — no conditional instruction support │ Warning: "Instruction will apply globally. Original globs were: {globs}. Add manual file-path references in the instruction text." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Cursor .mdc with globs │ Copilot .instructions.md │ Partial — globs maps to applyTo │ Auto-convert: globs → applyTo frontmatter (best-effort, glob syntax may differ) │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot .instructions.md applyTo │ Cursor .mdc │ Partial — applyTo maps to globs │ Auto-convert: applyTo → globs frontmatter │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot .instructions.md applyTo │ Claude / Codex │ applyTo field lost │ Warning: "Conditional instruction converted to always-on. Original applyTo: {applyTo}." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Cursor .mdc alwaysApply │ All others │ alwaysApply field │ Warning: "alwaysApply metadata not supported on target platform." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot agent tools list │ Cursor / Claude / Codex │ Tool bindings are platform-specific │ Warning: "Tools {tools} are Copilot-specific and were removed. Configure tools manually on target platform." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot agent handoffs │ All others │ Handoff workflows │ Warning: "Handoffs are Copilot-specific and were removed: {handoffs}." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot agent model │ Cursor / Claude / Codex │ Model identifiers differ per platform │ Warning: "Model {model} is Copilot-specific. Set model manually on target platform." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Cursor subagent readonly │ Copilot / Codex │ readonly flag │ Warning: "readonly=true not supported on target. Enforce via instruction text." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Cursor subagent is_background │ Copilot / Codex │ is_background flag │ Warning: "is_background not supported on target." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Cursor subagent model: fast │ Copilot │ fast model alias │ Warning: "Model alias fast is Cursor-specific. Map manually." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot agent user-invokable │ All others │ Visibility control │ Warning: "user-invokable flag removed." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot agent target │ All others │ Environment targeting (vscode/github-copilot) │ Warning: "target field removed — not applicable on target platform." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot prompt agent field │ Cursor / Claude │ Agent binding │ Warning: "Prompt was bound to agent {agent}. Bind manually on target platform." │ +│ │ │ +│ ├──────────────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot prompt tools field │ Cursor / Claude │ Tool availability │ Warning: "Prompt tools {tools} removed." │ +│ │ │ +│ └──────────────────────────────────┴──────────────────────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┘ │ +│ │ +│ Incompatible Conversions (cannot convert — user notified) │ +│ │ +│ ┌───────────────────────────────────────┬───────────┬───────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┐ │ +│ │ Source │ Target │ Reason │ User Notification │ +│ │ │ +│ ├───────────────────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Codex Starlark rules │ Any │ Completely different concept — execution policy rules in Starlark, │ Error: "Codex Starlark rules define command execution policies, not AI instructions. These cannot be converted. Create │ +│ │ │ +│ │ (.codex/rules/*.rules) │ │ not AI instructions │ equivalent instructions manually." │ +│ │ │ +│ ├───────────────────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Any agent │ Codex │ Codex has no discrete agent files — uses AGENTS.md sections │ Warning: "Codex does not support discrete agent files. Agent content will be appended as a section in AGENTS.md with a │ +│ │ │ +│ │ │ │ │ heading." │ +│ │ │ +│ ├───────────────────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Any prompt │ Codex │ Codex has no prompt file concept │ Warning: "Codex does not support prompt files. Prompt content will be appended to AGENTS.md as a reference section." │ +│ │ │ +│ ├───────────────────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┤ │ +│ │ Copilot agent mcp-servers │ All │ MCP config is platform-specific │ Warning: "MCP server configuration {servers} must be configured separately. See target platform docs." │ +│ │ │ +│ │ │ others │ │ │ +│ │ │ +│ └───────────────────────────────────────┴───────────┴───────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ +│ ─┘ │ +│ │ +│ --- │ +│ Conversion Logic Detail │ +│ │ +│ Instruction Conversion │ +│ │ +│ Cursor .mdc → Copilot .instructions.md: │ +│ INPUT: .cursor/rules/python-style.mdc │ +│ --- │ +│ description: "Python coding standards" │ +│ alwaysApply: false │ +│ globs: ["**/*.py"] │ +│ --- │ +│ # Python Standards │ +│ Use type hints... │ +│ │ +│ OUTPUT: .github/instructions/python-style.instructions.md │ +│ --- │ +│ name: "python-style" │ +│ description: "Python coding standards" │ +│ applyTo: "**/*.py" │ +│ --- │ +│ # Python Standards │ +│ Use type hints... │ +│ │ +│ WARNINGS: "alwaysApply field dropped (not supported in Copilot instructions.md)" │ +│ │ +│ Cursor .mdc → Claude CLAUDE.md: │ +│ INPUT: .cursor/rules/python-style.mdc (with globs: ["**/*.py"]) │ +│ │ +│ OUTPUT: Appended to CLAUDE.md with section markers: │ +│ │ +│ ## Python Standards (applies to: **/*.py) │ +│ Use type hints... │ +│ │ +│ │ +│ WARNINGS: "Glob-scoped instruction converted to always-on. Original globs: **/*.py" │ +│ │ +│ Copilot .instructions.md → Cursor .mdc: │ +│ INPUT: .github/instructions/react.instructions.md │ +│ --- │ +│ applyTo: "**/*.tsx" │ +│ --- │ +│ Use functional components... │ +│ │ +│ OUTPUT: .cursor/rules/react.mdc │ +│ --- │ +│ description: "Converted from Copilot instructions" │ +│ alwaysApply: false │ +│ globs: ["**/*.tsx"] │ +│ --- │ +│ Use functional components... │ +│ │ +│ Agent Conversion │ +│ │ +│ Copilot .agent.md → Cursor subagent: │ +│ INPUT: .github/agents/reviewer.agent.md │ +│ --- │ +│ description: "Code review specialist" │ +│ tools: ["github/*", "terminal"] │ +│ model: "gpt-4o" │ +│ --- │ +│ Review code for bugs... │ +│ │ +│ OUTPUT: .cursor/agents/reviewer.md │ +│ --- │ +│ name: reviewer │ +│ description: "Code review specialist" │ +│ model: inherit │ +│ --- │ +│ Review code for bugs... │ +│ │ +│ WARNINGS: │ +│ - "tools ['github/*', 'terminal'] removed — configure tools separately in Cursor" │ +│ - "model 'gpt-4o' is Copilot-specific, set to 'inherit'" │ +│ │ +│ Cursor subagent → Claude subagent: │ +│ INPUT: .cursor/agents/helper.md │ +│ --- │ +│ name: helper │ +│ description: "Helper subagent" │ +│ model: fast │ +│ readonly: true │ +│ is_background: true │ +│ --- │ +│ Help with tasks... │ +│ │ +│ OUTPUT: .claude/agents/helper.md │ +│ --- │ +│ name: helper │ +│ description: "Helper subagent" │ +│ --- │ +│ Help with tasks... │ +│ │ +│ WARNINGS: │ +│ - "model 'fast' is Cursor-specific, removed" │ +│ - "readonly=true not supported in Claude, removed" │ +│ - "is_background=true not supported in Claude, removed" │ +│ │ +│ Prompt Conversion │ +│ │ +│ Copilot .prompt.md → Cursor command: │ +│ INPUT: .github/prompts/review.prompt.md │ +│ --- │ +│ description: "Review current changes" │ +│ agent: "agent" │ +│ model: "gpt-4o" │ +│ tools: ["terminal", "github/*"] │ +│ --- │ +│ Review all staged changes... │ +│ │ +│ OUTPUT: .cursor/commands/review.md │ +│ Review all staged changes... │ +│ │ +│ WARNINGS: │ +│ - "agent binding 'agent' removed — Cursor commands don't bind to agents" │ +│ - "model 'gpt-4o' removed — Cursor commands don't specify models" │ +│ - "tools ['terminal', 'github/*'] removed — Cursor commands don't specify tools" │ +│ │ +│ --- │ +│ Output Format │ +│ │ +│ Console Output (default) │ +│ │ +│ $ aam convert -s cursor -t copilot │ +│ │ +│ Converting Cursor → Copilot... │ +│ │ +│ INSTRUCTIONS: │ +│ ✓ .cursor/rules/python-style.mdc → .github/instructions/python-style.instructions.md │ +│ ⚠ alwaysApply field dropped │ +│ ✓ .cursor/rules/general.mdc → .github/copilot-instructions.md (appended) │ +│ ⚠ globs field dropped — instruction now applies globally │ +│ ✓ .cursorrules → .github/copilot-instructions.md (appended) │ +│ │ +│ AGENTS: │ +│ ✓ .cursor/agents/reviewer.md → .github/agents/reviewer.agent.md │ +│ ⚠ model 'fast' → removed (Copilot uses different model identifiers) │ +│ ⚠ readonly=true → removed (not supported) │ +│ ⚠ is_background=true → removed (not supported) │ +│ ✓ .cursor/rules/agent-helper.mdc → .github/agents/helper.agent.md │ +│ │ +│ PROMPTS: │ +│ ✓ .cursor/commands/review.md → .github/prompts/review.prompt.md │ +│ ✓ .cursor/prompts/template.md → .github/prompts/template.prompt.md │ +│ │ +│ SKILLS: │ +│ ✓ .cursor/skills/code-review/ → .github/skills/code-review/ (direct copy) │ +│ │ +│ SUMMARY: 7 converted, 0 failed, 5 warnings │ +│ │ +│ Warnings indicate metadata that could not be converted. │ +│ Run with --verbose for detailed workaround instructions. │ +│ │ +│ Dry-run Output │ +│ │ +│ Same format but prefixed with [DRY RUN] and no files written. │ +│ │ +│ Verbose Output │ +│ │ +│ Includes full workaround text for each warning: │ +│ ⚠ model 'fast' → removed │ +│ Workaround: Copilot uses model identifiers like "gpt-4o" or "claude-sonnet-4-5-20250929". │ +│ Set the model field manually in the converted .agent.md file. │ +│ See: https://code.visualstudio.com/docs/copilot/customization/custom-agents │ +│ │ +│ --- │ +│ Conflict Handling │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────┐ │ +│ │ Scenario │ Behavior │ │ +│ ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤ │ +│ │ Target file already exists │ Skip with warning: "Target exists, use --force to overwrite" │ │ +│ ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤ │ +│ │ Target file exists + --force │ Overwrite with backup: creates .bak file │ │ +│ ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤ │ +│ │ Multiple source instructions → single target file (e.g., → CLAUDE.md) │ Append with section markers │ │ +│ ├───────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤ │ +│ │ Source and target are same platform │ Error: "Source and target platform cannot be the same" │ │ +│ └───────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────┘ │ +│ │ +│ --- │ +│ Implementation Notes │ +│ │ +│ Reusable components from existing codebase │ +│ │ +│ ┌──────────────────────────┬──────────────────────┬────────────────────────────────────┐ │ +│ │ Component │ File │ Reuse │ │ +│ ├──────────────────────────┼──────────────────────┼────────────────────────────────────┤ │ +│ │ scan_project() │ detection/scanner.py │ Detect source artifacts │ │ +│ ├──────────────────────────┼──────────────────────┼────────────────────────────────────┤ │ +│ │ DetectedArtifact model │ detection/scanner.py │ Artifact data structure │ │ +│ ├──────────────────────────┼──────────────────────┼────────────────────────────────────┤ │ +│ │ Platform adapters │ adapters/*.py │ Deployment logic for target format │ │ +│ ├──────────────────────────┼──────────────────────┼────────────────────────────────────┤ │ +│ │ _upsert_marker_section() │ adapters/copilot.py │ Marker-based section merging │ │ +│ ├──────────────────────────┼──────────────────────┼────────────────────────────────────┤ │ +│ │ ArtifactRef model │ core/manifest.py │ Artifact reference with metadata │ │ +│ └──────────────────────────┴──────────────────────┴────────────────────────────────────┘ │ +│ │ +│ New components needed │ +│ │ +│ ┌─────────────────────────────┬───────────────────────────────────────────┐ │ +│ │ Component │ Description │ │ +│ ├─────────────────────────────┼───────────────────────────────────────────┤ │ +│ │ commands/convert.py │ Click command definition │ │ +│ ├─────────────────────────────┼───────────────────────────────────────────┤ │ +│ │ services/convert_service.py │ Core conversion logic │ │ +│ ├─────────────────────────────┼───────────────────────────────────────────┤ │ +│ │ converters/frontmatter.py │ YAML frontmatter parse/generate utilities │ │ +│ ├─────────────────────────────┼───────────────────────────────────────────┤ │ +│ │ converters/mappings.py │ Platform-to-platform field mapping tables │ │ +│ └─────────────────────────────┴───────────────────────────────────────────┘ │ +│ │ +│ --- │ +│ Files to Create/Modify │ +│ │ +│ ┌──────────────────────────────────────────────────────┬──────────────────────────────────────────┐ │ +│ │ File │ Action │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/src/aam_cli/commands/convert.py │ New — Click command │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/src/aam_cli/services/convert_service.py │ New — conversion logic │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/src/aam_cli/converters/__init__.py │ New — converter package │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/src/aam_cli/converters/frontmatter.py │ New — YAML frontmatter parsing │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/src/aam_cli/converters/mappings.py │ New — field mapping tables │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/src/aam_cli/commands/__init__.py │ Modify — register convert command │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ apps/aam-cli/tests/unit/test_convert.py │ New — unit tests │ │ +│ ├──────────────────────────────────────────────────────┼──────────────────────────────────────────┤ │ +│ │ docs/specs/SPEC-convert-command.md │ New — save this specification to project │ │ +│ └──────────────────────────────────────────────────────┴──────────────────────────────────────────┘ │ +│ diff --git a/docs/specs/SPEC-convert-command.md b/docs/specs/SPEC-convert-command.md new file mode 100644 index 0000000..b8d87c5 --- /dev/null +++ b/docs/specs/SPEC-convert-command.md @@ -0,0 +1,372 @@ +# Feature Specification: `aam convert` Command + +## Context + +Teams often migrate between AI coding agents (Cursor, VS Code Copilot, Claude Code, Codex) or use multiple platforms simultaneously. Each platform uses different file paths, formats, and frontmatter conventions for the same concepts (instructions, agents, prompts, skills). Currently there is no automated way to convert configurations between platforms. The `aam convert` command solves this by reading artifacts from one platform's format and writing them in another's format, with clear warnings about what cannot be directly converted. + +**Spec location:** `docs/specs/SPEC-convert-command.md` + +--- + +## Command Interface + +``` +aam convert --source-platform --target-platform [OPTIONS] +``` + +### Arguments + +| Flag | Required | Description | +|------|----------|-------------| +| `--source-platform` / `-s` | Yes | Source platform: `cursor`, `copilot`, `claude`, `codex` | +| `--target-platform` / `-t` | Yes | Target platform: `cursor`, `copilot`, `claude`, `codex` | +| `--type` | No | Filter by artifact type: `instruction`, `agent`, `prompt`, `skill` | +| `--dry-run` | No | Show what would be converted without writing files | +| `--force` | No | Overwrite existing target files | +| `--verbose` | No | Show detailed conversion notes and warnings | + +### Example Usage + +```bash +# Convert all Cursor configs to Copilot format +aam convert -s cursor -t copilot + +# Convert only instructions, dry-run +aam convert -s copilot -t claude --type instruction --dry-run + +# Convert Codex AGENTS.md to Cursor rules +aam convert -s codex -t cursor --force +``` + +--- + +## Platform Configuration Reference + +### Instructions + +| Platform | File(s) | Format | Metadata | +|----------|---------|--------|----------| +| **Cursor** | `.cursor/rules/*.mdc` | MDC with YAML frontmatter | `description`, `alwaysApply`, `globs` | +| **Cursor** (legacy) | `.cursorrules` | Plain markdown | None | +| **Copilot** | `.github/copilot-instructions.md` | Markdown | None (always-on) | +| **Copilot** | `.github/instructions/*.instructions.md` | Markdown + YAML frontmatter | `name`, `description`, `applyTo` (glob) | +| **Claude** | `CLAUDE.md` or `.claude/CLAUDE.md` | Plain markdown | None (always-on) | +| **Codex** | `AGENTS.md` | Plain markdown | None (always-on) | +| **Codex** | `AGENTS.override.md` | Plain markdown | None (takes priority) | + +### Agents / Subagents + +| Platform | File(s) | Format | Metadata | +|----------|---------|--------|----------| +| **Cursor** | `.cursor/rules/agent-*.mdc` | MDC + YAML frontmatter | `description`, `alwaysApply` | +| **Cursor** | `.cursor/agents/*.md` (subagents) | Markdown + YAML frontmatter | `name`, `description`, `model`, `readonly`, `is_background` | +| **Copilot** | `.github/agents/*.agent.md` | Markdown + YAML frontmatter | `description`, `name`, `tools`, `model`, `agents`, `handoffs`, `user-invokable`, `target` | +| **Claude** | `.claude/agents/*.md` (subagents) | Markdown + YAML frontmatter | Same fields as Cursor subagents | +| **Codex** | Sections within `AGENTS.md` | Plain markdown | None | + +### Prompts / Commands + +| Platform | File(s) | Format | Metadata | +|----------|---------|--------|----------| +| **Cursor** | `.cursor/prompts/*.md` | Plain markdown | None | +| **Cursor** | `.cursor/commands/*.md` | Plain markdown | None (invoked via `/name`) | +| **Copilot** | `.github/prompts/*.prompt.md` | Markdown + YAML frontmatter | `description`, `name`, `agent`, `model`, `tools`, `argument-hint` | +| **Claude** | `.claude/prompts/*.md` | Plain markdown | None | +| **Codex** | N/A | N/A | No dedicated prompt files | + +### Skills + +| Platform | File(s) | Format | Metadata | +|----------|---------|--------|----------| +| **Cursor** | `.cursor/skills/*/SKILL.md` | Markdown | None | +| **Copilot** | Follows agentskills.io standard | `SKILL.md` + optional metadata | Via agent integration | +| **Claude** | `.claude/skills/*/` | Directory with files | None | +| **Codex** | `.agents/skills/*/SKILL.md` | Markdown + optional `agents/openai.yaml` | `interface`, `policy`, `dependencies` | + +--- + +## Conversion Matrix + +### Direct Conversions (lossless or near-lossless) + +| Source | Target | Artifact | Conversion | +|--------|--------|----------|------------| +| Any | Any | **Skill** (SKILL.md) | Direct copy — universal format across all platforms. Only deployment path changes. | +| Cursor `.cursorrules` | Claude `CLAUDE.md` | instruction | Direct content copy | +| Cursor `.cursorrules` | Codex `AGENTS.md` | instruction | Direct content copy | +| Cursor `.cursorrules` | Copilot `copilot-instructions.md` | instruction | Direct content copy | +| Copilot `copilot-instructions.md` | Claude `CLAUDE.md` | instruction | Direct content copy | +| Copilot `copilot-instructions.md` | Codex `AGENTS.md` | instruction | Direct content copy | +| Copilot `copilot-instructions.md` | Cursor `.cursorrules` | instruction | Direct content copy | +| Claude `CLAUDE.md` | Codex `AGENTS.md` | instruction | Direct content copy | +| Cursor prompts | Cursor commands | prompt | Direct copy (same format) | +| Cursor prompts | Claude prompts | prompt | Direct copy | +| Cursor commands | Copilot prompts | prompt | Add `.prompt.md` extension, can add frontmatter | + +### Lossy Conversions (data loss — user notified) + +| Source | Target | What's Lost | Workaround | +|--------|--------|-------------|------------| +| **Cursor `.mdc` with `globs`** | Claude / Codex | `globs` field — no conditional instruction support | Warning: "Instruction will apply globally. Original globs were: `{globs}`. Add manual file-path references in the instruction text." | +| **Cursor `.mdc` with `globs`** | Copilot `.instructions.md` | Partial — `globs` maps to `applyTo` | Auto-convert: `globs` → `applyTo` frontmatter (best-effort, glob syntax may differ) | +| **Copilot `.instructions.md` `applyTo`** | Cursor `.mdc` | Partial — `applyTo` maps to `globs` | Auto-convert: `applyTo` → `globs` frontmatter | +| **Copilot `.instructions.md` `applyTo`** | Claude / Codex | `applyTo` field lost | Warning: "Conditional instruction converted to always-on. Original applyTo: `{applyTo}`." | +| **Cursor `.mdc` `alwaysApply`** | All others | `alwaysApply` field | Warning: "alwaysApply metadata not supported on target platform." | +| **Copilot agent `tools` list** | Cursor / Claude / Codex | Tool bindings are platform-specific | Warning: "Tools `{tools}` are Copilot-specific and were removed. Configure tools manually on target platform." | +| **Copilot agent `handoffs`** | All others | Handoff workflows | Warning: "Handoffs are Copilot-specific and were removed: `{handoffs}`." | +| **Copilot agent `model`** | Cursor / Claude / Codex | Model identifiers differ per platform | Warning: "Model `{model}` is Copilot-specific. Set model manually on target platform." | +| **Cursor subagent `readonly`** | Copilot / Codex | `readonly` flag | Warning: "readonly=true not supported on target. Enforce via instruction text." | +| **Cursor subagent `is_background`** | Copilot / Codex | `is_background` flag | Warning: "is_background not supported on target." | +| **Cursor subagent `model: fast`** | Copilot | `fast` model alias | Warning: "Model alias `fast` is Cursor-specific. Map manually." | +| **Copilot agent `user-invokable`** | All others | Visibility control | Warning: "user-invokable flag removed." | +| **Copilot agent `target`** | All others | Environment targeting (`vscode`/`github-copilot`) | Warning: "target field removed — not applicable on target platform." | +| **Copilot prompt `agent` field** | Cursor / Claude | Agent binding | Warning: "Prompt was bound to agent `{agent}`. Bind manually on target platform." | +| **Copilot prompt `tools` field** | Cursor / Claude | Tool availability | Warning: "Prompt tools `{tools}` removed." | + +### Incompatible Conversions (cannot convert — user notified) + +| Source | Target | Reason | User Notification | +|--------|--------|--------|-------------------| +| **Codex Starlark rules** (`.codex/rules/*.rules`) | Any | Completely different concept — execution policy rules in Starlark, not AI instructions | Error: "Codex Starlark rules define command execution policies, not AI instructions. These cannot be converted. Create equivalent instructions manually." | +| **Any agent** | Codex | Codex has no discrete agent files — uses AGENTS.md sections | Warning: "Codex does not support discrete agent files. Agent content will be appended as a section in AGENTS.md with a heading." | +| **Any prompt** | Codex | Codex has no prompt file concept | Warning: "Codex does not support prompt files. Prompt content will be appended to AGENTS.md as a reference section." | +| **Copilot agent `mcp-servers`** | All others | MCP config is platform-specific | Warning: "MCP server configuration `{servers}` must be configured separately. See target platform docs." | + +--- + +## Conversion Logic Detail + +### Instruction Conversion + +**Cursor `.mdc` → Copilot `.instructions.md`:** +``` +INPUT: .cursor/rules/python-style.mdc +--- +description: "Python coding standards" +alwaysApply: false +globs: ["**/*.py"] +--- +# Python Standards +Use type hints... + +OUTPUT: .github/instructions/python-style.instructions.md +--- +name: "python-style" +description: "Python coding standards" +applyTo: "**/*.py" +--- +# Python Standards +Use type hints... + +WARNINGS: "alwaysApply field dropped (not supported in Copilot instructions.md)" +``` + +**Cursor `.mdc` → Claude `CLAUDE.md`:** +``` +INPUT: .cursor/rules/python-style.mdc (with globs: ["**/*.py"]) + +OUTPUT: Appended to CLAUDE.md with section markers: + +## Python Standards (applies to: **/*.py) +Use type hints... + + +WARNINGS: "Glob-scoped instruction converted to always-on. Original globs: **/*.py" +``` + +**Copilot `.instructions.md` → Cursor `.mdc`:** +``` +INPUT: .github/instructions/react.instructions.md +--- +applyTo: "**/*.tsx" +--- +Use functional components... + +OUTPUT: .cursor/rules/react.mdc +--- +description: "Converted from Copilot instructions" +alwaysApply: false +globs: ["**/*.tsx"] +--- +Use functional components... +``` + +### Agent Conversion + +**Copilot `.agent.md` → Cursor subagent:** +``` +INPUT: .github/agents/reviewer.agent.md +--- +description: "Code review specialist" +tools: ["github/*", "terminal"] +model: "gpt-4o" +--- +Review code for bugs... + +OUTPUT: .cursor/agents/reviewer.md +--- +name: reviewer +description: "Code review specialist" +model: inherit +--- +Review code for bugs... + +WARNINGS: +- "tools ['github/*', 'terminal'] removed — configure tools separately in Cursor" +- "model 'gpt-4o' is Copilot-specific, set to 'inherit'" +``` + +**Cursor subagent → Claude subagent:** +``` +INPUT: .cursor/agents/helper.md +--- +name: helper +description: "Helper subagent" +model: fast +readonly: true +is_background: true +--- +Help with tasks... + +OUTPUT: .claude/agents/helper.md +--- +name: helper +description: "Helper subagent" +--- +Help with tasks... + +WARNINGS: +- "model 'fast' is Cursor-specific, removed" +- "readonly=true not supported in Claude, removed" +- "is_background=true not supported in Claude, removed" +``` + +### Prompt Conversion + +**Copilot `.prompt.md` → Cursor command:** +``` +INPUT: .github/prompts/review.prompt.md +--- +description: "Review current changes" +agent: "agent" +model: "gpt-4o" +tools: ["terminal", "github/*"] +--- +Review all staged changes... + +OUTPUT: .cursor/commands/review.md +Review all staged changes... + +WARNINGS: +- "agent binding 'agent' removed — Cursor commands don't bind to agents" +- "model 'gpt-4o' removed — Cursor commands don't specify models" +- "tools ['terminal', 'github/*'] removed — Cursor commands don't specify tools" +``` + +--- + +## Output Format + +### Console Output (default) + +``` +$ aam convert -s cursor -t copilot + +Converting Cursor → Copilot... + +INSTRUCTIONS: + ✓ .cursor/rules/python-style.mdc → .github/instructions/python-style.instructions.md + ⚠ alwaysApply field dropped + ✓ .cursor/rules/general.mdc → .github/copilot-instructions.md (appended) + ⚠ globs field dropped — instruction now applies globally + ✓ .cursorrules → .github/copilot-instructions.md (appended) + +AGENTS: + ✓ .cursor/agents/reviewer.md → .github/agents/reviewer.agent.md + ⚠ model 'fast' → removed (Copilot uses different model identifiers) + ⚠ readonly=true → removed (not supported) + ⚠ is_background=true → removed (not supported) + ✓ .cursor/rules/agent-helper.mdc → .github/agents/helper.agent.md + +PROMPTS: + ✓ .cursor/commands/review.md → .github/prompts/review.prompt.md + ✓ .cursor/prompts/template.md → .github/prompts/template.prompt.md + +SKILLS: + ✓ .cursor/skills/code-review/ → .github/skills/code-review/ (direct copy) + +SUMMARY: 7 converted, 0 failed, 5 warnings + +Warnings indicate metadata that could not be converted. +Run with --verbose for detailed workaround instructions. +``` + +### Dry-run Output + +Same format but prefixed with `[DRY RUN]` and no files written. + +### Verbose Output + +Includes full workaround text for each warning: +``` + ⚠ model 'fast' → removed + Workaround: Copilot uses model identifiers like "gpt-4o" or "claude-sonnet-4-5-20250929". + Set the model field manually in the converted .agent.md file. + See: https://code.visualstudio.com/docs/copilot/customization/custom-agents +``` + +--- + +## Conflict Handling + +| Scenario | Behavior | +|----------|----------| +| Target file already exists | Skip with warning: "Target exists, use --force to overwrite" | +| Target file exists + `--force` | Overwrite with backup: creates `.bak` file | +| Multiple source instructions → single target file (e.g., → CLAUDE.md) | Append with `` section markers | +| Source and target are same platform | Error: "Source and target platform cannot be the same" | + +--- + +## Implementation Notes + +### Reusable components from existing codebase + +| Component | File | Reuse | +|-----------|------|-------| +| `scan_project()` | `detection/scanner.py` | Detect source artifacts | +| `DetectedArtifact` model | `detection/scanner.py` | Artifact data structure | +| Platform adapters | `adapters/*.py` | Deployment logic for target format | +| `_upsert_marker_section()` | `adapters/copilot.py` | Marker-based section merging | +| `ArtifactRef` model | `core/manifest.py` | Artifact reference with metadata | + +### New components needed + +| Component | Description | +|-----------|-------------| +| `commands/convert.py` | Click command definition | +| `services/convert_service.py` | Core conversion logic | +| `converters/frontmatter.py` | YAML frontmatter parse/generate utilities | +| `converters/mappings.py` | Platform-to-platform field mapping tables | + +--- + +## Files to Create/Modify + +| File | Action | +|------|--------| +| `apps/aam-cli/src/aam_cli/commands/convert.py` | New — Click command | +| `apps/aam-cli/src/aam_cli/services/convert_service.py` | New — conversion logic | +| `apps/aam-cli/src/aam_cli/converters/__init__.py` | New — converter package | +| `apps/aam-cli/src/aam_cli/converters/frontmatter.py` | New — YAML frontmatter parsing | +| `apps/aam-cli/src/aam_cli/converters/mappings.py` | New — field mapping tables | +| `apps/aam-cli/src/aam_cli/commands/__init__.py` | Modify — register convert command | +| `apps/aam-cli/tests/unit/test_convert.py` | New — unit tests | +| `docs/specs/SPEC-convert-command.md` | New — save this specification to project | + +--- + +## Action on Approval + +1. Save this specification to `docs/specs/SPEC-convert-command.md` +2. No code implementation in this round — spec only diff --git a/docs/user_docs/docs/cli/convert.md b/docs/user_docs/docs/cli/convert.md new file mode 100644 index 0000000..814c680 --- /dev/null +++ b/docs/user_docs/docs/cli/convert.md @@ -0,0 +1,160 @@ +# aam convert + +**Utilities** + +## Synopsis + +```bash +aam convert --source-platform --target-platform [OPTIONS] +``` + +## Description + +Convert AI agent configurations between platforms. Reads artifacts from one +platform's format (instructions, agents, prompts, skills) and writes them in +another platform's format, with clear warnings about metadata that cannot be +directly converted. + +Supported platforms: **cursor**, **copilot**, **claude**, **codex**. + +This is useful when: + +- Migrating between AI coding agents (Cursor, VS Code Copilot, Claude Code, Codex) +- Using multiple platforms simultaneously and wanting consistent configuration +- Onboarding a team that uses different editors + +## Options + +| Option | Short | Required | Description | +|--------|-------|----------|-------------| +| `--source-platform` | `-s` | Yes | Source platform: `cursor`, `copilot`, `claude`, `codex` | +| `--target-platform` | `-t` | Yes | Target platform: `cursor`, `copilot`, `claude`, `codex` | +| `--type` | | No | Filter by artifact type: `instruction`, `agent`, `prompt`, `skill` | +| `--dry-run` | | No | Show what would be converted without writing files | +| `--force` | | No | Overwrite existing target files (creates `.bak` backup) | +| `--verbose` | | No | Show detailed workaround instructions for warnings | + +## Examples + +### Example 1: Convert all Cursor configs to Copilot + +```bash +aam convert -s cursor -t copilot +``` + +**Output:** +``` +Converting Cursor → Copilot... + +INSTRUCTIONS: + ✓ .cursor/rules/python-style.mdc → .github/instructions/python-style.instructions.md + ⚠ alwaysApply field dropped + ✓ .cursor/rules/general.mdc → .github/copilot-instructions.md (appended) + +AGENTS: + ✓ .cursor/agents/reviewer.md → .github/agents/reviewer.agent.md + ⚠ model 'fast' is cursor-specific, removed + ⚠ readonly=true not supported on target. Enforce via instruction text. + +SKILLS: + ✓ .cursor/skills/code-review/ → .github/skills/code-review/ (direct copy) + +SUMMARY: 4 converted, 0 failed, 3 warnings +``` + +### Example 2: Dry-run instructions only + +```bash +aam convert -s copilot -t claude --type instruction --dry-run +``` + +**Output:** +``` +[DRY RUN] Converting Copilot → Claude... + +INSTRUCTIONS: + ✓ .github/copilot-instructions.md → CLAUDE.md (appended) + ✓ .github/instructions/react.instructions.md → CLAUDE.md (appended) + ⚠ Conditional instruction converted to always-on. Original applyTo: **/*.tsx + +SUMMARY: 2 converted, 0 failed, 1 warnings +``` + +### Example 3: Force overwrite with backup + +```bash +aam convert -s codex -t cursor --force +``` + +When `--force` is used, existing target files are backed up to `.bak` before +being overwritten. + +### Example 4: Verbose output with workarounds + +```bash +aam convert -s cursor -t copilot --verbose +``` + +Verbose mode includes detailed workaround text for each warning: + +``` + ⚠ model 'fast' is cursor-specific, removed + Model identifiers differ between platforms. Set the model + manually on the target platform. +``` + +## What gets converted + +### Instructions + +| Source → Target | Behavior | +|-----------------|----------| +| Cursor `.mdc` → Copilot | `globs` mapped to `applyTo`; `alwaysApply` dropped | +| Cursor `.mdc` → Claude/Codex | Appended with markers; `globs` lost (warning) | +| Cursor `.cursorrules` → Any | Direct content copy | +| Copilot `.instructions.md` → Cursor | `applyTo` mapped to `globs` | +| Copilot → Claude/Codex | Direct content copy; `applyTo` lost (warning) | +| Claude `CLAUDE.md` → Codex | Direct content copy | + +### Agents + +| Source → Target | Behavior | +|-----------------|----------| +| Any → Codex | Appended as section in `AGENTS.md` (no discrete files) | +| Copilot `.agent.md` → Cursor/Claude | `tools`, `handoffs`, `model` dropped (warning) | +| Cursor subagent → Claude | `readonly`, `is_background`, `model` dropped (warning) | + +### Prompts + +| Source → Target | Behavior | +|-----------------|----------| +| Any → Codex | Appended to `AGENTS.md` (no prompt files in Codex) | +| Copilot `.prompt.md` → Cursor/Claude | `agent`, `model`, `tools` dropped (warning) | +| Cursor → Copilot | Plain markdown gets `.prompt.md` extension | + +### Skills + +Skills use a universal `SKILL.md` format and are **directly copied** between +all platforms. Only the deployment path changes. + +## Conflict handling + +| Scenario | Behavior | +|----------|----------| +| Target file already exists | Skip with warning: "Target exists, use --force to overwrite" | +| Target file exists + `--force` | Overwrite; original saved as `.bak` file | +| Multiple sources → single target (e.g. → `CLAUDE.md`) | Append with `` section markers | +| Source and target are same platform | Error: "Source and target platform cannot be the same" | + +## Exit codes + +| Code | Meaning | +|------|---------| +| 0 | All conversions succeeded (warnings are allowed) | +| 1 | One or more conversions failed | + +## See also + +- [Platform Support Overview](../platforms/index.md) -- Platform comparison and configuration +- [Migration Guide](../troubleshooting/migration.md) -- Migrating between platforms +- [aam doctor](doctor.md) -- Diagnose environment issues diff --git a/docs/user_docs/docs/cli/index.md b/docs/user_docs/docs/cli/index.md index 8736044..552321d 100644 --- a/docs/user_docs/docs/cli/index.md +++ b/docs/user_docs/docs/cli/index.md @@ -69,6 +69,13 @@ Complete command reference for the AAM (Agent Artifact Manager) command-line int | [`aam config get`](config-get.md) | Get a configuration value | | [`aam config list`](config-list.md) | List all configuration values | +### Utilities + +| Command | Description | +|---------|-------------| +| [`aam doctor`](doctor.md) | Run environment diagnostics | +| [`aam convert`](convert.md) | Convert configs between platforms (Cursor, Copilot, Claude, Codex) | + ## Global Options All commands support these global options: diff --git a/docs/user_docs/docs/cli/list.md b/docs/user_docs/docs/cli/list.md index 2ca96cc..a979f25 100644 --- a/docs/user_docs/docs/cli/list.md +++ b/docs/user_docs/docs/cli/list.md @@ -99,7 +99,7 @@ Installed packages: ┃ Name ┃ Version ┃ Source ┃ Artifacts ┃ ┡━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩ │ docs-writer │ 0.0.0 │ google-gemini/gemini-skills │ 1 (1 skill) │ -│ code-reviewer │ 0.0.0 │ cursor/community-skills │ 1 (1 skill) │ +│ code-reviewer │ 0.0.0 │ anthropics/skills │ 1 (1 skill) │ └───────────────┴─────────┴────────────────────────────┴─────────────────────┘ ``` diff --git a/docs/user_docs/docs/cli/source-enable-defaults.md b/docs/user_docs/docs/cli/source-enable-defaults.md index 8f4df63..f4320fc 100644 --- a/docs/user_docs/docs/cli/source-enable-defaults.md +++ b/docs/user_docs/docs/cli/source-enable-defaults.md @@ -36,8 +36,8 @@ AAM ships with 4 curated community sources: |---|------|------------|------| | 1 | `github/awesome-copilot` | github.com/github/awesome-copilot | `skills` | | 2 | `openai/skills:.curated` | github.com/openai/skills | `skills/.curated` | -| 3 | `cursor/community-skills` | github.com/cursor/community-skills | `skills` | -| 4 | `anthropic/claude-prompts` | github.com/anthropic/claude-prompts | `prompts` | +| 3 | `anthropics/skills` | github.com/anthropics/skills | `skills` | +| 4 | `microsoft/skills` | github.com/microsoft/skills | `.github/skills` | ## Examples @@ -51,8 +51,8 @@ aam source enable-defaults ``` ✓ github/awesome-copilot — added ✓ openai/skills:.curated — added - ✓ cursor/community-skills — added - ✓ anthropic/claude-prompts — added + ✓ anthropics/skills — added + ✓ microsoft/skills — added 4 source(s) enabled, 0 already configured (out of 4 defaults) @@ -64,8 +64,8 @@ aam source enable-defaults ┡━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ │ 1 │ github/awesome-copilot │ https://github.com/github/awesome-… │ skills │ │ 2 │ openai/skills:.curated │ https://github.com/openai/skills.… │ skills/.curated │ - │ 3 │ cursor/community-skills │ https://github.com/cursor/communit… │ skills │ - │ 4 │ anthropic/claude-prompts │ https://github.com/anthropic/claud… │ prompts │ + │ 3 │ anthropics/skills │ https://github.com/anthropics/skil… │ skills │ + │ 4 │ microsoft/skills │ https://github.com/microsoft/skills │ .github/skills │ └───┴───────────────────────────┴──────────────────────────────────────┴─────────────────┘ ``` @@ -83,8 +83,8 @@ aam source enable-defaults ``` ✓ openai/skills:.curated — re-enabled – github/awesome-copilot — already configured - – cursor/community-skills — already configured - – anthropic/claude-prompts — already configured + – anthropics/skills — already configured + – microsoft/skills — already configured 1 source(s) enabled, 3 already configured (out of 4 defaults) @@ -101,7 +101,7 @@ aam source enable-defaults --json ```json { "registered": ["github/awesome-copilot", "openai/skills:.curated", - "cursor/community-skills", "anthropic/claude-prompts"], + "anthropics/skills", "microsoft/skills"], "re_enabled": [], "skipped": [], "total": 4 diff --git a/docs/user_docs/docs/concepts/git-sources.md b/docs/user_docs/docs/concepts/git-sources.md index d957c16..ae1be27 100644 --- a/docs/user_docs/docs/concepts/git-sources.md +++ b/docs/user_docs/docs/concepts/git-sources.md @@ -90,8 +90,8 @@ to popular skills, agents, and prompts: - `github/awesome-copilot` (from `github.com/github/awesome-copilot`) - `openai/skills:.curated` (curated subset of `github.com/openai/skills`) -- `cursor/community-skills` (from `github.com/cursor/community-skills`) -- `anthropic/claude-prompts` (from `github.com/anthropic/claude-prompts`) +- `anthropics/skills` (from `github.com/anthropics/skills`) +- `microsoft/skills` (from `github.com/microsoft/skills`) These are automatically registered when you run `aam init`. You can also enable them at any time with `aam source enable-defaults`. diff --git a/docs/user_docs/docs/index.md b/docs/user_docs/docs/index.md index a3da974..0c332ea 100644 --- a/docs/user_docs/docs/index.md +++ b/docs/user_docs/docs/index.md @@ -16,6 +16,7 @@ The package manager for AI agent artifacts. Install, share, and deploy skills, a [Get Started](getting-started/index.md){ .md-button .md-button--primary } [CLI Reference](cli/index.md){ .md-button } +[MCP Interface](mcp/index.md){ .md-button } @@ -102,22 +103,186 @@ AAM manages four types of AI agent artifacts: --- -## Where to Go Next +## Tools & Prerequisites + +AAM is designed to be lightweight. Install only what you need based on your planned tasks: + +
+ +
+ +### Always Required + +| Tool | Version | Purpose | +|------|---------|---------| +| **Python** | 3.11+ | Runtime for AAM | +| **pip** | 22.0+ | Install the `aam` package | + +```bash +pip install aam +``` + +
+ +
+ +### For Git Source Workflows + +| Tool | Version | Purpose | +|------|---------|---------| +| **Git** | 2.25+ | Clone and fetch community skill sources | + +Required when using `aam source add`, `aam source update`, or `aam source enable-defaults` to manage git-based package sources. + +
+ +
+ +### For MCP / IDE Integration + +| Tool | Version | Purpose | +|------|---------|---------| +| **Cursor**, **Claude Desktop**, **VS Code**, or any MCP client | Latest | Connect to AAM's MCP server | + +AAM ships with a built-in MCP server. No extra install needed --- just configure your IDE to spawn `aam mcp serve`. + +
+ +
+ +### For Package Authoring & Publishing + +| Tool | Version | Purpose | +|------|---------|---------| +| **Text editor** | Any | Edit `aam.yaml` manifests and artifact files | +| **Git** | 2.25+ | Version control your packages | + +Use `aam pkg create` or `aam pkg init` to scaffold packages, then `aam pkg publish` to share them. + +
+ +
+ +!!! tip "Check your environment" + Run `aam doctor` at any time to verify your setup. It checks Python version, configuration files, registry accessibility, and package integrity. + +--- + +## Get Started
-### Getting Started +### :material-download: Install AAM + +Install with pip, set your default AI platform, and configure a local registry. Everything you need in five minutes. + +[Installation guide](getting-started/installation.md){ .md-button .md-button--primary } + +
+ +
+ +### :material-rocket-launch: Quick Start -Install AAM, configure your platform, and install your first package in under five minutes. +Create a registry, build a package, publish it, and install it --- all in a single walkthrough. -[Start here](getting-started/index.md){ .md-button } +[Quick start](getting-started/quickstart.md){ .md-button .md-button--primary }
+### :material-package-variant: Your First Package + +Build a complete package with skills, agents, prompts, and instructions from scratch. + +[First package](getting-started/first-package.md){ .md-button .md-button--primary } + +
+ +
+ +--- + +## CLI Reference + +AAM provides a comprehensive CLI organized into command groups. Every command is also available as an MCP tool for IDE agents. + +| Command Group | Key Commands | What It Does | +|---------------|-------------|--------------| +| **Getting Started** | [`aam init`](cli/init.md) | Set up AAM (platform, default sources) | +| **Package Management** | [`aam install`](cli/install.md), [`aam search`](cli/search.md), [`aam list`](cli/list.md) | Install, search, list, upgrade, and remove packages | +| **Package Integrity** | [`aam verify`](cli/verify.md), [`aam diff`](cli/diff.md) | Verify checksums and show diffs for modified files | +| **Package Authoring** | [`aam pkg create`](cli/create-package.md), [`aam pkg publish`](cli/publish.md) | Create, validate, pack, and publish packages | +| **Source Management** | [`aam source add`](cli/source-add.md), [`aam source scan`](cli/source-scan.md) | Manage git-based package sources | +| **Registry Management** | [`aam registry init`](cli/registry-init.md), [`aam registry add`](cli/registry-add.md) | Create and configure local or remote registries | +| **Configuration** | [`aam config set`](cli/config-set.md), [`aam config list`](cli/config-list.md) | Get and set configuration values | +| **Diagnostics** | [`aam doctor`](cli/doctor.md) | Run health checks on your AAM setup | + +[Full CLI reference](cli/index.md){ .md-button } + +--- + +## MCP Interface + +AAM ships with a built-in **MCP (Model Context Protocol) server** that lets AI agents in your IDE manage packages directly --- no terminal required. + +
+ +
+ +### Read-Only Tools (17) + +Search packages, list sources, check configuration, verify integrity, and run diagnostics. Always safe, always available. + +
+ +
+ +### Write Tools (12) + +Install, uninstall, upgrade, and publish packages. Create packages and manage sources. Requires explicit `--allow-write` opt-in. + +
+ +
+ +### Resources (9) + +Passive data endpoints: installed packages, configuration, registries, source lists, and manifest data. Available to any connected agent. + +
+ +
+ +### IDE Integration + +Works with **Cursor**, **Claude Desktop**, **VS Code**, **Windsurf**, and any MCP-compatible client. One line of config to connect. + +
+ +
+ +```bash +# Start MCP server (read-only, safe for exploration) +aam mcp serve + +# Start with full read/write access +aam mcp serve --allow-write +``` + +[MCP Interface documentation](mcp/index.md){ .md-button } + +--- + +## Where to Go Next + +
+ +
+ ### Tutorials Step-by-step guides for common tasks: packaging existing skills, building packages, and deploying to multiple platforms. @@ -128,11 +293,21 @@ Step-by-step guides for common tasks: packaging existing skills, building packag
-### CLI Reference +### Platform Guides + +Deploy artifacts to Cursor, Claude Desktop, GitHub Copilot, and OpenAI Codex with platform-specific configuration. + +[Platform guides](platforms/index.md){ .md-button } + +
+ +
+ +### Concepts -Complete reference for every AAM command, flag, and option. +Understand packages, artifacts, git sources, registries, dependency resolution, and platform adapters. -[View commands](cli/index.md){ .md-button } +[Learn concepts](concepts/index.md){ .md-button }
diff --git a/docs/user_docs/docs/mcp/index.md b/docs/user_docs/docs/mcp/index.md new file mode 100644 index 0000000..e1cec0a --- /dev/null +++ b/docs/user_docs/docs/mcp/index.md @@ -0,0 +1,256 @@ +# MCP Interface + +AAM exposes its full functionality as an **MCP (Model Context Protocol) server**, allowing AI agents and IDE integrations to manage packages, sources, and configuration programmatically. Any MCP-compatible client --- Cursor, VS Code, Claude Desktop, Windsurf, or custom toolchains --- can connect to AAM and operate it directly. + +--- + +## What Is the MCP Interface? + +The Model Context Protocol (MCP) is an open standard that lets AI agents call tools and read resources from external services over a structured transport (stdio or HTTP/SSE). AAM implements an MCP server using [FastMCP](https://github.com/jlowin/fastmcp), exposing CLI functionality as **tools** (callable actions) and **resources** (passive data endpoints). + +```mermaid +flowchart LR + IDE["IDE / AI Agent
(Cursor, Claude Desktop, etc.)"] -->|MCP protocol| AAM["AAM MCP Server
(aam mcp serve)"] + AAM -->|reads/writes| Config["~/.aam/config.yaml"] + AAM -->|manages| Packages["~/.aam/packages/"] + AAM -->|clones/fetches| Sources["~/.aam/sources-cache/"] + + style IDE fill:#e3f2fd + style AAM fill:#f3e5f5 + style Config fill:#e8f5e9 + style Packages fill:#e8f5e9 + style Sources fill:#e8f5e9 +``` + +Instead of typing `aam install @author/my-skill` in a terminal, your AI agent calls the `aam_install` tool through MCP and gets structured results back. + +--- + +## Starting the MCP Server + +### Basic (Read-Only) + +```bash +aam mcp serve +``` + +By default the server starts in **read-only mode** on **stdio** transport. This is the safest option --- agents can search, list, and inspect packages but cannot modify anything. + +### With Write Access + +```bash +aam mcp serve --allow-write +``` + +Enables mutating tools (install, uninstall, publish, etc.). Use this when you trust the connected agent to make changes. + +### Full Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--transport` | `stdio` | Transport protocol: `stdio` or `http` | +| `--port` | `8000` | HTTP port (only for `http` transport) | +| `--allow-write` | `false` | Enable write/mutating tools | +| `--log-file` | stderr | Path to log file | +| `--log-level` | `INFO` | Logging level: `DEBUG`, `INFO`, `WARNING`, `ERROR` | + +### Example: HTTP Transport + +```bash +aam mcp serve --transport http --port 9000 --allow-write +``` + +This starts an SSE server on port 9000 with full read/write access, suitable for remote or multi-client setups. + +--- + +## IDE Integration + +### Cursor + +Add AAM as an MCP server in your Cursor settings (`.cursor/mcp.json`): + +```json +{ + "mcpServers": { + "aam": { + "command": "aam", + "args": ["mcp", "serve", "--allow-write"] + } + } +} +``` + +Once configured, Cursor's AI agent can call AAM tools directly --- searching for packages, installing skills, managing sources --- all without leaving the editor. + +### Claude Desktop + +Add to your Claude Desktop configuration (`claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "aam": { + "command": "aam", + "args": ["mcp", "serve", "--allow-write"] + } + } +} +``` + +### VS Code / Other MCP Clients + +Any client that supports the MCP stdio transport can connect by spawning `aam mcp serve` as a subprocess. + +--- + +## Available Tools + +AAM exposes **29 tools** split into two categories with a safety-first design: read tools are always available, write tools require explicit opt-in. + +### Read Tools (17 tools, always available) + +These tools are safe to call at any time --- they never modify state. + +#### Package Discovery + +| Tool | Description | +|------|-------------| +| `aam_search` | Search registries and sources for packages (relevance-ranked) | +| `aam_list` | List installed packages | +| `aam_available` | List all available packages from sources | +| `aam_info` | Show detailed package metadata | +| `aam_recommend_skills` | Get AI-powered skill recommendations for the current project | + +#### Package Integrity + +| Tool | Description | +|------|-------------| +| `aam_validate` | Validate a package manifest | +| `aam_verify` | Verify installed file checksums | +| `aam_diff` | Show unified diffs for modified installed files | +| `aam_outdated` | Check for outdated packages | + +#### Source Management + +| Tool | Description | +|------|-------------| +| `aam_source_list` | List configured git sources | +| `aam_source_scan` | Scan a source for artifacts | +| `aam_source_candidates` | List unpackaged artifact candidates in a source | +| `aam_source_diff` | Show what changed in a source since last update | + +#### Configuration & Diagnostics + +| Tool | Description | +|------|-------------| +| `aam_config_get` | Read a configuration value | +| `aam_registry_list` | List configured registries | +| `aam_doctor` | Run health checks on the AAM installation | +| `aam_init_info` | Get client initialization status | + +### Write Tools (12 tools, require `--allow-write`) + +These tools modify state and are excluded by default for safety. + +#### Package Management + +| Tool | Description | +|------|-------------| +| `aam_install` | Install a package and its dependencies | +| `aam_uninstall` | Remove an installed package | +| `aam_upgrade` | Upgrade outdated packages | +| `aam_publish` | Publish a package to a registry | + +#### Package Authoring + +| Tool | Description | +|------|-------------| +| `aam_create_package` | Create a new package from an existing project | +| `aam_init_package` | Scaffold a new package from scratch | + +#### Source Management + +| Tool | Description | +|------|-------------| +| `aam_source_add` | Add a remote git repository as a source | +| `aam_source_remove` | Remove a configured source | +| `aam_source_update` | Fetch upstream changes for sources | + +#### Configuration + +| Tool | Description | +|------|-------------| +| `aam_config_set` | Set a configuration value | +| `aam_registry_add` | Add a registry endpoint | +| `aam_init` | Run first-time client initialization | + +--- + +## Available Resources + +Resources provide passive, read-only data that agents can fetch at any time without side effects. + +| Resource URI | Description | +|-------------|-------------| +| `aam://config` | Full merged configuration (global + project) | +| `aam://packages/installed` | List of all installed packages | +| `aam://packages/{name}` | Detailed info for a specific package | +| `aam://registries` | List of configured registries | +| `aam://manifest` | Current directory's `aam.yaml` manifest | +| `aam://sources` | List of configured git sources | +| `aam://sources/{source_id}` | Detailed source info with discovered artifacts | +| `aam://sources/{source_id}/candidates` | Unpackaged candidate artifacts from a source | +| `aam://init_status` | Client initialization status | + +!!! tip "Scoped package names in resource URIs" + For scoped packages like `@author/my-skill`, replace the `/` with `--` in the URI: `aam://packages/author--my-skill`. + +--- + +## Safety Model + +AAM's MCP server follows a **read-only-by-default** principle: + +| Mode | Read Tools | Write Tools | Use Case | +|------|-----------|------------|----------| +| Default (`aam mcp serve`) | 17 tools | Excluded | Safe exploration, search, diagnostics | +| Write-enabled (`--allow-write`) | 17 tools | 12 tools | Full package management | + +This design ensures that agents cannot accidentally install, remove, or publish packages unless explicitly authorized. + +--- + +## Example Agent Workflows + +### Discover and Install a Skill + +An AI agent connected via MCP might follow this workflow: + +1. **Search** for relevant skills: call `aam_search` with a query +2. **Inspect** a candidate: call `aam_info` for metadata and dependencies +3. **Install** the skill: call `aam_install` with the package name +4. **Verify** the installation: call `aam_verify` to check file integrity + +### Audit Installed Packages + +1. **List** installed packages: call `aam_list` +2. **Check** for outdated versions: call `aam_outdated` +3. **Upgrade** stale packages: call `aam_upgrade` +4. **Run diagnostics**: call `aam_doctor` to confirm health + +### Explore Available Sources + +1. **List** configured sources: call `aam_source_list` +2. **Scan** a source: call `aam_source_scan` to discover artifacts +3. **Find candidates**: call `aam_source_candidates` for unpackaged artifacts +4. **Add a new source**: call `aam_source_add` with the git URL + +--- + +## Related Documentation + +- [Getting Started](../getting-started/installation.md) --- Install AAM and configure your environment +- [CLI Reference](../cli/index.md) --- The same commands exposed as MCP tools +- [Platform Guides](../platforms/index.md) --- Deploy artifacts to Cursor, Claude, Copilot, and Codex +- [Configuration](../configuration/global.md) --- Configure AAM behavior diff --git a/docs/user_docs/docs/platforms/index.md b/docs/user_docs/docs/platforms/index.md index c498a3a..6f516a7 100644 --- a/docs/user_docs/docs/platforms/index.md +++ b/docs/user_docs/docs/platforms/index.md @@ -302,6 +302,27 @@ aam platforms # Valid platforms: cursor, copilot, claude, codex ``` +## Converting Between Platforms + +Use `aam convert` to migrate existing platform configurations between any two platforms: + +```bash +# Convert Cursor configs to Copilot format +aam convert -s cursor -t copilot + +# Preview conversion without writing files +aam convert -s copilot -t claude --dry-run + +# Convert only instructions +aam convert -s cursor -t claude --type instruction +``` + +The convert command handles field mapping (e.g. Cursor `globs` ↔ Copilot `applyTo`), +strips unsupported metadata, and warns about lossy conversions. Skills use a +universal format and are directly copied. + +See [`aam convert`](../cli/convert.md) for the full reference. + ## Next Steps Explore each platform's detailed deployment guide: @@ -314,5 +335,6 @@ Explore each platform's detailed deployment guide: Or continue learning: - [Platform Adapters Concept](../concepts/platform-adapters.md) - Deep dive into adapter architecture +- [`aam convert` CLI Reference](../cli/convert.md) - Cross-platform conversion command - [Configuration: Project config](../configuration/project.md) - Platform settings in config - [Getting Started: Quick Start](../getting-started/quickstart.md) - Hands-on package installation diff --git a/docs/user_docs/docs/troubleshooting/migration.md b/docs/user_docs/docs/troubleshooting/migration.md index 484097f..df51055 100644 --- a/docs/user_docs/docs/troubleshooting/migration.md +++ b/docs/user_docs/docs/troubleshooting/migration.md @@ -241,20 +241,52 @@ aam registry add file:///home/user/local-registry --name local --default ## Migrating Between Platforms +### Using `aam convert` (Recommended) + +The `aam convert` command directly converts platform configuration files +between Cursor, Copilot, Claude, and Codex: + +```bash +# Preview the conversion first +aam convert -s cursor -t claude --dry-run + +# Run the conversion +aam convert -s cursor -t claude + +# Convert only specific artifact types +aam convert -s cursor -t copilot --type instruction + +# Force overwrite existing targets (creates .bak backups) +aam convert -s copilot -t cursor --force +``` + +This handles instructions, agents, prompts, and skills — mapping platform-specific +fields where possible and warning about metadata that cannot be converted. + +See [`aam convert`](../cli/convert.md) for the full reference. + ### Cursor to Claude -#### 1. Install Package +#### 1. Convert existing configs + +```bash +aam convert -s cursor -t claude +``` + +This converts: +- `.cursor/rules/*.mdc` → Appended to `CLAUDE.md` with section markers +- `.cursor/agents/*.md` → `.claude/agents/*.md` +- `.cursor/prompts/*.md` → `.claude/prompts/*.md` +- `.cursor/skills/*/` → `.claude/skills/*/` + +#### 2. Install AAM packages for the new platform ```bash aam config set active_platforms claude aam install @author/my-package ``` -AAM automatically converts: -- `.cursor/skills/` → `.claude/skills/` -- `.cursor/rules/` → `CLAUDE.md` - -#### 2. Verify Deployment +#### 3. Verify Deployment ```bash ls .claude/skills/ diff --git a/docs/user_docs/docs/tutorials/index.md b/docs/user_docs/docs/tutorials/index.md index b0018fd..1b978fa 100644 --- a/docs/user_docs/docs/tutorials/index.md +++ b/docs/user_docs/docs/tutorials/index.md @@ -14,7 +14,7 @@ All tutorials include: - **Clear explanations** of what's happening at each step !!! tip "New to AAM?" - Start with [Packaging Existing Skills](package-existing-skills.md) to learn the basics, then move on to [Building a Code Review Package](build-code-review-package.md) for a complete end-to-end example. + Start with [Installing Skills from Sources](install-from-sources.md) to get up and running quickly, then try [Skill Consolidation](skill-consolidation.md) to build a curated team package. For package authoring, begin with [Packaging Existing Skills](package-existing-skills.md). --- @@ -22,6 +22,17 @@ All tutorials include:
+- :material-download:{ .lg .middle } __Installing Skills from Sources__ {#install-from-sources} + + --- + + Set up AAM, connect to community skill repositories, discover artifacts, and install skills into your project. + + **Difficulty:** Beginner + **Time:** 10 minutes + + [:octicons-arrow-right-24: Start tutorial](install-from-sources.md) + - :material-package-variant:{ .lg .middle } __Packaging Existing Skills__ {#packaging-existing-skills} --- @@ -66,6 +77,17 @@ All tutorials include: [:octicons-arrow-right-24: Start tutorial](multi-platform-deployment.md) +- :material-layers-triple:{ .lg .middle } __Skill Consolidation__ {#skill-consolidation} + + --- + + Cherry-pick skills from multiple community sources and your own project, then bundle them into a single curated team package. + + **Difficulty:** Intermediate + **Time:** 20 minutes + + [:octicons-arrow-right-24: Start tutorial](skill-consolidation.md) + - :material-graph:{ .lg .middle } __Working with Dependencies__ --- @@ -97,6 +119,8 @@ Some tutorials have additional prerequisites, which are listed at the start of e Once you've completed these tutorials, you'll be ready to: +- Install and manage community skills from upstream sources +- Consolidate skills from multiple sources into curated team packages - Package your own skills and agents for distribution - Set up a team registry for sharing artifacts - Deploy packages across multiple AI platforms diff --git a/docs/user_docs/docs/tutorials/install-from-sources.md b/docs/user_docs/docs/tutorials/install-from-sources.md new file mode 100644 index 0000000..0b91082 --- /dev/null +++ b/docs/user_docs/docs/tutorials/install-from-sources.md @@ -0,0 +1,542 @@ +# Tutorial: Installing Skills from Sources + +**Difficulty:** Beginner +**Time:** 10 minutes + +## What You'll Learn + +In this tutorial, you'll walk through the complete workflow of setting up AAM, connecting to community skill sources, discovering artifacts, and installing them into your project. By the end, you'll have community skills deployed and working in your IDE. + +## Prerequisites + +- AAM installed (`aam --version` works) +- A project directory where you want to use skills +- Git installed (for source cloning) + +--- + +## The Scenario + +You've just started a new project and want to supercharge your AI coding assistant with community skills. Rather than writing everything from scratch, you'll: + +1. Initialize AAM and connect to community skill repositories +2. Browse and search for useful skills +3. Install skills directly into your project +4. Verify the installation and manage updates + +--- + +## Step 1: Initialize AAM + +Navigate to your project directory and run the setup: + +```bash +cd ~/my-project +aam init +``` + +AAM detects your IDE platform and walks you through setup: + +``` + Detected platform: cursor +Choose platform [cursor]: +Register community artifact sources? [Y/n] y + +✓ AAM initialized successfully. + Platform: cursor + Config: ~/.aam/config.yaml + Sources: 4 community source(s) added + +Next steps: + aam search — Find packages to install + aam install — Install a package + aam list --available — Browse source artifacts + aam pkg init — Create a new package +``` + +!!! info "What just happened?" + `aam init` did two things: + + 1. **Set your platform** — AAM knows to deploy skills to `.cursor/skills/`, agents to `.cursor/rules/`, etc. + 2. **Registered default sources** — Added 4 curated community repositories as git sources: + - `github/awesome-copilot` + - `openai/skills:.curated` + - `anthropics/skills` + - `microsoft/skills` + + If you missed adding default sources during init, run `aam source enable-defaults` at any time. + +For non-interactive setup (e.g., in CI or scripts): + +```bash +aam init --yes +``` + +This auto-detects the platform and registers default sources without prompting. + +--- + +## Step 2: Update Source Caches + +The sources are registered but not yet cloned. Fetch them: + +```bash +aam source update --all +``` + +``` +Updating all sources... + + github/awesome-copilot + ✓ Cloned https://github.com/github/awesome-copilot (main) + Found 12 artifacts (8 skills, 2 agents, 2 prompts) + + openai/skills:.curated + ✓ Cloned https://github.com/openai/skills (main) → skills/.curated + Found 6 artifacts (6 skills) + + anthropics/skills + ✓ Cloned https://github.com/anthropics/skills (main) + Found 4 artifacts (3 skills, 1 agent) + + microsoft/skills + ✓ Cloned https://github.com/microsoft/skills (main) → .github/skills + Found 5 artifacts (5 skills) + +✓ Updated 4 sources (27 artifacts total) +``` + +!!! tip "Where are the clones?" + Source repositories are cached at `~/.aam/cache/git/{host}/{owner}/{repo}/`. This cache is shared across all your projects — you only clone once. + +--- + +## Step 3: Browse Available Skills + +See everything available across all sources: + +```bash +aam list --available +``` + +``` +Source: github/awesome-copilot + Type Name Description + skill commit-message-writer Write conventional commit messages + skill code-reviewer Review code for best practices + skill test-generator Generate unit tests + skill documentation-writer Write project documentation + ... + +Source: openai/skills:.curated + Type Name Description + skill code-review Comprehensive code review + skill refactoring Suggest refactoring improvements + ... + +Source: anthropics/skills + Type Name Description + skill skill-creator Create new skills from descriptions + skill debugging-assistant Systematic debugging helper + ... +``` + +### Search for Specific Skills + +Use `aam search` to find skills by keyword: + +```bash +aam search review +``` + +``` +Search results for "review" (3 matches) + +Name Version Type Source Description +code-reviewer — skill github/awesome-copilot Review code for best practices +code-review — skill openai/skills:.curated Comprehensive code review +``` + +Filter by artifact type: + +```bash +aam search deploy --type skill +``` + +--- + +## Step 4: Install a Skill + +Install a skill by name: + +```bash +aam install code-reviewer +``` + +``` +Searching sources for 'code-reviewer'... + Found code-reviewer (skill) in source github/awesome-copilot + +Installing code-reviewer from source github/awesome-copilot... + ✓ Copied skill files + ✓ Generated manifest + ✓ Computed checksums + ✓ Deployed to .cursor/skills/code-reviewer/ + +✓ Installed code-reviewer from source github/awesome-copilot @ a1b2c3d +``` + +That's it — the skill is now installed and deployed to your IDE. + +### What Happened Under the Hood + +1. **Resolved** — AAM searched all configured sources and found `code-reviewer` in `github/awesome-copilot` +2. **Copied** — Skill files were copied from the source cache to `.aam/packages/code-reviewer/` +3. **Manifest** — An `aam.yaml` was generated with provenance metadata (source URL, commit SHA) +4. **Checksums** — Per-file SHA-256 checksums were computed for integrity verification +5. **Deployed** — The skill was deployed to `.cursor/skills/code-reviewer/` (based on your configured platform) +6. **Locked** — The lock file (`.aam/aam-lock.yaml`) was updated with the exact version and commit + +--- + +## Step 5: Install from a Specific Source + +When multiple sources have skills with similar names, use the **qualified name** to be explicit: + +```bash +# Install from a specific source using: source-name/artifact-name +aam install openai/skills:.curated/code-review +``` + +``` +Searching sources for 'openai/skills:.curated/code-review'... + Found code-review (skill) in source openai/skills:.curated + +✓ Installed code-review from source openai/skills:.curated @ d4e5f6a +``` + +!!! tip "Finding qualified names" + The qualified name format is `source-name/artifact-name`. You can discover these from: + + - `aam search ` — the **Source** column shows the source name + - `aam list --available` — the group header is the source name + - `aam info source-name/artifact-name` — shows details and the install command + +--- + +## Step 6: Verify the Installation + +Check what's installed: + +```bash +aam list +``` + +``` +Installed packages: + code-reviewer — 1 artifact (1 skill) source: github/awesome-copilot + code-review — 1 artifact (1 skill) source: openai/skills:.curated +``` + +Verify file integrity: + +```bash +aam verify +``` + +``` +Verifying installed packages... + + code-reviewer + ✓ All files match checksums (2 files) + + code-review + ✓ All files match checksums (3 files) + +✓ All packages verified +``` + +Check what was deployed to your IDE: + +```bash +ls .cursor/skills/ +``` + +``` +code-review/ +code-reviewer/ +``` + +--- + +## Step 7: Get Package Details + +View detailed information about an installed package: + +```bash +aam info code-reviewer +``` + +``` +code-reviewer + Type: skill + Source: github/awesome-copilot + Commit: a1b2c3d + Installed: .aam/packages/code-reviewer/ + Deployed: .cursor/skills/code-reviewer/ + + Provenance: + source_url: https://github.com/github/awesome-copilot + source_ref: main + source_commit: a1b2c3d4e5f6 + fetched_at: 2026-02-13T10:30:00Z +``` + +--- + +## Step 8: Check for Updates + +After some time, check if upstream sources have new content: + +```bash +# Fetch latest from all sources +aam source update --all +``` + +``` +Updating all sources... + + github/awesome-copilot + ✓ Updated (a1b2c3d → f7g8h9i) + 2 new artifacts, 1 modified + + openai/skills:.curated + ✓ Already up to date + + anthropics/skills + ✓ Updated (j1k2l3m → n4o5p6q) + 1 new artifact +``` + +Check which installed packages have upstream changes: + +```bash +aam outdated +``` + +``` +Outdated packages: + + Package Current Commit Latest Commit Source + code-reviewer a1b2c3d f7g8h9i github/awesome-copilot +``` + +Upgrade outdated packages: + +```bash +aam upgrade code-reviewer +``` + +``` +Upgrading code-reviewer... + Source: github/awesome-copilot + a1b2c3d → f7g8h9i + + ✓ Updated skill files + ✓ Recomputed checksums + ✓ Redeployed to .cursor/skills/code-reviewer/ + +✓ Upgraded code-reviewer +``` + +Or upgrade everything at once: + +```bash +aam upgrade +``` + +--- + +## Step 9: Add Your Own Sources + +Beyond the defaults, add any Git repository as a source: + +```bash +# GitHub shorthand +aam source add myorg/ai-skills + +# Full HTTPS URL +aam source add https://github.com/myorg/ai-skills + +# With a branch and subdirectory +aam source add myorg/monorepo@develop:skills/curated + +# SSH URL +aam source add git@github.com:myorg/private-skills.git +``` + +After adding, update and browse: + +```bash +aam source update myorg/ai-skills +aam list --available +``` + +### Manage Sources + +```bash +# List all configured sources +aam source list + +# Scan a specific source for artifacts +aam source scan anthropics/skills + +# Remove a source (optionally delete cached clone) +aam source remove myorg/old-skills --purge-cache +``` + +--- + +## Step 10: Uninstall a Package + +If you no longer need a skill: + +```bash +aam uninstall code-review +``` + +``` +Uninstalling code-review... + ✓ Removed from .aam/packages/code-review/ + ✓ Undeployed from .cursor/skills/code-review/ + ✓ Updated lock file + +✓ Uninstalled code-review +``` + +--- + +## Install Options Reference + +Here's a quick reference for `aam install` options: + +```bash +# Install latest from any source +aam install + +# Install from a specific source (qualified name) +aam install / + +# Install from a local directory +aam install ./path/to/package/ + +# Install from an .aam archive +aam install my-package-1.0.0.aam + +# Install without deploying to IDE +aam install --no-deploy + +# Force reinstall +aam install --force + +# Preview what would happen +aam install --dry-run + +# Install to a specific platform +aam install --platform claude + +# Install globally (available across all projects) +aam install -g +``` + +--- + +## Next Steps + +Now that you know how to discover and install skills, you can: + +- **Consolidate skills** — Bundle favorites into a team package with [Skill Consolidation](skill-consolidation.md) +- **Create your own** — Build a package from scratch in [Building a Code Review Package](build-code-review-package.md) +- **Package existing skills** — Wrap what you already have in [Packaging Existing Skills](package-existing-skills.md) +- **Deploy to multiple platforms** — Configure multi-platform deployment in [Multi-Platform Deployment](multi-platform-deployment.md) +- **Manage dependencies** — Learn about dependency resolution in [Working with Dependencies](working-with-dependencies.md) + +--- + +## Troubleshooting + +### Source clone fails + +**Problem:** `aam source update` fails with a git error + +**Solutions:** + +- Check your internet connection +- Verify the repository URL is correct: `aam source list` +- For private repos, ensure SSH keys or credentials are configured +- Try removing and re-adding: `aam source remove --purge-cache` then `aam source add ` + +### Artifact not found + +**Problem:** `aam install my-skill` says "not found" + +**Solutions:** + +- Run `aam source update --all` to refresh caches +- Search to verify the name: `aam search my-skill` +- Use `aam list --available` to see all available artifacts +- Check if the skill requires a qualified name: `aam search my-skill` and note the Source column + +### Deployment fails + +**Problem:** Skill is installed but not deployed to the IDE + +**Solutions:** + +- Check your platform: `aam config get default_platform` +- Try redeploying: `aam install --force` +- Verify the deploy path exists: `ls .cursor/skills/` (for Cursor) +- Restart your IDE to pick up new skills + +### Lock file conflicts + +**Problem:** Multiple team members get different versions + +**Solution:** Commit `.aam/aam-lock.yaml` to version control. The lock file records exact source commits, so everyone gets the same versions: + +```bash +git add .aam/aam-lock.yaml +git commit -m "Lock AAM package versions" +``` + +--- + +## Summary + +In this tutorial, you learned how to: + +- Initialize AAM and configure your platform with `aam init` +- Update source caches with `aam source update --all` +- Browse available artifacts with `aam list --available` and `aam search` +- Install skills from sources with `aam install` +- Use qualified names to install from a specific source +- Verify installations with `aam verify` +- Check for and apply updates with `aam outdated` and `aam upgrade` +- Add custom git sources with `aam source add` +- Uninstall packages with `aam uninstall` + +**Key Commands:** + +```bash +aam init # Set up AAM (platform + sources) +aam source update --all # Clone/refresh source caches +aam list --available # Browse all available artifacts +aam search # Search for skills +aam install # Install a skill +aam install / # Install from specific source +aam verify # Check file integrity +aam outdated # List outdated packages +aam upgrade # Upgrade to latest versions +``` + +Ready to consolidate your favorite skills into a team package? Continue to [Skill Consolidation](skill-consolidation.md)! diff --git a/docs/user_docs/docs/tutorials/skill-consolidation.md b/docs/user_docs/docs/tutorials/skill-consolidation.md new file mode 100644 index 0000000..ac472f9 --- /dev/null +++ b/docs/user_docs/docs/tutorials/skill-consolidation.md @@ -0,0 +1,606 @@ +# Tutorial: Skill Consolidation + +**Difficulty:** Intermediate +**Time:** 20 minutes + +## What You'll Learn + +In this tutorial, you'll learn how to consolidate skills from multiple upstream sources and your own project into a single, curated AAM package. This is the "playlist" approach — pick the best skills from the community, add your own, and distribute a single package that gives your team everything they need. + +## Prerequisites + +- AAM installed (`aam --version` works) +- `aam init` completed (platform configured, default sources registered) +- Basic familiarity with `aam source` and `aam install` commands + +!!! tip "New to sources?" + If you haven't set up sources yet, run `aam init` first, then `aam source update --all` to clone the default community repositories. See [Git Sources](../concepts/git-sources.md) for background. + +--- + +## The Scenario + +Your team uses skills from several places: + +- **Community sources** — Useful skills from `openai/skills`, `anthropics/skills`, and `github/awesome-copilot` +- **Internal skills** — Custom skills you've written for your specific stack (e.g., internal API conventions, deployment workflows) +- **Modified community skills** — Community skills you've tweaked to fit your team's coding standards + +You want to: + +1. Cherry-pick the best community skills +2. Combine them with your internal skills +3. Bundle everything into a single team package +4. Version and distribute it so everyone stays in sync + +--- + +## Step 1: Review Available Skills + +First, let's see what's available across all configured sources: + +```bash +aam list --available +``` + +**Expected output:** + +``` +Source: github/awesome-copilot + skill commit-message-writer Write conventional commit messages + skill code-reviewer Review code for best practices + skill test-generator Generate unit tests + +Source: openai/skills:.curated + skill code-review Comprehensive code review + skill refactoring Suggest refactoring improvements + skill documentation-writer Generate documentation + +Source: anthropics/skills + skill skill-creator Create new skills from descriptions + skill debugging-assistant Systematic debugging helper + +Source: microsoft/skills + skill csharp-analyzer C# code analysis + skill azure-deployer Azure deployment helper +``` + +You can also search for specific skills: + +```bash +aam search review --type skill +``` + +``` +Search results for "review" (3 matches) + +Name Version Type Source Description +code-reviewer — skill github/awesome-copilot Review code for best practices +code-review — skill openai/skills:.curated Comprehensive code review +``` + +--- + +## Step 2: Create the Consolidation Package + +Create a directory for your consolidated package: + +```bash +mkdir team-skills && cd team-skills +``` + +Now use `aam pkg create --from-source` to pull skills from remote sources. Start with the first source: + +```bash +aam pkg create --from-source anthropics/skills \ + --type skill \ + --name @myteam/consolidated-skills \ + --version 1.0.0 \ + --description "Curated team skill set" \ + --author "My Team" \ + --output-dir . \ + --yes +``` + +**Expected output:** + +``` +Scanning source 'anthropics/skills' for skill artifacts... + +Found 2 artifacts: + [x] 1. skill-creator skills/skill-creator/SKILL.md + [x] 2. debugging-assistant skills/debugging-assistant/SKILL.md + +Creating package... + ✓ Created aam.yaml + ✓ Copied skill-creator → skills/skill-creator/ + ✓ Copied debugging-assistant → skills/debugging-assistant/ + ✓ Added provenance metadata + ✓ Computed file checksums + +✓ Package created: @myteam/consolidated-skills@1.0.0 + 2 artifacts (2 skills) +``` + +!!! info "What's `--from-source`?" + The `--from-source` flag tells `aam pkg create` to pull artifacts from a registered git source instead of scanning the local project directory. AAM copies the files from the source cache and records provenance (source URL, commit SHA, fetch timestamp) in the manifest. + +--- + +## Step 3: Add Skills from Other Sources + +Now add skills from additional sources. Use `aam pkg create --from-source` with `--artifacts` to cherry-pick specific skills: + +```bash +# Add just the code-review skill from openai/skills:.curated +aam pkg create --from-source openai/skills:.curated \ + --artifacts code-review \ + --output-dir . \ + --yes +``` + +``` +Found 1 matching artifact: + [x] 1. code-review skills/.curated/code-review/SKILL.md + + ✓ Copied code-review → skills/code-review/ + ✓ Updated aam.yaml (3 skills total) +``` + +```bash +# Add the commit-message-writer from github/awesome-copilot +aam pkg create --from-source github/awesome-copilot \ + --artifacts commit-message-writer \ + --output-dir . \ + --yes +``` + +``` + ✓ Copied commit-message-writer → skills/commit-message-writer/ + ✓ Updated aam.yaml (4 skills total) +``` + +!!! tip "Cherry-picking with `--artifacts`" + Use `--artifacts` to select specific artifacts by name. You can specify multiple names separated by commas: `--artifacts code-review,refactoring`. Without `--artifacts`, all discovered artifacts from the source are included. + +--- + +## Step 4: Add Your Own Internal Skills + +Now add your team's custom skills. Create them directly in the package: + +```bash +# Create a custom skill for your internal API conventions +mkdir -p skills/internal-api-conventions +cat > skills/internal-api-conventions/SKILL.md << 'EOF' +--- +name: internal-api-conventions +description: Enforce team API design conventions and patterns +--- + +# Internal API Conventions + +## When to Use + +Use this skill when designing or reviewing REST API endpoints. + +## Conventions + +### Naming +- Use kebab-case for URL paths: `/user-profiles/`, not `/userProfiles/` +- Use plural nouns for collections: `/users/`, not `/user/` +- Use `snake_case` for JSON field names + +### Versioning +- Always version APIs: `/api/v1/...` +- Never break backward compatibility in the same major version + +### Error Responses +- Always return structured errors: + ```json + {"error": {"code": "NOT_FOUND", "message": "User not found", "details": {}}} + ``` +- Use standard HTTP status codes + +### Pagination +- Use cursor-based pagination for lists +- Always include `next_cursor` and `has_more` fields + +## Review Checklist + +When reviewing API changes: + +1. ✅ Endpoints follow naming conventions +2. ✅ Error responses use the standard format +3. ✅ New fields don't break existing clients +4. ✅ Pagination is implemented for list endpoints +5. ✅ Input validation returns 422 with field-level errors +EOF +``` + +```bash +# Create a deployment workflow skill +mkdir -p skills/deploy-workflow +cat > skills/deploy-workflow/SKILL.md << 'EOF' +--- +name: deploy-workflow +description: Guide through the team deployment process +--- + +# Deployment Workflow + +## When to Use + +Use this skill when preparing or executing a deployment. + +## Pre-Deployment Checklist + +1. **Tests pass** — All CI checks are green +2. **Changelog updated** — Version bump and changelog entry added +3. **Database migrations** — Any pending migrations are reviewed +4. **Feature flags** — New features are behind flags if needed +5. **Rollback plan** — Know how to revert if something goes wrong + +## Deployment Steps + +### Staging +```bash +git tag -a v{version}-rc.1 -m "Release candidate" +git push origin v{version}-rc.1 +# Wait for staging deploy to complete +# Run smoke tests against staging +``` + +### Production +```bash +git tag -a v{version} -m "Release v{version}" +git push origin v{version} +# Monitor error rates for 30 minutes +# Verify key user flows +``` + +## Rollback + +If issues are detected: +```bash +# Revert to previous version +git revert HEAD +git push origin main +# Or: redeploy the previous tag +``` +EOF +``` + +Now update the manifest to include your custom skills: + +```bash +cat >> aam.yaml << 'EOF' + +# Added manually — custom skills below are appended to the artifacts list +EOF +``` + +Or better yet, just edit `aam.yaml` to add them to the existing artifacts list: + +```yaml title="aam.yaml (updated)" +name: "@myteam/consolidated-skills" +version: 1.0.0 +description: Curated team skill set +author: My Team +license: Apache-2.0 + +artifacts: + skills: + # From anthropics/skills + - name: skill-creator + path: skills/skill-creator/ + description: Create new skills from descriptions + - name: debugging-assistant + path: skills/debugging-assistant/ + description: Systematic debugging helper + + # From openai/skills:.curated + - name: code-review + path: skills/code-review/ + description: Comprehensive code review + + # From github/awesome-copilot + - name: commit-message-writer + path: skills/commit-message-writer/ + description: Write conventional commit messages + + # Internal team skills + - name: internal-api-conventions + path: skills/internal-api-conventions/ + description: Enforce team API design conventions and patterns + - name: deploy-workflow + path: skills/deploy-workflow/ + description: Guide through the team deployment process + +dependencies: {} + +platforms: + cursor: + skill_scope: project + claude: + merge_instructions: true + copilot: + merge_instructions: true +``` + +--- + +## Step 5: Review the Package Structure + +Your consolidated package should now look like this: + +```bash +tree -L 2 +``` + +``` +. +├── aam.yaml +└── skills/ + ├── code-review/ + ├── commit-message-writer/ + ├── debugging-assistant/ + ├── deploy-workflow/ + ├── internal-api-conventions/ + └── skill-creator/ +``` + +Six skills from three different sources plus your own — all in one package. + +--- + +## Step 6: Validate and Pack + +Validate the consolidated package: + +```bash +aam pkg validate +``` + +``` +Validating @myteam/consolidated-skills@1.0.0... + +Manifest: + ✓ name: valid scoped format + ✓ version: valid semver (1.0.0) + ✓ description: present + ✓ author: present + +Artifacts: + ✓ skill: skill-creator + ✓ skill: debugging-assistant + ✓ skill: code-review + ✓ skill: commit-message-writer + ✓ skill: internal-api-conventions + ✓ skill: deploy-workflow + +✓ Package is valid and ready to publish +``` + +Build the archive: + +```bash +aam pkg pack +``` + +``` +Building @myteam/consolidated-skills@1.0.0... + +✓ Built myteam-consolidated-skills-1.0.0.aam (18.5 KB) + 6 skills, checksums computed +``` + +--- + +## Step 7: Distribute to Your Team + +### Option A: Publish to a Registry + +```bash +# Publish to your team's registry +aam pkg publish --registry team-registry +``` + +Your teammates can then install with a single command: + +```bash +aam install @myteam/consolidated-skills +``` + +### Option B: Share the Archive Directly + +Share the `.aam` file via Slack, email, or a shared drive: + +```bash +# Teammate installs from the archive +aam install ./myteam-consolidated-skills-1.0.0.aam +``` + +### Option C: Use a Git Repository as a Source + +Commit the package to a Git repository and add it as a source: + +```bash +# In the package directory +git init && git add . && git commit -m "Initial consolidated skills" +git remote add origin git@github.com:myteam/team-skills.git +git push -u origin main + +# Teammates add it as a source +aam source add myteam/team-skills +aam install consolidated-skills +``` + +--- + +## Step 8: Keep It Updated + +When upstream sources release new skills or updates, you can refresh your package: + +```bash +# Update all sources to fetch latest changes +aam source update --all + +# Check what changed +aam outdated + +# Re-create from source to pick up changes +aam pkg create --from-source anthropics/skills \ + --artifacts skill-creator,debugging-assistant \ + --output-dir . \ + --yes +``` + +Bump the version in `aam.yaml`, then validate, pack, and publish the update: + +```bash +# Edit aam.yaml: version: 1.0.0 → 1.1.0 +aam pkg validate +aam pkg pack +aam pkg publish --registry team-registry +``` + +Your teammates upgrade with: + +```bash +aam upgrade @myteam/consolidated-skills +``` + +--- + +## Consolidation Patterns + +Here are common patterns for skill consolidation: + +### Pattern 1: Role-Based Packages + +Create separate packages for different roles: + +``` +@myteam/frontend-skills → React, CSS, accessibility skills +@myteam/backend-skills → API, database, security skills +@myteam/devops-skills → CI/CD, Docker, monitoring skills +``` + +### Pattern 2: Project-Specific Packages + +Bundle skills relevant to a specific project: + +``` +@myteam/project-alpha → API conventions + deploy workflow + code review +@myteam/project-beta → ML pipeline + data validation + notebook helpers +``` + +### Pattern 3: Onboarding Package + +A single package for new team members: + +``` +@myteam/onboarding → Coding standards + git workflow + deploy process + code review +``` + +### Pattern 4: Layered Packages with Dependencies + +Use dependencies to create a layered structure: + +```yaml +# @myteam/base-skills/aam.yaml +name: "@myteam/base-skills" +artifacts: + skills: + - name: code-review + - name: commit-message-writer +``` + +```yaml +# @myteam/frontend-skills/aam.yaml +name: "@myteam/frontend-skills" +dependencies: + "@myteam/base-skills": "^1.0.0" +artifacts: + skills: + - name: react-patterns + - name: css-conventions +``` + +--- + +## Next Steps + +Now that you've consolidated skills, you can: + +- **Install across platforms** — Follow the [Multi-Platform Deployment](multi-platform-deployment.md) tutorial +- **Add dependencies** — Learn about dependencies in [Working with Dependencies](working-with-dependencies.md) +- **Share with your team** — Follow the [Sharing with Your Team](share-with-team.md) tutorial +- **Keep skills updated** — Use `aam outdated` and `aam upgrade` to stay current + +--- + +## Troubleshooting + +### Source not found + +**Problem:** `aam pkg create --from-source myrepo` fails with "source not found" + +**Solution:** Make sure the source is registered: + +```bash +aam source list # Check registered sources +aam source add github.com/org/repo # Add missing source +aam source update org/repo # Ensure cache is fresh +``` + +### Artifact name conflicts + +**Problem:** Two sources have skills with the same name + +**Solution:** Use qualified names when installing or rename one skill in your package: + +```bash +# Install with qualified name +aam install openai/skills:.curated/code-review + +# Or rename in your package by editing the name in aam.yaml +# and renaming the directory +``` + +### Stale source cache + +**Problem:** Source shows old artifacts that have been removed upstream + +**Solution:** Update the source cache: + +```bash +aam source update --all +``` + +--- + +## Summary + +In this tutorial, you learned how to: + +- Browse available skills across multiple sources with `aam list --available` and `aam search` +- Pull skills from remote sources with `aam pkg create --from-source` +- Cherry-pick specific artifacts with `--artifacts` +- Add your own custom skills alongside community ones +- Validate, pack, and distribute a consolidated package +- Keep the package updated as upstream sources change + +**Key Commands:** + +```bash +aam list --available # Browse all source artifacts +aam search --type skill # Search for specific skills +aam pkg create --from-source --artifacts # Pull from source +aam pkg validate # Verify package +aam pkg pack # Build archive +aam source update --all # Refresh source caches +``` + +Ready to share your consolidated package? Continue to [Sharing with Your Team](share-with-team.md)! diff --git a/docs/user_docs/mkdocs.yml b/docs/user_docs/mkdocs.yml index 0907312..1dd2b87 100644 --- a/docs/user_docs/mkdocs.yml +++ b/docs/user_docs/mkdocs.yml @@ -116,10 +116,12 @@ nav: - Your First Package: getting-started/first-package.md - Tutorials: - tutorials/index.md + - Install Skills from Sources: tutorials/install-from-sources.md - Package Existing Skills: tutorials/package-existing-skills.md - Build a Code Review Package: tutorials/build-code-review-package.md - Share Packages with Your Team: tutorials/share-with-team.md - Multi-Platform Deployment: tutorials/multi-platform-deployment.md + - Skill Consolidation: tutorials/skill-consolidation.md - Working with Dependencies: tutorials/working-with-dependencies.md - Concepts: - concepts/index.md @@ -172,7 +174,10 @@ nav: - aam config list: cli/config-list.md - Utilities: - aam doctor: cli/doctor.md + - aam convert: cli/convert.md - Global Options: cli/global-options.md + - MCP Interface: + - mcp/index.md - Configuration: - configuration/index.md - Global Config: configuration/global.md From f5901eb9237c3bdd2f1cae9cc39e6595f1a6463b Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Mon, 16 Feb 2026 01:56:50 +0100 Subject: [PATCH 6/9] Update test assertion for project config status in unit tests - Changed the expected status from "pass" to "warn" for the scenario where the project config does not exist. - Added a check to ensure the warning message includes "not found, using defaults" for clarity in test feedback. --- apps/aam-cli/tests/unit/test_services_doctor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/aam-cli/tests/unit/test_services_doctor.py b/apps/aam-cli/tests/unit/test_services_doctor.py index 9f46d9b..7c8d5f1 100644 --- a/apps/aam-cli/tests/unit/test_services_doctor.py +++ b/apps/aam-cli/tests/unit/test_services_doctor.py @@ -274,7 +274,8 @@ def test_unit_doctor_config_files_invalid_yaml(self, tmp_path) -> None: # Project config does not exist — should still pass pc = checks[1] - assert pc["status"] == "pass" + assert pc["status"] == "warn" + assert "not found, using defaults" in pc["message"] def test_unit_doctor_config_files_invalid_schema(self, tmp_path) -> None: """Config file with valid YAML but invalid schema reports fail.""" From 7823e947d9e580f38e5bc2761ff5d233283ff2a7 Mon Sep 17 00:00:00 2001 From: Roman Marek~ Date: Fri, 20 Feb 2026 01:07:24 +0100 Subject: [PATCH 7/9] Refactor GitHub Copilot and Claude integration documentation to support discrete agent and instruction files. Update deployment mappings, configuration options, and examples to reflect new file structures. Remove marker-based merging for agents and instructions, ensuring clear separation of user content and AAM-managed content. Enhance clarity and organization throughout the documentation. --- README.md | 4 +- apps/aam-cli/src/aam_cli/adapters/claude.py | 61 ++- apps/aam-cli/src/aam_cli/adapters/copilot.py | 218 +++------- apps/aam-cli/src/aam_cli/commands/convert.py | 4 +- .../src/aam_cli/services/convert_service.py | 17 +- .../tests/unit/test_adapters_factory.py | 69 +-- apps/aam-cli/tests/unit/test_convert.py | 1 - docs/DESIGN.md | 71 ++-- .../docs/concepts/platform-adapters.md | 139 +++--- docs/user_docs/docs/platforms/claude.md | 156 +++---- docs/user_docs/docs/platforms/copilot.md | 396 ++++-------------- 11 files changed, 405 insertions(+), 731 deletions(-) diff --git a/README.md b/README.md index 90dfb6d..30ed705 100644 --- a/README.md +++ b/README.md @@ -338,8 +338,8 @@ AAM automatically deploys artifacts to the correct locations for each platform: | Platform | Skills | Agents | Prompts | Instructions | |----------|--------|--------|---------|--------------| | **Cursor** | `.cursor/skills/` | `.cursor/rules/` | `.cursor/prompts/` | `.cursor/rules/` | -| **Claude** | `.claude/skills/` | `CLAUDE.md` | `.claude/prompts/` | `CLAUDE.md` | -| **Copilot** | `.github/skills/` | `copilot-instructions.md` | `.github/prompts/` | `copilot-instructions.md` | +| **Claude** | `.claude/skills/` | `.claude/agents/` | `.claude/prompts/` | `CLAUDE.md` | +| **Copilot** | `.github/skills/` | `.github/agents/` | `.github/prompts/` | `.github/instructions/` | | **Codex** | `~/.codex/skills/` | `AGENTS.md` | `~/.codex/prompts/` | `AGENTS.md` | --- diff --git a/apps/aam-cli/src/aam_cli/adapters/claude.py b/apps/aam-cli/src/aam_cli/adapters/claude.py index 73d9260..5550866 100644 --- a/apps/aam-cli/src/aam_cli/adapters/claude.py +++ b/apps/aam-cli/src/aam_cli/adapters/claude.py @@ -2,7 +2,7 @@ Deploys AAM artifacts into the Claude Code filesystem structure: - Skills → ``.claude/skills//`` - - Agents → ``CLAUDE.md`` (marker-delimited section) + - Agents → ``.claude/agents/.md`` - Prompts → ``.claude/prompts/.md`` - Instructions → ``CLAUDE.md`` (marker-delimited section) @@ -38,7 +38,8 @@ # # ################################################################################ -# Marker templates for AAM-managed sections inside CLAUDE.md +# Marker templates for AAM-managed sections inside CLAUDE.md. +# Only used for instructions — agents are now discrete files. BEGIN_MARKER_TEMPLATE: str = "" END_MARKER_TEMPLATE: str = "" @@ -52,9 +53,9 @@ class ClaudeAdapter: """Claude Code platform adapter. - Skills and prompts are deployed to a ``.claude/`` directory. - Agents and instructions are merged into ``CLAUDE.md`` at the project - root using HTML comment markers. + Skills, agents, and prompts are deployed to a ``.claude/`` directory. + Instructions are merged into ``CLAUDE.md`` at the project root using + HTML comment markers. """ def __init__(self, project_root: Path) -> None: @@ -117,10 +118,10 @@ def deploy_agent( agent_ref: ArtifactRef, _config: dict[str, str], ) -> Path: - """Deploy an agent section into ``CLAUDE.md``. + """Deploy an agent to ``.claude/agents/.md``. - Reads the agent's system-prompt.md and appends it as a - marker-delimited section in the project-root ``CLAUDE.md``. + Reads the agent's system-prompt.md and writes it as a discrete + markdown file in the ``.claude/agents/`` directory. Args: agent_path: Path to the extracted agent directory. @@ -128,17 +129,20 @@ def deploy_agent( _config: Platform config. Returns: - Path to CLAUDE.md. + Path to the created agent file. """ - logger.info(f"Deploying agent '{agent_ref.name}' -> CLAUDE.md") + fs_name = self._artifact_fs_name(agent_ref.name) + dest = self.claude_dir / "agents" / f"{fs_name}.md" + + logger.info(f"Deploying agent '{agent_ref.name}' -> {dest}") content = self._read_agent_content(agent_path, agent_ref) - claude_md = self.project_root / "CLAUDE.md" - self._upsert_marker_section(claude_md, agent_ref.name, "agent", content) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(content, encoding="utf-8") - logger.info(f"Agent deployed to: {claude_md}") - return claude_md + logger.info(f"Agent deployed: {dest}") + return dest def deploy_prompt( self, @@ -217,9 +221,15 @@ def undeploy(self, artifact_name: str, artifact_type: str) -> None: shutil.rmtree(path) logger.info(f"Removed skill directory: {path}") - elif artifact_type in ("agent", "instruction"): + elif artifact_type == "agent": + path = self.claude_dir / "agents" / f"{fs_name}.md" + if path.is_file(): + path.unlink() + logger.info(f"Removed agent file: {path}") + + elif artifact_type == "instruction": claude_md = self.project_root / "CLAUDE.md" - self._remove_marker_section(claude_md, artifact_name, artifact_type) + self._remove_marker_section(claude_md, artifact_name, "instruction") elif artifact_type == "prompt": path = self.claude_dir / "prompts" / f"{fs_name}.md" @@ -242,6 +252,13 @@ def list_deployed(self) -> list[tuple[str, str, Path]]: if entry.is_dir(): deployed.append((entry.name, "skill", entry)) + # Agents + agents_dir = self.claude_dir / "agents" + if agents_dir.is_dir(): + for entry in agents_dir.iterdir(): + if entry.suffix == ".md": + deployed.append((entry.stem, "agent", entry)) + # Prompts prompts_dir = self.claude_dir / "prompts" if prompts_dir.is_dir(): @@ -249,7 +266,7 @@ def list_deployed(self) -> list[tuple[str, str, Path]]: if entry.suffix == ".md": deployed.append((entry.stem, "prompt", entry)) - # Agents and instructions from CLAUDE.md markers + # Instructions from CLAUDE.md markers claude_md = self.project_root / "CLAUDE.md" if claude_md.is_file(): deployed.extend(self._list_marker_sections(claude_md)) @@ -257,7 +274,7 @@ def list_deployed(self) -> list[tuple[str, str, Path]]: return deployed # ------------------------------------------------------------------ - # Marker-based section management + # Marker-based section management (instructions only) # ------------------------------------------------------------------ def _upsert_marker_section( @@ -275,7 +292,7 @@ def _upsert_marker_section( Args: file_path: Path to the target markdown file. name: Artifact name (used in markers). - kind: Artifact kind (``"agent"`` or ``"instruction"``). + kind: Artifact kind (``"instruction"``). content: Markdown body to place between the markers. """ file_path.parent.mkdir(parents=True, exist_ok=True) @@ -314,7 +331,7 @@ def _remove_marker_section( Args: file_path: Path to the target markdown file. name: Artifact name used in markers. - kind: Artifact kind (``"agent"`` or ``"instruction"``). + kind: Artifact kind (``"instruction"``). """ if not file_path.is_file(): return @@ -352,7 +369,7 @@ def _list_marker_sections( content = file_path.read_text(encoding="utf-8") results: list[tuple[str, str, Path]] = [] - pattern = re.compile(r"") + pattern = re.compile(r"") for match in pattern.finditer(content): name = match.group(1) kind = match.group(2) @@ -376,7 +393,7 @@ def _artifact_fs_name(self, artifact_name: str) -> str: return artifact_name def _read_agent_content(self, agent_path: Path, agent_ref: ArtifactRef) -> str: - """Read agent content for embedding in CLAUDE.md. + """Read agent content for the agent file. Reads the system-prompt.md file from the agent directory. diff --git a/apps/aam-cli/src/aam_cli/adapters/copilot.py b/apps/aam-cli/src/aam_cli/adapters/copilot.py index 2c5ae4a..499bf6b 100644 --- a/apps/aam-cli/src/aam_cli/adapters/copilot.py +++ b/apps/aam-cli/src/aam_cli/adapters/copilot.py @@ -2,9 +2,9 @@ Deploys AAM artifacts into the GitHub Copilot filesystem structure: - Skills → ``.github/skills//`` - - Agents → ``.github/copilot-instructions.md`` (marker-delimited section) + - Agents → ``.github/agents/.agent.md`` - Prompts → ``.github/prompts/.md`` - - Instructions → ``.github/copilot-instructions.md`` (marker-delimited section) + - Instructions → ``.github/instructions/.instructions.md`` Decision reference: DESIGN.md Section 8.2. """ @@ -31,17 +31,6 @@ # Initialize logger for this module logger = logging.getLogger(__name__) -################################################################################ -# # -# CONSTANTS # -# # -################################################################################ - -# Marker templates used to delimit AAM-managed sections inside shared files. -# Copilot agents and instructions are merged into copilot-instructions.md. -BEGIN_MARKER_TEMPLATE: str = "" -END_MARKER_TEMPLATE: str = "" - ################################################################################ # # # COPILOT ADAPTER # @@ -52,9 +41,11 @@ class CopilotAdapter: """GitHub Copilot platform adapter. - All deploy methods write to a ``.github/`` directory within the - project root, except agents and instructions which are appended - to ``.github/copilot-instructions.md`` using HTML comment markers. + Deploys artifacts to the ``.github/`` directory within the project root: + - Skills to ``.github/skills//`` + - Agents to ``.github/agents/.agent.md`` + - Prompts to ``.github/prompts/.md`` + - Instructions to ``.github/instructions/.instructions.md`` """ def __init__(self, project_root: Path) -> None: @@ -117,10 +108,10 @@ def deploy_agent( agent_ref: ArtifactRef, _config: dict[str, str], ) -> Path: - """Deploy an agent section into ``.github/copilot-instructions.md``. + """Deploy an agent to ``.github/agents/.agent.md``. Reads the agent's system-prompt.md (or agent.yaml → system_prompt - reference) and appends it as a marker-delimited section. + reference) and writes it as a discrete ``.agent.md`` file. Args: agent_path: Path to the extracted agent directory. @@ -128,9 +119,12 @@ def deploy_agent( _config: Platform config. Returns: - Path to the copilot-instructions.md file. + Path to the created agent file. """ - logger.info(f"Deploying agent '{agent_ref.name}' -> copilot-instructions.md") + fs_name = self._artifact_fs_name(agent_ref.name) + dest = self.github_dir / "agents" / f"{fs_name}.agent.md" + + logger.info(f"Deploying agent '{agent_ref.name}' -> {dest}") # ----- # Read the system prompt content @@ -138,13 +132,13 @@ def deploy_agent( content = self._read_agent_content(agent_path, agent_ref) # ----- - # Merge into copilot-instructions.md + # Write discrete agent file # ----- - instructions_path = self.github_dir / "copilot-instructions.md" - self._upsert_marker_section(instructions_path, agent_ref.name, "agent", content) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(content, encoding="utf-8") - logger.info(f"Agent deployed to: {instructions_path}") - return instructions_path + logger.info(f"Agent deployed: {dest}") + return dest def deploy_prompt( self, @@ -179,10 +173,10 @@ def deploy_instruction( instr_ref: ArtifactRef, _config: dict[str, str], ) -> Path: - """Deploy an instruction section into ``.github/copilot-instructions.md``. + """Deploy an instruction to ``.github/instructions/.instructions.md``. - Reads the instruction markdown and appends it as a marker-delimited - section alongside any agents already present. + Copies the instruction markdown as a discrete ``.instructions.md`` file + in the ``.github/instructions/`` directory. Args: instr_path: Path to the instruction file. @@ -190,21 +184,18 @@ def deploy_instruction( _config: Platform config. Returns: - Path to the copilot-instructions.md file. + Path to the created instruction file. """ - logger.info( - f"Deploying instruction '{instr_ref.name}' -> copilot-instructions.md" - ) + fs_name = self._artifact_fs_name(instr_ref.name) + dest = self.github_dir / "instructions" / f"{fs_name}.instructions.md" - content = instr_path.read_text(encoding="utf-8") + logger.info(f"Deploying instruction '{instr_ref.name}' -> {dest}") - instructions_path = self.github_dir / "copilot-instructions.md" - self._upsert_marker_section( - instructions_path, instr_ref.name, "instruction", content - ) + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(instr_path, dest) - logger.info(f"Instruction deployed to: {instructions_path}") - return instructions_path + logger.info(f"Instruction deployed: {dest}") + return dest # ------------------------------------------------------------------ # Undeploy and list @@ -227,10 +218,17 @@ def undeploy(self, artifact_name: str, artifact_type: str) -> None: shutil.rmtree(path) logger.info(f"Removed skill directory: {path}") - elif artifact_type in ("agent", "instruction"): - # Remove marker-delimited section from copilot-instructions.md - instructions_path = self.github_dir / "copilot-instructions.md" - self._remove_marker_section(instructions_path, artifact_name, artifact_type) + elif artifact_type == "agent": + path = self.github_dir / "agents" / f"{fs_name}.agent.md" + if path.is_file(): + path.unlink() + logger.info(f"Removed agent file: {path}") + + elif artifact_type == "instruction": + path = self.github_dir / "instructions" / f"{fs_name}.instructions.md" + if path.is_file(): + path.unlink() + logger.info(f"Removed instruction file: {path}") elif artifact_type == "prompt": path = self.github_dir / "prompts" / f"{fs_name}.md" @@ -253,6 +251,15 @@ def list_deployed(self) -> list[tuple[str, str, Path]]: if entry.is_dir(): deployed.append((entry.name, "skill", entry)) + # Agents + agents_dir = self.github_dir / "agents" + if agents_dir.is_dir(): + for entry in agents_dir.iterdir(): + if entry.name.endswith(".agent.md"): + # Strip the .agent.md suffix to get the artifact name + name = entry.name.removesuffix(".agent.md") + deployed.append((name, "agent", entry)) + # Prompts prompts_dir = self.github_dir / "prompts" if prompts_dir.is_dir(): @@ -260,125 +267,16 @@ def list_deployed(self) -> list[tuple[str, str, Path]]: if entry.suffix == ".md": deployed.append((entry.stem, "prompt", entry)) - # Agents and instructions from copilot-instructions.md markers - instructions_path = self.github_dir / "copilot-instructions.md" - if instructions_path.is_file(): - deployed.extend(self._list_marker_sections(instructions_path)) + # Instructions + instructions_dir = self.github_dir / "instructions" + if instructions_dir.is_dir(): + for entry in instructions_dir.iterdir(): + if entry.name.endswith(".instructions.md"): + name = entry.name.removesuffix(".instructions.md") + deployed.append((name, "instruction", entry)) return deployed - # ------------------------------------------------------------------ - # Marker-based section management - # ------------------------------------------------------------------ - - def _upsert_marker_section( - self, - file_path: Path, - name: str, - kind: str, - content: str, - ) -> None: - """Insert or replace a marker-delimited section in a file. - - If the file does not exist it is created. Existing user content - outside AAM markers is preserved. - - Args: - file_path: Path to the target markdown file. - name: Artifact name (used in markers). - kind: Artifact kind (``"agent"`` or ``"instruction"``). - content: Markdown body to place between the markers. - """ - file_path.parent.mkdir(parents=True, exist_ok=True) - - begin_marker = BEGIN_MARKER_TEMPLATE.format(name=name, kind=kind) - end_marker = END_MARKER_TEMPLATE.format(name=name, kind=kind) - - section_block = f"{begin_marker}\n{content.rstrip()}\n{end_marker}\n" - - if file_path.is_file(): - existing = file_path.read_text(encoding="utf-8") - - # ----- - # Check if a section for this artifact already exists - # ----- - if begin_marker in existing and end_marker in existing: - # Replace existing section - before = existing[: existing.index(begin_marker)] - after = existing[existing.index(end_marker) + len(end_marker) :] - # Strip leading newline from the remainder - after = after.lstrip("\n") - new_content = before.rstrip("\n") + "\n\n" + section_block - if after.strip(): - new_content += "\n" + after - else: - # Append new section - new_content = existing.rstrip("\n") + "\n\n" + section_block - else: - new_content = section_block - - file_path.write_text(new_content, encoding="utf-8") - logger.debug(f"Upserted section '{name}' ({kind}) in {file_path}") - - def _remove_marker_section( - self, - file_path: Path, - name: str, - kind: str, - ) -> None: - """Remove a marker-delimited section from a file. - - Args: - file_path: Path to the target markdown file. - name: Artifact name used in markers. - kind: Artifact kind (``"agent"`` or ``"instruction"``). - """ - if not file_path.is_file(): - return - - begin_marker = BEGIN_MARKER_TEMPLATE.format(name=name, kind=kind) - end_marker = END_MARKER_TEMPLATE.format(name=name, kind=kind) - - existing = file_path.read_text(encoding="utf-8") - - if begin_marker not in existing or end_marker not in existing: - return - - before = existing[: existing.index(begin_marker)] - after = existing[existing.index(end_marker) + len(end_marker) :] - - new_content = (before.rstrip("\n") + "\n" + after.lstrip("\n")).strip() - - if new_content: - file_path.write_text(new_content + "\n", encoding="utf-8") - else: - file_path.unlink() - logger.info(f"Removed empty file: {file_path}") - - logger.info(f"Removed section '{name}' ({kind}) from {file_path}") - - def _list_marker_sections( - self, - file_path: Path, - ) -> list[tuple[str, str, Path]]: - """Parse marker-delimited sections from a file. - - Returns: - List of ``(name, type, path)`` tuples for each AAM section found. - """ - content = file_path.read_text(encoding="utf-8") - results: list[tuple[str, str, Path]] = [] - - import re - - pattern = re.compile(r"") - for match in pattern.finditer(content): - name = match.group(1) - kind = match.group(2) - results.append((name, kind, file_path)) - - return results - # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ @@ -395,7 +293,7 @@ def _artifact_fs_name(self, artifact_name: str) -> str: return artifact_name def _read_agent_content(self, agent_path: Path, agent_ref: ArtifactRef) -> str: - """Read agent content for embedding in copilot-instructions.md. + """Read agent content for the ``.agent.md`` file. Reads the system-prompt.md file from the agent directory. diff --git a/apps/aam-cli/src/aam_cli/commands/convert.py b/apps/aam-cli/src/aam_cli/commands/convert.py index c8914ac..c3e92c9 100644 --- a/apps/aam-cli/src/aam_cli/commands/convert.py +++ b/apps/aam-cli/src/aam_cli/commands/convert.py @@ -19,7 +19,7 @@ from rich.console import Console from aam_cli.converters.mappings import PLATFORMS, VERBOSE_WORKAROUNDS -from aam_cli.services.convert_service import run_conversion +from aam_cli.services.convert_service import ConversionResult, run_conversion ################################################################################ # # @@ -149,7 +149,7 @@ def convert( return # Group results by artifact type - grouped: dict[str, list] = {} + grouped: dict[str, list[ConversionResult]] = {} for result in report.results: grouped.setdefault(result.artifact_type.upper() + "S", []).append(result) diff --git a/apps/aam-cli/src/aam_cli/services/convert_service.py b/apps/aam-cli/src/aam_cli/services/convert_service.py index c61fc79..a723937 100644 --- a/apps/aam-cli/src/aam_cli/services/convert_service.py +++ b/apps/aam-cli/src/aam_cli/services/convert_service.py @@ -22,7 +22,6 @@ AGENT_SUPPORTED_FIELDS, AGENT_TARGET_DIRS, AGENT_TARGET_EXTENSIONS, - INSTRUCTION_SUPPORTED_FIELDS, PROMPT_SUPPORTED_FIELDS, PROMPT_TARGET_DIRS, PROMPT_TARGET_EXTENSIONS, @@ -441,17 +440,21 @@ def _convert_instruction( target_file = target_dir / f"{name}.mdc" target_path_str = str(target_file.relative_to(root)) - new_meta: dict[str, object] = {} - new_meta["description"] = frontmatter.get("description", "Converted instruction") + cursor_meta: dict[str, object] = {} + cursor_meta["description"] = frontmatter.get( + "description", "Converted instruction" + ) if frontmatter.get("applyTo"): - new_meta["alwaysApply"] = False + cursor_meta["alwaysApply"] = False apply_to = frontmatter["applyTo"] - new_meta["globs"] = [apply_to] if isinstance(apply_to, str) else apply_to + cursor_meta["globs"] = ( + [apply_to] if isinstance(apply_to, str) else apply_to + ) else: - new_meta["alwaysApply"] = True + cursor_meta["alwaysApply"] = True - content = generate_frontmatter(new_meta, body) + content = generate_frontmatter(cursor_meta, body) if not dry_run: if target_file.exists() and not force: diff --git a/apps/aam-cli/tests/unit/test_adapters_factory.py b/apps/aam-cli/tests/unit/test_adapters_factory.py index 983c8f8..e7dc7fe 100644 --- a/apps/aam-cli/tests/unit/test_adapters_factory.py +++ b/apps/aam-cli/tests/unit/test_adapters_factory.py @@ -129,8 +129,8 @@ def test_unit_deploy_skill(self, tmp_path: Path) -> None: assert dest == project / ".github" / "skills" / "my-skill" assert (dest / "SKILL.md").is_file() - def test_unit_deploy_agent_to_copilot_instructions(self, tmp_path: Path) -> None: - """Agent is merged into .github/copilot-instructions.md with markers.""" + def test_unit_deploy_agent_to_agents_dir(self, tmp_path: Path) -> None: + """Agent is deployed to .github/agents/.agent.md.""" project = tmp_path / "project" project.mkdir() agent_dir = self._make_agent_dir(tmp_path) @@ -139,11 +139,10 @@ def test_unit_deploy_agent_to_copilot_instructions(self, tmp_path: Path) -> None ref = ArtifactRef(name="my-agent", path="agents/my-agent", description="test") dest = adapter.deploy_agent(agent_dir, ref, {}) - assert dest == project / ".github" / "copilot-instructions.md" + assert dest == project / ".github" / "agents" / "my-agent.agent.md" + assert dest.is_file() content = dest.read_text() - assert "" in content assert "You are a helpful agent." in content - assert "" in content def test_unit_deploy_prompt(self, tmp_path: Path) -> None: """Prompt is deployed to .github/prompts/.md.""" @@ -158,8 +157,8 @@ def test_unit_deploy_prompt(self, tmp_path: Path) -> None: assert dest == project / ".github" / "prompts" / "my-prompt.md" assert dest.is_file() - def test_unit_deploy_instruction_to_copilot_instructions(self, tmp_path: Path) -> None: - """Instruction is merged into .github/copilot-instructions.md.""" + def test_unit_deploy_instruction_to_instructions_dir(self, tmp_path: Path) -> None: + """Instruction is deployed to .github/instructions/.instructions.md.""" project = tmp_path / "project" project.mkdir() instr_file = self._make_instruction_file(tmp_path) @@ -172,13 +171,13 @@ def test_unit_deploy_instruction_to_copilot_instructions(self, tmp_path: Path) - ) dest = adapter.deploy_instruction(instr_file, ref, {}) + assert dest == project / ".github" / "instructions" / "coding-standards.instructions.md" + assert dest.is_file() content = dest.read_text() - assert "" in content assert "Follow PEP 8." in content - assert "" in content - def test_unit_upsert_replaces_existing_section(self, tmp_path: Path) -> None: - """Re-deploying an agent replaces the existing section.""" + def test_unit_redeploy_overwrites_agent_file(self, tmp_path: Path) -> None: + """Re-deploying an agent overwrites the existing agent file.""" project = tmp_path / "project" project.mkdir() adapter = CopilotAdapter(project) @@ -198,11 +197,10 @@ def test_unit_upsert_replaces_existing_section(self, tmp_path: Path) -> None: adapter.deploy_agent(agent_dir_v2, ref, {}) - content = (project / ".github" / "copilot-instructions.md").read_text() + agent_file = project / ".github" / "agents" / "my-agent.agent.md" + content = agent_file.read_text() assert "Version 2 content." in content assert "Version 1 content." not in content - # Only one begin marker - assert content.count("") == 1 def test_unit_undeploy_skill(self, tmp_path: Path) -> None: """Undeploying a skill removes its directory.""" @@ -218,8 +216,8 @@ def test_unit_undeploy_skill(self, tmp_path: Path) -> None: adapter.undeploy("my-skill", "skill") assert not (project / ".github" / "skills" / "my-skill").exists() - def test_unit_undeploy_agent_section(self, tmp_path: Path) -> None: - """Undeploying an agent removes its marker section.""" + def test_unit_undeploy_agent_file(self, tmp_path: Path) -> None: + """Undeploying an agent removes its .agent.md file.""" project = tmp_path / "project" project.mkdir() agent_dir = self._make_agent_dir(tmp_path) @@ -228,11 +226,11 @@ def test_unit_undeploy_agent_section(self, tmp_path: Path) -> None: ref = ArtifactRef(name="my-agent", path="agents/my-agent", description="test") adapter.deploy_agent(agent_dir, ref, {}) - adapter.undeploy("my-agent", "agent") + agent_file = project / ".github" / "agents" / "my-agent.agent.md" + assert agent_file.is_file() - instructions_path = project / ".github" / "copilot-instructions.md" - # File should be removed if it was the only section - assert not instructions_path.exists() + adapter.undeploy("my-agent", "agent") + assert not agent_file.exists() def test_unit_list_deployed(self, tmp_path: Path) -> None: """list_deployed returns all deployed artifact tuples.""" @@ -250,14 +248,21 @@ def test_unit_list_deployed(self, tmp_path: Path) -> None: ref_agent = ArtifactRef(name="a1", path="agents/a1", description="test") adapter.deploy_agent(agent_dir, ref_agent, {}) + # Deploy an instruction + instr_file = self._make_instruction_file(tmp_path) + ref_instr = ArtifactRef(name="i1", path="instructions/i1.md", description="test") + adapter.deploy_instruction(instr_file, ref_instr, {}) + deployed = adapter.list_deployed() names = [d[0] for d in deployed] types = [d[1] for d in deployed] assert "s1" in names assert "a1" in names + assert "i1" in names assert "skill" in types assert "agent" in types + assert "instruction" in types ################################################################################ @@ -285,8 +290,8 @@ def test_unit_deploy_skill_to_claude_dir(self, tmp_path: Path) -> None: assert dest == project / ".claude" / "skills" / "my-skill" assert (dest / "SKILL.md").is_file() - def test_unit_deploy_agent_to_claude_md(self, tmp_path: Path) -> None: - """Agent is merged into CLAUDE.md with markers.""" + def test_unit_deploy_agent_to_agents_dir(self, tmp_path: Path) -> None: + """Agent is deployed to .claude/agents/.md.""" project = tmp_path / "project" project.mkdir() agent_dir = tmp_path / "src-agent" @@ -297,9 +302,9 @@ def test_unit_deploy_agent_to_claude_md(self, tmp_path: Path) -> None: ref = ArtifactRef(name="audit-agent", path="agents/audit-agent", description="test") dest = adapter.deploy_agent(agent_dir, ref, {}) - assert dest == project / "CLAUDE.md" + assert dest == project / ".claude" / "agents" / "audit-agent.md" + assert dest.is_file() content = dest.read_text() - assert "" in content assert "You help with audits." in content def test_unit_deploy_prompt_to_claude_dir(self, tmp_path: Path) -> None: @@ -331,8 +336,8 @@ def test_unit_deploy_instruction_to_claude_md(self, tmp_path: Path) -> None: content = dest.read_text() assert "" in content - def test_unit_preserves_existing_claude_md(self, tmp_path: Path) -> None: - """Deploying an agent preserves existing user content in CLAUDE.md.""" + def test_unit_deploy_agent_does_not_modify_claude_md(self, tmp_path: Path) -> None: + """Deploying an agent does not modify CLAUDE.md — agents go to .claude/agents/.""" project = tmp_path / "project" project.mkdir() (project / "CLAUDE.md").write_text("# My Project\n\nUser content here.\n") @@ -345,10 +350,14 @@ def test_unit_preserves_existing_claude_md(self, tmp_path: Path) -> None: ref = ArtifactRef(name="my-agent", path="agents/my-agent", description="test") adapter.deploy_agent(agent_dir, ref, {}) - content = (project / "CLAUDE.md").read_text() - assert "# My Project" in content - assert "User content here." in content - assert "Agent content." in content + # CLAUDE.md should be untouched + claude_content = (project / "CLAUDE.md").read_text() + assert claude_content == "# My Project\n\nUser content here.\n" + + # Agent should be in .claude/agents/ + agent_file = project / ".claude" / "agents" / "my-agent.md" + assert agent_file.is_file() + assert "Agent content." in agent_file.read_text() ################################################################################ diff --git a/apps/aam-cli/tests/unit/test_convert.py b/apps/aam-cli/tests/unit/test_convert.py index 29bbc06..67de525 100644 --- a/apps/aam-cli/tests/unit/test_convert.py +++ b/apps/aam-cli/tests/unit/test_convert.py @@ -9,7 +9,6 @@ import logging from pathlib import Path -import pytest from click.testing import CliRunner from aam_cli.converters.frontmatter import generate_frontmatter, parse_frontmatter diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 521b8f0..fb7a3df 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -791,7 +791,7 @@ Detection patterns: ─ instructions/*.md (AAM convention) ─ .cursor/rules/*.mdc (Cursor rules, excluding agent-* rules) ─ CLAUDE.md (Claude instructions) - ─ .github/copilot-instructions.md (Copilot instructions) + ─ .github/instructions/*.instructions.md (Copilot instructions) ─ AGENTS.md (Codex instructions) ``` @@ -877,7 +877,7 @@ When artifacts are found in platform-specific locations, they may need conversio | `.cursor/rules/*.mdc` (instruction) | `instructions/*.md` | Strip `.mdc` frontmatter, convert to instruction markdown with YAML frontmatter | | `.cursor/rules/agent-*.mdc` (agent) | `agents/*/` | Extract system prompt from rule body, generate `agent.yaml` | | `CLAUDE.md` sections | `instructions/*.md` | Split `CLAUDE.md` into individual instruction files | -| `.github/copilot-instructions.md` sections | `instructions/*.md` | Split into individual instruction files | +| `.github/instructions/*.instructions.md` | `instructions/*.md` | Copy instruction files | **Manual include:** @@ -2165,29 +2165,29 @@ alwaysApply: {{true if no globs, false otherwise}} | Artifact Type | Copilot Location | Format | |---------------|-----------------|--------| | skill | `.github/skills//SKILL.md` | SKILL.md (as-is, Copilot supports this) | -| agent | `.github/copilot-instructions.md` (appended section) | Markdown section | +| agent | `.github/agents/.agent.md` | Discrete markdown file | | prompt | `.github/prompts/.md` | Stored as markdown | -| instruction | `.github/copilot-instructions.md` (appended section) | Markdown section | +| instruction | `.github/instructions/.instructions.md` | Discrete markdown file | -**Agent → Copilot conversion:** +**Agent deployment:** -Agents and instructions are merged into `.github/copilot-instructions.md` as clearly delineated sections: +Each agent is deployed as a discrete `.agent.md` file in `.github/agents/`, following Copilot's custom agents convention: -```markdown - -# ASVC Compliance Auditor +``` +.github/agents/ +├── asvc-audit.agent.md +└── code-reviewer.agent.md +``` -{{system-prompt.md contents}} - +**Instruction deployment:** - -# ASVC Coding Standards +Each instruction is deployed as a discrete `.instructions.md` file in `.github/instructions/`, supporting conditional application via glob patterns: -{{instruction contents}} - ``` - -AAM uses the `` / `` markers to manage its own sections without disturbing user-written content. +.github/instructions/ +├── python-standards.instructions.md +└── typescript-standards.instructions.md +``` ### 8.3 Claude Adapter @@ -2196,21 +2196,33 @@ AAM uses the `` / `` markers to manage its own | Artifact Type | Claude Location | Format | |---------------|----------------|--------| | skill | `.claude/skills//SKILL.md` | SKILL.md (as-is) | -| agent | `CLAUDE.md` (appended section) | Markdown section | +| agent | `.claude/agents/.md` | Discrete markdown file | | prompt | `.claude/prompts/.md` | Stored as markdown | | instruction | `CLAUDE.md` (appended section) | Markdown section | -**Conversion approach:** +**Agent deployment:** -Same marker-based merging as Copilot, but targeting `CLAUDE.md`: +Each agent is deployed as a discrete `.md` file in `.claude/agents/`, following Claude Code's subagents convention: + +``` +.claude/agents/ +├── asvc-audit.md +└── code-reviewer.md +``` + +**Instruction deployment:** + +Instructions are merged into `CLAUDE.md` using marker-based sections: ```markdown - -# ASVC Compliance Auditor -{{system-prompt.md contents}} - + +# Python Coding Standards +{{instruction contents}} + ``` +AAM uses the `` / `` markers to manage its own sections without disturbing user-written content in `CLAUDE.md`. + ### 8.4 Codex (OpenAI) Adapter **Deployment mapping:** @@ -2234,8 +2246,8 @@ flowchart TB Package --> CodexAdapter["Codex Adapter"] CursorAdapter --> CursorFiles[".cursor/
rules/, skills/, prompts/"] - CopilotAdapter --> CopilotFiles[".github/
copilot-instructions.md
prompts/"] - ClaudeAdapter --> ClaudeFiles["CLAUDE.md
.claude/skills/, prompts/"] + CopilotAdapter --> CopilotFiles[".github/
agents/, instructions/
skills/, prompts/"] + ClaudeAdapter --> ClaudeFiles["CLAUDE.md
.claude/agents/, skills/, prompts/"] CodexAdapter --> CodexFiles["AGENTS.md
~/.codex/skills/"] style Package fill:#e1f5fe @@ -2378,9 +2390,12 @@ my-project/ │ └── prompts/ │ ├── audit-finding.md │ └── audit-summary.md -├── CLAUDE.md # Deployed Claude artifacts (sections) +├── CLAUDE.md # Deployed Claude instructions (sections) +├── .claude/ +│ └── agents/ # Deployed Claude agents └── .github/ - └── copilot-instructions.md # Deployed Copilot artifacts (sections) + ├── agents/ # Deployed Copilot agents + └── instructions/ # Deployed Copilot instructions ``` ### 10.3 What Gets Committed to Git diff --git a/docs/user_docs/docs/concepts/platform-adapters.md b/docs/user_docs/docs/concepts/platform-adapters.md index 8864b9d..7a56798 100644 --- a/docs/user_docs/docs/concepts/platform-adapters.md +++ b/docs/user_docs/docs/concepts/platform-adapters.md @@ -207,16 +207,16 @@ my-project/ ## GitHub Copilot Adapter -**GitHub Copilot** is GitHub's AI pair programmer. It uses a single `copilot-instructions.md` file for instructions. +**GitHub Copilot** is GitHub's AI pair programmer. It uses `.github/agents/` for custom agents and `.github/instructions/` for conditional instruction files. ### Deployment Mapping | Artifact Type | Copilot Location | Format | Merging | |---------------|-----------------|--------|---------| | **Skill** | `.github/skills//SKILL.md` | SKILL.md (as-is) | No | -| **Agent** | `.github/copilot-instructions.md` | Markdown section | Yes (markers) | +| **Agent** | `.github/agents/.agent.md` | Markdown file | No | | **Prompt** | `.github/prompts/.md` | Markdown (as-is) | No | -| **Instruction** | `.github/copilot-instructions.md` | Markdown section | Yes (markers) | +| **Instruction** | `.github/instructions/.instructions.md` | Markdown file | No | ### Configuration @@ -224,13 +224,9 @@ my-project/ # aam.yaml platforms: copilot: - merge_instructions: true # Merge into copilot-instructions.md + enabled: true ``` -| Option | Values | Default | Description | -|--------|--------|---------|-------------| -| `merge_instructions` | `true`, `false` | `true` | Merge agents/instructions into single file | - ### Skill Deployment Skills are copied to `.github/skills/`: @@ -244,60 +240,35 @@ Skills are copied to `.github/skills/`: └── SKILL.md ``` -### Agent and Instruction Deployment (Merged) - -Agents and instructions are merged into `.github/copilot-instructions.md` using markers: - -```markdown - -# My Project Instructions - -This is my project. Follow these rules... - - -# ASVC Compliance Auditor - -You are an ASVC compliance auditor. Your role is to analyze codebases, -configurations, and documentation against ASVC framework requirements. - -## Core Responsibilities -- Identify compliance gaps against ASVC standards -- Generate structured audit findings -... - - - -# Python Coding Standards +### Agent Deployment -- Use type hints on all functions -- Follow PEP 8 style guide -... - +Agents are deployed as discrete `.agent.md` files in `.github/agents/`: - +``` +.github/agents/ +├── asvc-audit.agent.md +└── code-reviewer.agent.md ``` -### Marker-Based Merging - -AAM uses `` and `` markers to: - -1. **Identify AAM-managed sections** — Only edit content within markers -2. **Preserve user content** — Never touch content outside markers -3. **Update cleanly** — Replace marker content on re-deploy -4. **Remove cleanly** — Remove marked sections on undeploy +### Instruction Deployment -**Benefits:** +Instructions are deployed as discrete `.instructions.md` files in `.github/instructions/`: -- Users can add their own instructions alongside AAM-managed ones -- AAM updates don't interfere with user content -- Clear boundaries between managed and manual content +``` +.github/instructions/ +├── python-standards.instructions.md +└── typescript-standards.instructions.md +``` ### Copilot Directory Structure After Deploy ``` my-project/ ├── .github/ -│ ├── copilot-instructions.md # Merged agents + instructions +│ ├── agents/ +│ │ └── asvc-audit.agent.md +│ ├── instructions/ +│ │ └── python-standards.instructions.md │ ├── skills/ │ │ └── author--asvc-report/ │ └── prompts/ @@ -309,14 +280,14 @@ my-project/ ## Claude Adapter -**Claude** is Anthropic's AI assistant. Projects use `CLAUDE.md` for instructions. +**Claude** is Anthropic's AI assistant. Projects use `CLAUDE.md` for instructions and `.claude/agents/` for custom subagents. ### Deployment Mapping | Artifact Type | Claude Location | Format | Merging | |---------------|----------------|--------|---------| | **Skill** | `.claude/skills//SKILL.md` | SKILL.md (as-is) | No | -| **Agent** | `CLAUDE.md` | Markdown section | Yes (markers) | +| **Agent** | `.claude/agents/.md` | Markdown file | No | | **Prompt** | `.claude/prompts/.md` | Markdown (as-is) | No | | **Instruction** | `CLAUDE.md` | Markdown section | Yes (markers) | @@ -326,12 +297,12 @@ my-project/ # aam.yaml platforms: claude: - merge_instructions: true # Merge into CLAUDE.md + merge_instructions: true # Merge instructions into CLAUDE.md ``` | Option | Values | Default | Description | |--------|--------|---------|-------------| -| `merge_instructions` | `true`, `false` | `true` | Merge agents/instructions into CLAUDE.md | +| `merge_instructions` | `true`, `false` | `true` | Merge instructions into CLAUDE.md | ### Skill Deployment @@ -344,21 +315,25 @@ Skills are copied to `.claude/skills/`: └── scripts/ ``` -### Agent and Instruction Deployment (Merged) +### Agent Deployment + +Agents are deployed as discrete `.md` files in `.claude/agents/`: + +``` +.claude/agents/ +├── asvc-audit.md +└── code-reviewer.md +``` + +### Instruction Deployment (Merged) -Similar to Copilot, agents and instructions merge into `CLAUDE.md`: +Instructions are merged into `CLAUDE.md` using markers: ```markdown # Project: ASVC Compliance Tool This project implements ASVC compliance auditing... - -# ASVC Compliance Auditor - -You are an ASVC compliance auditor... - - # Python Coding Standards @@ -370,8 +345,10 @@ You are an ASVC compliance auditor... ``` my-project/ -├── CLAUDE.md # Merged agents + instructions +├── CLAUDE.md # Instructions (marker-based) ├── .claude/ +│ ├── agents/ +│ │ └── asvc-audit.md │ ├── skills/ │ │ └── author--asvc-report/ │ └── prompts/ @@ -472,8 +449,8 @@ graph TB Package --> CodexAdapter[Codex Adapter] CursorAdapter --> CursorDeploy[".cursor/
skills/, rules/, prompts/"] - CopilotAdapter --> CopilotDeploy[".github/
copilot-instructions.md
skills/, prompts/"] - ClaudeAdapter --> ClaudeDeploy["CLAUDE.md
.claude/skills/, prompts/"] + CopilotAdapter --> CopilotDeploy[".github/
agents/, instructions/
skills/, prompts/"] + ClaudeAdapter --> ClaudeDeploy["CLAUDE.md
.claude/agents/, skills/, prompts/"] CodexAdapter --> CodexDeploy["AGENTS.md
~/.codex/skills/, prompts/"] style Package fill:#e1f5fe @@ -495,11 +472,11 @@ graph TB |---------|--------|---------|--------|-------| | **Skill format** | SKILL.md | SKILL.md | SKILL.md | SKILL.md (native) | | **Skill location** | `.cursor/skills/` | `.github/skills/` | `.claude/skills/` | `~/.codex/skills/` | -| **Agent format** | `.mdc` rule | Merged section | Merged section | Merged section | -| **Agent location** | `.cursor/rules/` | `copilot-instructions.md` | `CLAUDE.md` | `AGENTS.md` | -| **Instruction format** | `.mdc` rule | Merged section | Merged section | Merged section | -| **Instruction location** | `.cursor/rules/` | `copilot-instructions.md` | `CLAUDE.md` | `AGENTS.md` | -| **Merging strategy** | Separate files | Marker-based | Marker-based | Marker-based | +| **Agent format** | `.mdc` rule | `.agent.md` file | `.md` file | Merged section | +| **Agent location** | `.cursor/rules/` | `.github/agents/` | `.claude/agents/` | `AGENTS.md` | +| **Instruction format** | `.mdc` rule | `.instructions.md` file | Merged section | Merged section | +| **Instruction location** | `.cursor/rules/` | `.github/instructions/` | `CLAUDE.md` | `AGENTS.md` | +| **Merging strategy** | Separate files | Separate files | Marker-based (instructions) | Marker-based | --- @@ -522,12 +499,11 @@ ls -R .cursor/ ### Copilot ```bash -# Check merged file -cat .github/copilot-instructions.md | grep "BEGIN AAM" +# List deployed agents +ls .github/agents/ -# Expected: -# -# +# List deployed instructions +ls .github/instructions/ # List skills ls .github/skills/ @@ -536,7 +512,10 @@ ls .github/skills/ ### Claude ```bash -# Check merged file +# List deployed agents +ls .claude/agents/ + +# Check instruction markers in CLAUDE.md cat CLAUDE.md | grep "BEGIN AAM" # List skills @@ -575,9 +554,9 @@ aam install @author/asvc-auditor # Deploys to: # - .cursor/skills/author--asvc-report/ # - .cursor/rules/agent-author--asvc-audit.mdc -# - CLAUDE.md (merged section) +# - .claude/agents/asvc-audit.md # - .claude/skills/author--asvc-report/ -# - .github/copilot-instructions.md (merged section) +# - .github/agents/asvc-audit.agent.md # - .github/skills/author--asvc-report/ ``` @@ -612,8 +591,8 @@ aam undeploy asvc-auditor --platform cursor | Platform | Removal Behavior | |----------|------------------| | **Cursor** | Delete skill dirs, delete rule files, delete prompt files | -| **Copilot** | Remove marked sections from `copilot-instructions.md`, delete skills/prompts | -| **Claude** | Remove marked sections from `CLAUDE.md`, delete skills/prompts | +| **Copilot** | Delete agent/instruction/skill/prompt files from `.github/` | +| **Claude** | Delete agent files from `.claude/agents/`, remove instruction markers from `CLAUDE.md`, delete skills/prompts | | **Codex** | Remove marked sections from `AGENTS.md`, delete skills/prompts | --- diff --git a/docs/user_docs/docs/platforms/claude.md b/docs/user_docs/docs/platforms/claude.md index 4ebf0aa..2889e91 100644 --- a/docs/user_docs/docs/platforms/claude.md +++ b/docs/user_docs/docs/platforms/claude.md @@ -2,13 +2,13 @@ ## Overview -**Claude Desktop** is Anthropic's AI assistant application. Projects using Claude Desktop use a `CLAUDE.md` file for project-specific instructions. AAM integrates with Claude by merging agents and instructions into `CLAUDE.md` using marker-based sections, while deploying skills and prompts to the `.claude/` directory. +**Claude Desktop** is Anthropic's AI assistant application. Projects using Claude Desktop use a `CLAUDE.md` file for project-specific instructions and a `.claude/agents/` directory for custom subagents. AAM integrates with Claude by deploying agents as discrete files in `.claude/agents/`, merging instructions into `CLAUDE.md` using marker-based sections, and deploying skills and prompts to the `.claude/` directory. **Key features:** -- Marker-based merging into `CLAUDE.md` +- Discrete agent files in `.claude/agents/` +- Marker-based merging of instructions into `CLAUDE.md` - Preserves user-written content outside markers - Native SKILL.md support -- Clean separation of AAM-managed vs manual content - Project-based instruction organization ## Deployment Mapping @@ -16,7 +16,7 @@ | Artifact Type | Claude Location | Format | Merging | |---------------|----------------|--------|---------| | **Skill** | `.claude/skills//SKILL.md` | SKILL.md (as-is) | No | -| **Agent** | `CLAUDE.md` | Markdown section | Yes (markers) | +| **Agent** | `.claude/agents/.md` | Markdown file | No | | **Prompt** | `.claude/prompts/.md` | Markdown (as-is) | No | | **Instruction** | `CLAUDE.md` | Markdown section | Yes (markers) | @@ -56,7 +56,7 @@ Skills are copied as-is to `.claude/skills/`. The entire skill directory structu ### Agents -Agents are merged into `CLAUDE.md` as markdown sections with AAM markers. The system prompt content is included directly between markers. +Agents are deployed as discrete `.md` files in `.claude/agents/`. Each agent gets its own file following Claude Code's [custom subagents convention](https://code.claude.com/docs/en/sub-agents). **Source** (`agents/asvc-audit/`): @@ -86,12 +86,9 @@ configurations, and documentation against ASVC framework requirements. - Provide remediation recommendations ``` -**Merged into `CLAUDE.md`:** +**Deployed to** `.claude/agents/asvc-audit.md`: ```markdown - -# ASVC Compliance Auditor - You are an ASVC compliance auditor. Your role is to analyze codebases, configurations, and documentation against ASVC framework requirements. @@ -100,25 +97,14 @@ configurations, and documentation against ASVC framework requirements. - Identify compliance gaps against ASVC standards - Generate structured audit findings - Provide remediation recommendations - -## Available Skills - -- asvc-report: Generate ASVC audit reports -- generic-auditor: General-purpose code auditing - -## Available Prompts - -- audit-finding: Use for documenting individual findings -- audit-summary: Use for executive summaries - ``` **Conversion rules:** -1. Content wrapped in `` and `` markers -2. Marker includes artifact name and type (e.g., `asvc-audit agent`) -3. System prompt content included directly -4. Skills and prompts listed as references -5. No YAML frontmatter in merged content +1. Each agent is a separate `.md` file in `.claude/agents/` +2. System prompt content written directly to the file +3. File naming: `.md` +4. Re-deploying overwrites the existing agent file +5. `CLAUDE.md` is not modified by agent deployments ### Prompts @@ -296,12 +282,14 @@ platforms: | Option | Values | Default | Description | |--------|--------|---------|-------------| -| `merge_instructions` | `true`, `false` | `true` | Whether to merge agents/instructions into CLAUDE.md | +| `merge_instructions` | `true`, `false` | `true` | Whether to merge instructions into CLAUDE.md | **merge_instructions:** -- `true`: Agents and instructions merge into `CLAUDE.md` (recommended) -- `false`: Would deploy as separate files (not typical for Claude) +- `true`: Instructions merge into `CLAUDE.md` (recommended) +- `false`: Would deploy instructions as separate files (not typical for Claude) + +> **Note:** Agents are always deployed as discrete files in `.claude/agents/` regardless of this setting. ## Installation Example @@ -331,36 +319,13 @@ aam install @author/asvc-auditor ### After Installation -**CLAUDE.md** (created or updated): +**CLAUDE.md** (instructions merged): ```markdown # My Project This is my custom project description... - -# ASVC Compliance Auditor - -You are an ASVC compliance auditor. Your role is to analyze codebases, -configurations, and documentation against ASVC framework requirements. - -## Core Responsibilities - -- Identify compliance gaps against ASVC standards -- Generate structured audit findings -- Provide remediation recommendations - -## Available Skills - -- asvc-report: Generate ASVC audit reports -- generic-auditor: General-purpose code auditing - -## Available Prompts - -- audit-finding: Use for documenting individual findings -- audit-summary: Use for executive summaries - - # Python Coding Standards @@ -381,8 +346,10 @@ configurations, and documentation against ASVC framework requirements. ``` my-project/ -├── CLAUDE.md # Merged agents + instructions +├── CLAUDE.md # Instructions (marker-based) ├── .claude/ +│ ├── agents/ +│ │ └── asvc-audit.md # Agent file │ ├── skills/ │ │ ├── author--asvc-report/ │ │ │ ├── SKILL.md @@ -412,7 +379,7 @@ Downloaded 2 packages (145 KB) Deployed to claude: skill: author--asvc-report -> .claude/skills/author--asvc-report/ skill: author--generic-auditor -> .claude/skills/author--generic-auditor/ - agent: asvc-audit -> CLAUDE.md (merged) + agent: asvc-audit -> .claude/agents/asvc-audit.md prompt: audit-finding -> .claude/prompts/author--audit-finding.md prompt: audit-summary -> .claude/prompts/author--audit-summary.md instruction: python-standards -> CLAUDE.md (merged) @@ -424,15 +391,23 @@ Successfully installed @author/asvc-auditor@1.0.0 After deployment, verify that artifacts are correctly placed: +### Check Agents + +```bash +# List deployed agents +ls .claude/agents/ + +# Expected output: +# asvc-audit.md +``` + ### Check CLAUDE.md ```bash -# View CLAUDE.md +# View CLAUDE.md (instructions only) cat CLAUDE.md -# Should contain AAM markers: -# -# +# Should contain instruction markers: # # ``` @@ -440,7 +415,7 @@ cat CLAUDE.md ### Check Markers ```bash -# Find all AAM markers +# Find all AAM markers in CLAUDE.md grep "BEGIN AAM" CLAUDE.md # Expected output: @@ -479,7 +454,7 @@ ls .claude/prompts/ ## Tips & Best Practices -### Preserve User Content +### Preserve User Content in CLAUDE.md **Always write your own content outside AAM markers:** @@ -488,9 +463,9 @@ ls .claude/prompts/ - + ...AAM-managed content... - + ``` @@ -509,15 +484,6 @@ Structure your `CLAUDE.md` logically: ## Overview Project description... -## AAM Agents - -... - - - -... - - ## AAM Instructions ... @@ -537,20 +503,13 @@ aam install @author/code-reviewer aam install @author/doc-writer ``` -Each agent gets its own marked section in `CLAUDE.md`: +Each agent gets its own file in `.claude/agents/`: -```markdown - -... - - - -... - - - -... - +``` +.claude/agents/ +├── asvc-audit.md +├── code-reviewer.md +└── doc-writer.md ``` ### Skill References @@ -577,7 +536,7 @@ AAM should never touch content outside markers, but it's good practice. **Symptom:** `aam install` succeeds but `CLAUDE.md` doesn't exist. -**Cause:** No agents or instructions in the package. +**Cause:** No instructions in the package (agents go to `.claude/agents/`, not `CLAUDE.md`). **Solution:** @@ -586,7 +545,8 @@ Check what was deployed: ```bash aam list -# If only skills/prompts are deployed, CLAUDE.md won't be created +# If only skills/prompts/agents are deployed, CLAUDE.md won't be created +# CLAUDE.md is only created when instructions are deployed ``` ### Markers Appear in Claude Output @@ -599,7 +559,7 @@ aam list ### Content Between Markers Disappears -**Symptom:** Manual edits inside AAM markers are lost. +**Symptom:** Manual edits inside AAM instruction markers are lost. **Cause:** AAM overwrites content between markers on deployment. @@ -610,16 +570,16 @@ Move your custom content outside AAM markers: ```markdown - + ...AAM content... - + ``` ### Duplicate Markers -**Symptom:** Multiple `` sections. +**Symptom:** Multiple `` sections. **Cause:** Manual duplication or AAM deployment bug. @@ -657,19 +617,25 @@ nano CLAUDE.md ### Cannot Undeploy -**Symptom:** `aam undeploy` fails to remove markers. +**Symptom:** `aam undeploy` fails to remove an artifact. -**Cause:** Markers manually edited or deleted. +**Cause:** File or markers manually edited or deleted. **Solution:** -Manually remove AAM sections from `CLAUDE.md`: +For agents, manually delete the file: + +```bash +rm .claude/agents/artifact-name.md +``` + +For instructions, manually remove AAM sections from `CLAUDE.md`: ```bash # Edit CLAUDE.md and remove: -# +# # ...content... -# +# ``` ## Next Steps diff --git a/docs/user_docs/docs/platforms/copilot.md b/docs/user_docs/docs/platforms/copilot.md index f539264..46d290f 100644 --- a/docs/user_docs/docs/platforms/copilot.md +++ b/docs/user_docs/docs/platforms/copilot.md @@ -2,23 +2,23 @@ ## Overview -**GitHub Copilot** is GitHub's AI pair programming tool. It uses `.github/copilot-instructions.md` for project-specific instructions and coding guidelines. AAM integrates with Copilot by merging agents and instructions into `copilot-instructions.md` using marker-based sections, while deploying skills and prompts to the `.github/` directory. +**GitHub Copilot** is GitHub's AI pair programming tool. It supports custom agents via `.github/agents/*.agent.md`, conditional instructions via `.github/instructions/*.instructions.md`, and reusable prompts via `.github/prompts/`. AAM integrates with Copilot by deploying discrete files into the `.github/` directory structure. **Key features:** -- Marker-based merging into `copilot-instructions.md` -- Preserves user-written content outside markers +- Discrete agent files in `.github/agents/` +- Conditional instruction files in `.github/instructions/` - SKILL.md support in `.github/skills/` - GitHub-native directory structure -- Clean separation of AAM-managed vs manual content +- Prompt files in `.github/prompts/` ## Deployment Mapping | Artifact Type | Copilot Location | Format | Merging | |---------------|-----------------|--------|---------| | **Skill** | `.github/skills//SKILL.md` | SKILL.md (as-is) | No | -| **Agent** | `.github/copilot-instructions.md` | Markdown section | Yes (markers) | +| **Agent** | `.github/agents/.agent.md` | Markdown file | No | | **Prompt** | `.github/prompts/.md` | Markdown (as-is) | No | -| **Instruction** | `.github/copilot-instructions.md` | Markdown section | Yes (markers) | +| **Instruction** | `.github/instructions/.instructions.md` | Markdown file | No | > **Note:** `` uses the double-hyphen convention for scoped packages: `@author/name` becomes `author--name`. @@ -58,7 +58,7 @@ Skills are copied as-is to `.github/skills/`. The entire skill directory structu ### Agents -Agents are merged into `.github/copilot-instructions.md` as markdown sections with AAM markers. The system prompt content is included directly between markers. +Agents are deployed as discrete `.agent.md` files in `.github/agents/`. Each agent gets its own file following Copilot's [custom agents convention](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents). **Source** (`agents/asvc-audit/`): @@ -88,12 +88,9 @@ configurations, and documentation against ASVC framework requirements. - Provide remediation recommendations ``` -**Merged into `.github/copilot-instructions.md`:** +**Deployed to** `.github/agents/asvc-audit.agent.md`: ```markdown - -# ASVC Compliance Auditor - You are an ASVC compliance auditor. Your role is to analyze codebases, configurations, and documentation against ASVC framework requirements. @@ -102,25 +99,13 @@ configurations, and documentation against ASVC framework requirements. - Identify compliance gaps against ASVC standards - Generate structured audit findings - Provide remediation recommendations - -## Available Skills - -- asvc-report: Generate ASVC audit reports -- generic-auditor: General-purpose code auditing - -## Available Prompts - -- audit-finding: Use for documenting individual findings -- audit-summary: Use for executive summaries - ``` **Conversion rules:** -1. Content wrapped in `` and `` markers -2. Marker includes artifact name and type (e.g., `asvc-audit agent`) -3. System prompt content included directly -4. Skills and prompts listed as references -5. No YAML frontmatter in merged content +1. Each agent is a separate `.agent.md` file in `.github/agents/` +2. System prompt content written directly to the file +3. File naming: `.agent.md` +4. Re-deploying overwrites the existing agent file ### Prompts @@ -174,7 +159,7 @@ description: "Template for documenting audit findings" ### Instructions -Instructions are merged into `.github/copilot-instructions.md` as markdown sections with AAM markers. +Instructions are deployed as discrete `.instructions.md` files in `.github/instructions/`. This follows Copilot's [custom instructions convention](https://code.visualstudio.com/docs/copilot/customization/custom-instructions), which supports conditional application via glob patterns. **Source** (`instructions/python-standards.md`): @@ -204,10 +189,15 @@ scope: project - Use Google-style docstring format ``` -**Merged into `.github/copilot-instructions.md`:** +**Deployed to** `.github/instructions/python-standards.instructions.md`: ```markdown - +--- +name: python-standards +description: "Python coding standards" +scope: project +--- + # Python Coding Standards ## Type Hints @@ -225,68 +215,45 @@ scope: project - Docstrings for all public functions - Use Google-style docstring format - ``` -## Marker-Based Merging +## File-Based Deployment -AAM uses HTML comment markers to manage sections in `copilot-instructions.md`: +AAM deploys agents and instructions as discrete files in the `.github/` directory: -```markdown - -...content... - -``` +- **Agents:** `.github/agents/.agent.md` +- **Instructions:** `.github/instructions/.instructions.md` ### How It Works -1. **First deployment:** If `copilot-instructions.md` doesn't exist, AAM creates it with AAM sections -2. **Subsequent deployments:** AAM finds existing markers and updates only the content between them -3. **User content preserved:** Any content outside AAM markers is never modified -4. **Undeploy:** AAM removes the entire marked section, including markers - -### Example copilot-instructions.md - -```markdown -# Coding Guidelines for This Project - -Our team follows these standards when writing code... - -## General Principles - -- Write clean, readable code -- Test thoroughly -- Document complex logic - - -# ASVC Compliance Auditor - -You are an ASVC compliance auditor... - - -## Project-Specific Context - -This is a compliance auditing tool built for enterprise clients... - - -# Python Coding Standards +1. **First deployment:** AAM creates the target directory and writes the file +2. **Subsequent deployments:** AAM overwrites the existing file with updated content +3. **Undeploy:** AAM deletes the file -- Use type hints on all functions... - +### Example Directory Structure -## Additional Resources - -- [Internal wiki](https://wiki.example.com) -- [Architecture docs](./docs/architecture.md) +``` +.github/ +├── agents/ +│ └── asvc-audit.agent.md +├── instructions/ +│ └── python-standards.instructions.md +├── skills/ +│ └── author--asvc-report/ +│ └── SKILL.md +├── prompts/ +│ └── author--audit-finding.md +└── workflows/ # Existing GitHub Actions (untouched) + └── ci.yml ``` ### Benefits -- **Coexistence:** AAM-managed and user-written content live together -- **Clean updates:** Re-deploying updates only AAM sections -- **Clear boundaries:** Easy to see what AAM manages vs what you wrote -- **Safe removal:** Undeploying removes only AAM sections -- **GitHub integration:** Lives in `.github/` with other GitHub configs +- **Discrete files:** Each agent and instruction is a separate file +- **Conditional instructions:** `.instructions.md` files support glob-based conditional application +- **GitHub-native:** Follows official Copilot directory conventions +- **Clean updates:** Re-deploying overwrites only the specific file +- **Easy management:** Standard file operations for adding/removing artifacts ## Platform-Specific Configuration @@ -295,19 +262,14 @@ This is a compliance auditing tool built for enterprise clients... platforms: copilot: - merge_instructions: true # Merge into copilot-instructions.md (default) + enabled: true ``` ### Configuration Options | Option | Values | Default | Description | |--------|--------|---------|-------------| -| `merge_instructions` | `true`, `false` | `true` | Whether to merge agents/instructions into copilot-instructions.md | - -**merge_instructions:** - -- `true`: Agents and instructions merge into `copilot-instructions.md` (recommended) -- `false`: Would deploy as separate files (not typical for Copilot) +| `enabled` | `true`, `false` | `true` | Whether to deploy artifacts for Copilot | ## Installation Example @@ -316,10 +278,6 @@ Let's install the `@author/asvc-auditor` package and see how it deploys to GitHu ### Before Installation ```bash -# Check if copilot-instructions.md exists -cat .github/copilot-instructions.md -# File might not exist or contains only user content - # Check .github/ directory ls -R .github/ # Directory might not exist or contains only GitHub workflows @@ -337,58 +295,15 @@ aam install @author/asvc-auditor ### After Installation -**.github/copilot-instructions.md** (created or updated): - -```markdown -# Project Instructions - -This project implements ASVC compliance auditing... - - -# ASVC Compliance Auditor - -You are an ASVC compliance auditor. Your role is to analyze codebases, -configurations, and documentation against ASVC framework requirements. - -## Core Responsibilities - -- Identify compliance gaps against ASVC standards -- Generate structured audit findings -- Provide remediation recommendations - -## Available Skills - -- asvc-report: Generate ASVC audit reports -- generic-auditor: General-purpose code auditing - -## Available Prompts - -- audit-finding: Use for documenting individual findings -- audit-summary: Use for executive summaries - - - -# Python Coding Standards - -## Type Hints - -- Use type hints on all functions -- Import from `typing` for complex types - -## Style - -- Follow PEP 8 style guide -- Use 4 spaces for indentation -- Maximum line length: 88 characters - -``` - **Directory structure:** ``` my-project/ ├── .github/ -│ ├── copilot-instructions.md # Merged agents + instructions +│ ├── agents/ +│ │ └── asvc-audit.agent.md # Agent definition +│ ├── instructions/ +│ │ └── python-standards.instructions.md # Instruction file │ ├── skills/ │ │ ├── author--asvc-report/ │ │ │ ├── SKILL.md @@ -420,10 +335,10 @@ Downloaded 2 packages (145 KB) Deployed to copilot: skill: author--asvc-report -> .github/skills/author--asvc-report/ skill: author--generic-auditor -> .github/skills/author--generic-auditor/ - agent: asvc-audit -> .github/copilot-instructions.md (merged) + agent: asvc-audit -> .github/agents/asvc-audit.agent.md prompt: audit-finding -> .github/prompts/author--audit-finding.md prompt: audit-summary -> .github/prompts/author--audit-summary.md - instruction: python-standards -> .github/copilot-instructions.md (merged) + instruction: python-standards -> .github/instructions/python-standards.instructions.md Successfully installed @author/asvc-auditor@1.0.0 ``` @@ -432,28 +347,24 @@ Successfully installed @author/asvc-auditor@1.0.0 After deployment, verify that artifacts are correctly placed: -### Check copilot-instructions.md +### Check Agents ```bash -# View copilot-instructions.md -cat .github/copilot-instructions.md - -# Should contain AAM markers: -# -# -# -# +# List deployed agents +ls .github/agents/ + +# Expected output: +# asvc-audit.agent.md ``` -### Check Markers +### Check Instructions ```bash -# Find all AAM markers -grep "BEGIN AAM" .github/copilot-instructions.md +# List deployed instructions +ls .github/instructions/ # Expected output: -# -# +# python-standards.instructions.md ``` ### Check Skills @@ -482,38 +393,21 @@ ls .github/prompts/ 1. Open project in VS Code or your IDE 2. Ensure GitHub Copilot extension is installed -3. Copilot automatically reads `.github/copilot-instructions.md` +3. Copilot automatically reads `.github/agents/` and `.github/instructions/` 4. Test Copilot suggestions reflect the deployed instructions ## Tips & Best Practices -### Preserve User Content - -**Always write your own content outside AAM markers:** - -```markdown -# Our Team's Coding Guidelines - - - - -...AAM-managed content... - - - -``` - -**Never edit content between markers:** - -AAM will overwrite any manual changes inside markers on the next deployment. - ### GitHub Integration Copilot's `.github/` directory coexists with other GitHub features: ``` .github/ -├── copilot-instructions.md # AAM-managed + custom instructions +├── agents/ # AAM-deployed agents +│ └── asvc-audit.agent.md +├── instructions/ # AAM-deployed instructions +│ └── python-standards.instructions.md ├── skills/ # AAM-deployed skills ├── prompts/ # AAM-deployed prompts ├── workflows/ # GitHub Actions (unrelated to AAM) @@ -523,39 +417,7 @@ Copilot's `.github/` directory coexists with other GitHub features: └── pull_request_template.md # PR templates (unrelated) ``` -AAM only touches `copilot-instructions.md`, `skills/`, and `prompts/`. - -### Organize copilot-instructions.md - -Structure your instructions logically: - -```markdown -# Project Instructions - -## Overview -Project description and general guidelines... - -## AAM Agents - -... - - - -... - - -## AAM Coding Standards - -... - - - -... - - -## Team-Specific Guidelines -Your custom guidelines that aren't managed by AAM... -``` +AAM only touches `agents/`, `instructions/`, `skills/`, and `prompts/`. ### Multiple Instruction Sets @@ -567,59 +429,27 @@ aam install @standards/typescript aam install @standards/rust ``` -Each gets its own marked section in `copilot-instructions.md`: +Each gets its own file in `.github/instructions/`: -```markdown - -...Python guidelines... - - - -...TypeScript guidelines... - - - -...Rust guidelines... - +``` +.github/instructions/ +├── python-standards.instructions.md +├── typescript-standards.instructions.md +└── rust-standards.instructions.md ``` ### Copilot Instruction Processing -GitHub Copilot reads `copilot-instructions.md` and uses it to guide code suggestions. Instructions are most effective when: +GitHub Copilot reads `.github/instructions/` files and uses them to guide code suggestions. Instructions are most effective when: - **Clear and specific:** Concrete rules work better than vague guidelines - **Well-structured:** Use headings to organize by topic - **Example-driven:** Include code examples where appropriate - **Focused:** Instructions should be relevant to your project - -### Backup Before Major Changes - -Before significant updates, backup your instructions: - -```bash -cp .github/copilot-instructions.md .github/copilot-instructions.md.backup -``` - -AAM should never touch content outside markers, but it's good practice. +- **Conditionally scoped:** Use `applyTo` frontmatter for file-type-specific instructions ## Troubleshooting -### copilot-instructions.md Not Created - -**Symptom:** `aam install` succeeds but `copilot-instructions.md` doesn't exist. - -**Cause:** No agents or instructions in the package. - -**Solution:** - -Check what was deployed: - -```bash -aam list - -# If only skills/prompts are deployed, copilot-instructions.md won't be created -``` - ### Copilot Not Following Instructions **Symptom:** Copilot suggestions don't reflect deployed instructions. @@ -627,7 +457,7 @@ aam list **Possible causes:** 1. **Copilot cache:** Copilot might cache instructions -2. **Content location:** Instructions must be in `.github/copilot-instructions.md` +2. **Content location:** Instructions must be in `.github/instructions/` 3. **Instruction clarity:** Vague instructions are harder for Copilot to follow **Solutions:** @@ -636,9 +466,10 @@ aam list - Close and reopen VS Code - Or restart the Copilot extension -2. **Verify file location:** +2. **Verify file locations:** ```bash - ls -la .github/copilot-instructions.md + ls -la .github/agents/ + ls -la .github/instructions/ ``` 3. **Check instruction clarity:** @@ -646,60 +477,17 @@ aam list - Use concrete examples - Focus on actionable guidelines -### Markers Appear in Copilot Context - -**Symptom:** HTML comments visible in Copilot's context window. - -**Cause:** This is expected - HTML comments are standard markdown. - -**Solution:** Copilot typically ignores HTML comments. If they're affecting behavior, it's a Copilot issue, not AAM. - -### Content Between Markers Disappears - -**Symptom:** Manual edits inside AAM markers are lost. - -**Cause:** AAM overwrites content between markers on deployment. - -**Solution:** - -Move your custom content outside AAM markers: - -```markdown - - - -...AAM content... - - - -``` - -### Duplicate Markers - -**Symptom:** Multiple `` sections. - -**Cause:** Manual duplication or AAM deployment bug. - -**Solution:** - -Manually remove duplicate sections, keeping only one: - -```bash -# Edit copilot-instructions.md and remove duplicate marker pairs -code .github/copilot-instructions.md -``` - ### Skills Not Recognized **Symptom:** Skills in `.github/skills/` not available in Copilot. **Cause:** GitHub Copilot's skill support is experimental and may not be fully functional. -**Note:** As of early 2024, Copilot's SKILL.md support is limited. AAM deploys skills to `.github/skills/` for future compatibility, but Copilot may not use them yet. +**Note:** Copilot's SKILL.md support is limited. AAM deploys skills to `.github/skills/` for future compatibility, but Copilot may not use them yet. **Workaround:** -Reference skills in `copilot-instructions.md`: +Reference skills in an instruction file in `.github/instructions/`: ```markdown ## Available Skills @@ -713,20 +501,20 @@ Skills are located in `.github/skills/`: ### Cannot Undeploy -**Symptom:** `aam undeploy` fails to remove markers. +**Symptom:** `aam undeploy` fails to remove an agent or instruction. -**Cause:** Markers manually edited or deleted. +**Cause:** File may have been manually renamed or deleted. **Solution:** -Manually remove AAM sections from `copilot-instructions.md`: +Manually delete the file: ```bash -# Edit and remove: -# -# ...content... -# -code .github/copilot-instructions.md +# Remove agent file +rm .github/agents/artifact-name.agent.md + +# Remove instruction file +rm .github/instructions/artifact-name.instructions.md ``` ## Next Steps From 532d30b20a1502809b849ae35ebe466febcb1d40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:44:50 +0000 Subject: [PATCH 8/9] Initial plan From e4aab3869b09ce367aee6b16f1a55bd7ddccc2a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:51:29 +0000 Subject: [PATCH 9/9] Fix review comments: frontmatter parsing, verbose workaround case, doc paths and patterns Co-authored-by: spazyCZ <22116740+spazyCZ@users.noreply.github.com> --- apps/aam-cli/src/aam_cli/commands/convert.py | 8 ++++++- .../src/aam_cli/converters/frontmatter.py | 21 ++++++++++++++----- docs/DESIGN.md | 1 + docs/user_docs/docs/mcp/index.md | 2 +- docs/user_docs/docs/platforms/copilot.md | 2 +- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/apps/aam-cli/src/aam_cli/commands/convert.py b/apps/aam-cli/src/aam_cli/commands/convert.py index c3e92c9..f77f5a0 100644 --- a/apps/aam-cli/src/aam_cli/commands/convert.py +++ b/apps/aam-cli/src/aam_cli/commands/convert.py @@ -218,6 +218,12 @@ def _print_verbose_workaround(console: Console, warning: str) -> None: warning_lower = warning.lower() for key, workaround in VERBOSE_WORKAROUNDS.items(): - if key.replace("_", " ").replace("removed", "").strip() in warning_lower: + normalized_key = ( + key.replace("_", " ") + .replace("removed", "") + .strip() + .lower() + ) + if normalized_key and normalized_key in warning_lower: console.print(f" [dim]{workaround}[/dim]") return diff --git a/apps/aam-cli/src/aam_cli/converters/frontmatter.py b/apps/aam-cli/src/aam_cli/converters/frontmatter.py index c6e126e..0fe71fc 100644 --- a/apps/aam-cli/src/aam_cli/converters/frontmatter.py +++ b/apps/aam-cli/src/aam_cli/converters/frontmatter.py @@ -53,13 +53,24 @@ def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]: if not stripped.startswith("---"): return {}, text - # Find the closing --- - end_idx = stripped.find("---", 3) - if end_idx == -1: + # Split into lines and find the closing '---' on its own line + lines = stripped.splitlines(keepends=True) + if not lines: return {}, text - yaml_block = stripped[3:end_idx].strip() - body = stripped[end_idx + 3:].lstrip("\n") + closing_idx: int | None = None + for i, line in enumerate(lines[1:], start=1): + if line.strip() == "---": + closing_idx = i + break + + if closing_idx is None: + return {}, text + + # YAML block is everything between the opening and closing delimiters + yaml_block = "".join(lines[1:closing_idx]).strip() + # Body is everything after the closing delimiter + body = "".join(lines[closing_idx + 1:]).lstrip("\n") try: frontmatter = yaml.safe_load(yaml_block) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index fb7a3df..f7e14e2 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -791,6 +791,7 @@ Detection patterns: ─ instructions/*.md (AAM convention) ─ .cursor/rules/*.mdc (Cursor rules, excluding agent-* rules) ─ CLAUDE.md (Claude instructions) + ─ .github/copilot-instructions.md (Copilot legacy instructions) ─ .github/instructions/*.instructions.md (Copilot instructions) ─ AGENTS.md (Codex instructions) ``` diff --git a/docs/user_docs/docs/mcp/index.md b/docs/user_docs/docs/mcp/index.md index e1cec0a..47411cd 100644 --- a/docs/user_docs/docs/mcp/index.md +++ b/docs/user_docs/docs/mcp/index.md @@ -13,7 +13,7 @@ flowchart LR IDE["IDE / AI Agent
(Cursor, Claude Desktop, etc.)"] -->|MCP protocol| AAM["AAM MCP Server
(aam mcp serve)"] AAM -->|reads/writes| Config["~/.aam/config.yaml"] AAM -->|manages| Packages["~/.aam/packages/"] - AAM -->|clones/fetches| Sources["~/.aam/sources-cache/"] + AAM -->|clones/fetches| Sources["~/.aam/cache/git/"] style IDE fill:#e3f2fd style AAM fill:#f3e5f5 diff --git a/docs/user_docs/docs/platforms/copilot.md b/docs/user_docs/docs/platforms/copilot.md index 46d290f..3e8dc50 100644 --- a/docs/user_docs/docs/platforms/copilot.md +++ b/docs/user_docs/docs/platforms/copilot.md @@ -195,7 +195,7 @@ scope: project --- name: python-standards description: "Python coding standards" -scope: project +applyTo: "**" --- # Python Coding Standards