# agdt-setup — Expectations Document

This document describes the observable behavior of `agdt-setup`. It is the
single source of truth for phases and exit codes. A sync-gate validator
enforces that the phase list and exit-code table stay consistent with the
source code, and presence-checks the Report Schema section and Mermaid
diagram. Report schema field definitions are maintained by convention.

## TL;DR

`agdt-setup` is a one-shot setup orchestrator that prepares a development
environment for agentic-devtools usage. It installs managed CLI tools (GitHub
CLI, Copilot CLI), prefetches CA certificate bundles for corporate proxies,
verifies required dependencies, persists environment variables to the shell
profile, and applies repository file modifications (gitignore rules, platform
configuration, agent/prompt skill injection, workflow templates, and setup
scripts). After successful argument parsing, setup-controlled terminal paths
write a structured JSON report to `~/.agdt/last-setup-report.json` and exit
with a deterministic exit code from a stable 0–6 taxonomy.

## Phases

The following phases execute in this order:

1. `version_check`
2. `certificate_prefetch`
3. `cli_installation`
4. `dependency_check`
5. `environment_persistence`
6. `file_modifications`
7. `autorun_setup`

## Parameters & State Keys

### CLI Flags

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--system-only` | boolean | `false` | Skip managed installs (cert prefetch + CLI installation) and the branch/PR workflow for repo file changes; dependency checks and repo file modifications (`file_modifications` phase) still run when inside a git repository. Also suppresses shell-profile env-var persistence (same effect as `--no-persist-env`). |
| `--no-verify-ssl` | boolean | `false` | Disable SSL certificate verification (insecure; use only on trusted networks). |
| `--no-persist-env` | boolean | `false` | Do not persist environment variables to shell profile. |
| `--overwrite-env` | boolean | `false` | Overwrite existing environment variable lines in shell profile. |
| `--skip-platform-detection` | boolean | `false` | Skip automatic platform detection step. |
| `--issue-adapter` | choice | `null` | Override the issue-adapter axis for skill filtering. Code hosting detection still runs independently unless `--skip-platform-detection` is also supplied. |
| `--skip-templates` | boolean | `false` | Skip workflow template generation and commit template creation/validation. |
| `--reconfigure` | boolean | `false` | Re-prompt for all project configuration values and Copilot model selection, even if already set. |
| `--skip-pr-workflow` | boolean | `false` | Skip the automatic branch/PR workflow for repo file changes. |
| `--force-old-version` | boolean | `false` | Bypass the version guard when installed version is older than the project pin. Repo file modifications are skipped in that case. |
| `--npm` | boolean | `false` | Force npm certificate and configuration work regardless of npm footprint. Mutually exclusive with `--no-npm`. |
| `--no-npm` | boolean | `false` | Skip npm certificate and configuration work regardless of npm footprint. Mutually exclusive with `--npm`. |
| `--run` | boolean or null | `null` | Resolve `autorun_enabled` as `True`, overriding CI/TTY detection and the `AGDT_SETUP_RUN`/`AGDT_SETUP_NO_AUTORUN` environment variables. Mutually exclusive with `--no-run`. |
| `--no-run` | boolean or null | `null` | Resolve `autorun_enabled` as `False`, overriding CI/TTY detection and the `AGDT_SETUP_RUN`/`AGDT_SETUP_NO_AUTORUN` environment variables. Mutually exclusive with `--run`. |
| `--refresh-issue-types` | boolean | `false` | Force re-discovery of issue types from the configured provider without full reconfiguration. When used standalone (without `--reconfigure`), runs immediately after git-root/version-guard resolution and exits early after discovery (skipping cert/CLI/dependency/profile setup phases and recording phases 2–6 as `skipped` in the setup report). Respects `--skip-platform-detection` precedence in both live and `--dry-run` mode; in dry-run, suppressed cases are previewed as skipped instead of claiming a refresh would run. |
| `--dry-run` | boolean | `false` | Preview what setup would do without making any changes. All mutator phases (including `autorun_setup`) are recorded as `skipped`, "would …" messages are printed for each gated action, `report.mode` is set to `dry-run`, and the command exits `0` (unless `VERSION_BLOCKED`). For the environment phase, when persistence is disabled (`--no-persist-env` or `--system-only`) dry-run previews the same manual-instructions path instead of printing a persistence preview. The certificate preview still returns the planned bundle / npmrc paths so that manual export guidance matches a real run. In `--force-old-version` mode, repo-mutating dry-run previews are explicitly marked as skipped to match live behavior. |

Standalone setup subcommands (`agdt-setup-copilot-cli`, `agdt-setup-gh-cli`,
and `agdt-setup-certs`) also support `--dry-run` as a preview-only mode.

### Environment Variables

| Variable | Description |
|----------|-------------|
| `AGDT_SETUP_REPORT_PATH` | Override the default report output path (`~/.agdt/last-setup-report.json`). |
| `AGDT_NO_VERIFY_SSL` | Set to `"1"` to disable SSL verification globally (equivalent to `--no-verify-ssl`). |
| `AGENTIC_DEVTOOLS_STATE_DIR` | Override the state directory used by agdt commands. |
| `AGDT_SETUP_RUN` | Set to a truthy value (`"1"`, `"true"`, `"yes"`, case-insensitive) to resolve `autorun_enabled` as `True`. Takes effect when no `--run`/`--no-run` CLI flag is passed and `AGDT_SETUP_NO_AUTORUN` is not truthy. Overrides CI/non-TTY detection. |
| `AGDT_SETUP_NO_AUTORUN` | Set to a truthy value (`"1"`, `"true"`, `"yes"`, case-insensitive) to resolve `autorun_enabled` as `False`. Takes effect when no `--run`/`--no-run` CLI flag is passed. Takes precedence over `AGDT_SETUP_RUN`. |
| `AGDT_SETUP_AUTORUN` | Child-only recursion marker. Set to `"1"` in the child environment when `_autorun_setup_dev_tools` invokes the generated `setup-dev-tools.py`. If present in the parent environment, the autorun phase is skipped (recursion guard). Never set in the parent process. |

### State Interaction

`agdt-setup` is **not** a workflow command — it does not use or modify
workflow state (`_workflow` key). However, when the PR workflow path is active
(repo file changes are committed via a branch/PR), the command temporarily
writes `source_branch`, `title`, and `draft` to state, then restores the
original values after the PR creation attempt (in a `finally` block, so the restore
happens regardless of whether PR creation succeeds or fails). The `pull_request_id`
key may be persisted if a PR is successfully created.

## Decision Points / Paths

| Decision | Condition | Path |
|----------|-----------|------|
| Version guard — block | Installed version < project pin and `--force-old-version` not set | Write report → exit 3 (`VERSION_BLOCKED`) |
| Version guard — force | Installed version < project pin but `--force-old-version` set | Skip repo file modification steps; continue setup |
| Version guard — allow | Installed version ≥ project pin (or no pin defined) | Continue normally |
| `--system-only` | Flag set | Skip `certificate_prefetch` and `cli_installation` phases (marked `skipped`) |
| Not in a git repository | `_get_git_repo_root()` returns `None` | Skip repo file modification steps; record `file_modifications` as `skipped`; continue to determine final exit code |
| Missing required dep | After `environment_persistence`: a required dependency is not found | Skip `file_modifications` (marked `skipped`) → write report → exit 2 (`MISSING_REQUIRED_DEP`) |
| File modification exception | An exception during `file_modifications` phase | Write report → exit 5 (`REPO_MUTATION_FAILED`) |
| Unhandled exception | Any uncaught exception during setup | Write report → exit 6 (`AUTORUN_FAILED`) |
| Warnings present | Setup completes but optional checks produced warnings | Write report → exit 1 (`WARNINGS`) |
| All OK | All phases succeed with no warnings | Write report → exit 0 (`OK`) |

### Skill Injection Classification Filter

`inject_skills` supports a context-aware classification filter that constrains which
`agdt.*` agent/prompt files are injected, based on per-file YAML frontmatter
(`agdt.requires` block) and caller-supplied platform values.

> **Current `agdt-setup` behavior:** during Phase 6 (`file_modifications`), `agdt-setup`
> first resolves the platform configuration via detection/override/skip, persists it via
> `save_platform_config`, and **then** resolves the injection axes and forwards them to
> `inject_skills_with_summary(git_root, issue_adapter=…, code_hosting=…)`.
>
> **Ordering guarantee:** platform resolution → config persistence → skill injection.
> This ensures that `inject_skills_with_summary` always receives the current-run's
> detected/configured axes rather than stale values from a prior run.
> *Applies when platform setup is active, including `--issue-adapter`
> overrides (even with `--skip-platform-detection`). On the pure-skip path
> (`--skip-platform-detection` without `--issue-adapter`), detection and
> persistence are omitted and injection falls through to inject-all directly.*
>
> **Axis resolution rules:** only confidently-resolved, filter-capable values activate
> an axis (`jira`/`github` `issue_adapter`, `github`/`azure_devops` `code_hosting`);
> every other value — a non-filter-capable catch-all (`markdown`/`other`), an absent
> key, or a missing/malformed config — leaves that axis unrestricted (`None`).
> When FR-003 fallback conditions are not active, `agdt-setup` resolves injection axes
> from fresh detection first. Persisted `code_hosting` is only used when detection
> cannot determine a host in the current run. For `issue_adapter`, an existing
> authoritative configured value can still prevent detection from replacing that
> axis for injection; `markdown` still maps to `None` (unrestricted) because it is
> not filter-capable.
> **`--issue-adapter` override + hosting detection:** on the explicit
> `--issue-adapter` path, hosting detection still runs independently. If detection
> returns a host, that fresh host is persisted and used for injection. If detection
> finds no host, `code_hosting` is reset to the catch-all (`DEFAULT_CODE_HOSTING`)
> and the hosting axis stays unrestricted (`None`).
>
> **Three-condition inject-all fallback:** when any of the following hold, both axes
> are set to `(None, None)` and injection is byte-identical to the legacy inject-all:
>
> 1. `--skip-platform-detection` is active
> 2. Platform setup failed (detection exception **or** platform-config save failure)
> 3. Non-interactive session (no TTY on stdin)
>
> **`--issue-adapter` override:** when `--issue-adapter` is given, code hosting detection
> still runs independently so the hosting axis can be resolved. If hosting remains
> undetected and filtering is active (i.e., the adapter axis is resolved), a warning is
> emitted: `⚠ Code hosting could not be detected — hosting axis unrestricted for skill
> filtering`.
>
> After a successful injection, setup prints a summary line reporting the number of files
> injected and (when a filter was applied) pruned, plus the resolved axes — for example
> `✓ Injected 145 agent/prompt skills, pruned 112 (issue_adapter=github, code_hosting=github)`,
> or `✓ Injected 257 agent/prompt skills (no platform filter applied)` for the inject-all
> fallback. When one axis is resolved and the other is not, the unresolved axis renders
> as `unrestricted` — for example
> `✓ Injected 145 agent/prompt skills, pruned 50 (issue_adapter=github, code_hosting=unrestricted)`.

| Platform State | Behavior |
|----------------|----------|
| Both `issue_adapter` and `code_hosting` resolved (non-None) | Full two-axis AND filtering: a file is injected only if its `requires.issue_adapter` matches (or is absent) AND its `requires.code_hosting` matches (or is absent). |
| Single axis resolved (one non-None, one None) | Only the resolved axis constrains injection; the unresolved axis is unrestricted (all files pass on that axis). |
| Both None (default/legacy) | All `agdt.*` files are injected — backward-compatible behavior identical to pre-filter releases. No frontmatter reads occur during the filter phase. |

**Additional rules:**

- Files with `agdt.always: true` in frontmatter are always injected regardless of platform values.
- Files with no `agdt` or no `requires` block are treated as universal (always injected).
- Files that raise `UnicodeDecodeError` during the filter phase are treated as injectable.
- Malformed YAML frontmatter results in universal classification (file is kept).
- The existing stale-cleanup loop (removes `agdt.*` files not in the current source set) handles pruning of previously-injected files that no longer pass the filter.
- Non-`agdt.*` user files and `agdt.README.md` are never removed by the filter or stale-cleanup.
- **Argument normalization:** empty strings and whitespace-only `issue_adapter`/`code_hosting` values are
  silently normalized to `None` (unresolved / inject-all for that axis). Unknown values (not in the valid
  allowlists) emit a `RuntimeWarning` and are also normalized to `None`, preserving the
  "unresolved ⇒ inject-all" contract.

## Execution Flow

1. Parse CLI arguments (argparse). If invalid → argparse `SystemExit` (no report written).
2. Initialize `SetupReport` with default exit code `AUTORUN_FAILED` (safety net).
3. If `--no-verify-ssl` → set `AGDT_NO_VERIFY_SSL=1` in process environment.
4. **Phase 1 — `version_check`**: Run version guard (`check_version_guard`).
   - If `"block"` → finalize report with exit 3, write, exit.
   - If `"force"` → set `skip_repo_steps = True`.
5. If `--system-only` → record phases 2–3 as `skipped`, skip to step 8.
6. **Phase 2 — `certificate_prefetch`**: Fetch CA bundles for configured hosts, build unified bundle, optionally write `.npmrc`.
   Produced artifacts (unified bundle / `.npmrc`) are then recorded in `~/.agdt/registry.json` — an append-only,
   content-addressed reference index that tracks which repository contexts installed which shared artifacts
   when a Git repository root is available (not written under `--dry-run`) — see File System side effects (FR-002, partial).
7. **Phase 3 — `cli_installation`**: Install Copilot CLI and GitHub CLI to `~/.agdt/bin/`.
8. **Phase 4 — `dependency_check`**: Check all required and optional dependencies, print report.
9. **Phase 5 — `environment_persistence`**: Persist `PATH` and `REQUESTS_CA_BUNDLE` to shell profile (unless `--no-persist-env` or `--system-only`).
   `NODE_EXTRA_CA_CERTS` and `NPM_CONFIG_USERCONFIG` are also persisted when npm work is enabled (`npm_enabled` is true).
10. Check for missing required dependencies. If any missing → record phase 6 as `skipped`, write report with exit 2, exit.
11. **Phase 6 — `file_modifications`**: Apply repository file changes
    (gitignore, skill injection, platform config, workflow templates, setup scripts, PR workflow).
    Platform configuration is resolved and persisted **before** skill injection when platform
    detection or override is active. Pure `--skip-platform-detection` (without `--issue-adapter`)
    bypasses both detection and persistence, forwarding `(None, None)` to injection (inject-all).
    The combined `--skip-platform-detection --issue-adapter <value>` path still persists the
    explicit adapter override, but injection remains `(None, None)` for this run. `--system-only`
    also skips detection/persistence and loads any previously persisted config; however, in a
    non-interactive (no-TTY) run `_resolve_injection_axes` returns `(None, None)` regardless,
    so injection still receives an inject-all fallback. In interactive `--system-only` runs,
    the previously persisted axes are forwarded to injection when the earlier setup run
    recorded `platform.issue_adapter_resolved: true`, meaning the issue-adapter axis was
    genuinely resolved (detected or explicitly chosen). A persisted
    `platform.issue_adapter_resolved: false` means the stored `platform.issue_adapter`
    value is only the default fallback and must not activate Jira-only filtering. A
    **legacy config where the marker is absent entirely** treats non-default adapters as
    independently authoritative, but markerless `platform.issue_adapter: "jira"` remains
    ambiguous (older setup runs persisted this as a generated fallback) and therefore stays
    unresolved unless another explicit resolved signal exists. A
    **present but malformed** `platform.issue_adapter_resolved` value (for example
    `"false"` or `null`) is never authoritative, including the unattended preserve-existing
    branch when detection finds no issue platform.
    During Jira detection (`_detect_jira`), the `explicit_jira_adapter` config signal
    is authoritative when the raw adapter is `"jira"` **and** the
    `issue_adapter_resolved` marker is authoritative under the migration-compatible
    rule: an **absent** marker (legacy pre-PR config) or an explicit `True` both count
    as authoritative, so an existing markerless `"jira"` config is detected (medium
    confidence) rather than silently overwritten; an explicit `False` **or** a
    present-but-malformed (non-boolean, e.g. `"false"` or `null`) marker is
    non-authoritative and does not count as an explicit Jira config.
    This detection rule intentionally differs from the `--system-only` path, which
    reuses persisted axes directly without re-running detection: that path treats a
    markerless default `"jira"` adapter as non-authoritative (older setup runs
    persisted it as a generated fallback with no explicit user intent), whereas
    active detection preserves the existing config for migration compatibility.
    `inject_skills_with_summary` therefore receives the current-run's detected/configured axes
    (interactive paths with platform setup active) or `(None, None)` (skip/no-TTY/system-only-non-interactive)
    (see [Skill Injection Classification Filter](#skill-injection-classification-filter)).
    After skill injection, issue type discovery is attempted (non-fatal):
    calls the adapter's `get_issue_types()`, trims surrounding whitespace from persisted
    issue type names/ids, and writes the results to `.agdt/config/project.json`.
    After type discovery, **property discovery** is attempted for each discovered type:
    calls the adapter's `get_type_properties(type_name)` per type, maps results to
    `PropertyEntry` records with `included_in_template=True` default, and persists them
    in the `properties` array of each `IssueTypeEntry`. Property discovery handles partial
    failures gracefully (per-type warning to stderr, other types continue). On refresh,
    existing `included_in_template` values are preserved for matching property names.
    `NotImplementedError` from the adapter triggers an immediate short-circuit (debug log,
    no warning). The `lastRefreshed` timestamp is updated only when at least one type's
    properties were successfully fetched.
    Skipped when `--skip-platform-detection` is active or platform config save failed.
    Timing and status for this phase are recorded explicitly rather than via `_PhaseTracker`, so the phase is always present in the report even if an exception fires mid-phase.
12. **Phase 7 — `autorun_setup`**: Invoke the generated `setup-dev-tools.py --foreground` as a
    child process with `AGDT_SETUP_AUTORUN=1` in the child environment. Skipped when: the
    recursion marker is already present (recursion guard), `autorun_enabled` is false,
    `git_root` is None, `--system-only` is set, `skip_repo_steps` is set, `branch_created`
    is a non-empty string (indicating the PR workflow committed changes to a review branch),
    or the root entry-point is missing/legacy. When the skip is caused by `branch_created`,
    the `PhaseResult` error contains the branch name and the remediation phrase
    "review & merge the setup PR, then run \`python setup-dev-tools.py\`".
    On failure, the error is recorded in the report but
    does not change the overall exit code (non-fatal).
13. Determine final exit code: warnings present → exit 1; all OK → exit 0.
14. Write report and exit.

Phases 1–5 are wrapped in a `_PhaseTracker` context manager that records
`PhaseResult` entries (name, status, duration\_ms, error) into the report
automatically on both success and exception paths. Phase 6
(`file_modifications`) is recorded explicitly for the same schema, but outside
that context manager.

## Observable Side Effects

### File System (`~/.agdt/`)

| Path | Description |
|------|-------------|
| `~/.agdt/bin/` | Managed CLI binaries (Copilot CLI, GitHub CLI). |
| `~/.agdt/certs/` | Per-host PEM files and `unified-ca-bundle.pem`. |
| `~/.agdt/npmrc` | npm configuration pointing to the unified CA bundle. Written only when npm work is enabled (`npm_enabled` is true). |
| `~/.agdt/registry.json` | Append-only, content-addressed reference index recording which repository *contexts* installed which shared artifacts (cert bundle, `npmrc`), keyed by content hash. Written after the `certificate_prefetch` phase when at least one artifact was produced **and** a Git repository root is available. Registering a context only ever *appends* it to an artifact's `referenced_by` list and never removes another repository's reference, so the registry itself is non-clobbering. Removing a context (`deregister_context`) is a true no-op when the context is absent — the file is not rewritten and its mtime is not updated. Note this tracks references only — it does **not** yet store each artifact at a hash-derived path, so a differing rebuild of a singleton file (e.g. `unified-ca-bundle.pem`) still overwrites the previous bytes in place; hash-derived artifact storage and repo-scoped shell-profile block markers (spec tasks T038–T039) remain follow-up work, so FR-002 is only partially delivered. Registration is best-effort — a registry failure prints a warning and never fails setup. Not written under `--dry-run`. |
| `~/.agdt/registry.json.lock` | Sidecar lock file guarding the registry read-modify-write cycle so concurrent setups in different repositories do not race. |
| `~/.agdt/last-setup-report.json` | Structured JSON report (overridable via `AGDT_SETUP_REPORT_PATH`). |
| Shell profile (e.g., `~/.bashrc`) | `PATH` is persisted idempotently when `environment_persistence` runs **and** neither `--no-persist-env` nor `--system-only` is set: entries are appended/replaced only when missing (or when `--overwrite-env` is used), otherwise left unchanged. `REQUESTS_CA_BUNDLE` follows the same persistence gating **and** is additionally conditional on the unified CA bundle having been built successfully (`unified_path` is not `None`); if bundle generation fails (e.g., certifi unavailable), the variable is not written to the profile. When `--no-persist-env` or `--system-only` is passed, all profile writes are suppressed and setup prints manual instructions instead. If profile detection fails for any other reason, setup also falls back to printing manual instructions. `NODE_EXTRA_CA_CERTS` (also conditional on `unified_path`) and `NPM_CONFIG_USERCONFIG` follow the same persistence behavior, but only when npm work is enabled. |

### Repository Files

| Path | Description |
|------|-------------|
| `.agdt/.gitignore` | Ensures agdt state directories are git-ignored. |
| `.agdt/config/project.json` | Project configuration (issue adapter, platform settings). |
| `.agdt/config/commit-template.j2` | Jinja2 commit message template. |
| `.agdt/config/pr-title-template.j2` | PR title template. |
| `.agdt/config/pull-request-template.j2` | PR description template. |
| `.agdt/workflow-definitions/README.md` | Workflow definitions documentation. |
| `.agdt/workflow-definitions/work-on-issue.py` | Work-on-issue workflow definition (fully autonomous; no `human_approved` field in state schema per #1898). |
| `.agdt/workflow-definitions/review-pr.py` | PR review workflow definition. |
| `.github/agdt-config.json` | Repository-level agdt configuration. |
| `.github/agents/` | Injected Copilot Chat agent definitions. |
| `.github/prompts/` | Injected Copilot Chat prompt files. |

### Generated Setup Scripts

| Path | Description |
|------|-------------|
| `.agdt/agentic-devtools-required-setup.py` | Minimal required setup script. |
| `.agdt/agentic-devtools-configured-setup.py` | Configured setup with project-specific options. |
| `.agdt/agentic-devtools-complete-setup.py` | Full setup script with all options. |
| `setup-dev-tools.py` | Developer tools setup (repo root). |
| `setup-repo-specific-dev-tools.py` | Repo-specific developer tools setup (repo root). |

### Environment Variables (Set During Execution)

| Variable | Condition |
|----------|-----------|
| `AGDT_NO_VERIFY_SSL` | Set to `"1"` when `--no-verify-ssl` is passed. |
| `REQUESTS_CA_BUNDLE` | Pointed at `~/.agdt/certs/unified-ca-bundle.pem` after cert prefetch. Only set when the unified CA bundle was built successfully **and** the variable is not already defined in the calling environment. |
| `NODE_EXTRA_CA_CERTS` | Pointed at the unified CA bundle for Node.js processes. Only set when npm work is enabled, the unified CA bundle was built, **and** the variable is not already defined in the calling environment. |
| `NPM_CONFIG_USERCONFIG` | Pointed at `~/.agdt/npmrc`. Only set when npm work is enabled, the `~/.agdt/npmrc` file was written, **and** the variable is not already defined in the calling environment. |

## Exit Codes

| Code | Name | Description |
|------|------|-------------|
| 0 | OK | Setup completed successfully. |
| 1 | WARNINGS | Setup completed but one or more optional checks produced warnings. |
| 2 | MISSING_REQUIRED_DEP | A required dependency is missing. |
| 3 | VERSION_BLOCKED | Running version is older than the project-pinned minimum. |
| 4 | UPGRADED_RERUN_NEEDED | Package version changed; a re-run is required. Emitted when the autorun phase changed the installed package version and a re-run is needed to pick up the new version. |
| 5 | REPO_MUTATION_FAILED | A repository file-modification step failed. |
| 6 | AUTORUN_FAILED | An unexpected internal error occurred. |

## Report Schema

Once argument parsing succeeds, `agdt-setup` writes a JSON report to
`~/.agdt/last-setup-report.json` on every terminal path (success, warnings,
VERSION_BLOCKED, and unhandled exceptions). Callers should not expect a report
when the command exits due to invalid CLI arguments (argparse `SystemExit`).
The report uses atomic writes (tempfile + `os.replace`) so readers never
observe a partial file.

The output path can be overridden via the `AGDT_SETUP_REPORT_PATH` environment
variable (useful for tests and CI). See also
[docs/setup-report.md](../setup-report.md) for the consumer-facing entry point.

```json
{
  "schema_version": 1,
  "mode": "setup",
  "git_root": "/home/user/project",
  "timestamp": "2026-01-15T10:30:00+00:00",
  "exit_code": 0,
  "exit_code_name": "OK",
  "autorun_enabled": true,
  "total_phases": 7,
  "passed": 7,
  "failed": 0,
  "skipped": 0,
  "phases": [
    {
      "name": "version_check",
      "status": "success",
      "duration_ms": 12,
      "error": null
    }
  ],
  "details": {}
}
```

### Field Reference

| Field | Type | Description |
|-------|------|-------------|
| `schema_version` | integer | Schema version (currently `1`). |
| `mode` | string | Execution mode (see Mode Values below). |
| `git_root` | string or null | Absolute path to git repo root, or `null` if not in a repo. |
| `timestamp` | string | ISO-8601 UTC timestamp of report generation. |
| `exit_code` | integer | Numeric exit code from the taxonomy. |
| `exit_code_name` | string | Named constant for the exit code. |
| `autorun_enabled` | boolean or null | Computed flag indicating whether auto-run is requested. Resolved from CLI flags (`--run`/`--no-run`), environment variables (`AGDT_SETUP_RUN`/`AGDT_SETUP_NO_AUTORUN`), and CI/TTY detection. Does not trigger any execution in the current release. `null` when not computed (e.g., non-setup modes). |
| `total_phases` | integer | Number of recorded phases (derived; not a dataclass field). |
| `passed` | integer | Phases with `status == "success"` (derived; not a dataclass field). |
| `failed` | integer | Phases with `status == "failed"` (derived; not a dataclass field). |
| `skipped` | integer | Phases with `status == "skipped"` (derived; not a dataclass field). |
| `phases` | array | Ordered list of `PhaseResult` objects. |
| `phases[].name` | string | Phase name (matches the Phases list above). |
| `phases[].status` | string | One of `success`, `failed`, `skipped`. |
| `phases[].duration_ms` | integer | Wall-clock duration in milliseconds (`0` when unavailable). |
| `phases[].error` | string or null | Error message if status is `failed`. |
| `details` | object | Additional context (varies by exit code). |

### Mode Values

| Mode | Description |
|------|-------------|
| `setup` | Full setup execution (default). |
| `check` | Read-only validation — no mutations. |
| `check-fix` | Validation with auto-fix of recoverable issues. |
| `dry-run` | Preview mode — reports what would happen without executing. |

### Last-Writer-Wins Limitation

When multiple concurrent `agdt-setup` invocations target the same report path,
the last writer wins. Each individual write is atomic (no partial files), but
there is no inter-process locking. Consumers reading the report immediately
after a setup run should verify the `timestamp` field matches the expected
invocation.

## Decision Flowchart

```mermaid
flowchart TD
    A[agdt-setup invoked] --> B{Version guard}
    B -->|blocked| C[Write report exit_code=3]
    C --> D[Exit 3: VERSION_BLOCKED]
    B -->|force| E[Set skip_repo_steps=true]
    B -->|allow| F[Continue]
    E --> F
    F --> SYS{--system-only?}
    SYS -->|yes| SKIP23[Mark phases 2-3 skipped]
    SKIP23 --> I[Dependency check]
    SYS -->|no| G[Certificate prefetch]
    G --> H[CLI installation]
    H --> I
    I --> J[Environment persistence]
    J --> K{Required deps OK?}
    K -->|missing| S[Write report exit_code=2]
    S --> T[Exit 2: MISSING_REQUIRED_DEP]
    K -->|ok| REPO{skip_repo_steps or no git root?}
    REPO -->|yes| SKIPFM[Record file_modifications=skipped]
    SKIPFM --> AR[Autorun setup-dev-tools.py]
    REPO -->|no| L[File modifications]
    L --> AR
    AR --> ARRAN{Autorun ran?}
    ARRAN -->|skipped| M{All OK?}
    ARRAN -->|success or failed| VC{Version changed?}
    VC -->|changed| YR[Write report exit_code=4]
    YR --> Y[Exit 4: UPGRADED_RERUN_NEEDED]
    VC -->|unchanged or read failed| M{All OK?}
    M -->|yes| N[Write report exit_code=0]
    N --> O[Exit 0: OK]
    M -->|warnings| P[Write report exit_code=1]
    P --> Q[Exit 1: WARNINGS]
    L -->|exception| U[Write report exit_code=5]
    U --> V[Exit 5: REPO_MUTATION_FAILED]
    F -->|unhandled exception| W[Write report exit_code=6]
    W --> X[Exit 6: AUTORUN_FAILED]
```

> **Note:** Exit 4 (`UPGRADED_RERUN_NEEDED`) is emitted when the autorun phase
> changes the installed package version. A post-autorun subprocess re-reads
> the installed version; if it differs from the in-process startup version,
> `agdt-setup` writes the report and exits with code 4, instructing the caller
> to re-run.

## Success Criteria

A successful `agdt-setup` invocation (exit code 0) means:

- Phases 1–6 completed with status `"success"` or `"skipped"` (legitimately
  bypassed, e.g. by `--system-only` or version-guard early exit).
- Phase 7 (`autorun_setup`) may have any terminal status — `"success"`,
  `"skipped"`, or `"failed"` — without affecting the overall exit code,
  **unless** the autorun phase changed the installed package version.
  In that case, `agdt-setup` exits with code 4 (`UPGRADED_RERUN_NEEDED`)
  to signal that a re-run is required.
  Autorun failures are recorded in the report but are non-fatal: the phase
  swallows all exceptions internally and never propagates them to the caller.
- No required dependencies are missing.
- The structured report was written to `~/.agdt/last-setup-report.json` (or the
  overridden path) with `exit_code: 0` and `exit_code_name: "OK"`.
- Managed CLI tools (`gh`, `copilot`) are installed to `~/.agdt/bin/` and available on `PATH`
  (only when `cli_installation` ran; this phase is skipped when `--system-only` is passed).
- Repository file modifications were applied without exceptions (unless
  `--force-old-version` caused repo steps to be skipped).
