---
phase: 01-config-and-command-foundation
plan: 02
type: execute
wave: 2
depends_on:
  - 01
files_modified:
  - pyproject.toml
  - paperforge/__main__.py
  - paperforge/cli.py
  - tests/test_cli_paths.py
  - tests/test_cli_worker_dispatch.py
autonomous: true
requirements:
  - CONF-02
  - CMD-01
  - CMD-03
must_haves:
  truths:
    - "User can run `python -m paperforge paths` from the repo and see resolved paths."
    - "Editable install exposes `paperforge` as the canonical command."
    - "`paperforge ocr` and `paperforge ocr run` dispatch to the same OCR worker behavior."
  artifacts:
    - path: "paperforge/cli.py"
      provides: "Stable command parser and worker dispatch"
      exports: ["main", "build_parser"]
    - path: "paperforge/__main__.py"
      provides: "`python -m paperforge` fallback"
    - path: "pyproject.toml"
      provides: "`paperforge` console script entry point"
      contains: "paperforge = \"paperforge.cli:main\""
  key_links:
    - from: "paperforge/cli.py"
      to: "paperforge.config"
      via: "resolve_vault/load_vault_config/paperforge_paths"
      pattern: "from paperforge.config import"
    - from: "paperforge/cli.py"
      to: "pipeline.worker.scripts.literature_pipeline"
      via: "worker function dispatch"
      pattern: "run_selection_sync|run_index_refresh|run_ocr|run_deep_reading|run_status"
---

<objective>
Create the stable `paperforge` command surface.

Purpose: Users need copy-pasteable commands (`paperforge paths`, `paperforge status`, `paperforge ocr run`) that resolve config dynamically and avoid unresolved path tokens.
Output: CLI package entry points, launcher tests, and minimal packaging metadata.
</objective>

<execution_context>
@C:/Users/Lin/.codex/get-shit-done/workflows/execute-plan.md
@C:/Users/Lin/.codex/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-config-and-command-foundation/01-CONTEXT.md
@.planning/phases/01-config-and-command-foundation/01-RESEARCH.md
@paperforge/config.py
@pipeline/worker/scripts/literature_pipeline.py
@requirements.txt

<interfaces>
Use the resolver contract from Plan 01:
```python
def resolve_vault(cli_vault=None, env=None, cwd=None) -> Path:
def load_vault_config(vault, env=None, overrides=None) -> dict:
def paperforge_paths(vault, cfg=None) -> dict[str, Path]:
def paths_as_strings(paths) -> dict[str, str]:
```

Existing worker functions to dispatch:
```python
def run_selection_sync(vault: Path) -> int:
def run_index_refresh(vault: Path) -> int:
def run_ocr(vault: Path) -> int:
def run_deep_reading(vault: Path) -> int:
def run_status(vault: Path) -> int:
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add CLI tests for paths output and worker dispatch</name>
  <files>tests/test_cli_paths.py, tests/test_cli_worker_dispatch.py</files>
  <read_first>
    - `paperforge/config.py`
    - `pipeline/worker/scripts/literature_pipeline.py`
    - `.planning/phases/01-config-and-command-foundation/01-CONTEXT.md`
  </read_first>
  <behavior>
    - `main(["--vault", tmp_vault, "paths", "--json"])` prints only valid JSON with the exact D-Path Output keys.
    - `main(["--vault", tmp_vault, "paths"])` prints one `key: absolute_path` line per required path and does not print `<system_dir>` or `<resources_dir>`.
    - `main(["--vault", tmp_vault, "status"])` calls `literature_pipeline.run_status(Path(tmp_vault))`.
    - `main(["--vault", tmp_vault, "selection-sync"])` calls `run_selection_sync`.
    - `main(["--vault", tmp_vault, "index-refresh"])` calls `run_index_refresh`.
    - `main(["--vault", tmp_vault, "deep-reading"])` calls `run_deep_reading`.
    - `main(["--vault", tmp_vault, "ocr"])` and `main(["--vault", tmp_vault, "ocr", "run"])` both call `run_ocr`.
  </behavior>
  <action>Create pytest tests that import `paperforge.cli.main`. Use `monkeypatch` to replace worker functions in `pipeline.worker.scripts.literature_pipeline` with functions that append the command name and vault path to a list and return `0`. Use `capsys` to assert JSON/text path output. Do not call real OCR, Zotero, or filesystem mutation workers.</action>
  <verify>
    <automated>python -m pytest tests/test_cli_paths.py tests/test_cli_worker_dispatch.py -q</automated>
  </verify>
  <acceptance_criteria>
    - `rg -n "paths --json|ocr.*run|run_status|run_ocr|<system_dir>" tests/test_cli_paths.py tests/test_cli_worker_dispatch.py` shows CLI coverage and unresolved token rejection.
    - `python -m pytest tests/test_cli_paths.py tests/test_cli_worker_dispatch.py -q` passes after Task 2.
  </acceptance_criteria>
  <done>Tests prove the locked command surface from D-Command Surface without invoking real workers.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement CLI and packaging entry point</name>
  <files>pyproject.toml, paperforge/__main__.py, paperforge/cli.py, tests/test_cli_paths.py, tests/test_cli_worker_dispatch.py</files>
  <read_first>
    - `tests/test_cli_paths.py`
    - `tests/test_cli_worker_dispatch.py`
    - `paperforge/config.py`
    - `requirements.txt`
    - `pipeline/worker/scripts/literature_pipeline.py`
  </read_first>
  <behavior>
    - `python -m paperforge --vault . paths --json` emits pure JSON.
    - `python -m paperforge --vault . paths` emits concise text paths and no unresolved path tokens.
    - `paperforge` is installable through `pyproject.toml` console script.
    - Stable worker commands delegate to existing worker functions and return their integer status codes.
  </behavior>
  <action>Create `paperforge/cli.py` using stdlib `argparse` with global `--vault` plus subcommands `paths`, `status`, `selection-sync`, `index-refresh`, `deep-reading`, and nested `ocr run`. Set the parent `ocr` command default to the same handler as `ocr run` per D-Command Surface. Load `.env` exactly as the legacy worker currently does before worker dispatch: call `literature_pipeline.load_simple_env(vault / ".env")`, then `load_simple_env(paths["paperforge"] / ".env")`. Create `paperforge/__main__.py` that exits through `cli.main()`. Add `pyproject.toml` with setuptools backend, project name `paperforge-lite`, Python `>=3.10`, dependencies matching `requirements.txt`, optional `test = ["pytest>=7.4.0"]`, and `[project.scripts] paperforge = "paperforge.cli:main"` per D-Packaging.</action>
  <verify>
    <automated>python -m pytest tests/test_cli_paths.py tests/test_cli_worker_dispatch.py tests/test_config.py -q</automated>
    <automated>python -m paperforge --vault . paths --json</automated>
  </verify>
  <acceptance_criteria>
    - `rg -n "paperforge = \"paperforge.cli:main\"|build-system|project.scripts" pyproject.toml` confirms packaging.
    - `rg -n "add_subparsers|ocr|set_defaults|run_worker|paths_as_strings" paperforge/cli.py` confirms argparse command wiring.
    - `python -m paperforge --vault . paths --json` exits 0 and prints JSON keys `vault`, `worker_script`, and `ld_deep_script`.
    - `python -m pytest tests/test_cli_paths.py tests/test_cli_worker_dispatch.py tests/test_config.py -q` passes.
  </acceptance_criteria>
  <done>`paperforge` has a package entry point and `python -m paperforge` fallback with stable Phase 1 commands.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| CLI args -> worker dispatch | User-provided command names select worker functions. |
| CLI args/env -> path output | User-controlled vault/config values are printed and passed to worker functions. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-03 | Tampering | `paperforge/cli.py` worker dispatch | mitigate | Dispatch through a fixed command-to-function map; do not eval command names or construct shell commands. |
| T-01-04 | Information Disclosure | `paths --json` | accept | Output only filesystem paths from resolver; never print `.env` values or OCR tokens. |
</threat_model>

<verification>
Run `python -m pytest tests/test_cli_paths.py tests/test_cli_worker_dispatch.py tests/test_config.py -q` and `python -m paperforge --vault . paths --json`.
</verification>

<success_criteria>
The canonical launcher command is implemented per D-Command Surface, and the module fallback works without requiring editable install.
</success_criteria>

<output>
After completion, create `.planning/phases/01-config-and-command-foundation/01-config-and-command-foundation-02-SUMMARY.md`
</output>
