---
name: afx-incident
description: File, deduplicate, and manage incident-driven tickets in Artifex. Use this skill when an automated source (CI pipeline failure, eval regression) needs to create a ticket, when you want to check or change the trust-level policy for automated ticket actions, or when you need to initialize the Incidents epic in a project. Adapters (afx-cicd, afx-eval) pipe their structured payloads into this skill's `file` subcommand.
---

# Artifex Incident Skill

**For Claude Code AI Assistant**

This skill provides the shared plumbing for incident-driven tickets. When an automated adapter (CI failure detector in ticket 13.2, eval regression detector in ticket 13.3) observes a problem, it formats a structured JSON payload and pipes it to `afx-incident file`. This skill handles:

- Validation of the payload schema and Gherkin acceptance criterion
- Deduplication against existing open Incidents tickets (by signature)
- Filing a new ticket under the `Incidents` epic with the source tag in the title prefix
- Appending repeat-occurrence entries to `status_history` for already-open tickets
- Reading and writing the per-project **trust slider** (`incident-config.json`)

## Architecture

```
CI adapter (ticket 13.2) ─┐
                           ├──> echo '{...payload...}' | afx-incident file
Eval adapter (ticket 13.3)─┘
                                        ↓
                           Validate schema + Gherkin AC
                                        ↓
                           Dedup by signature in open Incidents tickets
                          /                        \
                   New incident               Existing ticket
                        ↓                          ↓
              File TO_DO ticket           Append repeat-occurrence
              under Incidents epic        entry to status_history
                        ↓
              Apply trust slider
              (v1: file-only always)
```

## Subcommands

| Subcommand | Purpose |
|------------|---------|
| `init` | Add the Incidents epic (idempotent) and create `incident-config.json` with `trust_level: file-only` |
| `file` | Read structured incident payload JSON from stdin; validate, dedup, and file or append |
| `from-ci <run-url-or-id>` | Manual CI adapter: fetch failed GHA run logs, extract failure, build payload, file incident |
| `intake` | Pull adapter: process all `.artifex/incoming-incidents/*.json` files in order |
| `from-eval <eval-id>` | Eval adapter (single): read one afx-eval case file, build payload, file incident |
| `from-eval --batch` | Eval adapter (batch): iterate all `captured` afx-eval cases, file incidents, skip deduped |
| `graduate <signature>` | Manually promote a recurring incident signature to a temporary lesson |
| `config get <key>` | Print a config value from `incident-config.json` |
| `config set <key> <value>` | Write a config value to `incident-config.json` |

## Payload Schema

Adapters (13.2, 13.3) must pipe a JSON object with this schema to `afx-incident file`:

```json
{
  "source": "ci",
  "signature": "<stable hash — e.g., failing test class + method>",
  "replay": {
    "command": "mvn test -Dtest=SaleControllerIT#scenarioX",
    "env": {}
  },
  "acceptance_criterion": "Given the application is running When SaleControllerIT#scenarioX executes Then it passes",
  "context": {
    "commit": "abcdef",
    "branch": "main",
    "run_id": "12345"
  }
}
```

Required fields: `source`, `signature`, `replay.command`, `acceptance_criterion`.
The `context` object is optional but should be included for diagnostics.

## Deduplication

The signature field is a stable identifier for the specific problem. When a CI adapter fires, it uses the same signature for the same failure (e.g., `SaleControllerIT#scenarioX` is the signature for that specific failing test). The skill checks whether any `TO_DO` or `IN_PROGRESS` Incidents ticket contains that signature (in the title or in a `technical_notes` row that starts with `signature: <value>`). If found: no new ticket is created — instead a `Repeat occurrence: <ISO timestamp>` entry is appended to `status_history`. This allows ticket 13.4 (knowledge-graduation hook) to detect recurring incidents.

## Title Prefix Convention

Every filed incident ticket has a source tag as its title prefix:

- `[ci] SaleControllerIT#scenarioX failing on commit abcdef`
- `[eval] regression in eval-042: precision dropped below 0.90`

This prefix makes the source immediately visible in `ticket-manager.sh status` and `ticket-manager.sh next` listings, without having to open the ticket details.

## Trust Slider

The trust slider controls how far the skill is allowed to go beyond just filing a ticket. The slider value lives in `.artifex/incident-config.json`, **not** in the ticket database — it is project policy, not ticket data.

| Level | Meaning (v1 behavior) |
|-------|----------------------|
| `file-only` | File ticket and exit. **(Default. Operative in v1.)** |
| `file-and-plan` | Wired, disabled in v1. Prints notice and exits after filing. |
| `file-and-implement` | Wired, disabled in v1. Prints notice and exits after filing. |
| `file-implement-pr` | Wired, disabled in v1. Prints notice and exits after filing. |

In v1, all four levels result in ticket filing followed by exit. The higher levels print a "wired but disabled" notice. Future tickets will wire up the plan/implement/PR flows.

## Priority

Incident tickets are filed with `implementation_order = 1` (a very low integer). This means they sort before regular feature stories (which typically have `implementation_order` values in the hundreds or thousands) in `ticket-manager.sh next`. Incidents are higher priority than feature work.

## Conversational Routing

When the user or an agent asks about incident management, route to the appropriate subcommand:

| Request | Subcommand |
|---------|-----------|
| "set up incident tracking", "add the Incidents epic", "initialize afx-incident" | `init` |
| "file an incident", "create a ticket from a CI failure" (with payload) | `file` |
| "what is the trust level?", "show the incident config" | `config get trust_level` |
| "enable automated implementation", "set trust to file-and-implement" | `config set trust_level file-and-implement` |

## from-ci Adapter (ticket 13.2)

The `from-ci` subcommand converts a GitHub Actions workflow run failure into an incident payload automatically.

### Manual mode

```bash
# By run URL
afx-incident.sh from-ci https://github.com/org/repo/actions/runs/12345678

# By run ID
afx-incident.sh from-ci 12345678
```

The subcommand:
1. Calls `gh run view <id> --json` to fetch job/sha/branch metadata
2. Calls `gh run view <id> --log-failed` to fetch failed-step logs (same pattern as `afx-cicd logs`)
3. Detects stack from the current working directory (borrowed from `afx-cicd-scaffold`)
4. Extracts: first failing job name, first failing test (JUnit pattern) or step name
5. Constructs signature: `<job_name>:<test_or_step>`
6. Constructs replay command: `./gradlew <task>` (JVM), `npm test` (Node), etc.
7. Builds payload via `build_incident_payload` — the same function used by `intake`
8. Pipes payload to `cmd_file` for validation, dedup, and filing

### GHA-side integration (pull mode)

Add the snippet from `skills/afx-cicd-scaffold/templates/incident-payload.yml.snippet` to your workflow as an `if: failure()` step. On failure it:
- Generates a JSON payload inline using GHA context variables
- Writes to `.artifex/incoming-incidents/<run-id>.json` on a branch called `incidents/auto`

Locally, after a CI failure:
```bash
git fetch origin incidents/auto
git checkout incidents/auto -- .artifex/incoming-incidents/
afx-incident.sh intake
```

`intake` processes each JSON file through `cmd_file`, removes it on success, and renames to `.failed` with an adjacent `.failed.log` on error.

### Payload canonicalization

Both `from-ci` and `intake` call `build_incident_payload()` with identical arguments for the same failure. This ensures dedup in `cmd_file` fires correctly regardless of which path was used.

The Gherkin AC is always: `Given commit <sha>, When CI re-runs, Then job <job_name> passes`

## How Adapters Use This Skill

Adapters in tickets 13.2 (CI) and 13.3 (eval) call this skill by piping a payload:

```bash
echo '{
  "source": "ci",
  "signature": "SaleControllerIT#scenarioX",
  "replay": {"command": "mvn test -Dtest=SaleControllerIT#scenarioX", "env": {}},
  "acceptance_criterion": "Given the app is deployed When SaleControllerIT#scenarioX runs Then it passes",
  "context": {"commit": "abc123"}
}' | ~/.claude/skills/afx-incident/afx-incident.sh file
```

The adapter does not need to handle deduplication — that logic lives here.

## from-eval Adapter (ticket 13.3)

The `from-eval` subcommand converts an afx-eval captured failure into an incident ticket without requiring any changes to afx-eval itself.

### Eval failure store format

afx-eval stores captured failures in `.artifex/evals/cases/EVAL-NNN.json`:

```json
{
  "id": "EVAL-001",
  "ticket_id": "2.3",
  "timestamp": "2026-04-23T10:00:00Z",
  "failure_type": "test_failure",
  "acceptance_criteria": ["Given... When... Then..."],
  "implementation_summary": "...",
  "failure_details": "...",
  "resolution": "",
  "status": "captured"
}
```

A case with `status: "captured"` is currently failing. `"resolved"` and `"regression"` are skipped by batch mode (only `captured` is processed).

### Payload mapping

| Incident field | Value |
|----------------|-------|
| `source` | `"eval"` |
| `signature` | The eval id (e.g., `EVAL-001`) — stable, used for dedup |
| `replay.command` | Synthesised pointer: `eval-manager.sh list # Inspect: <case_file>` |
| `acceptance_criterion` | `"Given the eval inputs, When the code runs, Then eval <id> passes"` |
| `context` | eval_id, ticket_id, failure_type, failure_details, captured_at |

### Single mode

```bash
afx-incident.sh from-eval EVAL-001
```

Reads the case file, builds the payload, pipes to `afx-incident file`.

### Batch mode

```bash
afx-incident.sh from-eval --batch
```

Iterates all `EVAL-*.json` files in `.artifex/evals/cases/`. For each `captured` case, files an incident via `afx-incident file`. Already-open tickets for the same eval get a repeat-occurrence entry (dedup via `cmd_file`) — re-running batch is safe, new ticket count = 0 on second run.

### No auto-close policy

When an eval transitions from `captured` to `resolved` (the developer fixed the code), its Incidents ticket is **NOT** automatically closed. This is intentional: a passing eval after a code change might still leave a real bug uncovered. Closure goes through normal `ticket-manager.sh done <id>`.

In batch mode, if a resolved eval still has an open Incidents ticket, a reminder is printed:

```
Warning: Eval EVAL-003 now passes but Incidents ticket inc-20260425... is still open.
--> Reminder: incident tickets are NOT auto-closed when an eval starts passing.
--> Close inc-20260425... manually via 'ticket-manager.sh done <id>' after verifying the fix.
```

### Eval-rewrite refusal (v1 policy)

Passing any rewrite-flavoured flag (`--rewrite-eval`, `--update-eval`, `--accept-current-output`, `--accept-output`, `--update-expected`) exits immediately with a v1 explainer:

```
Error: Eval-rewrite path is not supported in v1.

  The acceptance criterion for an eval is fixed: the code must pass the eval,
  not the other way around. If you believe the eval itself is wrong, that is
  a human decision -- edit the eval file directly:

    .artifex/evals/cases/<eval-id>.json
  ...
```

This is explicit policy. No escape hatch is provided in v1. A future v2 may add an approval-gated path.

### Environment override (for testing)

```bash
EVALS_CASES_DIR=/custom/path afx-incident.sh from-eval --batch
```

## Conversational Routing (updated for 13.3)

| Request | Subcommand |
|---------|-----------|
| "file an incident from this GHA run URL" | `from-ci <url>` |
| "process my incoming incidents" | `intake` |
| "set up incident tracking" | `init` |
| "file an incident from eval EVAL-001" | `from-eval EVAL-001` |
| "file incidents for all failing evals" | `from-eval --batch` |
| "file an incident" (with payload) | `file` |
| "what is the trust level?" | `config get trust_level` |

## Knowledge Graduation Hook (ticket 13.4)

When a single incident signature accumulates enough repeat occurrences, it is automatically promoted to a temporary lesson via `afx-knowledge-graduation`. Standard graduation rules then apply: the lesson can later be manually graduated to a permanent `CLAUDE.md` rule via `knowledge-manager.sh graduate <lesson_id>`.

### Trigger Point

The hook fires inside `afx-incident file` each time a repeat-occurrence entry is recorded (dedup path). After recording the entry, it counts all `status_history` rows with `note LIKE 'Repeat occurrence:%'` for the existing ticket. If the count reaches the configured threshold **and** no lesson has been graduated yet for that signature, `do_graduate_signature` is called.

### Configuration

Graduation behavior is controlled by the `graduation` object in `.artifex/incident-config.json`:

```json
{
  "trust_level": "file-only",
  "graduation": {
    "enabled": true,
    "threshold": 3,
    "per_source": {
      "ci": 5,
      "eval": 2
    }
  }
}
```

| Key | Default | Description |
|-----|---------|-------------|
| `graduation.enabled` | `true` | Set to `false` to disable graduation entirely |
| `graduation.threshold` | `3` | Global repeat-count threshold |
| `graduation.per_source.ci` | `5` | CI-source override (higher — flaky-test tolerance) |
| `graduation.per_source.eval` | `2` | Eval-source override (lower — eval failures are genuine) |

Per-source thresholds take precedence over the global `threshold`. Set `enabled: false` to opt out entirely.

### Idempotency

Idempotency is enforced by a `graduated: true` technical_note written to the incident ticket the first time graduation runs. On subsequent crossings of the threshold, `is_graduated()` detects the flag and skips the lesson-creation step, logging the existing lesson ID instead.

### Lesson Template

Each auto-generated lesson (written to `.artifex/lessons/L*.md`) contains:

```markdown
**Failure Signature:** <signature>
**Source:** ci | eval
**Replay Command:** `<most recent replay command>`
**Common Cause:** <!-- Fill in the recurring root cause before graduating to permanent rule -->
**When to Apply This Lesson:** <!-- Describe conditions when this rule should be checked -->
```

The user should fill in "Common Cause" before running `knowledge-manager.sh graduate <lesson_id>` to promote it to a permanent `CLAUDE.md` rule.

The lesson file also contains an `incident_signature: "<signature>"` frontmatter field used by `find_lesson_for_signature()` to detect existing lessons when filing new incidents.

### New-Incident Lesson Reference

When a new incident is filed for a signature that already has a graduated lesson, the ticket's description and a `technical_notes` row both reference the lesson:

- Description: `...See lesson L001 for recurring-pattern analysis.`
- Technical note: `lesson: L001 (auto-graduated recurring incident)`

This ensures the implementer sees the existing analysis without consulting `CLAUDE.md` separately.

### Manual Graduation

```bash
afx-incident.sh graduate <signature>
```

Promotes a signature immediately, regardless of repeat count. Idempotent — running a second time prints a no-op message and exits 0.

### Interop with afx-knowledge-graduation

This skill calls `knowledge-manager.sh add <description> <story_id>` to create the lesson, then appends incident-specific content (signature, replay, common-cause slot) directly to the lesson file. It does **not** modify `afx-knowledge-graduation` in any way. The lesson becomes a standard `afx-knowledge-graduation` entry and follows the same lifecycle (active → graduated → `CLAUDE.md`, or archived).

The `KNOWLEDGE_MANAGER` environment variable can override the path to `knowledge-manager.sh` (default: resolved relative to `afx-incident.sh`'s directory).

### Conversational Routing (graduation)

| Request | Subcommand |
|---------|-----------|
| "graduate this recurring incident", "promote signature X to a lesson" | `graduate <signature>` |
| "how many times until graduation?", "show graduation config" | `config get graduation` (reads JSON) |
| "disable graduation", "opt out of knowledge graduation" | edit `graduation.enabled: false` in `incident-config.json` |

## Version

1.4.0 (ticket 13.4 — knowledge-graduation hook for recurring incidents)
