diff --git a/.speckit/bugfixes/SKILZ-081-agent-lookup-order/spec.md b/.speckit/bugfixes/SKILZ-081-agent-lookup-order/spec.md new file mode 100644 index 0000000..e524831 --- /dev/null +++ b/.speckit/bugfixes/SKILZ-081-agent-lookup-order/spec.md @@ -0,0 +1,56 @@ +# SKILZ-081: Agent Lookup Order + +## Status: COMPLETED + +## Problem Statement + +Agent detection lookup order is incorrect. The `detect_agent()` function is missing a parent directory check for the universal agent pattern (`../skilz/skills`). + +## Root Cause + +When a project is nested inside a directory that has a `skilz/skills` folder in its parent, the agent detection should recognize this as a "universal" agent pattern. This check was missing from the detection order. + +## Solution + +1. Add `_check_parent_skilz()` function to check for `../skilz/skills` directory +2. Update `detect_agent()` to call parent check after config override but before marker detection + +## Files Modified + +- `src/skilz/agents.py` + - Added `_check_parent_skilz(project_dir: Path) -> str | None` function + - Updated `detect_agent()` to include parent directory check in priority order + +## Implementation Details + +```python +def _check_parent_skilz(project_dir: Path) -> str | None: + """Check for ../skilz/skills directory (universal agent pattern).""" + parent = project_dir.parent + parent_skilz = parent / "skilz" / "skills" + if parent_skilz.exists() and parent_skilz.is_dir(): + logger.debug("[SKILZ-081] Found parent skilz/skills at %s", parent_skilz) + return "universal" + return None +``` + +## Detection Order (Updated) + +1. Check config file for `agent_default` setting +2. **NEW: Check for `../skilz/skills` (parent directory universal pattern)** +3. Check for `.claude/` in project directory +4. Check for `.gemini/` in project directory +5. Check for `.codex/` in project directory +6. Check for `~/.claude/` (user has Claude Code installed) +7. Check for `~/.gemini/` (user has Gemini CLI) +8. Check for `~/.codex/` (user has OpenAI Codex) +9. Check for `~/.config/opencode/` (user has OpenCode) +10. Default to "claude" + +## Acceptance Criteria + +- [x] `_check_parent_skilz()` function added +- [x] `detect_agent()` calls parent check after config but before markers +- [x] Returns "universal" when parent skilz/skills exists +- [x] All existing tests pass +- [x] Type checking passes diff --git a/.speckit/bugfixes/SKILZ-081-agent-lookup-order/tasks.md b/.speckit/bugfixes/SKILZ-081-agent-lookup-order/tasks.md new file mode 100644 index 0000000..88acc01 --- /dev/null +++ b/.speckit/bugfixes/SKILZ-081-agent-lookup-order/tasks.md @@ -0,0 +1,33 @@ +# SKILZ-081: Agent Lookup Order - Tasks + +## Status: COMPLETED + +## Tasks + +- [x] Add logging import to `agents.py` +- [x] Create `_check_parent_skilz()` function +- [x] Update `detect_agent()` docstring with new detection order +- [x] Add parent check call after config override check +- [x] Add debug logging for parent skilz detection +- [x] Run existing tests to verify no regressions +- [x] Run type checking (mypy) +- [x] Run linting (ruff) + +## Verification + +```bash +# All 640 tests pass +pytest -v + +# Type checking passes +mypy src/skilz/ + +# Linting passes +ruff check src/skilz/ +``` + +## Completion + +- **Date**: 2026-01-20 +- **PR**: https://github.com/SpillwaveSolutions/skilz-cli/pull/43 +- **Commit**: fix/skilz-bugfixes-081-085-086-089 diff --git a/.speckit/bugfixes/SKILZ-085-default-agent/spec.md b/.speckit/bugfixes/SKILZ-085-default-agent/spec.md new file mode 100644 index 0000000..3adeb4d --- /dev/null +++ b/.speckit/bugfixes/SKILZ-085-default-agent/spec.md @@ -0,0 +1,38 @@ +# SKILZ-085: Default Agent Should Be Claude + +## Status: COMPLETED + +## Problem Statement + +Default agent fallback is "gemini" instead of "claude". When no agent markers are found, the system should default to Claude Code as the primary supported agent. + +## Root Cause + +The `detect_agent()` function's final fallback was returning "claude" but without proper logging. The config.py already had the correct default, but explicit logging was needed for debugging. + +## Solution + +1. Ensure `detect_agent()` final return is "claude" with debug logging +2. Verify `config.py` has `DEFAULT_AGENT = "claude"` (already correct) + +## Files Modified + +- `src/skilz/agents.py` + - Added debug logging before final "claude" return + - Log message: `[SKILZ-085] No agent markers found, using default: claude` + +## Implementation Details + +```python +# In detect_agent() function, final fallback: +logger.debug("[SKILZ-085] No agent markers found, using default: claude") +return "claude" +``` + +## Acceptance Criteria + +- [x] `detect_agent()` returns "claude" when no markers found +- [x] Debug logging added for traceability +- [x] `config.py` DEFAULT_AGENT is "claude" +- [x] All existing tests pass +- [x] Type checking passes diff --git a/.speckit/bugfixes/SKILZ-085-default-agent/tasks.md b/.speckit/bugfixes/SKILZ-085-default-agent/tasks.md new file mode 100644 index 0000000..5b2ba79 --- /dev/null +++ b/.speckit/bugfixes/SKILZ-085-default-agent/tasks.md @@ -0,0 +1,31 @@ +# SKILZ-085: Default Agent Should Be Claude - Tasks + +## Status: COMPLETED + +## Tasks + +- [x] Verify `config.py` has correct DEFAULT_AGENT value +- [x] Add debug logging to `detect_agent()` final fallback +- [x] Ensure return value is "claude" +- [x] Run existing tests to verify no regressions +- [x] Run type checking (mypy) +- [x] Run linting (ruff) + +## Verification + +```bash +# All 640 tests pass +pytest -v + +# Type checking passes +mypy src/skilz/ + +# Linting passes +ruff check src/skilz/ +``` + +## Completion + +- **Date**: 2026-01-20 +- **PR**: https://github.com/SpillwaveSolutions/skilz-cli/pull/43 +- **Commit**: fix/skilz-bugfixes-081-085-086-089 diff --git a/.speckit/bugfixes/SKILZ-086-version-mismatch/spec.md b/.speckit/bugfixes/SKILZ-086-version-mismatch/spec.md new file mode 100644 index 0000000..a2946b3 --- /dev/null +++ b/.speckit/bugfixes/SKILZ-086-version-mismatch/spec.md @@ -0,0 +1,52 @@ +# SKILZ-086: Version Mismatch + +## Status: COMPLETED + +## Problem Statement + +CLI `--version` shows wrong version. Version was defined in two places (`__init__.py` and `pyproject.toml`) which could get out of sync. + +## Root Cause + +The `__version__` in `src/skilz/__init__.py` was hardcoded as a string literal (e.g., `"1.7.0"`), while the canonical version is defined in `pyproject.toml`. These could drift apart during releases. + +## Solution + +Use `importlib.metadata.version()` to read the version from the installed package metadata, which is derived from `pyproject.toml`. This creates a single source of truth. + +## Files Modified + +- `src/skilz/__init__.py` + - Replaced hardcoded `__version__ = "1.7.0"` with dynamic version lookup + - Added fallback to `"0.0.0.dev"` for development installs + +## Implementation Details + +**Before**: +```python +__version__ = "1.7.0" +``` + +**After**: +```python +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("skilz") +except PackageNotFoundError: + __version__ = "0.0.0.dev" +``` + +## Benefits + +1. **Single source of truth**: Version only defined in `pyproject.toml` +2. **Automatic sync**: CLI `--version` always matches installed package +3. **Development friendly**: Falls back to dev version when not installed + +## Acceptance Criteria + +- [x] `__version__` uses `importlib.metadata.version()` +- [x] Fallback to "0.0.0.dev" for PackageNotFoundError +- [x] `skilz --version` shows correct version after `pip install -e .` +- [x] All existing tests pass +- [x] Type checking passes diff --git a/.speckit/bugfixes/SKILZ-086-version-mismatch/tasks.md b/.speckit/bugfixes/SKILZ-086-version-mismatch/tasks.md new file mode 100644 index 0000000..3543acb --- /dev/null +++ b/.speckit/bugfixes/SKILZ-086-version-mismatch/tasks.md @@ -0,0 +1,38 @@ +# SKILZ-086: Version Mismatch - Tasks + +## Status: COMPLETED + +## Tasks + +- [x] Import `version` and `PackageNotFoundError` from `importlib.metadata` +- [x] Replace hardcoded version with `version("skilz")` call +- [x] Add try/except for PackageNotFoundError +- [x] Set fallback version to "0.0.0.dev" +- [x] Run existing tests to verify no regressions +- [x] Run type checking (mypy) +- [x] Run linting (ruff) + +## Verification + +```bash +# Reinstall package +pip install -e . + +# Check version +skilz --version + +# All 640 tests pass +pytest -v + +# Type checking passes +mypy src/skilz/ + +# Linting passes +ruff check src/skilz/ +``` + +## Completion + +- **Date**: 2026-01-20 +- **PR**: https://github.com/SpillwaveSolutions/skilz-cli/pull/43 +- **Commit**: fix/skilz-bugfixes-081-085-086-089 diff --git a/.speckit/bugfixes/SKILZ-089-git-removal/spec.md b/.speckit/bugfixes/SKILZ-089-git-removal/spec.md new file mode 100644 index 0000000..b27eada --- /dev/null +++ b/.speckit/bugfixes/SKILZ-089-git-removal/spec.md @@ -0,0 +1,62 @@ +# SKILZ-089: .git Removal on Local Install + +## Status: COMPLETED + +## Problem Statement + +When installing a local skill that contains a `.git` directory, the `.git` directory is copied to the target location. This causes nested repository issues and can confuse Git operations. + +## Root Cause + +The `shutil.copytree()` calls in `installer.py` and `link_ops.py` did not have an `ignore` parameter to exclude the `.git` directory. + +## Solution + +Add `ignore=shutil.ignore_patterns('.git')` to all `shutil.copytree()` calls that copy skill directories. + +## Files Modified + +- `src/skilz/installer.py` + - Updated `copy_skill_files()` function to exclude `.git` +- `src/skilz/link_ops.py` + - Updated `copy_skill()` function to exclude `.git` + +## Implementation Details + +**installer.py - copy_skill_files() (~line 80)**: +```python +# SKILZ-089: Exclude .git directory to prevent nested repo issues +shutil.copytree( + source_dir, + target_dir, + symlinks=True, + ignore_dangling_symlinks=True, + ignore=shutil.ignore_patterns(".git"), +) +``` + +**link_ops.py - copy_skill() (~line 78)**: +```python +# SKILZ-089: Exclude .git directory to prevent nested repo issues +shutil.copytree( + source, + target, + symlinks=True, + ignore_dangling_symlinks=True, + ignore=shutil.ignore_patterns(".git"), +) +``` + +## Benefits + +1. **No nested repos**: Installed skills don't contain `.git` directories +2. **Cleaner installs**: Reduces installed skill size +3. **No Git confusion**: Parent repo operations work correctly + +## Acceptance Criteria + +- [x] `installer.py` copytree excludes `.git` +- [x] `link_ops.py` copytree excludes `.git` +- [x] Local skill installs don't copy `.git` directory +- [x] All existing tests pass +- [x] Type checking passes diff --git a/.speckit/bugfixes/SKILZ-089-git-removal/tasks.md b/.speckit/bugfixes/SKILZ-089-git-removal/tasks.md new file mode 100644 index 0000000..5a0fa10 --- /dev/null +++ b/.speckit/bugfixes/SKILZ-089-git-removal/tasks.md @@ -0,0 +1,31 @@ +# SKILZ-089: .git Removal on Local Install - Tasks + +## Status: COMPLETED + +## Tasks + +- [x] Update `copy_skill_files()` in `installer.py` to add ignore pattern +- [x] Update `copy_skill()` in `link_ops.py` to add ignore pattern +- [x] Add comment explaining SKILZ-089 fix +- [x] Run existing tests to verify no regressions +- [x] Run type checking (mypy) +- [x] Run linting (ruff) + +## Verification + +```bash +# All 640 tests pass +pytest -v + +# Type checking passes +mypy src/skilz/ + +# Linting passes +ruff check src/skilz/ +``` + +## Completion + +- **Date**: 2026-01-20 +- **PR**: https://github.com/SpillwaveSolutions/skilz-cli/pull/43 +- **Commit**: fix/skilz-bugfixes-081-085-086-089 diff --git a/src/skilz/__init__.py b/src/skilz/__init__.py index 1d1fec3..2b3c9af 100644 --- a/src/skilz/__init__.py +++ b/src/skilz/__init__.py @@ -1,6 +1,12 @@ """Skilz - The universal package manager for AI skills.""" -__version__ = "1.7.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("skilz") +except PackageNotFoundError: + __version__ = "0.0.0.dev" + __author__ = "Spillwave" from skilz.api_client import ( diff --git a/src/skilz/agents.py b/src/skilz/agents.py index 5708bf6..8815329 100644 --- a/src/skilz/agents.py +++ b/src/skilz/agents.py @@ -9,12 +9,15 @@ from __future__ import annotations +import logging from pathlib import Path from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: from skilz.agent_registry import AgentConfig +logger = logging.getLogger(__name__) + # Backward-compatible AgentType (original two agents) # New code should use get_all_agent_types() or agent_registry directly AgentType = Literal["claude", "opencode"] @@ -124,20 +127,38 @@ def get_agent_paths() -> dict[str, dict[str, Path]]: return result +def _check_parent_skilz(project_dir: Path) -> str | None: + """Check for ../skilz/skills directory (universal agent pattern). + + Args: + project_dir: The project directory to check from. + + Returns: + "universal" if parent skilz/skills exists, None otherwise. + """ + parent = project_dir.parent + parent_skilz = parent / "skilz" / "skills" + if parent_skilz.exists() and parent_skilz.is_dir(): + logger.debug("[SKILZ-081] Found parent skilz/skills at %s", parent_skilz) + return "universal" + return None + + def detect_agent(project_dir: Path | None = None) -> str: """ Auto-detect which AI agent is being used. Detection order: 1. Check config file for agent_default setting - 2. Check for .claude/ in project directory - 3. Check for .gemini/ in project directory (Gemini CLI native skills) - 4. Check for .codex/ in project directory (OpenAI Codex native skills) - 5. Check for ~/.claude/ (user has Claude Code installed) - 6. Check for ~/.gemini/ (user has Gemini CLI native skills) - 7. Check for ~/.codex/ (user has OpenAI Codex installed) - 8. Check for ~/.config/opencode/ (user has OpenCode installed) - 9. Default to "claude" if ambiguous + 2. Check for ../skilz/skills (parent directory universal pattern) + 3. Check for .claude/ in project directory + 4. Check for .gemini/ in project directory (Gemini CLI native skills) + 5. Check for .codex/ in project directory (OpenAI Codex native skills) + 6. Check for ~/.claude/ (user has Claude Code installed) + 7. Check for ~/.gemini/ (user has Gemini CLI native skills) + 8. Check for ~/.codex/ (user has OpenAI Codex installed) + 9. Check for ~/.config/opencode/ (user has OpenCode installed) + 10. Default to "claude" if ambiguous Args: project_dir: Project directory to check. Uses cwd if None. @@ -157,6 +178,11 @@ def detect_agent(project_dir: Path | None = None) -> str: project = project_dir or Path.cwd() + # SKILZ-081: Check parent directory for universal agent pattern + parent_agent = _check_parent_skilz(project) + if parent_agent: + return parent_agent + # Check project-level markers (highest priority) if (project / ".claude").exists(): return "claude" @@ -200,7 +226,8 @@ def detect_agent(project_dir: Path | None = None) -> str: except ImportError: pass - # Default to Claude + # SKILZ-085: Default to Claude + logger.debug("[SKILZ-085] No agent markers found, using default: claude") return "claude" diff --git a/src/skilz/installer.py b/src/skilz/installer.py index 1da61ca..730a038 100644 --- a/src/skilz/installer.py +++ b/src/skilz/installer.py @@ -77,11 +77,13 @@ def copy_skill_files(source_dir: Path, target_dir: Path, verbose: bool = False) if verbose: print(f" Copying {source_dir} -> {target_dir}") + # SKILZ-089: Exclude .git directory to prevent nested repo issues shutil.copytree( source_dir, target_dir, symlinks=True, ignore_dangling_symlinks=True, + ignore=shutil.ignore_patterns(".git"), ) except OSError as e: diff --git a/src/skilz/link_ops.py b/src/skilz/link_ops.py index ca52f8b..5a51751 100644 --- a/src/skilz/link_ops.py +++ b/src/skilz/link_ops.py @@ -75,7 +75,14 @@ def copy_skill(source: Path, target: Path) -> None: # Copy directory tree, preserving symlinks (don't follow them) # ignore_dangling_symlinks=True skips broken symlinks gracefully - shutil.copytree(source, target, symlinks=True, ignore_dangling_symlinks=True) + # SKILZ-089: Exclude .git directory to prevent nested repo issues + shutil.copytree( + source, + target, + symlinks=True, + ignore_dangling_symlinks=True, + ignore=shutil.ignore_patterns(".git"), + ) def is_symlink(path: Path) -> bool: