# AI Agent Instructions: agentic-devtools Python Package

> Parent Instruction Chain
>
> This file is the repository root instruction file for this project.
>
> [`copilot-instructions.md` Precedence Model](../docs/copilot-instructions-precedence-model.md)

## 1. Purpose

`agentic-devtools` is a pip-installable Python package that provides CLI commands for AI agents to interact with
Git, Azure DevOps, Jira, and other services. The design prioritizes **auto-approval** in VS Code by using:

- A generic `agdt-set key value` command (approve once, works for all keys)
- Parameterless action commands like `agdt-git-save-work`, `agdt-add-jira-comment`
- Native Python CLI handling of special characters and multiline content (no replacement tokens needed!)

### Copilot Chat Agents

Workflow steps can be started from VS Code Copilot Chat using `/agdt.<workflow>.<step>` commands.
Agents live in `.github/agents/` and prompts live in `.github/prompts/`, mirroring the
existing `speckit.*` pattern.

### Workflow Expectation Docs

General expectation templates (each with a Mermaid diagram) for every *initiate workflow* live in
[`docs/workflow-expectations/`](../docs/workflow-expectations/). When you change an initiate
workflow's behavior, parameters, or decision paths (under `agentic_devtools/cli/workflows/**` or
`agentic_devtools/prompts/**`), you MUST update the matching document and its diagram in the
**same PR**, and reviewers must confirm the docs match the code. See
[`.github/instructions/workflow-expectations.instructions.md`](instructions/workflow-expectations.instructions.md)
for the full requirement.

### Multi-Worktree Development

The package supports **multi-worktree development** using a single global pip/pipx install of `agentic-devtools` shared across all
worktrees. Each worktree has its own `.agdt/workflows/{identity}/{worktree_key}/state.json` file, so state is fully isolated between branches.

- **Single Global Install**: Install once with `uv pip install --system agentic-devtools` (preferred) or `pip install agentic-devtools` or `pipx install agentic-devtools`; all worktrees use the same installation
- **No Command Changes**: The same `agdt-*` commands work everywhere - entry points call implementations directly

### Background Task Architecture

**All action commands** spawn background tasks and return immediately:

- Commands like `agdt-git-save-work`, `agdt-add-jira-comment`, `agdt-create-pull-request` run in the background
- They return immediately with a task ID for tracking
- Results are written to output files in the workflow state directory when the task completes
- Use `agdt-task-status`, `agdt-task-log`, or `agdt-task-wait` to monitor progress
- The immediate console output tells the AI agent:
  1. What background task was triggered
  2. The task ID for status checking
  3. Where to find results when complete (output file path)

### ⚠️ CRITICAL: Testing Commands

**When working on `agentic-devtools`, ALWAYS use `agdt-test` commands - NEVER run pytest directly!**

```bash
# Full test suite (background, ~55 seconds for 2000+ tests)
agdt-test
agdt-task-wait

# Quick tests without coverage (background)
agdt-test-quick
agdt-task-wait

# Specific source file — ONLY for legacy flat test layout (tests/test_<module>.py)
# ⚠️ Does NOT work with 1:1:1 tests/unit/ layout — use agdt-test-pattern instead.
# Note: only runs the inferred flat file (e.g. tests/test_release_commands.py) —
# does NOT run tests/unit/cli/release/commands/ even if that directory exists
agdt-test-file --source-file agentic_devtools/cli/release/commands.py  # Auto-saves to state
agdt-task-wait

# Specific test - synchronous (simpler for quick checks)
agdt-test-pattern tests/unit/cli/github/repo_resolution/test_resolve_github_repo.py::TestResolveGithubRepo -v -o addopts=
```

Why:

- Tests run as background tasks to prevent AI agents from thinking something went wrong
- Logs are captured properly in `.agdt/workflows/…/background-tasks/logs/`
- Direct pytest calls don't integrate with the background task system
- `agdt-test-file` shows coverage ONLY for the specified source file (legacy flat `tests/test_<module>.py` paths)
- `agdt-test-file` **does NOT work** with the 1:1:1 `tests/unit/` layout — use `agdt-test-pattern tests/unit/<module>/ -v` or `bash scripts/targeted-checks.sh` instead
- `agdt-test-pattern` does **not** verify 100% branch coverage; for focused runs, pass `-o addopts=` to avoid inherited repo-wide coverage gates

See [Testing](#13-testing) for all test commands.

### ⚠️ CRITICAL: Run PR Checks Before Pushing

**A `.githooks/pre-push` hook automatically runs targeted checks before each push.** Ensure hooks are configured:

```bash
git config core.hooksPath .githooks
```

The pre-push hook runs fast, scoped checks on changed files only (~30s):

1. ruff format (auto-fix — aborts push if files were reformatted)
2. ruff check (lint)
3. markdownlint (on changed `.md` files)
4. Per-file 100% branch coverage
5. mypy type checking
6. Test structure validation (if test files changed)

**If the hook aborts the push**, fix the issues, stage and amend your commit, then push again.

For manual targeted checks without pushing:

```bash
bash scripts/targeted-checks.sh
```

For the full test suite (manual pre-merge validation):

```bash
bash scripts/run-pr-checks.sh --full
```

For quick auto-fixable lint issues:

```bash
ruff check --fix .
ruff format .
```

Then push again — the pre-push hook will re-validate.

### ⚠️ CRITICAL: Single Commit Per PR

**Every pull request MUST contain exactly one commit.** This is a hard policy, not a guideline.

- **NEVER use `git commit`** — always use `agdt-git-save-work`, which automatically amends when the branch has commits ahead of
  `main` (or `origin/main` when available). This enforces the single-commit policy: once you have one commit on the branch, all subsequent saves amend it.
- **NEVER create additional commits** on a feature branch — always amend and force-push
- Adding a second commit to a PR branch is a **policy violation**

```bash
# Correct: amend existing commit (agdt-git-save-work auto-detects amend vs new commit)
agdt-git-save-work

# WRONG: never do this
git commit -m "fix review feedback"   # ❌ creates a second commit (policy violation)
git commit --amend                    # ❌ wrong tool (use agdt-git-save-work)
```

Why this matters:

- **Clean git history** — one commit per PR = linear, readable history after squash-merge
- **Atomic changes** — each PR represents a single logical change
- **Easier reverts** — one commit = one revert if something goes wrong

### ⚠️ CRITICAL: Auto-Merge Label (`ai-auto-merge-allowed`)

**Before kicking off any AI agent session that will result in a PR being created, you MUST ask the user whether the `ai-auto-merge-allowed` label should be added to the PR.**

Rules for AI agents:

- **NEVER add `ai-auto-merge-allowed` to a PR unless you were explicitly instructed to do so** in the session instructions.
- When starting an agent session (e.g. `/agdt.address-copilot-review`, `agdt-initiate-*-workflow`, `agdt.pr-merge-manager`, or any other workflow that will result in a PR), **ask the user first**:
  > "Should the `ai-auto-merge-allowed` label be added to the PR? (This enables auto-merge via the AI PR loop once all checks pass and Copilot approves.)"
- If the user says yes, **include an explicit instruction in the agent's session prompt**: `"Add the 'ai-auto-merge-allowed' label to the PR after creating it."`
- If the user says no or does not answer, **do not add the label**.
- Repo workflows may auto-apply this label only when explicitly enabled (for example `SPECKIT_AUTO_MERGE_ALLOWED_LABEL == 'true'`); if not enabled, agents must treat it as not approved.
- Do not infer intent from context — only add the label when explicitly instructed.

### ⚠️ CRITICAL: Always Use agdt Commands

**AI agents MUST prefer `agdt-*` CLI commands over raw equivalents wherever support exists.**

Using raw commands (e.g., `git`, raw REST API calls, `pytest`) bypasses:

- The **background task system** — raw `git` commands stall agents; `agdt-git-save-work` runs in the background
- **Workflow integration** — `agdt-*` commands auto-advance workflow steps, update checklists, and manage state
- **Auto-approval benefits** — the `agdt-set` / `agdt-<action>` pattern allows VS Code to approve once for all keys
- **Dry-run safety** — `agdt-*` commands respect `dry_run` state; raw commands do not

#### Command Mapping

| Instead of... | Use... |
|---------------|--------|
| `git add . && git commit && git push` | `agdt-git-save-work` |
| `git commit --amend` | `agdt-git-save-work` (auto-detects amend) |
| `git push --force` | `agdt-git-force-push` |
| `git push -u origin <branch>` | `agdt-git-publish` |
| `pytest ...` | `agdt-test`, `agdt-test-file`, `agdt-test-pattern`, `agdt-test-quick` |
| Raw Azure DevOps REST API calls | `agdt-create-pull-request`, `agdt-add-pull-request-comment`, etc. |
| Raw Jira REST API calls | `agdt-add-jira-comment`, `agdt-get-jira-issue`, etc. |
| `gh issue create` (for this repo) | `agdt-create-agdt-feature-issue`, `agdt-create-agdt-bug-issue`, etc. |
| `gh pr view ... --json` | `agdt-gh-pr-state` |
| `gh pr checks` + `gh api check-suites` | `agdt-gh-pr-checks-status` |
| `gh api .../reviews` + GraphQL suppressed pagination | `agdt-gh-copilot-review-status` |
| `gh api ... actions/runs/{id}/rerun` | `agdt-gh-rerun-checks` |
| `gh api graphql` (thread pagination + `resolveReviewThread` mutation) | `agdt-gh-resolve-review-threads` |
| `gh pr review --approve` + manual verification | `agdt-gh-pr-approve` |
| `gh api .../requested_reviewers -X POST` | `agdt-gh-request-copilot-review` |
| Manual Phase 2 PR polling loop | `agdt-gh-pr-poll-ready` |
| `gh pr merge --rebase --delete-branch` + manual verification | `agdt-gh-pr-merge` |
| `gh api` loop for PR review comment replies | `agdt-gh-reply-to-review-comments` |

#### Exceptions — Raw Commands Still Required

> ⚠️ No agdt equivalent yet. Switch to agdt command when support is added.

| Raw command | Reason still needed |
|-------------|---------------------|
| `gh issue` / `gh pr` for repos other than agentic-devtools | No agdt support yet for general GitHub operations |
| `gh pr comment` for external repos | No agdt support yet for general GitHub PR comments |
| Other `gh` operations not listed in the command mapping above | No agdt equivalent exists |

### GitHub PR Actions (Synchronous)

| Command | Purpose | Required State / CLI Args |
|---------|---------|---------------------------|
| `agdt-gh-pr-state` | Fetch structured GitHub PR state | `--pr` (int) and optionally `--repo` (owner/repo); falls back to `github.pull_request_number` and `github.repo` state keys or git remote auto-detection |
| `agdt-gh-pr-poll-ready` | Poll PR until ready to merge or blocked | `--pr` or `github.pull_request_number`; optional `--repo`, `--poll-interval`, `--max-wait`, `--background`, `--rerun-stale-checks` |
| `agdt-gh-pr-approve` | Approve a GitHub PR with verification and retry | `--pr` (int) or `github.pull_request_number`; optional `--repo` (owner/repo), `--body` (approval comment) |
| `agdt-gh-pr-merge` | Merge a GitHub PR with verification and retry | `--pr` (int) or `github.pull_request_number`; optional `--repo` (owner/repo), `--strategy` (squash/merge/rebase, default rebase), `--no-delete-branch`; `--repo` falls back to `github.repo` state or git remote auto-detection |
| `agdt-gh-rerun-checks` | Re-run stale/failed GitHub Actions workflows | `--pr` (int) or `github.pull_request_number`; optional `--repo`, `--head-sha`, `--filter` (workflow name substring), `--include-cancelled` |
| `agdt-gh-request-copilot-review` | Request Copilot review with verification | `--pr` (int) or `github.pull_request_number`; optional `--repo` |
| `agdt-gh-reply-to-review-comments` | Post batch replies to PR review comments with verification | `--pr` (int) or `github.pull_request_number`; `--review-id` (int) or `github.copilot_review_id`; `--replies-file` (path to JSON); optional `--repo` |

**State keys written by `agdt-gh-pr-state`:**
`github.pull_request_number`, `github.repo`, `github.pr_state`,
`github.head_ref_oid`, `github.head_ref_oid_short`, `github.mergeable`,
`github.merge_state_status`, `github.is_draft`, `github.is_terminal`

**State keys written by `agdt-gh-pr-approve`:**
`github.pr_approval_verified` (bool), `github.pr_approval_review_id` (int or null)

**State keys written by `agdt-gh-pr-merge`:**
`github.pr_merged` (bool), `github.pr_merged_at` (ISO-8601 string or null),
`github.pr_merge_strategy` (string: squash/merge/rebase)

**State keys written by `agdt-gh-rerun-checks`:**
`github.rerun_checks_count` (int), `github.rerun_checks_failed_to_rerun` (int)

**State keys written by `agdt-gh-request-copilot-review`:**
`github.copilot_review_requested` (bool), `github.copilot_review_request_verified` (bool)

**State keys written by `agdt-gh-reply-to-review-comments`:**
`github.review_replies_total` (int), `github.review_replies_successful` (int),
`github.review_replies_failed` (int), `github.review_replies_verified` (bool)

**Usage:**

```bash
# With explicit args
agdt-gh-pr-state --pr 1115 --repo swai-factory/agentic-devtools

# With state (repo auto-detected from git remote)
agdt-set github.pull_request_number 1115
agdt-gh-pr-state
```

**`agdt-gh-pr-poll-ready` replaces the entire Phase 2 polling loop of the pr-merge-manager prompt.** It calls sibling Python functions directly in-process each iteration:

```bash
# Foreground (blocks until ready or blocked)
agdt-gh-pr-poll-ready --pr 1115 --repo owner/repo --poll-interval 60 --max-wait 600

# Background task mode
agdt-gh-pr-poll-ready --pr 1115 --background

# Disable stale check re-runs
agdt-gh-pr-poll-ready --pr 1115 --no-rerun-stale-checks
```

**Output:** Structured JSON to stdout with `ready`, `reason`, `actionRequired` fields.

**State keys written by `agdt-gh-pr-poll-ready`:** `github.pr_poll_ready_result`, `github.pr_poll_ready_action`.

## 2. Package Structure

```text
agentic_devtools/
├── __init__.py          # Package metadata
├── state.py             # JSON state management (single file: agdt-state.json)
├── file_locking.py      # Cross-platform file locking for state
├── task_state.py        # Background task state schema and CRUD
├── background_tasks.py  # Background task execution infrastructure
├── cli/
│   ├── __init__.py
│   ├── state.py         # Generic set/get/delete/clear/show commands
│   ├── azure_devops/    # Azure DevOps module (modular package)
│   │   ├── __init__.py  # Re-exports public API for backward compatibility
│   │   ├── config.py    # Constants + AzureDevOpsConfig dataclass
│   │   ├── auth.py      # PAT and auth header functions
│   │   ├── helpers.py   # Pure utility functions (no state reading)
│   │   ├── commands.py  # CLI commands with dry_run + state reading
│   │   ├── mark_reviewed.py         # Mark files as reviewed in Azure DevOps
│   │   ├── review_commands.py       # PR review workflow entry point
│   │   ├── review_state.py          # Review state schema & CRUD
│   │   ├── review_templates.py      # Markdown template engine for summaries
│   │   ├── review_scaffold.py       # Upfront thread scaffolding
│   │   ├── status_cascade.py        # Status derivation & cascade logic
│   │   ├── review_attribution.py    # AI model attribution & commit tagging
│   │   ├── suggestion_verification.py # Suggestion verification for re-reviews
│   │   └── suggestion_commands.py   # Confirm/reject suggestion CLI commands
│   ├── git/             # Git workflow commands (package)
│   │   ├── __init__.py  # Command exports
│   │   ├── core.py      # State helpers, git execution, temp files
│   │   ├── operations.py # Individual git operations
│   │   └── commands.py  # CLI entry points
│   ├── github/          # GitHub CLI commands (package)
│   │   ├── __init__.py  # Command exports
│   │   ├── state_helpers.py  # issue.* namespace helpers
│   │   ├── repo_resolution.py # Shared owner/repo resolution for agdt-gh-* commands
│   │   ├── request_copilot_review.py # Request Copilot review with verification
│   │   ├── issue_commands.py # Synchronous issue creation commands
│   │   ├── async_commands.py # Background/async command wrappers + CLI entry points
│   │   └── review_reply.py   # Batch reply to PR review comments with verification
│   ├── jira/            # Jira commands (package)
│   │   ├── __init__.py  # Command exports
│   │   ├── config.py    # Jira configuration and auth
│   │   ├── commands.py  # Synchronous CLI entry points
│   │   ├── update_commands.py # Issue update command
│   │   └── async_commands.py  # Background/async command wrappers
│   └── tasks/           # Background task monitoring (package)
│       ├── __init__.py  # Command exports
│       └── commands.py  # Task monitoring CLI commands
├── prompts/             # Prompt template system (package)
│   ├── __init__.py      # Package exports
│   └── loader.py        # Template loading, validation, substitution
└── pyproject.toml       # Package config with entry points
```

### Module Responsibilities

| Module | Responsibility |
|--------|---------------|
| `azure_devops/config.py` | Constants (`DEFAULT_ORGANIZATION`, `APPROVAL_SENTINEL`, etc.) and `AzureDevOpsConfig` dataclass |
| `azure_devops/auth.py` | `get_pat()` and `get_auth_headers()` for Azure DevOps authentication |
| `azure_devops/helpers.py` | Pure utility functions: `build_thread_context`, `convert_to_pull_request_title`, `format_approval_content`, etc. |
| `azure_devops/commands.py` | CLI entry points: `reply_to_pull_request_thread`, `add_pull_request_comment`, `create_pull_request`, etc. |
| `azure_devops/pr_review_progress.py` | v2 PR-review progress counts (`compute_review_progress`) sourced from `manifest.json` total + the answer ledger (replaces the retired `queue.json` / `get_queue_status` progress source) |
| `azure_devops/mark_reviewed.py` | Mark files as reviewed in Azure DevOps: updates Reviewers API and Contribution API for UI display |
| `azure_devops/review_commands.py` | PR review workflow: `review_pull_request`, prompt generation, Jira integration |
| `azure_devops/__init__.py` | Re-exports all public API for backward compatibility (`from agentic_devtools.cli.azure_devops import reply_to_pr_thread`) |
| `azure_devops/review_state.py` | Review state schema (`ReviewState`, `FileEntry`, `SuggestionEntry`, `ReviewSession`) and CRUD (`load_review_state`, `save_review_state`, `update_file_status`) for `review-state.json` |
| `azure_devops/review_templates.py` | Markdown template engine: `render_file_summary`, `render_overall_summary`, `build_discussion_url` for review comment rendering |
| `azure_devops/review_scaffold.py` | Upfront scaffolding: creates a per-commit consolidated review comment during `agdt-review-pull-request`, with re-review semantics (`force_rereview` skip/override, different-model in-place reply, new-thread-per-commit archiving) |
| `azure_devops/consolidated_review.py` | Pure renderer: `render_commit_review_comments` (lightweight-in-progress / full-on-complete, 50k smart-cutoff → root + continuation list), `render_consolidated_review_comment` (root string), `render_model_review_reply`, previous-reviews index, and marker helpers (`consolidated`/`continuation`/`model-review`) |
| `azure_devops/consolidated_scaffold.py` | I/O layer for per-commit comments: `upsert_consolidated_comment` (POST new commit thread / PATCH root + reconcile continuations), `find_consolidated_thread` (marker recovery) |
| `azure_devops/status_cascade.py` | Status derivation rules (`compute_aggregate_status`) and overall PR summary PATCH operations (`cascade_status_update`, `execute_cascade`) derived directly from file statuses (no folder-level threads) |
| `azure_devops/review_attribution.py` | AI model attribution: model icons, commit hash links, emoji status formatting |
| `azure_devops/suggestion_verification.py` | Suggestion verification for re-reviews: `verify_previous_suggestions`, `SuggestionVerificationResult` |
| `azure_devops/suggestion_commands.py` | Confirm/reject suggestion CLI commands: `confirm_suggestion_addressed`, `reject_suggestion_resolution` |
| `git/core.py` | State helpers, git command execution, temp file handling |
| `git/operations.py` | Individual git operations (stage, commit, push, etc.) and smart amend detection |
| `git/commands.py` | CLI entry points: `agdt-git-save-work` (auto-detects amend), `agdt-git-stage`, `agdt-git-push`, etc. |
| `github/__init__.py` | Re-exports public API for `agdt-create-agdt-*` commands |
| `github/state_helpers.py` | `get_issue_value()` and `set_issue_value()` for the `issue.*` state namespace |
| `github/issue_commands.py` | Sync implementations: `create_agdt_issue`, `create_agdt_bug_issue`, `create_agdt_feature_issue`, `create_agdt_documentation_issue`, `create_agdt_task_issue` |
| `github/async_commands.py` | Async wrappers (background tasks) and argparse CLI entry points for all `agdt-create-agdt-*` commands |
| `github/repo_resolution.py` | `resolve_github_repo()` for resolving `owner/repo` from CLI args or git remote |
| `github/pr_checks_status.py` | Dual-source CI check verification: `get_pr_checks_status()`, `pr_checks_status_command()` |
| `prompts/loader.py` | Template loading, variable extraction, validation, substitution, and output saving |
| `cli/workflows/base.py` | Base workflow utilities: `validate_required_state`, `initiate_workflow`, `advance_workflow_step` |
| `cli/workflows/commands.py` | Workflow initiation CLI commands for each workflow type |
| `cli/workflows/preflight.py` | Pre-flight checks for work-on-jira-issue: worktree/branch validation |
| `cli/workflows/advancement.py` | Auto-advancement helpers called by other commands to progress workflows |

## 3. State Management Pattern

All state is stored in a single JSON file (`.agdt/workflows/{identity}/{worktree_key}/state.json`):

```python
from agentic_devtools.state import get_value, set_value, load_state

# Set values
set_value("pr_id", 23046)
set_value("content", "My reply message")

# Get values
pr_id = get_value("pr_id")
content = get_value("content")

# Load entire state
state = load_state()  # Returns dict
```

### Workflow State

Workflow state is stored separately under the `_workflow` key and managed through dedicated functions:

```python
from agentic_devtools.state import (
    get_workflow_state,
    set_workflow_state,
    clear_workflow_state,
    is_workflow_active,
    update_workflow_step,
    update_workflow_context,
)

# Set workflow state
set_workflow_state(name="pull-request-review", status="active", step="initiate", context={"pull_request_id": "123"})

# Check if a workflow is active
if is_workflow_active():
    workflow = get_workflow_state()
    print(f"Active: {workflow['name']} at step {workflow['step']}")

# Update workflow progress
update_workflow_step("reviewing")
update_workflow_context(files_reviewed=5)

# Clear when done
clear_workflow_state()
```

### Workflow Transition Behavior

Workflows advance through steps via event-driven transitions. The transition behavior depends on the `auto_advance` flag and `required_tasks`:

1. **Immediate Advancement** (auto_advance=True, no required_tasks):
   - The workflow step changes immediately
   - The next step's prompt is rendered and printed to the console
   - Examples: `CHECKLIST_CREATED`, `CHECKLIST_COMPLETE`, `JIRA_COMMENT_ADDED`

2. **Deferred Advancement** (auto_advance=True, has required_tasks):
   - A `pending_transition` is recorded in the workflow context
   - The step changes when `agdt-get-next-workflow-prompt` is called AND the background tasks complete
   - Examples: `GIT_COMMIT_CREATED` (waits for agdt-git-commit task)

3. **Manual Advancement** (auto_advance=False):
   - Requires explicit `agdt-advance-workflow` command
   - Used for steps that need human confirmation

When a transition fires immediately, the output looks like:

```text
================================================================================
WORKFLOW ADVANCED: work-on-jira-issue
NEW STEP: implementation-review
================================================================================

# Work on Jira Issue - Implementation Review Step
...
```

### Issue Key (Provider-Agnostic)

The top-level `issue_key` is the canonical, provider-agnostic issue identifier. It supports
both Jira keys (`PROJECT-1234`) and GitHub issue numbers (`#42`, `42`):

```bash
# Primary way to set the current issue identifier (provider-agnostic)
agdt-set issue_key 42

# Jira-specific legacy key (backward compatible — continues to work)
agdt-set jira.issue_key PROJECT-1234
```

Numeric issue IDs (e.g., `42`) are JSON-parsed as integers by `agdt-set`.
Read paths that resolve the current issue key normalize that value to its
string representation, so `agdt-set issue_key 42` and `agdt-set issue_key "42"`
produce equivalent behavior.

All core functions that resolve an issue key check `issue_key` first, then fall back to
`jira.issue_key`. Both keys can coexist with different values — setting one does **not**
clear the other. Both are context-switching keys — setting either one clears
`pull_request_id`, and setting `pull_request_id` clears both `issue_key` and `jira.issue_key`.

**Resolution priority** (used by `_get_issue_key_from_state`, `resolve_worktree_key`, etc.):

1. `issue_key` (top-level, provider-agnostic)
2. `jira.issue_key` (legacy Jira-specific key)
3. Workflow context `jira_issue_key` (for active workflows)
4. `pull_request_id` (fallback used by `resolve_worktree_key` when no issue key is set)

### Jira Namespace

Jira commands use keys prefixed with `jira.`:

```bash
agdt-set jira.issue_key PROJECT-1234
agdt-set jira.comment "My comment"
```

## 4. CLI Commands

### State Management

| Command | Purpose | Example |
|---------|---------|---------|
| `agdt-set` | Set any key-value | `agdt-set pull_request_id 23046` |
| `agdt-get` | Get a value | `agdt-get content` |
| `agdt-delete` | Remove a key | `agdt-delete thread_id` |
| `agdt-clear` | Clear all state | `agdt-clear` |
| `agdt-show` | Show all state | `agdt-show` |

### Azure DevOps Actions (Background Tasks)

All action commands that mutate state or perform API calls spawn background tasks and return immediately with a task ID for tracking.

| Command | Purpose | Required State / CLI Args |
|---------|---------|---------------------------|
| `agdt-add-pull-request-comment` | Add new PR comment | pull_request_id, content |
| `agdt-approve-pull-request` | Approve PR with sentinel banner | pull_request_id, content |
| `agdt-create-pull-request` | Create a new pull request | source_branch, title OR `--source-branch`, `--title`, `--description` |
| `agdt-reply-to-pull-request-thread` | Reply to PR thread | pull_request_id, thread_id, content |
| `agdt-resolve-thread` | Resolve a PR thread | pull_request_id, thread_id |
| `agdt-mark-pull-request-draft` | Mark PR as draft | pull_request_id |
| `agdt-publish-pull-request` | Publish draft PR | pull_request_id |
| `agdt-run-e2e-tests` | Trigger E2E test pipeline | (pipeline params) |
| `agdt-run-wb-patch` | Trigger workbench patch pipeline | (pipeline params) |
| `agdt-review-pull-request` | Start PR review workflow | (optional) pull_request_id, jira.issue_key |

**Create Pull Request CLI Parameter Support:**

```bash
# Option A: With CLI parameters (explicit)
agdt-create-pull-request --source-branch "feature/PROJECT-1234/new-feature" --title "feat(#42): add new feature" --description "Description here"

# Option B: Parameterless (uses current state)
# Check current values: agdt-get source_branch, agdt-get title
agdt-create-pull-request
```

### Azure DevOps Query Commands (Background Tasks)

Query commands also spawn background tasks - results are written to output files:

| Command | Purpose | Required State | Output File |
|---------|---------|----------------|-------------|
| `agdt-get-pull-request-threads` | Get all PR comment threads | pull_request_id | `temp/pr-threads-{pr_id}.json` |
| `agdt-get-pull-request-details` | Get full PR details (diff, threads, iterations) | pull_request_id | `temp/pr-details-{pr_id}.json` |
| `agdt-get-run-details` | Get pipeline/build run details | run_id | `temp/run-details-{run_id}.json` |

### Azure DevOps Review Comment Cleanup (Synchronous)

`agdt-delete-pr-review-comments` bulk-deletes agentic-devtools review-scaffolding
(and/or a given author's) comment threads on a pull request. It runs **synchronously**
(not a background task) and is routed through the `CIPlatformProvider` abstraction:
Azure DevOps is the implemented backend, while GitHub raises `NotImplementedError`
(this is an Azure DevOps-only review-tooling operation). Defaults to a **dry run** —
pass `--execute` to actually delete.

| Command | Purpose | CLI Args |
|---------|---------|----------|
| `agdt-delete-pr-review-comments` | Bulk-delete review-scaffolding / author comment threads on an ADO PR | `--pr`/`-p` (required), `--org`, `--project`, `--repo`, `--author SUBSTRING`, `--execute` |

**Selection:** a non-deleted `text` comment that carries an `agdt-review` marker (the
safe default), plus — optionally — any comment whose author display/unique name contains
`--author` (case-insensitive). The org/project/repo coordinates resolve from explicit
args first, then `platform.azure_devops` in `.github/agdt-config.json`, then
`AzureDevOpsConfig.from_state()` (state values / git remote). An explicit `--org` always
targets the Azure DevOps backend.

```bash
# Dry-run (default): list the comments that WOULD be deleted, delete nothing
agdt-delete-pr-review-comments --pr 28127 --org https://dev.azure.com/swica --project DragonflyMgmt --repo dfly-platform-management --author Marsnik

# Execute: actually delete the selected comments
agdt-delete-pr-review-comments --pr 28127 --author Marsnik --execute
```

**Exit codes:** `0` on success (a dry run, or an execute with no failures), `1` when a
deletion fails or the provider is unsupported (e.g. a GitHub-hosted repository), and `2`
for configuration/resolution errors (e.g. an unresolvable organization/project/repository).

### Pull Request Review Commands (Background Tasks)

| Command | Purpose | Required State / CLI Args |
|---------|---------|---------------------------|
| `agdt-review-pull-request` | Start PR review workflow | (optional) pull_request_id or jira.issue_key |

**File Review Submission (v2):**

In v2, each file's review is submitted by a one-shot `file-reviewer` subagent using the
atomic answer write `agdt-file-review-write --file-key <fileKey> --answer-file <path-to-draft.json>`.
The subagent's answer `outcome` (`approve` / `request-changes` /
`request-changes-with-suggestion`) sets the file's status. There are no standalone
file-level CLI commands.

**Review Submission Behavior:**

When a file-reviewer subagent writes its answer, the review pipeline automatically:

1. Re-render and PATCH the current commit's consolidated review comment in Azure DevOps.
   While the review is in progress the comment stays lightweight (status headline only);
   once all files in scope are reviewed it is re-rendered with full content (root + 50k
   continuation replies). Code suggestions are also POSTed as separate line-anchored
   threads (background task)
2. Mark the file as reviewed in Azure DevOps (visible as "viewed" eye icon in PR UI)
3. Save the refreshed state to `review-state.json`
4. Print next steps (continue with next file or advance to summary step)

### GitHub PR CI Checks (Synchronous)

These commands interact with GitHub PRs via the `gh` CLI and return results directly (not background tasks).

| Command | Purpose | Required State / CLI Args |
|---------|---------|---------------------------|
| `agdt-gh-pr-checks-status` | Dual-source CI check verification | `--pr` or `github.pull_request_number`; `--repo` or git remote; optional `--head-sha` or `github.head_ref_oid` |

**`agdt-gh-pr-checks-status`** performs dual verification:

1. Fetches structured check results via `gh pr checks --json`
2. Verifies check-suite completion via `gh api` check-suites endpoint (when `head_sha` is available)

State keys written: `github.pr_checks_status`, `github.pr_checks_failed`, `github.pr_checks_pending`.

```bash
# With CLI parameters
agdt-gh-pr-checks-status --pr 1115 --repo swai-factory/agentic-devtools --head-sha abc123

# Using state (set via agdt-set)
agdt-gh-pr-checks-status
```

### GitHub PR Analysis (Synchronous)

| Command | Purpose | Required State / CLI Args |
|---------|---------|---------------------------|
| `agdt-gh-copilot-review-status` | Analyze Copilot review status for a PR | `github.pull_request_number`, `github.head_ref_oid` OR `--pr`, `--head-sha`; optional `github.repo` / `--repo` (auto-detected from git remote if omitted) |

**State Keys Written:**

- `github.copilot_review_status` — status string (`clean`, `no-review`, `has-feedback`, `changes-requested`, `unknown-state`)
- `github.copilot_review_id` — numeric review ID (or `null`)
- `github.copilot_review_node_id` — GraphQL node ID (or `null`)
- `github.copilot_review_url` — review URL (or `null`)

### Git Workflow Actions (Background Tasks)

All Git workflow commands spawn background tasks:

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-git-save-work` | Stage, commit/amend, publish/force-push | commit_message |
| `agdt-git-stage` | Stage all changes | (none) |
| `agdt-git-push` | Push to origin | (none) |
| `agdt-git-force-push` | Force push to origin | (none) |
| `agdt-git-publish` | Publish branch upstream | (none) |

**Smart Commit Detection:**
The `agdt-git-save-work` command automatically detects whether to create a new commit or amend:

- If branch has commits ahead of `origin/main` AND last commit contains current issue key → **amend + force push**
- Otherwise → **new commit + publish**

**Checklist Integration:**
Use `--completed` to mark checklist items during commit:

```bash
agdt-git-save-work --completed 1 2 3  # Marks items 1, 2, 3 complete before committing
```

**Optional CLI flags:**

- `--commit-message-title` - Title for a new commit (errors if branch already has commits ahead)
- `--overwrite-commit-message-title` - Overwrite the title of an existing commit (errors if no commits ahead)
- `--dry-run` - Preview operations without executing
- `--skip-stage` - Skip the staging step
- `--skip-push` - Skip the push step
- `--skip-rebase` - Skip the fetch/rebase onto main step

**Commit title parameters:**

Use `--commit-message-title` for creating new commits (the title comes from this flag; the body comes from the resolved `commit_message` source: `--commit-message` if provided, otherwise template/state):

```bash
agdt-git-save-work --commit-message-title "feat(#42): add webhook support"
```

Use `--overwrite-commit-message-title` to replace the title of an existing commit (preserves body):

```bash
agdt-git-save-work --overwrite-commit-message-title "feat(#42): improved webhook support"
```

`--overwrite-commit-message-title` preserves the existing commit body verbatim. The command prints transparency output showing:

- The resolved final commit message (for both create and amend)
- A before/after title diff (when `--overwrite-commit-message-title` is used)

```bash
agdt-git-save-work --dry-run
agdt-git-save-work --skip-stage --skip-push
agdt-set commit_message "feat(#42): my change

#42"
agdt-git-save-work --skip-push
```

**Optional Git state keys:**

- `commit_message_title` - Title for a new commit (same as `--commit-message-title`)
- `overwrite_commit_message_title` - Overwrite title for amend (same as `--overwrite-commit-message-title`)
- `dry_run` - Preview operations without executing
- `skip_stage` - Skip the staging step
- `skip_push` - Skip the push step
- `skip_publish` - Skip branch publish after initial commit

**State keys written by `agdt-git-save-work`:**

After a successful commit or amend (non-dry-run), `agdt-git-save-work` attempts to persist the following keys (best-effort; skipped if the `git log` read-back fails or returns empty output):

- `git.last_commit_title` - First line of the commit message (title)
- `git.last_commit_message` - Full commit message (trailing newline(s) stripped)
- `git.last_commit_body` - Body (everything after title, leading blank separator stripped)

**State key `git.last_commit_message` fallback:**

`agdt-git-save-work` does not currently fall back to `git.last_commit_message` as an input
when no commit message source is provided. If no `--commit-message` flag is passed and no
commit message is available from template/state, the command exits with an error.

### Commit Body File (`commit-body.md`)

The commit message body can be maintained in a dedicated per-worktree file:

```text
.agdt/workflows/{identity}/{worktree_key}/files/commit-body.md
```

**How it works:**

- AI agents (and humans) edit this markdown file for the commit body (and footer)
- When `agdt-git-save-work` runs, if `commit-body.md` exists and has non-whitespace content, the first line of the resolved commit message is
  used as the title and the file content replaces any inline body/footer
- If the file is missing, empty, or whitespace-only, `commit_message` is used unchanged (backward compatible)
- Overwrite-title mode (`--overwrite-commit-message-title` / `overwrite_commit_message_title`) preserves the existing commit body and ignores `commit-body.md`
- The file supports optional YAML frontmatter (delimited by `---`) for structured metadata
- Frontmatter is parsed but **excluded** from the git commit message — only the content after the closing `---` is injected
- Note: This repo requires the issue reference(s) (`#NNN` on GitHub) to be repeated in the commit footer (see `COMMIT_CONVENTION.md`); include them at the end of `commit-body.md` when using this feature.
- File must be under 100KB and valid UTF-8

**Example `commit-body.md`:**

```markdown
---
checklist_items_completed: [1, 2, 3]
review_status: approved
---
## Summary of changes

- Implemented webhook handler
- Added comprehensive unit tests
- Updated API documentation

#42
```

### Commit Body Commands (Synchronous)

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-commit-body-show` | Print commit-body.md content with metadata | (none) |

**`agdt-commit-body-show`** prints the file path, character length, frontmatter detection status, parsed frontmatter keys, and body content to stdout.
Exits with code 1 if the file is missing, exceeds 100KB, or has encoding errors.

### Jira Actions (Background Tasks)

All Jira action commands that mutate state spawn background tasks:

| Command | Purpose | Required State / CLI Args |
|---------|---------|---------------------------|
| `agdt-add-jira-comment` | Add comment to issue | `jira.issue_key`, `jira.comment` OR `--jira-comment`, `--jira-issue-key` |
| `agdt-update-jira-issue` | Update issue fields | jira.issue_key, plus field keys |
| `agdt-create-epic` | Create a new epic | jira.project_key, jira.summary, jira.epic_name, jira.role, jira.desired_outcome, jira.benefit |
| `agdt-create-issue` | Create a new issue | jira.project_key, jira.summary, jira.description (or role/outcome/benefit) |
| `agdt-create-subtask` | Create a subtask | jira.parent_key, jira.summary, jira.description |

**CLI Parameter Support:**
Some commands accept optional CLI arguments that override state values:

- `agdt-add-jira-comment --jira-comment "..." [--jira-issue-key KEY]` - Comment text and issue key can be passed directly
- When CLI args are provided, they are stored in state before execution

**Documentation Pattern for Commands with Optional Parameters:**

When documenting commands that support both CLI parameters and parameterless execution, show both options:

```bash
# Option A: With CLI parameters (explicit)
agdt-add-jira-comment --jira-comment "Your comment text here"

# Option B: Parameterless (uses current state)
# Current jira.issue_key: run `agdt-get jira.issue_key` to check
# Current jira.comment: run `agdt-get jira.comment` to check
agdt-add-jira-comment
```

This pattern:

1. Shows CLI parameters first for explicit, self-documenting usage
2. Shows parameterless alternative that uses current state values
3. Indicates how to check current state values before running parameterless

### Jira Role Management (Background Tasks)

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-list-project-roles` | List project roles | jira.project_key |
| `agdt-get-project-role-details` | Get role details | jira.project_key, jira.role_id |
| `agdt-add-users-to-project-role` | Add users to role | jira.project_key, jira.role_id, jira.usernames |
| `agdt-add-users-to-project-role-batch` | Batch add users | jira.project_key, jira.role_id, jira.usernames |
| `agdt-find-role-id-by-name` | Find role ID | jira.project_key, jira.role_name |
| `agdt-check-user-exists` | Check if user exists | jira.username |
| `agdt-check-users-exist` | Check multiple users | jira.usernames |

### Jira Query Commands (Background Tasks)

Jira query commands also spawn background tasks - results are written to output files:

| Command | Purpose | Required State | Output File |
|---------|---------|----------------|-------------|
| `agdt-get-jira-issue` | Get issue details | jira.issue_key | `temp/temp-get-issue-details-response.json` |
| `agdt-parse-jira-error-report` | Parse error report | (file input) | `temp/jira-error-report.json` |

**`agdt-get-jira-issue` Features:**

- **Automatic subtask detection**: Checks `issuetype.subtask` field
- **Automatic parent retrieval**: Fetches parent issue for subtasks → `temp/temp-get-parent-issue-details-response.json`
- **Automatic epic retrieval**: Fetches linked epic via `customfield_10008` → `temp/temp-get-epic-details-response.json`
- **Metadata references in state**: Stores file locations and timestamps, NOT full JSON:
  - `jira.issue_details` → `{location, retrievalTimestamp}`
  - `jira.parent_issue_details` → `{location, key, retrievalTimestamp}` (if subtask)
  - `jira.epic_details` → `{location, key, retrievalTimestamp}` (if epic linked)
- **Console output**: Prints formatted issue details including parent/epic info

**Update Issue State Keys:**

- `jira.summary` - New summary text
- `jira.description` - New description text
- `jira.labels` - Comma-separated labels (replaces existing)
- `jira.labels_add` - Comma-separated labels to add
- `jira.labels_remove` - Comma-separated labels to remove
- `jira.assignee` - Username to assign (empty string to unassign)
- `jira.priority` - Priority name (e.g., "High", "Medium", "Low")
- `jira.custom_fields` - JSON object of custom field IDs to values

### Background Task Management

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-tasks` | List all background tasks | (none) |
| `agdt-task-status` | Show detailed task status | background.task_id |
| `agdt-task-log` | Display task output log | background.task_id |
| `agdt-task-wait` | Wait for task completion | background.task_id |
| `agdt-tasks-clean` | Clean up expired tasks | (none) |

**Optional Background Task State Keys:**

- `background.task_id` - Task ID to query/wait for
- `background.timeout` - Wait timeout in seconds (default: 300)
- `background.poll_interval` - Poll interval in seconds (default: 2)
- `background.log_lines` - Number of log lines to show (negative for tail)
- `background.expiry_hours` - Hours before tasks expire (default: 24)

**Background Task Pattern:**

Action commands spawn background processes and return immediately:

```bash
# Start background operation
agdt-set jira.issue_key PROJECT-1234
agdt-set jira.comment "Processing complete"
agdt-add-jira-comment
# Output: Background task started: <task-id>

# Monitor the task
agdt-set background.task_id <task-id>
agdt-task-status   # Check current status
agdt-task-log      # View output log
agdt-task-wait     # Wait for completion
```

### GitHub PR Thread Resolution (Synchronous)

These commands interact with GitHub PRs via the `gh` CLI and GraphQL. They run **synchronously** (not as background tasks) and print structured JSON to stdout.

| Command | Purpose | Required CLI Args / State |
|---------|---------|---------------------------|
| `agdt-gh-resolve-review-threads` | Resolve PR review threads via GraphQL | `--pr` or `github.pull_request_number`; `--review-id` or `github.copilot_review_id` or `--comment-ids` |

**`agdt-gh-resolve-review-threads` Usage:**

```bash
# Resolve all threads for a specific review
agdt-gh-resolve-review-threads --pr 1115 --repo swai-factory/agentic-devtools --review-id 4066913338

# Resolve specific comment IDs only
agdt-gh-resolve-review-threads --pr 1115 --comment-ids 12345,12346,12347
```

**State keys written:** `github.threads_resolved_count`, `github.threads_failed_count`, `github.threads_already_resolved_count`, `github.threads_resolution_verified`.

### Testing Actions

**⚠️ CRITICAL FOR AI AGENTS: ALWAYS use these agdt-test commands, NEVER run pytest directly!**

These commands run tests in BACKGROUND TASKS and return immediately with a task ID.
The test suite has 2000+ tests and takes ~55 seconds - running synchronously causes
AI agents to think something went wrong and restart tests multiple times.

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-test` | Run full test suite with coverage (~55s) | (none) |
| `agdt-test-quick` | Run tests without coverage (faster) | (none) |
| `agdt-test-file --source-file <path>` | Run tests for a specific source file with 100% coverage — **legacy flat layout only** (`tests/test_<module>.py`); infers a flat test file path and FAILS for the 1:1:1 `tests/unit/` layout | (none - uses `--source-file` param) |
| `agdt-test-pattern <args>` | Run specific tests (SYNCHRONOUS; may inherit pytest `addopts` coverage settings). Use this for all 1:1:1 layout modules (`tests/unit/`). It is not a 100% branch-coverage verifier. | (none - takes args) |

**Basic workflow (full test suite):**

```bash
# Run tests in background
agdt-test

# Wait for completion (REQUIRED - do not skip!)
agdt-task-wait
```

**Running tests for a specific source file (with focused coverage):**

```bash
# Specify source file via --source-file (auto-saved to state for future runs)
agdt-test-file --source-file agentic_devtools/cli/release/commands.py
agdt-task-wait
```

This is ideal when working on a single module and wanting to ensure full test coverage.
The command:

- Infers the test file from the source file name (e.g., `commands.py` in `cli/release` → `test_release_commands.py`)
- Shows coverage ONLY for the specified source file (not the entire codebase)
- Requires 100% coverage for the target file to pass
- Auto-saves `--source-file` to state, so subsequent runs can omit the parameter

**⚠️ CRITICAL: `agdt-test-file` only works with legacy flat test layout**

`agdt-test-file` infers the test file as `tests/test_<module>.py`. It **FAILS** for modules
whose tests follow the 1:1:1 `tests/unit/` layout (which is the required layout for all new tests) —
it will fail with an `Error: Test file not found: ...` message when no matching flat test file exists.
If your module's tests are under `tests/unit/`, use these alternatives:

```bash
# Run tests for a 1:1:1 module (e.g., tests/unit/cli/git/core/):
agdt-test-pattern tests/unit/cli/git/core/ -v -o addopts=

# Run coverage check for specific changed files only:
bash scripts/targeted-checks.sh

# Full test suite with coverage:
agdt-test && agdt-task-wait
```

**Note:** `agdt-test-pattern` is not a 100% branch-coverage verifier. In this repo it may inherit default pytest `addopts`; for focused runs use `-o addopts=`.
Use `bash scripts/targeted-checks.sh` for per-file 100% branch-coverage verification.
`agdt-test` runs the full suite with an overall line-coverage gate (`--cov-fail-under=100`) but does **not** enable `--cov-branch` and therefore does not verify branch coverage.

**Note:** `agdt-test-pattern` runs **synchronously** — do **NOT** call `agdt-task-wait`
after it. Only `agdt-test`, `agdt-test-quick`, and `agdt-test-file` spawn background tasks that require waiting.
(Based on audit of PRs #2681, #2702, #2714)

**Running specific test classes or methods (synchronous):**

```bash
# Test a specific class
agdt-test-pattern tests/unit/cli/github/repo_resolution/test_resolve_github_repo.py::TestResolveGithubRepo -v -o addopts=

# Test a specific method
agdt-test-pattern "tests/unit/state/test_get_workflow_state.py::test_get_workflow_state_returns_active_workflow" -v -o addopts=
```

**When to use which command:**

- `agdt-test` - Full suite validation before committing (required!)
- `agdt-test-quick` - Full suite without coverage for faster iteration
- `agdt-test-file` - **Legacy flat layout only** (`tests/test_<module>.py`); does **not** run `tests/unit/**` tests. Use `agdt-test-pattern` for 1:1:1 layout modules.
- `agdt-test-pattern tests/unit/<module_path>/<source_file_name>/ -v -o addopts=` - Focused run for 1:1:1 layout modules (any module under `tests/unit/`); **does NOT verify coverage**
- `bash scripts/targeted-checks.sh` - Per-file 100% branch coverage for changed files; works with 1:1:1 layout

**DO NOT:**

- Run `pytest` directly
- Run multiple test commands in parallel
- Assume tests failed if command returns quickly (they run in background)

### Workflow Management

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-get-workflow` | Display current workflow state | (none) |
| `agdt-clear-workflow` | Clear workflow state | (none) |
| `agdt-create-checklist` | Create checklist from CLI args | (none - takes args) |
| `agdt-update-checklist` | Update checklist items | (checklist in workflow) |
| `agdt-show-checklist` | Display current checklist | (checklist in workflow) |

### Workflow Initiation Commands

These commands initiate a workflow, loading and rendering the appropriate prompt template:

| Command | Purpose | Required Parameters |
|---------|---------|---------------------|
| `agdt-initiate-pull-request-review-workflow` | Start PR review workflow | `--pull-request-id` or `--issue-key`; optional `--interactive false` for pipeline mode; optional `--force-rereview` to re-run a same-model review of the current commit |
| `agdt-initiate-work-on-jira-issue-workflow` | Start work on Jira issue | `--issue-key` |
| `agdt-initiate-create-jira-issue-workflow` | Start create issue workflow | `--project-key` |
| `agdt-initiate-create-jira-epic-workflow` | Start create epic workflow | `--project-key` |
| `agdt-initiate-create-jira-subtask-workflow` | Start create subtask workflow | `--parent-key` |
| `agdt-initiate-update-jira-issue-workflow` | Start update issue workflow | `--issue-key` |
| `agdt-initiate-apply-pr-suggestions-workflow` | Apply PR review suggestions | `--pull-request-id` |
| `agdt-advance-workflow` | Advance to next workflow step | (active workflow) |

**Workflow Command Behavior:**

1. Validates required state keys are present
2. Collects variables from state (both required and optional)
3. Loads the appropriate prompt template (override if exists, else default)
4. Validates override template doesn't introduce new variables
5. Substitutes variables into the template
6. Saves the rendered prompt to `temp-<workflow>-<step>-prompt.md` in the workflow state directory
7. Logs the prompt to console with a notice of where it was saved
8. Updates workflow state (name, status=active, step, context)

### Work-on-Jira-Issue Workflow (State Machine)

The `agdt-initiate-work-on-jira-issue-workflow` uses an enhanced state-machine approach:

**Pre-flight Checks:**

1. Validates that the current folder contains the issue key (case-insensitive)
2. Validates that the current git branch contains the issue key
3. If either check fails → outputs setup instructions with worktree/branch commands

**Steps:**

1. **setup** - If pre-flight fails: instructions to create worktree and branch
2. **retrieve** - Auto-fetches Jira issue details via `agdt-get-jira-issue`
3. **planning** - Analyze issue and post plan comment to Jira
4. **checklist-creation** - Create implementation checklist based on plan
5. **implementation** - Code changes, tests, documentation (with checklist tracking)
6. **implementation-review** - Review completed checklist before verification
7. **verification** - Run tests and quality gates
8. **commit** - Stage and commit changes
9. **pull-request** - Create PR
10. **completion** - Post final Jira comment

**Automatic Workflow Advancement:**

- `agdt-add-jira-comment` advances from `planning` → `checklist-creation`
- `agdt-create-checklist` advances from `checklist-creation` → `implementation`
- When all checklist items are complete → advances to `implementation-review`
- `agdt-git-save-work` advances from `commit` → `pull-request`
- `agdt-create-pull-request` advances from `pull-request` → `completion`

### Checklist Management Commands

| Command | Purpose | Required State |
|---------|---------|----------------|
| `agdt-create-checklist` | Create checklist from CLI args | (none - takes args) |
| `agdt-update-checklist` | Update checklist items | (checklist in workflow) |
| `agdt-show-checklist` | Display current checklist | (checklist in workflow) |

**Usage:**

```bash
# Create checklist
agdt-create-checklist "Implement domain model" "Add tests" "Update docs"

# Mark items as complete
agdt-update-checklist --completed 1 2

# View checklist
agdt-show-checklist

# Mark items complete during commit
agdt-git-save-work --completed 1 2 3
```

**Manual Advancement:**

```bash
# Advance to a specific step
agdt-advance-workflow implementation
agdt-advance-workflow verification
agdt-advance-workflow commit

# View current step
agdt-get-workflow
```

**Usage Examples:**

```bash
# Start work on a Jira issue
agdt-set jira.issue_key PROJECT-1234
agdt-initiate-work-on-jira-issue-workflow
# If pre-flight fails: shows worktree/branch setup instructions
# If pre-flight passes: auto-fetches issue and shows planning prompt

# Check current workflow
agdt-get-workflow
# Output: name=work-on-jira-issue, status=in-progress, step=planning

# Post plan comment (auto-advances to implementation)
agdt-set jira.comment "h4. Plan..."
agdt-add-jira-comment

# Advance manually when needed
agdt-advance-workflow verification

# Clear workflow when done
agdt-clear-workflow
```

**Usage examples:**

```bash
# ⚠️ ALWAYS use agdt-test commands, NEVER pytest directly!

# Full suite with coverage (runs in background, ~55 seconds)
agdt-test
agdt-task-wait   # REQUIRED: Wait for completion

# Quick run without coverage (runs in background)
agdt-test-quick
agdt-task-wait

# Specific file, class, or test via CLI argument (runs synchronously)
agdt-test-pattern tests/unit/cli/github/repo_resolution/test_resolve_github_repo.py::TestResolveGithubRepo -v -o addopts=
agdt-test-pattern tests/unit/cli/github/repo_resolution/test_resolve_github_repo.py::TestResolveGithubRepo::test_cli_arg_preferred -v -o addopts=

# Test a specific source file with 100% coverage (runs in background)
agdt-test-file --source-file agentic_devtools/cli/release/commands.py  # Auto-saved to state
agdt-task-wait   # REQUIRED: Wait for completion
```

## 5. Key Design Decisions

### No Replacement Tokens

Unlike PowerShell, Python CLI handles special characters natively:

```bash
# This just works - no token replacement needed!
agdt-set content "Code with (parentheses) and [brackets]"
```

### No Multi-line Builder

Python preserves multiline strings directly:

```bash
agdt-set content "Line 1
Line 2
Line 3"
```

### Generic Setter Pattern

The `agdt-set` command accepts any key, so AI agents only need to approve it once:

```bash
agdt-set pr_id 12345       # Same command pattern
agdt-set thread_id 67890   # Same command pattern
agdt-set content "text"    # Same command pattern
```

### ⚠️ Security: `run_safe` and User-Controlled Content

When calling `run_safe` with a list of arguments that includes
**user-controlled text** (e.g., issue titles, PR body content,
commit messages, file paths from state),
always pass **`shell=False`** explicitly:

```python
# ✅ CORRECT — user-controlled text, shell=False prevents env-var expansion on Windows
result = run_safe(
    ["gh", "issue", "create", "--title", title, "--body", body], capture_output=True, text=True, shell=False
)

# ❌ DANGEROUS — default shell=None uses shell=True on Windows, allowing cmd.exe to
#    expand %VARIABLE% patterns (e.g., %GITHUB_TOKEN%) in user-supplied text
result = run_safe(["gh", "issue", "create", "--title", title, "--body", body], capture_output=True, text=True)
```

Only omit `shell=False` (or pass `shell=True` explicitly) for commands
known to be **`.cmd`/`.bat` batch scripts** on Windows, such as `az`
(Azure CLI) and `npm`. Proper executables like `gh`, `git`, `code`,
`python`, etc. must always be called with `shell=False` when argument
values contain user-supplied data.

For **`.cmd`/`.bat` batch scripts** like `az`, rely on `run_safe`'s
Windows-only default (`shell=None`) rather than passing `shell=True`
explicitly. This way, Linux/macOS runs use `shell=False` (more secure),
and Windows still gets `shell=True` automatically for batch scripts:

```python
# ✅ CORRECT — shell=None default: shell=True on Windows only, shell=False elsewhere
result = run_safe(["az", "repos", "pr", "show", "--id", str(pr_id)], capture_output=True, text=True)

# ❌ AVOID — forces shell=True on all platforms, expanding attack surface on Linux/macOS
result = run_safe(["az", "repos", "pr", "show", "--id", str(pr_id)], capture_output=True, text=True, shell=True)
```

## 6. Prompt Template System

Workflow commands use a template system for generating prompts:

### Template Location

- **Default templates**: `agentic_devtools/prompts/default-<step>-<workflow>-prompt.md`
- **Override templates**: `agentic_devtools/prompts/override-<step>-<workflow>-prompt.md`
- **Generated output**: `temp-<workflow>-<step>-prompt.md` in the workflow state directory

### Template Variables

Templates use `{{variable_name}}` syntax for substitution:

```markdown
# Working on {{jira_issue_key}}

You are working on Jira issue **{{jira_issue_key}}**.
```

Variables are populated from state, with dot notation converted to underscores:

- State key `jira.issue_key` → Template variable `{{jira_issue_key}}`
- State key `pull_request_id` → Template variable `{{pull_request_id}}`

### Override Templates

Users can create override templates to customize workflow prompts:

1. Copy the default template to an override file
2. Modify as needed
3. **Important**: Override templates cannot introduce new variables not present in the default

Example validation error:

```text
TemplateValidationError: Override template uses variables not in default: {'new_variable'}
```

### Creating Custom Workflows

To add a new workflow:

1. Create the default prompt template in `agentic_devtools/prompts/`
2. Add the workflow initiation function in `cli/workflows/commands.py`
3. Export the function in `cli/workflows/__init__.py`
4. Add the CLI entry point in `pyproject.toml`
5. Document the required state keys

## 7. Installation

```bash
# From the package directory (scripts/agentic_devtools)
pip install -e .

# Or with dev dependencies
pip install -e ".[dev]"
```

### Multi-Worktree State Isolation

When working with multiple git worktrees (e.g., one for feature development and another for the main branch),
the `agdt-ai-helpers` package uses **smart repo-local state detection** to keep state isolated per worktree.

**How it works:**

1. State files are stored in `.agdt/workflows/{identity}/{worktree_key}/` relative to the repository root
2. The `get_state_dir()` function resolves the path via `.agdt/runtime-bootstrap.json`
3. Directories are automatically created as needed
4. This ensures each worktree has its own isolated state, even with a single global pip installation

**State directory resolution priority:**

1. `AGENTIC_DEVTOOLS_STATE_DIR` environment variable (explicit override)
2. `.agdt/workflows/{identity}/{worktree_key}/` via bootstrap (auto-detected)
3. `.agdt/workflows/_unscoped/` fallback
4. `.agdt-temp/` in current working directory (fallback for non-repo contexts)

**New worktree setup:**
When creating a new worktree, `.agdt/workflows/` doesn't exist initially (it's gitignored). The helpers automatically create it on first use, so no manual setup is required.

**Tip:** Use the `agdt-initiate-work-on-jira-issue-workflow` command which includes worktree setup automation with VS Code integration.

## 8. Environment Variables

| Variable | Purpose |
|----------|---------||
| `AZURE_DEV_OPS_COPILOT_PAT` | PAT for Azure DevOps API calls |
| `JIRA_COPILOT_PAT` | PAT for Jira API calls |
| `JIRA_BASE_URL` | Override default Jira URL (default: `https://jira.swica.ch`) |
| `JIRA_SSL_VERIFY` | Set to "0" to disable SSL verification |
| `JIRA_CA_BUNDLE` | Path to custom CA bundle PEM file for Jira SSL |
| `REQUESTS_CA_BUNDLE` | Standard requests library CA bundle path (fallback) |
| `AGENTIC_DEVTOOLS_STATE_DIR` | Override default state directory (`.agdt/workflows/_unscoped`) |
| `AGDT_DRY_RUN` | Set to "1" for dry-run mode globally |
| `AGDT_PLAN_CONTEXT_BUDGET` | Plan-phase context budget in characters (default: `32000`). If set, oversized spec content is reduced deterministically before calling the LLM |

### SSL Certificate Handling

The Jira commands automatically handle SSL certificate verification for corporate internal CAs:

1. **Environment override**: Set `JIRA_SSL_VERIFY=0` to disable verification entirely
2. **Custom CA bundle**: Set `JIRA_CA_BUNDLE` to a path containing your CA certificates
3. **Repo-committed bundle**: The preferred CA bundle is committed at `scripts/jira_ca_bundle.pem` (contains the full certificate chain for jira.swica.ch)
4. **Auto-fetch fallback**: If the repo bundle doesn't exist, certificates are auto-fetched from the Jira server using `openssl s_client` and cached to `jira_ca_bundle.pem` in the state directory
5. **Last resort**: If auto-fetch fails, SSL verification is disabled with a warning

## 9. Adding New Commands

### Adding a new action command (background task pattern)

When adding a new command that performs side effects (API calls, git operations, file mutations):

1. **Add the sync function** in the appropriate module (e.g., `cli/azure_devops/commands.py`)
2. Read required state with `get_value()` or use existing helper functions like `parse_bool_from_state()`
3. Export the function in the module's `__init__.py`
4. **Create an async wrapper** that uses `run_in_background()` to spawn the sync command as a background task
5. **Add entry point** in `pyproject.toml`:

   ```toml
   [project.scripts]
   agdt-new-command = "agentic_devtools.cli.module.commands:new_command_async"
   ```

6. Reinstall: `pip install -e .`
7. **Document the command** in this file

### Adding new helper functions

1. Add pure utility functions (no state reading) to `cli/azure_devops/helpers.py`
2. Add functions that read state to `cli/azure_devops/commands.py`
3. Export in `__init__.py` if needed for external use

### Adding convenience state functions

Add typed functions in `state.py`:

```python
def get_new_field(required: bool = False) -> Optional[str]:
    """Get the new field from state."""
    return get_value("new_field", required=required)
```

## 10. Protected / Auto-Generated Files

> **⚠️ AI AGENTS: Never edit, stage, or commit the files listed here.**
> They are auto-generated at build time. Any local changes are noise and must be discarded.

### `agentic_devtools/_version.py`

Generated automatically by `hatch-vcs` / `setuptools-scm` from Git tags. Its header reads:
*"file generated by setuptools-scm — don't change, don't track in version control."*

The file is listed in `.gitignore` and is **not tracked by git**. **Do not** `git add`, edit, or
commit it. Because it is untracked, `.gitignore` prevents `git add .` from ever staging it —
this applies to all tools including `report_progress` and the Copilot coding agent's own git
operations.

> **ℹ️ Note:** `agdt-git-save-work` also explicitly unstages `agentic_devtools/_version.py`
> before creating a commit, so it is doubly protected. If you ever see it appearing in
> `git status`, do **not** stage or commit it. For test-specific guidance see `tests/README.md`.

- To bump the package version, create a new Git tag — see [RELEASING.md](../RELEASING.md).

### Identifying auto-generated files in the future

A file is auto-generated when it contains a comment such as:

```text
# file generated by <tool>
# don't change, don't track in version control
```

When you encounter such a comment in a file you are about to edit or commit, **stop** — the file
should not be touched manually.

### Excluding auto-generated files from source-scanning tools

Any script that iterates over `agentic_devtools/` source files (e.g., `scripts/scaffold_tests.py`)
must **explicitly exclude** `_version.py` by name, in addition to `__init__.py`. Even though
`_version.py` currently has no public functions, the exclusion makes the intent clear and prevents
the file from accidentally appearing in generated output if it ever gains a public symbol.

Pattern to follow:

```python
if p.name not in {"__init__.py", "_version.py"} and "__pycache__" not in p.parts
```

## 11. 1:1:1 Test Structure

All **unit tests** must be placed under `tests/unit/` and follow the 1:1:1 policy.
For the full reference, see [tests/README.md](../tests/README.md).

### Policy Summary

- **One folder per source file** — the test path mirrors the source path.
- **One test file per symbol** — one function or class per test file.
- **Every directory must contain `__init__.py`**.

### Path Convention

```text
tests/unit/{module_path}/{source_file_name}/test_{symbol_name}.py
```

| Source symbol | Test file |
|---|---|
| `agentic_devtools/cli/git/core.py` → `get_current_branch()` | `tests/unit/cli/git/core/test_get_current_branch.py` |
| `agentic_devtools/state.py` → `get_value()` | `tests/unit/state/test_get_value.py` |
| `agentic_devtools/cli/workflows/worktree_setup.py` → `WorktreeSetupResult` | `tests/unit/cli/workflows/worktree_setup/test_worktreesetupresult.py` |

Symbol naming rules:

- **Functions**: exact snake\_case name (e.g., `get_current_branch` → `test_get_current_branch.py`).
- **Classes / dataclasses / enums**: class name lowercased **without** added underscores
  (e.g., `WorktreeSetupResult` → `test_worktreesetupresult.py`).

### Top-level source files (e.g., `state.py`, `background_tasks.py`)

Source files directly in `agentic_devtools/` (with no subdirectory) map to:

```text
tests/unit/{source_file_name}/test_{symbol_name}.py
```

For example:

- `agentic_devtools/state.py` → `tests/unit/state/test_get_workflow_state.py`
- `agentic_devtools/background_tasks.py` → `tests/unit/background_tasks/test_run_function_in_background.py`

The validator (`scripts/validate_test_structure.py`) requires a minimum of **2 path components**
under `tests/unit/`, so top-level source files are fully supported without any workaround.

> **⚠️ Do NOT create proxy/stub source files** (e.g., `agentic_devtools/root/state.py`) just to
> add an extra path component. This approach pollutes the package with files that serve no
> purpose beyond satisfying a validator that already handles the case natively.

### How to Add a New Test

1. Identify the source file, e.g. `agentic_devtools/cli/git/core.py`.
2. Create `tests/unit/cli/git/core/` (and parent dirs if missing), each with `__init__.py`.
3. Create `tests/unit/cli/git/core/test_<symbol>.py` for the function or class under test.
4. Run the validator: `python scripts/validate_test_structure.py`

See [tests/README.md](../tests/README.md) for complete step-by-step instructions and examples.

### CI Enforcement

`scripts/validate_test_structure.py` runs in CI and **fails the build** when it finds structural
issues in `tests/unit/` (incorrect source-file folder mapping or missing `__init__.py` files).
It does **not** verify that every symbol has a corresponding test file or that each
`test_<symbol>.py` maps to a real symbol — those parts of the 1:1:1 pattern are enforced via
convention and code review. Run it locally before pushing:

```bash
python scripts/validate_test_structure.py
```

### Class symbols: all class methods go in one test file

The "one test file per symbol" rule means **one test file per class** — not one file per method.
For all **new** code, all methods of a class should be tested in the single class-level test file
named after the class. Existing per-method files are grandfathered; standalone module-level functions
still get their own test files as per the 1:1:1 convention.

❌ **Wrong** — splitting one class into per-method files:

```text
tests/unit/adapters/jira_adapter/test_get_issue_raw.py
tests/unit/adapters/jira_adapter/test_normalize.py
tests/unit/adapters/jira_adapter/test_create_issue.py
tests/unit/adapters/jira_adapter/test_set_issue_type.py
```

✅ **Correct** — one file for the whole class (`JiraAdapter`):

```text
tests/unit/adapters/jira_adapter/test_jiraadapter.py
```

Inside `test_jiraadapter.py`, use top-level `Test*` classes to group coverage by method:

```python
class TestJiraAdapterGetIssueRaw:
    def test_returns_raw_issue_dict(self) -> None: ...


class TestJiraAdapterNormalize:
    def test_normalizes_title(self) -> None: ...


class TestJiraAdapterCreateIssue:
    def test_creates_issue_and_returns_key(self) -> None: ...
```

Based on audit of PRs #2663, #2653, #2644, #2656, #2666, #2675.

### Integration and Workflow Tests

The 1:1:1 policy and `tests/unit/` directory apply exclusively to **unit tests** — tests that
exercise a single symbol in isolation, without external services or persistent side-effects
outside the test's own scope. Temporary filesystem usage via pytest's `tmp_path` fixture is
permitted in unit tests.

**Do NOT place integration or workflow tests in `tests/unit/`.**

Integration tests (tests that cross multiple components, use persistent external state, or exercise
full agent workflows) belong in `tests/workflows/`. Placing an integration test under `tests/unit/`
violates the 1:1:1 contract and must be caught in code review — `scripts/validate_test_structure.py` only
verifies directory/source-file path mapping and `__init__.py` presence; it cannot detect whether a
test is integration-style.

| Test type | Correct location |
|---|---|
| Unit test for `IssueAdapter` | `tests/unit/adapters/base/test_issueadapter.py` |
| Integration test for complete workflow lifecycles | `tests/workflows/test_workflow_end_to_end.py` |

A test is an **integration test** when it:

- Exercises more than one module or component together
- Uses persistent external state, real network sockets, external services, or real subprocesses (temporary file writes via pytest's `tmp_path` do not qualify)
- Simulates a full agent workflow or multi-step command sequence

Based on audit of PRs #2641, #2653.

### Performance Benchmarks in Tests

**Do NOT write wall-clock performance assertions in `tests/unit/` files.** Time-based thresholds
(e.g., "must complete in <5ms", "must complete in <100ms") are inherently flaky on shared CI
runners due to load variation and produce false failures unrelated to code correctness.

If a specification requires performance validation, plan it as a **manual validation step** or a
separate benchmark task — not as a pytest assertion with a fixed wall-clock threshold.

Based on audit of PRs #2635, #2641.

## 12. Python Coding Patterns

### Import ordering (ruff/isort)

Ruff enforces isort-style import ordering (rule set `I`). All Python files must use this order:

1. **Standard library** imports
2. **Third-party** imports (e.g., `pytest`, `requests`)
3. **First-party** imports (`agentic_devtools.*` — the only first-party package; the legacy `agdt_ai_helpers` shim has been removed)

Within each group, `import X` lines come before `from X import Y` lines.

```python
# Correct
import os
import time
from unittest.mock import patch  # stdlib — same group as os/time

import pytest  # third-party

from agentic_devtools.state import get_value  # first-party
```

```python
# Wrong — stdlib 'from unittest.mock import patch' placed after third-party 'import pytest'
import pytest
from unittest.mock import patch  # ← ruff I001 violation
```

To auto-fix: `ruff check --fix tests/` or `ruff check --fix --select I tests/`

### Defensive validation in dict-accepting functions

When implementing functions that accept `dict[str, Any]` or untyped external data
(e.g., adapter `normalize()` methods, `derive_*` helpers that receive node dicts), always
validate that required values are present **and have the expected type** before using them.
Never let `AttributeError`, `KeyError`, or `TypeError` propagate to callers uncontrolled.

```python
# ✅ CORRECT — validate type before calling .get()
raw = issue_detail.get("raw")
if not isinstance(raw, dict):
    raise AdapterValidationError(f"Expected 'raw' to be a dict, got {type(raw).__name__}")
title = raw.get("title", "")

# ❌ DANGEROUS — AttributeError if raw is a list, None, or any non-dict truthy value
title = issue_detail["raw"].get("title", "")
```

Additional rules:

- **Typed dataclass fields**: if a dataclass field is typed as `str`, ensure the value
  assigned to it is actually a `str` (not `None` or another type from `node.get()`).
  Use an explicit type check with a controlled error or an explicit empty-string default
  rather than `str(value)` coercion — `str(None)` silently produces the literal `"None"`,
  which masks missing data.
- **Public API argument validation**: functions exported from a module (e.g., from
  `__init__.py`) must guard against invalid argument combinations (e.g.,
  `max_depth <= 0`) with a clear `ValueError` and message — negative values can produce
  a silent wrong result (Python's negative list indexing returns an element rather than
  raising an error) or an `IndexError` that is harder to diagnose.
- **Path/ID validation**: if a function builds a file path or URL from a caller-supplied
  identifier (e.g., `issue_id`), validate the identifier against the expected pattern
  (e.g., `_ID_PATTERN`) before constructing the path to prevent path-traversal issues.

*Based on audit of PRs #2722, #2725 (6+ observations across 2 PRs).*

### Dynamic script loading with `importlib`

When loading a script dynamically (e.g., loading a `scripts/*.py` file by path), always guard
against `None` returns from `spec_from_file_location()` before calling `module_from_spec()` and
`exec_module()`:

```python
import importlib.util

spec = importlib.util.spec_from_file_location("module_name", script_path)
if spec is None or spec.loader is None:
    raise RuntimeError(f"Could not load module from {script_path!s}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
```

`spec_from_file_location()` returns `None` when the file cannot be found or the path is not
recognised as a loadable Python source. Without the guard, the very first call — `module_from_spec(spec)`
— raises an obscure `AttributeError: 'NoneType' object has no attribute ...` that hides the real
problem. The explicit `RuntimeError` surfaces the actual path that failed, making failures
trivially easy to diagnose.

### Docstring accuracy for config/option parameters

When a function accepts a config or options object, the docstring's description of which
fields are read must exactly match the implementation. Do not write "the client uses
`config.headers`" if the implementation only reads `config.headers.get("Authorization")`. Do not
describe a return value as "the full parsed response" if the implementation only preserves a
subset of fields. Inaccurate docstrings mislead callers and reviewers, and create false
assumptions in tests.

```python
# ❌ INCORRECT — docstring claims all headers are used but only Authorization is read
def build_jira_client(config: JiraConfig | None = None):
    """Build a configured client.

    Args:
        config: When provided, the client uses ``config.base_url``,
            ``config.headers``, and ``config.ssl_verify`` directly.  ← inaccurate
    """
    ...
    auth_header = config.headers.get("Authorization")  # only Authorization is read


# ✅ CORRECT — docstring matches what is actually accessed
def build_jira_client(config: JiraConfig | None = None):
    """Build a configured client.

    Args:
        config: When provided, the client uses ``config.base_url``,
            ``config.ssl_verify``, and ``config.headers.get("Authorization")``
            directly.  ← matches implementation exactly
    """
```

Applies equally to return-value docstrings: if a method returns an object whose `raw` field
contains only a subset of the provider response, document which keys are preserved rather
than claiming to return the "full" response.

*Rationale: based on audit findings in PRs #2838 and #2820 (3+ observations where docstrings
misrepresented which config fields or response fields were actually used).*

### Retry / backoff logic

Retry helpers (e.g. `agentic_devtools/adapters/retry.py`, `agentic_devtools/cli/shared/retry.py`)
have repeatedly shipped with the same set of pitfalls. When writing or modifying retry/backoff
logic:

- **Match transient-error conditions narrowly.** Don't classify an exception as transient by
  doing a substring search for a status code (e.g. `"429" in str(exc)`) — this also matches
  unrelated exceptions such as `ValueError("1429 rows")`. Check the actual status code /
  exception type instead.
- **Preserve the original traceback when re-raising after retries are exhausted.** Inside the
  handler that caught the exception, use a bare `raise` to preserve the original traceback. If you
  must re-raise outside that handler, capture the traceback and raise with
  `exc.with_traceback(tb)` rather than `raise exc`/`raise last_exc`, which starts a new traceback.
- **Retry only the failing step, not the whole operation.** If a multi-page or multi-step
  operation (e.g. paginated search) is wrapped in a single retry decorator, a transient failure
  on a later page restarts the entire operation from the beginning and discards progress/API
  quota already spent. Scope the retry decorator to the smallest failing unit of work.
- **Validate retry/backoff configuration parameters** (e.g. `jitter_factor`, base delay) at the
  point they're accepted, not just in the docstring. An unvalidated negative `jitter_factor` can
  produce a negative sleep duration passed to `random.uniform(...)`/`time.sleep(...)`.
- **Name the exception raised when retries are exhausted after the actual failure**, not a
  single catch-all like `ProviderRateLimitError` for every retryable exception type — a
  misleading name makes the real cause harder to diagnose from logs/error reports.

*Rationale: based on audit findings in PRs #2777 and #2801.*

### Keep docstrings and comments synchronized with implementation

Several PRs contained docstrings/comments describing behavior that no longer matched the code
(e.g., a "diff direction" described backwards, a module docstring claiming a function builds a
"hierarchical" structure when it just returns the input lists unchanged, a docstring pointing to
the wrong function as the one wrapped by a decorator). When changing behavior, update the
docstring/comment in the same change; when reviewing, treat a stale docstring as a defect, not
a nit.

Test docstrings must describe the actual behavior under test in plain language — do not use
placeholder text (e.g., `"******"`) in place of the real assertion being made (such as which
auth scheme takes precedence). Placeholder docstrings make test failures harder to interpret and
provide no value over no docstring at all.

*Rationale: based on audit findings in PRs #2777 and #2801 (8+ occurrences).*

### Remove or wire up dead code

Don't leave unused constants, unused compiled regexes, or no-op expressions (e.g., a stray
triple-quoted string directly under a constant definition that is never assigned to a docstring
attribute and never read) in the codebase. These artifacts read as if they are load-bearing
(validation source-of-truth, documentation, etc.) but are not, which misleads future readers and
maintainers. Either delete the dead artifact or wire it up to the behavior it appears to describe.

*Rationale: based on audit findings in PRs #2777, #2801, and #2771.*

### Docstring accuracy

When writing or modifying a docstring, verify that every claim in it is accurate **at the
time of writing**. Three common inaccuracy patterns to avoid:

1. **Wrong requirement reference** — Verify that any `FR-XXX` or `SC-XXX` ID in a
   docstring matches the actual requirement by checking the spec. A copy-pasted docstring
   with an incorrect FR number misleads future maintainers who trace behavior back to the
   spec.

2. **Non-existent attribute** — Do not document an attribute or parameter in a class
   docstring unless it already exists on the class. Documenting a planned field before it
   is implemented causes confusion for consumers who read the docstring expecting the
   attribute to be present.

3. **Behavior mismatch** — The docstring description must match what the code actually
   does, not what was intended or what a future refactor will do. If the docstring says
   "requires zero state changes recorded" but the implementation only checks elapsed time,
   the docstring is wrong and must be corrected.

```python
# ❌ Wrong FR reference (should be FR-003 per the spec, not FR-004)
#: _GITHUB_PREFIX_PATTERN — matches the GitHub URL prefix (FR-004)

# ✅ Correct FR reference (verified against spec.md)
#: _GITHUB_PREFIX_PATTERN — matches the GitHub URL prefix (FR-003)
```

```python
# ❌ Documenting an attribute that doesn't exist yet
class SetupReport:
    """
    Report for setup execution.

    Dry-run mode is indicated via the `mode` field.  # ← mode doesn't exist
    """


# ✅ Only document what is actually on the class
class SetupReport:
    """Report for setup execution."""
```

*Based on audit of PRs #2712, #2744, #2783 (3 observations across 3 PRs).*

## 13. Testing

**⚠️ AI AGENTS: ALWAYS use agdt-test commands, NEVER pytest directly!**

```bash
# Run tests (ALWAYS use these commands)
agdt-test          # Full suite with coverage (background, ~55s)
agdt-task-wait     # Wait for completion (REQUIRED!)

agdt-test-quick    # Fast run without coverage (background)
agdt-task-wait     # Wait for completion (REQUIRED!)

# For specific tests (with CLI arguments, runs synchronously; use -o addopts= for focused runs)
agdt-test-pattern tests/unit/state/test_set_value.py::TestSetValue -v -o addopts=

# Per-file coverage check for 1:1:1 modules (tests/unit/):
bash scripts/targeted-checks.sh

# Test a specific source file with 100% coverage (LEGACY: flat tests/test_<module>.py ONLY)
# ⚠️ Only use agdt-test-file for legacy flat-layout modules (tests/test_<module>.py).
# For 1:1:1 layout modules (tests/unit/), use agdt-test-pattern or bash scripts/targeted-checks.sh.
# agdt-test-file ONLY runs the inferred flat test file — it does NOT run tests/unit/** tests.
agdt-test-file --source-file agentic_devtools/cli/release/commands.py  # Auto-saved for future runs
agdt-task-wait                                          # Wait for completion (REQUIRED!)

# For 1:1:1 layout modules, use agdt-test-pattern with the tests/unit/ folder:
agdt-test-pattern tests/unit/config/ -v -o addopts=  # For agentic_devtools/config.py (1:1:1 layout)
```

**WHY background tasks?** The test suite has 2000+ tests and takes ~55 seconds.
Running synchronously causes AI agents to think something went wrong during the
wait, leading to multiple restarts and wasted resources.

### ⚠️ `agdt-test-file` only works for legacy flat tests — NOT 1:1:1 unit tests

`agdt-test-file` infers the test file from the source *filename* (e.g. `state.py` → `tests/test_state.py`),
not from the full module path. For any source file
whose tests live under `tests/unit/`, this command **fails** with `Error: Test file not found`.

❌ **Wrong** (source file has 1:1:1 tests under `tests/unit/`):

```bash
agdt-test-file --source-file agentic_devtools/adapters/jira_adapter.py
# Fails: looks for tests/test_jira_adapter.py — does NOT run tests/unit/adapters/jira_adapter/
```

✅ **Correct** (use `agdt-test-pattern` or targeted checks):

```bash
agdt-test-pattern tests/unit/adapters/jira_adapter/ -v -o addopts=
# or
bash scripts/targeted-checks.sh
```

**Spec, plan, and task documents** must use `agdt-test-pattern` or `bash scripts/targeted-checks.sh`
when referencing test commands for modules under `tests/unit/`. Never reference `agdt-test-file`
for 1:1:1 layout modules in planning artifacts.

Based on audit of PRs #2663, #2656, #2666, #2644, #2675.

### ⚠️ `agdt-task-wait` is only for background test commands

Not all test commands are background tasks. Using `agdt-task-wait` after a synchronous command
is incorrect; omitting it after a background command leaves results unread.

| Command | Background? | Requires `agdt-task-wait`? |
|---|---|---|
| `agdt-test` | ✅ Yes | ✅ Yes |
| `agdt-test-quick` | ✅ Yes | ✅ Yes |
| `agdt-test-file` | ✅ Yes | ✅ Yes |
| `agdt-test-pattern` | ❌ No (synchronous) | ❌ No |

Based on audit of PRs #2663, #2656.

### TDD Workflow (Required for All Implementation Work)

**⚠️ AI AGENTS: Always follow the red-green-refactor TDD cycle. Write tests BEFORE
implementation code.**

For every new function, class, or behaviour change, follow this cycle:

#### Step 1 — RED: Write a failing test first

Create the test file following the 1:1:1 policy **before** touching source code:

```bash
# Create tests/unit/cli/git/core/test_new_function.py with expected behaviour
# Then confirm the test fails (RED):
agdt-test-pattern tests/unit/cli/git/core/test_new_function.py -v -o addopts=
# Expected output: FAILED
```

#### Step 2 — GREEN: Write the minimal implementation

Write only enough code to make the failing test pass:

```bash
# Edit agentic_devtools/cli/git/core.py with minimal implementation
# Then confirm tests pass (GREEN):
agdt-test-pattern tests/unit/cli/git/core/test_new_function.py -v -o addopts=
# Expected output: PASSED
```

#### Step 3 — REFACTOR: Improve without breaking tests

Tidy the code while keeping tests green. Use `agdt-test-pattern` pointed at the
1:1:1 test folder for the source file — **not** `agdt-test-file`, which only works
with legacy flat test files and will fail for any file under `tests/unit/`:

```bash
# Refactor and re-run focused tests:
agdt-test-pattern tests/unit/cli/git/core/ -v -o addopts=
```

#### Step 4 — Full suite check

After all checklist items are complete, run the full test suite:

```bash
agdt-test
agdt-task-wait
```

#### Why TDD?

- Ensures tests reflect **intended** behaviour, not implementation accidents.
- Forces minimal code — no speculative features that lack test coverage.
- Aligns with the existing 100% coverage requirement already enforced in CI.

## 14. Pre-commit Hooks

The repository has pre-commit hooks for linting Python files in `scripts/`. To enable:

```bash
git config core.hooksPath .githooks
```

The hooks run automatically on `git commit` and check:

- **ruff check**: Lints for pycodestyle errors (E), Pyflakes (F - unused imports/variables), warnings (W), import sorting (I), and pyupgrade (UP)
- **ruff format**: Auto-formats code
- **cspell**: Checks spelling against the dictionary in `cspell.json`

To disable hooks:

```bash
git config --unset core.hooksPath
```

To run checks manually:

```bash
# Ruff lint (with auto-fix)
ruff check --fix --config scripts/agentic_devtools/pyproject.toml scripts/agentic_devtools/

# Ruff format
ruff format --config scripts/agentic_devtools/pyproject.toml scripts/agentic_devtools/

# cspell (use global install, not npx)
cspell lint "scripts/**/*.py"
```

### cspell Dictionary Setup

The `cspell.json` file is the single source of truth for spelling. The VS Code workspace imports it via `cSpell.import`.

**Required global installs** (one-time setup, requires disconnecting from VPN):

```bash
npm install -g cspell @cspell/dict-de-de @cspell/dict-python @cspell/dict-dotnet @cspell/dict-companies @cspell/dict-fullstack @cspell/dict-typescript
```

To add new words, edit `cspell.json` directly (keep alphabetically sorted). Multiple language dictionaries are enabled via imports in `cspell.json`.

## 15. Commit Message Convention

Every commit **must** follow [Conventional Commits v1.0.0](https://www.conventionalcommits.org)
with a mandatory issue reference as the scope. See [COMMIT_CONVENTION.md](../COMMIT_CONVENTION.md)
for the full specification. This repository is GitHub-managed, so the scope is a plain
`#NNN` issue number (GitHub auto-links it).

### Quick reference

```text
type(#NNN): summary

body (optional bullet points)

#NNN
```

**Rules for AI agents:**

- The scope is **never** optional — always include the issue reference (`#NNN` on GitHub).
- The footer **must** repeat the same issue reference(s) as the scope (bare `#NNN`).
- Breaking changes: use `!` after the scope closing `)` **and** prefix the footer with `BREAKING CHANGE:`.
- Supported types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`.
- Parent issue comes **first** in parent/child references (scope: `#parent/#child`, footer: `#parent / #child`).
- Multiple unrelated issues: comma-separated, ascending order by number (`#5, #12`).
- **Do NOT** use a markdown-link scope like `feat([#42](...)):` — the GitHub
  scope is the bare `#NNN`. (Jira-managed repos use the bare key, e.g.
  `feat(PROJECT-1234):`, with a full markdown link only in the footer.)
- Branch names follow the `type/ISSUE-KEY/description` convention (e.g. `feature/PROJECT-1234/add-webhook` or `fix/2249/squash-fix`) — this is separate from the commit scope.

### Examples

Single issue:

```text
feat(#42): add webhook support

- Implemented webhook handler
- Added unit tests

#42
```

Parent + child issue:

```text
feat(#10/#42): implement sub-feature

- Added endpoint
- Wired up notification

#10 / #42
```

## 16. Common Workflows

> **Note:** All action commands (those that mutate state or make API calls) spawn background tasks.
> These return immediately. Use `agdt-task-status` or `agdt-task-wait` to monitor.

### Initial Git Commit & Publish

```bash
# Option A: With CLI parameter (explicit)
agdt-git-save-work --commit-message "feat(#42): implement feature

- Added new component
- Updated tests

#42"
# Returns task ID immediately - use agdt-task-wait to block until complete

# Option B: Parameterless (uses current state)
# Current commit_message: run `agdt-get commit_message` to check
agdt-git-save-work

# Option C: Title in state, body in commit-body.md (recommended for complex bodies)
# Edit .agdt/workflows/{identity}/{worktree_key}/files/commit-body.md with the body
# Then set only the title line in state:
agdt-set commit_message "feat(#42): implement feature"
agdt-commit-body-show  # Verify body content before committing
agdt-git-save-work
```

### Amend Commit (Single Commit Policy - Automatic Detection)

`agdt-git-save-work` now automatically detects when to amend instead of creating a new commit:

- If current branch has commits ahead of main AND the last commit contains the same issue key → amend
- Otherwise → new commit

```bash
# Option A: With CLI parameter (explicit)
agdt-git-save-work --commit-message "feat(#42): implement feature

- Added new component
- Updated tests
- Addressed PR feedback

#42"
# Automatically amends if already pushed a commit for this issue

# Option B: Parameterless (uses current state)
# Current commit_message: run `agdt-get commit_message` to check
agdt-git-save-work
```

### Create a Pull Request

```bash
agdt-set source_branch feature/PROJECT-1234/my-changes
agdt-set title "feat(#42): Add new feature"
agdt-set description "This PR implements the new feature."
agdt-create-pull-request
```

### Create a Draft PR (default)

```bash
agdt-set source_branch feature/my-branch
agdt-set title "My PR Title"
agdt-create-pull-request
# Note: PRs are created as drafts by default
```

### Create a Non-Draft PR

```bash
agdt-set source_branch feature/my-branch
agdt-set title "My PR Title"
agdt-set draft false
agdt-create-pull-request
```

### Reply to PR Thread

```bash
agdt-set pull_request_id 23046
agdt-set thread_id 139474
agdt-set content "Thanks for the feedback!

I've addressed your concerns."
agdt-reply-to-pull-request-thread
```

### Reply and Resolve Thread

```bash
agdt-set pull_request_id 23046
agdt-set thread_id 139474
agdt-set resolve_thread true
agdt-set content "Fixed, resolving thread."
agdt-reply-to-pull-request-thread
```

### Add a PR Comment

```bash
agdt-set pull_request_id 23046
agdt-set content "Here's a general comment on this PR."
agdt-add-pull-request-comment
```

### Add a PR Comment with Approval Sentinel

```bash
agdt-set pull_request_id 23046
agdt-set content "LGTM! All acceptance criteria met."
agdt-set is_pr_approval true
agdt-add-pull-request-comment
```

### Approve a Pull Request (Convenience Command)

```bash
agdt-set pull_request_id 23046
agdt-set content "LGTM! Great work on this feature."
agdt-approve-pull-request
# Note: This automatically adds the approval sentinel banner
```

### Resolve a Thread

```bash
agdt-set pull_request_id 23046
agdt-set thread_id 139474
agdt-resolve-thread
```

### Get All PR Threads (Sync - Immediate Output)

```bash
agdt-set pull_request_id 23046
agdt-get-pull-request-threads
```

### Dry Run Mode

```bash
agdt-set dry_run true
agdt-git-save-work  # Previews without executing git commands
agdt-reply-to-pull-request-thread  # Previews without API calls
```

### Pull Request Review Workflow

The end-to-end automated PR review is initiated with a **single command**:

```bash
# Start with a PR ID (interactive mode — VS Code terminal)
agdt-initiate-pull-request-review-workflow --pull-request-id 12345

# Start with a Jira issue key (looks up the PR automatically)
agdt-initiate-pull-request-review-workflow --issue-key PROJECT-1234

# Non-interactive / pipeline mode
agdt-initiate-pull-request-review-workflow --pull-request-id 12345 --interactive false

# Force a re-review of the current commit by the same model (overwrites in place)
agdt-initiate-pull-request-review-workflow --pull-request-id 12345 --force-rereview
```

**Automated flow:**

1. Resolves both the PR ID and Jira issue key (cross-lookup when only one is provided).
2. Fetches the PR source branch from Azure DevOps.
3. Validates the current worktree/branch context via pre-flight checks.
4. If the context is wrong, **automatically creates a dedicated worktree**, attempts to open VS Code (when available), then re-runs the command inside the new worktree.
5. Fetches full PR details (diff, threads, iterations) and Jira issue details.
6. Generates per-file review prompts in `pull-request-review/<commit_hash_short>/` under the state directory.

    > **⚠️ Migration note (upgrading from pre-refactor versions):**
    > The artifact directory layout changed from `pull-request-review/prompts/<pr_id>/` to
    > `pull-request-review/<commit_hash_short>/` in this release.
    > `_get_queue_path` transparently falls back to the legacy path when the new path's
    > `queue.json` is absent but the old one exists, so **in-progress reviews continue to
    > work after upgrade without any manual migration**.  Once a new review session is
    > started (via `agdt-review-pull-request`), artifacts for that PR will be written to
    > the new path.  Artifacts from the old path can be safely deleted after all active
    > reviews complete.

7. **When auto-setup ran (new worktree was created):** starts a `gh copilot` CLI session
   (interactive or background depending on `--interactive`). When already in the correct
   worktree context, the rendered initiate prompt is printed to the console for the agent
   to use manually.

#### `--interactive` Flag

| Value | Behavior (when a Copilot session is started) |
|-------|----------------------------------------------|
| `true` (default) | Starts `gh copilot suggest` with an attached terminal — the reviewer interacts with it directly in VS Code. |
| `false` | Starts `gh copilot suggest` as a detached background process, capturing output to `.agdt/workflows/…/background-tasks/logs/`. Use for Azure DevOps pipelines or other headless environments. |

If `gh copilot` is not installed, a warning is printed and the prompt file path is shown so the review can be started manually.

#### Generated Review Artifacts

| File | Purpose |
|------|---------|
| `review-state.json` | Review state: the current commit's thread/comment id (`overallSummary`), the per-commit `commitComments` registry (archived commits + per-model refs + continuation ids), per-file statuses/summaries, and suggestions |
| `queue.json` | Per-file prompt-link / processing-path manifest. **Not** the progress source — progress (`total/pending/completed/all_complete`) derives from `manifest.json` + the answer ledger via `pr_review_progress.compute_review_progress` |
| `pull-request-files.json` | Snapshot of all PR files |
| `pull-request-threads.json` | Snapshot of existing comment threads |
| `pull-request-jira-issue.json` | Copy of linked Jira issue |
| `file-<hash>.md` | Individual file review prompt with diff and threads |

#### File Review Submission

After a review session is active, the orchestrator delegates each file to a one-shot
`file-reviewer` subagent. Each subagent writes its verdict atomically with
`agdt-file-review-write --file-key <fileKey> --answer-file <path-to-draft.json>`; the
answer's `outcome` (`approve` / `request-changes` / `request-changes-with-suggestion`)
sets the file's status. There are no standalone file-level CLI commands.

#### Consolidated Review Comment Architecture (per-commit threads)

`agdt-review-pull-request` scaffolds **one top-level consolidated review comment per
reviewed commit** (not a single comment for the whole PR). The thread for the *current*
commit is tracked by `overallSummary`; previously reviewed commits are archived into a
`commitComments` registry keyed by full commit SHA. There are still no per-file or
per-folder threads — all of a commit's content is rendered inline in its one comment using
nested `<details>`/`<summary>` collapsible sections.

**Lightweight while in progress.** While files are still being reviewed, the comment is
kept deliberately minimal — only a status headline is rendered (and patched on each file
submission), e.g.:

```text
🔍 Pull Request Review — 🔃 In Progress
🤖 Being reviewed by 🧠 claude-opus-4.6 · latest commit 14d6c69f · 6 approved · 0 need work · 6/17 reviewed
```

This keeps per-file PATCH payloads tiny. Once **every file in scope is reviewed**, the
comment is re-rendered with the **full content** (per-file `<details>`, suggestions
embedded inline, and the "Previous reviews" index):

```text
🔍 Pull Request Review — ✅ Approved
🤖 Reviewed by 🧠 claude-opus-4.6 · latest commit 14d6c69f · 17 approved · 0 need work · 17/17 reviewed
```

**50k smart-cutoff + continuation replies.** The full content is rendered as an ordered
sequence of top-level `<details>` sections. Sections accumulate into the root comment until
the running character total would exceed the named constant `SMART_CUTOFF_CHARS` (50000);
the in-progress top-level section is allowed to finish, then the remaining sections roll
over into one or more **continuation reply comments** within the same thread (each titled
"… (continued N)"). There is no budget-governed degradation/compaction — suggestions and
files are always rendered at full fidelity.

**Previous reviews index.** Instead of embedding the full content of every prior commit's
review, the comment includes a simple bulleted **"Previous reviews"** index: one line per
prior reviewed commit = short hash + status/model + a jump link (built via
`build_discussion_url`) to that commit's thread. No embedded prior content.

**Markers & recovery.** Each comment carries a `v2` consolidated marker; continuation
comments carry a `continuation` marker tying them to their commit + sequence; a
different-model reply carries a `model-review` marker; and a skipped same-model re-review
reply carries a `rereview-skipped` marker. These let the per-commit threads + continuation
chains be recovered if local state is lost. Legacy `v1` per-thread comments are
intentionally ignored (no backwards compatibility). **Code suggestions remain separate
line-anchored threads**, but each suggestion's link and full content is also embedded in
the commit's consolidated comment so the entire review can be reconstructed from it.

#### Review State File (`review-state.json`)

Location: `<state_dir>/reviews/review-state.json` (where `<state_dir>` is `.agdt/workflows/{identity}/{worktree_key}`)

This file tracks all thread IDs, statuses, and suggestions for the entire review session:

```json
{
  "prId": 12345,
  "repoId": "<guid>",
  "repoName": "my-repo",
  "project": "MyProject",
  "organization": "myorg",
  "latestIterationId": 1,
  "scaffoldedUtc": "2024-12-19T15:50:26+00:00",
  "commitHash": "14d6c69f0e1a2b3c4d5e6f7890abcdef12345678",
  "modelId": "claude-opus-4.6",
  "overallSummary": { "threadId": 1, "commentId": 1, "status": "in-progress" },
  "commitComments": {
    "0badc0ffee1122334455667788990011223344556": {
      "commitHash": "0badc0ffee1122334455667788990011223344556",
      "threadId": 7,
      "status": "approved",
      "timestamp": "2024-12-18T09:00:00+00:00",
      "models": [
        { "modelId": "gpt-4o", "commentId": 1, "continuationCommentIds": [2], "status": "approved", "timestamp": "2024-12-18T09:00:00+00:00" }
      ]
    }
  },
  "folders": {
    "src/app": { "threadId": 2, "commentId": 1, "status": "in-progress", "files": ["/src/app/component.ts"] }
  },
  "files": {
    "/src/app/component.ts": {
      "threadId": 3,
      "commentId": 1,
      "folder": "src/app",
      "fileName": "component.ts",
      "status": "approved",
      "summary": "Clean implementation, no issues found.",
      "suggestions": [],
      "previousSuggestions": null
    }
  }
}
```

**`commitComments` registry fields** (one entry per reviewed commit SHA):

| Field | Meaning |
|-------|---------|
| `commitHash` | Full 40-char SHA of the reviewed commit (the map key). |
| `threadId` | Azure DevOps thread id for that commit's consolidated comment. |
| `status` | Aggregate status for the commit (`approved` / `needs-work` / `in-progress`). |
| `timestamp` | Last update time (ISO-8601). |
| `models` | Ordered list of `ModelCommentRef` entries — one per reviewing model. |

Each `ModelCommentRef` records `modelId`, `commentId` (the root comment for that model's
review — the first model uses the thread's root comment; additional models reply within the
thread), `continuationCommentIds` (the >50k roll-over replies, in order), `status`, and
`timestamp`. `CommitComment.rootCommentId` returns the first model's `commentId`.
`commitHash` (top-level) is the "latest reviewed commit" pointer; `overallSummary` tracks
the current commit's thread/root comment and is reset (POST a fresh thread) when a new
commit is reviewed.

#### Review Status State Machine

Each file, folder, and the overall PR follow this status progression:

```text
unreviewed → in-progress → approved
                        ↘ needs-work
```

- `unreviewed` — not yet started (initial state)
- `in-progress` — review started but not all files in scope are complete
- `approved` — all files approved (no blocking issues)
- `needs-work` — at least one file has changes requested

#### Status Derivation Rules

| Level | Rule |
|-------|------|
| **File** | Set by the `file-reviewer` subagent's answer `outcome` (`approve` → `approved`; `request-changes` / `request-changes-with-suggestion` → `needs-work`) written via `agdt-file-review-write` |
| **Folder** | All files unreviewed → `unreviewed`; ≥1 started, not all done → `in-progress`; all done, all approved → `approved`; all done, any needs-work → `needs-work` |
| **Overall PR** | Same rules as folder, applied to folder statuses |

#### Suggestion Severity Levels

| Severity | Meaning |
|----------|---------|
| `high` | Blocking — must be addressed before the PR can be approved |
| `medium` | Should be addressed but not strictly blocking |
| `low` | Nice-to-have improvement; can be deferred |

#### Out-of-Scope Suggestions

When a suggestion does not map to a specific line in the diff (e.g., an architectural
concern or a comment on a non-changed line), set `"out_of_scope": true` in the suggestion
entry. The `out_of_scope` flag is persisted on the suggestion entry for classification, but
suggestions are still posted as line-anchored threads (not PR-level comments), and each is also embedded in the commit's consolidated comment.

#### Re-Review Semantics (per commit / per model)

Re-review behavior is decided per reviewed commit, based on whether the same model and
identity already reviewed that commit. It is controlled by the `--force-rereview` flag on
`agdt-initiate-pull-request-review-workflow` (persisted as the `review.force_rereview` state
key and read by the review scaffold):

| Case | Behavior |
|------|----------|
| **Same commit + same model + already completed** | The review is **skipped**. A reply is posted on that commit's thread (carrying a `rereview-skipped` marker) stating that another review was requested but skipped because it was already performed, and giving the exact command to re-run with `--force-rereview`. |
| **Same commit + same model + `--force-rereview`** | The skip is overridden: files are reset for re-review and the commit's consolidated comment is re-rendered **in place** (PATCH). |
| **Same commit + different model** | The new model's review is posted as a **reply** within the existing commit thread (no `--force-rereview` needed), tracked via a `ModelCommentRef`. Re-running that same additional model updates its reply **in place**. |
| **Different (new) commit** | A **new top-level thread** is POSTed for the new commit. The prior commit is archived into the `commitComments` registry and appears in the "Previous reviews" hash+link index. |

When files are reset for a re-review of a file:

1. File status is reset from the terminal state (`approved`/`needs-work`) to `unreviewed`
2. Existing `suggestions` are rotated to `previousSuggestions` as an audit trail
3. New suggestions can be added from scratch for the fresh review

The rotation fires **once** per re-review cycle (when `previousSuggestions is None`).
Once rotated — even if the original suggestions list was empty — `previousSuggestions`
is set to `[]` (not `null`), so retries are safe and will not re-trigger rotation.

#### Pipeline Usage

For use in Azure DevOps pipelines (headless, no interactive terminal):

```yaml
# azure-pipelines.yml example
- script: agdt-initiate-pull-request-review-workflow --pull-request-id $(System.PullRequest.PullRequestId) --interactive false
  displayName: 'Run automated PR review'
  env:
    AZURE_DEV_OPS_COPILOT_PAT: $(AzureDevOpsCopilotPat)
    JIRA_COPILOT_PAT: $(JiraCopilotPat)
```

When running in pipeline mode (`--interactive false`):

- `gh copilot` output is captured to `.agdt/workflows/…/background-tasks/logs/copilot_session_*.log`.
- The Copilot process runs as a direct child process (via `subprocess.Popen`), not as an
  agdt background task, so there is no background task ID and `agdt-task-wait` is not
  applicable. Monitor completion using the `copilot.pid` state key and/or by tailing the
  Copilot session log file.
- The `worktree_setup.auto_execute_exit_code` state key records the exit code of the
  setup/auto-execute command, not the Copilot session itself.

#### Copilot / VS Code Graceful Degradation

When an interactive Copilot session cannot be started, the workflow falls back to a
non-interactive mode:

- If `gh copilot` is not available on the machine, a warning is printed to stderr.
- If the VS Code `code` CLI is not available, or there is no attached TTY (for example,
  in CI pipelines), interactive mode is automatically disabled and `copilot.mode` is set
  to `"non-interactive"`.
- In all of these cases, the rendered prompt is written to
  `copilot-session-<session_id>-prompt.md` in the state directory, and the session metadata is still
  persisted to state (`copilot.*` keys).
- The reviewer can open the prompt file in VS Code (or any editor) and start a manual
  Copilot session if desired.

#### Copilot CLI Session Management

State keys written to `agdt-state.json` after a session is started:

| Key | Type | Description |
|-----|------|-------------|
| `copilot.session_id` | string | UUID4 hex identifying the session |
| `copilot.mode` | string | `"interactive"` or `"non-interactive"` |
| `copilot.prompt_file` | string | Absolute path to the temporary prompt file |
| `copilot.start_time` | string | ISO-8601 UTC timestamp when the session was started |
| `copilot.pid` | `integer \| ""` | PID of background process (empty for interactive sessions) |

Additional `worktree_setup` keys written during automated environment setup:

| Key | Type | Description |
|-----|------|-------------|
| `worktree_setup.auto_execute_exit_code` | string | Exit code of the auto-executed setup command |

#### Target Repository Configuration (`.github/agdt-config.json`)

Target repositories can place a `.github/agdt-config.json` file in their root to customize the review workflow. The file is **optional** — if absent, the review proceeds with defaults.

**Schema:**

```json
{
  "review": {
    "focus-areas-file": ".github/review-focus-areas.md"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `review.focus-areas-file` | string | Repo-root-relative path to a markdown file listing review focus areas. |

**`.github/review-focus-areas.md` format:**

Free-form markdown injected into the review prompt to guide the Copilot reviewer. Example:

```markdown
## Review Focus Areas

- Ensure all public APIs have JSDoc/docstring documentation.
- Validate that new database migrations are backward-compatible.
- Check that error messages are user-friendly and don't leak internals.
```

Both files are optional and safe to omit; the review will still run without repo-specific guidance.

#### Phase 0 Configuration (`platform.phase_0`)

The `platform.phase_0` section controls Phase 0 issue normalization and sync-back behavior.
See [`docs/phase-0-configuration.md`](../docs/phase-0-configuration.md) for the full schema
reference, valid `sync_back_fields` values, default behavior, and configuration examples.

### Get Jira Issue Details

```bash
agdt-set jira.issue_key PROJECT-1234
agdt-get-jira-issue
```

This fetches the issue and:

- Prints formatted details (key, summary, type, labels, description, comments)
- Saves full JSON response to `temp-get-issue-details-response.json` in the state directory
- Stores issue in state as `jira.last_issue`

### Add Comment to Jira Issue

```bash
agdt-set jira.issue_key PROJECT-1234
agdt-set jira.comment "h4. Progress Update

*Completed:*
* Task 1
* Task 2

*Next Steps:*
* Task 3"
agdt-add-jira-comment
```

After posting, the command automatically refreshes issue details.

### Create Jira Epic

```bash
agdt-set jira.project_key PROJECT
agdt-set jira.summary "My Epic Title"
agdt-set jira.epic_name "My Epic Name"
agdt-set jira.role "developer"
agdt-set jira.desired_outcome "a new feature"
agdt-set jira.benefit "improved productivity"
agdt-create-epic
```

### Create Jira Issue

```bash
agdt-set jira.project_key PROJECT
agdt-set jira.summary "Bug: Something is broken"
agdt-set jira.description "Detailed description here"
agdt-create-issue
```

Or use the user story format:

```bash
agdt-set jira.project_key PROJECT
agdt-set jira.summary "Feature request"
agdt-set jira.role "user"
agdt-set jira.desired_outcome "new capability"
agdt-set jira.benefit "better experience"
agdt-create-issue
```

### Create Subtask

```bash
agdt-set jira.parent_key PROJECT-1234
agdt-set jira.summary "Subtask title"
agdt-set jira.description "Subtask details"
agdt-create-subtask
```

### Jira Dry Run Mode

```bash
agdt-set jira.dry_run true
agdt-add-jira-comment  # Previews without posting
```

### Commit Message Template System

The Jinja2 commit message template system generates commit messages automatically from workflow state.

**Priority chain** (in `commit_cmd()`):

1. `--commit-message` CLI argument (verbatim)
2. Template rendering from `.agdt/config/commit-template.j2`
3. `commit_message` state key fallback

**Template variables:**

| Variable | Resolution Source | Description |
|----------|------------------|-------------|
| `issueType` | `versionControl.commitMessageType` → mapped from `issueManagement.issueType` / `jira.issue_type` | Conventional commit type prefix |
| `issueKey` | `issue_key` → `jira.issue_key` → `workflow.context.jira_issue_key` (normalized: strips `#`, preserves Jira keys) | Issue identifier |
| `issueLink` | `issueManagement.issueLink` → derived from GitHub repo + numeric key | Full issue URL |
| `commitMessageTitle` | `versionControl.commitMessageTitle` | Commit summary line |
| `commitMessageBody` | File content from `versionControl.commitMessageBodyFile` path | Commit body text |

**Setup:**

- `agdt-setup` creates the default template at `.agdt/config/commit-template.j2` (unless `--skip-templates`)
- Template is validated during setup (warns on missing required variables)
- Template file is repo-local and versionable

**Usage:**

```bash
# Set state keys for template rendering
agdt-set issue_key 42
agdt-set versionControl.commitMessageType feat
agdt-set versionControl.commitMessageTitle "add webhook support"

# agdt-git-save-work will render template automatically
agdt-git-save-work
```

**Troubleshooting:**

- Empty/whitespace-only template → warning + falls back to `commit_message` state
- Jinja2 syntax errors → warning + falls back
- Unresolved variables → per-variable warning to stderr, renders as empty string
- No template file → silently falls back to `commit_message` state

## 17. Output Files

| File | Command | Content |
|------|---------|---------|
| `.agdt/workflows/…/state.json` | All commands | Persistent state storage |
| `.agdt/workflows/…/temp-get-issue-details-response.json` | `agdt-get-jira-issue` | Full Jira API response |
| `.agdt/workflows/…/temp-get-pull-request-details-response.json` | `agdt-get-pull-request-details` | Full PR details payload |
| `.agdt/workflows/…/pull-request-review/<commit_hash_short>/` | `agdt-review-pull-request` | Review prompts directory |
| `.agdt/workflows/…/reviews/review-state.json` | `agdt-review-pull-request` | Hierarchical review state (thread IDs, statuses, suggestions) |
| `.agdt/workflows/…/temp-<workflow>-<step>-prompt.md` | Workflow initiation commands | Rendered workflow prompts |

### Commit Hash Source of Truth

The PR commit hash is stored in multiple locations, each serving a distinct purpose.

| Location | Field | Format | Purpose |
|----------|-------|--------|---------|
| `review-state.json` | `commitHash` | Full 40-char SHA | **Canonical** source of truth for the reviewed commit, set from `lastMergeSourceCommit.commitId` during scaffolding |
| `state.json` | `review.commit_hash_short` | First 12 chars | Derived from the canonical hash; used as the artifact directory name under `pull-request-review/` |
| `review-state.json` | `sessions[].commitHash` | Full 40-char SHA | Records which commit each individual review session targeted (may differ across sessions after new pushes) |
| Artifact directory | `pull-request-review/<hash>/` | First 12 chars | On-disk directory scoping review prompts and queue files, derived from the canonical hash |

`review-state.json` → `commitHash` is the **canonical** full commit hash for the PR under review.
`state.json` → `review.commit_hash_short` is **derived** from it (first 12 characters) and used for artifact directory resolution.
Legacy 8-character directory names may still be recognized as a backward-compatibility fallback.
Per-session `commitHash` records which specific commit each review session targeted, which may differ from the top-level `commitHash` when the PR is re-reviewed after new pushes.

### Background Task Storage Structure

Background tasks use a separate storage structure in `.agdt/workflows/…/background-tasks/`:

```text
.agdt/workflows/{identity}/{worktree_key}/
├── state.json                         # Main state file (contains background.recentTasks)
└── background-tasks/
    ├── all-background-tasks.json      # Complete history of all tasks (never pruned)
    └── logs/
        └── <command>_<timestamp>.log  # Task output logs
```

**State Structure:**

The `agdt-state.json` file contains recent tasks under `background.recentTasks`:

```json
{
  "background": {
    "recentTasks": [
      {
        "id": "task-abc123",
        "command": "agdt-git-save-work",
        "status": "completed",
        "start_time": "2024-12-19T15:50:26Z",
        "end_time": "2024-12-19T15:50:28Z",
        "exit_code": 0,
        "log_file": "/path/to/logs/agdt_git_commit_20241219_155026.log"
      }
    ],
    "task_id": "task-abc123"
  }
}
```

**Sorting Rules:**

- Unfinished tasks appear first, sorted by start time (earliest first)
- Finished tasks appear after, sorted by end time (earliest first)

**Auto-Pruning:**

- Recent tasks are automatically pruned when not running and finished more than 5 minutes ago
- The `all-background-tasks.json` file keeps the complete history without pruning

## 18. Instruction Maintenance

Update this file when:

- Adding new CLI commands
- Changing state file structure
- Modifying the auto-approval pattern
- Adding new integrations
- Adding new workflow commands or prompt templates
- Discovering new anti-patterns that AI agents should be warned about

## 19. Copilot Code Review Instructions

> These instructions apply specifically to Copilot Code Review when reviewing pull requests
> in this repository. They help prevent false-positive suggestions that could interfere with
> the automated AI PR Loop.

### DO NOT Suggest Linting, Formatting, or Type Annotation Changes

This repository enforces the following via CI (all actionable CI jobs must pass BEFORE
Copilot Code Review is requested):

- **Linting**: `ruff check` (all rules enforced, auto-fixable violations caught by pre-push hook)
- **Formatting**: `ruff format` (canonical formatting, enforced by pre-push hook)
- **Type checking**: `mypy --ignore-missing-imports --follow-imports=silent` on changed Python files
- **Test coverage**: 100% branch coverage per source file

Because these are enforced BEFORE review is requested, the following types of suggestions
are always false positives and MUST NOT be included in reviews:

1. Import reordering or unused import removal (ruff handles this)
2. Whitespace, indentation, or formatting changes (ruff format handles this)
3. Variable/function naming that conflicts with ruff naming rules
4. Type-hint-only churn that doesn't affect correctness, behavior, or readability
5. Line length violations (ruff enforces this)
6. Trailing whitespace or missing newlines (ruff format handles this)

### Focus Review Suggestions On

- **Logic errors** and unhandled edge cases
- **Security concerns** (hardcoded secrets, injection risks, unsafe deserialization)
- **API design improvements** (naming clarity, parameter ordering, return type choices)
- **Missing test coverage** for complex conditional branches
- **Performance issues** (N+1 patterns, unnecessary allocations in hot paths)
- **Concurrency bugs** (race conditions, missing locks, unsafe shared state)
- **Error handling gaps** (swallowed exceptions, missing retry logic)

### Review Style Preferences

- Always explain the "why" behind a suggestion — not just what to change
- Prefer suggestions with explicit replacement code (GitHub `suggestion` blocks)
- Do not flag issues that would be caught by the existing CI pipeline
- When suggesting code changes, ensure they maintain 100% branch coverage compatibility

## 20. Agent Design Principles

These principles govern how AI agents interact with the agentic-devtools ecosystem.
They derive from Constitution Principles XII–XIV (see `.specify/memory/constitution.md`).

### Structured Inputs Are Pre-Provided

All relevant context (PR diffs, comment threads, issue details, metadata, instruction files)
is gathered deterministically and provided to you as structured input before your session begins.
Do not attempt to fetch, crawl, or discover context that should have been prepared upstream.
Focus your reasoning on analysis, judgment, and generation — not on data retrieval.

### Focus on Judgment, Not Retrieval

Your role is evaluation and decision-making. If you find yourself needing context that was not
provided, state clearly in your output what information is missing and what operation would
retrieve it, rather than attempting ad-hoc retrieval that may be incomplete or non-reproducible.

### Use `agdt-*` Commands for Provider Interactions

Never invoke provider-specific CLIs or APIs directly (e.g., `gh api`, `az repos`, raw `curl`
calls to GitHub/Azure DevOps/Jira endpoints). Always use the corresponding `agdt-*` command.
This ensures provider-agnostic operation and maintains the abstraction layer.

If no `agdt-*` command exists for a required operation, document the gap in your output:
state the operation needed, the provider it targets, and note that a tracking issue should
be created. Refer to the "Exceptions — Raw Commands Still Required" table in this file for
known gaps.

### No Business Logic in YAML or Pipeline Files

When creating or modifying CI/CD workflows (GitHub Actions, Azure DevOps pipelines), keep
the YAML minimal: invoke a single `agdt-*` CLI command or entrypoint with basic
secrets/token plumbing. Do not embed orchestration logic, complex conditionals, or
business rules in YAML files. Extract such logic into testable CLI commands instead.

## 21. Defensive Coding Patterns

> These patterns emerged from recurring review findings across multiple PRs. Every new module
> and every modification to an existing one must follow them.
> *Rationale: based on audit findings in PRs #2771, #2777, #2786, #2798, #2801.*

### Fail-Fast Validation in Constructors and Factory Functions

Validate required configuration in `__init__` and factory functions **before** doing any
work. Never defer validation to the first API call — late failures produce cryptic errors
(e.g., `Invalid URL`, `MissingSchema`, `TypeError: NoneType`), while early failures
pinpoint the misconfiguration immediately.

```python
# ✅ CORRECT — fail fast in __init__
class GitHubProvider:
    def __init__(self, owner_repo: str) -> None:
        if not owner_repo or "/" not in owner_repo:
            raise ValueError(f"owner_repo must be in 'owner/repo' form, got: {owner_repo!r}")
        self._owner_repo = owner_repo


# ❌ WRONG — defers failure to first API call, produces opaque error
class GitHubProvider:
    def __init__(self, owner_repo: str) -> None:
        self._owner_repo = owner_repo  # empty string silently accepted
```

Rules:

- String parameters that form URLs or identifiers must be validated as non-empty and
  structurally correct (`owner/repo`, non-empty project key, etc.).
- Numeric parameters that define retry/timeout budgets (e.g., `max_retries`,
  `initial_delay`, `jitter_factor`) must be validated as non-negative before use.
  Negative values can produce `None`-dereference errors or `time.sleep()` failures.
- Factory functions (`create_provider()`, `make_report()`, etc.) must perform the same
  validation — they must not return a partially-configured object that will fail at the
  first method call.

### Precise HTTP Transient Error Detection

When classifying exceptions as transient (retryable), match the documented HTTP status
pattern precisely. Do **not** use raw substring matching on the full error message string,
because digit sequences like `1429` or arbitrary numbers in paths can inadvertently
trigger retries for non-HTTP errors.

```python
import re as _re

# ✅ CORRECT — matches "HTTP 429", "HTTP 502", "HTTP 503" exactly
_TRANSIENT_PATTERN = _re.compile(r"\bHTTP (429|502|503)\b")


def is_transient_error(exc: Exception) -> bool:
    return bool(_TRANSIENT_PATTERN.search(str(exc)))


# ❌ WRONG — "error code 1429" or paths containing "503" also match
def is_transient_error(exc: Exception) -> bool:
    msg = str(exc)
    return any(code in msg for code in ("429", "502", "503"))
```

Apply the same rule anywhere transient detection logic is duplicated (e.g., in both
a retry decorator and a provider's `_exec_gh()` helper) — use the same regex,
defined once and imported rather than copied.

### Keep Docstrings Synchronized with Implementation

Every function/class/module docstring must describe **current** behavior, not historical
or aspirational behavior. When the implementation changes, update the docstring in the
same commit.

Common violations to avoid:

- A module docstring that says "re-exports from module X" when the module actually
  defines its own symbols.
- A function docstring that says "the retry is applied to the per-page call" when the
  decorator actually wraps the entire paginated loop.
- A class docstring that describes an edge for a dependency graph in one direction
  ("source depends on target") when the implementation and tests treat it as the opposite.

Test docstrings must not use opaque placeholders like `"******"` to obscure the behavior
under test. If the test asserts that an `Authorization` header takes precedence over a
fallback credential, say "asserts Authorization header takes precedence"; if it asserts
an empty credential is returned when no token is set, say so explicitly. Placeholder
docstrings make auth failures impossible to diagnose from the test output alone.

### Return Values Must Accurately Reflect Operation Outcome

A function's return value (especially `status`, `outcome`, or a result dataclass) must
accurately describe what actually happened during the call. Misleading statuses cause
callers to skip necessary follow-up actions or log incorrect audit trails.

```python
# ✅ CORRECT — distinguish no-op from mutation
def set_issue_type(self, identifier: str, issue_type: str, dry_run: bool) -> ProviderIssueResult:
    label = _ISSUE_TYPE_LABELS[issue_type.lower()]
    link_result = self.apply_labels(identifier, label, dry_run=dry_run)
    url = f"https://github.com/{self._owner_repo}/issues/{identifier}"
    return ProviderIssueResult(
        identifier=identifier,
        url=url,
        status="no-op" if link_result.status == "already-linked" else "updated",
        metadata={"label": label},
    )


# ❌ WRONG — always returns "updated" even when label was already present
def set_issue_type(self, identifier: str, issue_type: str, dry_run: bool) -> ProviderIssueResult:
    label = _ISSUE_TYPE_LABELS[issue_type.lower()]
    self.apply_labels(identifier, label, dry_run=dry_run)
    url = f"https://github.com/{self._owner_repo}/issues/{identifier}"
    return ProviderIssueResult(identifier=identifier, url=url, status="updated")  # misleading for no-ops
```

Rules:

- Never raise a domain-specific exception class (e.g., `ProviderRateLimitError`) for
  failures that have nothing to do with that domain — re-raise the original exception
  or use a generic `RuntimeError` instead.

### Documentation/Message ↔ Behavior Consistency

- When a change affects a public function, CLI option, state key, serialized/config field name,
  status value, error-path contract, or persistence guarantee, update all related docstrings,
  inline comments, help text, user-facing messages, and touched docs in the same PR. Use the exact
  exported key names and field casing the code actually returns or accepts. Do not mention flags,
  statuses, filters, or modes that the implementation does not support. If behavior is best-effort
  rather than guaranteed, document it as best-effort. If a guarantee only applies to a managed
  path/scope, state that scope explicitly instead of implying a broader contract.
  *Rationale: based on audit findings in PRs #2747, #2771, #2786, #2794, #2798, #2801.*

- User-facing remediation/error text must reference real, currently supported CLI flags/commands only.
  Never mention non-existent options or deprecated control paths.
  *Rationale: based on audit findings in PRs #2747 and #2801.*

## 22. Copilot Repair Session Non-Negotiables

> These rules apply to any Copilot cloud coding-agent session dispatched by the AI PR loop
> (the `@copilot` repair comment). They hold even when the dispatch comment carries only an
> excerpt of the agent/prompt files rather than their full text.

- **Suppressed review comments are first-class and actionable.** Evaluate each against its
  **full** body — suppressed means de-emphasized, not invalid.
- **"Generated no new comments" does not mean "no work".** A zero posted-comment count never
  implies zero suppressed comments; read the suppressed count and each full body first.
- **Definition of done = a verified pushed commit**, confirmed by comparing your local commit
  SHA against the PR's `headRefOid`. Narrating a fix you did not push is a failure.
- **`<!-- ai-pr-loop:repair-satisfied -->` is valid only for a fully-evidenced no-op** — every
  comment (visible and suppressed) evaluated against its full text, with a per-file
  justification row for each in the summary comment. A marker without that table is invalid.
- **Cloud agents MUST push a new commit with a plain fast-forward `git push`** — never
  `--amend`, `--force-with-lease`, or any other force-push. The AI PR loop squashes to a
  single commit later under a privileged identity.

Full workflow: [`.github/prompts/agdt.address-copilot-review.evaluate-and-respond.prompt.md`](prompts/agdt.address-copilot-review.evaluate-and-respond.prompt.md).
