# agdt-setup-check — Expectations Document

This document describes the observable behavior of `agdt-setup-check`. It is
the reference for phases, flags, and exit codes.

## TL;DR

`agdt-setup-check` is a read-only dependency verifier (and optionally a
repair dispatcher) for agentic-devtools. Without flags it prints a human-readable
dependency table and exits with code 0 (all required deps found) or 2 (a required
dep is missing). With `--fix` it additionally dispatches registered repair
factories (lazy-loaded callables that return repair functions); with `--json` it
emits a structured `SetupReport` JSON to stdout.
The command never installs anything unless a registered repair factory or the repair function it returns does so.

## Phases

The following phases execute in this order:

1. `check`
2. `repair:<dep-name>` *(one entry per dispatched repair; present only when `--fix` is passed and problems are detected)*

## Parameters & State Keys

### CLI Flags

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--fix` | boolean | `false` | Enable repair-dispatch mode. Invokes registered `RepairRegistry` factories to obtain and call repair functions. Report `mode` is `"check-fix"`. |
| `--json` | boolean | `false` | Emit the structured `SetupReport` as JSON to stdout instead of the human-readable dependency table. Combinable with `--fix`. |

### State Interaction

`agdt-setup-check` does not read or write workflow state (`_workflow` key)
and does not interact with `.agdt/workflows/` state files.

## Decision Points / Paths

| Decision | Condition | Path |
|----------|-----------|------|
| No flags | Neither `--fix` nor `--json` set | Print human-readable report → exit 0 (healthy) or 2 (missing required dep) |
| `--json` only | `--json` set, `--fix` not set | Run check, emit JSON with `mode="check"` → sys.exit with report exit code |
| `--fix` only | `--fix` set, `--json` not set | Run doctor (check + optional repair), print human-readable report + "OK" when healthy → sys.exit with report exit code |
| `--fix --json` | Both flags set | Run doctor, emit JSON with `mode="check-fix"` → sys.exit with report exit code |
| Healthy environment | No required deps missing | Short-circuit: emit OK, exit 0 (no repairs invoked even if `--fix`) |
| Missing required dep, no repair registered | `--fix` set, no repair in registry | Phase `repair:<dep>` status: `skipped`, exit 2 |
| Missing required dep, repair succeeds | `--fix` set, registered repair runs without exception | Phase `repair:<dep>` status: `success`, exit 0 |
| Missing required dep, repair raises | `--fix` set, repair throws an exception | Phase `repair:<dep>` status: `failed`, error captured, exit 2 |

## Execution Flow

1. Parse `--fix` / `--json` via `argparse.parse_args` (supports `-h/--help` and fails fast on unknown flags).
2. Call `check_all_dependencies()` to get `DependencyStatus` list (includes CLI tool checks,
   `path-profile` check for `~/.agdt/bin` in shell profile, and `git-hooks` check for
   `core.hooksPath` configuration; the git-hooks entry is shown as optional when git is
   unavailable or the current directory is not a git worktree; unreadable shell profiles
   are treated as a missing `path-profile` dependency instead of crashing the command; a
   superstring such as `~/.agdt/bin-tools` does not satisfy the `path-profile` check).
   A synthetic `ca-bundle` `DependencyStatus` is appended: found when
   `~/.agdt/certs/unified-ca-bundle.pem` exists as a file, is non-empty, and
   contains at least one `-----BEGIN CERTIFICATE-----` block; missing otherwise
   (filesystem/read errors such as `OSError` are treated as missing).
3. If neither flag is set → print human-readable report → sys.exit 2 if any required dep missing, else return normally.
4. If `--fix` or `--json` → register default repairs via `register_default_repairs()`, then call `run_doctor(statuses, fix=<bool>)`.
5. `run_doctor` classifies missing required deps: `path-profile` → `PATH_PROFILE_NOT_UPDATED`,
   `git-hooks` → `GIT_HOOKS_NOT_CONFIGURED`, `ca-bundle` → `CERT_CA_FETCH`,
   all others → `MISSING_DEPENDENCY`. Managed CLIs (`gh`, `copilot`) are classified as
   `MANAGED_CLI_MISSING` only when `--fix` is passed; in check-only mode they remain
   informational (not classified as problems).
6. If no problems found → short-circuit, return `DoctorResult` with `mode=<check|check-fix>`, exit 0.
7. If problems and `--fix` → for each problem (sorted by `ErrorClass` enum definition order,
   with same-class ties preserving check-phase discovery order) look up `RepairRegistry`,
   invoke registered factory to obtain repair function, then call repair function;
   or record `skipped` if no factory registered.
8. Repair functions return `True` (applied/mutated), `False` (no-op/already OK), or `None`
   (backward compatible, treated as applied). The `RepairOutcome.applied` field tracks this.
9. Catch all exceptions from factory invocation or repair functions; record as `failed` phase, continue.
10. Determine final exit code: all repairs succeeded → 0; otherwise 2.
11. Build `SetupReport` with correct `mode`.
12. If `--json` → emit `json.dumps(report.to_dict())` to stdout.
13. Else → print human-readable dependency report.
14. `sys.exit(report.exit_code)`.

## Registered Repairs

The following repairs are registered by `register_default_repairs()` (lazy-loaded):

| ErrorClass | Repair | Behavior |
|------------|--------|----------|
| `PATH_PROFILE_NOT_UPDATED` | `repair_path_profile` | Appends `export PATH="<absolute-path>:$PATH"` (absolute home directory, not `~`) to the user's shell profile. Idempotent: returns `False` only when the exact managed path is already present as a PATH component. |
| `GIT_HOOKS_NOT_CONFIGURED` | `repair_git_hooks` | Sets `git config core.hooksPath .githooks` and ensures `.githooks/` dir exists. Post-verifies via `git config --get core.hooksPath` that the value is actually `.githooks`. Idempotent: returns `False` if already configured. |
| `CERT_CA_FETCH` | `repair_certs` | Fetches TLS certificates for setup hosts (and npm registry when npm is detected), atomically rebuilds `~/.agdt/certs/unified-ca-bundle.pem` via `_build_unified_ca_bundle`, and atomically updates `~/.agdt/npmrc` with `cafile=<path>` when npm is enabled. Respects npm-conditionality: never writes `~/.agdt/npmrc` in non-npm repositories. Enforces a 60-second total timeout across all cert fetches, and reports npmrc write/rename failures as contextual `RuntimeError`s instead of leaking raw `OSError`s. |
| `MANAGED_CLI_MISSING` | `repair_managed_cli` | Installs a missing managed CLI (`gh` or `copilot`) via `install_gh_cli` or `install_copilot_cli`. Only classified as a problem in `--fix` mode (check-only mode treats missing managed CLIs as informational). Raises `RuntimeError` on installation failure. |

### `RepairOutcome.applied` semantics

`RepairOutcome.applied` is `True` when the repair function returned `True` or `None` (backward
compat), indicating it *attempted* a filesystem or state change — regardless of whether the
dependency is subsequently healthy. It is `False` when the repair returned `False` (no-op,
dependency was already OK), raised an exception, or no factory was
registered for the error class. A destructive repair that partially succeeded (some paths deleted,
others failed) will have `applied=True` and `success=False`.

### git-hooks non-blocking detection

`_check_git_hooks_config()` returns `required=False` (non-blocking) in all of the following cases:
git binary not available, subprocess timeout, not inside a git repository, git binary disappears
during the `--show-toplevel` probe, or git binary disappears during the `core.hooksPath` probe.
Only when git is available, the working directory is inside a git repository, and the configuration
probe completes is the result `required=True`.

## Observable Side Effects

### Stdout

| Mode | Output |
|------|--------|
| No flags | Human-readable dependency table (icons, versions, paths, install hints). |
| `--fix` (no `--json`) | Human-readable dependency table; per-repair summary lines (`<name>: fixed`, `<name>: ok (no-op)`, or `<name>: failed (<message>)`); "OK" printed when environment is healthy. |
| `--json` | JSON-serialised `SetupReport` (schema version 1). |
| `--fix --json` | JSON-serialised `SetupReport` with `"mode": "check-fix"`. |

### File System

`agdt-setup-check` does not write any files by default. Registered repair
functions (added by sibling subtasks) may write files; their side effects are
their own responsibility.

### Environment Variables

`agdt-setup-check` does not modify environment variables.

## Exit Codes

| Code | Name | Description |
|------|------|-------------|
| 0 | OK | All required deps present (or all detected problems repaired successfully). |
| 1 | WARNINGS | (Unused by this command — reserved.) |
| 2 | MISSING_REQUIRED_DEP | A required dependency is missing and was not repaired. |
| 3 | VERSION_BLOCKED | (Unused by this command — reserved.) |
| 4 | UPGRADED_RERUN_NEEDED | (Unused by this command — reserved.) |
| 5 | REPO_MUTATION_FAILED | (Unused by this command — reserved.) |
| 6 | AUTORUN_FAILED | (Unused by this command — reserved.) |

## Report Schema

When `--json` is passed, `agdt-setup-check` prints a `SetupReport.to_dict()`
JSON object to stdout. The schema is identical to `agdt-setup` (schema version 1)
and conforms to the same consumer contract as `last-setup-report.json`.
The JSON report is always emitted when `--json` is present, regardless of
whether problems were detected. When `--fix` is passed without `--json`,
the human-readable dependency table is printed, followed by one summary line
per dispatched repair (`<name>: fixed | ok (no-op) | failed (<message>)`),
and `OK` when no problems remain. Exit code communicates status to the shell;
`--json` requests structured detail on stdout.

Key fields:

| Field | Type | Description |
|-------|------|-------------|
| `schema_version` | int | Always `1`. |
| `mode` | string | `"check"` (no `--fix`) or `"check-fix"` (with `--fix`). |
| `exit_code` | int | 0 = OK, 2 = MISSING_REQUIRED_DEP. |
| `exit_code_name` | string | Symbolic name for `exit_code`. |
| `phases` | array | One entry per phase executed (name, status, duration_ms, error). |
| `timestamp` | string | ISO-8601 UTC timestamp of the invocation. |

## Decision Flowchart

```mermaid
flowchart TD
    A[agdt-setup-check invoked] --> B{--fix or --json?}
    B -- No --> C[check_all_dependencies]
    C --> D{required dep missing?}
    D -- No --> E[Print table, exit 0]
    D -- Yes --> F[Print table, exit 2]

    B -- Yes --> G[run_doctor]
    G --> H{problems detected?}
    H -- No --> I[emit OK report, exit 0]
    H -- Yes --> J{--fix?}
    J -- No --> K[Report problems, exit 2]
    J -- Yes --> L[For each problem sorted by ErrorClass enum order: lookup RepairRegistry]
    L --> M{factory registered?}
    M -- No --> N[Phase: skipped]
    M -- Yes --> O{factory + repair succeeds?}
    O -- Yes --> P[Phase: success]
    O -- No, exception --> Q[Phase: failed, capture error]
    N --> R[Determine final exit code]
    P --> R
    Q --> R
    R --> S{--json?}
    S -- Yes --> T[Print JSON report]
    S -- No --> U[Print human-readable table]
    T --> V[sys.exit report.exit_code]
    U --> V
```

## Success Criteria

- `agdt-setup-check` without flags produces identical output and exit codes to
  the pre-#2321 implementation (backward compatibility).
- `agdt-setup-check --fix` in a healthy environment exits 0 and does not invoke
  any repair functions.
- `agdt-setup-check --json` always emits valid JSON to stdout parseable by
  `json.loads()`, regardless of exit code or environment state.
- `agdt-setup-check --fix --json` emits JSON with `"mode": "check-fix"`.
- An unreadable detected shell profile does not crash the command; it is reported as a
  missing required `path-profile` dependency.
- A repair factory registered in `RepairRegistry` for `ErrorClass.MISSING_DEPENDENCY`
  is invoked exactly once per missing required dep when `--fix` is passed, and the
  returned repair function is called exactly once.
- When multiple error classes are detected, repairs are dispatched in `ErrorClass`
  enum definition order; same-class ties preserve check-phase discovery order.
- Repair function or factory exceptions are caught, recorded in the report, and do
  not crash the command.
- `agdt-setup-check --fix --json` with a missing required dep always includes at least
  one `repair:<dep-name>` phase in the JSON `phases` array (status `skipped`, `success`,
  or `failed`).

## Corrupted Install Artifacts Repair (#2322)

`agdt-setup-check` now detects corrupted `agentic-devtools` install artifacts
(tilde-prefixed directories, dist-info without RECORD, editable .pth files)
in all Python site-packages directories.

### Behavior by mode

| Mode | Behavior |
|------|----------|
| Plain (no flags) | Prints a grouped warning to stderr listing detected artifacts. Does NOT append to the dependency table, does NOT dispatch repair. |
| `--fix` | Appends a pseudo-dependency status (`corrupted-install-artifacts`) to statuses, dispatches the `STALE_PARTIAL_INSTALL` repair factory. The repair is DESTRUCTIVE (rmtree/unlink) and gated behind interactive TTY confirmation. |
| `--json` (without `--fix`) | Appends the pseudo-status and classifies it, but does not dispatch repair. |
| `--fix --json` | Full repair dispatch with JSON report output. |

### Repair confirmation gate

The repair is labeled DESTRUCTIVE. Before deletion:

1. A grouped preview of artifacts (by parent site-packages directory) is printed to stderr.
2. If non-interactive (no TTY), the repair is declined automatically.
3. If interactive, the user must type `y` or `yes` to proceed.

### Exit codes

- Healthy: no impact on exit code.
- Corrupted + `--fix` + confirmed + all deleted: exit 0.
- Corrupted + `--fix` + declined: exit 2 (repair failed — UserDeclinedRepairError).
- Corrupted + `--fix` + partial failure: exit 2 (some artifacts could not be removed).

### Safety invariants

- Only removes paths explicitly returned by `detect_corrupted_artifacts()`.
- Never touches `~/.agdt/` user data: `repair_corrupted_artifacts()` resolves every artifact
  path and raises `ValueError` immediately if any of them falls inside `~/.agdt/`, regardless
  of how the entry got into `repair_details`.
- `repair_details["detected_artifacts"]` is validated before use: raises `TypeError` if the
  value is not a `list`, and raises `TypeError` for any list entry that is not `str` or `Path`.
  Fails fast before any confirmation prompt or deletion attempt.
- `detect_corrupted_artifacts()` is called exactly once per invocation (NFR-003): during
  `check_corrupted_artifacts_status()`, which stores the result in
  `DependencyStatus.repair_details["detected_artifacts"]` as `list[str]` (JSON-serializable).
  The `repair_corrupted_artifacts()` function converts each entry back to `Path` and reuses
  that cached list, falling back to a fresh scan only when the key is absent.
- `repair_details` values are always JSON-serializable (`str`, `list[str]`, `list[dict]`)
  so `agdt-setup-check --fix --json` never crashes at `json.dumps(report.to_dict())`.
- `failed_artifacts` values in `repair_details` are normalized before formatting: a non-list truthy
  value is treated as a single entry, dict entries use `.get("path")` / `.get("error")`, and any
  non-dict entry falls back to `str()` so the error summary is robust against malformed extension
  data.
- Exception-based repair outcomes also copy `DependencyStatus.repair_details` into
  `RepairOutcome.details`, so structured context (for example `detected_artifacts`) is still
  available in `report.details` even when a repair raises.
