From 39d35f2bb6d956a8f9125e00a7d91659b70e1dca Mon Sep 17 00:00:00 2001 From: Rick Hightower Date: Thu, 8 Jan 2026 18:28:30 -0600 Subject: [PATCH 1/2] feat: Release v1.7.0 - Gemini native support and universal agent enhancements SKILZ-49: Gemini CLI Native Support - Gemini now reads skills natively from .gemini/skills/ - Auto-detection from .gemini/ directories - Updated agent_registry.py with new Gemini config SKILZ-50: Universal Agent Custom Config - Added --config flag for custom config file targeting - Legacy Gemini workflow support via universal agent Feature 11: Skill Path Fallback Discovery - Always display warning when skill found at different path than expected - Warning goes to stderr for proper script integration - Verbose mode shows expected vs found paths BUG-001: Fixed Codex agent detection from .codex/ directories Documentation: - Added docs/GEMINI_MIGRATION.md - Added docs/UNIVERSAL_AGENT_GUIDE.md - Updated USER_MANUAL.md with new sections - Added SDD specs for features 09-12 Tests: 620 tests passing (added 3 new tests for path fallback) --- .../features/09-gemini-agent-skills/plan.md | 426 +++++++++ .../09-gemini-agent-skills/specify.md | 204 ++++ .../features/09-gemini-agent-skills/tasks.md | 363 +++++++ .../specify.md | 384 ++++++++ .../features/11-skill-path-fallback/plan.md | 106 +++ .../11-skill-path-fallback/specify.md | 80 ++ .../features/11-skill-path-fallback/tasks.md | 78 ++ .../12-marketplace-submission/specify.md | 100 ++ CHANGELOG.md | 96 ++ README.md | 69 +- docs/GEMINI_MIGRATION.md | 466 +++++++++ docs/UNIVERSAL_AGENT_GUIDE.md | 893 ++++++++++++++++++ docs/USER_MANUAL.md | 125 ++- docs/project_notes/bugs.md | 71 +- pyproject.toml | 2 +- scripts/end_to_end.sh | 255 ++++- src/skilz/__init__.py | 2 +- src/skilz/agent_registry.py | 11 +- src/skilz/agents.py | 30 +- src/skilz/cli.py | 5 + src/skilz/commands/install_cmd.py | 8 + src/skilz/config_sync.py | 44 +- src/skilz/installer.py | 66 +- tests/test_agent_registry.py | 33 +- tests/test_agents.py | 85 +- tests/test_gemini_integration.py | 287 ++++++ tests/test_install_cmd.py | 71 ++ tests/test_installer.py | 116 ++- tests/test_universal_integration.py | 388 ++++++++ 29 files changed, 4779 insertions(+), 85 deletions(-) create mode 100644 .speckit/features/09-gemini-agent-skills/plan.md create mode 100644 .speckit/features/09-gemini-agent-skills/specify.md create mode 100644 .speckit/features/09-gemini-agent-skills/tasks.md create mode 100644 .speckit/features/10-universal-agent-enhancements/specify.md create mode 100644 .speckit/features/11-skill-path-fallback/plan.md create mode 100644 .speckit/features/11-skill-path-fallback/specify.md create mode 100644 .speckit/features/11-skill-path-fallback/tasks.md create mode 100644 .speckit/features/12-marketplace-submission/specify.md create mode 100644 docs/GEMINI_MIGRATION.md create mode 100644 docs/UNIVERSAL_AGENT_GUIDE.md create mode 100644 tests/test_gemini_integration.py create mode 100644 tests/test_universal_integration.py diff --git a/.speckit/features/09-gemini-agent-skills/plan.md b/.speckit/features/09-gemini-agent-skills/plan.md new file mode 100644 index 0000000..5bce732 --- /dev/null +++ b/.speckit/features/09-gemini-agent-skills/plan.md @@ -0,0 +1,426 @@ +# Feature 09: Gemini CLI Native Agent Skills - Technical Plan + +## Technology Stack + +| Component | Choice | Rationale | +|-----------|--------|-----------| +| Agent Registry | Python dataclasses | Already used, immutable configs | +| Validation | Regex + unicodedata | Lightweight, stdlib-only for name validation | +| Detection | pathlib | Consistent with existing agent detection | +| Config Files | YAML (manifest) | Maintains consistency with existing Skilz | + +## Architecture + +### Component Diagram + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Agent Registry Layer │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ agent_registry.py (UPDATED) │ │ +│ │ │ │ +│ │ AgentConfig(gemini): │ │ +│ │ - home_dir: ~/.gemini/skills (NEW) │ │ +│ │ - project_dir: .gemini/skills (CHANGED) │ │ +│ │ - supports_home: True (CHANGED) │ │ +│ │ - native_skill_support: "all" (CHANGED) │ │ +│ │ - invocation: "/skills or activate_skill" (NEW) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ Detection Layer │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ agents.py (UPDATED) │ │ +│ │ │ │ +│ │ detect_agent(): │ │ +│ │ 1. Check config default │ │ +│ │ 2. Check .claude/ │ │ +│ │ 3. Check ~/.claude/ │ │ +│ │ 4. Check .gemini/ (NEW) │ │ +│ │ 5. Check ~/.gemini/ (NEW) │ │ +│ │ 6. Check ~/.config/opencode/ │ │ +│ │ 7. Other agents │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ Validation Layer (NEW) │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ agent_registry.py (skill validation) │ │ +│ │ │ │ +│ │ validate_skill_name(name): │ │ +│ │ - NFKC Unicode normalization │ │ +│ │ - Regex: ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ │ │ +│ │ - Max 64 chars │ │ +│ │ - Returns: SkillNameValidation │ │ +│ │ │ │ +│ │ check_skill_directory_name(dir, expected_name): │ │ +│ │ - Compare directory name with SKILL.md name │ │ +│ │ - Suggest rename if mismatch │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ Installation Layer │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ git_install.py / installer.py │ │ +│ │ │ │ +│ │ 1. Clone/checkout skill from Git │ │ +│ │ 2. Parse SKILL.md frontmatter │ │ +│ │ 3. Validate skill name (Gemini only) │ │ +│ │ 4. Check directory name matches (Gemini only) │ │ +│ │ 5. Copy to .gemini/skills/ or ~/.gemini/skills/ │ │ +│ │ 6. Write .skilz-manifest.yaml │ │ +│ │ 7. Skip GEMINI.md sync (unless --force-config) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Data Flow: Native Gemini Installation + +``` +1. User runs: skilz install anthropics_skills/theme-factory --agent gemini + +2. install_cmd.py: + - Detects agent="gemini" + - Checks AgentConfig(gemini).native_skill_support == "all" + - Sets skip_config_sync=True (no GEMINI.md sync) + +3. installer.py or git_install.py: + - Clones repository to cache + - Finds SKILL.md at skill_path + - Parses frontmatter: + name: "theme-factory" + description: "..." + +4. validation (NEW for Gemini): + - validate_skill_name("theme-factory") + → SkillNameValidation(is_valid=True, normalized="theme-factory") + - check_skill_directory_name(skill_dir, "theme-factory") + → matches=True + +5. copy to .gemini/skills/: + - Target: .gemini/skills/theme-factory/ + - Copy all files (SKILL.md, scripts/, references/, assets/) + - Write .skilz-manifest.yaml + +6. NO GEMINI.md sync (native support) + +7. User opens Gemini CLI: + - /skills list → shows "theme-factory" (discovered automatically) + - Can use /skills enable theme-factory, /skills disable theme-factory +``` + +### Data Flow: Backward Compatibility (--force-config) + +``` +1. User runs: skilz install --agent gemini --force-config + +2. install_cmd.py: + - Detects agent="gemini" + - --force-config flag overrides native_skill_support + - Sets skip_config_sync=False + +3. installer.py: + - Installs to .skilz/skills/ (old path) + - Writes GEMINI.md entry with skilz read command + +4. User must manually load skill via skilz read +``` + +## Key Design Decisions + +### D1: User-Level Support for Gemini +**Decision:** Enable `supports_home=True` for Gemini CLI + +**Rationale:** +- Gemini CLI's spec explicitly supports `~/.gemini/skills/` as "User Skills" tier +- Precedence model (project > user > extension) matches other agents (claude, opencode) +- Users expect to share skills across multiple projects + +**Code Change:** +```python +# agent_registry.py:150-159 +"gemini": AgentConfig( + home_dir=Path.home() / ".gemini" / "skills", # NEW + supports_home=True, # CHANGED +) +``` + +### D2: Native Support Level +**Decision:** Set `native_skill_support="all"` for Gemini CLI + +**Rationale:** +- Gemini CLI reads `.gemini/skills/` directly via `activate_skill` tool +- Skills are discovered by name/description without config file parsing +- Same as Claude Code and OpenCode native support + +**Code Change:** +```python +# agent_registry.py:158 +native_skill_support="all", # CHANGED from "none" +``` + +### D3: Skill Name Validation +**Decision:** Validate skill names only for Gemini (and agents with native support) + +**Rationale:** +- Claude Code and Gemini CLI fail if skill names don't match agentskills.io spec +- Better to catch errors at install time than runtime +- Non-native agents (aider, cursor) don't care about name format + +**Implementation:** +- Add validation functions to `agent_registry.py` (reusable) +- Call during installation for agents with `native_skill_support != "none"` +- Show helpful error with suggested fix + +### D4: Detection Priority +**Decision:** Check Gemini after Claude but before OpenCode + +**Rationale:** +- Claude Code is most common, check first +- Gemini CLI is newer than OpenCode +- Avoids false positives from `.skilz/` directory (universal fallback) + +**Code Change:** +```python +# agents.py:detect_agent() +# Order: Claude → Gemini → OpenCode → Others +if (Path.home() / ".claude").exists(): + return "claude" +if (project / ".gemini").exists() or (Path.home() / ".gemini").exists(): + return "gemini" +if (Path.home() / ".config" / "opencode").exists(): + return "opencode" +``` + +### D5: Backward Compatibility via Flag +**Decision:** Keep `.skilz/skills/` support with `--force-config` flag + +**Rationale:** +- Users on older Gemini CLI (without `experimental.skills`) still need to work +- Gradual migration path for existing users +- Same pattern as `--force-config` for other native agents + +**Behavior:** +- Default (no flag): use `.gemini/skills/` (native) +- With `--force-config`: use `.skilz/skills/` + GEMINI.md sync (legacy) + +### D6: Skip Directory Name Mismatch for Non-Gemini +**Decision:** Only validate directory name for agents with native support + +**Rationale:** +- Non-native agents don't care about directory names +- Avoids breaking existing installs where dir name != skill name +- Native agents (Claude, Gemini, OpenCode) require name matching for discovery + +## Error Handling Strategy + +| Error | Agent | Exit Code | Message | +|-------|-------|-----------|---------| +| Invalid skill name | Gemini (native) | 1 | "Skill name '' is invalid. Must be lowercase, letters/digits/hyphens only.\nSuggested name: ''" | +| Directory name mismatch | Gemini (native) | 1 | "Skill directory '' doesn't match skill name ''.\nConsider renaming to ''" | +| Native skills not enabled | Gemini | 1 | "Gemini CLI native skills not detected. Enable 'experimental.skills' or use --force-config" | +| No home support | Gemini (old) | 1 | "Gemini CLI does not support user-level installation. Use --project flag." (only if supports_home=False) | + +## Testing Strategy + +### Unit Tests (New) + +#### test_agent_registry.py +- `test_gemini_config_updated` - Verify home_dir, supports_home, native_skill_support +- `test_validate_skill_name_valid` - Valid names pass +- `test_validate_skill_name_invalid_uppercase` - Suggest lowercase +- `test_validate_skill_name_invalid_underscores` - Suggest hyphens +- `test_validate_skill_name_invalid_leading_hyphen` - Suggest fix +- `test_validate_skill_name_max_length` - Truncate to 64 chars +- `test_check_skill_directory_name_match` - Dir matches name +- `test_check_skill_directory_name_mismatch` - Suggest rename +- `test_rename_skill_directory` - Actually rename directory + +#### test_agents.py +- `test_detect_agent_gemini_project` - Detect `.gemini/` +- `test_detect_agent_gemini_user` - Detect `~/.gemini/` +- `test_detect_agent_gemini_priority` - Gemini before OpenCode +- `test_get_skills_dir_gemini_project` - Resolve `.gemini/skills/` +- `test_get_skills_dir_gemini_user` - Resolve `~/.gemini/skills/` + +#### test_install_cmd.py (Updated) +- `test_install_gemini_native_skip_config` - No GEMINI.md sync by default +- `test_install_gemini_force_config` - GEMINI.md sync with --force-config +- `test_install_gemini_user_level` - Install to `~/.gemini/skills/` + +#### test_git_install.py (Updated) +- `test_validate_skill_name_during_install` - Validation runs for Gemini +- `test_invalid_skill_name_error` - Helpful error message +- `test_skip_validation_non_native` - No validation for aider, cursor + +### Integration Tests + +#### test_gemini_end_to_end.py (NEW) +```python +def test_install_skill_to_gemini_native(): + """Install skill to .gemini/skills/ with native support.""" + # Given: Gemini project with .gemini/ directory + # When: skilz install --agent gemini + # Then: Skill in .gemini/skills/, no GEMINI.md sync + +def test_install_skill_to_gemini_user_level(): + """Install skill to ~/.gemini/skills/ for user-level.""" + # Given: User wants personal skill + # When: skilz install --agent gemini --user + # Then: Skill in ~/.gemini/skills/ + +def test_gemini_auto_detection(): + """Auto-detect Gemini from .gemini/ directory.""" + # Given: Project with .gemini/ directory + # When: skilz install (no --agent flag) + # Then: Detects agent=gemini, installs to .gemini/skills/ + +def test_backward_compat_force_config(): + """Backward compat with --force-config.""" + # Given: Older Gemini setup + # When: skilz install --agent gemini --force-config + # Then: Installs to .skilz/skills/, syncs GEMINI.md +``` + +## Implementation Phases + +### Phase 9a: Agent Registry Update +- [x] Update `agent_registry.py` Gemini CLI config + - home_dir = `~/.gemini/skills/` + - project_dir = `.gemini/skills/` + - supports_home = True + - native_skill_support = "all" + - invocation = "/skills or activate_skill tool" + +### Phase 9b: Skill Name Validation +- [ ] Add `validate_skill_name()` to `agent_registry.py` +- [ ] Add `check_skill_directory_name()` to `agent_registry.py` +- [ ] Add `rename_skill_directory()` to `agent_registry.py` +- [ ] Add `SkillNameValidation` dataclass + +### Phase 9c: Detection Enhancement +- [ ] Update `detect_agent()` in `agents.py` + - Check `.gemini/` in project + - Check `~/.gemini/` in user home + - Insert before OpenCode check + +### Phase 9d: Installation Logic +- [ ] Update `git_install.py` to validate skill names for native agents +- [ ] Update `installer.py` to skip GEMINI.md sync for native Gemini +- [ ] Handle `--force-config` override for backward compatibility + +### Phase 9e: Testing +- [ ] Add unit tests for validation functions +- [ ] Add unit tests for detection +- [ ] Add integration tests for end-to-end flows +- [ ] Test backward compatibility with --force-config + +### Phase 9f: Documentation +- [ ] Update README.md with Gemini native support +- [ ] Update USER_MANUAL.md with Gemini examples +- [ ] Update COMPREHENSIVE_USER_GUIDE.md +- [ ] Create GEMINI_MIGRATION.md guide +- [ ] Update agent table in README (native support column) + +## Migration Path for Existing Users + +### For Users on Older Gemini CLI (No Native Skills) + +```bash +# Continue using legacy mode +skilz install --agent gemini --force-config +``` + +### For Users Upgrading to Native Gemini CLI + +1. **Enable experimental.skills in Gemini CLI:** + ```bash + # In Gemini CLI /settings UI, search for "Skills" and toggle "experimental.skills" + ``` + +2. **Migrate existing skills:** + ```bash + # Option 1: Reinstall skills (recommended) + skilz uninstall --agent gemini + skilz install --agent gemini # Now goes to .gemini/skills/ + + # Option 2: Manual move (advanced) + mv .skilz/skills/theme-factory .gemini/skills/ + # Remove GEMINI.md entries manually + ``` + +3. **Verify:** + ```bash + # In Gemini CLI + /skills list # Should show migrated skills + ``` + +## Rollback Plan + +If native support causes issues: + +1. **Revert agent config:** + ```python + # In ~/.config/skilz/config.json (user override) + { + "agents": { + "gemini": { + "display_name": "Gemini CLI", + "home_dir": null, + "project_dir": ".skilz/skills", + "config_files": ["GEMINI.md"], + "supports_home": false, + "default_mode": "copy", + "native_skill_support": "none" + } + } + } + ``` + +2. **Use --force-config flag:** + ```bash + skilz install --agent gemini --force-config + ``` + +## Performance Considerations + +- Skill name validation adds ~1ms per install (regex + Unicode normalization) +- Directory checks add ~5ms per install (stat calls) +- Overall install time impact: <1% (dominated by git clone) + +## Security Considerations + +- Validation prevents directory traversal attacks (rejects `../` in names) +- NFKC normalization prevents Unicode homograph attacks +- No new file permissions or execution beyond existing installer + +## Compatibility Matrix + +| Gemini CLI Version | experimental.skills | Skilz Behavior | +|--------------------|-------------------|----------------| +| <2.0 (hypothetical) | Not available | Install to `.skilz/skills/` + GEMINI.md | +| ≥2.0 without flag | Disabled | Install to `.skilz/skills/` + GEMINI.md (use --force-config) | +| ≥2.0 with flag | Enabled | Install to `.gemini/skills/` (native) | + +## Dependencies + +No new external dependencies. All new code uses: +- `pathlib` (stdlib) +- `re` (stdlib) +- `unicodedata` (stdlib) +- `dataclasses` (stdlib) + +## Estimated Complexity + +| Phase | Complexity | LOC | Days | +|-------|------------|-----|------| +| 9a: Registry Update | Low | 10 | 0.25 | +| 9b: Validation | Medium | 120 | 1 | +| 9c: Detection | Low | 15 | 0.5 | +| 9d: Installation | Medium | 50 | 1 | +| 9e: Testing | High | 300 | 2 | +| 9f: Documentation | Medium | 200 | 1 | +| **Total** | | **695** | **5.75** | diff --git a/.speckit/features/09-gemini-agent-skills/specify.md b/.speckit/features/09-gemini-agent-skills/specify.md new file mode 100644 index 0000000..cd857f2 --- /dev/null +++ b/.speckit/features/09-gemini-agent-skills/specify.md @@ -0,0 +1,204 @@ +# Feature 09: Gemini CLI Native Agent Skills Support + +## Feature Summary + +Update Skilz to support Gemini CLI's new native Agent Skills feature. Gemini CLI now supports the Agent Skills open standard with progressive disclosure via `activate_skill` tool, discovery tiers (project, user, extension), and SKILL.md format with YAML frontmatter. + +This feature aligns Skilz with Gemini CLI's native skill capabilities, similar to how Claude Code and OpenCode already work. + +## Background + +Gemini CLI has added experimental native support for Agent Skills based on the Agent Skills open standard (https://github.com/agentskills). Key features: + +- **Progressive Disclosure**: Skills are discovered by name/description, then activated on-demand +- **Discovery Tiers**: + - Project skills: `.gemini/skills/` (highest precedence) + - User skills: `~/.gemini/skills/` + - Extension skills: bundled with extensions (lowest precedence) +- **SKILL.md Format**: YAML frontmatter with `name` and `description`, plus Markdown instructions +- **Management**: `/skills` slash command and `gemini skills` CLI for enable/disable/reload +- **Resource Bundling**: Skills can include `scripts/`, `references/`, `assets/` subdirectories + +## Target Agent + +| Agent | Current Skills Directory | New Native Skills Directory | Native Support | +|-------|-------------------------|----------------------------|----------------| +| Gemini CLI | `.skilz/skills/` (non-native) | `.gemini/skills/` (native) | Experimental (requires `experimental.skills` flag) | + +## User Stories + +### US-1: Install Skills for Native Gemini CLI Support +**As a** developer using Gemini CLI with native skills enabled +**I want to** run `skilz install --agent gemini` +**So that** skills are installed to `.gemini/skills/` (project) or `~/.gemini/skills/` (user) and automatically discovered + +**Acceptance Criteria:** +- Skills installed to `.gemini/skills/` at project level (default) +- Skills installed to `~/.gemini/skills/` with `--user` flag (NEW: user-level support) +- SKILL.md format is preserved (YAML frontmatter + Markdown body) +- Gemini CLI's `/skills list` shows installed skills +- Skills are auto-discovered without GEMINI.md entries + +### US-2: Auto-Detect Gemini CLI with Native Skills +**As a** developer with native Gemini skills enabled +**I want to** run `skilz install ` without `--agent gemini` +**So that** Skilz auto-detects Gemini CLI from `.gemini/` directory presence + +**Acceptance Criteria:** +- Detection checks for `.gemini/` directory in project +- Detection checks for `~/.gemini/` directory at user level +- Falls back to other agent detection if not found +- Works alongside existing Claude/OpenCode detection + +### US-3: Backward Compatibility with Non-Native Installs +**As a** developer using older Gemini CLI without native skills +**I want to** continue installing skills to `.skilz/skills/` with GEMINI.md sync +**So that** I don't break my existing workflow + +**Acceptance Criteria:** +- `skilz install --agent gemini --force-config` continues to use `.skilz/skills/` +- GEMINI.md config sync still works when explicitly requested +- Clear error message if user tries native install without `experimental.skills` enabled +- Documentation explains migration path from old to new + +### US-4: Skill Name Validation for Gemini +**As a** skill maintainer +**I want to** know if my skill name is valid for Gemini CLI +**So that** I can fix issues before installation fails + +**Acceptance Criteria:** +- SKILL.md `name` field is validated against agentskills.io spec: + - Lowercase only + - Letters, digits, hyphens only + - No leading/trailing hyphens + - No consecutive hyphens + - Max 64 characters +- Helpful error message with suggested fix if invalid +- Directory name matches `name` field (or suggests rename) + +### US-5: User-Level Installation Support +**As a** developer sharing skills across multiple Gemini projects +**I want to** install skills to `~/.gemini/skills/` once +**So that** they're available in all my projects + +**Acceptance Criteria:** +- `skilz install --agent gemini --user` installs to `~/.gemini/skills/` +- `skilz list --agent gemini --user` shows user-level skills +- User skills have lower precedence than project skills (per Gemini spec) +- Agent registry updated: `supports_home=True` for Gemini + +## Functional Requirements + +### FR-1: Agent Configuration Update +- Update `agent_registry.py` Gemini CLI configuration: + ```python + "gemini": AgentConfig( + name="gemini", + display_name="Gemini CLI", + home_dir=Path.home() / ".gemini" / "skills", # NEW: user-level support + project_dir=Path(".gemini") / "skills", # CHANGED: .skilz → .gemini + config_files=("GEMINI.md",), # Keep for backward compat + supports_home=True, # CHANGED: False → True + default_mode="copy", + native_skill_support="all", # CHANGED: "none" → "all" + invocation="/skills or activate_skill tool", # NEW: document invocation + ) + ``` + +### FR-2: Detection Logic Enhancement +- Update `detect_agent()` in `agents.py`: + - Check for `.gemini/` in project directory + - Check for `~/.gemini/` in user home directory + - Add priority before less common agents +- Order: Claude → Gemini → OpenCode → Other agents + +### FR-3: SKILL.md Format Validation +- Validate SKILL.md frontmatter during installation: + - `name` field is required + - `description` field is required + - `name` follows agentskills.io spec (see US-4) +- Scanner module already handles frontmatter parsing +- Add validation step in `git_install.py` before copy + +### FR-4: Backward Compatibility Mode +- When `--force-config` flag is used with Gemini: + - Install to `.skilz/skills/` (old behavior) + - Sync to GEMINI.md + - Set `native_skill_support="none"` temporarily +- Default behavior (no flag): use native `.gemini/skills/` + +### FR-5: Migration Guide +- Add `docs/GEMINI_MIGRATION.md`: + - How to enable `experimental.skills` in Gemini CLI + - How to migrate from `.skilz/skills/` to `.gemini/skills/` + - How to check if native skills are enabled (`/settings` UI) + - Troubleshooting common issues + +## Non-Functional Requirements + +### NFR-1: No Breaking Changes +- Existing installs to `.skilz/skills/` continue to work +- GEMINI.md sync is opt-in (via `--force-config`) +- Clear upgrade path for users + +### NFR-2: Performance +- Native skill installs should be as fast as Claude/OpenCode +- No additional overhead for skill validation + +### NFR-3: Documentation +- README.md updated with Gemini native support +- USER_MANUAL.md includes Gemini examples +- COMPREHENSIVE_USER_GUIDE.md has Gemini section +- New GEMINI_MIGRATION.md for upgrading users + +## Out of Scope + +- Gemini CLI extension skills (bundled with extensions) +- Automatic skill enable/disable via Skilz (use `gemini skills` CLI) +- Conversion of existing `.skilz/skills/` installs to `.gemini/skills/` +- Support for Gemini CLI's `--scope` flag (project vs user) - use Skilz's `--project` flag + +## Gemini CLI Skill Schema + +Per Gemini CLI docs, SKILL.md format: + +```markdown +--- +name: skill-name +description: When to use this skill (shown to Gemini for discovery) +--- + +# Skill Title + +Your instructions for how the agent should behave with this skill. + +## Resource Structure + +- `scripts/` - Executable scripts (bash, python, node) +- `references/` - Static documentation, schemas, examples +- `assets/` - Code templates, boilerplate, binaries +``` + +## Discovery Tiers (Gemini CLI) + +1. **Project Skills** (`.gemini/skills/`) - Highest precedence, committed to git +2. **User Skills** (`~/.gemini/skills/`) - Personal skills, available to all projects +3. **Extension Skills** - Bundled with installed extensions, lowest precedence + +When duplicate names exist, higher precedence wins. + +## Success Metrics + +Feature is complete when: +1. `skilz install --agent gemini` installs to `.gemini/skills/` +2. `skilz install --agent gemini --user` installs to `~/.gemini/skills/` +3. Gemini CLI's `/skills list` shows Skilz-installed skills +4. Auto-detection works when `.gemini/` directory exists +5. Tests pass with 80%+ coverage for new code paths +6. Migration guide is published + +## References + +- [Gemini CLI Agent Skills Docs](https://code.gemini.com/docs/agent-skills) (assumed URL) +- [Agent Skills Open Standard](https://github.com/agentskills) +- [agentskills.io Spec](https://agentskills.io) diff --git a/.speckit/features/09-gemini-agent-skills/tasks.md b/.speckit/features/09-gemini-agent-skills/tasks.md new file mode 100644 index 0000000..877a1c1 --- /dev/null +++ b/.speckit/features/09-gemini-agent-skills/tasks.md @@ -0,0 +1,363 @@ +# Feature 09: Gemini CLI Native Agent Skills - Tasks + +## Phase 9a: Agent Registry Update + +### T1: Update Gemini CLI AgentConfig +- [ ] Modify `src/skilz/agent_registry.py:150-159` + - Change `home_dir` from `None` to `Path.home() / ".gemini" / "skills"` + - Change `project_dir` from `Path(".skilz") / "skills"` to `Path(".gemini") / "skills"` + - Change `supports_home` from `False` to `True` + - Change `native_skill_support` from `"none"` to `"all"` + - Add `invocation="/skills or activate_skill tool"` +- **DoD:** Gemini config matches Claude/OpenCode structure for native support + +**Files Changed:** +- `src/skilz/agent_registry.py:150-159` + +**Test Coverage:** +- `tests/test_agent_registry.py:test_gemini_config_native_support` (new) +- `tests/test_agent_registry.py:test_gemini_supports_home` (new) + +--- + +## Phase 9b: Skill Name Validation + +### T2: Add Skill Name Validation Functions +- [ ] Add `SkillNameValidation` dataclass to `src/skilz/agent_registry.py` + - Fields: `is_valid`, `normalized_name`, `errors`, `suggested_name` +- [ ] Implement `validate_skill_name(name: str) -> SkillNameValidation` + - NFKC Unicode normalization + - Regex pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` + - Max 64 character check + - Return validation result with suggestions +- [ ] Implement `_suggest_valid_name(name: str) -> str` helper + - Convert uppercase → lowercase + - Replace spaces/underscores → hyphens + - Remove invalid characters + - Remove consecutive hyphens + - Ensure starts with letter +- [ ] Implement `check_skill_directory_name(skill_dir: Path, expected_name: str) -> tuple[bool, str | None]` + - Compare directory name with skill name + - Return (matches, suggested_path) +- [ ] Implement `rename_skill_directory(skill_dir: Path, new_name: str) -> Path` + - Rename directory to match skill name + - Raise FileExistsError if target exists +- **DoD:** All validation functions working with comprehensive error messages + +**Files Changed:** +- `src/skilz/agent_registry.py:280-433` (add after existing functions) + +**Test Coverage:** +- `tests/test_agent_registry.py:test_validate_skill_name_valid` +- `tests/test_agent_registry.py:test_validate_skill_name_invalid_uppercase` +- `tests/test_agent_registry.py:test_validate_skill_name_invalid_underscores` +- `tests/test_agent_registry.py:test_validate_skill_name_invalid_special_chars` +- `tests/test_agent_registry.py:test_validate_skill_name_leading_hyphen` +- `tests/test_agent_registry.py:test_validate_skill_name_consecutive_hyphens` +- `tests/test_agent_registry.py:test_validate_skill_name_max_length` +- `tests/test_agent_registry.py:test_validate_skill_name_unicode_normalization` +- `tests/test_agent_registry.py:test_check_skill_directory_name_match` +- `tests/test_agent_registry.py:test_check_skill_directory_name_mismatch` +- `tests/test_agent_registry.py:test_rename_skill_directory_success` +- `tests/test_agent_registry.py:test_rename_skill_directory_exists_error` + +--- + +## Phase 9c: Detection Enhancement + +### T3: Update Agent Detection Logic +- [ ] Modify `src/skilz/agents.py:detect_agent()` function + - After Claude checks, add Gemini checks: + - Check for `.gemini/` in project directory + - Check for `~/.gemini/` in user home + - Return "gemini" if found + - Place before OpenCode check (priority order) +- **DoD:** Gemini auto-detected when `.gemini/` directory exists + +**Files Changed:** +- `src/skilz/agents.py:127-188` (update detect_agent function) + +**Test Coverage:** +- `tests/test_agents.py:test_detect_agent_gemini_project_dir` +- `tests/test_agents.py:test_detect_agent_gemini_user_dir` +- `tests/test_agents.py:test_detect_agent_gemini_priority_over_opencode` +- `tests/test_agents.py:test_detect_agent_claude_priority_over_gemini` +- `tests/test_agents.py:test_get_skills_dir_gemini_project` +- `tests/test_agents.py:test_get_skills_dir_gemini_user` + +--- + +## Phase 9d: Installation Logic Updates + +### T4: Add Skill Name Validation to Installation Flow +- [ ] Modify `src/skilz/git_install.py:install_from_git()` + - After parsing SKILL.md frontmatter, validate skill name + - Only for agents with `native_skill_support != "none"` + - Show error with suggestion if invalid: + ``` + Error: Skill name 'My_Cool_Skill' is invalid for Gemini CLI. + + Skill names must be lowercase with hyphens only. + Suggested name: my-cool-skill + + Update SKILL.md frontmatter: + --- + name: my-cool-skill + description: ... + --- + ``` +- [ ] Check directory name matches skill name + - Show warning if mismatch + - Suggest rename command +- **DoD:** Invalid skill names caught at install time with helpful errors + +**Files Changed:** +- `src/skilz/git_install.py:~200-280` (add validation step) + +**Test Coverage:** +- `tests/test_git_install.py:test_validate_skill_name_gemini_native` +- `tests/test_git_install.py:test_invalid_skill_name_error_message` +- `tests/test_git_install.py:test_skill_name_validation_skipped_non_native` +- `tests/test_git_install.py:test_directory_name_mismatch_warning` + +### T5: Update Config Sync Logic for Native Gemini +- [ ] Modify `src/skilz/commands/install_cmd.py` + - For Gemini with `native_skill_support="all"`, set `skip_config_sync=True` by default + - If `--force-config` flag provided, override to use legacy paths: + - Install to `.skilz/skills/` instead of `.gemini/skills/` + - Enable GEMINI.md sync +- [ ] Update `src/skilz/config_sync.py` (if changes needed) + - Ensure GEMINI.md sync can still be forced for backward compatibility +- **DoD:** Native Gemini installs skip config sync; --force-config restores old behavior + +**Files Changed:** +- `src/skilz/commands/install_cmd.py:~70-100` +- `src/skilz/config_sync.py` (verify, may not need changes) + +**Test Coverage:** +- `tests/test_install_cmd.py:test_install_gemini_native_skip_config` +- `tests/test_install_cmd.py:test_install_gemini_force_config_legacy_path` +- `tests/test_install_cmd.py:test_install_gemini_force_config_syncs_md` + +### T6: Update Installer Module +- [ ] Review `src/skilz/installer.py` for any Gemini-specific logic + - Ensure native path resolution works correctly + - Verify manifest generation includes correct paths +- **DoD:** installer.py handles Gemini native paths correctly + +**Files Changed:** +- `src/skilz/installer.py` (review and update if needed) + +**Test Coverage:** +- `tests/test_installer.py:test_install_gemini_native_path` +- `tests/test_installer.py:test_install_gemini_user_level` + +--- + +## Phase 9e: Testing + +### T7: Add Unit Tests for Validation +- [ ] Create comprehensive unit tests in `tests/test_agent_registry.py` + - Cover all validation edge cases (see T2 test list) + - Test Unicode normalization (e.g., full-width characters) + - Test max length boundary conditions +- [ ] Update `tests/conftest.py` if new fixtures needed +- **DoD:** 100% coverage of validation code paths + +**Files Changed:** +- `tests/test_agent_registry.py` (add ~150 LOC of tests) + +### T8: Add Integration Tests +- [ ] Create `tests/test_gemini_integration.py` + - `test_install_skill_to_gemini_native_project` + - `test_install_skill_to_gemini_native_user` + - `test_gemini_auto_detection_project` + - `test_gemini_auto_detection_user` + - `test_gemini_backward_compat_force_config` + - `test_gemini_invalid_skill_name_error` + - `test_gemini_list_user_and_project_skills` +- [ ] Use temporary directories for test isolation +- [ ] Mock git operations where appropriate +- **DoD:** All integration scenarios pass, 80%+ overall coverage maintained + +**Files Changed:** +- `tests/test_gemini_integration.py` (new file, ~200 LOC) + +### T9: Update Existing Tests +- [ ] Review all existing tests for Gemini assumptions + - `tests/test_agents.py` - Add Gemini detection tests + - `tests/test_list_cmd.py` - Test listing Gemini skills + - `tests/test_update_cmd.py` - Test updating Gemini skills + - `tests/test_remove_cmd.py` - Test removing Gemini skills +- [ ] Fix any tests broken by Gemini config changes +- **DoD:** All existing tests pass with new Gemini config + +**Files Changed:** +- Various test files (update as needed) + +--- + +## Phase 9f: Documentation + +### T10: Create Gemini Migration Guide +- [ ] Create `docs/GEMINI_MIGRATION.md` + - Explain native skills vs legacy mode + - How to enable `experimental.skills` in Gemini CLI + - Step-by-step migration from `.skilz/skills/` to `.gemini/skills/` + - Troubleshooting section + - When to use `--force-config` +- **DoD:** Clear guide for users upgrading to native Gemini support + +**Files Created:** +- `docs/GEMINI_MIGRATION.md` (new file, ~150 LOC) + +### T11: Update README.md +- [ ] Update agent support table + - Show Gemini with user-level support + - Update "Native Skill Support" column to "all" +- [ ] Add Gemini CLI examples in Quick Start section + ```bash + # Install for Gemini CLI (native support) + skilz install anthropics_skills/theme-factory --agent gemini + + # User-level install for Gemini + skilz install anthropics_skills/theme-factory --agent gemini --user + ``` +- [ ] Update "How It Works" section to mention Gemini native discovery +- **DoD:** README reflects Gemini native support + +**Files Changed:** +- `README.md:220-242` (agent table) +- `README.md:62-93` (quick start examples) + +### T12: Update USER_MANUAL.md +- [ ] Add Gemini CLI section under "Supported Agents" + - Native skills directory: `.gemini/skills/` + - User-level: `~/.gemini/skills/` + - Invocation: `/skills` slash command or `activate_skill` tool + - Link to GEMINI_MIGRATION.md +- [ ] Add Gemini examples to each command section +- [ ] Add troubleshooting section for Gemini +- **DoD:** USER_MANUAL.md has complete Gemini documentation + +**Files Changed:** +- `docs/USER_MANUAL.md` (add Gemini sections) + +### T13: Update COMPREHENSIVE_USER_GUIDE.md +- [ ] Add Gemini CLI to agent-specific instructions + - Discovery tiers explanation + - Progressive disclosure concept + - Native vs legacy mode comparison +- [ ] Update workflow diagrams if needed +- **DoD:** COMPREHENSIVE_USER_GUIDE.md includes Gemini workflows + +**Files Changed:** +- `docs/COMPREHENSIVE_USER_GUIDE.md` (add Gemini section) + +### T14: Update CHANGELOG.md +- [ ] Add entry for version 1.7.0 (or next version): + ```markdown + ## [1.7.0] - 2025-01-XX + + ### Added + - Native support for Gemini CLI Agent Skills (experimental.skills) + - User-level installation for Gemini CLI (`~/.gemini/skills/`) + - Skill name validation for native agents (agentskills.io spec) + - `docs/GEMINI_MIGRATION.md` - Migration guide for Gemini users + + ### Changed + - Gemini CLI now installs to `.gemini/skills/` by default (native) + - Agent detection now checks for `.gemini/` directories + - Config sync skipped for native Gemini installs + + ### Deprecated + - Legacy Gemini mode (`.skilz/skills/` + GEMINI.md) still available via --force-config + ``` +- **DoD:** CHANGELOG.md documents all changes + +**Files Changed:** +- `CHANGELOG.md` (add new version section) + +--- + +## Task Dependencies + +``` +T1 (Registry Update) +│ +├─► T2 (Validation Functions) +│ │ +│ └─► T4 (Validation in Install Flow) +│ │ +│ ├─► T7 (Unit Tests - Validation) +│ └─► T8 (Integration Tests) +│ +└─► T3 (Detection Logic) + │ + └─► T5 (Config Sync Update) + │ + └─► T6 (Installer Module Review) + │ + └─► T9 (Update Existing Tests) + │ + └─► T10-T14 (Documentation) +``` + +## Estimated Complexity + +| Task | Complexity | LOC | Days | +|------|------------|-----|------| +| T1: Registry Update | Low | 10 | 0.25 | +| T2: Validation Functions | Medium | 120 | 1 | +| T3: Detection Logic | Low | 15 | 0.5 | +| T4: Validation in Install | Medium | 40 | 0.75 | +| T5: Config Sync Update | Low | 20 | 0.5 | +| T6: Installer Review | Low | 10 | 0.25 | +| T7: Unit Tests - Validation | Medium | 150 | 1 | +| T8: Integration Tests | High | 200 | 1.5 | +| T9: Update Existing Tests | Medium | 50 | 0.5 | +| T10: Migration Guide | Medium | 150 | 0.75 | +| T11: Update README | Low | 30 | 0.25 | +| T12: Update USER_MANUAL | Low | 80 | 0.5 | +| T13: Update GUIDE | Low | 60 | 0.5 | +| T14: Update CHANGELOG | Low | 20 | 0.25 | +| **Total** | | **955** | **8.5** | + +## Completion Checklist + +- [ ] **Code Complete:** + - [ ] All 14 tasks implemented + - [ ] Code reviewed and approved + - [ ] No linting or type errors + +- [ ] **Testing Complete:** + - [ ] All new unit tests passing + - [ ] All integration tests passing + - [ ] Overall coverage ≥80% + - [ ] Manual testing on real Gemini CLI project + +- [ ] **Documentation Complete:** + - [ ] README.md updated + - [ ] USER_MANUAL.md updated + - [ ] COMPREHENSIVE_USER_GUIDE.md updated + - [ ] GEMINI_MIGRATION.md created + - [ ] CHANGELOG.md updated + +- [ ] **Verification:** + - [ ] Can install skill to `.gemini/skills/` (native) + - [ ] Can install skill to `~/.gemini/skills/` (user-level) + - [ ] Gemini CLI auto-detects installed skills + - [ ] Backward compat works with --force-config + - [ ] Invalid skill names show helpful errors + +## Success Metrics + +Feature is complete when: +1. `skilz install --agent gemini` installs to `.gemini/skills/` ✓ +2. `skilz install --agent gemini --user` installs to `~/.gemini/skills/` ✓ +3. Gemini CLI's `/skills list` shows Skilz-installed skills ✓ +4. Auto-detection works when `.gemini/` directory exists ✓ +5. Invalid skill names are caught with helpful messages ✓ +6. Tests pass with ≥80% coverage ✓ +7. Migration guide published ✓ +8. No regressions in existing agent support ✓ diff --git a/.speckit/features/10-universal-agent-enhancements/specify.md b/.speckit/features/10-universal-agent-enhancements/specify.md new file mode 100644 index 0000000..bf7e7fb --- /dev/null +++ b/.speckit/features/10-universal-agent-enhancements/specify.md @@ -0,0 +1,384 @@ +# Feature Specification: Universal Agent Project-Level Support + +**Feature ID:** SKILZ-50 +**Status:** Draft +**Created:** 2026-01-08 +**Updated:** 2026-01-08 + +--- + +## Overview + +Add project-level installation support for the "universal" agent with optional custom config file targeting. This enables legacy Gemini CLI workflows (when `experimental.skills` is disabled) and provides a fallback installation method for any agent. + +--- + +## Problem Statement + +### Current Limitations + +1. **Universal agent only supports user-level installs:** + - `skilz install skill --agent universal` → installs to `~/.skilz/skills/` + - No project-level option exists + +2. **No way to target legacy Gemini installations:** + - Gemini CLI without `experimental.skills` plugin reads from `GEMINI.md` + - Native Gemini support (`.gemini/skills/`) requires plugin enabled + - Users without plugin access are stuck + +3. **No way to override config file target:** + - Config sync always writes to agent's default config file + - Can't redirect to custom file for special cases + +### User Pain Points + +**Scenario 1: Corporate Gemini User** +```bash +# User's company hasn't enabled experimental.skills plugin yet +gemini --version # experimental.skills: disabled + +# User wants to install skills but native location won't work +skilz install pdf-reader --agent gemini --project +# ❌ Installs to .gemini/skills/ but Gemini can't see it (plugin disabled) +``` + +**Scenario 2: Multi-Agent Project** +```bash +# User wants skills available to ALL agents in one location +# Currently must install multiple times: +skilz install pdf --agent claude --project # → .claude/skills/ +skilz install pdf --agent gemini --project # → .gemini/skills/ +skilz install pdf --agent codex --project # → .codex/skills/ +# 😢 Three copies of the same skill +``` + +--- + +## Proposed Solution + +### Feature 1: Universal Agent Project-Level Support + +Enable `--project` flag for universal agent with standard behavior: + +```bash +skilz install my-skill --agent universal --project +``` + +**Behavior:** +- Installs to `./skilz/skills/my-skill/` (non-native location) +- Updates `AGENTS.md` with skill reference +- Works for any agent that reads `AGENTS.md` (Codex, legacy workflows) + +**File Structure:** +``` +project/ +├── skilz/ +│ └── skills/ +│ └── my-skill/ +│ ├── SKILL.md +│ └── .skilz-manifest.json +└── AGENTS.md ← Skill reference added here +``` + +### Feature 2: Custom Config File Target + +Add `--config` flag to override default config file: + +```bash +skilz install my-skill --agent universal --project --config GEMINI.md +``` + +**Behavior:** +- Still installs to `./skilz/skills/my-skill/` +- Updates `GEMINI.md` instead of `AGENTS.md` +- Enables legacy Gemini support + +**Use Cases:** +1. **Legacy Gemini:** Corporate users without `experimental.skills` plugin +2. **Testing:** Test config file generation without affecting native installations +3. **Multi-agent:** Share one skill location, multiple config file references + +--- + +## User Stories + +### US-1: Legacy Gemini Installation + +**As a** corporate Gemini CLI user without the `experimental.skills` plugin +**I want to** install skills that Gemini can discover +**So that** I can use Skilz without waiting for admin approval + +**Acceptance Criteria:** +```bash +# Install skill for legacy Gemini +skilz install pdf-reader --agent universal --project --config GEMINI.md + +# Verify installation +ls ./skilz/skills/pdf-reader/SKILL.md # ✅ Exists +cat GEMINI.md | grep pdf-reader # ✅ Referenced + +# Gemini can now read it +gemini # Skill shows up in /skills command +``` + +### US-2: Universal Project Installation + +**As a** developer working on a multi-agent project +**I want to** install skills once in a shared location +**So that** multiple agents can reference the same files + +**Acceptance Criteria:** +```bash +# Install skill universally at project level +skilz install web-scraper --agent universal --project + +# Verify installation +ls ./skilz/skills/web-scraper/SKILL.md # ✅ Exists +cat AGENTS.md | grep web-scraper # ✅ Referenced + +# All agents using AGENTS.md can discover it +``` + +### US-3: Config File Override + +**As a** power user testing skill installations +**I want to** control which config file gets updated +**So that** I can test without breaking my main setup + +**Acceptance Criteria:** +```bash +# Test installation with custom config +skilz install test-skill --agent universal --project --config TEST.md + +# Verify only TEST.md was modified +cat TEST.md | grep test-skill # ✅ Referenced +cat AGENTS.md | grep test-skill # ❌ Not modified +``` + +--- + +## Technical Design + +### Changes Required + +#### 1. Update Universal Agent Config + +**File:** `src/skilz/agent_registry.py` + +**Current:** +```python +"universal": AgentConfig( + name="universal", + display_name="Universal (Skilz)", + home_dir=Path.home() / ".skilz" / "skills", + project_dir=Path(".skilz") / "skills", + config_files=(), # ← Empty! + supports_home=True, + default_mode="copy", + native_skill_support="none", +), +``` + +**Proposed:** +```python +"universal": AgentConfig( + name="universal", + display_name="Universal (Skilz)", + home_dir=Path.home() / ".skilz" / "skills", + project_dir=Path(".skilz") / "skills", + config_files=("AGENTS.md",), # ← Add default config + supports_home=True, + default_mode="copy", + native_skill_support="none", +), +``` + +#### 2. Add --config Flag to CLI + +**File:** `src/skilz/cli.py` + +```python +# Add to install command parser +parser_install.add_argument( + "--config", + metavar="FILE", + help="Config file to update (overrides agent default). Example: --config GEMINI.md" +) +``` + +#### 3. Update Installer Logic + +**File:** `src/skilz/installer.py` + +```python +def install_local_skill( + source_path: Path, + agent: AgentType | None = None, + project_level: bool = False, + verbose: bool = False, + mode: InstallMode | None = None, + git_url: str | None = None, + git_sha: str | None = None, + skill_name: str | None = None, + force_config: bool = False, + config_file: str | None = None, # ← NEW parameter +) -> None: + # ... existing logic ... + + # Step 5: Sync skill reference to config files + if project_level and should_sync: + # Determine config files to update + if config_file: + # Use user-specified config file + config_files = (config_file,) + else: + # Use agent's default config files + config_files = agent_config.config_files + + sync_results = sync_skill_to_configs( + skill=skill_ref, + project_dir=project_dir, + target_files=config_files, # ← Use determined files + ) +``` + +#### 4. Update Config Sync Logic + +**File:** `src/skilz/config_sync.py` + +```python +def sync_skill_to_configs( + skill: SkillReference, + project_dir: Path, + target_files: tuple[str, ...] | None = None, # ← NEW: override files +) -> list[ConfigSyncResult]: + """Sync skill reference to agent config files. + + Args: + skill: Skill information to add to config files. + project_dir: Project root directory. + target_files: Optional list of config files to update. + If None, uses all known agent config files. + """ + if target_files: + # Use provided files only + config_files = target_files + else: + # Auto-detect all agent config files + config_files = ("CLAUDE.md", "AGENTS.md", "GEMINI.md", "OPENCODE.md") + + # ... rest of logic +``` + +--- + +## Implementation Plan + +### Phase 10a: Universal Agent Config Update +- [ ] Update `universal` AgentConfig to include `config_files=("AGENTS.md",)` +- [ ] Add tests for universal agent config +- **Files:** `src/skilz/agent_registry.py`, `tests/test_agent_registry.py` +- **Effort:** 1 hour + +### Phase 10b: CLI Flag Addition +- [ ] Add `--config FILE` argument to install command +- [ ] Add argument validation (file name only, no path traversal) +- [ ] Update help text +- **Files:** `src/skilz/cli.py`, `tests/test_cli.py` +- **Effort:** 2 hours + +### Phase 10c: Installer Integration +- [ ] Add `config_file` parameter to `install_local_skill()` +- [ ] Add `config_file` parameter to `install_skill()` +- [ ] Pass custom config to `sync_skill_to_configs()` +- [ ] Add validation: `--config` only works with `--project` +- **Files:** `src/skilz/installer.py`, `tests/test_installer.py` +- **Effort:** 3 hours + +### Phase 10d: Config Sync Enhancement +- [ ] Add `target_files` parameter to `sync_skill_to_configs()` +- [ ] Update sync logic to use custom files when provided +- [ ] Preserve backward compatibility (default behavior unchanged) +- **Files:** `src/skilz/config_sync.py`, `tests/test_config_sync.py` +- **Effort:** 2 hours + +### Phase 10e: Integration Testing +- [ ] Test: Universal project install creates AGENTS.md +- [ ] Test: Universal + --config creates custom file +- [ ] Test: --config without --project shows error +- [ ] Test: Legacy Gemini workflow (--agent universal --config GEMINI.md) +- **Files:** `tests/test_universal_integration.py` (new) +- **Effort:** 3 hours + +### Phase 10f: Documentation +- [ ] Update USER_MANUAL.md with universal project examples +- [ ] Update README.md with legacy Gemini workflow +- [ ] Create UNIVERSAL_AGENT_GUIDE.md +- [ ] Update CHANGELOG.md for version 1.7.0 +- **Files:** `docs/` +- **Effort:** 2 hours + +--- + +## Total Effort Estimate + +**Total:** 13 hours (~2 days) + +--- + +## Testing Strategy + +### Unit Tests +- `test_universal_agent_config()` - Config includes AGENTS.md +- `test_config_flag_validation()` - Only works with --project +- `test_custom_config_file_sync()` - Updates specified file only + +### Integration Tests +- `test_universal_project_install()` - Creates ./skilz/skills/ + AGENTS.md +- `test_universal_custom_config()` - Creates ./skilz/skills/ + custom file +- `test_legacy_gemini_workflow()` - Full legacy install flow + +### Manual Testing +```bash +# Test 1: Universal project install +cd test-project/ +skilz install pdf --agent universal --project +ls ./skilz/skills/pdf/ # ✅ Exists +cat AGENTS.md # ✅ Has pdf reference + +# Test 2: Custom config file +skilz install pdf --agent universal --project --config GEMINI.md +cat GEMINI.md # ✅ Has pdf reference +cat AGENTS.md # ❌ Not modified + +# Test 3: Error handling +skilz install pdf --agent universal --config TEST.md +# ❌ Error: --config requires --project +``` + +--- + +## Success Metrics + +- [ ] Universal agent supports `--project` flag +- [ ] `--config` flag allows custom config file targeting +- [ ] All 602 existing tests pass +- [ ] 8+ new tests for universal agent features +- [ ] Documentation includes legacy Gemini workflow example + +--- + +## Future Enhancements + +### Multi-Config Support +```bash +# Update multiple config files at once +skilz install pdf --agent universal --project \ + --config AGENTS.md --config GEMINI.md --config CLAUDE.md +``` + +### Auto-Detect Config Files +```bash +# Scan project for all *AGENTS*.md files and update them all +skilz install pdf --agent universal --project --config auto +``` diff --git a/.speckit/features/11-skill-path-fallback/plan.md b/.speckit/features/11-skill-path-fallback/plan.md new file mode 100644 index 0000000..c54b572 --- /dev/null +++ b/.speckit/features/11-skill-path-fallback/plan.md @@ -0,0 +1,106 @@ +# Implementation Plan: Skill Path Fallback Discovery + +**Branch**: `11-skill-path-fallback` | **Date**: 2026-01-08 | **Spec**: specify.md + +## Summary + +Enhance the existing path fallback logic in `installer.py` to ALWAYS display a user-visible warning when a skill is found at a different path than expected. Currently, this information is only shown in verbose mode. This change improves user experience when marketplace/registry data becomes stale due to repository reorganizations. + +## Technical Context + +**Language/Version**: Python 3.10+ +**Primary Dependencies**: None (stdlib only) +**Storage**: N/A +**Testing**: pytest +**Target Platform**: macOS, Linux, Windows +**Project Type**: Single (CLI tool) +**Performance Goals**: No measurable impact (single string comparison + print) +**Constraints**: Warning must go to stderr, not stdout +**Scale/Scope**: ~15-25 lines of code changes + +## Constitution Check + +- **Cross-Agent Universality**: This feature affects all agents equally +- **Reproducibility First**: No impact on reproducibility +- **Progressive Complexity**: Simple warning message, no new flags +- **Minimal Dependencies**: No new dependencies +- **Auditable by Default**: Improves auditability by informing user of path changes + +## Project Structure + +### Documentation (this feature) + +```text +.speckit/features/11-skill-path-fallback/ +├── specify.md # This specification +├── plan.md # This implementation plan +└── tasks.md # Task breakdown +``` + +### Source Code (changes) + +```text +src/skilz/ +├── installer.py # Add warning message (lines ~437-441) +└── (no other changes) + +tests/ +├── test_installer.py # Add tests for warning behavior +└── test_git_ops.py # (existing tests sufficient) +``` + +**Structure Decision**: Minimal changes to existing structure. Single file modification + test additions. + +## Files to Modify + +| File | Change Type | Description | +|------|-------------|-------------| +| `src/skilz/installer.py` | Modify | Add always-visible warning when path differs (lines 437-441) | +| `tests/test_installer.py` | Add | Tests for warning visibility | +| `CHANGELOG.md` | Update | Document the enhancement | + +## Code Changes (Detailed) + +### `src/skilz/installer.py` (lines 437-441) + +**Current Code:** +```python +if found_path: + source_dir = found_path + if verbose: + rel_path = source_dir.relative_to(cache_path) + print(f" Using found location: {rel_path}") +``` + +**Proposed Code:** +```python +if found_path: + source_dir = found_path + # Always warn user about path change (not just verbose mode) + print( + f"Warning: Skill '{skill_info.skill_name}' found at different path than expected", + file=sys.stderr, + ) + if verbose: + rel_path = source_dir.relative_to(cache_path) + print(f" Expected: {skill_info.skill_path}", file=sys.stderr) + print(f" Found at: {rel_path}", file=sys.stderr) +``` + +### Test Cases + +1. `test_install_skill_warns_on_path_change` - Verify warning is printed when path differs +2. `test_install_skill_no_warning_when_path_matches` - Verify NO warning when path matches +3. `test_install_skill_warning_goes_to_stderr` - Verify warning goes to stderr, not stdout + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Breaking existing tests | Low | Medium | Run full test suite before/after | +| Warning too verbose | Low | Low | Use minimal message format | +| Performance impact | None | None | Single print statement | + +## Complexity Tracking + +No constitution violations. This is a minimal, targeted change. diff --git a/.speckit/features/11-skill-path-fallback/specify.md b/.speckit/features/11-skill-path-fallback/specify.md new file mode 100644 index 0000000..85f004b --- /dev/null +++ b/.speckit/features/11-skill-path-fallback/specify.md @@ -0,0 +1,80 @@ +# Feature Specification: Skill Path Fallback Discovery + +**Feature Branch**: `11-skill-path-fallback` +**Created**: 2026-01-08 +**Status**: Approved +**Input**: User request for graceful handling of repository reorganizations + +## User Scenarios & Testing + +### User Story 1 - Path Fallback with Warning (Priority: P1) + +As a developer installing a skill from the marketplace, when the skill maintainer has reorganized their repository, I want skilz to still find and install the skill and warn me that the path changed, so that my installation doesn't fail just because the marketplace has stale path data. + +**Why this priority**: This is the core functionality - without it, installations fail when paths change. + +**Independent Test**: Can be fully tested by installing a skill whose expected path doesn't exist but whose SKILL.md can be found elsewhere in the repo. + +**Acceptance Scenarios**: + +1. **Given** a skill with marketplace path `/main/old-location/SKILL.md`, **When** the repo has been reorganized and the skill now exists at `/main/new-location/skill-name/SKILL.md`, **Then** skilz finds the skill by searching for SKILL.md files with matching name, installs it successfully, AND displays a warning message to the user (always, not just verbose mode). + +2. **Given** a skill path that doesn't exist AND no matching SKILL.md can be found anywhere in the repo, **When** user runs `skilz install`, **Then** skilz raises an InstallError with a clear message explaining the skill may have been removed. + +3. **Given** multiple SKILL.md files with the same skill name in different locations, **When** user runs `skilz install`, **Then** skilz uses the first match and logs a verbose warning about multiple matches. + +--- + +### User Story 2 - Warning Message Content (Priority: P1) + +As a developer, when a skill is found at a different path than expected, I want to see a clear warning message that explains what happened, so that I understand why the installation took longer or behaved differently. + +**Why this priority**: User visibility is essential - users need to know their installation succeeded but from a different location. + +**Independent Test**: Can be verified by checking stderr output contains expected warning format. + +**Acceptance Scenarios**: + +1. **Given** skill found at different path, **When** installation completes, **Then** warning message is printed to stderr (not stdout) with format: + ``` + Warning: Skill 'skill-name' found at different path than expected + ``` + +2. **Given** skill found at expected path (normal case), **When** installation completes, **Then** NO warning message is displayed. + +--- + +### Edge Cases + +- What happens when SKILL.md exists but has no `name:` field in frontmatter? + - Falls back to directory name matching (existing behavior) +- What happens when repository has been completely deleted/emptied? + - Standard Git clone/fetch error is raised +- What happens when skill name contains special characters? + - Handled by existing `validate_skill_name` function + +## Requirements + +### Functional Requirements + +- **FR-001**: System MUST search for SKILL.md files when expected path doesn't exist +- **FR-002**: System MUST match skills by directory name OR `name:` field in frontmatter +- **FR-003**: System MUST display warning message to stderr when path differs (ALWAYS, not just verbose) +- **FR-004**: System MUST continue installation using found path when discovered +- **FR-005**: Warning message MUST be minimal but informative + +### Key Entities + +- **SkillInfo**: Contains expected `skill_path` from registry/API +- **Found Path**: Actual path discovered via `find_skill_by_name()` +- **Warning**: Message displayed when paths differ + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: All existing tests continue to pass (617 tests) +- **SC-002**: New tests verify warning is displayed when path differs +- **SC-003**: New tests verify warning is NOT displayed when path matches +- **SC-004**: Installation succeeds when skill moved to different location +- **SC-005**: Warning appears on stderr, not stdout diff --git a/.speckit/features/11-skill-path-fallback/tasks.md b/.speckit/features/11-skill-path-fallback/tasks.md new file mode 100644 index 0000000..a365e5a --- /dev/null +++ b/.speckit/features/11-skill-path-fallback/tasks.md @@ -0,0 +1,78 @@ +# Tasks: Skill Path Fallback Discovery + +**Input**: Design documents from `.speckit/features/11-skill-path-fallback/` +**Prerequisites**: specify.md (required), plan.md (required) + +## Phase 1: Setup + +- [x] T001 Create feature directory `.speckit/features/11-skill-path-fallback/` +- [x] T002 Create specify.md +- [x] T003 Create plan.md +- [x] T004 Create tasks.md + +--- + +## Phase 2: User Story 1 - Path Fallback with Warning (Priority: P1) + +**Goal**: Add user-visible warning when skill path differs from expected + +**Independent Test**: Install skill with mismatched path, verify warning appears + +### Tests for User Story 1 + +- [x] T005 [P] [US1] Add test `test_install_skill_warns_on_path_change` in `tests/test_installer.py` +- [x] T006 [P] [US1] Add test `test_install_skill_no_warning_when_path_matches` in `tests/test_installer.py` +- [x] T007 [P] [US1] Add test `test_install_skill_warning_goes_to_stderr` in `tests/test_installer.py` + +### Implementation for User Story 1 + +- [x] T008 [US1] Modify warning logic in `src/skilz/installer.py` (lines 437-441) + - Change from `if verbose:` to always print warning + - Add `file=sys.stderr` to print statement + - Use minimal message format: `Warning: Skill 'X' found at different path than expected` + - Keep verbose details (expected/found paths) behind `if verbose:` + +**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently + +--- + +## Phase 3: Polish & Documentation + +- [x] T009 Update `CHANGELOG.md` with enhancement description +- [x] T010 Run full test suite to verify no regressions (expect 620+ tests) +- [x] T011 Run `task check` (lint + typecheck + test) + +--- + +## Phase 4: Feature 12 Placeholder + +- [x] T012 Create `.speckit/features/12-marketplace-submission/specify.md` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1 (Setup)**: No dependencies +- **Phase 2 (US1)**: Depends on Phase 1 + - Tests (T005-T007) can run in parallel + - Implementation (T008) can start after tests are written +- **Phase 3 (Polish)**: Depends on Phase 2 + +### Parallel Opportunities + +```bash +# Launch tests in parallel: +T005: test_install_skill_warns_on_path_change +T006: test_install_skill_no_warning_when_path_matches +T007: test_install_skill_warning_goes_to_stderr +``` + +--- + +## Notes + +- Total new tests: 3 +- Total lines changed: ~15-20 +- Estimated time: 30-45 minutes diff --git a/.speckit/features/12-marketplace-submission/specify.md b/.speckit/features/12-marketplace-submission/specify.md new file mode 100644 index 0000000..981764f --- /dev/null +++ b/.speckit/features/12-marketplace-submission/specify.md @@ -0,0 +1,100 @@ +# Feature Specification: Marketplace Submission + +**Feature Branch**: `12-marketplace-submission` +**Created**: 2026-01-08 +**Status**: Future (Not Ready for Implementation) +**Blocked By**: Marketplace REST API endpoint development + +## Feature Summary + +Enable users to submit skills to the skillzwave.ai marketplace directly from the CLI when installing from Git URLs (`-g` flag) or GitHub repositories. + +## User Scenarios & Testing + +### User Story 1 - Submit Skill to Marketplace (Priority: P1) + +As a skill developer who has installed a skill using `-g https://github.com/myorg/my-skill.git`, I want to be prompted (or use a flag) to submit this skill to the marketplace, so that other users can discover and install it more easily. + +**Why this priority**: Core feature - enables community contribution to marketplace + +**Acceptance Scenarios**: + +1. **Given** user installs with `-g `, **When** installation succeeds, **Then** user is prompted: "Would you like to submit this skill to the marketplace? [y/N]" + +2. **Given** user installs with `-g --submit-to-marketplace`, **When** installation succeeds, **Then** skill is automatically submitted without prompt + +3. **Given** skill is submitted, **When** API responds with success, **Then** user sees: "Skill submitted to marketplace for review" + +--- + +### User Story 2 - Submission Validation (Priority: P2) + +As a marketplace maintainer, I want submitted skills to be validated before acceptance, so that the marketplace maintains quality standards. + +**Acceptance Scenarios**: + +1. **Given** skill has valid SKILL.md with required frontmatter, **When** submitted, **Then** submission is accepted for review + +2. **Given** skill is missing required fields, **When** submitted, **Then** submission is rejected with clear error message + +--- + +## Requirements + +### Functional Requirements + +- **FR-001**: System MUST provide `--submit-to-marketplace` flag on install command +- **FR-002**: System MUST prompt user after successful `-g` install (unless `--no-prompt`) +- **FR-003**: System MUST call marketplace REST API with skill metadata +- **FR-004**: System MUST display submission status/result to user +- **FR-005**: System MUST handle API errors gracefully + +### Blocked Requirements (Need Backend Work) + +- **BLOCKED-001**: Marketplace REST endpoint `POST /api/skills/submit` [NEEDS BACKEND] +- **BLOCKED-002**: Skill validation endpoint `POST /api/skills/validate` [NEEDS BACKEND] +- **BLOCKED-003**: Authentication for submission (API key or OAuth) [NEEDS BACKEND] + +### API Schema (Proposed) + +```json +POST /api/skills/submit +{ + "git_repo": "https://github.com/owner/repo.git", + "skill_path": "/main/skills/my-skill/SKILL.md", + "git_sha": "abc123...", + "skill_name": "my-skill", + "description": "From SKILL.md frontmatter", + "submitter_email": "optional@email.com" +} + +Response: +{ + "status": "pending_review" | "rejected", + "submission_id": "uuid", + "message": "Skill submitted for review" +} +``` + +## Success Criteria + +- **SC-001**: Users can submit skills via CLI +- **SC-002**: Submissions appear in marketplace admin queue +- **SC-003**: 90% of valid skills pass automated validation +- **SC-004**: Clear error messages for invalid submissions + +## Implementation Notes + +This feature is **NOT READY** for implementation until: + +1. Marketplace backend team implements `POST /api/skills/submit` endpoint +2. Authentication/authorization strategy is defined +3. Skill validation rules are finalized + +**Estimated Backend Work**: 2-3 weeks +**Estimated CLI Work**: 3-5 days (after backend ready) + +## Related Features + +- Feature 11: Skill Path Fallback Discovery (implements path search that could validate submissions) +- Feature 08: Multi-Skill Repository Support (provides skill discovery logic) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f32a51..c1fd8f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,102 @@ All notable changes to Skilz CLI will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.0] - 2026-01-08 + +### Added + +- **Gemini CLI Native Skill Support (SKILZ-49)**: Gemini now reads skills natively from `.gemini/skills/` + - Project-level: Skills installed to `.gemini/skills/` (native location) + - User-level: Skills installed to `~/.gemini/skills/` (native location) + - No config file injection needed when using native directories + - Requires Gemini CLI with `experimental.skills` plugin enabled + - Auto-detects Gemini from `.gemini/` directory (project and user level) + +- **Universal Agent Project-Level Support (SKILZ-50)**: Universal agent now supports project installations + - Install to project: `skilz install --agent universal --project` + - Default behavior: Updates `AGENTS.md` config file + - Custom config support: `--config ` flag to target specific config files + - Legacy Gemini workflow: `skilz install --agent universal --project --config GEMINI.md` + - Universal agent config updated: `config_files=("AGENTS.md",)` (was empty tuple) + +- **Custom Config File Targeting**: New `--config` flag for install command + - Syntax: `skilz install --project --config ` + - Requires `--project` flag (only works with project-level installs) + - Supports any filename: `GEMINI.md`, `CUSTOM_SKILLS.md`, etc. + - Only updates specified file (overrides auto-detection) + - Use case: Legacy Gemini users without `experimental.skills` plugin + +- **Comprehensive Integration Tests**: Added 9 new integration tests for universal agent + - Test default AGENTS.md creation + - Test custom config file targeting + - Test CLI validation (--config requires --project) + - Test multiple skills with custom config + - Test legacy Gemini workflow + - Test arbitrary custom filenames + - Test config file isolation (only target updated) + - All 617 tests passing (100% success rate) + +- **Enhanced E2E Test Suite**: Updated end-to-end tests for 1.7.0 + - Isolated test environment in `e2e/test_folder/` with mock Python project + - New `test_gemini_native()` test for native `.gemini/skills/` support + - New `test_universal_custom_config()` test for custom config workflows + - Tests verify no GEMINI.md created for native agents + - Tests verify custom config files work with arbitrary names + +### Changed + +- **Gemini Agent Config**: Updated to support native skill directories + - `home_dir`: Changed from `None` to `Path.home() / ".gemini" / "skills"` + - `project_dir`: Changed from `.skilz/skills` to `.gemini/skills` + - `supports_home`: Changed from `False` to `True` + - `native_skill_support`: Changed from "none" to "all" + +- **Universal Agent Config**: Updated to enable project-level installations + - `config_files`: Changed from empty tuple `()` to `("AGENTS.md",)` + - Now supports both user-level (`~/.skilz/skills/`) and project-level (`.skilz/skills/`) + +- **Config Sync Enhancement**: `sync_skill_to_configs()` now supports `target_files` parameter + - When `target_files` specified, only those files are updated + - Overrides auto-detection for explicit control + - Enables custom config workflows for any agent + +- **Agent Auto-Detection**: Enhanced detection to include Gemini + - Priority order: config default → `.claude/` → `.gemini/` → `.codex/` → `~/.claude/` → `~/.gemini/` → `~/.codex/` → `~/.config/opencode/` → default (Claude) + +### Enhanced + +- **Skill Path Fallback Discovery (Feature 11)**: When installing a skill whose expected path doesn't exist in the repository, skilz now always displays a warning message when the skill is found at a different location. Previously this information was only shown in verbose mode. This helps users understand when marketplace/registry data may be stale due to repository reorganizations. + - Warning format: `Warning: Skill 'skill-name' found at different path than expected` + - Warning goes to stderr (not stdout) for proper script integration + - Verbose mode shows expected and found paths for debugging + - Installation continues successfully from the discovered location + +### Fixed + +- **Codex Agent Auto-Detection (BUG-001)**: Codex agent now properly detected from directories + - Fixed detection from project-level `.codex/` directory + - Fixed detection from user-level `~/.codex/` directory + - Added to auto-detection priority list (was missing despite being in registry) + - Added 3 tests to verify Codex detection works correctly + +### Migration Notes + +**For Gemini CLI Users:** +- **With experimental.skills plugin**: Use native support with `--agent gemini --project` + - Skills install to `.gemini/skills/` (native location) + - No config file needed - Gemini reads directory natively +- **Without experimental.skills plugin**: Use legacy workflow with universal agent + - `skilz install --agent universal --project --config GEMINI.md` + - Skills install to `.skilz/skills/` + - GEMINI.md file created for Gemini CLI to read + +**For Universal Agent Users:** +- Project-level installations now supported with automatic AGENTS.md creation +- Use `--config ` to target specific config files +- Backward compatible: User-level installs work as before + +See [GEMINI_MIGRATION.md](docs/GEMINI_MIGRATION.md) and [UNIVERSAL_AGENT_GUIDE.md](docs/UNIVERSAL_AGENT_GUIDE.md) for detailed guides. + ## [1.6.0] - 2026-01-03 ### Added diff --git a/README.md b/README.md index ee6340c..1de743a 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,20 @@ skilz install anthropics_skills/algorithmic-art skilz install https://github.com/owner/repo # Install for a specific agent -skilz install anthropics_skills/brand-guidelines --agent gemini +skilz install anthropics_skills/brand-guidelines --agent opencode # Install at project level (for sandboxed agents) skilz install anthropics_skills/frontend-design --agent copilot -p +# Gemini CLI native support (NEW in 1.7 - requires experimental.skills plugin) +skilz install anthropics_skills/pdf-reader --agent gemini --project + +# Legacy Gemini workflow (NEW in 1.7 - for users without experimental.skills) +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md + +# Universal agent with custom config (NEW in 1.7) +skilz install anthropics_skills/excel --agent universal --project --config CUSTOM.md + # List installed skills (or use alias: skilz ls) skilz list @@ -133,6 +142,64 @@ The registry tells Skilz exactly where to find each skill and which version to i --- +## Agent Installation Modes (NEW in 1.7) + +Skilz supports different installation modes depending on the AI agent: + +### Native Agent Support + +Agents with **native skill support** read skills directly from their designated directories: + +- **Claude Code**: `.claude/skills/` → No config file needed +- **OpenCode**: `.opencode/skill/` → No config file needed +- **Codex**: `.codex/skills/` → No config file needed +- **GitHub Copilot**: `.github/skills/` → No config file needed +- **Gemini CLI** (NEW in 1.7): `.gemini/skills/` → Requires `experimental.skills` plugin + +**Example:** +```bash +# Install for Gemini with native support (requires experimental.skills plugin) +skilz install pdf-reader --agent gemini --project +# → Installs to: .gemini/skills/pdf-reader/ +# → Config file: None (Gemini reads directory natively) +``` + +### Legacy/Universal Mode + +For agents without native support or legacy workflows, use the **universal agent** with custom config files: + +**Example:** +```bash +# Legacy Gemini workflow (for users without experimental.skills plugin) +skilz install pdf-reader --agent universal --project --config GEMINI.md +# → Installs to: .skilz/skills/pdf-reader/ +# → Config file: GEMINI.md (created/updated with skill reference) +``` + +**Custom Config Example:** +```bash +# Use any custom config file name +skilz install excel --agent universal --project --config MY_SKILLS.md +# → Installs to: .skilz/skills/excel/ +# → Config file: MY_SKILLS.md (created/updated) +``` + +### When to Use Which Mode + +| Scenario | Command | Result | +|----------|---------|--------| +| Gemini with plugin | `--agent gemini --project` | Native: `.gemini/skills/` | +| Gemini without plugin | `--agent universal --project --config GEMINI.md` | Universal: `.skilz/skills/` + GEMINI.md | +| Claude Code | `--agent claude` | Native: `~/.claude/skills/` | +| Multi-agent project | `--agent universal --project` | Universal: `.skilz/skills/` + AGENTS.md | +| Custom workflow | `--agent universal --project --config CUSTOM.md` | Universal: `.skilz/skills/` + CUSTOM.md | + +For detailed migration guides, see: +- [Gemini Migration Guide](docs/GEMINI_MIGRATION.md) +- [Universal Agent Guide](docs/UNIVERSAL_AGENT_GUIDE.md) + +--- + ## CLI Reference ### `skilz install ` diff --git a/docs/GEMINI_MIGRATION.md b/docs/GEMINI_MIGRATION.md new file mode 100644 index 0000000..8b5e086 --- /dev/null +++ b/docs/GEMINI_MIGRATION.md @@ -0,0 +1,466 @@ +# Gemini CLI Migration Guide + +**Version:** Skilz 1.7.0 +**Date:** January 8, 2026 + +--- + +## Overview + +Skilz 1.7.0 introduces native support for Gemini CLI's `.gemini/skills/` directory. This guide helps you choose the right installation method and migrate existing installations if needed. + +--- + +## Which Installation Method Should I Use? + +### ✅ Native Support (Recommended) + +**Use this if you have:** +- Gemini CLI with `experimental.skills` plugin enabled + +**Benefits:** +- Skills install directly to `.gemini/skills/` (native location) +- No config file needed - Gemini reads the directory natively +- Cleaner project structure +- Faster skill loading + +**Command:** +```bash +skilz install --agent gemini --project +``` + +**What happens:** +- Skill installs to: `.gemini/skills//` +- Config file: None (not needed) +- Gemini reads skills automatically from the directory + +--- + +### 🔧 Legacy/Universal Mode + +**Use this if you:** +- Don't have the `experimental.skills` plugin +- Use an older version of Gemini CLI +- Prefer explicit config file management + +**Benefits:** +- Works with any version of Gemini CLI +- Skills listed explicitly in GEMINI.md +- Compatible with multi-agent projects + +**Command:** +```bash +skilz install --agent universal --project --config GEMINI.md +``` + +**What happens:** +- Skill installs to: `.skilz/skills//` +- Config file: `GEMINI.md` (created/updated) +- You reference the config file in your Gemini prompts + +--- + +## How to Check if You Have Native Support + +### Option 1: Check Gemini CLI Version + +```bash +gemini --version +``` + +If you see version **0.5.0 or higher**, you likely have native support available. + +### Option 2: Check for experimental.skills Plugin + +```bash +# Check Gemini's configuration +cat ~/.gemini/settings.json | grep experimental +``` + +If you see `"experimental.skills": true`, native support is enabled. + +### Option 3: Test It + +```bash +# Try installing with native support +skilz install anthropics_skills/algorithmic-art --agent gemini --project + +# Check if directory was created +ls -la .gemini/skills/ +``` + +If `.gemini/skills/algorithmic-art/` exists, native support is working. + +--- + +## Migration Paths + +### Scenario 1: Moving from Universal to Native + +**Before (Universal Mode):** +``` +project/ +├── .skilz/ +│ └── skills/ +│ └── pdf-reader/ +└── GEMINI.md ← Config file with skill reference +``` + +**After (Native Mode):** +``` +project/ +└── .gemini/ + └── skills/ + └── pdf-reader/ ← Direct native location +``` + +**Migration Steps:** + +1. **Check if native support is available** (see above) + +2. **Remove old installation:** + ```bash + skilz rm pdf-reader --agent universal --project -y + ``` + +3. **Reinstall with native support:** + ```bash + skilz install --agent gemini --project + ``` + +4. **Clean up config file (optional):** + ```bash + rm GEMINI.md # Only if no other skills use it + ``` + +5. **Verify installation:** + ```bash + skilz list --agent gemini --project + ``` + +--- + +### Scenario 2: Staying on Universal Mode + +If you prefer to keep using universal mode (or can't use native support), no migration is needed. Your existing installations will continue to work. + +**To install new skills:** +```bash +skilz install --agent universal --project --config GEMINI.md +``` + +--- + +### Scenario 3: Mixed Environment (Multiple Projects) + +You can use different modes for different projects: + +**Project A (Native):** +```bash +cd project-a +skilz install pdf-reader --agent gemini --project +# → Uses .gemini/skills/ +``` + +**Project B (Universal):** +```bash +cd project-b +skilz install pdf-reader --agent universal --project --config GEMINI.md +# → Uses .skilz/skills/ + GEMINI.md +``` + +--- + +## Detailed Examples + +### Example 1: Install PDF Reader with Native Support + +```bash +# Navigate to your project +cd ~/my-project + +# Install with native Gemini support +skilz install anthropics_skills/pdf-reader --agent gemini --project + +# Verify installation +ls -la .gemini/skills/pdf-reader/ + +# List installed skills +skilz list --agent gemini --project +``` + +**Output:** +``` +Skill Version Installed Status +────────────────────────────────────────────────────────────────── +anthropics_skills/pdf-reader abc123 2026-01-08 up-to-date +``` + +**Directory structure:** +``` +my-project/ +└── .gemini/ + └── skills/ + └── pdf-reader/ + ├── SKILL.md + ├── .skilz-manifest.yaml + └── ... (skill files) +``` + +--- + +### Example 2: Install Excel Skill with Legacy Mode + +```bash +# Navigate to your project +cd ~/my-project + +# Install with universal agent + GEMINI.md +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md + +# Verify installation +ls -la .skilz/skills/excel/ +cat GEMINI.md # View the generated config + +# List installed skills +skilz list --agent universal --project +``` + +**Output:** +``` +Skill Version Installed Status +──────────────────────────────────────────────────────────────── +anthropics_skills/excel def456 2026-01-08 up-to-date +``` + +**Directory structure:** +``` +my-project/ +├── .skilz/ +│ └── skills/ +│ └── excel/ +│ ├── SKILL.md +│ ├── .skilz-manifest.yaml +│ └── ... (skill files) +└── GEMINI.md ← Config file with skill reference +``` + +**GEMINI.md contents:** +```markdown +# GEMINI.md + +## Available Skills + + + + + excel + Work with Excel spreadsheets + skilz read excel + + + +``` + +--- + +### Example 3: Installing Multiple Skills + +**Native mode (multiple skills):** +```bash +skilz install anthropics_skills/pdf-reader --agent gemini --project +skilz install anthropics_skills/excel --agent gemini --project +skilz install anthropics_skills/docx --agent gemini --project + +# All skills in .gemini/skills/ +ls -la .gemini/skills/ +# pdf-reader/ excel/ docx/ +``` + +**Legacy mode (multiple skills):** +```bash +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md +skilz install anthropics_skills/docx --agent universal --project --config GEMINI.md + +# All skills in .skilz/skills/, all referenced in GEMINI.md +ls -la .skilz/skills/ +# pdf-reader/ excel/ docx/ +cat GEMINI.md # Shows all 3 skills +``` + +--- + +## Troubleshooting + +### Issue: Native installation fails + +**Error:** +``` +Error: Agent 'gemini' does not support home-level installations +``` + +**Solution:** Add the `--project` flag: +```bash +skilz install --agent gemini --project +``` + +--- + +### Issue: Skills not loading in Gemini CLI + +**For Native Mode:** + +1. **Check if experimental.skills is enabled:** + ```bash + cat ~/.gemini/settings.json | grep experimental + ``` + +2. **Verify skills are in correct location:** + ```bash + ls -la .gemini/skills/ + ``` + +3. **Try restarting Gemini CLI:** + ```bash + # Exit and restart Gemini + ``` + +**For Legacy Mode:** + +1. **Check if GEMINI.md exists:** + ```bash + cat GEMINI.md + ``` + +2. **Manually load skills in your prompts:** + ``` + Use the skills defined in GEMINI.md + ``` + +--- + +### Issue: Config file not created (legacy mode) + +**Error:** +``` +GEMINI.md not created after installation +``` + +**Solution:** Make sure you're using the `--config` flag: +```bash +skilz install --agent universal --project --config GEMINI.md +# ^^^^^^ Must include this +``` + +--- + +### Issue: Mixed native and legacy installations + +If you accidentally installed some skills with native mode and others with legacy mode: + +**Check which mode each skill uses:** +```bash +# Native skills +ls -la .gemini/skills/ + +# Universal/legacy skills +ls -la .skilz/skills/ +``` + +**Standardize on one mode:** + +**Option A: Move to native (recommended):** +```bash +# Remove universal installations +skilz rm --agent universal --project -y + +# Reinstall with native +skilz install --agent gemini --project +``` + +**Option B: Move to universal:** +```bash +# Remove native installations +skilz rm --agent gemini --project -y + +# Reinstall with universal +skilz install --agent universal --project --config GEMINI.md +``` + +--- + +## FAQ + +### Q: Which mode is better? + +**A:** Native mode is recommended if you have `experimental.skills` enabled. It's cleaner, faster, and follows Gemini's intended architecture. + +### Q: Can I use both modes in the same project? + +**A:** Technically yes, but it's not recommended. Choose one mode to avoid confusion. + +### Q: Will my existing installations break after upgrading to Skilz 1.7.0? + +**A:** No. Existing installations continue to work. You only get native support if you explicitly use `--agent gemini`. + +### Q: How do I enable experimental.skills in Gemini CLI? + +**A:** Check Gemini CLI's documentation: +```bash +gemini config set experimental.skills true +``` +(Note: Exact command may vary by Gemini version) + +### Q: Can I switch between modes easily? + +**A:** Yes. Just uninstall and reinstall with the desired mode (see migration steps above). + +### Q: Does native mode work for user-level installs? + +**A:** Yes! In Skilz 1.7.0, Gemini supports both: +- Project-level: `.gemini/skills/` +- User-level: `~/.gemini/skills/` + +```bash +# User-level native install (NEW in 1.7.0) +skilz install --agent gemini +``` + +--- + +## Summary + +| Feature | Native Mode | Legacy/Universal Mode | +|---------|-------------|----------------------| +| **Requires** | experimental.skills plugin | Any Gemini version | +| **Install location** | `.gemini/skills/` | `.skilz/skills/` | +| **Config file** | None | GEMINI.md | +| **Performance** | Faster (direct read) | Slightly slower (config parse) | +| **Recommended for** | New projects | Older Gemini versions | +| **User-level support** | ✅ Yes (NEW in 1.7) | ✅ Yes | +| **Project-level support** | ✅ Yes | ✅ Yes | + +--- + +## Next Steps + +1. **Determine which mode you need** (native vs legacy) +2. **Test with one skill** before migrating all skills +3. **Update your project documentation** to reflect the chosen mode +4. **Consider setting a default agent** in `.skilz/config.yaml`: + +```yaml +default_agent: gemini # For native mode projects +# OR +default_agent: universal # For legacy mode projects +``` + +For more information, see: +- [Universal Agent Guide](UNIVERSAL_AGENT_GUIDE.md) +- [User Manual](USER_MANUAL.md) +- [CHANGELOG](../CHANGELOG.md) + +--- + +**Need help?** Open an issue on [GitHub](https://github.com/spillwave/skilz-cli/issues). diff --git a/docs/UNIVERSAL_AGENT_GUIDE.md b/docs/UNIVERSAL_AGENT_GUIDE.md new file mode 100644 index 0000000..44f5b1d --- /dev/null +++ b/docs/UNIVERSAL_AGENT_GUIDE.md @@ -0,0 +1,893 @@ +# Universal Agent Guide + +## Overview + +The **Universal Agent** is a special agent type in Skilz that provides maximum flexibility for skill management. Unlike native agents (Claude, Gemini, Codex) that have specific directory structures and behaviors, the universal agent lets you: + +- Install skills to custom locations +- Use custom configuration files +- Support multiple agents in the same project +- Work with legacy agent configurations + +**When to Use Universal Agent:** +- Legacy Gemini workflow (without `experimental.skills` plugin) +- Multi-agent projects requiring shared skill documentation +- Custom config file workflows +- Projects needing explicit skill documentation in specific files + +**When to Use Native Agents:** +- Single-agent projects (Claude, Gemini with native support, Codex) +- Simpler workflows without custom config needs +- Taking advantage of agent-specific optimizations + +--- + +## Installation Modes + +The universal agent supports two installation modes: + +### User-Level Installation (Default) + +Installs skills to `~/.skilz/skills/` for use across all projects. + +```bash +skilz install --agent universal +# or simply (universal is default): +skilz install +``` + +**Directory Structure:** +``` +~/.skilz/ +└── skills/ + └── / + └── SKILL.md +``` + +**No Config File:** User-level installations don't create/update config files. + +--- + +### Project-Level Installation (NEW in 1.7) + +Installs skills to `.skilz/skills/` within your project and creates/updates a configuration file. + +```bash +skilz install --agent universal --project +``` + +**Directory Structure:** +``` +my-project/ +├── .skilz/ +│ └── skills/ +│ └── / +│ └── SKILL.md +└── AGENTS.md # Created/updated automatically +``` + +**Default Config File:** `AGENTS.md` (auto-detected and created if needed) + +--- + +### Custom Config File (NEW in 1.7) + +Specify a custom configuration file for project-level installations. + +```bash +skilz install --agent universal --project --config CUSTOM.md +``` + +**Directory Structure:** +``` +my-project/ +├── .skilz/ +│ └── skills/ +│ └── / +│ └── SKILL.md +└── CUSTOM.md # Created/updated (NOT AGENTS.md) +``` + +**Requirements:** +- `--config` flag **requires** `--project` flag +- Can use any filename (e.g., `GEMINI.md`, `CUSTOM.md`, `AI_CONFIG.md`) +- Only the specified file is created/updated (overrides auto-detection) + +--- + +## Basic Usage + +### 1. Default Project Installation + +Install a skill to your project using the default `AGENTS.md` config: + +```bash +cd my-project +skilz install anthropics_skills/pdf-reader --agent universal --project +``` + +**What happens:** +1. Skill downloaded to `.skilz/skills/pdf-reader/` +2. `AGENTS.md` created (if doesn't exist) +3. Skill entry added to `AGENTS.md` + +**AGENTS.md content:** +```markdown +# AGENTS.md + + + +## Available Skills + + +- **pdf-reader**: Extract text and tables from PDF files + - Invoke: `skilz read pdf-reader` + + + +``` + +--- + +### 2. Custom Config File Installation + +Install a skill using a custom config file: + +```bash +cd my-project +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md +``` + +**What happens:** +1. Skill downloaded to `.skilz/skills/excel/` +2. `GEMINI.md` created (if doesn't exist) +3. Skill entry added to `GEMINI.md` (NOT `AGENTS.md`) + +**GEMINI.md content:** +```markdown +# GEMINI.md + + + +## Available Skills + + +- **excel**: Create and manipulate Excel spreadsheets + - Invoke: `skilz read excel` + + + +``` + +**Note:** `AGENTS.md` is NOT created or modified when using `--config`. + +--- + +### 3. Multiple Skills in One Config + +Install multiple skills to the same config file: + +```bash +cd my-project +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md +skilz install anthropics_skills/docx --agent universal --project --config GEMINI.md +``` + +**GEMINI.md content:** +```markdown +# GEMINI.md + + + +## Available Skills + + +- **pdf-reader**: Extract text and tables from PDF files + - Invoke: `skilz read pdf-reader` +- **excel**: Create and manipulate Excel spreadsheets + - Invoke: `skilz read excel` +- **docx**: Create and edit Word documents + - Invoke: `skilz read docx` + + + +``` + +--- + +### 4. Multiple Config Files in One Project + +Use different config files for different purposes: + +```bash +cd my-project + +# Skills for Gemini CLI (legacy mode) +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md + +# Skills for OpenCode +skilz install anthropics_skills/docx --agent universal --project --config AGENTS.md +skilz install anthropics_skills/plantuml --agent universal --project --config AGENTS.md + +# Skills for custom agent +skilz install anthropics_skills/jira --agent universal --project --config CUSTOM_AGENT.md +``` + +**Project Structure:** +``` +my-project/ +├── .skilz/ +│ └── skills/ +│ ├── pdf-reader/ +│ ├── excel/ +│ ├── docx/ +│ ├── plantuml/ +│ └── jira/ +├── GEMINI.md # pdf-reader, excel +├── AGENTS.md # docx, plantuml +└── CUSTOM_AGENT.md # jira +``` + +Each config file only contains the skills you explicitly installed to it. + +--- + +## Use Cases + +### Use Case 1: Legacy Gemini Workflow + +**Scenario:** You're using Gemini CLI without the `experimental.skills` plugin. + +**Solution:** Use universal agent with custom `GEMINI.md` config: + +```bash +cd my-ai-project + +# Check if native Gemini support is available +skilz install test-skill --agent gemini --project +# If error: "Gemini does not support project-level installations" +# → Use universal agent instead + +# Install skills for Gemini using universal agent +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md +skilz install anthropics_skills/docx --agent universal --project --config GEMINI.md + +# List installed skills +skilz list --agent universal --project +``` + +**Result:** +``` +my-ai-project/ +├── .skilz/ +│ └── skills/ +│ ├── pdf-reader/ +│ ├── excel/ +│ └── docx/ +└── GEMINI.md # All three skills documented here +``` + +**Gemini CLI will read:** `GEMINI.md` and load skills from `.skilz/skills/` + +--- + +### Use Case 2: Multi-Agent Project with Shared Skills + +**Scenario:** You're working on a project that uses multiple AI agents (Claude, Gemini, custom agents). + +**Solution:** Use universal agent with different config files for each agent: + +```bash +cd multi-agent-project + +# Skills for Claude Code +skilz install anthropics_skills/pdf-reader --agent claude --project +# → Creates .claude/skills/pdf-reader/ (native) + +# Skills for Gemini (legacy) +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md +skilz install anthropics_skills/docx --agent universal --project --config GEMINI.md + +# Skills for OpenCode +skilz install anthropics_skills/plantuml --agent universal --project --config AGENTS.md +skilz install anthropics_skills/jira --agent universal --project --config AGENTS.md + +# Skills for custom agent +skilz install anthropics_skills/confluence --agent universal --project --config CUSTOM.md +``` + +**Result:** +``` +multi-agent-project/ +├── .claude/ +│ └── skills/ +│ └── pdf-reader/ # Claude native +├── .skilz/ +│ └── skills/ +│ ├── excel/ # Universal (Gemini) +│ ├── docx/ # Universal (Gemini) +│ ├── plantuml/ # Universal (OpenCode) +│ ├── jira/ # Universal (OpenCode) +│ └── confluence/ # Universal (Custom) +├── GEMINI.md # excel, docx +├── AGENTS.md # plantuml, jira +└── CUSTOM.md # confluence +``` + +Each agent reads its own config file and loads the appropriate skills. + +--- + +### Use Case 3: Explicit Skill Documentation + +**Scenario:** You want to maintain explicit documentation of which skills are available in your project. + +**Solution:** Use project-level universal agent installations: + +```bash +cd documented-project + +# Install skills with explicit documentation +skilz install anthropics_skills/pdf-reader --agent universal --project +skilz install anthropics_skills/excel --agent universal --project +skilz install anthropics_skills/docx --agent universal --project + +# AGENTS.md is automatically maintained with skill list +cat AGENTS.md +``` + +**Benefit:** `AGENTS.md` serves as: +- Single source of truth for available skills +- Documentation for team members +- Version-controlled skill inventory +- Agent configuration reference + +--- + +### Use Case 4: Migrating from Native to Universal + +**Scenario:** You started with Gemini native support but need to switch to universal agent. + +**Before (Gemini Native):** +``` +my-project/ +└── .gemini/ + └── skills/ + ├── pdf-reader/ + └── excel/ +``` + +**Migration Steps:** +```bash +cd my-project + +# Option 1: Keep using Gemini native (recommended if plugin available) +# No action needed + +# Option 2: Switch to universal agent +# Remove old skills +skilz remove pdf-reader --agent gemini --project +skilz remove excel --agent gemini --project + +# Install with universal agent +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md +skilz install anthropics_skills/excel --agent universal --project --config GEMINI.md +``` + +**After (Universal):** +``` +my-project/ +├── .skilz/ +│ └── skills/ +│ ├── pdf-reader/ +│ └── excel/ +└── GEMINI.md +``` + +**When to Migrate:** +- Lost access to `experimental.skills` plugin +- Need more control over skill locations +- Want to use multiple agents in same project +- Need custom config file organization + +--- + +## Comparison with Native Agents + +| Feature | Universal Agent | Native Agents (Claude/Gemini/Codex) | +|---------|----------------|-------------------------------------| +| **User Install Location** | `~/.skilz/skills/` | `~/./skills/` | +| **Project Install Location** | `.skilz/skills/` | `./skills/` | +| **Config File** | Required for project | Optional or None | +| **Custom Config** | Yes (`--config` flag) | No | +| **Multi-Agent Support** | Yes (multiple configs) | No (one agent per project) | +| **Flexibility** | High | Medium | +| **Simplicity** | Medium | High | +| **Agent Detection** | Manual (`--agent universal`) | Automatic (from directory) | +| **Native Integration** | No | Yes | +| **Project Installs** | Yes (as of 1.7.0) | Yes | +| **Custom Config Files** | Yes (as of 1.7.0) | No | + +**Choose Universal Agent when:** +- You need custom config files +- You're using legacy Gemini workflow +- You want multiple agents in one project +- You need explicit skill documentation + +**Choose Native Agent when:** +- You're using a single agent (Claude, Gemini with native support, Codex) +- You want simpler workflows +- You want automatic agent detection +- You prefer agent-native directory structures + +--- + +## Detailed Examples + +### Example 1: Single Skill Installation + +**Goal:** Install one skill to a project using default config. + +```bash +cd my-project +skilz install anthropics_skills/pdf-reader --agent universal --project +``` + +**Output:** +``` +✓ Cloning skill repository... +✓ Installing skill: pdf-reader +✓ Skill installed to: .skilz/skills/pdf-reader +✓ Config updated: AGENTS.md +``` + +**Verification:** +```bash +# Check directory structure +ls -la .skilz/skills/pdf-reader/ +# → SKILL.md + +# Check config file +cat AGENTS.md +# → Contains pdf-reader entry + +# List installed skills +skilz list --agent universal --project +# → pdf-reader +``` + +--- + +### Example 2: Multiple Skills, One Config + +**Goal:** Install three skills to the same custom config file. + +```bash +cd my-project + +# Install skills one by one +skilz install anthropics_skills/pdf-reader --agent universal --project --config TOOLS.md +skilz install anthropics_skills/excel --agent universal --project --config TOOLS.md +skilz install anthropics_skills/docx --agent universal --project --config TOOLS.md +``` + +**Output (first install):** +``` +✓ Cloning skill repository... +✓ Installing skill: pdf-reader +✓ Skill installed to: .skilz/skills/pdf-reader +✓ Config updated: TOOLS.md +``` + +**Output (subsequent installs):** +``` +✓ Cloning skill repository... +✓ Installing skill: excel +✓ Skill installed to: .skilz/skills/excel +✓ Config updated: TOOLS.md (skill added) +``` + +**Verification:** +```bash +cat TOOLS.md +``` + +**TOOLS.md content:** +```markdown +# TOOLS.md + + + +## Available Skills + + +- **pdf-reader**: Extract text and tables from PDF files + - Invoke: `skilz read pdf-reader` +- **excel**: Create and manipulate Excel spreadsheets + - Invoke: `skilz read excel` +- **docx**: Create and edit Word documents + - Invoke: `skilz read docx` + + + +``` + +--- + +### Example 3: Multiple Configs, Different Skills + +**Goal:** Organize skills into different config files by purpose. + +```bash +cd my-project + +# Document processing skills +skilz install anthropics_skills/pdf-reader --agent universal --project --config DOCUMENTS.md +skilz install anthropics_skills/docx --agent universal --project --config DOCUMENTS.md + +# Data analysis skills +skilz install anthropics_skills/excel --agent universal --project --config DATA.md +skilz install anthropics_skills/duckdb --agent universal --project --config DATA.md + +# Collaboration skills +skilz install anthropics_skills/jira --agent universal --project --config COLLAB.md +skilz install anthropics_skills/confluence --agent universal --project --config COLLAB.md +``` + +**Project Structure:** +``` +my-project/ +├── .skilz/ +│ └── skills/ +│ ├── pdf-reader/ +│ ├── docx/ +│ ├── excel/ +│ ├── duckdb/ +│ ├── jira/ +│ └── confluence/ +├── DOCUMENTS.md # pdf-reader, docx +├── DATA.md # excel, duckdb +└── COLLAB.md # jira, confluence +``` + +**List skills by config:** +```bash +# All universal skills in project +skilz list --agent universal --project +# → Shows all 6 skills + +# Skills from specific config (manually check file) +grep "^- \*\*" DOCUMENTS.md +# → pdf-reader, docx +``` + +--- + +### Example 4: Removing Skills + +**Goal:** Remove a skill from a custom config. + +```bash +cd my-project + +# Install skills +skilz install anthropics_skills/pdf-reader --agent universal --project --config TOOLS.md +skilz install anthropics_skills/excel --agent universal --project --config TOOLS.md + +# Remove one skill +skilz remove pdf-reader --agent universal --project +``` + +**Output:** +``` +✓ Removing skill: pdf-reader +✓ Skill removed from: .skilz/skills/pdf-reader +✓ Config updated: TOOLS.md (skill entry removed) +``` + +**TOOLS.md after removal:** +```markdown +# TOOLS.md + + + +## Available Skills + + +- **excel**: Create and manipulate Excel spreadsheets + - Invoke: `skilz read excel` + + + +``` + +**Note:** Config file is automatically updated to remove the skill entry. + +--- + +## Troubleshooting + +### Issue 1: Config File Not Created + +**Problem:** Installed skill but config file wasn't created. + +**Possible Causes:** +1. Used user-level install (no `--project` flag) +2. File permissions issue +3. Already exists but not recognized + +**Solution:** +```bash +# Verify you used --project flag +skilz install --agent universal --project --config CUSTOM.md + +# Check if file was created +ls -la CUSTOM.md + +# Check file permissions +ls -l CUSTOM.md +# Should be readable/writable + +# Manually create if needed +touch CUSTOM.md +skilz install --agent universal --project --config CUSTOM.md +``` + +--- + +### Issue 2: Skills in Wrong Location + +**Problem:** Skills installed to `~/.skilz/skills/` instead of `.skilz/skills/`. + +**Cause:** Missing `--project` flag. + +**Solution:** +```bash +# Wrong (user-level) +skilz install --agent universal + +# Correct (project-level) +skilz install --agent universal --project +``` + +--- + +### Issue 3: Wrong Config File Updated + +**Problem:** Installed with `--config CUSTOM.md` but `AGENTS.md` was updated. + +**Cause:** This shouldn't happen in 1.7.0+. If it does, it's a bug. + +**Solution:** +```bash +# Verify version +skilz --version +# Should be 1.7.0 or higher + +# Try again with explicit paths +skilz install --agent universal --project --config ./CUSTOM.md + +# If still broken, report bug at: +# https://github.com/spillwave/skilz-cli/issues +``` + +--- + +### Issue 4: Multiple Agents Conflict + +**Problem:** Have both `.claude/skills/` and `.skilz/skills/` and unclear which is used. + +**Explanation:** This is by design! Different agents read different locations: +- **Claude Code** reads `.claude/skills/` (native) +- **Gemini (native)** reads `.gemini/skills/` (native) +- **Universal agent** reads `.skilz/skills/` + config file + +**Solution:** Each agent manages its own skills independently: +```bash +# Claude native skills +skilz list --agent claude --project + +# Universal agent skills +skilz list --agent universal --project + +# They don't conflict - each agent has its own set +``` + +--- + +### Issue 5: `--config` Requires `--project` Error + +**Problem:** Got error "Error: --config flag requires --project flag". + +**Cause:** Used `--config` without `--project`. + +**Solution:** +```bash +# Wrong +skilz install --agent universal --config CUSTOM.md + +# Correct +skilz install --agent universal --project --config CUSTOM.md +``` + +**Why:** Custom configs only make sense for project-level installs. User-level installs don't use config files. + +--- + +### Issue 6: Skill Not Loading in Agent + +**Problem:** Installed skill but AI agent doesn't recognize it. + +**Possible Causes:** +1. Wrong config file name (agent expects different name) +2. Skill not properly documented in config +3. Agent not reading `.skilz/skills/` directory + +**Solution:** +```bash +# 1. Check which config file your agent expects +# - Gemini CLI (legacy): GEMINI.md +# - OpenCode: AGENTS.md +# - Custom: Whatever you configured + +# 2. Verify skill is in config file +cat AGENTS.md +# Should contain skill entry + +# 3. Verify skill files exist +ls -la .skilz/skills// +# Should contain SKILL.md + +# 4. Re-read skill +skilz read +# Should output skill content + +# 5. Reinstall if necessary +skilz remove --agent universal --project +skilz install / --agent universal --project --config AGENTS.md +``` + +--- + +## Best Practices + +### When to Use `--config` Flag + +**Use `--config` when:** +- Working with legacy Gemini (without native support) +- Organizing skills by purpose (DOCUMENTS.md, DATA.md, etc.) +- Supporting multiple agents in one project +- Need specific config file naming + +**Don't use `--config` when:** +- Default `AGENTS.md` is fine +- Using native agent support (Claude, Gemini native, Codex) +- User-level installs (doesn't apply) + +--- + +### Organizing Multi-Agent Projects + +**Strategy 1: One Config Per Agent** +``` +my-project/ +├── CLAUDE.md # Skills for Claude Code +├── GEMINI.md # Skills for Gemini CLI +├── AGENTS.md # Skills for OpenCode +└── .skilz/skills/ # All universal skills here +``` + +**Strategy 2: One Config Per Purpose** +``` +my-project/ +├── DOCUMENTS.md # pdf-reader, docx, confluence +├── DATA.md # excel, duckdb, postgresql +├── COLLAB.md # jira, notion, github +└── .skilz/skills/ # All skills here +``` + +**Strategy 3: Hybrid Approach** +``` +my-project/ +├── .claude/skills/ # Native Claude skills +├── .gemini/skills/ # Native Gemini skills (if available) +├── AGENTS.md # Universal skills for OpenCode +└── .skilz/skills/ # Universal skills shared across agents +``` + +--- + +### Config File Naming Conventions + +**Recommended Names:** +- `AGENTS.md` - Default, works with OpenCode +- `GEMINI.md` - Legacy Gemini CLI +- `CLAUDE.md` - If using universal instead of Claude native +- `SKILLS.md` - Generic alternative +- `AI_TOOLS.md` - Descriptive name + +**Avoid:** +- Generic names like `config.md` or `settings.md` +- Names that conflict with existing files +- Names with spaces or special characters + +**Case Sensitivity:** Use uppercase for visibility (convention), but lowercase works too. + +--- + +### Version Control + +**Always Commit:** +- Config files (`AGENTS.md`, `GEMINI.md`, etc.) +- `.skilz/` directory structure (but not skill contents) + +**Add to `.gitignore`:** +``` +# .gitignore +.skilz/skills/*/SKILL.md # Skill content (fetched from remote) +.skilz/skills/*/.git/ # Git metadata (if any) +``` + +**Rationale:** +- Config files document which skills are used +- Skill content can be re-fetched with `skilz update` +- Team members can clone repo and run `skilz update --all` to sync + +--- + +### Updating Skills + +**Update all universal skills in project:** +```bash +skilz update --all --agent universal --project +``` + +**Update specific skill:** +```bash +skilz update --agent universal --project +``` + +**After Update:** +- Config files are automatically maintained +- Skill content is refreshed from remote +- No manual changes needed + +--- + +## Summary + +The **universal agent** in Skilz 1.7+ provides powerful flexibility for managing AI agent skills: + +**Key Features:** +- ✅ Project-level installations with custom config files +- ✅ Support for multiple agents in one project +- ✅ Legacy Gemini workflow support +- ✅ Explicit skill documentation in custom files +- ✅ Works alongside native agents (Claude, Gemini, Codex) + +**Common Workflows:** +1. **Default project install:** `skilz install --agent universal --project` +2. **Custom config install:** `skilz install --agent universal --project --config CUSTOM.md` +3. **Legacy Gemini:** `skilz install --agent universal --project --config GEMINI.md` +4. **Multi-agent project:** Use different config files for each agent + +**Best Practices:** +- Use native agents when available (simpler) +- Use universal agent for custom workflows +- Organize skills by purpose or agent +- Commit config files to version control +- Ignore skill content (re-fetchable) + +**Learn More:** +- [USER_MANUAL.md](USER_MANUAL.md) - Complete user guide +- [GEMINI_MIGRATION.md](GEMINI_MIGRATION.md) - Gemini-specific migration guide +- [README.md](../README.md) - Quick start and examples + +--- + +**Questions or Issues?** + +Report issues at: https://github.com/spillwave/skilz-cli/issues diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index 185ab40..dc7a417 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -30,6 +30,8 @@ Skilz is the universal package manager for AI skills. It installs, manages, and 5. [Shell Completion](#shell-completion) 6. [Global Flags](#global-flags) 7. [Working with Multiple Agents](#working-with-multiple-agents) + - [Gemini CLI Support (NEW in 1.7)](#gemini-cli-support-new-in-17) + - [Universal Agent Custom Config (NEW in 1.7)](#universal-agent-custom-config-new-in-17) 8. [Project vs User Level Installation](#project-vs-user-level-installation) 9. [Scripting & Automation](#scripting--automation) 10. [Troubleshooting](#troubleshooting) @@ -794,18 +796,25 @@ skilz -y config --init Skilz supports multiple AI coding assistants. Use `--agent` to target a specific one: -| Agent | Skills Directory | -|-------|------------------| -| `claude` | `~/.claude/skills/` (user) or `.claude/skills/` (project) | -| `opencode` | `~/.config/opencode/skill/` (user) or `.opencode/skill/` (project) | +| Agent | Skills Directory | Notes | +|-------|------------------|-------| +| `claude` | `~/.claude/skills/` (user) or `.claude/skills/` (project) | Native support | +| `gemini` | `~/.gemini/skills/` (user) or `.gemini/skills/` (project) | Native support (NEW in 1.7, requires plugin) | +| `codex` | `~/.codex/skills/` (user) or `.codex/skills/` (project) | Auto-detected from `.codex/` | +| `opencode` | `~/.config/opencode/skill/` (user) or `.opencode/skill/` (project) | Native support | +| `universal` | `~/.skilz/skills/` (user) or `.skilz/skills/` (project) | Supports custom config files (NEW in 1.7) | **Auto-detection:** If you don't specify `--agent`, Skilz auto-detects based on: 1. Presence of `.claude/` in current directory -2. Presence of `~/.claude/` (user has Claude Code) -3. Presence of `~/.config/opencode/` (user has OpenCode) -4. Defaults to `claude` if ambiguous +2. Presence of `.gemini/` in current directory +3. Presence of `.codex/` in current directory +4. Presence of `~/.claude/` (user has Claude Code) +5. Presence of `~/.gemini/` (user has Gemini CLI) +6. Presence of `~/.codex/` (user has OpenAI Codex) +7. Presence of `~/.config/opencode/` (user has OpenCode) +8. Defaults to `claude` if none detected **Installing to multiple agents:** @@ -815,6 +824,108 @@ skilz install plantuml --agent claude skilz install plantuml --agent opencode ``` +### Gemini CLI Support (NEW in 1.7) + +Skilz 1.7+ supports two modes for Gemini CLI: + +#### Native Support (Recommended) + +**Requirements:** Gemini CLI with `experimental.skills` plugin installed. + +**Features:** +- Project-level: `.gemini/skills/` +- User-level: `~/.gemini/skills/` +- No config file needed (Gemini reads skills directly) + +**Example:** +```bash +# Install to Gemini (native mode) +skilz install anthropics_skills/pdf-reader --agent gemini --project + +# Skill is installed to: +# .gemini/skills/pdf-reader/ + +# Gemini CLI automatically detects and loads the skill +``` + +**When to use:** If you have the `experimental.skills` plugin, this is the recommended approach. + +--- + +#### Legacy Mode (Universal Agent) + +**For users without `experimental.skills` plugin.** + +**Features:** +- Project-level: `.skilz/skills/` +- Config file: `GEMINI.md` (requires `--config` flag) +- Manual skill documentation in config + +**Example:** +```bash +# Install using universal agent with Gemini config +skilz install anthropics_skills/pdf-reader --agent universal --project --config GEMINI.md + +# Skill is installed to: +# .skilz/skills/pdf-reader/ +# GEMINI.md is created/updated with skill entry + +# Configure Gemini CLI to read GEMINI.md +``` + +**When to use:** If you don't have the `experimental.skills` plugin or prefer explicit config files. + +--- + +**How to check which mode you have:** + +```bash +# Try native installation +skilz install test-skill --agent gemini --project + +# If successful → You have native support +# If error "Gemini does not support project-level installations" → Use legacy mode +``` + +**Migration Guide:** See [GEMINI_MIGRATION.md](GEMINI_MIGRATION.md) for detailed migration instructions. + +--- + +### Universal Agent Custom Config (NEW in 1.7) + +The universal agent now supports project-level installations with custom configuration files. + +#### Default Behavior + +```bash +skilz install --agent universal --project +# Creates/updates: AGENTS.md +# Skill location: .skilz/skills// +``` + +#### Custom Config File + +```bash +skilz install --agent universal --project --config GEMINI.md +# Creates/updates: GEMINI.md (not AGENTS.md) +# Skill location: .skilz/skills// +``` + +#### Requirements + +- `--config` flag **requires** `--project` flag +- Works with any filename (e.g., `GEMINI.md`, `CUSTOM.md`, `AI_CONFIG.md`) +- Only the specified file is updated (overrides auto-detection) + +#### Use Cases + +1. **Legacy Gemini workflow** (without native plugin support) +2. **Multi-agent projects** with different config files per agent +3. **Custom organization** (e.g., `DOCUMENTS.md`, `DATA.md`, etc.) +4. **Explicit documentation** of available skills + +**Complete Guide:** See [UNIVERSAL_AGENT_GUIDE.md](UNIVERSAL_AGENT_GUIDE.md) for detailed examples and use cases. + --- ## Project vs User Level Installation diff --git a/docs/project_notes/bugs.md b/docs/project_notes/bugs.md index 065033e..10c0a13 100644 --- a/docs/project_notes/bugs.md +++ b/docs/project_notes/bugs.md @@ -6,13 +6,80 @@ This document tracks bugs encountered during development and their solutions. ## Active Bugs -*No active bugs at this time.* +*(No active bugs at this time)* --- ## Resolved Bugs -### BUG-001: [Template] +### BUG-001: Codex Agent Not Auto-Detected + +**Date Discovered:** 2026-01-08 +**Date Resolved:** 2026-01-08 +**Severity:** Medium +**Related Issue:** SKILZ-49 implementation review + +**Symptoms:** +- Users with OpenAI Codex (`.codex/` or `~/.codex/`) must manually specify `--agent codex` +- Auto-detection skips over Codex even when directory markers are present +- Error-prone workflow: `skilz install skill` fails to detect Codex agent + +**Root Cause:** +- Detection logic in `src/skilz/agents.py:detect_agent()` does not check for `.codex/` directories +- Codex is configured in `agent_registry.py` with proper paths, but detection function never looks for them + +**Current Detection Order:** +```python +1. .claude/ (project) +2. .gemini/ (project) +3. ~/.claude/ (user) +4. ~/.gemini/ (user) +5. ~/.config/opencode/ (user) +6. Default to "claude" +``` + +**Expected Detection Order:** +```python +1. .claude/ (project) +2. .gemini/ (project) +3. .codex/ (project) ← MISSING +4. ~/.claude/ (user) +5. ~/.gemini/ (user) +6. ~/.codex/ (user) ← MISSING +7. ~/.config/opencode/ (user) +8. Default to "claude" +``` + +**Solution:** +Added Codex detection after Gemini in priority order in `src/skilz/agents.py` (lines 127-197): + +```python +# Check for .codex in project directory +codex_project = project_dir / ".codex" +if codex_project.exists(): + return "codex" + +# Later, after user-level Claude/Gemini checks: +codex_user = Path.home() / ".codex" +if codex_user.exists(): + return "codex" +``` + +**Tests Added:** +- `tests/test_agents.py::test_detect_codex_from_project_dir` (line 84) +- `tests/test_agents.py::test_detect_codex_from_user_dir` (line 97) +- `tests/test_agents.py::test_detect_gemini_priority_over_codex` (line 110) + +**Test Results:** ✅ 605 tests passing (added 3 new tests) + +**Prevention:** +- ✅ Added test coverage for Codex detection (3 tests) +- ✅ Verified all agents with `home_dir`/`project_dir` are included in detection +- Future: Add integration test to ensure all registry agents are covered by detection logic + +--- + +### BUG-002: [Template] **Date Discovered:** YYYY-MM-DD **Date Resolved:** YYYY-MM-DD diff --git a/pyproject.toml b/pyproject.toml index cd70c0d..4101b46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "skilz" -version = "1.6.0" +version = "1.7.0" description = "The universal package manager for AI skills" readme = "README.md" license = "MIT" diff --git a/scripts/end_to_end.sh b/scripts/end_to_end.sh index ed88e18..1054a67 100755 --- a/scripts/end_to_end.sh +++ b/scripts/end_to_end.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Skilz 1.6.0 End-to-End Test Script +# Skilz 1.7.0 End-to-End Test Script # # This script tests all major features of skilz: # - Install via marketplace ID @@ -8,11 +8,13 @@ # - Install from filesystem # - Install to various agents (claude, opencode, codex, gemini, copilot, universal) # - Install at project level +# - Gemini native support (NEW in 1.7) +# - Universal agent with custom config (NEW in 1.7) # - List commands (skilz list, skilz ls) # - Remove commands (skilz uninstall, skilz rm) # - Search command # - Visit command (dry-run only) -# - GitHub Copilot project-only installation (NEW in 1.6) +# - GitHub Copilot project-only installation # # Usage: ./scripts/end_to_end.sh # @@ -42,10 +44,9 @@ GIT_REPO_HTTPS="https://github.com/Jamie-BitFlight/claude_skills.git" GIT_REPO_SSH="git@github.com:Jamie-BitFlight/claude_skills.git" GIT_REPO_URL="https://github.com/Jamie-BitFlight/claude_skills" -# Test project directory +# Test directories +E2E_DIR="" TEST_PROJECT_DIR="" - -# Backup directories (to restore after tests) BACKUP_DIR="" #------------------------------------------------------------------------------ @@ -81,7 +82,7 @@ track_test() { print_summary_table() { echo "" echo "┌────────┬────────────────────────────────────────────────────────────────────────────────────────────────┐" - echo "│ SKILZ 1.6.0 END-TO-END TEST RESULTS │" + echo "│ SKILZ 1.7.0 END-TO-END TEST RESULTS │" echo "├────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤" printf "│ %-6s │ %-90s │\n" "STATUS" "COMMAND / DESCRIPTION" echo "├────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤" @@ -209,14 +210,60 @@ setup() { log_info "Script directory: $SCRIPT_DIR" log_info "Project root: $PROJECT_ROOT" + # Create isolated e2e test folder + E2E_DIR="$PROJECT_ROOT/e2e" + TEST_PROJECT_DIR="$E2E_DIR/test_folder" + + log_info "Creating isolated test environment: $TEST_PROJECT_DIR" + mkdir -p "$TEST_PROJECT_DIR" + + # Create mock Python project structure + mkdir -p "$TEST_PROJECT_DIR/src" + mkdir -p "$TEST_PROJECT_DIR/tests" + + # Create mock pyproject.toml + cat > "$TEST_PROJECT_DIR/pyproject.toml" <<'EOF' +[tool.poetry] +name = "skilz-e2e-test" +version = "0.1.0" +description = "Test project for Skilz E2E tests" +authors = ["Skilz Test "] + +[tool.poetry.dependencies] +python = "^3.10" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +EOF + + # Create mock requirements.txt + cat > "$TEST_PROJECT_DIR/requirements.txt" <<'EOF' +# Test dependencies +pytest>=7.0.0 +EOF + + # Create mock setup.py + cat > "$TEST_PROJECT_DIR/setup.py" <<'EOF' +from setuptools import setup, find_packages + +setup( + name="skilz-e2e-test", + version="0.1.0", + packages=find_packages(), +) +EOF + + # Create __init__.py files + touch "$TEST_PROJECT_DIR/src/__init__.py" + touch "$TEST_PROJECT_DIR/tests/__init__.py" + + log_success "Created mock project structure" + # Create backup directory for any existing installations BACKUP_DIR=$(mktemp -d) log_info "Backup directory: $BACKUP_DIR" - # Create test project directory - TEST_PROJECT_DIR=$(mktemp -d) - log_info "Test project directory: $TEST_PROJECT_DIR" - # Install the current version of skilz log_info "Installing skilz from source..." cd "$PROJECT_ROOT" @@ -448,16 +495,16 @@ test_install_project() { cd "$TEST_PROJECT_DIR" log_info "Working in project directory: $TEST_PROJECT_DIR" - local cmd="skilz install $SKILL_ID --agent gemini --project" + local cmd="skilz install $SKILL_ID --agent claude --project" - # Install to project for Gemini (project-only agent) - log_info "Installing to project for Gemini..." - if skilz install "$SKILL_ID" --agent gemini --project; then - log_success "Project install for Gemini succeeded" - track_test "$cmd" "Project install for gemini" "PASS" + # Install to project for Claude + log_info "Installing to project for Claude..." + if skilz install "$SKILL_ID" --agent claude --project; then + log_success "Project install for Claude succeeded" + track_test "$cmd" "Project install for claude" "PASS" else - log_fail "Project install for Gemini failed" - track_test "$cmd" "Project install for gemini" "FAIL" + log_fail "Project install for Claude failed" + track_test "$cmd" "Project install for claude" "FAIL" cd "$PROJECT_ROOT" return fi @@ -468,29 +515,159 @@ test_install_project() { assert_exists "$TEST_PROJECT_DIR/.skilz/skills/$SKILL_NAME/SKILL.md" "Project SKILL.md" track_test "verify: project SKILL.md" "Project SKILL.md exists" "PASS" - # Check for GEMINI.md config injection (Gemini doesn't have native support) - if [[ -f "$TEST_PROJECT_DIR/GEMINI.md" ]]; then - if grep -q "$SKILL_NAME" "$TEST_PROJECT_DIR/GEMINI.md"; then - log_success "GEMINI.md config injection verified" - track_test "verify: GEMINI.md injection" "Config injection for gemini" "PASS" + # Check for CLAUDE.md config injection + if [[ -f "$TEST_PROJECT_DIR/CLAUDE.md" ]]; then + if grep -q "$SKILL_NAME" "$TEST_PROJECT_DIR/CLAUDE.md"; then + log_success "CLAUDE.md config injection verified" + track_test "verify: CLAUDE.md injection" "Config injection for claude" "PASS" else - log_fail "GEMINI.md does not contain skill reference" - track_test "verify: GEMINI.md injection" "Config injection for gemini" "FAIL" + log_fail "CLAUDE.md does not contain skill reference" + track_test "verify: CLAUDE.md injection" "Config injection for claude" "FAIL" fi else - log_info "GEMINI.md not created (may be expected behavior)" - track_test "verify: GEMINI.md (optional)" "Config file (optional)" "PASS" + log_info "CLAUDE.md not created (may be expected behavior)" + track_test "verify: CLAUDE.md (optional)" "Config file (optional)" "PASS" fi # Verify in list - assert_skill_in_list "$SKILL_NAME" "gemini" "true" - track_test "skilz list --agent gemini --project" "Skill in project list" "PASS" + assert_skill_in_list "$SKILL_NAME" "claude" "true" + track_test "skilz list --agent claude --project" "Skill in project list" "PASS" # Cleanup - skilz rm "$SKILL_NAME" --agent gemini --project -y 2>/dev/null || \ - skilz remove "$SKILL_NAME" --agent gemini --project -y 2>/dev/null || true + rm -f "$TEST_PROJECT_DIR/CLAUDE.md" + skilz rm "$SKILL_NAME" --agent claude --project -y 2>/dev/null || \ + skilz remove "$SKILL_NAME" --agent claude --project -y 2>/dev/null || true assert_not_exists "$TEST_PROJECT_DIR/.skilz/skills/$SKILL_NAME" "Project skill directory" - track_test "skilz rm $SKILL_NAME --agent gemini -p -y" "Remove project skill" "PASS" + track_test "skilz rm $SKILL_NAME --agent claude -p -y" "Remove project skill" "PASS" + + cd "$PROJECT_ROOT" +} + +#------------------------------------------------------------------------------ +# Test: Gemini Native Support (NEW in 1.7) +#------------------------------------------------------------------------------ + +test_gemini_native() { + log_section "TEST: Gemini Native Support (NEW in 1.7)" + + cd "$TEST_PROJECT_DIR" + log_info "Working in project directory: $TEST_PROJECT_DIR" + + local cmd="skilz install $SKILL_ID --agent gemini --project" + + # Install to native .gemini/skills/ directory + log_info "Installing to native Gemini directory..." + if skilz install "$SKILL_ID" --agent gemini --project; then + log_success "Gemini native install succeeded" + track_test "$cmd" "Gemini native (.gemini/skills/)" "PASS" + else + log_fail "Gemini native install failed" + track_test "$cmd" "Gemini native (.gemini/skills/)" "FAIL" + cd "$PROJECT_ROOT" + return + fi + + # Verify installation in .gemini/skills/ (native location) + assert_exists "$TEST_PROJECT_DIR/.gemini/skills/$SKILL_NAME" "Gemini native skill directory" + track_test "verify: .gemini/skills/$SKILL_NAME" "Native Gemini dir exists" "PASS" + + # Native agents should NOT create config files + if [[ -f "$TEST_PROJECT_DIR/GEMINI.md" ]]; then + log_warn "GEMINI.md created (native agents should skip config sync)" + track_test "verify: no GEMINI.md" "Native agent skips config" "FAIL" + else + log_success "GEMINI.md correctly NOT created (native support)" + track_test "verify: no GEMINI.md" "Native agent skips config" "PASS" + fi + + # Verify in list + assert_skill_in_list "$SKILL_NAME" "gemini" "true" + track_test "skilz list --agent gemini --project" "Skill in native list" "PASS" + + # Cleanup + skilz rm "$SKILL_NAME" --agent gemini --project -y 2>/dev/null || true + assert_not_exists "$TEST_PROJECT_DIR/.gemini/skills/$SKILL_NAME" "Gemini skill directory" + track_test "skilz rm $SKILL_NAME --agent gemini -p -y" "Remove native skill" "PASS" + + cd "$PROJECT_ROOT" +} + +#------------------------------------------------------------------------------ +# Test: Universal Agent Custom Config (NEW in 1.7 - SKILZ-50) +#------------------------------------------------------------------------------ + +test_universal_custom_config() { + log_section "TEST: Universal Agent Custom Config (NEW in 1.7 - SKILZ-50)" + + cd "$TEST_PROJECT_DIR" + log_info "Working in project directory: $TEST_PROJECT_DIR" + + # Test 1: Universal + GEMINI.md (legacy Gemini workflow) + local cmd="skilz install $SKILL_ID --agent universal --project --config GEMINI.md" + + log_info "Testing universal agent with custom config file..." + if skilz install "$SKILL_ID" --agent universal --project --config GEMINI.md; then + log_success "Universal + --config GEMINI.md succeeded" + track_test "$cmd" "Universal with custom config" "PASS" + else + log_fail "Universal + --config GEMINI.md failed" + track_test "$cmd" "Universal with custom config" "FAIL" + cd "$PROJECT_ROOT" + return + fi + + # Verify installation in ./skilz/skills/ (universal location) + assert_exists "$TEST_PROJECT_DIR/.skilz/skills/$SKILL_NAME" "Universal skill directory" + track_test "verify: .skilz/skills/$SKILL_NAME" "Universal skill dir exists" "PASS" + + # Verify GEMINI.md was created and contains skill reference + if [[ -f "$TEST_PROJECT_DIR/GEMINI.md" ]]; then + if grep -q "$SKILL_NAME" "$TEST_PROJECT_DIR/GEMINI.md"; then + log_success "GEMINI.md created with skill reference" + track_test "verify: GEMINI.md updated" "Custom config updated" "PASS" + else + log_fail "GEMINI.md exists but no skill reference" + track_test "verify: GEMINI.md updated" "Custom config updated" "FAIL" + fi + else + log_fail "GEMINI.md was not created" + track_test "verify: GEMINI.md created" "Custom config created" "FAIL" + fi + + # Verify AGENTS.md was NOT created (only custom file should be updated) + if [[ ! -f "$TEST_PROJECT_DIR/AGENTS.md" ]]; then + log_success "AGENTS.md correctly NOT created (only custom config)" + track_test "verify: no AGENTS.md" "Only custom config updated" "PASS" + else + log_warn "AGENTS.md created (should only update specified file)" + track_test "verify: no AGENTS.md" "Only custom config updated" "FAIL" + fi + + # Cleanup + rm -f "$TEST_PROJECT_DIR/GEMINI.md" + skilz rm "$SKILL_NAME" --agent universal --project -y 2>/dev/null || true + + # Test 2: Universal + custom file + log_info "Testing with completely custom config file..." + if skilz install "$SKILL_ID" --agent universal --project --config CUSTOM_SKILLS.md; then + log_success "Universal + --config CUSTOM_SKILLS.md succeeded" + track_test "skilz install --config CUSTOM_SKILLS.md" "Arbitrary custom config" "PASS" + + if [[ -f "$TEST_PROJECT_DIR/CUSTOM_SKILLS.md" ]] && \ + grep -q "$SKILL_NAME" "$TEST_PROJECT_DIR/CUSTOM_SKILLS.md"; then + log_success "CUSTOM_SKILLS.md created with skill reference" + track_test "verify: CUSTOM_SKILLS.md" "Custom filename works" "PASS" + else + log_fail "CUSTOM_SKILLS.md not created or missing reference" + track_test "verify: CUSTOM_SKILLS.md" "Custom filename works" "FAIL" + fi + + rm -f "$TEST_PROJECT_DIR/CUSTOM_SKILLS.md" + skilz rm "$SKILL_NAME" --agent universal --project -y 2>/dev/null || true + fi + + assert_not_exists "$TEST_PROJECT_DIR/.skilz/skills/$SKILL_NAME" "Universal skill directory" + track_test "cleanup universal skills" "Cleanup successful" "PASS" cd "$PROJECT_ROOT" } @@ -934,10 +1111,10 @@ cleanup() { log_info "Cleaning up test directories..." - # Remove test project directory - if [[ -n "$TEST_PROJECT_DIR" && -d "$TEST_PROJECT_DIR" ]]; then - rm -rf "$TEST_PROJECT_DIR" - log_info "Removed test project directory" + # Clean up e2e test folder + if [[ -n "$E2E_DIR" && -d "$E2E_DIR" ]]; then + log_info "Removing e2e test directory: $E2E_DIR" + rm -rf "$E2E_DIR" fi # Remove backup directory @@ -948,7 +1125,7 @@ cleanup() { # Final cleanup of any remaining test skills log_info "Final cleanup of any remaining test installations..." - for agent in claude opencode codex universal; do + for agent in claude opencode codex universal gemini; do skilz rm "$SKILL_NAME" --agent "$agent" -y 2>/dev/null || \ skilz remove "$SKILL_NAME" --agent "$agent" -y 2>/dev/null || true done @@ -963,7 +1140,7 @@ cleanup() { main() { echo "" echo "==============================================" - echo " Skilz 1.6.0 End-to-End Test Suite" + echo " Skilz 1.7.0 End-to-End Test Suite" echo "==============================================" echo "" @@ -978,6 +1155,8 @@ main() { test_install_git_https_dotgit test_install_git_ssh test_install_project + test_gemini_native + test_universal_custom_config test_install_copilot test_install_filesystem test_multiple_agents diff --git a/src/skilz/__init__.py b/src/skilz/__init__.py index 0b68907..1d1fec3 100644 --- a/src/skilz/__init__.py +++ b/src/skilz/__init__.py @@ -1,6 +1,6 @@ """Skilz - The universal package manager for AI skills.""" -__version__ = "1.5.0" +__version__ = "1.7.0" __author__ = "Spillwave" from skilz.api_client import ( diff --git a/src/skilz/agent_registry.py b/src/skilz/agent_registry.py index 5a06df6..d534ee9 100644 --- a/src/skilz/agent_registry.py +++ b/src/skilz/agent_registry.py @@ -150,12 +150,13 @@ def _create_builtin_agents() -> dict[str, AgentConfig]: "gemini": AgentConfig( name="gemini", display_name="Gemini CLI", - home_dir=None, - project_dir=Path(".skilz") / "skills", + home_dir=Path.home() / ".gemini" / "skills", + project_dir=Path(".gemini") / "skills", config_files=("GEMINI.md",), - supports_home=False, + supports_home=True, default_mode="copy", - native_skill_support="none", + native_skill_support="all", + invocation="/skills or activate_skill tool", ), "copilot": AgentConfig( name="copilot", @@ -253,7 +254,7 @@ def _create_builtin_agents() -> dict[str, AgentConfig]: display_name="Universal (Skilz)", home_dir=Path.home() / ".skilz" / "skills", project_dir=Path(".skilz") / "skills", - config_files=(), + config_files=("AGENTS.md",), # SKILZ-50: Enable project-level config sync supports_home=True, default_mode="copy", native_skill_support="none", diff --git a/src/skilz/agents.py b/src/skilz/agents.py index a561bad..5708bf6 100644 --- a/src/skilz/agents.py +++ b/src/skilz/agents.py @@ -131,15 +131,19 @@ def detect_agent(project_dir: Path | None = None) -> str: Detection order: 1. Check config file for agent_default setting 2. Check for .claude/ in project directory - 3. Check for ~/.claude/ (user has Claude Code installed) - 4. Check for ~/.config/opencode/ (user has OpenCode installed) - 5. Default to "claude" if ambiguous + 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 Args: project_dir: Project directory to check. Uses cwd if None. Returns: - The detected agent type (e.g., "claude", "opencode", "gemini"). + The detected agent type (e.g., "claude", "opencode", "gemini", "codex"). """ # Check config for default agent first try: @@ -153,14 +157,26 @@ def detect_agent(project_dir: Path | None = None) -> str: project = project_dir or Path.cwd() - # Check project-level Claude first + # Check project-level markers (highest priority) if (project / ".claude").exists(): return "claude" - # Check user-level Claude (use Path.home() at detection time, not cached) + if (project / ".gemini").exists(): + return "gemini" + + if (project / ".codex").exists(): + return "codex" + + # Check user-level markers (use Path.home() at detection time, not cached) if (Path.home() / ".claude").exists(): return "claude" + if (Path.home() / ".gemini").exists(): + return "gemini" + + if (Path.home() / ".codex").exists(): + return "codex" + # Check user-level OpenCode if (Path.home() / ".config" / "opencode").exists(): return "opencode" @@ -173,7 +189,7 @@ def detect_agent(project_dir: Path | None = None) -> str: # Check less common agents for agent_name in registry.list_agents(): - if agent_name in ("claude", "opencode"): + if agent_name in ("claude", "opencode", "gemini", "codex"): continue # Already checked above agent = registry.get(agent_name) if agent: diff --git a/src/skilz/cli.py b/src/skilz/cli.py index d6c02a0..ba72e40 100644 --- a/src/skilz/cli.py +++ b/src/skilz/cli.py @@ -104,6 +104,11 @@ def create_parser() -> argparse.ArgumentParser: action="store_true", help="Install to project directory instead of user directory", ) + install_parser.add_argument( + "--config", + metavar="FILE", + help="Config file to update (requires --project). Example: --config GEMINI.md", + ) # Installation mode flags (mutually exclusive) mode_group = install_parser.add_mutually_exclusive_group() diff --git a/src/skilz/commands/install_cmd.py b/src/skilz/commands/install_cmd.py index 2949e12..8b840c4 100644 --- a/src/skilz/commands/install_cmd.py +++ b/src/skilz/commands/install_cmd.py @@ -51,6 +51,12 @@ def cmd_install(args: argparse.Namespace) -> int: skill_id: str | None = getattr(args, "skill_id", None) version_spec: str | None = getattr(args, "version_spec", None) force_config: bool = getattr(args, "force_config", False) + config_file: str | None = getattr(args, "config", None) # SKILZ-50 + + # Validate --config flag (SKILZ-50) + if config_file and not project_level: + print("Error: --config requires --project flag", file=sys.stderr) + return 1 # Handle source options file_path: str | None = getattr(args, "file", None) @@ -88,6 +94,7 @@ def cmd_install(args: argparse.Namespace) -> int: verbose=verbose, mode=mode, force_config=force_config, + config_file=config_file, # SKILZ-50 ) return 0 except SkilzError as e: @@ -125,6 +132,7 @@ def cmd_install(args: argparse.Namespace) -> int: mode=mode, version_spec=version_spec, force_config=force_config, + config_file=config_file, # SKILZ-50 ) return 0 except SkilzError as e: diff --git a/src/skilz/config_sync.py b/src/skilz/config_sync.py index 9a1b891..81fde79 100644 --- a/src/skilz/config_sync.py +++ b/src/skilz/config_sync.py @@ -417,6 +417,7 @@ def sync_skill_to_configs( project_dir: Path, agent: str | None = None, verbose: bool = False, + target_files: tuple[str, ...] | None = None, # SKILZ-50: Custom file override ) -> list[ConfigSyncResult]: """Sync a skill reference to all relevant config files. @@ -426,6 +427,9 @@ def sync_skill_to_configs( agent: If specified, only sync to this agent's config files. If None, sync to all agent config files that exist. verbose: If True, print progress information. + target_files: Optional tuple of config file names to update (e.g., ("GEMINI.md",)). + If provided, overrides auto-detection and only updates these files. + The agent parameter is ignored when target_files is specified. Returns: List of ConfigSyncResult for each config file processed. @@ -433,15 +437,37 @@ def sync_skill_to_configs( results: list[ConfigSyncResult] = [] registry = get_registry() - # Determine which config files to update - config_files = detect_project_config_files(project_dir, agent) - - if not config_files and agent: - # Agent specified but no config files found - create the first one - agent_config = registry.get(agent) - if agent_config and agent_config.config_files: - first_config = project_dir / agent_config.config_files[0] - config_files = [(agent, first_config)] + # SKILZ-50: Use custom target files if provided + if target_files: + # Convert file names to (agent, path) tuples + # For custom files, try to match to known agents, otherwise use "universal" + from skilz.agent_registry import get_builtin_agents + + config_files: list[tuple[str, Path]] = [] + all_agents = get_builtin_agents() + + for file_name in target_files: + config_path = project_dir / file_name + # Try to find agent that owns this config file + agent_owner = None + for agent_name, agent_cfg in all_agents.items(): + if file_name in agent_cfg.config_files: + agent_owner = agent_name + break + # If no owner found, use "universal" as fallback + if not agent_owner: + agent_owner = "universal" + config_files.append((agent_owner, config_path)) + else: + # Original behavior: auto-detect config files + config_files = detect_project_config_files(project_dir, agent) + + if not config_files and agent: + # Agent specified but no config files found - create the first one + agent_config = registry.get(agent) + if agent_config and agent_config.config_files: + first_config = project_dir / agent_config.config_files[0] + config_files = [(agent, first_config)] for agent_name, config_path in config_files: agent_config = registry.get(agent_name) diff --git a/src/skilz/installer.py b/src/skilz/installer.py index 64e6c77..7a3f178 100644 --- a/src/skilz/installer.py +++ b/src/skilz/installer.py @@ -1,6 +1,7 @@ """Core installation logic for Skilz.""" import shutil +import sys from pathlib import Path from typing import cast @@ -96,6 +97,7 @@ def install_local_skill( git_sha: str | None = None, skill_name: str | None = None, force_config: bool = False, + config_file: str | None = None, # SKILZ-50: Custom config file target ) -> None: """ Install a skill from a local directory. @@ -110,6 +112,7 @@ def install_local_skill( git_sha: Optional git SHA for manifest (overrides "local" default). skill_name: Optional skill name (overrides source_path.name, used for git installs). force_config: If True, write to config files even for native agents. + config_file: Optional config file to update (requires project_level=True). """ source_path = source_path.expanduser().resolve() @@ -123,6 +126,51 @@ def install_local_skill( skill_name = source_path.name skill_id = f"local/{skill_name}" + # Step 0: Validate skill name for native agents (Gemini, Claude, OpenCode) + # This validation is only needed when agent is specified or can be detected + resolved_agent_for_validation: AgentType | None = None + if agent is not None: + resolved_agent_for_validation = agent + else: + # Try to detect agent early for validation + try: + resolved_agent_for_validation = cast(AgentType, detect_agent()) + except Exception: + pass # Will be detected again later + + if resolved_agent_for_validation: + from skilz.agent_registry import get_registry, validate_skill_name + + registry = get_registry() + agent_config = registry.get(resolved_agent_for_validation) + + # Only validate for agents with native skill support + if agent_config and agent_config.native_skill_support != "none": + validation = validate_skill_name(skill_name) + if not validation.is_valid: + error_msg = f"Invalid skill name '{skill_name}' for {agent_config.display_name}.\n" + error_msg += "\n".join(f" - {err}" for err in validation.errors) + if validation.suggested_name: + error_msg += f"\n\nSuggested name: {validation.suggested_name}" + error_msg += "\n\nUpdate your SKILL.md frontmatter:" + error_msg += f"\n---\nname: {validation.suggested_name}\ndescription: ...\n---" + raise InstallError(str(source_path), error_msg) + + # Check if directory name matches skill name + from skilz.agent_registry import check_skill_directory_name + + matches, suggested_path = check_skill_directory_name(source_path, skill_name) + if not matches and suggested_path: + print( + f"Warning: Directory name '{source_path.name}' doesn't match " + f"skill name '{skill_name}'", + file=sys.stderr, + ) + print( + f" For better organization, consider renaming to: {suggested_path}", + file=sys.stderr, + ) + # Step 1: Determine target agent resolved_agent: AgentType if agent is None: @@ -204,11 +252,15 @@ def install_local_skill( if verbose: print("Syncing skill to config files...") + # SKILZ-50: Use custom config file if provided + target_files = (config_file,) if config_file else None + sync_results = sync_skill_to_configs( skill=skill_ref, project_dir=project_dir, agent=resolved_agent if agent else None, verbose=verbose, + target_files=target_files, ) for result in sync_results: @@ -229,6 +281,7 @@ def install_skill( mode: InstallMode | None = None, version_spec: str | None = None, force_config: bool = False, + config_file: str | None = None, # SKILZ-50: Custom config file target ) -> None: """ Install a skill from the registry. @@ -248,6 +301,7 @@ def install_skill( - 40-char hex: Specific commit SHA - Other: Treat as tag (tries "X" and "vX" formats) force_config: If True, write to config files even for native agents. + config_file: Optional config file to update (requires project_level=True). Raises: SkillNotFoundError: If the skill ID is not found in any registry. @@ -382,9 +436,15 @@ def install_skill( if found_path: source_dir = found_path + # Always warn user about path change (not just verbose mode) + print( + f"Warning: Skill '{skill_info.skill_name}' found at different path than expected", + file=sys.stderr, + ) if verbose: rel_path = source_dir.relative_to(cache_path) - print(f" Using found location: {rel_path}") + print(f" Expected: {skill_info.skill_path}", file=sys.stderr) + print(f" Found at: {rel_path}", file=sys.stderr) else: raise InstallError( skill_id, @@ -481,6 +541,9 @@ def install_skill( if verbose: print("Syncing skill to config files...") + # SKILZ-50: Use custom config file if provided + target_files = (config_file,) if config_file else None + # If agent was explicitly specified, only update that agent's config # Otherwise, update all existing config files in the project sync_results = sync_skill_to_configs( @@ -488,6 +551,7 @@ def install_skill( project_dir=project_dir, agent=resolved_agent if agent else None, verbose=verbose, + target_files=target_files, ) # Report what was updated diff --git a/tests/test_agent_registry.py b/tests/test_agent_registry.py index 9af36eb..fa293b4 100644 --- a/tests/test_agent_registry.py +++ b/tests/test_agent_registry.py @@ -168,16 +168,32 @@ def test_opencode_config(self): assert opencode.native_skill_support == "all" def test_gemini_config(self): - """Gemini agent has correct configuration (project-only).""" + """Gemini agent has correct configuration with native support (SKILZ-49).""" agents = get_builtin_agents() gemini = agents["gemini"] assert gemini.name == "gemini" assert gemini.display_name == "Gemini CLI" - assert gemini.home_dir is None - assert gemini.supports_home is False - assert gemini.default_mode == "copy" # copy for agents without native support - assert gemini.native_skill_support == "none" + assert gemini.home_dir == Path.home() / ".gemini" / "skills" + assert gemini.project_dir == Path(".gemini") / "skills" + assert gemini.supports_home is True # Now supports user-level + assert gemini.default_mode == "copy" + assert gemini.native_skill_support == "all" # Native support enabled + assert gemini.invocation == "/skills or activate_skill tool" + + def test_universal_config(self): + """Universal agent has correct configuration (SKILZ-50).""" + agents = get_builtin_agents() + universal = agents["universal"] + + assert universal.name == "universal" + assert universal.display_name == "Universal (Skilz)" + assert universal.home_dir == Path.home() / ".skilz" / "skills" + assert universal.project_dir == Path(".skilz") / "skills" + assert universal.config_files == ("AGENTS.md",) # Now has config file + assert universal.supports_home is True + assert universal.default_mode == "copy" + assert universal.native_skill_support == "none" # Not native def test_copilot_native_support(self): """Copilot should have native skill support (SKILZ-54).""" @@ -268,7 +284,7 @@ def test_registry_get_default_skills_dir(self): assert skills_dir == Path.home() / ".claude" / "skills" def test_registry_get_agents_with_home_support(self): - """get_agents_with_home_support returns agents supporting home install.""" + """get_agents_with_home_support returns agents with user-level support.""" registry = AgentRegistry() agents = registry.get_agents_with_home_support() @@ -277,7 +293,7 @@ def test_registry_get_agents_with_home_support(self): assert "opencode" in agents assert "codex" in agents assert "universal" in agents - assert "gemini" not in agents + assert "gemini" in agents # SKILZ-49: Gemini now has home support def test_registry_get_agents_by_native_support(self): """get_agents_by_native_support filters by support level.""" @@ -291,8 +307,9 @@ def test_registry_get_agents_by_native_support(self): assert "codex" in all_support assert "copilot" in all_support # SKILZ-54: Copilot has native support assert "opencode" in all_support # OpenCode has full native support + assert "gemini" in all_support # SKILZ-49: Gemini has native support assert len(home_support) == 0 # No agents currently use "home" only - assert "gemini" in none_support + assert "gemini" not in none_support # Gemini moved from none to all def test_registry_loads_user_config(self, tmp_path): """Registry merges user configuration on top of built-ins.""" diff --git a/tests/test_agents.py b/tests/test_agents.py index 5ad1889..7370b31 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -42,9 +42,92 @@ def test_detect_claude_from_user_dir(self, temp_dir, monkeypatch): assert agent == "claude" + def test_detect_gemini_from_project_dir(self, temp_dir, monkeypatch): + """Detect Gemini from .gemini in project directory (SKILZ-49).""" + # Create a fake home without claude + fake_home = temp_dir / "home" + fake_home.mkdir() + monkeypatch.setattr(Path, "home", lambda: fake_home) + + (temp_dir / ".gemini").mkdir() + + agent = detect_agent(temp_dir) + + assert agent == "gemini" + + def test_detect_gemini_from_user_dir(self, temp_dir, monkeypatch): + """Detect Gemini from ~/.gemini (SKILZ-49).""" + # Create a fake home with .gemini but no .claude + fake_home = temp_dir / "home" + (fake_home / ".gemini").mkdir(parents=True) + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Use a different temp dir as project dir (no .gemini there) + project_dir = temp_dir / "project" + project_dir.mkdir() + + agent = detect_agent(project_dir) + + assert agent == "gemini" + + def test_detect_claude_priority_over_gemini(self, temp_dir, monkeypatch): + """Claude takes priority over Gemini when both present (SKILZ-49).""" + # Create both .claude and .gemini in project + (temp_dir / ".claude").mkdir() + (temp_dir / ".gemini").mkdir() + + agent = detect_agent(temp_dir) + + # Claude should be detected first + assert agent == "claude" + + def test_detect_codex_from_project_dir(self, temp_dir, monkeypatch): + """Detect Codex from .codex in project directory (BUG-001).""" + # Create a fake home without claude or gemini + fake_home = temp_dir / "home" + fake_home.mkdir() + monkeypatch.setattr(Path, "home", lambda: fake_home) + + (temp_dir / ".codex").mkdir() + + agent = detect_agent(temp_dir) + + assert agent == "codex" + + def test_detect_codex_from_user_dir(self, temp_dir, monkeypatch): + """Detect Codex from ~/.codex (BUG-001).""" + # Create a fake home with .codex but no .claude or .gemini + fake_home = temp_dir / "home" + (fake_home / ".codex").mkdir(parents=True) + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Use a different temp dir as project dir (no .codex there) + project_dir = temp_dir / "project" + project_dir.mkdir() + + agent = detect_agent(project_dir) + + assert agent == "codex" + + def test_detect_gemini_priority_over_codex(self, temp_dir, monkeypatch): + """Gemini takes priority over Codex when both present (BUG-001).""" + # Create a fake home without claude + fake_home = temp_dir / "home" + fake_home.mkdir() + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Create both .gemini and .codex in project + (temp_dir / ".gemini").mkdir() + (temp_dir / ".codex").mkdir() + + agent = detect_agent(temp_dir) + + # Gemini should be detected first + assert agent == "gemini" + def test_detect_opencode(self, temp_dir, monkeypatch): """Detect OpenCode from ~/.config/opencode.""" - # Create a fake home with opencode but no claude + # Create a fake home with opencode but no claude or gemini fake_home = temp_dir / "home" (fake_home / ".config" / "opencode").mkdir(parents=True) monkeypatch.setattr(Path, "home", lambda: fake_home) diff --git a/tests/test_gemini_integration.py b/tests/test_gemini_integration.py new file mode 100644 index 0000000..cbaac50 --- /dev/null +++ b/tests/test_gemini_integration.py @@ -0,0 +1,287 @@ +"""Integration tests for Gemini CLI native skill support (SKILZ-49).""" + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from skilz.agents import detect_agent +from skilz.commands.list_cmd import cmd_list +from skilz.installer import install_local_skill +from skilz.manifest import read_manifest + + +class TestGeminiNativeInstall: + """Test Gemini native skill installation scenarios.""" + + def test_install_skill_to_gemini_native_project(self, tmp_path, monkeypatch): + """Install skill to Gemini project-level (.gemini/skills/) without config sync.""" + # Setup: Create a Gemini project structure + project_dir = tmp_path / "project" + project_dir.mkdir() + target_root = project_dir / ".gemini" / "skills" + target_root.mkdir(parents=True) + + # Create a test skill source + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test Skill\nA test skill.") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install skill with mocked detection and ensure_skills_dir + with ( + patch("skilz.installer.detect_agent", return_value="gemini"), + patch("skilz.installer.ensure_skills_dir", return_value=target_root), + patch("skilz.installer.sync_skill_to_configs") as mock_sync, + ): + install_local_skill( + source_path=skill_source, + agent=None, # Let detect_agent work + project_level=True, + verbose=False, + force_config=False, + ) + + # Verify: Config sync was NOT called (native support) + mock_sync.assert_not_called() + + # Verify: Skill installed to .gemini/skills/ + installed_skill = target_root / "test-skill" + assert installed_skill.exists() + assert (installed_skill / "SKILL.md").exists() + + # Verify: Manifest created + manifest = read_manifest(installed_skill) + assert manifest is not None + assert manifest.git_repo == "local" # Local installs use "local" as git_repo + + def test_install_skill_to_gemini_native_user(self, tmp_path, monkeypatch): + """Install skill to Gemini user-level (~/.gemini/skills/) without config sync.""" + # Setup: Create a fake home directory + fake_home = tmp_path / "home" + target_root = fake_home / ".gemini" / "skills" + target_root.mkdir(parents=True) + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Create a test skill source + skill_source = tmp_path / "user-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test Skill\nA user-level test skill.") + + # Install skill to user-level + with ( + patch("skilz.installer.detect_agent", return_value="gemini"), + patch("skilz.installer.ensure_skills_dir", return_value=target_root), + ): + install_local_skill( + source_path=skill_source, + agent=None, + project_level=False, + verbose=False, + force_config=False, + ) + + # Verify: Skill installed to ~/.gemini/skills/ + installed_skill = target_root / "user-skill" + assert installed_skill.exists() + assert (installed_skill / "SKILL.md").exists() + + # Verify: Manifest created + manifest = read_manifest(installed_skill) + assert manifest is not None + assert manifest.git_repo == "local" + + def test_gemini_backward_compat_force_config(self, tmp_path, monkeypatch): + """Test --force-config flag creates GEMINI.md for backward compatibility.""" + # Setup: Create a Gemini project structure + project_dir = tmp_path / "project" + project_dir.mkdir() + target_root = project_dir / ".gemini" / "skills" + target_root.mkdir(parents=True) + + # Create a test skill source + skill_source = tmp_path / "legacy-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Legacy Skill\nBackward compat test.") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install skill with force_config=True + with ( + patch("skilz.installer.detect_agent", return_value="gemini"), + patch("skilz.installer.ensure_skills_dir", return_value=target_root), + patch("skilz.installer.sync_skill_to_configs") as mock_sync, + ): + install_local_skill( + source_path=skill_source, + agent=None, + project_level=True, + verbose=False, + force_config=True, # Force config sync + ) + + # Verify: Config sync WAS called (backward compat mode) + mock_sync.assert_called_once() + + # Verify: Skill installed + installed_skill = target_root / "legacy-skill" + assert installed_skill.exists() + + +class TestGeminiAutoDetection: + """Test Gemini auto-detection scenarios.""" + + def test_gemini_auto_detection_project(self, tmp_path, monkeypatch): + """Auto-detect Gemini from .gemini/ in project directory.""" + # Setup: Create a Gemini project + project_dir = tmp_path / "project" + project_dir.mkdir() + (project_dir / ".gemini").mkdir() + + # Create fake home without Claude + fake_home = tmp_path / "home" + fake_home.mkdir() + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Detect agent + agent = detect_agent(project_dir) + + assert agent == "gemini" + + def test_gemini_auto_detection_user(self, tmp_path, monkeypatch): + """Auto-detect Gemini from ~/.gemini/ in user home.""" + # Setup: Create fake home with Gemini but no Claude + fake_home = tmp_path / "home" + (fake_home / ".gemini").mkdir(parents=True) + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Empty project directory + project_dir = tmp_path / "project" + project_dir.mkdir() + + # Detect agent + agent = detect_agent(project_dir) + + assert agent == "gemini" + + def test_claude_priority_over_gemini(self, tmp_path, monkeypatch): + """Ensure Claude takes priority over Gemini when both present.""" + # Setup: Create project with both Claude and Gemini + project_dir = tmp_path / "project" + project_dir.mkdir() + (project_dir / ".claude").mkdir() + (project_dir / ".gemini").mkdir() + + # Detect agent - Claude should win + agent = detect_agent(project_dir) + + assert agent == "claude" + + +class TestGeminiSkillNameValidation: + """Test skill name validation for Gemini native skills.""" + + def test_gemini_invalid_skill_name_warning(self, tmp_path, monkeypatch, capsys): + """Installing skill with invalid name to Gemini raises an error with helpful suggestion.""" + # Setup: Create a Gemini project + project_dir = tmp_path / "project" + project_dir.mkdir() + target_root = project_dir / ".gemini" / "skills" + target_root.mkdir(parents=True) + + # Create a skill with invalid name (uppercase + underscore) + skill_source = tmp_path / "Invalid_Skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Invalid Skill") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Import needed for exception catching + from skilz.errors import InstallError + + # Install should raise InstallError about invalid name + with ( + patch("skilz.installer.detect_agent", return_value="gemini"), + patch("skilz.installer.ensure_skills_dir", return_value=target_root), + pytest.raises(InstallError, match="invalid-skill"), + ): + install_local_skill( + source_path=skill_source, + agent=None, + project_level=True, + verbose=False, + force_config=False, + ) + + def test_gemini_valid_skill_name_success(self, tmp_path, monkeypatch): + """Installing skill with valid name to Gemini succeeds without warnings.""" + # Setup: Create a Gemini project + project_dir = tmp_path / "project" + project_dir.mkdir() + target_root = project_dir / ".gemini" / "skills" + target_root.mkdir(parents=True) + + # Create a skill with valid name + skill_source = tmp_path / "valid-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Valid Skill") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install should succeed + with ( + patch("skilz.installer.detect_agent", return_value="gemini"), + patch("skilz.installer.ensure_skills_dir", return_value=target_root), + ): + install_local_skill( + source_path=skill_source, + agent=None, + project_level=True, + verbose=False, + force_config=False, + ) + + # Verify installation + installed_skill = target_root / "valid-skill" + assert installed_skill.exists() + + +class TestGeminiConfigSync: + """Test config sync behavior for Gemini.""" + + def test_gemini_skips_config_sync_by_default(self, tmp_path, monkeypatch): + """Gemini skips config sync by default (native support).""" + # Setup: Create Gemini project + project_dir = tmp_path / "project" + project_dir.mkdir() + target_root = project_dir / ".gemini" / "skills" + target_root.mkdir(parents=True) + + # Create a test skill + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test") + + monkeypatch.chdir(project_dir) + + # Install skill + with ( + patch("skilz.installer.detect_agent", return_value="gemini"), + patch("skilz.installer.ensure_skills_dir", return_value=target_root), + patch("skilz.installer.sync_skill_to_configs") as mock_sync, + ): + install_local_skill( + source_path=skill_source, + agent=None, + project_level=True, + verbose=False, + force_config=False, + ) + + # Verify: sync was not called + mock_sync.assert_not_called() diff --git a/tests/test_install_cmd.py b/tests/test_install_cmd.py index 9fa219f..555f184 100644 --- a/tests/test_install_cmd.py +++ b/tests/test_install_cmd.py @@ -22,6 +22,8 @@ def test_install_success(self): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_skill") as mock_install: @@ -36,6 +38,7 @@ def test_install_success(self): mode=None, version_spec=None, force_config=False, + config_file=None, ) def test_install_with_agent(self): @@ -50,6 +53,8 @@ def test_install_with_agent(self): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_skill") as mock_install: @@ -64,6 +69,7 @@ def test_install_with_agent(self): mode=None, version_spec=None, force_config=False, + config_file=None, ) def test_install_with_claude_agent(self): @@ -78,6 +84,8 @@ def test_install_with_claude_agent(self): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_skill") as mock_install: @@ -92,6 +100,7 @@ def test_install_with_claude_agent(self): mode=None, version_spec=None, force_config=False, + config_file=None, ) def test_install_skill_not_found_error(self, capsys): @@ -206,6 +215,7 @@ def test_install_missing_verbose_attribute(self): mode=None, version_spec=None, force_config=False, + config_file=None, ) def test_install_with_copy_flag(self): @@ -220,6 +230,8 @@ def test_install_with_copy_flag(self): copy=True, symlink=False, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_skill") as mock_install: @@ -234,6 +246,7 @@ def test_install_with_copy_flag(self): mode="copy", version_spec=None, force_config=False, + config_file=None, ) def test_install_with_symlink_flag(self): @@ -248,6 +261,8 @@ def test_install_with_symlink_flag(self): copy=False, symlink=True, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_skill") as mock_install: @@ -262,6 +277,7 @@ def test_install_with_symlink_flag(self): mode="symlink", version_spec=None, force_config=False, + config_file=None, ) def test_install_no_source_error(self, capsys): @@ -315,6 +331,8 @@ def test_install_from_file_success(self): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_local_skill") as mock_install: @@ -405,6 +423,8 @@ def test_https_url_routes_to_git_install(self, capsys): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, install_all=False, yes_all=False, skill=None, @@ -431,6 +451,8 @@ def test_ssh_url_routes_to_git_install(self, capsys): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, install_all=False, yes_all=False, skill=None, @@ -457,6 +479,8 @@ def test_explicit_git_flag_takes_precedence(self): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, install_all=False, yes_all=False, skill=None, @@ -483,6 +507,8 @@ def test_registry_id_still_works(self): copy=False, symlink=False, version_spec=None, + force_config=False, + config=None, ) with patch("skilz.installer.install_skill") as mock_install: @@ -492,3 +518,48 @@ def test_registry_id_still_works(self): mock_install.assert_called_once() call_args = mock_install.call_args[1] assert call_args["skill_id"] == "anthropics/excel" + + def test_config_flag_requires_project(self, capsys): + """--config flag requires --project flag (SKILZ-50).""" + args = argparse.Namespace( + skill_id="test/skill", + agent="universal", + project=False, # Missing --project + config="GEMINI.md", # But has --config + verbose=False, + file=None, + git=None, + copy=False, + symlink=False, + version_spec=None, + force_config=False, + ) + + result = cmd_install(args) + + assert result == 1 # Should fail + captured = capsys.readouterr() + assert "--config requires --project" in captured.err + + def test_config_flag_with_project(self): + """--config flag works when --project is provided (SKILZ-50).""" + args = argparse.Namespace( + skill_id="test/skill", + agent="universal", + project=True, # Has --project + config="GEMINI.md", # And --config + verbose=False, + file=None, + git=None, + copy=False, + symlink=False, + version_spec=None, + force_config=False, + ) + + with patch("skilz.installer.install_skill") as mock_install: + result = cmd_install(args) + + # Should succeed (validation passes) + assert result == 0 + mock_install.assert_called_once() diff --git a/tests/test_installer.py b/tests/test_installer.py index 763705a..3b837a0 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -659,14 +659,14 @@ def test_force_config_overrides_native_support(self, mock_dependencies, capsys): mock_sync.assert_called_once() def test_config_sync_for_gemini(self, mock_dependencies, capsys): - """Config sync should happen for Gemini (native_skill_support='none').""" + """Config sync should be skipped for Gemini (native_skill_support='all', SKILZ-49).""" deps = mock_dependencies with ( patch("skilz.installer.detect_agent", return_value="gemini"), patch("skilz.installer.get_agent_display_name", return_value="Gemini CLI"), patch("skilz.installer.get_agent_default_mode", return_value="copy"), - patch("skilz.installer.supports_home_install", return_value=False), + patch("skilz.installer.supports_home_install", return_value=True), patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]), patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]), patch("skilz.installer.needs_install", return_value=(True, "not_installed")), @@ -681,8 +681,8 @@ def test_config_sync_for_gemini(self, mock_dependencies, capsys): install_skill("test/skill", agent="gemini", project_level=True) - # Sync SHOULD be called for Gemini (no native support) - mock_sync.assert_called_once() + # Sync should NOT be called for Gemini (now has native support) + mock_sync.assert_not_called() def test_skip_config_sync_for_copilot(self, mock_dependencies, capsys): """Config sync should be skipped for Copilot (native_skill_support='all', SKILZ-54).""" @@ -755,3 +755,111 @@ def test_local_skill_force_config_overrides(self, temp_dir): # Sync SHOULD be called with --force-config mock_sync.assert_called_once() + + +class TestInstallSkillPathFallback: + """Tests for skill path fallback with warning (Feature 11).""" + + @pytest.fixture + def mock_dependencies(self, temp_dir): + """Common test fixtures for path fallback tests.""" + cache_path = temp_dir / "cache" + cache_path.mkdir() + + source_path = cache_path / "skills" / "test-skill" + source_path.mkdir(parents=True) + (source_path / "SKILL.md").write_text("# Test Skill") + + skills_dir = temp_dir / ".claude" / "skills" + skills_dir.mkdir(parents=True) + + skill_info = SkillInfo( + skill_id="test/skill", + git_repo="https://github.com/test/repo.git", + skill_path="/main/old-location/SKILL.md", + git_sha="abc123def456", + ) + + return { + "cache_path": cache_path, + "source_path": source_path, + "skills_dir": skills_dir, + "skill_info": skill_info, + } + + def test_install_skill_warns_on_path_change(self, mock_dependencies, capsys): + """Warning is displayed when skill found at different path.""" + deps = mock_dependencies + nonexistent = deps["cache_path"] / "nonexistent" + found_path = deps["cache_path"] / "new-location" / "my-skill" + found_path.mkdir(parents=True) + (found_path / "SKILL.md").write_text("# Test") + + with ( + patch("skilz.installer.detect_agent", return_value="claude"), + patch("skilz.installer.get_agent_display_name", return_value="Claude Code"), + patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]), + patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]), + patch("skilz.installer.needs_install", return_value=(True, "not_installed")), + patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]), + patch("skilz.installer.parse_skill_path", return_value=("main", "old-location")), + patch("skilz.installer.checkout_sha"), + patch("skilz.installer.get_skill_source_path", return_value=nonexistent), + patch("skilz.installer.find_skill_by_name", return_value=found_path), + patch("skilz.installer.write_manifest"), + ): + install_skill("test/skill", project_level=True) + + captured = capsys.readouterr() + # skill_name is derived from skill_path: /main/old-location/SKILL.md -> "old-location" + assert "Warning: Skill 'old-location' found at different path than expected" in captured.err + + def test_install_skill_no_warning_when_path_matches(self, mock_dependencies, capsys): + """No warning when skill found at expected path.""" + deps = mock_dependencies + + with ( + patch("skilz.installer.detect_agent", return_value="claude"), + patch("skilz.installer.get_agent_display_name", return_value="Claude Code"), + patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]), + patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]), + patch("skilz.installer.needs_install", return_value=(True, "not_installed")), + patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]), + patch("skilz.installer.parse_skill_path", return_value=("main", "skills/test-skill")), + patch("skilz.installer.checkout_sha"), + patch("skilz.installer.get_skill_source_path", return_value=deps["source_path"]), + patch("skilz.installer.write_manifest"), + ): + install_skill("test/skill", project_level=True) + + captured = capsys.readouterr() + assert "Warning:" not in captured.err + assert "different path" not in captured.err + + def test_install_skill_warning_goes_to_stderr(self, mock_dependencies, capsys): + """Warning message goes to stderr, not stdout.""" + deps = mock_dependencies + nonexistent = deps["cache_path"] / "nonexistent" + found_path = deps["cache_path"] / "new-location" / "my-skill" + found_path.mkdir(parents=True) + (found_path / "SKILL.md").write_text("# Test") + + with ( + patch("skilz.installer.detect_agent", return_value="claude"), + patch("skilz.installer.get_agent_display_name", return_value="Claude Code"), + patch("skilz.installer.lookup_skill", return_value=deps["skill_info"]), + patch("skilz.installer.ensure_skills_dir", return_value=deps["skills_dir"]), + patch("skilz.installer.needs_install", return_value=(True, "not_installed")), + patch("skilz.installer.clone_or_fetch", return_value=deps["cache_path"]), + patch("skilz.installer.parse_skill_path", return_value=("main", "old-location")), + patch("skilz.installer.checkout_sha"), + patch("skilz.installer.get_skill_source_path", return_value=nonexistent), + patch("skilz.installer.find_skill_by_name", return_value=found_path), + patch("skilz.installer.write_manifest"), + ): + install_skill("test/skill", project_level=True) + + captured = capsys.readouterr() + # Warning should be in stderr, not stdout + assert "Warning:" in captured.err + assert "Warning:" not in captured.out diff --git a/tests/test_universal_integration.py b/tests/test_universal_integration.py new file mode 100644 index 0000000..59e1f5d --- /dev/null +++ b/tests/test_universal_integration.py @@ -0,0 +1,388 @@ +"""Integration tests for universal agent project-level support with custom config. + +Tests SKILZ-50: Universal agent project-level installations with --config flag. +""" + +import argparse +from pathlib import Path +from unittest.mock import patch + +import pytest + +from skilz.commands.install_cmd import cmd_install +from skilz.installer import install_local_skill +from skilz.manifest import read_manifest + + +class TestUniversalProjectInstall: + """Test universal agent project-level installation scenarios.""" + + def test_universal_project_install_default_config(self, tmp_path, monkeypatch): + """Universal project install updates AGENTS.md by default.""" + # Setup: Create a project directory + project_dir = tmp_path / "project" + project_dir.mkdir() + + # Create universal skills directory + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + # Create a test skill source + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test Skill\n\nTest skill for universal agent.") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install skill with universal agent, project-level, no custom config + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file=None, # Default behavior + ) + + # Verify: Skill installed to .skilz/skills/ + installed_skill = skills_dir / "test-skill" + assert installed_skill.exists(), "Skill should be installed" + assert (installed_skill / "SKILL.md").exists(), "SKILL.md should exist" + + # Verify: AGENTS.md was created by default + agents_md = project_dir / "AGENTS.md" + assert agents_md.exists(), "AGENTS.md should be created by default" + + content = agents_md.read_text() + assert "test-skill" in content, "AGENTS.md should contain skill reference" + + def test_universal_project_install_custom_config(self, tmp_path, monkeypatch): + """Universal project install with --config updates specified file.""" + # Setup: Create a project directory + project_dir = tmp_path / "project" + project_dir.mkdir() + + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + # Create a test skill source + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test Skill\n\nTest skill with custom config.") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install with custom config file + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="GEMINI.md", # Custom config + ) + + # Verify: Skill installed to .skilz/skills/ + installed_skill = skills_dir / "test-skill" + assert installed_skill.exists(), "Skill should be installed" + + # Verify: GEMINI.md was created (not AGENTS.md) + gemini_md = project_dir / "GEMINI.md" + assert gemini_md.exists(), "GEMINI.md should be created" + + content = gemini_md.read_text() + assert "test-skill" in content, "GEMINI.md should contain skill reference" + + # Verify: AGENTS.md was NOT created + agents_md = project_dir / "AGENTS.md" + assert not agents_md.exists(), "AGENTS.md should NOT be created with --config" + + def test_config_flag_without_project_fails(self, tmp_path): + """--config flag requires --project flag.""" + args = argparse.Namespace( + skill_id="test/skill", + agent="universal", + project=False, # Not project-level + verbose=False, + file=None, + git=None, + copy=False, + symlink=False, + version_spec=None, + force_config=False, + config="GEMINI.md", # Has --config + ) + + # Should fail and return 1 + result = cmd_install(args) + assert result == 1, "Install should fail without --project flag" + + def test_universal_custom_config_multiple_skills(self, tmp_path, monkeypatch): + """Multiple skills can be installed with custom config.""" + # Setup: Create a project directory + project_dir = tmp_path / "project" + project_dir.mkdir() + + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + # Create two test skills + skill1 = tmp_path / "skill1" + skill1.mkdir() + (skill1 / "SKILL.md").write_text("# Skill 1") + + skill2 = tmp_path / "skill2" + skill2.mkdir() + (skill2 / "SKILL.md").write_text("# Skill 2") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install first skill + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill1, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="CUSTOM.md", + ) + + # Install second skill + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill2, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="CUSTOM.md", + ) + + # Verify: Both skills in CUSTOM.md + custom_md = project_dir / "CUSTOM.md" + assert custom_md.exists(), "CUSTOM.md should exist" + + content = custom_md.read_text() + assert "skill1" in content, "CUSTOM.md should contain skill1" + assert "skill2" in content, "CUSTOM.md should contain skill2" + + def test_legacy_gemini_workflow(self, tmp_path, monkeypatch): + """Legacy Gemini workflow (no experimental.skills plugin).""" + # Setup: Project directory for legacy Gemini user + project_dir = tmp_path / "project" + project_dir.mkdir() + + # Universal skills directory (not .gemini/skills/) + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + # Create a test skill + skill_source = tmp_path / "pdf-reader" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# PDF Reader\n\nRead PDFs.") + + # Change to project directory + monkeypatch.chdir(project_dir) + + # Install with universal agent + GEMINI.md (legacy workflow) + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="GEMINI.md", + ) + + # Verify: Installs to .skilz/skills/ (NOT .gemini/skills/) + universal_dir = skills_dir / "pdf-reader" + gemini_native_dir = project_dir / ".gemini" / "skills" / "pdf-reader" + + assert universal_dir.exists(), "Should install to .skilz/skills/" + assert not gemini_native_dir.exists(), "Should NOT install to .gemini/skills/" + + # Verify: GEMINI.md is updated for Gemini CLI to read + gemini_md = project_dir / "GEMINI.md" + assert gemini_md.exists(), "GEMINI.md should be created" + + content = gemini_md.read_text() + assert "pdf-reader" in content, "GEMINI.md should reference skill" + + def test_custom_config_with_arbitrary_filename(self, tmp_path, monkeypatch): + """Custom config file with arbitrary filename works.""" + # Setup + project_dir = tmp_path / "project" + project_dir.mkdir() + + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test") + + monkeypatch.chdir(project_dir) + + # Install with completely custom filename + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="MY_CUSTOM_SKILLS.md", + ) + + # Verify custom filename was created + custom_file = project_dir / "MY_CUSTOM_SKILLS.md" + assert custom_file.exists(), "Custom filename should be created" + + content = custom_file.read_text() + assert "test-skill" in content, "Custom file should contain skill reference" + + def test_universal_config_file_updates_only_target(self, tmp_path, monkeypatch): + """When --config is used, only the specified file is updated.""" + # Setup + project_dir = tmp_path / "project" + project_dir.mkdir() + + # Pre-create AGENTS.md + agents_md = project_dir / "AGENTS.md" + agents_md.write_text("# Existing AGENTS.md\n\nSome content") + original_content = agents_md.read_text() + + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test") + + monkeypatch.chdir(project_dir) + + # Install with --config GEMINI.md + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="GEMINI.md", + ) + + # Verify: GEMINI.md was updated + gemini_md = project_dir / "GEMINI.md" + assert gemini_md.exists(), "GEMINI.md should be created" + assert "test-skill" in gemini_md.read_text() + + # Verify: AGENTS.md was NOT modified + assert agents_md.exists(), "AGENTS.md should still exist" + assert agents_md.read_text() == original_content, ( + "AGENTS.md should NOT be modified when using --config" + ) + + def test_universal_project_without_config_creates_agents_md(self, tmp_path, monkeypatch): + """Universal project install without --config creates AGENTS.md.""" + # Setup + project_dir = tmp_path / "project" + project_dir.mkdir() + + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test") + + monkeypatch.chdir(project_dir) + + # Install without --config flag + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file=None, # No custom config + ) + + # Verify: AGENTS.md was created (universal default) + agents_md = project_dir / "AGENTS.md" + assert agents_md.exists(), "AGENTS.md should be created by default" + + content = agents_md.read_text() + assert "test-skill" in content, "AGENTS.md should contain skill" + + # Verify: No other config files created + assert not (project_dir / "GEMINI.md").exists() + assert not (project_dir / "CLAUDE.md").exists() + + def test_universal_manifest_created(self, tmp_path, monkeypatch): + """Universal agent skills have proper manifest after install.""" + # Setup + project_dir = tmp_path / "project" + project_dir.mkdir() + + skills_dir = project_dir / ".skilz" / "skills" + skills_dir.mkdir(parents=True) + + skill_source = tmp_path / "test-skill" + skill_source.mkdir() + (skill_source / "SKILL.md").write_text("# Test Skill") + + monkeypatch.chdir(project_dir) + + # Install with custom config + with ( + patch("skilz.installer.detect_agent", return_value="universal"), + patch("skilz.installer.ensure_skills_dir", return_value=skills_dir), + ): + install_local_skill( + source_path=skill_source, + agent="universal", + project_level=True, + verbose=False, + force_config=False, + config_file="GEMINI.md", + ) + + # Verify: Manifest exists + installed_skill = skills_dir / "test-skill" + manifest = read_manifest(installed_skill) + + assert manifest is not None, "Manifest should exist" + assert manifest.git_repo == "local", "Local installs use 'local' as git_repo" From 30e3f1e3e39109e497a3dfdb474ec497242d1674 Mon Sep 17 00:00:00 2001 From: Rick Hightower Date: Thu, 8 Jan 2026 18:30:34 -0600 Subject: [PATCH 2/2] fix: Remove unused imports in test files - Remove unused cmd_list import from test_gemini_integration.py - Remove unused Path and pytest imports from test_universal_integration.py --- tests/test_gemini_integration.py | 1 - tests/test_universal_integration.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/tests/test_gemini_integration.py b/tests/test_gemini_integration.py index cbaac50..767d466 100644 --- a/tests/test_gemini_integration.py +++ b/tests/test_gemini_integration.py @@ -6,7 +6,6 @@ import pytest from skilz.agents import detect_agent -from skilz.commands.list_cmd import cmd_list from skilz.installer import install_local_skill from skilz.manifest import read_manifest diff --git a/tests/test_universal_integration.py b/tests/test_universal_integration.py index 59e1f5d..8010cfa 100644 --- a/tests/test_universal_integration.py +++ b/tests/test_universal_integration.py @@ -4,11 +4,8 @@ """ import argparse -from pathlib import Path from unittest.mock import patch -import pytest - from skilz.commands.install_cmd import cmd_install from skilz.installer import install_local_skill from skilz.manifest import read_manifest