Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
39 changes: 37 additions & 2 deletions docs/integrations/droid.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,42 @@ Skills are installed as `.factory/skills/speckit-<command>/SKILL.md`.
Invoke them in the agent's chat, for example `/speckit-specify` or
`/speckit-bug-assess` for the installed bug extension.

Generated skills are both user- and model-invocable.
Generated skills are both user- and model-invocable by default.

## User-Invocable-Only Skills

To stop Droid invoking the core Spec Kit command skills automatically:

```bash
specify init my-project --integration droid --integration-options="--no-model-invocation"
```

This writes `disable-model-invocation: true` into the core command skills.
In Droid 1.0.0 the model no longer lists them among its available skills, so it
does not run them unprompted, while `/speckit-<command>` and workflow dispatch
still run them. It is not a hard block: a prompt that names a skill (for
example "use the speckit-specify skill") still lets the model invoke it through
its `Skill` tool. Factory's
[skills documentation](https://docs.factory.com/cli/configuration/skills) does
not describe this behaviour.

- Extension skills stay model-invocable, because mandatory extension hooks rely
on the model invoking them. If an extension's mandatory hook runs a core
command (its `command:` is a core `speckit.<command>`), leave this option off:
the model cannot run that hook while the core skill is user-invocable only.
- The option is stored with the integration's other options and reapplied by
`specify integration upgrade droid`. Passing `--integration-options` to
`upgrade` replaces all stored options; `--integration-options=""` clears
them. Re-running `specify init` likewise stores only the options passed to it.
- Core command skills that a preset overrides, or stops overriding, are reverted
to the default by commands that re-register preset skills without reinstalling
Droid: commands that install, remove or update presets (including `specify
bundle` commands that carry presets), `specify integration use droid`, and
`specify integration switch droid` when Droid is installed but not the
default. To reapply the option, run `specify integration upgrade droid
--force` while Droid is the default integration (`--force` is needed because
those commands modify installed skill files, and it overwrites local edits to
Spec Kit-managed files).

## Workflow Dispatch

Expand All @@ -32,4 +67,4 @@ Workflow steps use `droid exec <prompt>`, forwarding `--model` and requesting
permission-bypass flag. Operators can explicitly configure permission flags
through `SPECKIT_INTEGRATION_DROID_EXTRA_ARGS`; those arguments follow Spec
Kit's model/output flags and may override them under duplicate-flag parsing.
There are no integration-specific setup options.
The only integration-specific setup option is `--no-model-invocation`.
83 changes: 80 additions & 3 deletions src/specify_cli/integrations/droid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from collections.abc import Mapping, Sequence
from typing import Any

from ..base import SkillsIntegration
from ..base import IntegrationOption, SkillsIntegration
from ..manifest import IntegrationManifest


class DroidIntegration(SkillsIntegration):
Expand All @@ -36,6 +37,48 @@ class DroidIntegration(SkillsIntegration):
}
multi_install_safe = True

# Set by ``setup()`` from ``--no-model-invocation``; applies to core
# command skills only, since mandatory extension hooks rely on the model
# invoking the hook skill. Integrations are process-wide singletons, so
# preset overrides re-registered later in the same run see the same value.
# Commands that rewrite skills without calling ``setup()`` (e.g.
# ``preset add``, ``integration use``) get the default.
_disable_model_invocation: bool = False

@classmethod
def options(cls) -> list[IntegrationOption]:
opts = super().options()
opts.append(
IntegrationOption(
"--no-model-invocation",
is_flag=True,
default=False,
help=(
"Install core command skills as user-invocable only "
"(disable-model-invocation: true)"
),
)
)
return opts

def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
self._disable_model_invocation = (
(parsed_options or {}).get("no_model_invocation") is True
)
try:
return super().setup(
project_root, manifest, parsed_options=parsed_options, **opts
)
except BaseException:
self._disable_model_invocation = False
raise

@staticmethod
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
"""Insert ``key: value`` before the closing ``---`` if not already present.
Expand Down Expand Up @@ -75,17 +118,51 @@ def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str
out.append(line)
return "".join(out)

@staticmethod
def _skill_name_from_content(content: str) -> str | None:
"""Return the ``name:`` field of a skill's first frontmatter block."""
dash_count = 0
for line in content.splitlines():
stripped = line.rstrip("\r\n")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1 and stripped.startswith("name:"):
name = stripped[len("name:"):].strip().strip('"').strip("'")
return name or None
return None

@staticmethod
def _is_core_command_skill(name: str | None) -> bool:
from specify_cli.extensions import CORE_COMMAND_NAMES # lazy: avoids circular import

return bool(name) and name.startswith("speckit-") and (
name.removeprefix("speckit-") in CORE_COMMAND_NAMES
)

def post_process_skill_content(self, content: str) -> str:
"""Inject Droid-specific skill frontmatter flags.

Applies the shared hook-command normalization note (skills agents use
hyphenated ``/speckit-<name>`` invocations, not dotted ``/speckit.<name>``)
and the Droid-specific ``user-invocable`` / ``disable-model-invocation``
frontmatter flags so skills are both user- and Droid-invocable.
frontmatter flags so skills are both user- and Droid-invocable by
default.

``disable-model-invocation`` is ``true`` for core command skills when
the last ``setup()`` in this process was given
``--no-model-invocation``, else ``false``.
"""
updated = super().post_process_skill_content(content)
updated = self._inject_frontmatter_flag(updated, "user-invocable")
updated = self._inject_frontmatter_flag(updated, "disable-model-invocation", "false")
disable = self._disable_model_invocation and self._is_core_command_skill(
self._skill_name_from_content(updated)
)
updated = self._inject_frontmatter_flag(
updated, "disable-model-invocation", "true" if disable else "false"
)
return updated

def build_exec_args(
Expand Down
Loading
Loading