Skip to content

Commit 39cf3a7

Browse files
echarrodclaudeCopilot
authored
feat(copilot): add --no-model-invocation integration option (#5118)
* feat(copilot): add --no-model-invocation integration option `specify init --integration copilot --integration-options="--no-model-invocation"` writes `disable-model-invocation: true` on the core command skills in the skills layout (#2654). VS Code documents the field as requiring manual `/` invocation; Copilot CLI docs do not list it. The default is unchanged, the option persists through integration.json so `integration upgrade` reapplies it, extension skills stay model-invocable, and `--commands` ignores it. Confined to the Copilot integration, its tests and docs. Assisted-by: Claude Code (claude-sonnet-5-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhaPZg7kBwheHtH9wf2AZv * docs(copilot): record Copilot CLI behaviour of --no-model-invocation Assisted-by: Claude Code (claude-sonnet-5-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhaPZg7kBwheHtH9wf2AZv * fix(copilot): reset model-invocation flag when event emission fails Move emit_events() inside the protected block in setup() so a failure while installing native hooks no longer leaves _disable_model_invocation set for later core-skill rendering in the same process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015w5WirGUspLsYEpCkaHWxV * Update documentation on option effects with commands Clarify the effect of the option in commands layout. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * docs(copilot): warn that the --skills migration drops stored options The documented commands-to-skills migration passes --integration-options, which replaces all stored options. A stored --no-model-invocation is dropped without warning and the skills become model-invocable, so say so where the migration command is documented. 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_015w5WiqwEFQkgenh2YdQLV8 --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 738fc22 commit 39cf3a7

3 files changed

Lines changed: 426 additions & 12 deletions

File tree

‎docs/integrations/copilot.md‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,51 @@ See [integration management](cli.md) for activation, upgrades, and removal.
2222
Skills are installed as `.github/skills/speckit-<command>/SKILL.md`.
2323
Invoke them in the agent's chat as `/speckit-specify` or
2424
`/speckit-bug-assess` for the installed bug extension. These are agent skills,
25-
not terminal commands.
25+
not terminal commands. Generated skills are both user- and model-invocable by
26+
default.
27+
28+
## User-Invocable-Only Skills
29+
30+
To stop Copilot loading the core Spec Kit command skills automatically in the
31+
skills layout:
32+
33+
```bash
34+
specify init my-project --integration copilot --integration-options="--no-model-invocation"
35+
```
36+
37+
This writes `disable-model-invocation: true` into the core command skills.
38+
VS Code's [agent skills documentation](https://code.visualstudio.com/docs/agent-customization/agent-skills)
39+
says this requires manual invocation through the `/` slash command only.
40+
GitHub's Copilot CLI skills documentation does not list the field, but in a
41+
manual check with Copilot CLI 1.0.70 the model called its `skill` tool for
42+
`speckit-specify` without the flag and did not with it, and an explicit
43+
`/speckit-specify` still ran. With the flag the model could still read the
44+
skill's `SKILL.md` as an ordinary file, so it is not a hard block. Neither
45+
source says whether a skill with this flag keeps its description out of the
46+
model's context.
47+
48+
- The option has no effect in the commands layout, whether selected explicitly
49+
with `--commands` or retained from an existing installation when no layout flag
50+
is passed: the `.agent.md` and `.prompt.md` files are not skills and no flag is
51+
written.
52+
- Extension skills stay model-invocable, because mandatory extension hooks rely
53+
on the model invoking them. If an extension's mandatory hook runs a core
54+
command (its `command:` is a core `speckit.<command>`), leave this option off:
55+
the model cannot run that hook while the core skill is user-invocable only.
56+
- The option is stored with the integration's other options and reapplied by
57+
`specify integration upgrade copilot`. Passing `--integration-options` to
58+
`upgrade` replaces all stored options;
59+
`--integration-options=""` clears them. Re-running `specify init` likewise
60+
stores only the options passed to it.
61+
- Core command skills that a preset overrides, or stops overriding, are reverted
62+
to the default by commands that re-register preset skills without reinstalling
63+
Copilot: commands that install, remove or update presets (including `specify
64+
bundle` commands that carry presets), `specify integration use copilot`, and
65+
`specify integration switch copilot` when Copilot is installed but not the
66+
default. To reapply the option, run `specify integration upgrade copilot
67+
--force` while Copilot is the default integration (`--force` is needed because
68+
those commands modify installed skill files, and it overwrites local edits to
69+
Spec Kit-managed files).
2670

2771
## Commands Layout
2872

@@ -39,7 +83,9 @@ it directly in a prompt.
3983

4084
Use `--integration-options="--skills"` to explicitly select the default skills
4185
layout when migrating an existing commands installation with
42-
`specify integration upgrade copilot`. See the
86+
`specify integration upgrade copilot`. Passing it replaces all stored options,
87+
so include any you want to keep: without `--no-model-invocation` in that value,
88+
for example, the migrated skills are written as model-invocable. See the
4389
[shared upgrade rules](cli.md#upgrade-an-integration),
4490
including the restriction on changing layouts while preset artifacts are
4591
registered.

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

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ class CopilotIntegration(IntegrationBase):
178178
# Mutable flag set by setup() — indicates the active scaffolding mode.
179179
_skills_mode: bool = True
180180

181+
# Set by ``setup()`` from ``--no-model-invocation`` (skills layout only);
182+
# applies to core command skills only, since mandatory extension hooks rely
183+
# on the model invoking the hook skill. Integrations are process-wide
184+
# singletons, so preset overrides re-registered later in the same run see
185+
# the same value. Commands that rewrite skills without calling ``setup()``
186+
# (e.g. ``preset add``, ``integration use``) get the default.
187+
_disable_model_invocation: bool = False
188+
181189
def effective_invoke_separator(
182190
self,
183191
parsed_options: dict[str, Any] | None = None,
@@ -299,6 +307,17 @@ def options(cls) -> list[IntegrationOption]:
299307
),
300308
),
301309
)
310+
opts.append(
311+
IntegrationOption(
312+
"--no-model-invocation",
313+
is_flag=True,
314+
default=False,
315+
help=(
316+
"Install core command skills as user-invocable only "
317+
"(disable-model-invocation: true); skills layout only, documented for VS Code"
318+
),
319+
),
320+
)
302321
return opts
303322

304323
def _resolve_executable(self) -> str:
@@ -482,14 +501,78 @@ def stale_cleanup_exclusions(self) -> set[str]:
482501
exclusions.add(".vscode/settings.json")
483502
return exclusions
484503

504+
@staticmethod
505+
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
506+
"""Insert ``key: value`` before the closing ``---`` if not already present."""
507+
lines = content.splitlines(keepends=True)
508+
dash_count = 0
509+
for line in lines:
510+
stripped = line.rstrip("\n\r")
511+
if stripped == "---":
512+
dash_count += 1
513+
if dash_count == 2:
514+
break
515+
continue
516+
if dash_count == 1 and stripped.startswith(f"{key}:"):
517+
return content
518+
519+
out: list[str] = []
520+
dash_count = 0
521+
injected = False
522+
for line in lines:
523+
if line.rstrip("\n\r") == "---":
524+
dash_count += 1
525+
if dash_count == 2 and not injected:
526+
eol = "\r\n" if line.endswith("\r\n") else "\n"
527+
out.append(f"{key}: {value}{eol}")
528+
injected = True
529+
out.append(line)
530+
return "".join(out)
531+
532+
@staticmethod
533+
def _skill_name_from_content(content: str) -> str | None:
534+
"""Return the ``name:`` field of a skill's first frontmatter block."""
535+
dash_count = 0
536+
for line in content.splitlines():
537+
stripped = line.rstrip("\r\n")
538+
if stripped == "---":
539+
dash_count += 1
540+
if dash_count == 2:
541+
break
542+
continue
543+
if dash_count == 1 and stripped.startswith("name:"):
544+
name = stripped[len("name:"):].strip().strip('"').strip("'")
545+
return name or None
546+
return None
547+
548+
@staticmethod
549+
def _is_core_command_skill(name: str | None) -> bool:
550+
from specify_cli.extensions import CORE_COMMAND_NAMES # lazy: avoids circular import
551+
552+
return bool(name) and name.startswith("speckit-") and (
553+
name.removeprefix("speckit-") in CORE_COMMAND_NAMES
554+
)
555+
485556
def post_process_skill_content(self, content: str) -> str:
486557
"""Inject shared hook guidance into Copilot skill content.
487558
488559
Delegates to :class:`_CopilotSkillsHelper` for shared post-processing.
489560
The ``mode:`` frontmatter field is intentionally omitted: VS Code
490561
Copilot Agent Skills do not support it (see issue #2799).
562+
563+
``disable-model-invocation: true`` is added to core command skills
564+
when the last ``setup()`` in this process was given
565+
``--no-model-invocation`` in the skills layout. Otherwise the key is
566+
omitted, which VS Code treats as ``false``.
491567
"""
492-
return _CopilotSkillsHelper().post_process_skill_content(content)
568+
updated = _CopilotSkillsHelper().post_process_skill_content(content)
569+
if self._disable_model_invocation and self._is_core_command_skill(
570+
self._skill_name_from_content(updated)
571+
):
572+
updated = self._inject_frontmatter_flag(
573+
updated, "disable-model-invocation"
574+
)
575+
return updated
493576

494577
def setup(
495578
self,
@@ -506,16 +589,24 @@ def setup(
506589
"""
507590
parsed_options = parsed_options or {}
508591
self._skills_mode = self.is_skills_mode(parsed_options, project_root)
509-
if self._skills_mode:
510-
created = self._setup_skills(project_root, manifest, parsed_options, **opts)
511-
else:
512-
created = self._setup_commands(project_root, manifest, parsed_options, **opts)
513-
514-
# Install agent runtime events
515-
event_files = self.emit_events(
516-
project_root, manifest, events=opts.get("events"), parsed_options=parsed_options,
517-
force=opts.get("force", False),
592+
self._disable_model_invocation = (
593+
self._skills_mode and parsed_options.get("no_model_invocation") is True
518594
)
595+
try:
596+
if self._skills_mode:
597+
created = self._setup_skills(project_root, manifest, parsed_options, **opts)
598+
else:
599+
created = self._setup_commands(project_root, manifest, parsed_options, **opts)
600+
601+
# Install agent runtime events
602+
event_files = self.emit_events(
603+
project_root, manifest, events=opts.get("events"), parsed_options=parsed_options,
604+
force=opts.get("force", False),
605+
)
606+
except BaseException:
607+
self._disable_model_invocation = False
608+
raise
609+
519610
created.extend(event_files)
520611
return created
521612

0 commit comments

Comments
 (0)