# Feature Specification: Automated Issue Type Discovery in agdt-setup

**Feature Branch**: `speckit/1785/phase-1-specify`
**Created**: 2026-07-18
**Status**: Draft
**Input**: User description: "Call `get_issue_types()` after provider config, save with timestamp."
**Source Issue**: #1785 (<https://github.com/swai-factory/agentic-devtools/issues/1785>)

## Problem Statement

The `agdt-setup` command currently performs platform detection
(identifying code hosting and issue-provider configuration, e.g., GitHub/Azure DevOps for code hosting and Jira/GitHub/Markdown as issue adapters)
and saves the provider configuration to
`.github/agdt-config.json`. It also performs Jira-specific instance discovery (fetching server version and deployment type) and caches that metadata. However, there is no automated step that discovers
and persists the issue types available in the configured project or repository. This means that downstream features — such as Phase 0 issue normalization, `issue.md` generation, and issue template
scaffolding — cannot rely on a pre-populated, timestamped catalog of issue types being present after setup completes.

Without automated discovery during setup, every downstream consumer that needs issue type information must independently call `get_issue_types()` on the adapter at runtime. This introduces latency
into workflows that should start from a known-good configuration baseline, creates inconsistent behavior when the provider is temporarily unreachable (some consumers fail, others degrade differently),
and prevents drift detection — the ability to compare a previously discovered schema against the current provider state to flag changes.

The parent feature (#1782) establishes the overall goal: auto-discover and persist the provider/project issue-type and property schema during setup, detecting drift on refresh. This subtask is
specifically responsible for the automated invocation of the adapter's `get_issue_types()` method immediately after provider configuration is saved, persisting the results using the
`ProjectIssueTypesMetadata` schema (defined by sibling #1783 and already implemented in `agentic_devtools/cli/config/project_config.py`), and recording an ISO-8601 UTC timestamp to enable staleness
detection and drift comparison in future refreshes. The implementation must degrade gracefully: if the adapter call fails due to network errors,
authentication failures, or any other provider exception, the setup flow must continue without interruption, logging a warning to stderr. When an adapter
raises `NotImplementedError` (indicating it does not yet support issue type discovery), setup continues silently — recording only a debug-level log with no
user-facing warning — since the provider simply does not implement this capability.

## User Scenarios & Testing

### User Story 1 - Automatic Issue Type Discovery on First Setup (Priority: P1)

A developer runs `agdt-setup` in a repository for the first time. The command detects the platform (e.g., GitHub or Jira), saves the provider configuration, and then immediately calls
`get_issue_types()` on the resolved adapter. The discovered issue types are persisted to `.agdt/config/project.json` under the `issue_types_metadata` key, keyed by project identifier, with both
`lastDiscovered` and `lastRefreshed` timestamps set to the current UTC time. The developer sees a success message confirming how many issue types were discovered. All downstream workflows that depend
on issue type metadata — such as Phase 0 normalization, template scaffolding, and issue creation — can now read from the persisted catalog without making additional provider API calls.

**Why this priority**: This is the core functionality described by the issue. Without it, no downstream Phase 0 features can rely on a pre-populated issue type catalog. It directly satisfies the
primary acceptance criterion ("runs after config save").

**Independent Test**: Can be fully tested by running `agdt-setup` in a repository with a configured adapter (or a mocked adapter in unit tests) and verifying that `project.json` contains a valid
`issue_types_metadata` entry with correct timestamps and at least one issue type.

**Acceptance Scenarios**:

1. **Given** a repository with no existing `project.json` and a GitHub remote, **When** the developer runs `agdt-setup`, **Then** the platform is detected as GitHub, `get_issue_types()` is called on
   the GitHub adapter, the results are persisted to `.agdt/config/project.json` with `lastDiscovered` and `lastRefreshed` set to the current UTC time in `+00:00` format, and a confirmation message is
   printed to stdout indicating the number of discovered types.

2. **Given** a repository with Jira configured as the issue adapter via `--issue-adapter jira`, **When** the developer runs `agdt-setup --issue-adapter jira`, **Then** after the platform configuration
   is saved, `get_issue_types()` is called on the Jira adapter, and the discovered Jira issue types (e.g., "Bug", "Story", "Task", "Epic") are persisted with timestamps.

3. **Given** a repository with an existing `project.json` that already contains `issue_types_metadata`, **When** the developer runs `agdt-setup` (without `--reconfigure` or `--refresh-issue-types`),
   **Then** the existing metadata is preserved and discovery is skipped unless a refresh is forced, and no duplicate entries are created.

---

### User Story 2 - Graceful Skip on Discovery Failure (Priority: P1)

A developer runs `agdt-setup` in an environment where the issue provider is unreachable — perhaps because of a VPN disconnection or an expired PAT. The setup command must not abort or raise an
unhandled exception. Instead, it logs a warning to stderr explaining that issue type discovery was skipped and the reason, then continues with the remaining setup steps (Phase 0 configuration,
template generation, etc.). If the adapter has not yet implemented `get_issue_types()` (raises `NotImplementedError`), setup still continues, but the skip is recorded only as a debug-level log
message rather than a user-facing warning. The developer can re-run setup later or use `--reconfigure` / `--refresh-issue-types` to retry discovery when supported.

**Why this priority**: This is equally critical to the happy path because setup is a one-time onboarding command that must never fail partway through. The acceptance criteria explicitly require
"graceful skip on failure," making this a P1 requirement.

**Independent Test**: Can be tested by mocking the adapter's `get_issue_types()` to raise both provider failures (`ConnectionError`, `requests.RequestException`, generic `Exception`) and
`NotImplementedError`, then verifying that setup completes successfully in all cases; provider failures print a warning to stderr, while `NotImplementedError` produces only a debug-level log, and
subsequent setup steps (Phase 0 config, template generation) still execute.

**Acceptance Scenarios**:

1. **Given** a Jira-configured repository where the Jira server is unreachable, **When** the developer runs `agdt-setup`, **Then** a warning message like `⚠ Issue type discovery skipped (<reason>)` is
   printed to stderr, the remaining setup steps complete normally, and `project.json` does not contain an `issue_types_metadata` entry (or retains any previously discovered metadata unchanged).

2. **Given** a repository using an adapter that has not yet implemented `get_issue_types()` (raises `NotImplementedError`), **When** the developer runs `agdt-setup`, **Then** the
   `NotImplementedError` is caught, a debug-level log message records that discovery was skipped, no user-facing warning is printed, and setup continues without persisting issue type metadata.

3. **Given** a repository where discovery fails due to an authentication error (e.g., expired PAT), **When** the developer runs `agdt-setup`, **Then** the error is caught, a descriptive warning is
   printed (including the exception message), and no partial or corrupted metadata is written to `project.json`.

---

### User Story 3 - Forced Refresh of Issue Types (Priority: P2)

A developer whose project's issue types have changed (e.g., a new "Spike" type was added in Jira, or a new issue form template was added to GitHub) wants to refresh the persisted catalog. The
developer runs `agdt-setup --reconfigure` or `agdt-setup --refresh-issue-types`, which forces a fresh call to `get_issue_types()` regardless of whether metadata already exists. The `lastRefreshed`
timestamp is updated to the current time, while `lastDiscovered` retains the original first-discovery timestamp. The developer sees a confirmation message indicating the refreshed type count.

**Why this priority**: Refresh support enables drift detection and ensures the catalog stays current as projects evolve. It is less critical than initial discovery and graceful failure handling
because it targets returning users rather than first-time onboarding.

**Independent Test**: Can be tested by pre-populating `project.json` with stale `issue_types_metadata`, then invoking setup with the refresh flag, and verifying that `lastRefreshed` is updated while
`lastDiscovered` is preserved, and the `issue_types` list reflects the new adapter response.

**Acceptance Scenarios**:

1. **Given** a repository with existing `issue_types_metadata` where `lastDiscovered` is "2026-06-01T00:00:00+00:00", **When** the developer runs `agdt-setup --reconfigure`, **Then**
   `get_issue_types()` is called again, `lastRefreshed` is updated to the current UTC time, `lastDiscovered` retains "2026-06-01T00:00:00+00:00", and the `issue_types` list is replaced with the fresh
   response.

2. **Given** a repository with existing metadata and the `--refresh-issue-types` flag, **When** the developer runs `agdt-setup --refresh-issue-types`, **Then** discovery is re-invoked even if cached
   metadata exists, and the metadata is updated in place.

---

### User Story 4 - Timestamp Recording for Staleness Detection (Priority: P2)

Downstream consumers (such as drift detection in sibling #1788 or Phase 0 normalization) need to determine when the issue type catalog was last refreshed. The persisted `issue_types_metadata` entry
includes `lastDiscovered` (the first time types were fetched for this project) and `lastRefreshed` (the most recent fetch time), both in ISO-8601 UTC format with `+00:00` offset. These timestamps
allow consumers to implement cache-expiry policies or surface warnings when the catalog is stale.

**Why this priority**: Timestamps are a structural requirement for drift detection and staleness policies. They are straightforward to implement alongside discovery persistence but only deliver value
when consumed by downstream features.

**Independent Test**: Can be tested by invoking discovery, reading the persisted timestamps, verifying they are valid ISO-8601 UTC strings with `+00:00` offset, and confirming that `lastDiscovered`
equals `lastRefreshed` on first discovery but only `lastRefreshed` changes on subsequent refreshes.

**Acceptance Scenarios**:

1. **Given** a first-time discovery run, **When** issue types are persisted, **Then** both `lastDiscovered` and `lastRefreshed` are set to the same UTC timestamp in `+00:00` format and are parseable
   by `datetime.fromisoformat()`.

2. **Given** a refresh run where metadata already exists with `lastDiscovered` = T1, **When** discovery succeeds at time T2, **Then** `lastDiscovered` remains T1 and `lastRefreshed` is updated to T2.

---

### Edge Cases

- What happens when the adapter returns an empty list from `get_issue_types()`? The system should persist the empty list with timestamps, indicating that discovery succeeded but no types were found.
  This is distinct from a failure (which leaves no metadata).

- What happens when `project.json` exists but contains malformed JSON? The existing `load_project_config()` function already handles this by returning `{}` with a warning. Discovery should proceed as
  if no prior metadata exists, creating a fresh entry.

- What happens when the platform config is not saved successfully (e.g., permissions error)? Discovery should be skipped entirely because there is no valid adapter configuration to resolve. The setup
  flow should continue to subsequent steps.

- What happens when the resolved adapter's project key cannot be determined? Discovery should be skipped with a warning, as the `issue_types_metadata` dict is keyed by project identifier and a missing
  key would prevent valid persistence.

- What happens when `agdt-setup --skip-platform-detection` is used? Since platform detection is skipped, no adapter is resolved, and issue type discovery should be skipped as well — the step depends
  on a successfully configured adapter.

## Requirements

### Functional Requirements

- **FR-001**: The system MUST invoke `get_issue_types()` on the resolved issue adapter immediately after provider configuration is successfully saved during `agdt-setup`. This invocation occurs in the
  setup flow between the platform config save step and the Phase 0 configuration step, positioned logically after Jira instance discovery (Step 1.5) but before Phase 0 prompts (Step 1b). The adapter
  is resolved from the saved `platform_config["issue_adapter"]` value using the existing adapter factory.

- **FR-002**: The system MUST persist the discovered issue types to `.agdt/config/project.json` under the `issue_types_metadata` key, keyed by the project identifier (e.g.,
  `"swai-factory/agentic-devtools"` for GitHub or the Jira project key). Each entry MUST conform to the `ProjectIssueTypesMetadata` schema defined in `agentic_devtools/cli/config/project_config.py`,
  including `lastDiscovered`, `lastRefreshed`, `provider`, and `issue_types` fields.

- **FR-003**: The system MUST record both `lastDiscovered` and `lastRefreshed` as ISO-8601 UTC timestamps with the canonical `+00:00` offset (not `Z` suffix). On first discovery, both timestamps MUST
  be set to the current UTC time. On subsequent refreshes, only `lastRefreshed` MUST be updated; `lastDiscovered` MUST retain its original value.

- **FR-004**: The system MUST distinguish between two failure modes during the `get_issue_types()` call. When `get_issue_types()` raises `NotImplementedError`, the system MUST catch it, emit a
  `DEBUG`-level log message via Python's `logging` module (not a user-facing warning), and skip discovery silently, since the provider simply does not support this capability (consistent with the
  Edge Cases section). For all other exceptions — network timeouts, `ConnectionError`, HTTP errors, serialization failures, and any unexpected provider errors — the system MUST catch them with a
  broad `except Exception`, print a human-readable warning to stderr that includes the exception type and a summary of the error, and allow setup to continue to completion without a non-zero exit
  code.

- **FR-005**: The system MUST NOT invoke discovery when platform configuration was not saved successfully (i.e., when `save_platform_config()` returned `False` or was skipped entirely due to
  `--skip-platform-detection`).

- **FR-006**: The system MUST skip discovery when cached `issue_types_metadata` already exists for the project and neither `--reconfigure` nor `--refresh-issue-types` is active. In this case, the
  system SHOULD print a message indicating that cached metadata was found and discovery was skipped.

- **FR-007**: The system MUST map the adapter's `IssueTypeInfo` response into the `IssueTypeEntry` schema required by `ProjectIssueTypesMetadata`. The `IssueTypeInfo` contains `name` and
  `description`; the `IssueTypeEntry` additionally requires `id`, `is_subtask`, and `properties`. The `id` field MUST be set equal to `info["name"]` (i.e., `name` doubles as the provider-scoped
  identifier), since `IssueTypeInfo` does not supply a separate identifier field. `is_subtask` MUST default to `False` (refined by sibling #1786 / #1787 for property discovery), and `properties`
  MUST default to an empty list (populated by subsequent property discovery steps). Because `validate_issue_types_metadata()` rejects entries where `id` or `name` is blank, any `IssueTypeInfo`
  entry whose `name` is `None` or empty (after stripping whitespace) MUST be skipped with a debug-level log message; it MUST NOT be persisted.

- **FR-008**: The system MUST print a success message to stdout upon successful discovery, indicating the number of issue types discovered
  and the provider name. The message MUST be indented with two leading spaces to match existing `agdt-setup` step output, e.g.:

  ```text
    ✓ Discovered 5 issue types (jira)
  ```

### Non-Functional Requirements

- **NFR-001**: Discovery MUST complete within 30 seconds under normal network conditions. If the adapter call exceeds this threshold due to provider latency, adapter-managed timeout behavior MUST
  eventually raise a timeout-related exception rather than blocking indefinitely. The setup layer does not add a separate timeout wrapper; once an exception is raised, FR-004's graceful degradation
  path handles it and setup continues.

- **NFR-002**: Console output MUST follow the existing `agdt-setup` formatting conventions: success messages use `✓` prefix (with two
  leading spaces) on stdout, warnings use `⚠` prefix (with two leading spaces) on stderr, and all output is indented with two leading spaces
  to align with existing setup step output.

- **NFR-003**: The implementation MUST NOT introduce new external dependencies. It relies on the existing adapter framework (`agentic_devtools/adapters/base.py`), the existing project config
  persistence layer (`agentic_devtools/cli/config/project_config.py`), and the existing `save_project_config()` validation.

- **NFR-004**: All new code MUST achieve 100% per-file branch coverage under the 1:1:1 test structure (`tests/unit/`), pass `ruff check`, `ruff format`, and `mypy --ignore-missing-imports` without
  errors, and be committed as a single Conventional Commit scoped to `#1785`.

### Key Entities

- **ProjectIssueTypesMetadata**: The persisted catalog of issue types for a single project. Contains `lastDiscovered` and `lastRefreshed` timestamps (ISO-8601 UTC), `provider` identifier (e.g.,
  `"github"`, `"jira"`), and an `issue_types` list of `IssueTypeEntry` items. Stored in `.agdt/config/project.json` under the `issue_types_metadata` key, keyed by project identifier.

- **IssueTypeEntry**: A single issue type record within the metadata catalog. Contains `id` (the
  provider-reported type name, set equal to `info["name"]` since `IssueTypeInfo` exposes no
  separate stable identifier), `name` (display name), `description`, `is_subtask` flag, and a
  `properties` list (initially empty, populated by sibling property discovery tasks #1786 / #1787).

- **IssueTypeInfo**: The adapter-level response type returned by `get_issue_types()`. Contains `name` and `description`. This is the input that must be mapped to `IssueTypeEntry` for persistence.

## Success Criteria

### Measurable Outcomes

- **SC-001**: After running `agdt-setup` with a functional GitHub or Jira adapter, `.agdt/config/project.json` contains a valid `issue_types_metadata` entry with at least 1 issue type and both
  `lastDiscovered` and `lastRefreshed` timestamps that pass `_validate_utc_timestamp()` validation — verified by 100% of test scenarios for FR-001 through FR-003.

- **SC-002**: When `get_issue_types()` raises a non-`NotImplementedError` exception (covering at least `ConnectionError`, `requests.RequestException`, `ValueError`, and a generic `Exception`),
  the setup command exits with code 0, a warning message containing the exception type appears on stderr, and all subsequent setup steps (Phase 0 config, template generation) execute — verified by
  at least 4 distinct failure-mode test cases. When `get_issue_types()` raises `NotImplementedError`, the setup command exits with code 0, **no** user-facing warning is printed to stderr (only a
  `DEBUG`-level log entry is emitted), and subsequent steps still execute — verified by a separate test case distinct from the general failure-mode cases above.

- **SC-003**: On a refresh run (`--reconfigure` or `--refresh-issue-types`), the `lastDiscovered` timestamp is unchanged from the prior value and `lastRefreshed` is strictly greater than the prior
  value — verified by at least 2 test cases comparing before and after timestamps.

- **SC-004**: The new source files achieve 100% branch coverage as measured by `bash scripts/targeted-checks.sh`
  (per-file branch coverage), tests pass via `agdt-test-pattern tests/unit/<module_path>/ -v -o addopts=`,
  with 0 ruff lint violations and 0 mypy type errors.

- **SC-005**: When `--skip-platform-detection` is active or `save_platform_config()` returns `False`, the discovery step is not invoked (0 calls to `get_issue_types()`) — verified by mock call-count
  assertions in at least 2 test cases.

- **SC-006**: The total number of new test files follows the 1:1:1 structure, with each public function or class having a dedicated test file under `tests/unit/` mirroring the source path, and
  `scripts/validate_test_structure.py` passes without errors.
