---
phase: 13-logging-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/logging_config.py
  - paperforge/cli.py
autonomous: true
requirements:
  - OBS-01
  - OBS-02
  - OBS-03
must_haves:
  truths:
    - "paperforge/logging_config.py exists as the single entry point for logging configuration"
    - "All subcommands accept `--verbose` / `-v` flag from the root parser"
    - "configure_logging() is called once in cli.py:main() before command dispatch"
    - "PAPERFORGE_LOG_LEVEL env var controls default log level (INFO/WARNING/ERROR/DEBUG)"
    - "Invalid PAPERFORGE_LOG_LEVEL values silently fall back to WARNING"
    - "verbose=True overrides to DEBUG level regardless of PAPERFORGE_LOG_LEVEL"
  artifacts:
    - path: "paperforge/logging_config.py"
      provides: "Single configure_logging(verbose) entry point"
      min_lines: 40
    - path: "paperforge/cli.py"
      provides: "Global --verbose flag on root parser, configure_logging() call in main()"
      contains: "configure_logging"
  key_links:
    - from: "paperforge/logging_config.py"
      to: "paperforge/cli.py:main()"
      via: "configure_logging(verbose=args.verbose) call"
      pattern: "configure_logging"
    - from: "paperforge/logging_config.py"
      to: "os.environ"
      via: "PAPERFORGE_LOG_LEVEL env var read"
      pattern: "PAPERFORGE_LOG_LEVEL"
---

<objective>
Create `paperforge/logging_config.py` as the single entry point for Python stdlib logging configuration, and wire it into the CLI entry point.

Purpose: This is the foundation for Phase 13 and all subsequent v1.4 observability work. A single, well-tested `configure_logging(verbose)` function prevents scattered `basicConfig()` calls and ensures consistent log behavior across all commands.

Output:
- `paperforge/logging_config.py` — `configure_logging(verbose: bool = False) -> None`
- Modified `paperforge/cli.py` — global `--verbose` flag on root parser, `configure_logging()` call in `main()`
</objective>

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

<context>
@.planning/STATE.md
@.planning/ROADMAP.md
@.planning/phases/13-logging-foundation/13-CONTEXT.md
@.planning/phases/13-logging-foundation/13-RESEARCH.md
@paperforge/cli.py
@paperforge/config.py
@paperforge/pdf_resolver.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create paperforge/logging_config.py</name>

  <files>paperforge/logging_config.py (NEW FILE)</files>

  <read_first>
  - paperforge/config.py — Reference: single-purpose module pattern, pure functions, no side effects at import time
  - paperforge/pdf_resolver.py — Reference: existing `import logging; logger = logging.getLogger(__name__)` pattern
  - .planning/phases/13-logging-foundation/13-CONTEXT.md — D-02: signature, D-03: basicConfig/dictConfig, D-06: env var, D-09: invalid fallback, D-10: early-boot guard
  - .planning/phases/13-logging-foundation/13-RESEARCH.md §4.1 — Integration point analysis
  </read_first>

  <action>
  Create `paperforge/logging_config.py` with the following implementation:

  ```python
  """PaperForge Lite — logging configuration single entry point.
  
  Call `configure_logging(verbose)` once at CLI startup before command dispatch.
  All worker and command modules use `logging.getLogger(__name__)` for
  diagnostic/trace/error output.
  
  Environment:
      PAPERFORGE_LOG_LEVEL — default log level (DEBUG/INFO/WARNING/ERROR).
      Invalid values silently fall back to WARNING.
  """
  
  import logging
  import os
  import sys
  
  
  def configure_logging(verbose: bool = False) -> None:
      """Configure the root ``paperforge`` logger.
  
      Sets up a StreamHandler writing to stderr with a ``LEVEL:name:message``
      format string. Idempotent — does nothing if handlers are already
      configured (guards against double ``basicConfig()`` or ``dictConfig()``).
  
      Args:
          verbose: If True, forces ``DEBUG`` level regardless of env var.
      """
      logger = logging.getLogger("paperforge")
  
      # Idempotency guard: if handlers already exist, only adjust level.
      # This prevents duplicate log lines from multiple configure_logging calls.
      if logger.handlers:
          if verbose:
              logger.setLevel(logging.DEBUG)
          return
  
      # Resolve log level from environment or default
      level_name = os.environ.get("PAPERFORGE_LOG_LEVEL", "INFO").strip().upper()
      level = getattr(logging, level_name, None)
      if not isinstance(level, int) or level_name not in ("DEBUG", "INFO", "WARNING", "ERROR"):
          level = logging.WARNING
  
      if verbose:
          level = logging.DEBUG
  
      logger.setLevel(level)
  
      handler = logging.StreamHandler(stream=sys.stderr)
      handler.setLevel(logging.DEBUG)  # handler-level always DEBUG; logger controls filtering
      formatter = logging.Formatter(
          fmt="%(levelname)s:%(name)s:%(message)s",
          datefmt="%H:%M:%S",
      )
      handler.setFormatter(formatter)
      logger.addHandler(handler)
  
  
  def get_paperforge_logger() -> logging.Logger:
      """Convenience accessor for the ``paperforge`` logger.
  
      Equivalent to ``logging.getLogger("paperforge")`` but provides a single
      well-known reference point for early-boot logging before
      ``configure_logging()`` has been called.
  
      Callers in worker/command modules should still prefer the per-module
      ``logger = logging.getLogger(__name__)`` pattern. This function exists
      for places (like ``cli.py:main()``) that need the root logger before
      per-module loggers are established.
      """
      return logging.getLogger("paperforge")
  ```

  **Key design decisions (per CONTEXT.md):**
  - D-03: Uses `logger.setLevel()` + `handler.setLevel()` programmatic setup (not dictConfig) — simpler, no external config files
  - D-02: Single `configure_logging(verbose)` export
  - D-06: Reads `PAPERFORGE_LOG_LEVEL` from `os.environ`; default `INFO`
  - D-09: Invalid env var values → silent fallback to `WARNING`
  - D-05: `verbose=True` overrides to `DEBUG`
  - D-10: Idempotency guard: `if logger.handlers: return` — safe for early-boot `basicConfig(WARNING)` calls
  - Handler writes to `sys.stderr` — diagnostic output only, stdout stays clean for user-facing `print()`
  - Formatter: `LEVEL:name:message` — compact, grep-friendly, no timestamps (user sees real-time output)
  - The root logger is named `paperforge`, producing hierarchy `paperforge.worker.sync`, `paperforge.commands.ocr`, etc.
  </action>

  <acceptance_criteria>
  - `paperforge/logging_config.py` exists as a new file
  - File contains `def configure_logging(verbose: bool = False) -> None:`
  - File reads `os.environ.get("PAPERFORGE_LOG_LEVEL", "INFO")`
  - File contains `StreamHandler(stream=sys.stderr)`
  - File contains `getattr(logging, level_name, None)` for level resolution
  - File contains `if logger.handlers: return` guard
  - Invalid env var (e.g., `PAPERFORGE_LOG_LEVEL=banana`) results in `logging.WARNING` level
  - `verbose=True` sets level to `logging.DEBUG`
  - `import paperforge.logging_config` raises no errors
  </acceptance_criteria>

  <verify>
  <automated>python -c "import sys; sys.path.insert(0, '.'); from paperforge.logging_config import configure_logging; configure_logging(verbose=True); import logging; assert logging.getLogger('paperforge').level == logging.DEBUG; print('OK')"</automated>
  </verify>

  <done>
  - `paperforge/logging_config.py` exists with `configure_logging()` function
  - Import and basic assertion test passes
  - StreamHandler targets stderr (confirmed by checking `handler.stream is sys.stderr`)
  </done>
</task>

<task type="auto">
  <name>Task 2: Add global --verbose to CLI root parser and wire configure_logging()</name>

  <files>paperforge/cli.py</files>

  <read_first>
  - paperforge/cli.py — Full file to see current parser structure, main() flow, and command dispatch
  - .planning/phases/13-logging-foundation/13-CONTEXT.md — D-04 (global flag), D-05 (verbose→DEBUG)
  - .planning/phases/13-logging-foundation/13-RESEARCH.md §3.1 (existing verbose locations), §4.1 (integration point at cli.py:main())
  </read_first>

  <action>
  Modify `paperforge/cli.py`:

  **1. Add import at top of file:**
  ```python
  from paperforge.logging_config import configure_logging
  ```

  **2. Add `--verbose` / `-v` to the ROOT parser** (after the existing `--vault` argument, before `add_subparsers()`):
  
  In `build_parser()`, add right after the `--vault` argument (after line 124):
  ```python
      parser.add_argument(
          "--verbose", "-v",
          action="store_true",
          help="Enable DEBUG-level diagnostic output on stderr",
      )
  ```

  **3. Remove the per-subcommand `--verbose` from `deep-reading` and `repair`** to avoid duplicate argument errors:
  
  For `deep-reading` (lines 170-173): Remove these four lines:
  ```python
      p_dr.add_argument(
          "--verbose", "-v", action="store_true",
          help="Show fix instructions for blocked papers"
      )
  ```
  
  For `repair` (lines 178-180): Remove these three lines:
  ```python
      p_repair.add_argument(
          "--verbose", "-v", action="store_true",
          help="Show detailed divergence report"
      )
  ```

  **4. Insert `configure_logging()` call in `main()`** right before the command dispatch section (between line 282 `args.paths = paperforge_paths(vault, cfg)` and line 286 `if args.command == "paths":`):

  ```python
      # Configure logging before command dispatch
      configure_logging(verbose=getattr(args, "verbose", False))
  ```

  **Why:** This ensures logging is configured as early as possible in the CLI lifecycle but after vault/env resolution (so env vars are available). All subsequent worker module logger calls will inherit this configuration.

  **Note on backward compatibility:** The deep-reading and repair subcommands previously had their own `--verbose`. Since we're adding `--verbose` to the root parser, it's now inherited by ALL subcommands automatically. The old per-subcommand flags worked via `action="store_true"` on the same `--verbose` / `-v` dest name, so argparse resolves them identically. No code changes needed in the command modules for backward compat — `args.verbose` will be `True` regardless of whether the user typed `paperforge deep-reading -v` or `paperforge -v deep-reading`.
  </action>

  <acceptance_criteria>
  - `cli.py` imports `configure_logging` from `paperforge.logging_config`
  - `build_parser()` has `--verbose` / `-v` as a root parser argument
  - `deep-reading` and `repair` subparsers no longer have their own `--verbose` argument
  - `main()` calls `configure_logging(verbose=args.verbose)` before command dispatch
  - `python -c "from paperforge.cli import main; main(['-v', 'status'])"` works without error
  - `python -c "from paperforge.cli import main; main(['--verbose', 'status'])"` works without error
  - `python -c "from paperforge.cli import main; main(['status'])"` works without error (no -v)
  - `python -c "from paperforge.cli import main; main(['deep-reading', '-v'])"` works (backward compat)
  - `python -c "from paperforge.cli import main; main(['repair', '-v'])"` works (backward compat)
  </acceptance_criteria>

  <verify>
  <automated>python -c "import sys; sys.path.insert(0, '.'); from paperforge.cli import build_parser; p = build_parser(); r = p.parse_args(['--verbose', 'status']); assert r.verbose == True; r2 = p.parse_args(['deep-reading']); assert r2.verbose == False; print('OK')"</automated>
  </verify>

  <done>
  - `cli.py` builds/runs successfully with and without `--verbose`
  - Global `--verbose` recognized by all subcommands
  - `configure_logging()` called before command dispatch in `main()`
  - Backward compatible: `deep-reading -v` and `repair -v` continue to work
  </done>
</task>

</tasks>

<verification>
1. Verify `logging_config.py` exists and can be imported
2. Verify `configure_logging(verbose=True)` sets DEBUG level
3. Verify CLI root parser accepts `--verbose` / `-v` globally
4. Verify old per-subcommand `--verbose` removal doesn't break deep-reading or repair
5. Verify `configure_logging()` is called in `main()` before dispatch
</verification>

<success_criteria>
- `paperforge/logging_config.py` created with `configure_logging(verbose)` function
- `paperforge/cli.py` has global `--verbose` flag and `configure_logging()` call in main()
- All tests pass (import, arg parse, config)
- No regression in command dispatch behavior
</success_criteria>

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