Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
1 change: 1 addition & 0 deletions docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<command>` |
| [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-<command>` |
| [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` |
Comment thread
nervgh marked this conversation as resolved.
Outdated
| [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-<command>` |
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
| [Forge](https://forgecode.dev/) | `forge` | |
Expand Down
9 changes: 9 additions & 0 deletions integrations/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@
"repository": "https://github.com/github/spec-kit",
"tags": ["cli", "skills"]
},
"docker-agent": {
Comment thread
nervgh marked this conversation as resolved.
"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",
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,6 +101,7 @@ def _register_builtins() -> None:
_register(CopilotIntegration())
_register(CursorAgentIntegration())
_register(DevinIntegration())
_register(DockerAgentIntegration())
Comment thread
nervgh marked this conversation as resolved.
_register(DroidIntegration())
_register(FirebenderIntegration())
_register(ForgeIntegration())
Expand Down
67 changes: 67 additions & 0 deletions src/specify_cli/integrations/docker_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -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):
Comment thread
mnriem marked this conversation as resolved.
"""Integration for Docker Agent."""

key = "docker-agent"
config = {
"name": "Docker Agent",
"folder": ".docker-agent/",
"commands_subdir": "skills",
Comment thread
nervgh marked this conversation as resolved.
Outdated
"install_url": "https://docs.docker.com/ai/docker-agent/getting-started/installation/",
"requires_cli": True,
Comment thread
nervgh marked this conversation as resolved.
}
registrar_config = {
"dir": ".docker-agent/skills",
Comment thread
nervgh marked this conversation as resolved.
Outdated
"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.<name>.hooks``.
CANONICAL_TO_NATIVE = {
"session_start": "session_start",
"pre_tool_use": "pre_tool_use",
"post_tool_use": "post_tool_use",
"session_end": "session_end",
Comment thread
nervgh marked this conversation as resolved.
Outdated
"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"]
Comment thread
nervgh marked this conversation as resolved.
Outdated
Comment thread
nervgh marked this conversation as resolved.
Outdated

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"]
Comment thread
nervgh marked this conversation as resolved.
Comment thread
nervgh marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.
if output_json:
args.append("--json")
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
args.append(prompt)
Comment thread
nervgh marked this conversation as resolved.
Outdated
return args
82 changes: 82 additions & 0 deletions tests/integrations/test_integration_docker_agent.py
Original file line number Diff line number Diff line change
@@ -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():
Comment thread
nervgh marked this conversation as resolved.
Outdated
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"
Comment thread
nervgh marked this conversation as resolved.
Outdated
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",
Comment thread
nervgh marked this conversation as resolved.
Outdated
"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")
Comment thread
nervgh marked this conversation as resolved.
Outdated

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",
]