---
name: issue-handler
description: >
  Use when asked to fix a GitHub issue end-to-end, run the agent pipeline
  on an issue, or process an `agent:active` / batch tracking issue.
  Orchestrates the full pipeline: `issue-triage` → `fix-reproduce`
  → `fix-root-cause` → `fix-implement` → `fix-verify` → report. Handles
  single-bug issues and batch-bug issues (a parent tracking multiple
  sub-bugs — skip-list or heterogeneous — fanned out one fix branch per
  sub-bug). On re-run, prioritizes new human feedback and skips work
  that is still valid.
---

# Issue Handler — End-to-End Orchestrator

This is the **high-level scenario skill** for handling a single GitHub
issue. It does not do the detailed work itself; it sequences the leaf
skills into one iterative pipeline and reports the result. Each stage's
mechanics live in its own skill — read and follow that skill when you
reach its stage.

Every agent-produced diff is a **proposal**. This skill commits the
staged fix onto a dedicated local branch `agent/fix-issue-<N>` (single
bug) or `agent/fix-issue-<N>-<seq>-<slug>` (per batch sub-item) and
records it in a `fix_result*.json`. When the bug is owned by an upstream
issue — a mirrored `pytorch/pytorch#<M>` DISABLED test, or a command
pointing at an upstream issue — the branch is named after **that**
number instead: `agent/fix-pytorch-issue-<M>[-<slug>]` (see
[Branch naming](#branch-naming)). It never
pushes, tags, or opens a PR — the invoking workflow reads each
`fix_result*.json`, exports `base_sha..branch` as a patch, and a human
applies it. The leaves themselves only stage (they never commit).

## Contents

- [Pipeline overview](#pipeline-overview)
- [Inputs](#inputs)
- [Execution modes](#execution-modes)
- [Branch naming](#branch-naming)
- Stage 0: [Re-run gate](#stage-0--re-run-gate-first-run-vs-re-run)
- Stage 1: [Triage](#stage-1--triage-issue-triage)
- Stage 2: [Reproduce](#stage-2--reproduce-fix-reproduce)
- [Stage 1u: Batch-issue fan-out](#stage-1u--batch-issue-fan-out) (skip-list + heterogeneous)
- Stage 3: [Root cause](#stage-3--root-cause-fix-root-cause)
- Stage 4: [Implement](#stage-4--implement-fix-implement)
- Stage 5: [Verify](#stage-5--verify-fix-verify)
- Stage 6: [Report](#stage-6--report)
- [Iterative loop bounds](#iterative-loop-bounds)
- [Issue-body status contract](#issue-body-status-contract)

## Pipeline overview

Single-bug path (default for `issue_type=single-bug`):

```
triage → reproduce → root-cause → implement → verify → report
                       ↑                          |
                       └─── loop up to 3 times ───┘
```

Batch path (`issue_type=batch-bug`, either `batch_kind`) — fan out the
single-bug pipeline per sub-item, each on its own fix branch:

```
triage → [preflight: install nightly wheel once]
       → for each sub-item:
             reset + reproduce
               ├─ NOT_REPRODUCED → heterogeneous: ALREADY_FIXED · skip-list: STALE_SKIP (+follow-up)
               └─ REPRODUCED → branch per [Branch naming](#branch-naming)
                               → root-cause → implement → verify
             (any failure marks the sub-item and continues the batch)
       → fan-out report
```

skip-list vs heterogeneous differ only in how a `NOT_REPRODUCED`
sub-item is labeled; the loop is identical.

| Stage | Leaf skill | Purpose |
|-------|-----------|---------|
| 1. Triage | `issue-triage` | Text-only classification: single-bug / batch-bug (+ `batch_kind`) / nonbug, `scope`, `runtime_dependencies`, preliminary verdict |
| 2. Reproduce | `fix-reproduce` | Verify the failure still reproduces against the nightly wheel (`stage=nightly`) |
| 3. Root cause | `fix-root-cause` | Deep source analysis, `target_repo`, `domain`, `IMPLEMENTING`/`NEEDS_HUMAN` |
| 4. Implement | `fix-implement` | Edit code, stage the diff (never commit) |
| 5. Verify | `fix-verify` | Run the refined command against source build, PASSED/FAILED/CANNOT_VERIFY |
| 6. Report | this skill | Summarize outcome to the user (or into the issue in pipeline mode) |

## Inputs

- A GitHub issue on `intel/torch-xpu-ops` or `pytorch/pytorch` (URL,
  number, or raw body).
- `pytorch_dir` — path to a local pytorch checkout, resolved as
  described in `fix-reproduce` Prepare. If absent, this skill lets
  `fix-reproduce` / `fix-root-cause` clone it into
  `$XPU_OPS_ROOT/agent_space_xpu/pytorch/`.
- Mode (see below).

## Execution modes

The pipeline runs in one of two modes — **interactive (default)** or
**pipeline** — which changes how every stage reports results and
whether it writes to the GitHub issue. Decide the mode at the start
and pass it to every leaf. See
[references/execution-modes.md](references/execution-modes.md) for
the full contract.

- **Interactive (default):** ask the user when blocked; report
  conversationally; do not touch the issue body / labels / comments
  unless the user asks.
- **Pipeline (explicit):** no human to interrupt — advance the
  issue's `agent:status` marker, update stage labels, let leaf
  skills leave their `<!-- agent:<name> -->` comments, and stop when
  the pipeline settles on a terminal verdict.

## Branch naming

One rule for both paths: **the branch is named after the issue that owns
the bug**, so a human reading a branch or a patch directory knows which
issue a PR closes.

| The bug is filed as | Branch |
|---|---|
| an `intel/torch-xpu-ops` issue `#N` | `agent/fix-issue-<N>` (batch sub-item: `agent/fix-issue-<N>-<seq>-<slug>`) |
| an upstream `pytorch/pytorch#<M>` (mirrored DISABLED test, or a command naming an upstream issue) | `agent/fix-pytorch-issue-<M>` (batch sub-item: `agent/fix-pytorch-issue-<M>-<slug>`) |

The upstream form applies whenever the sub-item's own identity is an
upstream issue — the common case on the CI DISABLED tracking issue,
where `#N` is only a queue and `#M` is the bug. `#M` is already unique
and stable, so the upstream form carries no `seq`. When one fix covers
several sub-items, the branch is named after the sub-item that was fixed;
the rest are recorded in `covers`.

`M` is the upstream issue the sub-item resolves to: the
`pytorch/pytorch#<M>` reference on its checklist line, its linked child
reference, or the upstream issue named in the invoking comment. If a
sub-item has no upstream number, use the `agent/fix-issue-<N>-…` form.

**A fix spanning both repos** ([two units](#machine-readable-outputs))
carries this **same name in both checkouts** — one bug, one name; do not
suffix the branch with the repo. The repo shows up in the patch directory
instead, which the export step names `<branch minus agent/>-<target_repo>`:
`agent/fix-pytorch-issue-197521` in each repo lands in
`fix-pytorch-issue-197521-pytorch` and
`fix-pytorch-issue-197521-torch-xpu-ops`. Two `fix_result` records naming
the same branch *and* the same `target_repo` are a naming bug, and the
export step fails there rather than overwrite one half (`format-patch`
restarts at 0001 in every directory).

## Stage 0 — Re-run gate (first-run vs re-run)

**Pipeline mode only; skip in interactive mode** (a human is already
driving, so just run the full pipeline). An `agent:active` issue is
frequently re-triggered — a maintainer leaves feedback, or the bot is
re-invoked with no new information. This gate decides, before spending
a build, whether this is a fresh run, a **human-feedback re-run**
(highest priority), or a **bare re-run** (skip work that already ran and
is still valid).

### Step 0.1: Detect prior agent activity

Find the most recent agent comment and its timestamp:

```bash
last_agent_ts=$(gh issue view "$N" --repo "$OWNER/$REPO" --json comments \
  --jq '[.comments[] | select(.body | test("<!-- agent:"))] | last | .createdAt // ""')
```

Empty `last_agent_ts` → **first run**. Skip the rest of Stage 0 and go
to Stage 1 normally.

### Step 0.2: Detect new human feedback

A comment is *human feedback* when it is authored by a non-bot account
**and** created after `last_agent_ts`. (The bot account is the one that
authored the `<!-- agent:* -->` comments; exclude it by login.)

```bash
new_human=$(gh issue view "$N" --repo "$OWNER/$REPO" --json comments \
  --jq --arg ts "$last_agent_ts" --arg bot "$BOT_LOGIN" \
  '[.comments[] | select(.author.login != $bot and .createdAt > $ts)] | length')
```

- `new_human > 0` → **human-feedback re-run**. Human feedback is the
  **highest priority signal.** Read every such comment verbatim and
  **prepend it to the failure description** you hand to Stage 3
  (`fix-root-cause` takes a free-form failure description; a leading
  "Maintainer feedback since last run: ..." block steers the
  re-analysis without any new leaf input). Run the **full** pipeline
  from Stage 1; do not take any of the skip fast-paths below. A human
  saying "still wrong" or "change X" overrides any cached verdict.
- `new_human == 0` → **bare re-run**. Continue to Step 0.3.

### Step 0.3: Bare re-run — skip what is still valid

No human pointed anything out since the last agent comment, so re-doing
the whole pipeline would just repeat identical work. Re-run **only** the
cheap front of the pipeline and compare against last time:

1. Run **Stage 1 (triage)** and **Stage 2 (reproduce)** as normal.
2. Compare the reproduce result to the previous run. The previous
   `refined_command` + verdict are recoverable from the last
   `<!-- agent:root-cause -->` comment's `analyzed_sha` context, or
   re-derived by reading the prior `<!-- agent:reproduce -->` /
   sweep comment. "Identical" means same verdict **and** same
   `refined_command`.
   - **Reproduce differs** (now passes, or a different command
     reproduces) → the situation changed on its own; resume the full
     pipeline **from Stage 3 (root-cause)** with the new reproduce
     result. Do not reuse the cached root-cause.
   - **Reproduce identical** → nothing observable changed. Hand off to
     Stage 3, which runs `fix-root-cause`'s own
     `<!-- agent:root-cause -->` `analyzed_sha` fast-path: if
     `target_repo` HEAD sha equals the recorded `analyzed_sha`, that
     leaf re-emits the prior verdict verbatim and this orchestrator
     **stops** (the earlier outcome — fix already staged, or
     `NEEDS_HUMAN` — still stands; there is nothing new to do). If the
     sha moved, `fix-root-cause` re-analyzes and the pipeline continues
     from Stage 3 as usual.

This never skips Stage 1/Stage 2 — they are cheap (text + nightly wheel)
and are the only way to notice the failure went away. It only avoids the
expensive Stage 3-5 rebuild+fix when both the observable failure and the
analyzed code are unchanged.

## Stage 1 — Triage (`issue-triage`)

Call `issue-triage` on the issue body + comments. It emits
`issue_type` (`single-bug` / `batch-bug` / `nonbug`), `batch_kind`
(`skip-list` / `heterogeneous` / `null`), `reproduction_missing`
(`yes` / `no`), `scope`, `runtime_dependencies`, and a preliminary
`handling` (`agent-fixable` / `needs-human`).

Branch on `issue_type` (triage already made the batch-vs-nonbug call;
no re-detection here):

- `nonbug` → stop the fix pipeline; skip to Stage 6 Report with
  `SKIPPED(reason=nonbug)`.
- `single-bug` with `reproduction_missing=yes` → stop; Stage 6 Report
  with `NEEDS_HUMAN(reason=reproduction_missing)`. `issue-triage`'s
  own comment already asks the reporter for a reproducer.
- `single-bug` with `reproduction_missing=no` → continue to Stage 2.
- `batch-bug` → **Stage 1u**, passing through `batch_kind` (the loop
  uses it only to label a `NOT_REPRODUCED` sub-item).



## Stage 2 — Reproduce (`fix-reproduce`)

Only for the single-bug path (`issue_type=single-bug`). Call
`fix-reproduce` with:

- `reproducer_command` — extracted by `issue-triage` from the issue
  body.
- `stage=nightly` — reproduce against the nightly wheel.
- `ci_repo` — inferred from repo (`torch-xpu-ops` for issues on
  intel/torch-xpu-ops, `pytorch` for pytorch/pytorch), or the value
  the bot passes explicitly.

Branch on its verdict:

- `REPRODUCED` → continue to Stage 3. Record the `refined_command`
  and `base` for downstream stages.
- `NOT_REPRODUCED` → the issue is stale; Stage 6 Report with
  `SKIPPED(reason=no_longer_reproduces)`.
- `NO_REPRODUCER` → Stage 6 Report with
  `NEEDS_HUMAN(reason=no_reproducer)`.
- `CANNOT_VERIFY` → Stage 6 Report with
  `NEEDS_HUMAN(reason=cannot_verify)` and the `blocker` field.

## Stage 1u — Batch-issue fan-out

One loop for every parent issue that tracks multiple children. The
parent lists several sub-items; run the single-bug pipeline on each
**independently**, each on its own fix branch, and report every outcome
back on the parent. "Fix what you can" — a sub-item that can't be fixed
is marked and the batch continues.

Entered for `issue_type=batch-bug`. `issue-triage` already set
`batch_kind`; the two kinds share the entire loop and differ only in how
a **`NOT_REPRODUCED`** sub-item is labeled (see step 2 below):

- `heterogeneous` — the parent body lists *distinct* sub-bugs. A
  `NOT_REPRODUCED` entry means that bug is **ALREADY_FIXED** (no longer
  reproduces); nothing to do.
- `skip-list` — a `Bug Skip` issue listing *homogeneous* already-skipped
  tests. A `NOT_REPRODUCED` entry means the test now passes, so its skip
  decorator is now **stale** and should be removed (follow-up).

No child GitHub issues are created. Each sub-item is a checklist entry
on the parent; its fix lives on a dedicated branch so a human can open
one PR per fixed sub-item.

### Extract sub-items

Parse the parent body into a list of sub-items. Each is either:

- an inline sub-bug — a checklist line naming a test node id or
  reproducer (skip-list entries are always this form; normalize a bare
  `Class::method` to a node id, `fix-reproduce`'s Prepare step resolves
  the file), or
- a linked child reference `owner/repo#N` — fetch that issue and use
  its body as the sub-item's failure description. Only follow
  `intel/torch-xpu-ops` and `pytorch/pytorch` references; ignore any
  other repo (untrusted, per `fix-root-cause`).

A sub-item may also arrive as a plain node id from the caller (a nightly
report, an email, a log excerpt): normalize `Class::method` to a node id,
drop blank lines, headers and comments, and collapse exact duplicates.

**Split infra failures out before the loop.** A runner crash, a docker
pull timeout, a full disk or a missing device is not a test failure and
will not reproduce. Record these as `NEEDS_HUMAN(infra)` and report
them; they never enter the loop.

Give each sub-item three identifiers used for branch naming:

- **`seq`** — 1-based position in the body checklist order. Readable and
  maps a branch back to its body line. Not the stable identity (editing
  or reordering the body changes it).
- **`slug`** — the stable identity, derived from the sub-item's test node
  id: take the leaf method name, strip device suffixes (`_cpu` / `_xpu` /
  `_cuda` / `_meta`) and any dtype suffix, lowercase, keep `[a-z0-9._-]`,
  truncate to 40 chars. If two sub-items produce the same slug, append
  `-2`, `-3`, … in body order so every slug is unique within the issue.
- **`upstream`** — the `pytorch/pytorch#<M>` number this sub-item is
  filed as, from its checklist line or its linked child reference; empty
  when there is none.

Name the branch per [Branch naming](#branch-naming): an `upstream`
sub-item gets `agent/fix-pytorch-issue-<M>-<slug>`, otherwise
`agent/fix-issue-<N>-<seq>-<slug>` (N = parent issue number). On a
**re-run**, match a sub-item to its prior branch by slug — look for an
existing `agent/fix-*-<slug>` branch; if found, that is the same
sub-item (rename it if its seq moved, never create a duplicate). Skip
headers, prose, and empty lines.

### Preflight (many entries): install nightly wheel once

When the list is long (skip-list issues routinely have dozens),
front-load the one real wheel install so the per-entry
`fix-reproduce(stage=nightly)` calls each find the env already current
and return fast. `fix-reproduce` always issues `pip install --pre
--upgrade` (it refuses to trust a stale wheel); running it once here
does the real work. There is no `skip_wheel_install` flag — this is
purely an ordering optimization:

```bash
pip3 install --pre --upgrade torch torchvision torchaudio \
  --index-url https://download.pytorch.org/whl/nightly/xpu
python -c "import torch; print('nightly:', torch.__version__)"
```

### One fix may cover several sub-items

Entries in a batch are often the same bug in the same class. Fixing each
one separately runs root-cause and a source build (1-3 h) per entry and
produces the same patch several times.

Do not guess which entries share a fix:

1. Order the `REPRODUCED` entries so likely relatives sit together (same
   test class or file, same error). Ordering only; it decides nothing.
2. Fix the first entry.
3. Re-run the other entries in that group against the staged fix. The
   build is already done, so this costs one test run each. `fix-verify`
   already produces a before/after table; extend it to the group.
4. Entries that now pass go in the fix's `covers` list and are not fixed
   again. Entries that still fail take their own turn from step 2.

One fix gets one branch and one `fix_result.json`, naming in `covers`
every entry it also fixed.

### Per-sub-item loop

Capture the two base SHAs once, then for each sub-item reset both
checkouts per the shared
[reset-between-entries recipe](references/execution-modes.md#reset-between-entries-recipe-batched-fan-out)
— a prior sub-item's staged diff must not bleed into the next.

For each sub-item:

1. **Reset** both checkouts to the base SHAs (shared recipe).
2. **Reproduce** (`fix-reproduce`). It detaches the pytorch tree to its
   base (`origin/main` or the `ci_commit` fallback) and returns that
   `base` plus a `refined_command`.

   `stage` follows the issue, like `allow_skip`:

   - **CI break** (`pytorch-ci-failure`, or a mirrored DISABLED test) →
     `stage=auto`. Many CI failures only reproduce on a source build.
     `stage=nightly` returns `NOT_REPRODUCED` as soon as the nightly
     wheel passes and does not fall through, which records unfixed work
     as already fixed.
   - **Everything else** → `stage=nightly`.

   Branch on the verdict:
   - `REPRODUCED` → continue to step 3; keep its `base` and
     `refined_command`.
   - `NOT_REPRODUCED` → nothing to fix; record it and go to the next
     sub-item. The label depends on `batch_kind`:
     - `heterogeneous` → **ALREADY_FIXED**: the reported bug no longer
       reproduces on latest nightly (upstream fixed it, or it was flaky).
       No action needed.
     - `skip-list` → **STALE_SKIP**: the test now passes, so its skip
       decorator is obsolete. Record a **follow-up** to remove the
       decorator — the orchestrator does **not** delete it here (see
       "Stale skips" below).
   - `NO_REPRODUCER` → **INVALID_ENTRY** (renamed/removed test, or
     malformed). Record, continue.
   - `CANNOT_VERIFY` → **UNVERIFIED** (environmental). Record, continue.
3. **Root-cause, then branch.** Run **Stage 3** (`fix-root-cause`)
   first — it returns `target_repo`, which decides *which* checkout the
   fix (and thus the branch) lives in:
   - `target_repo=pytorch` → `target_repo_dir = pytorch_dir`, base is
     the reproduce `base` on the pytorch tree.
   - `target_repo=torch-xpu-ops` →
     `target_repo_dir = pytorch_dir/third_party/torch-xpu-ops`, base is
     `xpu_ops_base` (the submodule's pinned commit from the shared
     recipe). `fix-implement` / `fix-verify` handle the `xpu.txt` pin
     rewrite internally; the orchestrator only creates the branch.

   Create the isolated fix branch on `target_repo_dir` off its base so
   the diff is pushable on its own ([Branch naming](#branch-naming)):

   ```bash
   # upstream sub-item: branch="agent/fix-pytorch-issue-${M}-${slug}"
   branch="agent/fix-issue-${N}-${seq}-${slug}"
   git -C "$target_repo_dir" checkout -B "$branch" "$base"
   ```

   Then run **Stage 4 → 5** (same contract and 3-attempt bound as the
   single-bug path). `fix-implement` expects exactly this: a fresh
   branch the orchestrator just created, clean worktree.
4. **On any leaf `NEEDS_HUMAN` / `CANNOT_VERIFY` / `FAILED`** (or
   attempts exhausted): mark **this sub-item** blocked with the reason,
   record it, and **continue to the next sub-item** — never abort the
   whole batch on one hard sub-item.
5. **On `fix-verify` PASSED**: do NOT push or open a PR. **Update** the
   sub-item's `fix_result-${slug}.json` in place (written at
   `verdict=PENDING_VERIFY` when the branch was committed) to
   `verdict=PASSED` with `fix_repo_dir`, `branch`, `base_sha` (the
   sub-item's base from step 3), `changed_files`,
   so the workflow can export `base_sha..branch` per sub-item. As in the
   single-bug path, commit the branch and write the `PENDING_VERIFY`
   record as soon as `fix-implement` returns `READY`, not only on PASSED,
   so a crash mid-sub-item leaves a recoverable record instead of an
   orphan branch.

### Stale skips (`batch_kind=skip-list` only)

A `STALE_SKIP` sub-item's skip decorator can be removed, but this
orchestrator does **not** delete it. Deleting a skip is itself a code
change that needs its own verify + PR; folding it into this sweep would
mix "the skip is obsolete" with "and here's the removal diff" and
obscure the batch outcome. Instead, surface every `STALE_SKIP` in the
report as an explicit follow-up (candidate for a human, or a separately
invoked `fix-implement` run to remove the decorator). The report is the
hand-off; nothing is auto-removed.

### Fan-out report

Post one summary comment on the parent (or surface to the user in
interactive mode), and mirror it into the parent's checklist:

```
<!-- agent:batch-fanout -->

## Batch fan-out results

Base: <torch nightly version or base sha>

| Sub-item | Outcome | Branch / Reason |
|---|---|---|
| test_bar_xpu_float32 | FIXED | agent/fix-issue-4321-1-test_bar |
| test_baz | NEEDS_HUMAN | needs a oneDNN change (`dependency component: oneDNN`) |
| test_qux | NEEDS_HUMAN | attempts_exhausted |
| test_new | ALREADY_FIXED | no longer reproduces on latest nightly |
| test_old | STALE_SKIP | follow-up: remove skip decorator |
| test_dup | COVERED | by test_bar's fix, agent/fix-issue-4321-1-test_bar |
| test_hard | SKIPPED | skip added, tracking issue intel/torch-xpu-ops#1234 |
| test_gone | INVALID_ENTRY | does not collect |

- **FIXED:** N sub-items — one branch each, ready for a human to open
  a PR.
- **NEEDS_HUMAN:** M sub-items — see per-sub-item reason.
- **ALREADY_FIXED:** J sub-items (heterogeneous only) — no longer
  reproduce; the reported bug is resolved, no action needed.
- **STALE_SKIP:** K sub-items (skip-list only) — the test now passes;
  follow up to remove the obsolete skip decorator.
- **INVALID_ENTRY / UNVERIFIED:** P sub-items — malformed/renamed, or
  environmental during reproduce.

*Automated by issue-handler.*
```

Omit category rows that have no members (a `heterogeneous` batch has
`ALREADY_FIXED` but never `STALE_SKIP`, and vice versa). The
`<!-- agent:batch-fanout -->` marker lets a
re-run locate and update this same comment in place. On a re-run
(Stage 0), only re-process sub-items that are not already `FIXED` on a
live branch, unless human feedback (Stage 0.2) reopens a specific one.

After the loop, go to Stage 6 Report with the aggregate outcome
(`IMPLEMENTING(fix_verified)` if any sub-item was fixed;
`NEEDS_HUMAN` only if *every* actionable sub-item needed a human —
`STALE_SKIP` follow-ups do not by themselves force `NEEDS_HUMAN`).

### Machine-readable outputs

Written on **both** paths: the single-bug Stage 4/5 writes
`fix_result.json`, and a batch fan-out writes `batch_summary.json` plus
one `fix_result-<slug>.json` per fixed sub-item. The
`<!-- agent:batch-fanout -->` comment is for humans; these files let the
invoking bot workflow export patches and drive re-verification without
re-parsing a comment.

They live under `$AGENT_SPACE` (the gitignored scratch dir). In pipeline
mode the bot workflow always sets it. In interactive mode it is usually
unset, which would resolve to `/fix_result.json` — so fall back to
`<repo>/agent_space_xpu/` (the same gitignored dir AGENTS.md defines) and
report the path you used:

```bash
agent_space="${AGENT_SPACE:-$(git -C "$target_repo_dir" rev-parse --show-toplevel)/agent_space_xpu}"
mkdir -p "$agent_space"
```

- **`batch_summary.json`** — one file listing every sub-item:

  ```json
  {
    "issue": 4321,
    "kind": "batch-bug",
    "batch_kind": "heterogeneous",
    "sub_items": [
      { "seq": 1, "slug": "test_bar", "outcome": "FIXED",
        "branch": "agent/fix-issue-4321-1-test_bar",
        "target_repo": "torch-xpu-ops",
        "fix_result": "fix_result-test_bar.json",
        "companion": "fix_result-test_bar-pytorch.json",
        "summary": "one-line what/why" },
      { "seq": 2, "slug": "test_baz", "outcome": "NEEDS_HUMAN",
        "branch": null, "reason": "other",
        "reason_detail": "needs a oneDNN change (dependency component: oneDNN)" },
      { "seq": 3, "slug": "test_new", "outcome": "ALREADY_FIXED",
        "branch": null, "reason": "no longer reproduces on latest nightly" }
    ]
  }
  ```

- **`fix_result-<slug>.json`** — for each `FIXED` sub-item, the same
  schema the single-bug Stage 5 writes as `fix_result.json`. It MUST
  include the keys the workflow's schema gate and patch-export read:
  - `verdict` — `PASSED` / `FAILED` / `CANNOT_VERIFY` / `PENDING_VERIFY`.
    Written incrementally: Stage 4 writes `PENDING_VERIFY` when it commits
    the branch, Stage 5 updates it to the terminal verdict. Only `PASSED`
    is exported as a normal patch; a non-PASSED verdict that still names a
    reachable `branch` + `base_sha` is salvaged as an `unverified/` patch
    so the committed work is not lost with the runner. Keep `branch` and
    `base_sha` accurate even on a non-PASSED record.
  - `target_repo` — `torch-xpu-ops` | `pytorch`,
  - `fix_repo_dir` — absolute path of the git repo holding the fix commit,
  - `branch` — per [Branch naming](#branch-naming),
  - `base_sha` — the commit the fix branch was started from,
  - `changed_files` — list of the files the fix touched,
  - `depends_on` — the fix_result file that must land BEFORE this one,
    or absent. It is the only record of the order (see below).
  plus `needs_build`, `refined_command`, `notes`. Suffixed by slug so each
  sub-item can be exported / re-verified independently.

**A fix spanning both repos** (`fix-root-cause` returned a
`companion_repo`) is two units:

- one branch per repo, named after the issue as usual,
- one fix_result per repo, the companion's being the primary's name plus
  `-<target_repo>` (`fix_result-grid_sample-pytorch.json`),
- `depends_on` on the companion, naming the primary's file,
- one verdict for both, and `notes` saying the pair was verified together,
- `batch_summary`'s sub-item points at the unit that fixes the test.

The patch-export step iterates every `fix_result*.json` and emits one
patch series per unit from `base_sha..branch` (the branches are not
pushed); `batch_summary.json` enriches it (target_repo, summary line).
A unit that reports a verified fix but yields no patch fails the step.

## Stage 3 — Root cause (`fix-root-cause`)

Called on the single-bug path, and once per sub-item from Stage 1u.
Call `fix-root-cause` with the failure description and the
`refined_command` from Stage 2.

Branch on its `verdict`:

- `IMPLEMENTING(reason=ok)` → record `target_repo`, `domain`,
  `analyzed_sha`, `root_cause`, `fix_strategy`, then create the fix
  branch before Stage 4 — `target_repo` is what decides which checkout
  it lives in, and `fix-implement` expects a fresh branch with a clean
  worktree (`$base` is Stage 2's reproduce base; the batch path already
  did this in its own step 3, with its own branch name):

  ```bash
  # upstream-filed bug: branch="agent/fix-pytorch-issue-${M}"
  branch="agent/fix-issue-${N}"
  git -C "$target_repo_dir" checkout -B "$branch" "$base"
  ```
- `NEEDS_HUMAN` → Stage 6 Report, carrying the leaf's one-line
  `reason_detail` as the justification — that is what a maintainer
  reads. Every reason lands on the same terminal status
  (`NEEDS_HUMAN` / `agent:needs-human`), so do not shop for a code:
  the two worth branching on are `no_registered_domain` (do not retry,
  see Retry policy) and `invalid_reproduction` (re-run Stage 2 first).

## Stage 4 — Implement (`fix-implement`)

Call `fix-implement` with `triage_result`, `pytorch_dir`,
`target_repo_dir` (derived from `target_repo`), and `allow_skip`:

`allow_skip` follows the issue:

- **CI break** — the issue carries the `pytorch-ci-failure` label, or
  mirrors a DISABLED test from pytorch/pytorch. `allow_skip=true`. Fix
  it in place where you can, in pytorch or here. Skip only when the fix
  needs a dependency, information you do not have, or feature-sized
  work; `fix-implement` then files a tracking issue for the real fix.
- **Everything else** — `allow_skip=false`. Never add a skip decorator.

If the caller states the flag, the caller wins.

Branch on the verdict:

- `READY(reason=ok)` → **commit the staged fix onto the branch created in
  Stage 3, and write the incremental `fix_result.json`**, then continue
  to Stage 5.
  Do NOT wait for Stage 5 to persist — if verify then crashes or the
  context runs out, the committed branch would otherwise be an
  orphan the Export step flags as a lost fix. Committing here keeps the
  branch and the hand-off record in lock-step:

  ```bash
  git -C "$target_repo_dir" commit -m "fix: <one-line summary> (#${N})"
  ```

  Then write `fix_result.json` (see "Machine-readable outputs" for the
  path and its interactive-mode fallback) with the fields known so far
  and `verdict=PENDING_VERIFY`: `target_repo`, `fix_repo_dir`, `branch`,
  `base_sha`, `changed_files`, `analyzed_sha`, `root_cause`, `notes`.
  The branch is local and dies with the runner, so this record is what
  lets the workflow salvage the commit as an `unverified/` patch if
  verification never finishes — without it, a crash after this point
  loses the work outright.

  **With a `companion_repo`**, do this twice, `target_repo` first: call
  `fix-implement` again with `target_repo_dir` set to the companion
  checkout, branch off that repo's HEAD, commit, and write its record per
  "Machine-readable outputs".
- `NEEDS_HUMAN` → Stage 6 Report, carrying the leaf's `reason_detail`
  as the justification. The label is `agent:needs-human` whatever the
  `reason` (`skip_outside_target_repo` / `skip_guard_rejected` /
  `no_fix_possible` / etc.).

## Stage 5 — Verify (`fix-verify`)

Call `fix-verify` with `refined_command` (from Stage 2),
`target_repo_dir`, and `changed_files` (from Stage 4). `fix-verify`
unconditionally produces the FAIL->PASS before/after table and runs
`spin fixlint` on a passing result — no flags to pass.

A two-repo fix is verified **once, as a pair** — one tree, one build.
Both records take that verdict.

Branch on the verdict:

- `PASSED(reason=ok)` → the fix is verified. The branch and
  `fix_result.json` already exist from Stage 4; **update** the record
  in place — set `verdict=PASSED` and add `needs_build`, the
  before/after result, and any final `notes` — then go to Stage 6 with
  `IMPLEMENTING(fix_verified)`. `fix-verify` leaves its `spin fixlint`
  fixes staged: amend them into the Stage 4 commit
  (`git commit --amend --no-edit`) so they reach the exported patch —
  never a second commit. The workflow exports `base_sha..branch` as the
  patch. Never push or open a PR.
- `FAILED` → **loop back to Stage 4** with the failure output as
  additional context (see "Iterative loop bounds"). On a retry the
  branch already carries the previous attempt's commit, so **amend it**
  (`git commit --amend --no-edit` after staging the new edits) rather
  than creating a second commit or a new branch — `base_sha..branch`
  must stay a single reviewable fix, and Stage 5's clean-worktree
  assertion requires nothing left uncommitted. If attempts are
  exhausted, update `fix_result.json` to `verdict=FAILED` with the last
  failure in `notes` so the record explains the branch, then Stage 6
  Report `NEEDS_HUMAN(reason=attempts_exhausted)`.
- `CANNOT_VERIFY` → update `fix_result.json` to
  `verdict=CANNOT_VERIFY` with the blocker in `notes`, then Stage 6
  Report `NEEDS_HUMAN(reason=<verify's reason>)`. Do not loop on
  CANNOT_VERIFY — the environment problem will not fix itself.

## Stage 6 — Report

At the end, summarize the outcome. In **interactive mode** present
this to the user in plain language. In **pipeline mode** advance
the issue's `agent:status` to the terminal stage
(`DONE` / `NEEDS_HUMAN` / `SKIPPED`) and update the checklist per
[execution-modes.md](references/execution-modes.md); a batch fan-out
run also posts the `<!-- agent:batch-fanout -->` summary.

Always include in the summary:

- **Issue:** link/number and one-line title.
- **Path:** `single-bug` / `batch` (with `batch_kind`).
- **Outcome:** `IMPLEMENTING(fix_verified)` / `NEEDS_HUMAN(<reason>)`
  / `SKIPPED(<reason>)`.
- **Root cause** (from Stage 3, if reached).
- **Files changed** (from Stage 4, if reached).
- **Verification** (from Stage 5, if reached).
- For a batch: per-sub-item outcome + branch name (FIXED) or reason
  (NEEDS_HUMAN / ALREADY_FIXED / INVALID_ENTRY / UNVERIFIED), plus any
  `STALE_SKIP` follow-ups when `batch_kind=skip-list`.

If the outcome is `IMPLEMENTING(fix_verified)`, the fix is committed on
its fix branch ([Branch naming](#branch-naming)) and recorded in
`fix_result.json`. The invoking workflow reads that, exports
`base_sha..branch` as a patch
artifact, and a human applies it. **Do not push or open the PR from this
skill.**

### `batch-fanout` and `summary` block templates

Both are this skill's own blocks, so write them collapsed as shown —
there is no `##` heading to strip, unlike the leaf blocks this skill
wraps.

Batch runs only:

```markdown
<!-- agent:batch-fanout -->

<details>
<summary><b>Batch fan-out results</b></summary>

Base: `torch-xpu-ops@<short_sha>`, pytorch `origin/main@<short_sha>`
(reproduce base: nightly `<wheel version>`)

| Sub-item | Outcome | Branch / Reason |
|---|---|---|
| 4. `test_foo_xpu_float8_e4m3fn` | FIXED | `agent/fix-pytorch-issue-197334-test_foo` |
| 5. `test_foo_xpu_float8_e5m2` | COVERED | by sub-item 4's fix, same branch |

- **FIXED:** <n> sub-items — <what a human should do with the branches>
- **COVERED:** <n> sub-items — <why no separate branch>
- **NEEDS_HUMAN / STALE_SKIP / INVALID_ENTRY / UNVERIFIED:** none.

*Automated by issue-handler.*

</details>
```

Every run. The sign-off is **outside** `</details>` — it is the comment's
footer, not part of the summary:

```markdown
<!-- agent:summary -->

<details>
<summary><b>Summary</b></summary>

<2-4 sentences: the one bug behind the sub-items, the fix in one clause,
and what verification showed. No bullets.>

- **Branch:** `agent/fix-issue-<N>[-<seq>-<slug>]`, or
  `agent/fix-pytorch-issue-<M>[-<slug>]` for an upstream-filed bug
- **Base:** `<full 40-char sha>`
- **Changed files:** `path/to/file.cpp`
- **Nothing was pushed.** The patch is exported as a workflow artifact for
  a human to review and apply.

</details>

_Generated by [fix job](<run url>)._
```

**A two-repo fix needs the order spelled out**, or whoever opens the PRs
gets it wrong and breaks CI. At the top of the Summary body:

```markdown
> [!IMPORTANT]
> **Two PRs, in this order.**
> 1. `<lands-first repo>` — patch dir `<branch>-<repo>`.
> 2. `<companion repo>` — patch dir `<branch>-<repo>`, after <what it waits
>    on, e.g. the `xpu.txt` pin bump carrying step 1>.
>
> **No closing keyword in either PR** (`Fixes` / `Closes` / `Resolves`):
> merging one would close the issue with the other half still out. Reference
> the issue plainly; a human closes it once both have landed.
```

and one line **outside** `</details>`, above the `_Generated by_` footer,
since that block renders collapsed:

```markdown
**Two patches, and the order matters — see Summary.**
```

### Review request block

When the job ends, the `fix` workflow appends the `<!-- agent:review -->`
block itself — the verdict menu a reviewer copies into a new comment.
**Do not write it yourself**, and do not restate it here: the template
lives in `bot.yml` and a second copy only drifts from it.

## Iterative loop bounds

The pipeline is not strictly linear. Loop when a later stage
invalidates an earlier assumption:

- Stage 5 `FAILED` → return to Stage 4 (refine the fix).
- Stage 4's Step 3.5 skip-guard rejects → the leaf itself re-runs
  once; a second rejection returns `NEEDS_HUMAN` and the
  orchestrator does not retry.

Bound: **maximum 3 fix attempts** (Stage 4 → Stage 5 → Stage 4 …).
This matches the legacy pipeline's `max_agent_attempts`. When you
stop without success, report `NEEDS_HUMAN(reason=attempts_exhausted)`
with the last `fix-verify` failure output in `reason_detail`.

Do **not** loop on:

- `CANNOT_VERIFY` at any stage (environment problem, not fix
  problem).
- `NEEDS_HUMAN` from any leaf (contract: the leaf already decided
  it needs a human).
- Stage 3 `no_registered_domain` (domain registry is a fixed set,
  looping won't unstick it).

## Issue-body status contract

**Pipeline mode only.** In interactive mode, do not touch the issue
body/markers/labels unless the user asks — report to the user
instead.

This orchestrator owns advancing the overall `<!-- agent:status:X -->`
marker through:

```
DISCOVERED → TRIAGING → REPRODUCING → TRIAGED → IMPLEMENTING →
VERIFYING → DONE
```

with terminal alternates `NEEDS_HUMAN` and `SKIPPED`. Stage-by-stage
mapping to labels is in
[references/execution-modes.md](references/execution-modes.md); each
leaf skill owns its own `<!-- agent:<name> -->` comment/log slot,
this orchestrator owns the overall `agent:status` marker + the
Action Items checklist.
