Skip to content

Commit dc31c57

Browse files
feat: add Factory Droid CLI integration (#822)
Adds a skills-based integration for the Factory Droid CLI alongside the existing Claude/Codex skills agents. The integration scaffolds `.factory/skills/speckit-*` directories and documents the install step in the devcontainer post-create script via the official npm distribution (`npm install -g droid`), which matches the layout of every other CLI install block above and avoids executing an unverified remote shell installer. The integration is also added to the user-facing supported-agent table in `docs/reference/integrations.md` so the new key is discoverable from the published documentation, as required by the "Updating this documentation" guideline in AGENTS.md. Operator-supplied extra args via `SPECKIT_INTEGRATION_DROID_EXTRA_ARGS` are appended after the canonical Spec Kit flags so the canonical flags are always present in argv. The Factory Droid CLI parser uses last-wins duplicate-flag semantics (verified empirically against droid 0.175.0), so a later operator-supplied value may override the canonical one — this is a deliberate inversion of the cursor-agent / opencode / codex ordering. Includes: - `src/specify_cli/integrations/droid/__init__.py` (subpackage) - `tests/integrations/test_integration_droid.py` (46 tests, including regression coverage for the no-trailing-newline frontmatter fusion bug, idempotent skill injection, and env-var path resolution) - `integrations/catalog.json` entry + `updated_at` bump - Alphabetical registration in `src/specify_cli/integrations/__init__.py` and `tests/integrations/test_registry.py` - Devcontainer Droid install block via the npm distribution (`npm install -g droid`), replacing the earlier curl-based installer - User-facing supported-agent table row in `docs/reference/integrations.md` (key `droid`, `.factory/skills/` layout, `/speckit-<command>` invocation) - `AGENT_CONFIG` entry and matching alphabetical entries in `tests/test_agent_config_consistency.py` (`ISSUE_TEMPLATE_AGENT_KEYS`) and the three issue-template dropdowns (`agent_request.yml`, `bug_report.yml`, `feature_request.yml`) so `test_issue_template_agent_lists_match_runtime_integrations` keeps the runtime/template surfaces synchronized Closes #822 Assisted-by: Droid (oracle-reviewer) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Assisted-by: Droid (model: MiniMax M3, autonomous)
1 parent 7bdf6c5 commit dc31c57

11 files changed

Lines changed: 428 additions & 2 deletions

File tree

.devcontainer/post-create.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ echo -e "\n🤖 Installing CodeBuddy CLI..."
9797
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
9898
echo "✅ Done"
9999

100+
echo -e "\n🤖 Installing Factory Droid CLI..."
101+
# https://docs.factory.ai/cli/getting-started/quickstart
102+
# Install via the npm distribution (``npm install -g droid``) rather than
103+
# fetching ``https://app.factory.ai/cli``: the upstream shell installer is
104+
# served from a mutable endpoint with no published digest, so downloading
105+
# it and executing it inside the devcontainer would run whatever the
106+
# server returned at build time. npm packages carry integrity checksums
107+
# verified against the registry signature, and this matches the layout of
108+
# every other CLI install block above.
109+
run_command "npm install -g droid@latest"
110+
111+
if ! command -v droid >/dev/null 2>&1; then
112+
echo -e "\033[0;31m[ERROR] Droid CLI installation did not create 'droid' in PATH.\033[0m" >&2
113+
exit 1
114+
fi
115+
116+
run_command "droid --version > /dev/null"
117+
echo "✅ Done"
118+
100119
# Installing UV (Python package manager)
101120
echo -e "\n🐍 Installing UV - Python Package Manager..."
102121
run_command "pipx install uv"

.github/ISSUE_TEMPLATE/agent_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ body:
88
value: |
99
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
1010
11-
**Currently supported agents**: Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Cursor, Devin for Terminal, 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
11+
**Currently supported agents**: Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, 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
1212
1313
- type: input
1414
id: agent-name

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ body:
7171
- Codex CLI
7272
- Cursor
7373
- Devin for Terminal
74+
- Factory Droid
7475
- Firebender
7576
- Forge
7677
- Gemini CLI

.github/ISSUE_TEMPLATE/feature_request.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ body:
6565
- Codex CLI
6666
- Cursor
6767
- Devin for Terminal
68+
- Factory Droid
6869
- Firebender
6970
- Forge
7071
- Gemini CLI

docs/reference/integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
1515
| [Codex CLI](https://github.com/openai/codex) | `codex` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `$speckit-<command>` |
1616
| [Cursor](https://cursor.sh/) | `cursor-agent` | |
1717
| [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-<command>` |
18+
| [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>` |
1819
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
1920
| [Forge](https://forgecode.dev/) | `forge` | |
2021
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |

integrations/catalog.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schema_version": "1.0",
3-
"updated_at": "2026-07-15T00:00:00Z",
3+
"updated_at": "2026-07-17T00:00:00Z",
44
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
55
"integrations": {
66
"claude": {
@@ -48,6 +48,15 @@
4848
"repository": "https://github.com/github/spec-kit",
4949
"tags": ["ide"]
5050
},
51+
"droid": {
52+
"id": "droid",
53+
"name": "Factory Droid",
54+
"version": "1.0.0",
55+
"description": "Factory Droid CLI skills-based integration",
56+
"author": "spec-kit-core",
57+
"repository": "https://github.com/github/spec-kit",
58+
"tags": ["cli", "skills", "factory"]
59+
},
5160
"amp": {
5261
"id": "amp",
5362
"name": "Amp",

src/specify_cli/integrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def _register_builtins() -> None:
5858
from .copilot import CopilotIntegration
5959
from .cursor_agent import CursorAgentIntegration
6060
from .devin import DevinIntegration
61+
from .droid import DroidIntegration
6162
from .firebender import FirebenderIntegration
6263
from .forge import ForgeIntegration
6364
from .gemini import GeminiIntegration
@@ -95,6 +96,7 @@ def _register_builtins() -> None:
9596
_register(CopilotIntegration())
9697
_register(CursorAgentIntegration())
9798
_register(DevinIntegration())
99+
_register(DroidIntegration())
98100
_register(FirebenderIntegration())
99101
_register(ForgeIntegration())
100102
_register(GeminiIntegration())
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Factory Droid CLI integration — skills-based agent.
2+
3+
Droid discovers project skills from
4+
``.factory/skills/speckit-<name>/SKILL.md``. Spec Kit installs into that
5+
native tree so the generated skills are visible to Droid without extra
6+
configuration.
7+
8+
See: https://docs.factory.ai/cli/configuration/skills
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from ..base import SkillsIntegration
14+
15+
16+
class DroidIntegration(SkillsIntegration):
17+
"""Integration for Factory Droid CLI."""
18+
19+
key = "droid"
20+
config = {
21+
"name": "Factory Droid",
22+
"folder": ".factory/",
23+
"commands_subdir": "skills",
24+
"install_url": "https://docs.factory.ai/cli/getting-started/overview",
25+
"requires_cli": True,
26+
}
27+
registrar_config = {
28+
"dir": ".factory/skills",
29+
"format": "markdown",
30+
"args": "$ARGUMENTS",
31+
"extension": "/SKILL.md",
32+
}
33+
multi_install_safe = True
34+
35+
@staticmethod
36+
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
37+
"""Insert ``key: value`` before the closing ``---`` if not already present.
38+
39+
Mirrors the helper used by ``ClaudeIntegration`` / ``VibeIntegration``
40+
so per-agent frontmatter injection stays consistent across skills-based
41+
integrations. Pre-scans for the key to keep injection idempotent.
42+
"""
43+
lines = content.splitlines(keepends=True)
44+
45+
# Pre-scan: bail out if already present in frontmatter
46+
dash_count = 0
47+
for line in lines:
48+
stripped = line.rstrip("\n\r")
49+
if stripped == "---":
50+
dash_count += 1
51+
if dash_count == 2:
52+
break
53+
continue
54+
if dash_count == 1 and stripped.startswith(f"{key}:"):
55+
return content
56+
57+
# Inject before the closing --- of frontmatter. Always emit a
58+
# newline after the injected key so the key and the closing ---
59+
# stay on separate lines even when the closing delimiter is the
60+
# last line of the file with no trailing newline.
61+
out: list[str] = []
62+
dash_count = 0
63+
injected = False
64+
for line in lines:
65+
stripped = line.rstrip("\n\r")
66+
if stripped == "---":
67+
dash_count += 1
68+
if dash_count == 2 and not injected:
69+
out.append(f"{key}: {value}\n")
70+
injected = True
71+
out.append(line)
72+
return "".join(out)
73+
74+
def post_process_skill_content(self, content: str) -> str:
75+
"""Inject Droid-specific skill frontmatter flags.
76+
77+
Applies the shared hook-command normalization note (skills agents use
78+
hyphenated ``/speckit-<name>`` invocations, not dotted ``/speckit.<name>``)
79+
and the Droid-specific ``user-invocable`` / ``disable-model-invocation``
80+
frontmatter flags so skills are both user- and Droid-invocable.
81+
"""
82+
updated = super().post_process_skill_content(content)
83+
updated = self._inject_frontmatter_flag(updated, "user-invocable")
84+
updated = self._inject_frontmatter_flag(updated, "disable-model-invocation", "false")
85+
return updated
86+
87+
def build_exec_args(
88+
self,
89+
prompt: str,
90+
*,
91+
model: str | None = None,
92+
output_json: bool = True,
93+
) -> list[str] | None:
94+
"""Build CLI arguments for non-interactive ``droid`` execution.
95+
96+
Uses ``droid exec "<prompt>"`` for headless dispatch. The
97+
``--skip-permissions-unsafe`` flag is mandatory for non-interactive
98+
runs because Droid's permission prompts otherwise block workflow
99+
dispatch (matches Cursor's ``--force`` and Grok's ``--always-approve``
100+
role for spec-kit automation).
101+
102+
Output format and model selection mirror the documented CLI flags:
103+
``--output-format json`` (when ``output_json`` is set) and
104+
``--model <id>``. Operator-supplied extra args via
105+
``SPECKIT_INTEGRATION_DROID_EXTRA_ARGS`` are appended after the
106+
canonical Spec Kit flags so the canonical flags are guaranteed to
107+
be present in argv. Note that with duplicate-flag CLI parsing the
108+
later (operator-supplied) value may take precedence over the
109+
canonical one, so operators can still override ``--model``,
110+
``--output-format``, or ``--skip-permissions-unsafe``.
111+
"""
112+
if not self.config or not self.config.get("requires_cli"):
113+
return None
114+
args = [
115+
self._resolve_executable(),
116+
"exec",
117+
prompt,
118+
"--skip-permissions-unsafe",
119+
]
120+
# Operator-injected extra args are appended after Spec Kit's
121+
# canonical --model / --output-format flags so the canonical
122+
# flags are guaranteed to be present in argv regardless of
123+
# whatever the operator passes via SPECKIT_INTEGRATION_DROID_EXTRA_ARGS.
124+
# This is a deliberate inversion of the cursor-agent / opencode /
125+
# codex ordering (which all apply extra args first, then append
126+
# canonical flags so the canonical values win under duplicate-flag
127+
# parsing). For Droid the canonical flag values are written into
128+
# argv first, then the operator-supplied values follow; with
129+
# duplicate-flag parsing the later (operator) value may therefore
130+
# take precedence.
131+
if model:
132+
args.extend(["--model", model])
133+
if output_json:
134+
args.extend(["--output-format", "json"])
135+
self._apply_extra_args_env_var(args)
136+
return args

0 commit comments

Comments
 (0)