Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b01f1f6
feat: update Bob integration to skills-based layout for Bob 2.0
davidebibm Jul 8, 2026
e893245
Merge remote-tracking branch 'origin/main' into feat/update-bob-skill…
davidebibm Jul 13, 2026
dea23f0
PR comments fix: keep old Bob 1 commands till next release
davidebibm Jul 13, 2026
1c3d1cf
Copilot suggested change
davidebibm Jul 14, 2026
674a8db
feat(bob): address copilot comments, make skills layout default, demo…
davidebibm Jul 14, 2026
71ba3b9
fix(bob): honor legacy_commands in ai_skills persistence and add bob …
davidebibm Jul 14, 2026
d5e0e95
Copilot suggestion
davidebibm Jul 14, 2026
3679697
fix(bob): extend IntegrationBase directly to avoid false isinstance(S…
davidebibm Jul 14, 2026
64c0b9d
fix(lint): remove unused SkillsIntegration import from _helpers.py
davidebibm Jul 14, 2026
6ab8d61
Copilot suggested change
davidebibm Jul 14, 2026
7edf5b5
feat(bob): add bob skills integration with registrar-based mode detec…
davidebibm Jul 15, 2026
68bb876
address 3 comments from copilot
davidebibm Jul 15, 2026
f47ab61
feat(bob): update registrar config to use legacy commands layout
davidebibm Jul 15, 2026
d851289
fix lint
davidebibm Jul 15, 2026
73725c5
Suggested fix from Copilot
davidebibm Jul 16, 2026
230b287
Merge branch 'main' into feat/update-bob-skills-integration
davidebibm Jul 16, 2026
c6edc66
fix pr comment
davidebibm Jul 16, 2026
ef2cb53
fix pr comment
davidebibm Jul 16, 2026
cad76d3
fix pr comment
davidebibm Jul 16, 2026
5a8cb61
refactor(bob): resolve skills mode via base-class hooks + fix command…
mnriem Jul 16, 2026
04278b2
Merge remote-tracking branch 'origin/main' into mnriem-review-bob-ski…
mnriem Jul 16, 2026
d929bfb
fix(bob,copilot): address review — preserve legacy layout, dual-mode …
mnriem Jul 16, 2026
8a162b3
fix(bob): resolve command-ref separator with project-aware mode befor…
mnriem Jul 16, 2026
eeaf85f
fix(bob): scope persisted ai_skills flag to active agent when resolvi…
mnriem Jul 17, 2026
21922f8
fix(bob): detect Spec Kit layout from managed artifacts, not any skil…
mnriem Jul 17, 2026
13db5f3
fix(bob): apply managed-artifact detection on upgrade + consistent sk…
mnriem Jul 17, 2026
1597973
Merge branch 'main' of https://github.com/github/spec-kit into mnriem…
mnriem Jul 17, 2026
ba6655c
feat(bob): add --skills migration opt-in; fix separator + manifest lo…
mnriem Jul 17, 2026
d3408e6
docs(agents): align token-resolution comment with output-layout separ…
mnriem Jul 17, 2026
a3dd141
fix(bob): reconcile extension artifacts on layout change (review #3415)
mnriem Jul 17, 2026
779eb89
Merge remote-tracking branch 'upstream/main' into mnriem-review-bob-s…
mnriem Jul 17, 2026
d2321b2
fix(bob): reject layout migration when preset overrides are installed…
mnriem Jul 17, 2026
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: 1 addition & 1 deletion docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [Goose](https://goose-docs.ai/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
| [Grok Build](https://docs.x.ai/build/overview) | `grok` | Skills-based integration; installs skills into `.grok/skills` and invokes them as `/speckit-<command>` |
| [Hermes](https://github.com/NousResearch/hermes-agent) | `hermes` | Skills-based integration; installs skills globally into `~/.hermes/skills/` |
| [IBM Bob](https://www.ibm.com/products/bob) | `bob` | IDE-based agent |
| [IBM Bob](https://www.ibm.com/products/bob) | `bob` | Skills-based integration by default; installs skills as `speckit-<command>/SKILL.md` under `.bob/skills/` and invokes them as `/speckit-<command>`. Pass `--integration-options="--legacy-commands"` to scaffold the deprecated Bob 1.x layout (`.bob/commands/*.md`) instead; that flag will be removed in a future release. Existing legacy installs can migrate with `specify integration upgrade bob --integration-options="--skills"`, which converts them to the skills layout and removes the old command files. If preset overrides are installed, the migration is rejected with an actionable error (preset artifacts cannot yet be reconciled across a layout change) — remove the preset(s), migrate, then reinstall them. |
| [Junie](https://junie.jetbrains.com/) | `junie` | |
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | `kilocode` | |
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; installs into `.kimi-code/skills/`. `--migrate-legacy` moves old `.kimi/skills/` installs to the new paths |
Expand Down
6 changes: 3 additions & 3 deletions integrations/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@
"bob": {
"id": "bob",
"name": "IBM Bob",
"version": "1.0.0",
"description": "IBM Bob IDE integration",
"version": "2.0.0",
Comment thread
davidebibm marked this conversation as resolved.
"description": "IBM Bob 2.0 IDE skills-based integration",
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
"tags": ["ide", "ibm"]
"tags": ["ide", "ibm", "skills"]
Comment on lines +180 to +184
},
"trae": {
"id": "trae",
Expand Down
1 change: 1 addition & 0 deletions src/specify_cli/_invocation_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset(
{
"agy",
"bob",
Comment thread
davidebibm marked this conversation as resolved.
"claude",
Comment thread
davidebibm marked this conversation as resolved.
"copilot",
"cursor-agent",
Expand Down
42 changes: 39 additions & 3 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,37 @@ def register_commands(
is_cline_ext = agent_name == "cline" and source_id != "core"
source_root = source_dir.resolve()

# Resolve the command-reference separator for the file THIS registrar
# is about to write. The separator must match the *output layout* the
# registrar produces for this agent — not the project's persisted
# ``ai_skills`` flag, and not unrelated sibling directories on disk. A
# skill scaffold ("/SKILL.md") uses the skills separator; any
# command-layout output (".md", ".agent.md", ".toml", …) uses the
# command separator.
#
# This holds for the *active* agent too. Dual-layout agents (Bob,
# Copilot) write their skills via their own setup()/skills path, so
# ``register_commands`` only ever emits their command-layout files.
# Deriving the separator from ``ai_skills`` would render such a
# ``.bob/commands/*.md`` (or ``.github/agents/*.agent.md``) file with
# ``/speckit-*`` whenever that agent is active in skills mode — even
# though a command-layout file must use ``/speckit.*``. Deriving it
# from the agent's static output config avoids that mismatch and stays
# correct when a stale ``.bob/skills`` directory coexists with
# ``.bob/commands``.
_sep = agent_config.get("invoke_separator", ".")
try:
from specify_cli.integrations import get_integration # noqa: PLC0415

_integ = get_integration(agent_name)
if _integ is not None:
registrar_writes_skills = (
agent_config.get("extension") == "/SKILL.md"
)
_sep = _integ.invoke_separator_for_mode(registrar_writes_skills)
except Exception:
pass

for cmd_info in commands:
cmd_name = cmd_info["name"]
aliases = cmd_info.get("aliases", [])
Expand Down Expand Up @@ -709,13 +740,18 @@ def register_commands(
)

# Resolve __SPECKIT_COMMAND_*__ tokens using the agent's invoke separator.
# The separator is sourced from agent_config (populated by _build_agent_configs,
# which propagates each integration's invoke_separator class attribute).
# For dual-layout agents (e.g. Bob) the separator differs between the
# skills and command layouts, so a single static AGENT_CONFIGS value is
# insufficient. ``_sep`` (resolved above) is derived from the *output
# layout* this registrar writes — a "/SKILL.md" scaffold uses the skills
# separator, any command-layout file uses the command separator — not
# the project's persisted ai_skills state. Single-layout agents fall back
# to the static AGENT_CONFIGS value unchanged (invoke_separator_for_mode
# default).
# Deferred import of IntegrationBase avoids a circular import at module load
# (base.py itself imports CommandRegistrar lazily).
from specify_cli.integrations.base import IntegrationBase # noqa: PLC0415

_sep = agent_config.get("invoke_separator", ".")
body = IntegrationBase.resolve_command_refs(body, _sep)

output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
Expand Down
24 changes: 14 additions & 10 deletions src/specify_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ def init(
script_type=selected_script,
raw_options=integration_options,
parsed_options=integration_parsed_options or None,
project_root=project_path,
)
_write_integration_json(
project_path,
Expand All @@ -478,7 +479,7 @@ def init(
tracker=tracker,
force=force,
invoke_separator=resolved_integration.effective_invoke_separator(
integration_parsed_options
integration_parsed_options, project_root=project_path
),
)
tracker.complete(
Expand Down Expand Up @@ -532,10 +533,8 @@ def init(
"feature_numbering": "sequential",
"speckit_version": get_speckit_version(),
}
from ..integrations.base import SkillsIntegration as _SkillsPersist

if isinstance(resolved_integration, _SkillsPersist) or getattr(
resolved_integration, "_skills_mode", False
if resolved_integration.is_skills_mode(
integration_parsed_options or None, project_root=project_path
):
init_opts["ai_skills"] = True
save_init_options(project_path, init_opts)
Expand Down Expand Up @@ -683,11 +682,9 @@ def init(
steps_lines.append("1. You're already in the project directory!")
step_num = 2

from ..integrations.base import SkillsIntegration as _SkillsInt

_is_skills_integration = isinstance(
resolved_integration, _SkillsInt
) or getattr(resolved_integration, "_skills_mode", False)
_is_skills_integration = resolved_integration.is_skills_mode(
integration_parsed_options or None, project_root=project_path
)

codex_skill_mode = selected_ai == "codex" and _is_skills_integration
zcode_skill_mode = selected_ai == "zcode" and _is_skills_integration
Expand All @@ -703,6 +700,7 @@ def init(
zed_skill_mode = selected_ai == "zed" and _is_skills_integration
grok_skill_mode = selected_ai == "grok" and _is_skills_integration
cline_skill_mode = selected_ai == "cline"
bob_skill_mode = selected_ai == "bob" and _is_skills_integration
native_skill_mode = (
codex_skill_mode
or zcode_skill_mode
Expand All @@ -715,6 +713,7 @@ def init(
or devin_skill_mode
or zed_skill_mode
or grok_skill_mode
or bob_skill_mode
)

if codex_skill_mode:
Expand Down Expand Up @@ -752,6 +751,11 @@ def init(
f"{step_num}. Start Grok Build in this project directory; spec-kit skills were installed to [cyan].grok/skills[/cyan]"
)
step_num += 1
if bob_skill_mode:
steps_lines.append(
f"{step_num}. Start Bob in this project directory; spec-kit skills were installed to [cyan].bob/skills[/cyan]"
)
step_num += 1
usage_label = "skills" if native_skill_mode else "slash commands"

from .._invocation_style import (
Expand Down
12 changes: 8 additions & 4 deletions src/specify_cli/integration_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def with_integration_setting(
script_type: str | None = None,
raw_options: str | None = None,
parsed_options: dict[str, Any] | None = None,
project_root: Any = None,
) -> dict[str, dict[str, Any]]:
"""Return integration settings with *key* updated."""
settings = integration_settings(state)
Expand All @@ -63,7 +64,9 @@ def with_integration_setting(
elif raw_options is not None:
current.pop("parsed_options", None)

current["invoke_separator"] = integration.effective_invoke_separator(parsed_options)
current["invoke_separator"] = integration.effective_invoke_separator(
parsed_options, project_root
)
settings[key] = current
return settings

Expand All @@ -73,10 +76,11 @@ def invoke_separator_for_integration(
state: dict[str, Any],
key: str,
parsed_options: dict[str, Any] | None = None,
project_root: Any = None,
) -> str:
"""Resolve the invocation separator for stored/default integration state."""
if parsed_options is not None:
return integration.effective_invoke_separator(parsed_options)
return integration.effective_invoke_separator(parsed_options, project_root)

setting = integration_setting(state, key)
stored_separator = setting.get("invoke_separator")
Expand All @@ -85,6 +89,6 @@ def invoke_separator_for_integration(

stored_parsed = setting.get("parsed_options")
if isinstance(stored_parsed, dict):
return integration.effective_invoke_separator(stored_parsed)
return integration.effective_invoke_separator(stored_parsed, project_root)

return integration.effective_invoke_separator(None)
return integration.effective_invoke_separator(None, project_root)
23 changes: 10 additions & 13 deletions src/specify_cli/integrations/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,24 +272,19 @@ def _update_init_options_for_integration(
load_init_options,
save_init_options,
)
from .base import SkillsIntegration
opts = load_init_options(project_root)
opts["integration"] = integration.key
opts["ai"] = integration.key
opts["speckit_version"] = _get_speckit_version()
if script_type:
opts["script"] = script_type
# Skills mode is either intrinsic (SkillsIntegration), set on the instance
# during setup() (_skills_mode), or requested via parsed options (e.g.
# Copilot's --skills, persisted as parsed_options["skills"]). The latter is
# the only signal available on the `use` path, where no setup() runs and a
# fresh integration instance has _skills_mode == False (issue #3550).
skills_mode = (
isinstance(integration, SkillsIntegration)
or getattr(integration, "_skills_mode", False)
or bool((parsed_options or {}).get("skills"))
)
if skills_mode:
# Whether skills mode is active is owned by each integration via the
# ``is_skills_mode`` hook (base default honors ``--skills``;
# SkillsIntegration returns True; skills-first integrations with a legacy
# opt-out such as Bob override it). This keeps shared code free of
# ``isinstance`` / ``_skills_mode`` probing. Passing parsed_options lets it
# work on the ``use``/``install`` path where no setup() runs (issue #3550).
if integration.is_skills_mode(parsed_options, project_root=project_root):
opts["ai_skills"] = True
else:
opts.pop("ai_skills", None)
Expand Down Expand Up @@ -325,6 +320,7 @@ def _set_default_integration(
script_type=resolved_script,
raw_options=raw_options,
parsed_options=parsed_options,
project_root=project_root,
)

if refresh_templates:
Expand All @@ -333,7 +329,8 @@ def _set_default_integration(
project_root,
resolved_script,
invoke_separator=_invoke_separator_for_integration(
integration, {"integration_settings": settings}, key, parsed_options
integration, {"integration_settings": settings}, key, parsed_options,
project_root=project_root,
),
force=refresh_templates_force,
refresh_managed=True,
Expand Down
11 changes: 9 additions & 2 deletions src/specify_cli/integrations/_install_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ def integration_install(
project_root,
selected_script,
invoke_separator=_invoke_separator_for_integration(
infra_integration, current, infra_key, infra_parsed
infra_integration, current, infra_key, infra_parsed,
project_root=project_root,
),
)
if os.name != "nt":
Expand Down Expand Up @@ -155,10 +156,16 @@ def integration_install(
script_type=selected_script,
raw_options=raw_options,
parsed_options=parsed_options,
project_root=project_root,
)
_write_integration_json(project_root, new_default, new_installed, settings)
if new_default == integration.key:
_update_init_options_for_integration(project_root, integration, script_type=selected_script)
_update_init_options_for_integration(
project_root,
integration,
script_type=selected_script,
parsed_options=parsed_options,
)
else:
_refresh_init_options_speckit_version(project_root)

Expand Down
Loading