---
phase: 01-foundation
plan: 05
type: execute
wave: 4
depends_on:
  - 01-02
  - 01-04
files_modified:
  - src/corpus_analyzer/cli.py
  - pyproject.toml
autonomous: true
requirements:
  - CONF-05
  - INGEST-01
  - INGEST-02
  - INGEST-03

must_haves:
  truths:
    - "Running 'corpus add ~/skills' appends a source to ~/.config/corpus/corpus.toml with name='skills'"
    - "Running 'corpus add ~/skills --name my-skills --include '*.md' --exclude '**/node_modules/**'' works"
    - "Running 'corpus add' on a duplicate path or name prints a clear error and exits non-zero"
    - "Running 'corpus index' reads corpus.toml, indexes all sources, and shows a progress bar per source"
    - "Running 'corpus index' a second time on unchanged files shows all files as skipped"
    - "Running 'corpus index' prints a per-source summary: files indexed, chunks written, files skipped, elapsed time"
    - "'corpus index --verbose' prints each file path as it is processed"
    - "Running 'corpus index' when Ollama is not running shows a clear error message"
  artifacts:
    - path: "src/corpus_analyzer/cli.py"
      provides: "corpus add and corpus index Typer commands"
      contains: "def add"
    - path: "pyproject.toml"
      provides: "lancedb, tomli-w, platformdirs in [project.dependencies]"
  key_links:
    - from: "src/corpus_analyzer/cli.py"
      to: "src/corpus_analyzer/config/io.py"
      via: "load_config() + save_config()"
      pattern: "load_config|save_config"
    - from: "src/corpus_analyzer/cli.py"
      to: "src/corpus_analyzer/ingest/indexer.py"
      via: "CorpusIndex.open() + index_source()"
      pattern: "CorpusIndex"
    - from: "src/corpus_analyzer/cli.py"
      to: "src/corpus_analyzer/config/schema.py"
      via: "CorpusConfig + SourceConfig"
      pattern: "CorpusConfig|SourceConfig"
---

<objective>
Add the 'corpus add' and 'corpus index' CLI commands to the existing cli.py and install new dependencies.

Purpose: This is the user-facing surface of Phase 1. Both commands integrate the config and ingest layers built in Plans 01-04. This is the final piece that makes Phase 1 end-to-end runnable.

Output: Working 'corpus add' and 'corpus index' commands in the existing cli.py Typer app.
</objective>

<execution_context>
@/home/ollie/.claude/get-shit-done/workflows/execute-plan.md
@/home/ollie/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-02-SUMMARY.md
@.planning/phases/01-foundation/01-04-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install new dependencies and wire 'corpus add' command</name>
  <files>
    pyproject.toml
    src/corpus_analyzer/cli.py
  </files>
  <action>
    Step 1 — Install dependencies (if not already done from Plan 01):
      Run: uv add lancedb tomli-w platformdirs
      Confirm they appear in pyproject.toml [project.dependencies].

    Step 2 — Add 'corpus add' command to src/corpus_analyzer/cli.py.
    The existing cli.py has a Typer app named `app`. Add to it (do NOT create a new app or sub-app):

    @app.command("add")
    def add_command(
        directory: Annotated[str, typer.Argument(help="Directory path to add as a source")],
        name: Annotated[Optional[str], typer.Option("--name", "-n", help="Source name (default: directory basename)")] = None,
        include: Annotated[list[str], typer.Option("--include", help="Glob patterns to include")] = ["**/*"],
        exclude: Annotated[list[str], typer.Option("--exclude", help="Glob patterns to exclude")] = [],
        force: Annotated[bool, typer.Option("--force", help="Re-add if source already exists")] = False,
    ) -> None:
        """Add a directory to corpus.toml as a named source."""

    Implementation:
      1. Resolve source_name: name if given, else Path(directory).name (basename)
      2. Load config: config = load_config(CONFIG_PATH) where CONFIG_PATH = Path(user_config_dir("corpus")) / "corpus.toml"
      3. Check for duplicates: if any source in config.sources has matching name or path AND not force:
         console.print(f"[red]Error:[/] Source '{source_name}' or path '{directory}' already exists. Use --force to re-add.")
         raise typer.Exit(code=1)
      4. If force: remove existing source with same name or path from config.sources
      5. Append SourceConfig(name=source_name, path=directory, include=include, exclude=exclude)
      6. save_config(CONFIG_PATH, config)
      7. console.print(f"[green]✔[/] Added source '[bold]{source_name}[/]' ({directory})")
      8. console.print(f"  Run [bold]corpus index[/] to index it.")

    Import required at top of cli.py: load_config, save_config, SourceConfig, CorpusConfig from respective modules.
    Import platformdirs.user_config_dir and platformdirs.user_data_dir at top of cli.py.
    Define module-level constants (after imports, before commands):
      CONFIG_PATH = Path(user_config_dir("corpus")) / "corpus.toml"
      DATA_DIR = Path(user_data_dir("corpus"))

    Full type annotations and PEP 257 docstring on the command function.
  </action>
  <verify>
    <automated>uv run corpus-analyzer add --help</automated>
    <manual>Verify 'corpus-analyzer add /tmp/testdir' creates ~/.config/corpus/corpus.toml with correct content</manual>
  </verify>
  <done>'corpus add' command visible in --help. Duplicate detection works (returns exit code 1). Config file written to correct XDG path.</done>
</task>

<task type="auto">
  <name>Task 2: Wire 'corpus index' command with progress bar and per-source summary</name>
  <files>
    src/corpus_analyzer/cli.py
  </files>
  <action>
    Add 'corpus index' command to the existing Typer app in src/corpus_analyzer/cli.py:

    @app.command("index")
    def index_command(
        verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Print each file as it is processed")] = False,
    ) -> None:
        """Index all configured sources into LanceDB."""

    Implementation:
      1. Load config: config = load_config(CONFIG_PATH)
      2. If config.sources is empty:
         console.print("[yellow]No sources configured. Run 'corpus add <dir>' first.[/]")
         raise typer.Exit(code=0)
      3. Validate Ollama connection:
         embedder = OllamaEmbedder(model=config.embedding.model, host=config.embedding.host)
         try: embedder.validate_connection()
         except RuntimeError as e: console.print(f"[red]Error:[/] {e}"); raise typer.Exit(code=1)
      4. Open index: index = CorpusIndex.open(DATA_DIR, embedder)
      5. For each source in config.sources:
         a. Resolve and validate source path exists (Path(source.path).expanduser().exists())
            If not: console.print(f"[yellow]Warning:[/] Source path not found: {source.path} — skipping")
            continue
         b. Count total files: total = sum(1 for _ in walk_source(Path(source.path).expanduser(), source.include, source.exclude))
         c. With rich Progress context (SpinnerColumn, TextColumn, BarColumn, TextColumn("{task.completed}/{task.total} files"), TimeElapsedColumn):
            - Add task: description=f"Indexing {source.name}...", total=total
            - Define progress_callback: lambda n: progress.advance(task_id, n)
            - If verbose: also print each file path (use --verbose flag to enable per-file console.print)
            - result = index.index_source(source, progress_callback=progress_callback)
         d. After progress completes, print summary line:
            console.print(f"[green]✔[/] Indexed [bold]{source.name}[/]: {result.files_indexed} files, {result.chunks_written} chunks ({result.elapsed:.1f}s) — {result.files_skipped} unchanged")
      6. Print total summary line if more than one source.

    Import OllamaEmbedder from corpus_analyzer.ingest.embedder.
    Import CorpusIndex from corpus_analyzer.ingest.indexer.
    Import walk_source from corpus_analyzer.ingest.scanner.
    Import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn from rich.progress.

    The verbose flag: when True, the progress_callback should also print the current file path via console.print.
    To do this cleanly, change the index_source() signature to accept an optional file_callback: Callable[[Path], None] | None = None
    and call it in the indexer before processing each file. Update indexer.py if needed (minimal change — just add the callback call).

    Full type annotations and PEP 257 docstring on the command function.
  </action>
  <verify>
    <automated>uv run corpus-analyzer index --help && uv run ruff check src/corpus_analyzer/cli.py && uv run mypy src/corpus_analyzer/cli.py</automated>
    <manual>
      1. Create a test directory: mkdir -p /tmp/corpus-test && echo "# Hello" > /tmp/corpus-test/test.md
      2. Run: uv run corpus-analyzer add /tmp/corpus-test
      3. Run: uv run corpus-analyzer index (requires Ollama running with nomic-embed-text pulled)
      4. Verify progress bar appears and summary line shows file/chunk counts
      5. Run: uv run corpus-analyzer index again — verify files_skipped == total files
    </manual>
    <sampling_rate>Manual verification after this task completes, before phase is marked done</sampling_rate>
  </verify>
  <done>'corpus index' runs end-to-end with progress bar. Second run shows all files as skipped. Per-source summary line printed.</done>
</task>

</tasks>

<verification>
uv run ruff check src/corpus_analyzer/cli.py
uv run mypy src/corpus_analyzer/cli.py
uv run corpus-analyzer --help
uv run corpus-analyzer add --help
uv run corpus-analyzer index --help
</verification>

<success_criteria>
Both 'corpus add' and 'corpus index' appear in --help output. mypy and ruff pass with no errors. The add command writes to the correct XDG config path. The index command shows progress and summary output. No existing CLI commands are broken.
</success_criteria>

<output>
After completion, create .planning/phases/01-foundation/01-05-SUMMARY.md
</output>
