Skip to content
Open
8 changes: 4 additions & 4 deletions integrations/catalog.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-06-23T00:00:00Z",
"updated_at": "2026-07-15T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
"integrations": {
"claude": {
Expand Down 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"]
},
"trae": {
"id": "trae",
Expand Down
10 changes: 7 additions & 3 deletions src/specify_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,13 @@ def init(
}
from ..integrations.base import SkillsIntegration as _SkillsPersist

if isinstance(resolved_integration, _SkillsPersist) or getattr(
resolved_integration, "_skills_mode", False
):
_legacy_commands = bool(
(integration_parsed_options or {}).get("legacy_commands")
)
if (
isinstance(resolved_integration, _SkillsPersist)
or getattr(resolved_integration, "_skills_mode", False)
) and not _legacy_commands:
init_opts["ai_skills"] = True
save_init_options(project_path, init_opts)

Expand Down
3 changes: 1 addition & 2 deletions src/specify_cli/integrations/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,13 @@ 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
if isinstance(integration, SkillsIntegration) or getattr(integration, "_skills_mode", False):
if getattr(integration, "_skills_mode", False):
opts["ai_skills"] = True
else:
opts.pop("ai_skills", None)
Comment on lines +272 to 275
Expand Down
152 changes: 149 additions & 3 deletions src/specify_cli/integrations/bob/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,44 @@
"""IBM Bob integration."""
"""IBM Bob integration.

from ..base import MarkdownIntegration
Bob 2.0 uses the ``.bob/skills/speckit-<name>/SKILL.md`` layout.
The legacy ``.bob/commands/*.md`` layout (Bob 1.x) is available as an
opt-in for projects that have not yet migrated, via
``--integration-options "--legacy-commands"``.

Deprecation cycle:
This release: Skills layout is the default; legacy ``.bob/commands/``
is opt-in via ``--legacy-commands``.
Next cycle: ``--legacy-commands`` flag removed.
"""

from __future__ import annotations

import warnings
from pathlib import Path
from typing import Any

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


def _warn_legacy_commands_deprecated() -> None:
"""Warn that Bob's legacy markdown layout is being phased out."""
warnings.warn(
"Bob legacy commands mode (.bob/commands/) is deprecated and will be "
"removed in a future Spec Kit release. Omit --legacy-commands to use "
"the default skills layout (.bob/skills/).",
UserWarning,
stacklevel=3,
)


class _BobMarkdownHelper(MarkdownIntegration):
"""Internal helper used when Bob is scaffolded in legacy commands mode.

Not registered in the integration registry — only used as a delegate
by ``BobIntegration`` when ``--legacy-commands`` is passed.
"""

class BobIntegration(MarkdownIntegration):
key = "bob"
config = {
"name": "IBM Bob",
Expand All @@ -18,3 +53,114 @@ class BobIntegration(MarkdownIntegration):
"args": "$ARGUMENTS",
"extension": ".md",
}


class _BobSkillsHelper(SkillsIntegration):
"""Internal helper used when Bob is scaffolded in skills mode.

Not registered in the integration registry — only used as a delegate
by ``BobIntegration`` for skills-mode setup.
"""

key = "bob"
config = {
"name": "IBM Bob",
"folder": ".bob/",
"commands_subdir": "skills",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".bob/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}


class BobIntegration(IntegrationBase):
"""Integration for IBM Bob IDE.

Default mode: installs ``.bob/skills/speckit-<name>/SKILL.md`` files
(Bob 2.0 skills layout).

Legacy mode (``--legacy-commands``): installs
``.bob/commands/speckit.<name>.md`` files (Bob 1.x layout — deprecated).

Extends ``IntegrationBase`` directly so that ``isinstance(integration,
SkillsIntegration)`` is ``False`` and consumers such as
``_update_init_options_for_integration`` and the ``specify init``
next-steps builder derive the effective mode from ``_skills_mode``
rather than the class hierarchy. ``invoke_separator = "-"`` is set
explicitly to match the skills-default behaviour expected by
``CommandRegistrar.AGENT_CONFIGS``.
"""

key = "bob"
invoke_separator = "-"
config = {
"name": "IBM Bob",
"folder": ".bob/",
"commands_subdir": "skills",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".bob/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
Comment on lines +108 to +112
}

# Set by setup() to reflect the active mode; read by _helpers.py and
# init.py via getattr(integration, "_skills_mode", False).
_skills_mode: bool = False

@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--legacy-commands",
is_flag=True,
default=False,
Comment thread
davidebibm marked this conversation as resolved.
help=(
"Scaffold commands as legacy .bob/commands/*.md files "
"(Bob 1.x layout, deprecated) instead of the default "
"skills layout"
),
),
]

def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Bob commands.

Default: skills layout (``.bob/skills/speckit-<name>/SKILL.md``).
When ``parsed_options["legacy_commands"]`` is truthy, falls back to
the deprecated ``.bob/commands/speckit.<name>.md`` layout.
"""
parsed_options = parsed_options or {}
if parsed_options.get("legacy_commands"):
self._skills_mode = False
_warn_legacy_commands_deprecated()
return self._setup_legacy(project_root, manifest, parsed_options, **opts)
self._skills_mode = True
return SkillsIntegration.setup(
_BobSkillsHelper(), project_root, manifest, parsed_options, **opts
)
Comment on lines +152 to +155

def _setup_legacy(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Legacy mode: ``.bob/commands/speckit.<name>.md`` layout."""
helper = _BobMarkdownHelper()
return MarkdownIntegration.setup(helper, project_root, manifest, parsed_options, **opts)
Loading
Loading