# AI Agent Instructions: `agentic_devtools/cli/audit/`

> Parent Instruction Chain
>
> 1. [agentic-devtools package instructions](../../../.github/copilot-instructions.md)
>
> [`copilot-instructions.md` Precedence Model](../../../docs/copilot-instructions-precedence-model.md)

## Scope

These instructions apply to Copilot and AI agents when writing or reviewing Python code
in the `agentic_devtools/cli/audit/` package (Sections 1–6).

They encode lessons from a review of PR #2517 (auto-apply audit output via scheduled
eval-PR takeover) and PR #2657 (token handling in the audit apply flow).

> **Note:** Section 4 (`$GITHUB_OUTPUT` newline sanitization) applies to Python helper
> code in this package that emits output for GitHub Actions. The equivalent instruction
> for workflow YAML files lives in `.github/workflows/copilot-instructions.md`.

---

## 1. Batch Operations Must Never Abort on Per-Item Errors

Any function that processes a collection of items (PRs, files, records) and whose docstring says
"per-item failures do not abort the run" **must** wrap each item's processing in a `try/except`
block and log the failure before continuing:

```python
# ✅ CORRECT — per-PR resilience
for pr in prs:
    try:
        result = process_pr(pr)
    except Exception as exc:  # noqa: BLE001
        pr_number = pr.get("number", "?") if isinstance(pr, dict) else "?"
        logger.warning("Skipping PR #%s: %s", pr_number, exc)
        continue

# ❌ WRONG — unguarded call aborts the entire batch on first failure
for pr in prs:
    result = process_pr(pr)
```

This applies to all provider calls (`provider.list_pr_files()`, `provider.get_pr_metadata()`,
`provider.create_pr()`, etc.) made inside a per-item loop. Network/API errors on one item must
never abort processing of subsequent items.

**Based on audit of PR:** #2517

---

## 2. Guard `json.loads()` Against Invalid Non-JSON Output from CLI Tools

When parsing stdout from CLI tools (e.g., `gh`, `az`), always guard against non-JSON output
(auth warnings, rate-limit messages, partial responses):

```python
# ✅ CORRECT
try:
    data = json.loads(result.stdout or "[]")
except json.JSONDecodeError as exc:
    snippet = (result.stdout or "")[:80]  # Keep only a short excerpt for the error message.
    raise RuntimeError(f"Unexpected non-JSON output from CLI tool (snippet: {snippet!r})") from exc

# ❌ WRONG — crashes the whole run if gh emits a warning message to stdout
data = json.loads(result.stdout or "[]")
```

**Based on audit of PR:** #2517

---

## 3. Validate Structured-Field Access on Untrusted Input

When iterating over items from an external source (API response, JSON file, subprocess output),
do not assume every element has the expected structure. Guard against `KeyError`, `TypeError`,
and `ValueError` before accessing fields:

```python
# ✅ CORRECT
for item in items:
    if not isinstance(item, dict):
        logger.warning("Skipping malformed item (expected dict): %r", item)
        continue
    number = item.get("number")
    if not isinstance(number, int):
        logger.warning("Skipping malformed item (missing int 'number'): %r", item)
        continue
    process(number)

# ❌ WRONG — KeyError/TypeError aborts the run if any item is malformed
for item in items:
    process(item["number"])
```

**Based on audit of PR:** #2517

---

## 4. Strip Newlines from Values Written to `$GITHUB_OUTPUT`

When Python helper code in this package writes values sourced from JSON files or external
data into `$GITHUB_OUTPUT`, strip CR and LF characters first. Embedded newlines break the
`key=value` line format and can inject additional output keys:

```python
import json
import os

# ✅ CORRECT — strip CR/LF before writing to $GITHUB_OUTPUT
with open("batch-meta.json") as f:
    meta = json.load(f)

base_sha = str(meta.get("base_sha") or "").replace("\r", "").replace("\n", "")

github_output = os.environ.get("GITHUB_OUTPUT", "")
if github_output:
    with open(github_output, "a") as out:
        out.write(f"base_sha={base_sha}\n")

# ❌ WRONG — embedded newline in base_sha splits the output, injecting a second key
with open(github_output, "a") as out:
    out.write(f"base_sha={meta['base_sha']}\n")
```

For equivalent sanitization in GitHub Actions workflow YAML (bash), see
`.github/workflows/copilot-instructions.md`.

**Based on audit of PR:** #2517

---

## 5. Strip Each Environment Variable Token Before `or`-Chaining

When reading a credential from multiple candidate environment variables, strip whitespace from
**each candidate independently** before the `or` fallback, not from the combined result.

**Problematic pattern:**

```python
# ❌ If GH_TOKEN is whitespace-only, `or` short-circuits to that non-empty raw value first.
#    After `.strip()` it becomes "", but fallback to SPECKIT_PR_TOKEN is already skipped.
token = (os.environ.get("GH_TOKEN") or os.environ.get("SPECKIT_PR_TOKEN", "")).strip()
```

**Correct pattern:**

```python
# ✅ Strip each candidate first so whitespace-only values are treated as absent
gh_token = os.environ.get("GH_TOKEN", "").strip()
pr_token = os.environ.get("SPECKIT_PR_TOKEN", "").strip()
token = gh_token or pr_token
if not token:
    logger.error("Neither GH_TOKEN nor SPECKIT_PR_TOKEN is set.")
    return ""
```

This ensures that a whitespace-only `GH_TOKEN` falls through to the correct `SPECKIT_PR_TOKEN`
value rather than silently masking it.

**Based on audit of PR:** #2657

---

## 6. Token Presence Check Must Match Downstream Authentication Expectations

When the audit `apply` flow checks whether a token is present before spawning a subprocess
(e.g., calling `gh pr create`), the token used in the presence check must be the **same** token
the downstream tool authenticates with.

- `gh` authenticates via `GH_TOKEN` (or the ambient `GITHUB_TOKEN`).
- `SPECKIT_PR_TOKEN` can be a valid input source **if** your flow normalizes/copies it into
  `GH_TOKEN` before invoking `gh`.

**Guideline:** if a subprocess relies on `GH_TOKEN`, the pre-flight check must validate the
final non-empty `GH_TOKEN` value that the subprocess will actually see after normalization.

**Based on audit of PR:** #2657
