⚡ Sangsakala Proxy 🏠 Home
Skip to content

Commit c6e98ea

Browse files
echarrodclaude
andauthored
feat(droid): add --no-model-invocation integration option (#5116)
* feat(droid): add --no-model-invocation integration option Droid skills are always written with `disable-model-invocation: false`. `specify init --integration droid --integration-options="--no-model-invocation"` now writes `true` for the core command skills (and preset overrides of them) so Droid does not invoke them automatically; they stay available as `/speckit-<command>`. The default is unchanged (#2654). Mirrors the Claude option from #4919: setup() records the option, and post_process_skill_content() applies it only to `speckit-<core>` skills in CORE_COMMAND_NAMES, so extension skills stay model-invocable for mandatory hooks. The option persists in .specify/integration.json and is reapplied by `specify integration upgrade droid`. Commands that rewrite skills without calling setup() (preset add/remove/update, integration use/switch) keep the default; the docs say so and point at `upgrade droid --force`. Assisted-by: Claude Code (model: claude-sonnet-5-5, autonomous) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J3taepmtmjGTxTn4W1xb7q * docs(droid): note skills are model-invocable by default The post_process_skill_content() docstring said skills are always both user- and Droid-invocable, which is no longer true once --no-model-invocation is set. Assisted-by: Claude Code (model: claude-opus-5-5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015w5WirGUspLsYEpCkaHWxV * docs(droid): record observed --no-model-invocation behaviour Validated against Droid 1.0.0: flagged core skills drop out of the model's skill listing, /speckit-<command> and workflow dispatch still run them, and the model can still invoke them via its Skill tool when a prompt names one. Assisted-by: Claude Code (model: claude-opus-5-5, autonomous) --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 7370e59 commit c6e98ea

3 files changed

Lines changed: 365 additions & 5 deletions

File tree

‎docs/integrations/droid.md‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,42 @@ Skills are installed as `.factory/skills/speckit-<command>/SKILL.md`.
2323
Invoke them in the agent's chat, for example `/speckit-specify` or
2424
`/speckit-bug-assess` for the installed bug extension.
2525

26-
Generated skills are both user- and model-invocable.
26+
Generated skills are both user- and model-invocable by default.
27+
28+
## User-Invocable-Only Skills
29+
30+
To stop Droid invoking the core Spec Kit command skills automatically:
31+
32+
```bash
33+
specify init my-project --integration droid --integration-options="--no-model-invocation"
34+
```
35+
36+
This writes `disable-model-invocation: true` into the core command skills.
37+
In Droid 1.0.0 the model no longer lists them among its available skills, so it
38+
does not run them unprompted, while `/speckit-<command>` and workflow dispatch
39+
still run them. It is not a hard block: a prompt that names a skill (for
40+
example "use the speckit-specify skill") still lets the model invoke it through
41+
its `Skill` tool. Factory's
42+
[skills documentation](https://docs.factory.com/cli/configuration/skills) does
43+
not describe this behaviour.
44+
45+
- Extension skills stay model-invocable, because mandatory extension hooks rely
46+
on the model invoking them. If an extension's mandatory hook runs a core
47+
command (its `command:` is a core `speckit.<command>`), leave this option off:
48+
the model cannot run that hook while the core skill is user-invocable only.
49+
- The option is stored with the integration's other options and reapplied by
50+
`specify integration upgrade droid`. Passing `--integration-options` to
51+
`upgrade` replaces all stored options; `--integration-options=""` clears
52+
them. Re-running `specify init` likewise stores only the options passed to it.
53+
- Core command skills that a preset overrides, or stops overriding, are reverted
54+
to the default by commands that re-register preset skills without reinstalling
55+
Droid: commands that install, remove or update presets (including `specify
56+
bundle` commands that carry presets), `specify integration use droid`, and
57+
`specify integration switch droid` when Droid is installed but not the
58+
default. To reapply the option, run `specify integration upgrade droid
59+
--force` while Droid is the default integration (`--force` is needed because
60+
those commands modify installed skill files, and it overwrites local edits to
61+
Spec Kit-managed files).
2762

2863
## Workflow Dispatch
2964

@@ -32,4 +67,4 @@ Workflow steps use `droid exec <prompt>`, forwarding `--model` and requesting
3267
permission-bypass flag. Operators can explicitly configure permission flags
3368
through `SPECKIT_INTEGRATION_DROID_EXTRA_ARGS`; those arguments follow Spec
3469
Kit's model/output flags and may override them under duplicate-flag parsing.
35-
There are no integration-specific setup options.
70+
The only integration-specific setup option is `--no-model-invocation`.

‎src/specify_cli/integrations/droid/__init__.py‎

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from collections.abc import Mapping, Sequence
1515
from typing import Any
1616

17-
from ..base import SkillsIntegration
17+
from ..base import IntegrationOption, SkillsIntegration
18+
from ..manifest import IntegrationManifest
1819

1920

2021
class DroidIntegration(SkillsIntegration):
@@ -36,6 +37,48 @@ class DroidIntegration(SkillsIntegration):
3637
}
3738
multi_install_safe = True
3839

40+
# Set by ``setup()`` from ``--no-model-invocation``; applies to core
41+
# command skills only, since mandatory extension hooks rely on the model
42+
# invoking the hook skill. Integrations are process-wide singletons, so
43+
# preset overrides re-registered later in the same run see the same value.
44+
# Commands that rewrite skills without calling ``setup()`` (e.g.
45+
# ``preset add``, ``integration use``) get the default.
46+
_disable_model_invocation: bool = False
47+
48+
@classmethod
49+
def options(cls) -> list[IntegrationOption]:
50+
opts = super().options()
51+
opts.append(
52+
IntegrationOption(
53+
"--no-model-invocation",
54+
is_flag=True,
55+
default=False,
56+
help=(
57+
"Install core command skills as user-invocable only "
58+
"(disable-model-invocation: true)"
59+
),
60+
)
61+
)
62+
return opts
63+
64+
def setup(
65+
self,
66+
project_root: Path,
67+
manifest: IntegrationManifest,
68+
parsed_options: dict[str, Any] | None = None,
69+
**opts: Any,
70+
) -> list[Path]:
71+
self._disable_model_invocation = (
72+
(parsed_options or {}).get("no_model_invocation") is True
73+
)
74+
try:
75+
return super().setup(
76+
project_root, manifest, parsed_options=parsed_options, **opts
77+
)
78+
except BaseException:
79+
self._disable_model_invocation = False
80+
raise
81+
3982
@staticmethod
4083
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
4184
"""Insert ``key: value`` before the closing ``---`` if not already present.
@@ -75,17 +118,51 @@ def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str
75118
out.append(line)
76119
return "".join(out)
77120

121+
@staticmethod
122+
def _skill_name_from_content(content: str) -> str | None:
123+
"""Return the ``name:`` field of a skill's first frontmatter block."""
124+
dash_count = 0
125+
for line in content.splitlines():
126+
stripped = line.rstrip("\r\n")
127+
if stripped == "---":
128+
dash_count += 1
129+
if dash_count == 2:
130+
break
131+
continue
132+
if dash_count == 1 and stripped.startswith("name:"):
133+
name = stripped[len("name:"):].strip().strip('"').strip("'")
134+
return name or None
135+
return None
136+
137+
@staticmethod
138+
def _is_core_command_skill(name: str | None) -> bool:
139+
from specify_cli.extensions import CORE_COMMAND_NAMES # lazy: avoids circular import
140+
141+
return bool(name) and name.startswith("speckit-") and (
142+
name.removeprefix("speckit-") in CORE_COMMAND_NAMES
143+
)
144+
78145
def post_process_skill_content(self, content: str) -> str:
79146
"""Inject Droid-specific skill frontmatter flags.
80147

81148
Applies the shared hook-command normalization note (skills agents use
82149
hyphenated ``/speckit-<name>`` invocations, not dotted ``/speckit.<name>``)
83150
and the Droid-specific ``user-invocable`` / ``disable-model-invocation``
84-
frontmatter flags so skills are both user- and Droid-invocable.
151+
frontmatter flags so skills are both user- and Droid-invocable by
152+
default.
153+
154+
``disable-model-invocation`` is ``true`` for core command skills when
155+
the last ``setup()`` in this process was given
156+
``--no-model-invocation``, else ``false``.
85157
"""
86158
updated = super().post_process_skill_content(content)
87159
updated = self._inject_frontmatter_flag(updated, "user-invocable")
88-
updated = self._inject_frontmatter_flag(updated, "disable-model-invocation", "false")
160+
disable = self._disable_model_invocation and self._is_core_command_skill(
161+
self._skill_name_from_content(updated)
162+
)
163+
updated = self._inject_frontmatter_flag(
164+
updated, "disable-model-invocation", "true" if disable else "false"
165+
)
89166
return updated
90167

91168
def build_exec_args(

0 commit comments

Comments
 (0)