Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/guides/adapter-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ All methods should preserve runtime contracts (`@icontract`) and runtime type ch
- `tool`, `version`, `layout`, `specs_dir`
- `supported_sync_modes`
- `has_external_config`, `has_custom_hooks`
- `extensions`, `extension_commands` β€” detected tool extensions and their provided commands
- `presets` β€” active preset names (e.g., from `presets/` directory)
- `hook_events` β€” detected hook event types (e.g., `before_task`, `after_task`)
- `detected_version_source` β€” how version was detected: `"cli"`, `"heuristic"`, or `None`

Sync selection and safe behavior depend on this model.

Expand Down
4 changes: 3 additions & 1 deletion docs/guides/integrations-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ SpecFact CLI integrations fall into four main categories:

**What it provides**:

- βœ… Interactive slash commands (`/speckit.specify`, `/speckit.plan`) with AI assistance
- βœ… Interactive slash commands (`/speckit.specify`, `/speckit.plan`, `/speckit.tasks`, `/speckit.implement`, `/speckit.constitution`, `/speckit.clarify`, `/speckit.analyze`) with AI assistance
- βœ… Rapid prototyping workflow: spec β†’ plan β†’ tasks β†’ code
- βœ… Constitution and planning for new features
- βœ… IDE integration with CoPilot chat
- βœ… Extension ecosystem β€” 46+ community extensions with pluggable presets
- βœ… Version-aware detection β€” SpecFact auto-detects Spec-Kit version, extensions, presets, and hook events

**When to use**:

Expand Down
2 changes: 2 additions & 0 deletions docs/guides/speckit-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ permalink: /guides/speckit-comparison/
| **GitHub integration** | βœ… Native slash commands | βœ… GitHub Actions + CLI | Spec-Kit for native integration |
| **Learning curve** | βœ… Low (markdown + slash commands) | ⚠️ Medium (decorators + contracts) | Spec-Kit for ease of use |
| **High-risk brownfield** | ⚠️ Good documentation | βœ… Formal verification | **SpecFact for high-risk** |
| **Extension awareness** | βœ… 46+ community extensions | βœ… Auto-detects extensions, presets, hooks | SpecFact bridges extension metadata |
| **Version detection** | N/A | βœ… CLI + heuristic detection (v0.4.x) | SpecFact adapts to detected version |
| **Free tier** | βœ… Open-source | βœ… Apache 2.0 | Both free |

---
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/speckit-journey.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ permalink: /guides/speckit-journey/

Spec-Kit is **excellent** for:

- βœ… **Interactive Specification** - Slash commands (`/speckit.specify`, `/speckit.plan`) with AI assistance
- βœ… **Interactive Specification** - Slash commands (`/speckit.specify`, `/speckit.plan`, `/speckit.tasks`, `/speckit.implement`, `/speckit.constitution`, `/speckit.clarify`, `/speckit.analyze`) with AI assistance
- βœ… **Rapid Prototyping** - Quick spec β†’ plan β†’ tasks β†’ code workflow for **NEW features**
- βœ… **Learning & Exploration** - Great for understanding state machines, contracts, requirements
- βœ… **IDE Integration** - CoPilot chat makes it accessible to less technical developers
Expand Down
44 changes: 44 additions & 0 deletions openspec/changes/speckit-02-v04-adapter-alignment/TDD_EVIDENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# TDD Evidence: speckit-02-v04-adapter-alignment

## Implementation Order

Production code for tasks 1.1–7.2 was written first (ToolCapabilities fields, scanner methods, version detection, bridge presets, get_capabilities integration). Tests were then written targeting all new behavior.

## Post-Implementation Test Run (passing)

**Timestamp:** 2026-03-27T23:13:50Z

**Command:** `hatch test -- tests/unit/models/test_capabilities.py tests/unit/models/test_bridge.py tests/unit/importers/test_speckit_scanner.py tests/unit/adapters/test_speckit.py -v`

**Result:** 110 passed in 4.90s

New tests added:
- `TestToolCapabilitiesV04Fields` β€” 8 tests (backward compat, all new fields)
- `TestScanExtensions` β€” 7 tests (catalog parsing, ignore, malformed JSON, merge)
- `TestScanPresets` β€” 4 tests (JSON, directory, malformed fallback)
- `TestScanHookEvents` β€” 4 tests (pattern detection, sorting, edge cases)
- `TestVersionDetection` β€” 8 tests (heuristics, CLI mock, priority)
- `TestGetCapabilitiesV04` β€” 7 tests (extensions, presets, hooks, version, cross-repo, legacy)
- `TestBridgeConfigPresets` β€” 4 new parametrized tests (7-command set validation)

## Full Suite Run (passing)

**Timestamp:** 2026-03-27T23:13:50Z

**Command:** `hatch test --cover -v`

**Result:** 2248 passed, 9 skipped in 171.02s

## Quality Gates

| Gate | Result | Timestamp |
|------|--------|-----------|
| `hatch run format` | All checks passed | 2026-03-27T23:12Z |
| `hatch run type-check` | 0 errors, 1437 warnings | 2026-03-27T23:12Z |
| `hatch run contract-test` | Passed (cached) | 2026-03-27T23:12Z |
| `hatch test --cover -v` | 2248 passed, 0 failed | 2026-03-27T23:13Z |
| `specfact code review run` | PASS, Score 120, 0 findings | 2026-03-27T23:13Z |

## Note

Production code was written before tests in this change (not strict TDD red-green-refactor). The OpenSpec change was created with specs and design first, followed by implementation and then test coverage. All new public methods have `@beartype` and `@icontract` contracts as primary validation, with unit tests as secondary coverage per project conventions.
2 changes: 2 additions & 0 deletions openspec/changes/speckit-02-v04-adapter-alignment/proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ GitHub Spec-Kit has advanced from an early-stage CLI to v0.4.3 with 46 community
## Capabilities

### New Capabilities

- `speckit-extension-catalog`: Detection, parsing, and modeling of spec-kit extension catalogs (community and core) and their provided commands
- `speckit-version-detection`: Version detection strategies for spec-kit installations (CLI probe, directory heuristics, preset presence)

### Modified Capabilities

- `bridge-adapter`: Expanded SpecKitAdapter with extension awareness, preset detection, hook modeling, and version-gated feature flags
- `bridge-registry`: ToolCapabilities extended with extension/preset/hook metadata fields

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The system SHALL return comprehensive tool capabilities including extension meta
- **THEN** extension and preset detection uses the `external_base_path` as base
- **AND** CLI version detection is skipped for cross-repo scenarios (filesystem-only)

## MODIFIED Requirements
## MODIFIED Requirements: Repository Detection

### Requirement: SpecKitAdapter detect identifies spec-kit repositories

Expand Down
58 changes: 29 additions & 29 deletions openspec/changes/speckit-02-v04-adapter-alignment/tasks.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
## 1. Expand ToolCapabilities dataclass

- [ ] 1.1 Add optional fields to `ToolCapabilities` in `src/specfact_cli/models/capabilities.py`: `extensions: list[str] | None`, `extension_commands: dict[str, list[str]] | None`, `presets: list[str] | None`, `hook_events: list[str] | None`, `detected_version_source: str | None`
- [ ] 1.2 Add unit tests for `ToolCapabilities` construction with new fields and verify backward compatibility (all new fields default to `None`)
- [ ] 1.3 Add `@beartype` and `@ensure` contracts on any new methods that consume the expanded fields
- [x] 1.1 Add optional fields to `ToolCapabilities` in `src/specfact_cli/models/capabilities.py`: `extensions: list[str] | None`, `extension_commands: dict[str, list[str]] | None`, `presets: list[str] | None`, `hook_events: list[str] | None`, `detected_version_source: str | None`
- [x] 1.2 Add unit tests for `ToolCapabilities` construction with new fields and verify backward compatibility (all new fields default to `None`)
- [x] 1.3 Add `@beartype` and `@ensure` contracts on any new methods that consume the expanded fields

## 2. Extension catalog detection in SpecKitScanner

- [ ] 2.1 Add `scan_extensions(self) -> list[dict]` method to `SpecKitScanner` in `src/specfact_cli/importers/speckit_scanner.py` β€” parse `extensions/catalog.community.json` and `extensions/catalog.core.json`
- [ ] 2.2 Add `.extensionignore` parsing β€” read ignore file and filter excluded extensions from scan results
- [ ] 2.3 Add defensive JSON parsing with warning logging for malformed catalogs
- [ ] 2.4 Add unit tests for `scan_extensions()`: catalog present, no catalog, malformed JSON, extensionignore filtering
- [x] 2.1 Add `scan_extensions(self) -> list[dict]` method to `SpecKitScanner` in `src/specfact_cli/importers/speckit_scanner.py` β€” parse `extensions/catalog.community.json` and `extensions/catalog.core.json`
- [x] 2.2 Add `.extensionignore` parsing β€” read ignore file and filter excluded extensions from scan results
- [x] 2.3 Add defensive JSON parsing with warning logging for malformed catalogs
- [x] 2.4 Add unit tests for `scan_extensions()`: catalog present, no catalog, malformed JSON, extensionignore filtering

## 3. Version detection in SpecKitAdapter

- [ ] 3.1 Add `_detect_version_from_cli(repo_path: Path) -> str | None` method to `SpecKitAdapter` β€” run `specify --version` with 5-second timeout, parse version string
- [ ] 3.2 Add `_detect_version_from_heuristics(repo_path: Path) -> str | None` method β€” check for `presets/` (>=0.3.0), `extensions/` (>=0.2.0), `.specify/` (>=0.1.0)
- [ ] 3.3 Integrate version detection into `get_capabilities()`: try CLI first, fall back to heuristics, populate `version` and `detected_version_source`
- [ ] 3.4 Add unit tests for both detection methods and the integration flow (CLI available, CLI missing, heuristic fallback, timeout)
- [x] 3.1 Add `_detect_version_from_cli(repo_path: Path) -> str | None` method to `SpecKitAdapter` β€” run `specify --version` with 5-second timeout, parse version string
- [x] 3.2 Add `_detect_version_from_heuristics(repo_path: Path) -> str | None` method β€” check for `presets/` (>=0.3.0), `extensions/` (>=0.2.0), `.specify/` (>=0.1.0)
- [x] 3.3 Integrate version detection into `get_capabilities()`: try CLI first, fall back to heuristics, populate `version` and `detected_version_source`
- [x] 3.4 Add unit tests for both detection methods and the integration flow (CLI available, CLI missing, heuristic fallback, timeout)

## 4. Preset detection in SpecKitScanner

- [ ] 4.1 Add `scan_presets(self) -> list[str]` method to `SpecKitScanner` β€” scan `presets/` directory for preset catalog files
- [ ] 4.2 Add unit tests for preset detection: presets present, no presets directory
- [x] 4.1 Add `scan_presets(self) -> list[str]` method to `SpecKitScanner` β€” scan `presets/` directory for preset catalog files
- [x] 4.2 Add unit tests for preset detection: presets present, no presets directory

## 5. Hook event detection

- [ ] 5.1 Add `scan_hook_events(self) -> list[str]` method to `SpecKitScanner` β€” detect before/after hook wiring in `.specify/prompts/` template files
- [ ] 5.2 Add unit tests for hook event detection
- [x] 5.1 Add `scan_hook_events(self) -> list[str]` method to `SpecKitScanner` β€” detect before/after hook wiring in `.specify/prompts/` template files
- [x] 5.2 Add unit tests for hook event detection

## 6. Expand BridgeConfig command mappings

- [ ] 6.1 Update `preset_speckit_classic()` in `src/specfact_cli/models/bridge.py` to include all 7 slash commands: specify, plan, tasks, implement, constitution, clarify, analyze
- [ ] 6.2 Update `preset_speckit_specify()` with the same 7 command mappings
- [ ] 6.3 Update `preset_speckit_modern()` with the same 7 command mappings
- [ ] 6.4 Add unit tests verifying all 3 presets contain the full 7-command set
- [x] 6.1 Update `preset_speckit_classic()` in `src/specfact_cli/models/bridge.py` to include all 7 slash commands: specify, plan, tasks, implement, constitution, clarify, analyze
- [x] 6.2 Update `preset_speckit_specify()` with the same 7 command mappings
- [x] 6.3 Update `preset_speckit_modern()` with the same 7 command mappings
- [x] 6.4 Add unit tests verifying all 3 presets contain the full 7-command set

## 7. Integrate extensions/presets/hooks into SpecKitAdapter.get_capabilities()

- [ ] 7.1 Update `get_capabilities()` in `src/specfact_cli/adapters/speckit.py` to call scanner methods and populate new `ToolCapabilities` fields
- [ ] 7.2 Ensure cross-repo scenarios (`external_base_path`) use filesystem-only detection (skip CLI probe)
- [ ] 7.3 Add integration tests for full `get_capabilities()` flow with v0.4.x repo structure
- [ ] 7.4 Add integration test for legacy repo structure (verify backward compat β€” all new fields are `None`)
- [x] 7.1 Update `get_capabilities()` in `src/specfact_cli/adapters/speckit.py` to call scanner methods and populate new `ToolCapabilities` fields
- [x] 7.2 Ensure cross-repo scenarios (`external_base_path`) use filesystem-only detection (skip CLI probe)
- [x] 7.3 Add integration tests for full `get_capabilities()` flow with v0.4.x repo structure
- [x] 7.4 Add integration test for legacy repo structure (verify backward compat β€” all new fields are `None`)

## 8. Documentation updates

- [ ] 8.1 Update `docs/guides/speckit-comparison.md` feature matrix with new detection capabilities
- [ ] 8.2 Update `docs/guides/speckit-journey.md` workflow steps to reference extension and preset awareness
- [ ] 8.3 Review and update any adapter reference docs that mention spec-kit capabilities
- [x] 8.1 Update `docs/guides/speckit-comparison.md` feature matrix with new detection capabilities
- [x] 8.2 Update `docs/guides/speckit-journey.md` workflow steps to reference extension and preset awareness
- [x] 8.3 Review and update any adapter reference docs that mention spec-kit capabilities

## 9. Contract and quality gates

- [ ] 9.1 Ensure all new public methods have `@icontract` (`@require`/`@ensure`) and `@beartype` decorators
- [ ] 9.2 Run `hatch run format && hatch run type-check && hatch run contract-test && hatch test --cover -v`
- [ ] 9.3 Record TDD evidence in `TDD_EVIDENCE.md`
- [x] 9.1 Ensure all new public methods have `@icontract` (`@require`/`@ensure`) and `@beartype` decorators
- [x] 9.2 Run `hatch run format && hatch run type-check && hatch run contract-test && hatch test --cover -v`
- [x] 9.3 Record TDD evidence in `TDD_EVIDENCE.md`
112 changes: 89 additions & 23 deletions src/specfact_cli/adapters/speckit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@

import hashlib
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any

from beartype import beartype
from icontract import ensure, require

from specfact_cli.adapters.base import BridgeAdapter
from specfact_cli.common import get_bridge_logger
from specfact_cli.importers.speckit_converter import SpecKitConverter
from specfact_cli.importers.speckit_scanner import SpecKitScanner
from specfact_cli.models.bridge import BridgeConfig
Expand All @@ -35,6 +38,9 @@
)


logger = get_bridge_logger(__name__)


class SpecKitAdapter(BridgeAdapter):
"""
Spec-Kit bridge adapter implementing BridgeAdapter interface.
Expand Down Expand Up @@ -83,6 +89,73 @@ def detect(self, repo_path: Path, bridge_config: BridgeConfig | None = None) ->
or (docs_specs_dir.exists() and docs_specs_dir.is_dir())
)

@beartype
@ensure(lambda result: result is None or isinstance(result, str), "Must return str or None")
def _detect_version_from_cli(self, repo_path: Path) -> str | None:
"""Attempt to detect spec-kit version by running the specify CLI."""
if not shutil.which("specify"):
return None
try:
result = subprocess.run(
["specify", "--version"],
capture_output=True,
text=True,
timeout=5,
cwd=str(repo_path),
)
if result.returncode == 0 and result.stdout.strip():
version_match = re.search(r"(\d+\.\d+\.\d+)", result.stdout)
if version_match:
return version_match.group(1)
except subprocess.TimeoutExpired:
logger.debug("specify --version timed out after 5 seconds")
except OSError:
pass
return None

@beartype
@ensure(lambda result: result is None or isinstance(result, str), "Must return str or None")
def _detect_version_from_heuristics(self, repo_path: Path) -> str | None:
"""Estimate spec-kit version from directory structure presence."""
if (repo_path / "presets").is_dir():
return ">=0.3.0"
if (repo_path / "extensions").is_dir():
return ">=0.2.0"
if (repo_path / ".specify").is_dir():
return ">=0.1.0"
return None

@staticmethod
def _detect_layout(base_path: Path) -> tuple[str, str]:
"""Determine spec-kit layout type and specs directory from repo structure."""
if (base_path / "docs" / "specs").exists():
return "modern", "docs/specs"
if (base_path / ".specify").exists():
return "modern", "specs"
return "classic", "specs"

def _detect_version(self, base_path: Path, *, skip_cli: bool) -> tuple[str | None, str | None]:
"""Detect spec-kit version, returning (version, source)."""
if not skip_cli:
version = self._detect_version_from_cli(base_path)
if version:
return version, "cli"
version = self._detect_version_from_heuristics(base_path)
if version:
return version, "heuristic"
return None, None

@staticmethod
def _extract_extension_fields(
ext_list: list[dict[str, Any]],
) -> tuple[list[str] | None, dict[str, list[str]] | None]:
"""Extract extension names and command maps from scanner output."""
if not ext_list:
return None, None
names = [e["name"] for e in ext_list]
commands = {e["name"]: e.get("commands", []) for e in ext_list}
return names, commands

@beartype
@require(require_repo_path_exists, "Repository path must exist")
@require(require_repo_path_is_dir, "Repository path must be a directory")
Expand All @@ -98,37 +171,30 @@ def get_capabilities(self, repo_path: Path, bridge_config: BridgeConfig | None =
Returns:
ToolCapabilities instance for Spec-Kit adapter
"""
base_path = repo_path
if bridge_config and bridge_config.external_base_path:
base_path = bridge_config.external_base_path

# Determine layout (classic vs modern)
specify_dir = base_path / ".specify"
docs_specs_dir = base_path / "docs" / "specs"

if docs_specs_dir.exists():
layout = "modern"
specs_dir_path = "docs/specs"
elif specify_dir.exists():
layout = "modern"
specs_dir_path = "specs"
else:
layout = "classic"
specs_dir_path = "specs"
is_cross_repo = bridge_config is not None and bridge_config.external_base_path is not None
base_path: Path = bridge_config.external_base_path if is_cross_repo else repo_path # type: ignore[assignment]

# Check for constitution file (set has_custom_hooks flag)
layout, specs_dir_path = self._detect_layout(base_path)
scanner = SpecKitScanner(base_path)
has_constitution, _ = scanner.has_constitution()
has_custom_hooks = has_constitution
version, detected_version_source = self._detect_version(base_path, skip_cli=is_cross_repo)
extensions, extension_commands = self._extract_extension_fields(scanner.scan_extensions())
preset_names = scanner.scan_presets()
hook_list = scanner.scan_hook_events()

return ToolCapabilities(
tool="speckit",
version=None, # Spec-Kit version not tracked in files
version=version,
layout=layout,
specs_dir=specs_dir_path,
has_external_config=bridge_config is not None and bridge_config.external_base_path is not None,
has_custom_hooks=has_custom_hooks,
supported_sync_modes=["bidirectional", "unidirectional"], # Spec-Kit supports bidirectional sync
has_external_config=is_cross_repo,
has_custom_hooks=has_constitution,
supported_sync_modes=["bidirectional", "unidirectional"],
extensions=extensions,
extension_commands=extension_commands,
presets=preset_names or None,
hook_events=hook_list or None,
detected_version_source=detected_version_source,
)

@beartype
Expand Down
Loading
Loading