From c500dd6478d8f6f46a5f895c0be9da10973e5385 Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Mon, 24 Aug 2026 21:57:18 +0300 Subject: [PATCH 1/7] feat(docker-agent): add Docker Agent integration Support both Docker Agent command forms and optional agent configuration through the integration environment variable. Register the skills-based integration and document its installation layout. Assisted-by: OpenAI ChatGPT (model: unknown, autonomous) Signed-off-by: Alexandr Zaytsev --- AGENTS.md | 2 + docs/reference/integrations.md | 1 + integrations/catalog.json | 9 ++ src/specify_cli/integrations/__init__.py | 2 + .../integrations/docker_agent/__init__.py | 67 +++++++++++++++ .../test_integration_docker_agent.py | 82 +++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 src/specify_cli/integrations/docker_agent/__init__.py create mode 100644 tests/integrations/test_integration_docker_agent.py diff --git a/AGENTS.md b/AGENTS.md index b6975339c3..0a6fd11b6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,8 @@ src/specify_cli/integrations/ │ └── __init__.py ├── copilot/ # Example: IntegrationBase subclass (custom setup) │ └── __init__.py +├── docker_agent/ # Example: Docker Agent SkillsIntegration subclass +│ └── __init__.py └── ... # One subpackage per supported agent ``` diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 57b079bcd1..088374315f 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -17,6 +17,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Command Code](https://commandcode.ai/docs) | `command-code` | Skills-based integration; installs skills into `.commandcode/skills/` and invokes them as `$speckit-` | | [Cursor](https://cursor.sh/) | `cursor-agent` | | | [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-` | +| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.docker-agent/skills/`. Initialize with `--ignore-agent-tools`; detects `docker-agent run` or `docker agent run`; pass the agent config through `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml` | | [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-` | | [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ | | [Forge](https://forgecode.dev/) | `forge` | | diff --git a/integrations/catalog.json b/integrations/catalog.json index f3f7a7fe7f..870de6b045 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -102,6 +102,15 @@ "repository": "https://github.com/github/spec-kit", "tags": ["cli", "skills"] }, + "docker-agent": { + "id": "docker-agent", + "name": "Docker Agent", + "version": "1.0.0", + "description": "Docker Agent skills-based integration", + "author": "spec-kit-core", + "repository": "https://github.com/github/spec-kit", + "tags": ["cli", "skills", "docker"] + }, "qwen": { "id": "qwen", "name": "Qwen Code", diff --git a/src/specify_cli/integrations/__init__.py b/src/specify_cli/integrations/__init__.py index 75c2f9d0de..041c27fec9 100644 --- a/src/specify_cli/integrations/__init__.py +++ b/src/specify_cli/integrations/__init__.py @@ -60,6 +60,7 @@ def _register_builtins() -> None: from .copilot import CopilotIntegration from .cursor_agent import CursorAgentIntegration from .devin import DevinIntegration + from .docker_agent import DockerAgentIntegration from .droid import DroidIntegration from .firebender import FirebenderIntegration from .forge import ForgeIntegration @@ -100,6 +101,7 @@ def _register_builtins() -> None: _register(CopilotIntegration()) _register(CursorAgentIntegration()) _register(DevinIntegration()) + _register(DockerAgentIntegration()) _register(DroidIntegration()) _register(FirebenderIntegration()) _register(ForgeIntegration()) diff --git a/src/specify_cli/integrations/docker_agent/__init__.py b/src/specify_cli/integrations/docker_agent/__init__.py new file mode 100644 index 0000000000..40cc7493ed --- /dev/null +++ b/src/specify_cli/integrations/docker_agent/__init__.py @@ -0,0 +1,67 @@ +"""Docker Agent integration — skills-based Docker CLI agent. + +Docker Agent discovers project skills from ``.docker-agent/skills``. Runtime +configuration is owned by Docker Agent and is not managed by Spec Kit. +""" + +from __future__ import annotations + +import shutil + +from ..base import SkillsIntegration + + +class DockerAgentIntegration(SkillsIntegration): + """Integration for Docker Agent.""" + + key = "docker-agent" + config = { + "name": "Docker Agent", + "folder": ".docker-agent/", + "commands_subdir": "skills", + "install_url": "https://docs.docker.com/ai/docker-agent/getting-started/installation/", + "requires_cli": True, + } + registrar_config = { + "dir": ".docker-agent/skills", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": "/SKILL.md", + } + multi_install_safe = True + + # Docker Agent hook names are lowercase snake_case and are configured in + # the agent team's YAML under ``agents..hooks``. + CANONICAL_TO_NATIVE = { + "session_start": "session_start", + "pre_tool_use": "pre_tool_use", + "post_tool_use": "post_tool_use", + "session_end": "session_end", + "user_prompt_submit": "user_prompt_submit", + "stop": "stop", + } + + + @staticmethod + def _agent_command() -> list[str]: + """Return the available Docker Agent command form.""" + if shutil.which("docker-agent"): + return ["docker-agent", "run"] + return ["docker", "agent", "run"] + + def build_exec_args( + self, + prompt: str, + *, + model: str | None = None, + output_json: bool = True, + ) -> list[str] | None: + """Build a headless Docker Agent invocation for workflow dispatch.""" + args = [*self._agent_command(), "--exec"] + if output_json: + args.append("--json") + self._apply_extra_args_env_var(args) + if model: + args.extend(["--model", model]) + args.append(prompt) + return args diff --git a/tests/integrations/test_integration_docker_agent.py b/tests/integrations/test_integration_docker_agent.py new file mode 100644 index 0000000000..14d28eb040 --- /dev/null +++ b/tests/integrations/test_integration_docker_agent.py @@ -0,0 +1,82 @@ +"""Tests for the Docker Agent integration.""" + + +from specify_cli.integrations import get_integration +from specify_cli.integrations.base import SkillsIntegration +from specify_cli.integrations.docker_agent import DockerAgentIntegration + + +def test_registered_metadata(): + integration = get_integration("docker-agent") + + assert isinstance(integration, DockerAgentIntegration) + assert isinstance(integration, SkillsIntegration) + assert integration.config["name"] == "Docker Agent" + assert integration.config["folder"] == ".docker-agent/" + assert integration.config["commands_subdir"] == "skills" + assert integration.config["requires_cli"] is True + assert integration.registrar_config["dir"] == ".docker-agent/skills" + assert integration.registrar_config["format"] == "markdown" + assert integration.registrar_config["args"] == "$ARGUMENTS" + assert integration.registrar_config["extension"] == "/SKILL.md" + assert integration.multi_install_safe is True + assert integration.CANONICAL_TO_NATIVE == { + "session_start": "session_start", + "pre_tool_use": "pre_tool_use", + "post_tool_use": "post_tool_use", + "session_end": "session_end", + "user_prompt_submit": "user_prompt_submit", + "stop": "stop", + } + + +def test_build_exec_args_without_config(monkeypatch): + monkeypatch.setattr("shutil.which", lambda name: None) + + args = DockerAgentIntegration().build_exec_args("/speckit-specify build an API") + + assert args == [ + "docker", + "agent", + "run", + "--exec", + "--json", + "/speckit-specify build an API", + ] + + +def test_uses_standalone_executable(monkeypatch): + monkeypatch.setattr( + "shutil.which", + lambda name: "/usr/bin/docker-agent" if name == "docker-agent" else None, + ) + + args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) + + assert args[:4] == ["docker-agent", "run", "--exec", "prompt"] + + +def test_standalone_executable_has_priority(monkeypatch): + monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/docker-agent") + + args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) + + assert args[:3] == ["docker-agent", "run", "--exec"] + + +def test_extra_args_can_supply_agent_config(monkeypatch): + monkeypatch.setattr("shutil.which", lambda name: None) + monkeypatch.setenv( + "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml" + ) + + args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) + + assert args == [ + "docker", + "agent", + "run", + "--exec", + "./agent.yaml", + "prompt", + ] From 0a13ed0a0518495884d650cc53ce10b7a8b23deb Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Tue, 25 Aug 2026 11:29:51 +0300 Subject: [PATCH 2/7] feat(docker-agent): address Copilot's feedback Add skills-based Docker Agent support using the shared `.agents/skills` layout, zero-config workflow dispatch, and automatic selection between `docker-agent` and `docker agent`. Keep co-installation opt-in because the shared skill manifests are not independently owned. Document Docker Agent setup and remove inert hook metadata; Docker Agent hooks remain configured in the agent-owned YAML file. Expand integration coverage with shared skills lifecycle tests and dispatch scenarios. Assisted-by: OpenAI ChatGPT (model: GPT-5.6 Luna, autonomous) Signed-off-by: Alexandr Zaytsev --- docs/reference/integrations.md | 3 +- .../integrations/docker_agent/__init__.py | 106 ++++++++++++++---- .../test_integration_docker_agent.py | 91 ++++++++------- 3 files changed, 132 insertions(+), 68 deletions(-) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 088374315f..829a0802a4 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -17,7 +17,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Command Code](https://commandcode.ai/docs) | `command-code` | Skills-based integration; installs skills into `.commandcode/skills/` and invokes them as `$speckit-` | | [Cursor](https://cursor.sh/) | `cursor-agent` | | | [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-` | -| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.docker-agent/skills/`. Initialize with `--ignore-agent-tools`; detects `docker-agent run` or `docker agent run`; pass the agent config through `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml` | +| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex). Initialize with `--ignore-agent-tools` because the integration key is not the Docker CLI executable; supports zero-config execution and detects `docker-agent run` or `docker agent run`. See the [zero-config Quick Start](https://docs.docker.com/ai/docker-agent/getting-started/quickstart/#option-a-run-the-default-agent) | | [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-` | | [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ | | [Forge](https://forgecode.dev/) | `forge` | | @@ -281,6 +281,7 @@ The currently declared multi-install safe integrations are: | `cline` | `.clinerules/workflows` | | `codebuddy` | `.codebuddy/commands` | | `codex` | `.agents/skills` | + | `command-code` | `.commandcode/skills` | | `cursor-agent` | `.cursor/skills` | | `droid` | `.factory/skills` | diff --git a/src/specify_cli/integrations/docker_agent/__init__.py b/src/specify_cli/integrations/docker_agent/__init__.py index 40cc7493ed..1539a762c5 100644 --- a/src/specify_cli/integrations/docker_agent/__init__.py +++ b/src/specify_cli/integrations/docker_agent/__init__.py @@ -1,14 +1,17 @@ """Docker Agent integration — skills-based Docker CLI agent. -Docker Agent discovers project skills from ``.docker-agent/skills``. Runtime +Docker Agent discovers project skills from ``.agents/skills``. Runtime configuration is owned by Docker Agent and is not managed by Spec Kit. """ from __future__ import annotations import shutil +import subprocess +from pathlib import Path +from typing import Any -from ..base import SkillsIntegration +from ..base import IntegrationOption, SkillsIntegration class DockerAgentIntegration(SkillsIntegration): @@ -17,38 +20,53 @@ class DockerAgentIntegration(SkillsIntegration): key = "docker-agent" config = { "name": "Docker Agent", - "folder": ".docker-agent/", + "folder": ".agents/", "commands_subdir": "skills", "install_url": "https://docs.docker.com/ai/docker-agent/getting-started/installation/", + # Docker Agent is exposed as either `docker-agent` or `docker agent`. + # The init command documents --ignore-agent-tools for the plugin form, + # because the generic preflight check looks up the integration key. "requires_cli": True, } registrar_config = { - "dir": ".docker-agent/skills", + "dir": ".agents/skills", "format": "markdown", "args": "$ARGUMENTS", "extension": "/SKILL.md", } - multi_install_safe = True - - # Docker Agent hook names are lowercase snake_case and are configured in - # the agent team's YAML under ``agents..hooks``. - CANONICAL_TO_NATIVE = { - "session_start": "session_start", - "pre_tool_use": "pre_tool_use", - "post_tool_use": "post_tool_use", - "session_end": "session_end", - "user_prompt_submit": "user_prompt_submit", - "stop": "stop", - } + # Docker Agent shares the ``.agents/skills`` layout with Codex and Zed. + # Keep co-installation opt-in until shared manifest ownership is supported. + multi_install_safe = False + # Docker Agent hooks are configured in the selected agent YAML under + # ``agents..hooks``. Spec Kit does not edit that user-owned file, so + # hooks are intentionally not exposed through the integration event system. - @staticmethod - def _agent_command() -> list[str]: + def _agent_command(self) -> list[str]: """Return the available Docker Agent command form.""" + executable = self._resolve_executable() + if executable != self.key: + if Path(executable).name in {"docker", "docker.exe"}: + return [executable, "agent", "run"] + return [executable, "run"] if shutil.which("docker-agent"): return ["docker-agent", "run"] return ["docker", "agent", "run"] + + @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 Docker Agent)", + ) + ) + return opts + def build_exec_args( self, prompt: str, @@ -57,11 +75,59 @@ def build_exec_args( output_json: bool = True, ) -> list[str] | None: """Build a headless Docker Agent invocation for workflow dispatch.""" + # The zero-config form is handled by dispatch_command(), which sends + # the prompt through stdin instead of using an agent-file position. args = [*self._agent_command(), "--exec"] if output_json: args.append("--json") - self._apply_extra_args_env_var(args) if model: args.extend(["--model", model]) - args.append(prompt) return args + + def dispatch_command( + self, + command_name: str, + args: str = "", + *, + project_root: Path | None = None, + model: str | None = None, + timeout: int = 600, + stream: bool = True, + ) -> dict[str, Any]: + """Dispatch a command, including Docker Agent's zero-config mode. + + Docker Agent's first positional argument is an agent file or registry + reference. With no extra arguments, send the Spec Kit prompt through + stdin instead of accidentally treating it as an agent reference. + """ + prompt = self.build_command_invocation(command_name, args) + exec_args = [*self._agent_command(), "--exec"] + if not stream: + exec_args.append("--json") + input_text: str | None = prompt + if model: + exec_args.extend(["--model", model]) + + resolved = shutil.which(exec_args[0]) + if resolved: + exec_args[0] = resolved + run_kwargs: dict[str, Any] = { + "text": True, + "cwd": str(project_root) if project_root else None, + "input": input_text, + } + if stream: + result = subprocess.run(exec_args, check=False, **run_kwargs) + return {"exit_code": result.returncode, "stdout": "", "stderr": ""} + result = subprocess.run( + exec_args, + capture_output=True, + timeout=timeout, + check=False, + **run_kwargs, + ) + return { + "exit_code": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + } diff --git a/tests/integrations/test_integration_docker_agent.py b/tests/integrations/test_integration_docker_agent.py index 14d28eb040..aa60f233b7 100644 --- a/tests/integrations/test_integration_docker_agent.py +++ b/tests/integrations/test_integration_docker_agent.py @@ -1,49 +1,46 @@ """Tests for the Docker Agent integration.""" - -from specify_cli.integrations import get_integration -from specify_cli.integrations.base import SkillsIntegration from specify_cli.integrations.docker_agent import DockerAgentIntegration +from .test_integration_base_skills import SkillsIntegrationTests + + +class TestDockerAgentIntegration(SkillsIntegrationTests): + KEY = "docker-agent" + FOLDER = ".agents/" + COMMANDS_SUBDIR = "skills" + REGISTRAR_DIR = ".agents/skills" -def test_registered_metadata(): - integration = get_integration("docker-agent") - - assert isinstance(integration, DockerAgentIntegration) - assert isinstance(integration, SkillsIntegration) - assert integration.config["name"] == "Docker Agent" - assert integration.config["folder"] == ".docker-agent/" - assert integration.config["commands_subdir"] == "skills" - assert integration.config["requires_cli"] is True - assert integration.registrar_config["dir"] == ".docker-agent/skills" - assert integration.registrar_config["format"] == "markdown" - assert integration.registrar_config["args"] == "$ARGUMENTS" - assert integration.registrar_config["extension"] == "/SKILL.md" - assert integration.multi_install_safe is True - assert integration.CANONICAL_TO_NATIVE == { - "session_start": "session_start", - "pre_tool_use": "pre_tool_use", - "post_tool_use": "post_tool_use", - "session_end": "session_end", - "user_prompt_submit": "user_prompt_submit", - "stop": "stop", - } - - -def test_build_exec_args_without_config(monkeypatch): + def test_multi_install_is_opt_in(self): + assert DockerAgentIntegration().multi_install_safe is False + + +def test_zero_config_dispatch_uses_stdin(monkeypatch, tmp_path): monkeypatch.setattr("shutil.which", lambda name: None) + completed = type("CompletedProcess", (), {"returncode": 0})() + captured = {} + + def fake_run(args, **kwargs): + captured["args"] = args + captured["kwargs"] = kwargs + return completed - args = DockerAgentIntegration().build_exec_args("/speckit-specify build an API") + monkeypatch.setattr( + "specify_cli.integrations.docker_agent.subprocess.run", fake_run + ) + + result = DockerAgentIntegration().dispatch_command( + "speckit.specify", "prompt", project_root=tmp_path + ) - assert args == [ + assert result["exit_code"] == 0 + assert captured["args"] == [ "docker", "agent", "run", "--exec", - "--json", - "/speckit-specify build an API", ] - + assert captured["kwargs"]["input"] == "/speckit-specify prompt" def test_uses_standalone_executable(monkeypatch): monkeypatch.setattr( @@ -53,30 +50,30 @@ def test_uses_standalone_executable(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args[:4] == ["docker-agent", "run", "--exec", "prompt"] - + assert args == ["docker-agent", "run", "--exec"] def test_standalone_executable_has_priority(monkeypatch): monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/docker-agent") args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args[:3] == ["docker-agent", "run", "--exec"] + assert args == ["docker-agent", "run", "--exec"] +def test_executable_override(monkeypatch): + monkeypatch.setenv( + "SPECKIT_INTEGRATION_DOCKER_AGENT_EXECUTABLE", "/opt/docker-agent" + ) -def test_extra_args_can_supply_agent_config(monkeypatch): - monkeypatch.setattr("shutil.which", lambda name: None) + args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) + + assert args == ["/opt/docker-agent", "run", "--exec"] + + +def test_docker_executable_override_uses_agent_subcommand(monkeypatch): monkeypatch.setenv( - "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml" + "SPECKIT_INTEGRATION_DOCKER_AGENT_EXECUTABLE", "/opt/docker" ) args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == [ - "docker", - "agent", - "run", - "--exec", - "./agent.yaml", - "prompt", - ] + assert args == ["/opt/docker", "agent", "run", "--exec"] From 444fd2bb7a0600ddd990efac82c37614fb23aec3 Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Tue, 25 Aug 2026 20:40:26 +0300 Subject: [PATCH 3/7] fix(docker-agent): pass prompts after agent configuration Require the agent source through the shared extra-arguments environment variable and append workflow prompts positionally instead of dispatching zero-config requests through stdin. Assisted-by: OpenAI ChatGPT (model: unknown, autonomous) Signed-off-by: Alexandr Zaytsev --- docs/reference/integrations.md | 3 +- .../integrations/docker_agent/__init__.py | 65 ++++--------------- .../test_integration_docker_agent.py | 52 ++++++++++----- 3 files changed, 47 insertions(+), 73 deletions(-) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 829a0802a4..fe9e5fb6a3 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -17,7 +17,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Command Code](https://commandcode.ai/docs) | `command-code` | Skills-based integration; installs skills into `.commandcode/skills/` and invokes them as `$speckit-` | | [Cursor](https://cursor.sh/) | `cursor-agent` | | | [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-` | -| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex). Initialize with `--ignore-agent-tools` because the integration key is not the Docker CLI executable; supports zero-config execution and detects `docker-agent run` or `docker agent run`. See the [zero-config Quick Start](https://docs.docker.com/ai/docker-agent/getting-started/quickstart/#option-a-run-the-default-agent) | +| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex). Configure workflow dispatch with `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml`; the Spec Kit prompt is passed after the configured agent source. Detects `docker-agent run` or `docker agent run`. Initialize with `--ignore-agent-tools` when only the Docker CLI plugin is available. | | [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-` | | [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ | | [Forge](https://forgecode.dev/) | `forge` | | @@ -281,7 +281,6 @@ The currently declared multi-install safe integrations are: | `cline` | `.clinerules/workflows` | | `codebuddy` | `.codebuddy/commands` | | `codex` | `.agents/skills` | - | `command-code` | `.commandcode/skills` | | `cursor-agent` | `.cursor/skills` | | `droid` | `.factory/skills` | diff --git a/src/specify_cli/integrations/docker_agent/__init__.py b/src/specify_cli/integrations/docker_agent/__init__.py index 1539a762c5..a33d484135 100644 --- a/src/specify_cli/integrations/docker_agent/__init__.py +++ b/src/specify_cli/integrations/docker_agent/__init__.py @@ -7,9 +7,7 @@ from __future__ import annotations import shutil -import subprocess from pathlib import Path -from typing import Any from ..base import IntegrationOption, SkillsIntegration @@ -44,7 +42,11 @@ class DockerAgentIntegration(SkillsIntegration): def _agent_command(self) -> list[str]: """Return the available Docker Agent command form.""" + + # The shared executable override supports both a standalone + # ``docker-agent`` binary and the Docker CLI plugin form. executable = self._resolve_executable() + if executable != self.key: if Path(executable).name in {"docker", "docker.exe"}: return [executable, "agent", "run"] @@ -74,60 +76,17 @@ def build_exec_args( model: str | None = None, output_json: bool = True, ) -> list[str] | None: - """Build a headless Docker Agent invocation for workflow dispatch.""" - # The zero-config form is handled by dispatch_command(), which sends - # the prompt through stdin instead of using an agent-file position. + """Build a headless Docker Agent invocation with an agent config.""" args = [*self._agent_command(), "--exec"] + + # Extra args carry the required agent source (for example + # ``./agent.yaml``) and any Docker Agent CLI flags. The shared helper + # also preserves shell-style quoting when splitting multiple args. + self._apply_extra_args_env_var(args) + + args.append(prompt) if output_json: args.append("--json") if model: args.extend(["--model", model]) return args - - def dispatch_command( - self, - command_name: str, - args: str = "", - *, - project_root: Path | None = None, - model: str | None = None, - timeout: int = 600, - stream: bool = True, - ) -> dict[str, Any]: - """Dispatch a command, including Docker Agent's zero-config mode. - - Docker Agent's first positional argument is an agent file or registry - reference. With no extra arguments, send the Spec Kit prompt through - stdin instead of accidentally treating it as an agent reference. - """ - prompt = self.build_command_invocation(command_name, args) - exec_args = [*self._agent_command(), "--exec"] - if not stream: - exec_args.append("--json") - input_text: str | None = prompt - if model: - exec_args.extend(["--model", model]) - - resolved = shutil.which(exec_args[0]) - if resolved: - exec_args[0] = resolved - run_kwargs: dict[str, Any] = { - "text": True, - "cwd": str(project_root) if project_root else None, - "input": input_text, - } - if stream: - result = subprocess.run(exec_args, check=False, **run_kwargs) - return {"exit_code": result.returncode, "stdout": "", "stderr": ""} - result = subprocess.run( - exec_args, - capture_output=True, - timeout=timeout, - check=False, - **run_kwargs, - ) - return { - "exit_code": result.returncode, - "stdout": result.stdout, - "stderr": result.stderr, - } diff --git a/tests/integrations/test_integration_docker_agent.py b/tests/integrations/test_integration_docker_agent.py index aa60f233b7..3584c0b000 100644 --- a/tests/integrations/test_integration_docker_agent.py +++ b/tests/integrations/test_integration_docker_agent.py @@ -15,32 +15,48 @@ def test_multi_install_is_opt_in(self): assert DockerAgentIntegration().multi_install_safe is False -def test_zero_config_dispatch_uses_stdin(monkeypatch, tmp_path): +def test_extra_args_are_applied_to_build_exec_args(monkeypatch): + monkeypatch.setenv( + "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", + "./agent.yaml --agent root --model openai/gpt-5", + ) monkeypatch.setattr("shutil.which", lambda name: None) - completed = type("CompletedProcess", (), {"returncode": 0})() - captured = {} - def fake_run(args, **kwargs): - captured["args"] = args - captured["kwargs"] = kwargs - return completed + args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - monkeypatch.setattr( - "specify_cli.integrations.docker_agent.subprocess.run", fake_run + assert args == [ + "docker", + "agent", + "run", + "--exec", + "./agent.yaml", + "--agent", + "root", + "--model", + "openai/gpt-5", + "prompt", + ] + + +def test_prompt_is_passed_after_agent_config(monkeypatch): + monkeypatch.setenv( + "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml" ) + monkeypatch.setattr("shutil.which", lambda name: None) - result = DockerAgentIntegration().dispatch_command( - "speckit.specify", "prompt", project_root=tmp_path + args = DockerAgentIntegration().build_exec_args( + "/speckit-specify prompt", output_json=False ) - assert result["exit_code"] == 0 - assert captured["args"] == [ + assert args == [ "docker", "agent", "run", "--exec", + "./agent.yaml", + "/speckit-specify prompt", ] - assert captured["kwargs"]["input"] == "/speckit-specify prompt" + def test_uses_standalone_executable(monkeypatch): monkeypatch.setattr( @@ -50,14 +66,14 @@ def test_uses_standalone_executable(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["docker-agent", "run", "--exec"] + assert args == ["docker-agent", "run", "--exec", "prompt"] def test_standalone_executable_has_priority(monkeypatch): monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/docker-agent") args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["docker-agent", "run", "--exec"] + assert args == ["docker-agent", "run", "--exec", "prompt"] def test_executable_override(monkeypatch): monkeypatch.setenv( @@ -66,7 +82,7 @@ def test_executable_override(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["/opt/docker-agent", "run", "--exec"] + assert args == ["/opt/docker-agent", "run", "--exec", "prompt"] def test_docker_executable_override_uses_agent_subcommand(monkeypatch): @@ -76,4 +92,4 @@ def test_docker_executable_override_uses_agent_subcommand(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["/opt/docker", "agent", "run", "--exec"] + assert args == ["/opt/docker", "agent", "run", "--exec", "prompt"] From 3409a065f1ae64a9a335852a1da0c571823d7cb8 Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Wed, 26 Aug 2026 21:20:37 +0300 Subject: [PATCH 4/7] fix(utils): detect Docker Agent CLI plugin Allow tool checks to recognize Docker Agent installations provided through the Docker CLI plugin, not only the standalone binary. Assisted-by: OpenAI (model: GPT-5.6 Luna, autonomous) Signed-off-by: Alexandr Zaytsev --- docs/reference/integrations.md | 2 +- src/specify_cli/_utils.py | 7 +++++++ tests/test_check_tool.py | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index fe9e5fb6a3..ad297cffb3 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -17,7 +17,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Command Code](https://commandcode.ai/docs) | `command-code` | Skills-based integration; installs skills into `.commandcode/skills/` and invokes them as `$speckit-` | | [Cursor](https://cursor.sh/) | `cursor-agent` | | | [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-` | -| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex). Configure workflow dispatch with `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml`; the Spec Kit prompt is passed after the configured agent source. Detects `docker-agent run` or `docker agent run`. Initialize with `--ignore-agent-tools` when only the Docker CLI plugin is available. | +| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex and Zed). Detects either the standalone `docker-agent` binary or the Docker CLI plugin (`docker agent`). Configure workflow dispatch with `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml`; the Spec Kit prompt is appended after these arguments. Not multi-install safe by default because the skills directory is shared. | | [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-` | | [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ | | [Forge](https://forgecode.dev/) | `forge` | | diff --git a/src/specify_cli/_utils.py b/src/specify_cli/_utils.py index 0562ea0142..b0ce7708b2 100644 --- a/src/specify_cli/_utils.py +++ b/src/specify_cli/_utils.py @@ -137,6 +137,13 @@ def check_tool(tool: str, tracker=None) -> bool: found = shutil.which("kiro-cli") is not None or shutil.which("kiro") is not None elif tool == "rovodev": found = shutil.which("acli") is not None + elif tool == "docker-agent": + # Docker Agent is available either as a standalone binary or as a + # Docker CLI plugin (`docker agent`). + found = ( + shutil.which("docker-agent") is not None + or shutil.which("docker") is not None + ) else: found = shutil.which(tool) is not None diff --git a/tests/test_check_tool.py b/tests/test_check_tool.py index 9520046168..00a558be2e 100644 --- a/tests/test_check_tool.py +++ b/tests/test_check_tool.py @@ -120,6 +120,20 @@ def fake_which(name): with patch("shutil.which", side_effect=fake_which): assert check_tool("rovodev") is True + def test_docker_agent_plugin_fallback(self): + """docker-agent should also detect the Docker CLI plugin form.""" + + def fake_which(name): + return "/usr/bin/docker" if name == "docker" else None + + with patch("shutil.which", side_effect=fake_which): + assert check_tool("docker-agent") is True + + def test_docker_agent_missing(self): + """docker-agent should be missing when neither form is installed.""" + with patch("shutil.which", return_value=None): + assert check_tool("docker-agent") is False + class TestCheckTip: """`specify check` should point users to the existing version check.""" From d5d93b128712c830c353fbd27c0ca9576a9e6452 Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Wed, 26 Aug 2026 22:36:44 +0300 Subject: [PATCH 5/7] fix(docker-agent): validate plugin availability and agent config Probe the Docker plugin before reporting it as installed, and require an agent configuration reference for headless execution. Preserve standalone binary and explicit executable overrides. Assisted-by: Zed Agent (model: GPT-5.6 Luna) Signed-off-by: Alexandr Zaytsev --- .github/ISSUE_TEMPLATE/agent_request.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 1 + integrations/catalog.json | 2 +- src/specify_cli/_utils.py | 46 +++++++++++++++--- .../integrations/docker_agent/__init__.py | 47 ++++++++++++++----- .../test_integration_docker_agent.py | 39 ++++++++++++--- tests/test_agent_config_consistency.py | 1 + tests/test_check_tool.py | 26 +++++++++- 9 files changed, 138 insertions(+), 27 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/agent_request.yml b/.github/ISSUE_TEMPLATE/agent_request.yml index 785f9193e3..377cc92f1f 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, Factory Droid, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Grok Build, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, 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, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Grok Build, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, 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 03fa6c124f..f5b701a77f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -73,6 +73,7 @@ body: - Command Code - Cursor - Devin for Terminal + - Docker Agent - Factory Droid - Firebender - Forge diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 4613c8ebae..e4abc5cb49 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -67,6 +67,7 @@ body: - Command Code - Cursor - Devin for Terminal + - Docker Agent - Factory Droid - Firebender - Forge diff --git a/integrations/catalog.json b/integrations/catalog.json index 870de6b045..a3f4e7c05f 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-07-27T00:00:00Z", + "updated_at": "2026-08-26T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json", "integrations": { "alquimia": { diff --git a/src/specify_cli/_utils.py b/src/specify_cli/_utils.py index b0ce7708b2..300ca4ff58 100644 --- a/src/specify_cli/_utils.py +++ b/src/specify_cli/_utils.py @@ -16,6 +16,45 @@ CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" CLAUDE_NPM_LOCAL_PATH = Path.home() / ".claude" / "local" / "node_modules" / ".bin" / "claude" +DOCKER_AGENT_CHECK_TIMEOUT = 5 + + +def docker_agent_command(executable: str | None = None) -> list[str] | None: + """Return a runnable Docker Agent command, or ``None`` if unavailable. + + Docker Agent is distributed either as the standalone ``docker-agent`` + executable or as the ``docker agent`` Docker CLI plugin. The plugin form + is verified with a bounded, read-only version probe so a plain Docker CLI + is not mistaken for an installed Docker Agent. + """ + resolved_from_path = executable is None + if executable is None: + if shutil.which("docker-agent"): + return ["docker-agent", "run"] + executable = shutil.which("docker") + if executable is None: + return None + + executable_name = Path(executable).name.lower() + if executable_name in {"docker", "docker.exe"}: + command = [executable, "agent", "version"] + run_command = [executable, "agent", "run"] + else: + # An explicit non-Docker executable is an operator override. Preserve + # the existing override contract without probing a custom binary. + return [executable, "run"] + try: + result = subprocess.run( + command, + capture_output=True, + check=False, + timeout=DOCKER_AGENT_CHECK_TIMEOUT, + ) + except (OSError, subprocess.TimeoutExpired): + return None + if result.returncode != 0: + return None + return ["docker", "agent", "run"] if resolved_from_path else run_command def relative_extension_path_violation(value: Any) -> str | None: @@ -138,12 +177,7 @@ def check_tool(tool: str, tracker=None) -> bool: elif tool == "rovodev": found = shutil.which("acli") is not None elif tool == "docker-agent": - # Docker Agent is available either as a standalone binary or as a - # Docker CLI plugin (`docker agent`). - found = ( - shutil.which("docker-agent") is not None - or shutil.which("docker") is not None - ) + found = docker_agent_command() is not None else: found = shutil.which(tool) is not None diff --git a/src/specify_cli/integrations/docker_agent/__init__.py b/src/specify_cli/integrations/docker_agent/__init__.py index a33d484135..008d82f2c6 100644 --- a/src/specify_cli/integrations/docker_agent/__init__.py +++ b/src/specify_cli/integrations/docker_agent/__init__.py @@ -6,8 +6,10 @@ from __future__ import annotations -import shutil -from pathlib import Path +import os +import shlex + +from specify_cli._utils import docker_agent_command from ..base import IntegrationOption, SkillsIntegration @@ -22,8 +24,6 @@ class DockerAgentIntegration(SkillsIntegration): "commands_subdir": "skills", "install_url": "https://docs.docker.com/ai/docker-agent/getting-started/installation/", # Docker Agent is exposed as either `docker-agent` or `docker agent`. - # The init command documents --ignore-agent-tools for the plugin form, - # because the generic preflight check looks up the integration key. "requires_cli": True, } registrar_config = { @@ -46,14 +46,14 @@ def _agent_command(self) -> list[str]: # The shared executable override supports both a standalone # ``docker-agent`` binary and the Docker CLI plugin form. executable = self._resolve_executable() - - if executable != self.key: - if Path(executable).name in {"docker", "docker.exe"}: - return [executable, "agent", "run"] + command = docker_agent_command( + None if executable == self.key else executable + ) + if command is None: + # Preserve the normal executable-shaped argv for dispatch callers; + # preflight and the subprocess runner report the unavailable CLI. return [executable, "run"] - if shutil.which("docker-agent"): - return ["docker-agent", "run"] - return ["docker", "agent", "run"] + return command @classmethod @@ -77,6 +77,31 @@ def build_exec_args( output_json: bool = True, ) -> list[str] | None: """Build a headless Docker Agent invocation with an agent config.""" + extra_env_name = "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS" + extra_args = os.environ.get(extra_env_name, "").strip() + if not extra_args: + raise ValueError( + "Docker Agent requires an agent configuration reference. " + f"Set {extra_env_name}, for example: " + f"{extra_env_name}=./agent.yaml" + ) + # Validate only the argument shape here: require a first positional + # agent reference and reject malformed quoting or a leading option. + # The reference may be a local file or a registry reference, so its + # existence and validity are intentionally left to Docker Agent. + try: + first_arg = shlex.split(extra_args)[0] + except (IndexError, ValueError) as exc: + raise ValueError( + f"{extra_env_name} must start with an agent configuration reference, " + "for example ./agent.yaml" + ) from exc + if first_arg.startswith("-"): + raise ValueError( + f"{extra_env_name} must start with an agent configuration reference, " + "for example ./agent.yaml" + ) + args = [*self._agent_command(), "--exec"] # Extra args carry the required agent source (for example diff --git a/tests/integrations/test_integration_docker_agent.py b/tests/integrations/test_integration_docker_agent.py index 3584c0b000..a7382021a7 100644 --- a/tests/integrations/test_integration_docker_agent.py +++ b/tests/integrations/test_integration_docker_agent.py @@ -1,5 +1,7 @@ """Tests for the Docker Agent integration.""" +import pytest + from specify_cli.integrations.docker_agent import DockerAgentIntegration from .test_integration_base_skills import SkillsIntegrationTests @@ -20,7 +22,11 @@ def test_extra_args_are_applied_to_build_exec_args(monkeypatch): "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml --agent root --model openai/gpt-5", ) - monkeypatch.setattr("shutil.which", lambda name: None) + monkeypatch.setattr( + "shutil.which", + lambda name: "/usr/bin/docker" if name == "docker" else None, + ) + monkeypatch.setattr("subprocess.run", lambda *args, **kwargs: type("Result", (), {"returncode": 0})()) args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) @@ -42,7 +48,11 @@ def test_prompt_is_passed_after_agent_config(monkeypatch): monkeypatch.setenv( "SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml" ) - monkeypatch.setattr("shutil.which", lambda name: None) + monkeypatch.setattr( + "shutil.which", + lambda name: "/usr/bin/docker" if name == "docker" else None, + ) + monkeypatch.setattr("subprocess.run", lambda *args, **kwargs: type("Result", (), {"returncode": 0})()) args = DockerAgentIntegration().build_exec_args( "/speckit-specify prompt", output_json=False @@ -58,7 +68,14 @@ def test_prompt_is_passed_after_agent_config(monkeypatch): ] +def test_requires_agent_config(monkeypatch): + monkeypatch.delenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", raising=False) + with pytest.raises(ValueError, match="requires an agent configuration reference"): + DockerAgentIntegration().build_exec_args("prompt", output_json=False) + + def test_uses_standalone_executable(monkeypatch): + monkeypatch.setenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml") monkeypatch.setattr( "shutil.which", lambda name: "/usr/bin/docker-agent" if name == "docker-agent" else None, @@ -66,30 +83,40 @@ def test_uses_standalone_executable(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["docker-agent", "run", "--exec", "prompt"] + assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "prompt"] + def test_standalone_executable_has_priority(monkeypatch): + monkeypatch.setenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml") monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/docker-agent") args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["docker-agent", "run", "--exec", "prompt"] + assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "prompt"] + def test_executable_override(monkeypatch): + monkeypatch.setenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml") monkeypatch.setenv( "SPECKIT_INTEGRATION_DOCKER_AGENT_EXECUTABLE", "/opt/docker-agent" ) args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["/opt/docker-agent", "run", "--exec", "prompt"] + assert args == ["/opt/docker-agent", "run", "--exec", "./agent.yaml", "prompt"] def test_docker_executable_override_uses_agent_subcommand(monkeypatch): + monkeypatch.setenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml") monkeypatch.setenv( "SPECKIT_INTEGRATION_DOCKER_AGENT_EXECUTABLE", "/opt/docker" ) + monkeypatch.setattr( + "subprocess.run", + lambda *args, **kwargs: type("Result", (), {"returncode": 0})(), + ) + args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["/opt/docker", "agent", "run", "--exec", "prompt"] + assert args == ["/opt/docker", "agent", "run", "--exec", "./agent.yaml", "prompt"] diff --git a/tests/test_agent_config_consistency.py b/tests/test_agent_config_consistency.py index 0cebe7bc33..abb5b76574 100644 --- a/tests/test_agent_config_consistency.py +++ b/tests/test_agent_config_consistency.py @@ -24,6 +24,7 @@ "command-code", "cursor-agent", "devin", + "docker-agent", "droid", "firebender", "forge", diff --git a/tests/test_check_tool.py b/tests/test_check_tool.py index 00a558be2e..ef8a9da2fb 100644 --- a/tests/test_check_tool.py +++ b/tests/test_check_tool.py @@ -121,19 +121,41 @@ def fake_which(name): assert check_tool("rovodev") is True def test_docker_agent_plugin_fallback(self): - """docker-agent should also detect the Docker CLI plugin form.""" + """docker-agent should detect a working Docker CLI plugin form.""" def fake_which(name): return "/usr/bin/docker" if name == "docker" else None - with patch("shutil.which", side_effect=fake_which): + with ( + patch("shutil.which", side_effect=fake_which), + patch("subprocess.run") as run, + ): + run.return_value.returncode = 0 assert check_tool("docker-agent") is True + run.assert_called_once_with( + ["/usr/bin/docker", "agent", "version"], + capture_output=True, + check=False, + timeout=5, + ) def test_docker_agent_missing(self): """docker-agent should be missing when neither form is installed.""" with patch("shutil.which", return_value=None): assert check_tool("docker-agent") is False + def test_docker_agent_plugin_missing(self): + """Plain Docker CLI should not count as Docker Agent.""" + def fake_which(name): + return "/usr/bin/docker" if name == "docker" else None + + with ( + patch("shutil.which", side_effect=fake_which), + patch("subprocess.run") as run, + ): + run.return_value.returncode = 1 + assert check_tool("docker-agent") is False + class TestCheckTip: """`specify check` should point users to the existing version check.""" From b27d500c63d2d818b724cc9e05a55fa4c207435c Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Thu, 27 Aug 2026 15:47:44 +0300 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/reference/integrations.md | 2 +- src/specify_cli/integrations/docker_agent/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index ad297cffb3..3132211d44 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -17,7 +17,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Command Code](https://commandcode.ai/docs) | `command-code` | Skills-based integration; installs skills into `.commandcode/skills/` and invokes them as `$speckit-` | | [Cursor](https://cursor.sh/) | `cursor-agent` | | | [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-` | -| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex and Zed). Detects either the standalone `docker-agent` binary or the Docker CLI plugin (`docker agent`). Configure workflow dispatch with `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml`; the Spec Kit prompt is appended after these arguments. Not multi-install safe by default because the skills directory is shared. | +| [Docker Agent](https://docs.docker.com/ai/docker-agent/) | `docker-agent` | Skills-based integration; installs skills into `.agents/skills/` (the same directory used by Codex and Zed). In the selected agent YAML, enable local skills with `skills: true` and provide filesystem read access. Detects either the standalone `docker-agent` binary or the Docker CLI plugin (`docker agent`). Configure workflow dispatch with `SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS=./agent.yaml`; the Spec Kit prompt is appended after these arguments. Not multi-install safe by default because the skills directory is shared. | | [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-` | | [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ | | [Forge](https://forgecode.dev/) | `forge` | | diff --git a/src/specify_cli/integrations/docker_agent/__init__.py b/src/specify_cli/integrations/docker_agent/__init__.py index 008d82f2c6..f213875cbd 100644 --- a/src/specify_cli/integrations/docker_agent/__init__.py +++ b/src/specify_cli/integrations/docker_agent/__init__.py @@ -1,6 +1,7 @@ """Docker Agent integration — skills-based Docker CLI agent. -Docker Agent discovers project skills from ``.agents/skills``. Runtime +Docker Agent discovers project skills from ``.agents/skills`` when the selected +agent configuration enables local skills and filesystem reads. Runtime configuration is owned by Docker Agent and is not managed by Spec Kit. """ From 6c3a889b774e936ed937eb46e8df1380b463edf7 Mon Sep 17 00:00:00 2001 From: Alexandr Zaytsev Date: Thu, 27 Aug 2026 16:00:05 +0300 Subject: [PATCH 7/7] fix(docker-agent): delimit prompts from CLI flags Insert `--` before the prompt so Cobra passes flag-like messages to the configured agent instead of parsing them as Docker Agent options. Add coverage for prompts beginning with CLI flags. Assisted-by: ChatGPT (model: unknown, autonomous) Signed-off-by: Alexandr Zaytsev --- .../integrations/docker_agent/__init__.py | 10 +++++++++- .../test_integration_docker_agent.py | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/specify_cli/integrations/docker_agent/__init__.py b/src/specify_cli/integrations/docker_agent/__init__.py index f213875cbd..d962f1f994 100644 --- a/src/specify_cli/integrations/docker_agent/__init__.py +++ b/src/specify_cli/integrations/docker_agent/__init__.py @@ -110,9 +110,17 @@ def build_exec_args( # also preserves shell-style quoting when splitting multiple args. self._apply_extra_args_env_var(args) - args.append(prompt) if output_json: args.append("--json") if model: args.extend(["--model", model]) + + # Stop Cobra flag parsing before the user prompt so values such as + # ``--help`` or ``--json`` are passed as messages, not CLI options. + # For example, the complete argv is + # ``docker-agent run --exec ./agent.yaml --agent root -- --help``; + # everything before ``--`` is parsed by Docker Agent, while ``--help`` + # is passed to the configured agent as the user message. + args.extend(["--", prompt]) + return args diff --git a/tests/integrations/test_integration_docker_agent.py b/tests/integrations/test_integration_docker_agent.py index a7382021a7..d962ba978f 100644 --- a/tests/integrations/test_integration_docker_agent.py +++ b/tests/integrations/test_integration_docker_agent.py @@ -40,6 +40,7 @@ def test_extra_args_are_applied_to_build_exec_args(monkeypatch): "root", "--model", "openai/gpt-5", + "--", "prompt", ] @@ -64,10 +65,20 @@ def test_prompt_is_passed_after_agent_config(monkeypatch): "run", "--exec", "./agent.yaml", + "--", "/speckit-specify prompt", ] +def test_prompt_starting_with_flag_is_delimited(monkeypatch): + monkeypatch.setenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", "./agent.yaml") + monkeypatch.setattr("shutil.which", lambda name: None) + + args = DockerAgentIntegration().build_exec_args("--help", output_json=False) + + assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "--", "--help"] + + def test_requires_agent_config(monkeypatch): monkeypatch.delenv("SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS", raising=False) with pytest.raises(ValueError, match="requires an agent configuration reference"): @@ -83,7 +94,7 @@ def test_uses_standalone_executable(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "prompt"] + assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "--", "prompt"] def test_standalone_executable_has_priority(monkeypatch): @@ -92,7 +103,7 @@ def test_standalone_executable_has_priority(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "prompt"] + assert args == ["docker-agent", "run", "--exec", "./agent.yaml", "--", "prompt"] def test_executable_override(monkeypatch): @@ -103,7 +114,7 @@ def test_executable_override(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["/opt/docker-agent", "run", "--exec", "./agent.yaml", "prompt"] + assert args == ["/opt/docker-agent", "run", "--exec", "./agent.yaml", "--", "prompt"] def test_docker_executable_override_uses_agent_subcommand(monkeypatch): @@ -119,4 +130,4 @@ def test_docker_executable_override_uses_agent_subcommand(monkeypatch): args = DockerAgentIntegration().build_exec_args("prompt", output_json=False) - assert args == ["/opt/docker", "agent", "run", "--exec", "./agent.yaml", "prompt"] + assert args == ["/opt/docker", "agent", "run", "--exec", "./agent.yaml", "--", "prompt"]