# REQ-011: Skills Install and Uninstall

## Metadata
- **Status:** draft
- **Priority:** 3
- **Dependencies:** REQ-010 (ievo uninstall command)
- **Created:** 2026-03-03
- **Implemented:**

## Summary

`ievo init` must copy all bundled skills from `src/ievo/marketplace/skills/<name>/SKILL.md` into `.claude/skills/<name>/SKILL.md`, and `ievo uninstall` must remove the `.claude/skills/` directory tree it installed. This makes Claude Code slash commands `/backlog` and `/evo` immediately available after `ievo init`.

## Context

### Existing system state

**Bundled skills** (shipped inside the pip package):

- `src/ievo/marketplace/skills/backlog/SKILL.md` — implements the `/backlog` slash command
- `src/ievo/marketplace/skills/evo/SKILL.md` — implements the `/evo` slash command

`evo/SKILL.md` has YAML frontmatter (`---\nname: evo\n---`); `backlog/SKILL.md` does not (plain markdown). Both are valid as-is — they are copied verbatim without modification.

**Marketplace access layer** (`src/ievo/marketplace/__init__.py`):

- `bundled_skill_path(skill_name: str) -> Path | None` — returns `Path` to `skills/<name>/SKILL.md` or `None` if missing
- `list_bundled_agents()` and `list_bundled_templates()` exist; **`list_bundled_skills()` does not exist yet** — it must be added

**`ievo init`** (`src/ievo/commands/init.py`):

- Entry point: `def init(name: str) -> None`
- Already creates `.claude/agents/` directory (listed in `dirs` at line 146)
- Deploys agents, templates, hooks via `_write_if_missing(path, content)` (write-if-missing, idempotent)
- Skills section is entirely absent — no `.claude/skills/` directory is created, no SKILL.md files are deployed

**`ievo uninstall`** (`src/ievo/commands/uninstall.py`):

- Entry point: `def uninstall(force: bool) -> None`
- `_INTEGRATION_FILES` list (`lines 18-25`) — static list of files to delete
- Removes `.claude/agents/*.md` via glob (dynamic discovery, not static list)
- `.claude/skills/` is not mentioned anywhere — not scanned, not removed
- `.ievo/` directory is explicitly preserved (project data)

**Claude Code slash command convention**: Claude Code discovers slash commands by reading `.claude/skills/<name>/SKILL.md`. The directory name determines the command name (e.g., directory `backlog` → command `/backlog`).

**`_write_if_missing`** (`src/ievo/commands/init.py:85`): writes file only if it does not exist, returns `bool`. Used for idempotent init.

**`_EMPTY_DIRS`** (`src/ievo/commands/init.py:76`): set of directories that get `.gitkeep` — skills dir should NOT be in this set (it will contain real files).

## Acceptance Criteria

### AC-1: `list_bundled_skills()` function

- [ ] `list_bundled_skills()` added to `src/ievo/marketplace/__init__.py`
- [ ] Returns `["backlog", "evo"]` (sorted list of skill directory names under `marketplace/skills/`)
- [ ] Returns `[]` if the `skills/` directory does not exist (defensive — matches pattern of `list_bundled_agents()`)
- [ ] Only returns directories that contain a `SKILL.md` file (not arbitrary subdirectories)

### AC-2: `ievo init` creates `.claude/skills/` directory

- [ ] `ievo init <name>` creates `.claude/skills/` directory under the project root
- [ ] Directory is created even if no skills exist (defensive mkdir)
- [ ] Directory creation is logged via `step(".claude/skills/")` (matches existing init output style)

### AC-3: `ievo init` copies each bundled skill

- [ ] `ievo init <name>` copies `marketplace/skills/backlog/SKILL.md` → `.claude/skills/backlog/SKILL.md`
- [ ] `ievo init <name>` copies `marketplace/skills/evo/SKILL.md` → `.claude/skills/evo/SKILL.md`
- [ ] Subdirectory `.claude/skills/<name>/` is created before writing the file
- [ ] File content is identical to bundled source (verbatim copy, no modification)
- [ ] Each deployment logged via `step(".claude/skills/<name>/SKILL.md")`

### AC-4: `ievo init` is idempotent for skills

- [ ] Re-running `ievo init` when `.claude/skills/backlog/SKILL.md` already exists does NOT overwrite the file (uses `_write_if_missing`)
- [ ] Re-running `ievo init` when `.claude/skills/evo/SKILL.md` already exists does NOT overwrite the file
- [ ] No step log line is printed for skills that were already present (consistent with `_write_if_missing` returning `False`)

### AC-5: `ievo uninstall` removes `.claude/skills/` directory tree

- [ ] `ievo uninstall --force` removes `.claude/skills/backlog/SKILL.md` when it exists
- [ ] `ievo uninstall --force` removes `.claude/skills/evo/SKILL.md` when it exists
- [ ] `ievo uninstall --force` removes the `.claude/skills/` directory itself when it is empty after removal (rmtree approach, not per-file glob)
- [ ] Skills removal is included in the pre-confirmation summary listing (each `SKILL.md` file listed or the directory listed)
- [ ] `ievo uninstall --force` succeeds silently when `.claude/skills/` does not exist (no error, no crash)

### AC-6: `ievo uninstall` confirmation prompt lists skills

- [ ] When `.claude/skills/` has installed skills, the summary shown before `typer.confirm()` includes each skill's path (e.g., `.claude/skills/backlog/SKILL.md`)
- [ ] When user declines (`typer.confirm` returns `False`), NO skill files are removed

### AC-7: Integration — init then uninstall roundtrip

- [ ] `ievo init` followed by `ievo uninstall --force` results in no `.claude/skills/` directory or its contents remaining
- [ ] `.ievo/` directory is preserved after uninstall (skills removal does not affect project data)
- [ ] After uninstall + manifest removal + re-init: `.claude/skills/backlog/SKILL.md` and `.claude/skills/evo/SKILL.md` are present again

### AC-8: Skills section in init output

- [ ] `ievo init` output includes a `[bold]Skills[/bold]` section header (via `info()`) before listing deployed skills — consistent with how "Claude Code hooks" section is presented
- [ ] If all skills already exist, the section header is still shown but no `step()` lines appear for individual skills (consistent with template deployment pattern)

## Negative Acceptance Criteria

- This does NOT add a `ievo skills` CLI subcommand — skills are managed only through `init`/`uninstall`
- This does NOT download skills from a remote registry — only bundled skills are deployed
- This does NOT validate or parse SKILL.md content — files are copied verbatim
- This does NOT remove `.claude/skills/` from `.gitignore` — skills should be committed (same as agents)
- This does NOT add `.gitkeep` to `.claude/skills/` — the directory will contain real files after init
- This does NOT remove other user-created subdirectories under `.claude/skills/` — only directories that match bundled skill names are removed
- This does NOT change the `_INTEGRATION_FILES` static list in `uninstall.py` — skills use dynamic discovery (same pattern as agent `.md` files)

## Technical Notes

### `list_bundled_skills()` implementation

Pattern mirrors `list_bundled_agents()` and `list_bundled_templates()` exactly:

```python
def list_bundled_skills() -> list[str]:
    """Return sorted list of bundled skill names (subdirectory names under skills/)."""
    skills_dir = _PKG_DIR / "skills"
    if not skills_dir.is_dir():
        return []
    return sorted(
        p.name for p in skills_dir.iterdir()
        if p.is_dir() and (p / "SKILL.md").exists()
    )
```

### `ievo init` — skills deployment

Insert after the "Claude Code hooks" section, before the final `success()` call. Use `list_bundled_skills()` to discover all bundled skills dynamically (not hardcoded names).

```python
# ── Skills ────────────────────────────────────────────────────
info("[bold]Skills[/bold]")

skills_base = root / ".claude" / "skills"
skills_base.mkdir(parents=True, exist_ok=True)

for skill_name in list_bundled_skills():
    skill_src = bundled_skill_path(skill_name)
    if skill_src:
        skill_dest = skills_base / skill_name / "SKILL.md"
        skill_dest.parent.mkdir(parents=True, exist_ok=True)
        if _write_if_missing(skill_dest, skill_src.read_text()):
            step(f".claude/skills/{skill_name}/SKILL.md")
```

Import `list_bundled_skills` alongside `bundled_template_path` in `init.py`.

Also add `.claude/skills` to the `dirs` list so the directory is created as part of the standard directory pass (even before the skills loop).

### `ievo uninstall` — skills removal

Dynamic discovery (mirrors agent `.md` file handling). Skills directory (`root / ".claude" / "skills"`) is scanned for subdirectories containing `SKILL.md`. Use `shutil.rmtree` on the whole skills directory OR remove per skill subdir and let parent remain. The safer approach that matches the requirement ("removes `.claude/skills/`") is `shutil.rmtree(skills_dir)` when the directory exists.

```python
# Collect installed skills
skills_dir = root / ".claude" / "skills"
skill_subdirs: list[Path] = []
if skills_dir.is_dir():
    skill_subdirs = sorted(
        p for p in skills_dir.iterdir()
        if p.is_dir() and (p / "SKILL.md").exists()
    )
    for sd in skill_subdirs:
        labels.append(f".claude/skills/{sd.name}/SKILL.md")

# ... in execution section:
for sd in skill_subdirs:
    if sd.is_dir():
        import shutil
        shutil.rmtree(sd)

# Remove .claude/skills/ dir if now empty
if skills_dir.is_dir() and not any(skills_dir.iterdir()):
    skills_dir.rmdir()
```

### Preserve user-created skills

Only bundled skill subdirectories are cleaned up — user-created subdirectories in `.claude/skills/` that don't match bundled names are left in place. The parent `.claude/skills/` directory is only removed if it becomes empty.

### `dirs` list update in `init.py`

Add `".claude/skills"` to the `dirs` list in `init()`. This ensures the directory is created in the standard mkdir pass and logged via `step()`. Skills files are still deployed separately in the skills section.

## Examples

### Example 1: Fresh init — skills deployed

```
$ ievo init my-project
...
Skills
  .claude/skills/backlog/SKILL.md
  .claude/skills/evo/SKILL.md
...
Project my-project initialized.
```

**File created:** `my-project/.claude/skills/backlog/SKILL.md` — identical content to `src/ievo/marketplace/skills/backlog/SKILL.md`

**File created:** `my-project/.claude/skills/evo/SKILL.md` — identical content to `src/ievo/marketplace/skills/evo/SKILL.md`

### Example 2: Re-init — existing skills not overwritten

```
$ ievo init .
...
Skills
  (no step lines — files already exist)
...
```

**Files unchanged:** `.claude/skills/backlog/SKILL.md` and `.claude/skills/evo/SKILL.md` retain their existing content.

### Example 3: Uninstall — skills removed

```
$ ievo uninstall --force
Files to remove:
  ...
  .claude/skills/backlog/SKILL.md
  .claude/skills/evo/SKILL.md
iEvo integration removed. Run ievo init to reinstall.
```

**Result:** `.claude/skills/` directory and all subdirectories are removed.

### Example 4: Uninstall — no skills directory

```
$ ievo uninstall --force
(no skills lines in summary — skills dir absent)
iEvo integration removed. Run ievo init to reinstall.
```

No error — `.claude/skills/` was already absent.

## Test Specification

Every acceptance criterion maps to at least one test. Tests follow the existing patterns in `tests/test_init.py` and `tests/test_uninstall.py`.

### Tests to add in `tests/test_marketplace.py` (or `tests/test_marketplace_skills.py`)

| Test | What it verifies |
|------|-----------------|
| `test_list_bundled_skills_returns_known_skills` | Returns `["backlog", "evo"]` from real bundled package |
| `test_list_bundled_skills_sorted` | Return value is sorted |
| `test_list_bundled_skills_missing_dir` | Returns `[]` when skills dir patched to non-existent path |
| `test_list_bundled_skills_skips_dirs_without_skill_md` | Dir without `SKILL.md` is excluded |

### Tests to add in `tests/test_init.py`

| Test | What it verifies |
|------|-----------------|
| `test_init_creates_claude_skills_dir` | `.claude/skills/` directory exists after init |
| `test_init_deploys_backlog_skill` | `.claude/skills/backlog/SKILL.md` exists and content matches bundled source |
| `test_init_deploys_evo_skill` | `.claude/skills/evo/SKILL.md` exists and content matches bundled source |
| `test_init_skill_content_verbatim` | Deployed SKILL.md content is byte-identical to bundled file |
| `test_init_skills_idempotent_backlog` | Re-init with existing `backlog/SKILL.md` does not overwrite |
| `test_init_skills_idempotent_evo` | Re-init with existing `evo/SKILL.md` does not overwrite |
| `test_init_skills_section_header_in_output` | `"Skills"` appears in captured stdout |
| `test_init_creates_skill_subdirectory` | `.claude/skills/backlog/` subdir is created by init |

### Tests to add in `tests/test_uninstall.py`

| Test | What it verifies |
|------|-----------------|
| `test_uninstall_force_removes_backlog_skill` | `--force` removes `.claude/skills/backlog/SKILL.md` |
| `test_uninstall_force_removes_evo_skill` | `--force` removes `.claude/skills/evo/SKILL.md` |
| `test_uninstall_force_removes_skills_dir_when_empty` | `.claude/skills/` itself removed when all subdirs gone |
| `test_uninstall_force_preserves_skills_dir_when_user_skills_remain` | User-created skill subdir not in bundled list is left alone; parent dir preserved |
| `test_uninstall_skills_in_summary` | Pre-confirmation summary lists skill paths |
| `test_uninstall_user_declines_skills_preserved` | Declining prompt leaves skill files intact |
| `test_uninstall_no_skills_dir_no_crash` | `--force` with no `.claude/skills/` does not raise |
| `test_uninstall_skills_skill_md_missing_in_subdir` | Subdir exists but has no `SKILL.md` — not listed in summary, not removed |

### Integration tests to add in `tests/test_uninstall.py`

| Test | What it verifies |
|------|-----------------|
| `test_init_then_uninstall_skills_roundtrip` | Full roundtrip: init deploys skills → uninstall removes them → `.ievo/` preserved |
| `test_uninstall_then_reinit_restores_skills` | After uninstall + manifest removal + re-init: both skills present again |

### Tests to update in `tests/test_uninstall.py`

| Existing test | Required update |
|--------------|----------------|
| `test_init_then_uninstall_roundtrip` | Add assertions that `.claude/skills/` is absent after uninstall |
| `_make_full_project` helper | Add `.claude/skills/backlog/SKILL.md` and `.claude/skills/evo/SKILL.md` to the fixture |

### Tests to update in `tests/test_init.py`

| Existing test | Required update |
|--------------|----------------|
| `test_init_skips_existing_files` | Pre-create `.claude/skills/backlog/SKILL.md` and `evo/SKILL.md`; assert they are unchanged after re-init |

## Dependencies

- **REQ-010**: `ievo uninstall` command (implemented) — this requirement extends it
- `bundled_skill_path()` — already exists in `src/ievo/marketplace/__init__.py`
- `list_bundled_skills()` — does not exist, must be added as part of this requirement
- `_write_if_missing()` — already exists in `src/ievo/commands/init.py`, reuse as-is
- `shutil` (stdlib) — needed for `rmtree` in uninstall; check if already imported
