diff --git a/.github/ISSUE_TEMPLATE/agent_request.yml b/.github/ISSUE_TEMPLATE/agent_request.yml index 05b77a117a..731487e311 100644 --- a/.github/ISSUE_TEMPLATE/agent_request.yml +++ b/.github/ISSUE_TEMPLATE/agent_request.yml @@ -8,7 +8,7 @@ body: value: | Thanks for requesting a new agent! Before submitting, please check if the agent is already supported. - **Currently supported agents**: Alquimia AI, Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Command Code, Cursor, Devin for Terminal, Docker Agent, Factory Droid, DeepSeek Harness, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Grok Build, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, Muse Code, Oh My Pi, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, ZCode, Zed + **Currently supported agents**: Alquimia AI, Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Command Code, Cursor, Devin for Terminal, Docker Agent, Factory Droid, DeepSeek Harness, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Grok Build, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, MiniMax Code, Mistral Vibe, Muse Code, Oh My Pi, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, ZCode, Zed - type: input id: agent-name diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9f1ec2abb..f80f9fe88d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -89,6 +89,7 @@ body: - Kimi Code - Kiro CLI - Lingma + - MiniMax Code - Mistral Vibe - Muse Code - Oh My Pi diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 3555be2841..171eedcb67 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -83,6 +83,7 @@ body: - Kimi Code - Kiro CLI - Lingma + - MiniMax Code - Mistral Vibe - Muse Code - Oh My Pi diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 122f7f180b..6b9bb863f2 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -33,6 +33,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; installs into `.kimi-code/skills/`. `--migrate-legacy` moves old `.kimi/skills/` installs to the new paths | | [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Kiro CLI does not substitute `$ARGUMENTS` in file-based prompts, so Spec Kit ships a prose fallback at render time (see [Manage prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) and issue [#1926](https://github.com/github/spec-kit/issues/1926)). Alias: `--integration kiro` | | [Lingma](https://lingma.aliyun.com/) | `lingma` | Skills-based integration; skills are installed automatically | +| [MiniMax Code](https://github.com/MiniMax-AI/minimax-code) | `mcode` | Skills-based integration; installs skills into `.minimax/skills` and invokes them as `/speckit-` | | [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | `vibe` | | | [Muse Code](https://dev.meta.ai/docs/muse-code) | `muse` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-` | | [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent) | `omp` | Installs slash commands into `.omp/commands` | diff --git a/examples/bundles/business-analyst/bundle.yml b/examples/bundles/business-analyst/bundle.yml index 7019d65e6b..58bd95bd5e 100644 --- a/examples/bundles/business-analyst/bundle.yml +++ b/examples/bundles/business-analyst/bundle.yml @@ -17,7 +17,7 @@ requires: provides: extensions: - id: "agent-context" - version: "1.0.1" + version: "1.0.2" presets: - id: "requirements-elicitation" version: "1.0.0" diff --git a/examples/bundles/developer/bundle.yml b/examples/bundles/developer/bundle.yml index b6d5c23811..95864f3e25 100644 --- a/examples/bundles/developer/bundle.yml +++ b/examples/bundles/developer/bundle.yml @@ -17,7 +17,7 @@ requires: provides: extensions: - id: "agent-context" - version: "1.0.1" + version: "1.0.2" presets: - id: "implementation-planning" version: "1.0.0" diff --git a/examples/bundles/product-manager/bundle.yml b/examples/bundles/product-manager/bundle.yml index 859195dc2a..b2e88c72c4 100644 --- a/examples/bundles/product-manager/bundle.yml +++ b/examples/bundles/product-manager/bundle.yml @@ -19,7 +19,7 @@ requires: provides: extensions: - id: "agent-context" - version: "1.0.1" + version: "1.0.2" presets: - id: "product-discovery" version: "1.0.0" diff --git a/examples/bundles/security-researcher/bundle.yml b/examples/bundles/security-researcher/bundle.yml index 7be968b847..5c6acceed9 100644 --- a/examples/bundles/security-researcher/bundle.yml +++ b/examples/bundles/security-researcher/bundle.yml @@ -17,7 +17,7 @@ requires: provides: extensions: - id: "agent-context" - version: "1.0.1" + version: "1.0.2" presets: - id: "security-compliance" version: "1.0.0" diff --git a/extensions/agent-context/agent-context-defaults.json b/extensions/agent-context/agent-context-defaults.json index 8c5f781ab9..a00b1d5276 100644 --- a/extensions/agent-context/agent-context-defaults.json +++ b/extensions/agent-context/agent-context-defaults.json @@ -25,6 +25,7 @@ "kimi": "AGENTS.md", "kiro-cli": "AGENTS.md", "lingma": ".lingma/rules/specify-rules.md", + "mcode": "AGENTS.md", "muse": "AGENTS.md", "omp": "AGENTS.md", "opencode": "AGENTS.md", diff --git a/extensions/agent-context/extension.yml b/extensions/agent-context/extension.yml index 080265c92e..7f9e9867ab 100644 --- a/extensions/agent-context/extension.yml +++ b/extensions/agent-context/extension.yml @@ -3,7 +3,7 @@ schema_version: "1.0" extension: id: agent-context name: "Coding Agent Context" - version: "1.0.1" + version: "1.0.2" description: "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers" author: spec-kit-core repository: https://github.com/github/spec-kit diff --git a/extensions/catalog.json b/extensions/catalog.json index 94f3336a5f..d99fa9c60c 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -6,7 +6,7 @@ "agent-context": { "name": "Coding Agent Context", "id": "agent-context", - "version": "1.0.1", + "version": "1.0.2", "description": "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers", "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", diff --git a/integrations/catalog.json b/integrations/catalog.json index 06349fa616..21848f8a09 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -371,6 +371,15 @@ "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", "tags": ["cli", "skills"] + }, + "mcode": { + "id": "mcode", + "name": "MiniMax Code", + "version": "1.0.0", + "description": "MiniMax Code CLI skills-based integration", + "author": "spec-kit-core", + "repository": "https://github.com/github/spec-kit", + "tags": ["cli", "skills"] } } } diff --git a/src/specify_cli/_invocation_style.py b/src/specify_cli/_invocation_style.py index bd01a9f7bb..c2b7856399 100644 --- a/src/specify_cli/_invocation_style.py +++ b/src/specify_cli/_invocation_style.py @@ -13,7 +13,7 @@ # Agents that always render /speckit-, regardless of ai_skills. ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset( - {"devin", "droid", "dsh", "grok", "muse", "qodercli", "trae", "zed"} + {"devin", "droid", "dsh", "grok", "mcode", "muse", "qodercli", "trae", "zed"} ) # Agents that render /speckit- only when ai_skills is enabled. diff --git a/src/specify_cli/command_init.py b/src/specify_cli/command_init.py index 459551b36c..c2630f0ef8 100644 --- a/src/specify_cli/command_init.py +++ b/src/specify_cli/command_init.py @@ -1010,6 +1010,7 @@ def init( zcode_skill_mode = selected_ai == "zcode" and _is_skills_integration claude_skill_mode = selected_ai == "claude" and _is_skills_integration kimi_skill_mode = selected_ai == "kimi" + mcode_skill_mode = selected_ai == "mcode" and _is_skills_integration agy_skill_mode = selected_ai == "agy" and _is_skills_integration trae_skill_mode = selected_ai == "trae" cursor_agent_skill_mode = ( @@ -1029,6 +1030,7 @@ def init( or zcode_skill_mode or claude_skill_mode or kimi_skill_mode + or mcode_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode @@ -1091,6 +1093,11 @@ def init( f"{step_num}. Start Bob in this project directory; spec-kit skills were installed to [cyan].bob/skills[/cyan]" ) step_num += 1 + if mcode_skill_mode: + steps_lines.append( + f"{step_num}. Start MiniMax Code in this project directory; spec-kit skills were installed to [cyan].minimax/skills[/cyan]" + ) + step_num += 1 usage_label = "skills" if native_skill_mode else "slash commands" from ._invocation_style import ( diff --git a/src/specify_cli/integrations/__init__.py b/src/specify_cli/integrations/__init__.py index 9f8dbd66fd..225c15cb75 100644 --- a/src/specify_cli/integrations/__init__.py +++ b/src/specify_cli/integrations/__init__.py @@ -89,6 +89,7 @@ def _register_builtins() -> None: from .kimi import KimiIntegration from .kiro_cli import KiroCliIntegration from .lingma import LingmaIntegration + from .mcode import McodeIntegration from .muse import MuseIntegration from .omp import OmpIntegration from .opencode import OpencodeIntegration @@ -132,6 +133,7 @@ def _register_builtins() -> None: _register(KimiIntegration()) _register(KiroCliIntegration()) _register(LingmaIntegration()) + _register(McodeIntegration()) _register(MuseIntegration()) _register(OmpIntegration()) _register(OpencodeIntegration()) diff --git a/src/specify_cli/integrations/mcode/__init__.py b/src/specify_cli/integrations/mcode/__init__.py new file mode 100644 index 0000000000..167e602cba --- /dev/null +++ b/src/specify_cli/integrations/mcode/__init__.py @@ -0,0 +1,71 @@ +"""MiniMax Code integration — skills-based agent. + +MiniMax Code discovers project skills from +``.minimax/skills/speckit-/SKILL.md`` and invokes them via their +slash shortcut (``/speckit-``). + +See: https://github.com/MiniMax-AI/minimax-code +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +from ..base import IntegrationOption, SkillsIntegration + + +class McodeIntegration(SkillsIntegration): + """Integration for MiniMax Code CLI.""" + + key = "mcode" + config = { + "name": "MiniMax Code", + "folder": ".minimax/", + "commands_subdir": "skills", + "install_url": "https://github.com/MiniMax-AI/minimax-code", + "requires_cli": True, + } + registrar_config = { + "dir": ".minimax/skills", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": "/SKILL.md", + } + multi_install_safe = True + + @classmethod + def options(cls) -> list[IntegrationOption]: + opts = super().options() + opts.append( + IntegrationOption( + "--skills", + is_flag=True, + default=True, + help="Install as agent skills (default for MiniMax Code)", + ) + ) + return opts + + def build_exec_args( + self, + prompt: str, + *, + model: str | None = None, + output_json: bool = True, + integration_args: Sequence[str] | None = None, + integration_options: Mapping[str, Any] | None = None, + project_root: Path | None = None, + ) -> list[str] | None: + self.validate_runtime_config( + integration_args, + integration_options, + ) + args = [self._resolve_executable(), "exec", prompt] + self._apply_extra_args_env_var(args) + if model: + args.extend(["--model", model]) + if output_json: + args.extend(["--output-format", "json"]) + return args diff --git a/tests/extensions/test_update_agent_context_python_parity.py b/tests/extensions/test_update_agent_context_python_parity.py index 06015bbdc7..b1676f79d1 100644 --- a/tests/extensions/test_update_agent_context_python_parity.py +++ b/tests/extensions/test_update_agent_context_python_parity.py @@ -459,13 +459,29 @@ def test_python_empty_config_matching_bash(tmp_path: Path) -> None: assert "context_files/context_file not set" in py.stderr -@requires_posix_bash -def test_python_self_seed_from_init_options_matching_bash(tmp_path: Path) -> None: +@pytest.mark.parametrize( + "run_shell", + [ + pytest.param(run_bash, id="bash", marks=requires_posix_bash), + pytest.param( + run_powershell, + id="powershell", + marks=pytest.mark.skipif(not POWERSHELL, reason="no PowerShell available"), + ), + ], +) +@pytest.mark.parametrize( + ("integration", "context_file"), + [("claude", "CLAUDE.md"), ("mcode", "AGENTS.md")], +) +def test_python_self_seed_from_init_options_matching_shell( + tmp_path: Path, run_shell, integration: str, context_file: str +) -> None: repo_a, repo_b = twin_projects(tmp_path) for repo in (repo_a, repo_b): add_plan(repo) (repo / ".specify" / "init-options.json").write_text( - json.dumps({"integration": "claude"}), encoding="utf-8" + json.dumps({"integration": integration}), encoding="utf-8" ) shutil.copy( EXT_DIR / "agent-context-defaults.json", @@ -476,11 +492,16 @@ def test_python_self_seed_from_init_options_matching_bash(tmp_path: Path) -> Non / "agent-context-defaults.json", ) - bash = run_bash(repo_a) + shell = run_shell(repo_a) py = run_python(repo_b) - assert_parity(bash, py, repo_a, repo_b) - assert (repo_a / "CLAUDE.md").read_bytes() == (repo_b / "CLAUDE.md").read_bytes() + assert shell.returncode == py.returncode == 0, shell.stderr + py.stderr + if run_shell is run_bash: + assert_parity(shell, py, repo_a, repo_b) + content = (repo_b / context_file).read_bytes() + assert (repo_a / context_file).read_bytes() == content + assert b"" in content + assert b"specs/001-demo/plan.md" in content @requires_posix_bash diff --git a/tests/integrations/test_integration_mcode.py b/tests/integrations/test_integration_mcode.py new file mode 100644 index 0000000000..bc8c53b81a --- /dev/null +++ b/tests/integrations/test_integration_mcode.py @@ -0,0 +1,109 @@ +"""Tests for McodeIntegration — skills-based integration (MiniMax Code).""" + +from .test_integration_base_skills import SkillsIntegrationTests + + +class TestMcodeIntegration(SkillsIntegrationTests): + KEY = "mcode" + FOLDER = ".minimax/" + COMMANDS_SUBDIR = "skills" + REGISTRAR_DIR = ".minimax/skills" + + +class TestMcodeMetadata: + """MiniMax Code-specific metadata and headless dispatch.""" + + def test_display_name(self): + from specify_cli.integrations import get_integration + + assert get_integration("mcode").config["name"] == "MiniMax Code" + + def test_requires_cli(self): + from specify_cli.integrations import get_integration + + assert get_integration("mcode").config["requires_cli"] is True + + def test_install_url_points_to_github(self): + from specify_cli.integrations import get_integration + + assert ( + get_integration("mcode").config["install_url"] + == "https://github.com/MiniMax-AI/minimax-code" + ) + + def test_multi_install_safe(self): + from specify_cli.integrations import get_integration + + assert get_integration("mcode").multi_install_safe is True + + def test_build_exec_args_uses_mcode_exec(self): + from specify_cli.integrations import get_integration + + args = get_integration("mcode").build_exec_args("do the thing") + assert args[:3] == ["mcode", "exec", "do the thing"] + assert "--output-format" in args + assert args[args.index("--output-format") + 1] == "json" + + def test_build_exec_args_model_flag(self): + from specify_cli.integrations import get_integration + + args = get_integration("mcode").build_exec_args( + "do the thing", model="custom_provider:ark/deepseek-v4-flash", output_json=False + ) + assert "--model" in args + assert args[args.index("--model") + 1] == "custom_provider:ark/deepseek-v4-flash" + assert "--output-format" not in args + + def test_build_exec_args_extra_args_env_var(self, monkeypatch): + monkeypatch.setenv( + "SPECKIT_INTEGRATION_MCODE_EXTRA_ARGS", "--verbose --timeout 60" + ) + from specify_cli.integrations import get_integration + + args = get_integration("mcode").build_exec_args("do the thing") + assert "--verbose" in args + assert "--timeout" in args + assert args[args.index("--timeout") + 1] == "60" + + def test_options_declares_skills_flag(self): + from specify_cli.integrations import get_integration + + opts = get_integration("mcode").options() + skills_opts = [o for o in opts if o.name == "--skills"] + assert len(skills_opts) == 1 + assert skills_opts[0].is_flag is True + assert skills_opts[0].default is True + + def test_next_steps_show_slash_skill_invocation(self, tmp_path): + """MiniMax Code next-steps guidance should display /speckit-* usage.""" + import os + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / "mcode-next-steps" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + runner = CliRunner() + result = runner.invoke( + app, + [ + "init", + "--here", + "--integration", + "mcode", + "--ignore-agent-tools", + "--script", + "sh", + ], + catch_exceptions=False, + ) + finally: + os.chdir(old_cwd) + + assert result.exit_code == 0 + assert "/speckit-constitution" in result.output + assert "/speckit.constitution" not in result.output + assert "MiniMax Code" in result.output + assert ".minimax/skills" in result.output diff --git a/tests/integrations/test_integration_zed.py b/tests/integrations/test_integration_zed.py index 1a55c9ae87..468d91daa6 100644 --- a/tests/integrations/test_integration_zed.py +++ b/tests/integrations/test_integration_zed.py @@ -143,6 +143,8 @@ def _render_invocation(project_path, ai: str, ai_skills: bool) -> str: ("devin", False, "/speckit-plan"), ("grok", True, "/speckit-plan"), ("grok", False, "/speckit-plan"), + ("mcode", True, "/speckit-plan"), + ("mcode", False, "/speckit-plan"), ("qodercli", True, "/speckit-plan"), ("qodercli", False, "/speckit-plan"), ("trae", True, "/speckit-plan"), diff --git a/tests/integrations/test_registry.py b/tests/integrations/test_registry.py index 590b9edf92..c7cb4d2f3d 100644 --- a/tests/integrations/test_registry.py +++ b/tests/integrations/test_registry.py @@ -28,7 +28,7 @@ "gemini", "tabnine", # Stage 5 — skills, generic & option-driven integrations "codex", "kimi", "agy", "zed", "generic", - "droid", "command-code", "dsh", "muse", + "droid", "command-code", "dsh", "muse", "mcode", ] diff --git a/tests/specify_cli/bundles/test_references.py b/tests/specify_cli/bundles/test_references.py index a020d64a9d..334f0ad87e 100644 --- a/tests/specify_cli/bundles/test_references.py +++ b/tests/specify_cli/bundles/test_references.py @@ -7,6 +7,8 @@ from pathlib import Path +import pytest + from specify_cli.bundles.manifest import ComponentRef from specify_cli.bundles.references import make_reference_checker from tests.specify_cli.bundles.helpers import make_project @@ -16,6 +18,14 @@ def _ref(kind: str, id_: str) -> ComponentRef: return ComponentRef(kind=kind, id=id_, version="1.0.0") +@pytest.fixture +def empty_step_catalog(monkeypatch): + """Make online step lookups definitive without fetching remote catalogs.""" + from specify_cli.workflows.catalog import StepCatalog + + monkeypatch.setattr(StepCatalog, "_get_merged_steps", lambda self: {}) + + def test_bundled_extension_resolves(tmp_path: Path): root = make_project(tmp_path) warnings: list[str] = [] @@ -46,7 +56,9 @@ def test_builtin_step_type_resolves(tmp_path: Path): assert warnings == [] -def test_community_step_is_not_treated_as_bundled(tmp_path: Path): +def test_community_step_is_not_treated_as_bundled( + tmp_path: Path, empty_step_catalog +): """A community step loaded for one project must not resolve for another. `load_custom_steps` adds project-installed ids to the process-global @@ -85,7 +97,7 @@ def execute(self, config, context): # pragma: no cover - never run STEP_REGISTRY.pop("community-only-step", None) -def test_unknown_step_type_still_errors_online(tmp_path: Path): +def test_unknown_step_type_still_errors_online(tmp_path: Path, empty_step_catalog): """The guard must not make every step id resolve.""" root = make_project(tmp_path) warnings: list[str] = [] diff --git a/tests/test_agent_config_consistency.py b/tests/test_agent_config_consistency.py index 6ca4de9c6d..294f4eed27 100644 --- a/tests/test_agent_config_consistency.py +++ b/tests/test_agent_config_consistency.py @@ -40,6 +40,7 @@ "kimi", "kiro-cli", "lingma", + "mcode", "vibe", "muse", "omp", @@ -442,3 +443,27 @@ def test_rovodev_in_extension_registrar(self): def test_agent_config_includes_rovodev(self): """AGENT_CONFIG should include rovodev.""" assert "rovodev" in AGENT_CONFIG + + # --- MiniMax Code consistency checks --- + + def test_mcode_in_agent_config(self): + """AGENT_CONFIG should include mcode with skills-based scaffold metadata.""" + assert "mcode" in AGENT_CONFIG + assert AGENT_CONFIG["mcode"]["folder"] == ".minimax/" + assert AGENT_CONFIG["mcode"]["commands_subdir"] == "skills" + assert AGENT_CONFIG["mcode"]["requires_cli"] is True + + def test_mcode_in_extension_registrar(self): + """CommandRegistrar.AGENT_CONFIGS should include mcode skill scaffold metadata.""" + cfg = CommandRegistrar.AGENT_CONFIGS + + assert "mcode" in cfg + mcode_cfg = cfg["mcode"] + assert mcode_cfg["dir"] == ".minimax/skills" + assert mcode_cfg["format"] == "markdown" + assert mcode_cfg["args"] == "$ARGUMENTS" + assert mcode_cfg["extension"] == "/SKILL.md" + + def test_agent_config_includes_mcode(self): + """AGENT_CONFIG should include mcode.""" + assert "mcode" in AGENT_CONFIG