Skip to content
Open
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
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)
Expand Down
160 changes: 157 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,122 @@ 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 = "-"
Comment on lines +99 to +100

def effective_invoke_separator(
self, parsed_options: dict[str, Any] | None = None
) -> str:
"""Return the invocation separator for the selected Bob layout."""
if parsed_options and parsed_options.get("legacy_commands"):
return "."
return "-"
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 +116 to +120
Comment on lines +116 to +120
}

# 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
Comment on lines +123 to +125

@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 +160 to +163

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