---
phase: 57-contract-layer
plan: 02
type: execute
wave: 2
depends_on:
  - 57-01
files_modified:
  - paperforge/worker/status.py
  - paperforge/cli.py
  - paperforge/commands/status.py
  - tests/cli/test_json_contracts.py
autonomous: true
requirements:
  - CTRT-03
  - CTRT-04

must_haves:
  truths:
    - "paperforge status --json returns PFResult envelope with data containing current flat dict shape"
    - "paperforge doctor --json returns PFResult envelope with structured checks in data"
    - "All PFResult output from status and doctor has valid {ok, command, version, data, error} shape"
  artifacts:
    - path: "paperforge/worker/status.py"
      provides: "Modified run_status (PFResult wrapping) + run_doctor with --json support"
      contains: "from paperforge.core.result import"
    - path: "paperforge/cli.py"
      provides: "doctor subparser with --json flag"
      contains: "add_argument.*--json"
    - path: "tests/cli/test_json_contracts.py"
      provides: "Updated PFResult shape tests for status --json, new tests for doctor --json"
      min_lines: 140
  key_links:
    - from: "paperforge/worker/status.py"
      to: "paperforge/core/result.py"
      via: "PFResult wrapper in JSON output path"
    - from: "paperforge/worker/status.py"
      to: "paperforge/core/errors.py"
      via: "ErrorCode entries for doctor checks"
    - from: "tests/cli/test_json_contracts.py"
      to: "paperforge/worker/status.py"
      via: "CLI subprocess invoker exercises both commands"
---

<objective>
Wrap the existing `paperforge status --json` output in a PFResult envelope (CTRT-03) and add `--json` output to `paperforge doctor` (CTRT-04). The existing flat dict output is preserved inside the `data` field (backward compatible). Doctor gets new structured JSON output with check results.

Purpose: All status and doctor commands produce machine-readable PFResult-format JSON with consistent shape.
Output: Modified status.py (PFResult wrapping for run_status, JSON for run_doctor), CLI updates, contract tests
</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/ROADMAP.md
@.planning/STATE.md
@.planning/phases/057-contract-layer/057-CONTEXT.md

<interfaces>
From paperforge/core/result.py (after Plan 57-01):
```python
@dataclass
class PFResult:
    ok: bool
    command: str
    version: str
    data: Any = None
    error: PFError | None = None
    def to_dict(self) -> dict: ...
    def to_json(self, indent: int = 2) -> str: ...
    @staticmethod def from_json(json_str: str) -> PFResult: ...

@dataclass
class PFError:
    code: str
    message: str
    details: dict
```

From paperforge/core/errors.py (after Plan 57-01):
```python
class ErrorCode(str, Enum):
    PYTHON_NOT_FOUND = "PYTHON_NOT_FOUND"
    VERSION_MISMATCH = "VERSION_MISMATCH"
    ...
```

Existing status --json output (paperforge/worker/status.py:973-998):
```python
data = {
    "version": __import__("paperforge").__version__,
    "vault": str(vault),
    "system_dir": cfg["system_dir"],
    "resources_dir": cfg["resources_dir"],
    "exports": len(export_files),
    "domains": len(config.get("domains", [])),
    "total_papers": record_count,
    "formal_notes": note_count,
    "bases": base_count,
    "ocr": {"total": ..., "pending": ..., "done": ..., "failed": ...},
    "path_errors": ...,
    "env_configured": ...,
    "lifecycle_level_counts": ...,
    "health_aggregate": ...,
    "maturity_distribution": ...,
}
print(_json.dumps(data, indent=2, ensure_ascii=False))
```

Existing doctor command dispatch (paperforge/cli.py:449-453):
```python
if args.command == "doctor":
    from paperforge.worker.status import run_doctor
    return run_doctor(vault)
```

Existing run_doctor signature (worker/status.py:359):
```python
def run_doctor(vault: Path, verbose: bool = False) -> int:
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Wrap status --json output in PFResult envelope</name>
  <files>
    paperforge/worker/status.py
  </files>
  <action>
    In paperforge/worker/status.py, modify the `run_status` function's JSON output block (around line 973):

    1. Import PFResult at the top of the function (lazy import, inside the `if json_output:` block): `from paperforge.core.result import PFResult`

    2. Build the same `data` dict as currently, preserving its exact shape (backward compatibility of the data payload).

    3. Wrap in PFResult:
    ```python
    result = PFResult(
        ok=True,
        command="status",
        version=data["version"],  # from the existing data dict
        data=data,
    )
    print(result.to_json())
    ```

    4. Remove the `import json as _json` at the function top if it becomes unused (check -- _json may still be used elsewhere in the function). If _json is only used for the print, replace with PFResult. Otherwise keep _json and suppress the warning.

    5. Handle error case: if any unrecoverable error occurs during status computation (e.g., vault doesn't exist), wrap in PFResult with ok=False and appropriate ErrorCode. Use try/except around the existing function body, catching broad exceptions and wrapping in PFResult(ok=False, command="status", error=PFError(code=ErrorCode.INTERNAL_ERROR.value, message=str(e), details={"vault": str(vault)})).

    IMPORTANT: Do NOT change the `data` dict structure. The existing flat dict is the backward-compatible payload. Only wrap it in PFResult.
  </action>
  <verify>
    <automated>python -m paperforge status --json --vault C:\Users\Lin\AppData\Local\Temp\opencode 2>&1 || true</automated>
  </verify>
  <done>
    status --json output is wrapped in PFResult: {"ok": true, "command": "status", "version": "1.4.17rc3", "data": {existing flat dict}, "error": null}
    Errors produce PFResult with ok=false.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add --json output to doctor command</name>
  <files>
    paperforge/cli.py
    paperforge/worker/status.py
    paperforge/commands/status.py
  </files>
  <action>
    Two modifications:

    A. Add --json flag to doctor subparser in paperforge/cli.py:
    ```python
    doctor_p = sub.add_parser("doctor", help="Validate PaperForge setup and configuration")
    doctor_p.add_argument("--json", action="store_true", dest="json_output", help="Output JSON")
    ```

    B. Modify run_doctor function in paperforge/worker/status.py:
    1. Add `json_output: bool = False` parameter to `def run_doctor(vault: Path, verbose: bool = False, json_output: bool = False) -> int:`
    2. At the end of the function (after verdict is computed but before print statements), add:
    ```python
    if json_output:
        from paperforge.core.result import PFResult, PFError
        from paperforge.core.errors import ErrorCode
        from paperforge import __version__

        if has_fail:
            result = PFResult(
                ok=False,
                command="doctor",
                version=__version__,
                data={"checks": checks_data, "verdict": "FAIL"},
                error=PFError(code=ErrorCode.INTERNAL_ERROR.value, message=f"{verdict}: {next_action}", details={"fail_count": fail_count, "categories": list(fail_categories)}),
            )
        else:
            result = PFResult(
                ok=True,
                command="doctor",
                version=__version__,
                data={"checks": checks_data, "verdict": verdict, "verdict_message": next_action},
            )
        print(result.to_json())
        return 1 if has_fail else 0
    ```
    3. Build `checks_data` as a list of dicts: `[{"category": cat, "status": status, "message": msg, "fix": fix} for cat, status, msg, fix in checks]`
    4. Compute `fail_count = sum(1 for _, s, _, _ in checks if s == "fail")` near the existing `has_fail` computation.
    5. Keep the existing text output path unchanged (everything before the `if json_output:` block).

    C. Update cli.py doctor dispatch to pass json_output:
    ```python
    if args.command == "doctor":
        from paperforge.worker.status import run_doctor
        return run_doctor(vault, verbose=getattr(args, "verbose", False), json_output=getattr(args, "json_output", False))
    ```
  </action>
  <verify>
    <automated>python -m paperforge doctor --json --vault C:\Users\Lin\AppData\Local\Temp\opencode 2>&1 || true</automated>
  </verify>
  <done>doctor --json produces PFResult with structured checks in data field. Text output path unchanged. CLI passes json_output to run_doctor.</done>
</task>

<task type="auto">
  <name>Task 3: Update CLI contract tests for PFResult-shaped status and doctor</name>
  <files>
    tests/cli/test_json_contracts.py
  </files>
  <action>
    Update tests/cli/test_json_contracts.py:

    1. In TestStatusJson, add a new method `test_status_json_pfresult_shape` that verifies the output is wrapped in PFResult:
    - Call cli_invoker(["status", "--json"])
    - assert_valid_json(result.stdout)
    - Parse into dict
    - Assert top-level keys are {"ok", "command", "version", "data", "error"} using assert_json_shape
    - Assert "ok" is True
    - Assert "command" == "status"
    - Assert "version" is a non-empty string
    - Assert "data" is a dict with at least {"version", "vault"} keys
    - Assert "error" is None

    2. Add TestDoctorJson class (mirroring TestStatusJson pattern) with:
    - REQUIRED_KEYS = {"ok", "command", "version", "data", "error"}
    - test_doctor_json_pfresult_shape: verifies PFResult envelope
    - test_doctor_json_data_has_checks: verifies data.checks is a list
    - test_doctor_json_no_vault: doctor --json on nonexistent vault exits non-zero

    3. Keep all existing tests (do NOT remove test_status_json_value_semantics etc. — they must continue to pass on the wrapped output, since the data field preserves the old shape)

    Do NOT modify test_status_json_value_semantics and test_status_json_snapshot_regression — these tests read the PFResult.data sub-dict and must still pass.
  </action>
  <verify>
    <automated>python -m pytest tests/cli/test_json_contracts.py -v --tb=short -k "pfresult or doctor" 2>&1 || true</automated>
  </verify>
  <done>New PFResult shape tests pass. Existing contract tests (old flat dict shape inside data field) continue to pass.</done>
</task>

</tasks>

<verification>
- paperforge status --json output starts with {"ok": true, "command": "status", ...}
- paperforge doctor --json output starts with {"ok": true/false, "command": "doctor", ...}
- All existing tests still pass (backward compatible data payload preserved)
- New PFResult shape tests pass
</verification>

<success_criteria>
- [ ] status --json wrapped in PFResult with backward-compatible data payload
- [ ] doctor --json produces PFResult with structured checks in data
- [ ] doctor subparser in cli.py has --json flag
- [ ] run_doctor accepts json_output parameter
- [ ] New PFResult contract tests pass alongside existing contract tests
</success_criteria>

<output>
After completion, create .planning/phases/057-contract-layer/057-02-SUMMARY.md
</output>