---
name: verify-diff
description: Empirically verify that a code diff achieves its stated objective by dispatching a bias-free executor that actually runs auto-derived evaluation scenarios against the post-diff file. The executor returns a JSON verdict with `suggested_edits`, and this skill applies them iteratively until the executor declares no further fixes are needed, max-iterations is reached, or a safety rail trips. Non-interactive — no user prompts. Use after applying an Edit when you need a dynamic cross-check that complements static reviewers like skill-review.
allowed-tools: Read, Edit, Agent, TaskCreate, TaskUpdate, TodoWrite, Bash(git diff *), Bash(git checkout HEAD -- *)
---

# Verify Diff

The convergence signal is the executor itself returning `suggested_edits: []` on a pass verdict — that is, the executor declares nothing more is left to fix. This skill loops until that signal, max iterations is reached, or a safety rail trips.

Designed to be called from non-interactive routines such as `dev-workflow-triage`. It never prompts the user; it either returns a structured summary or terminates early with a machine-readable reason code.

## Invocation contract

The caller passes these fields in natural language (the skill extracts them from the invocation text):

- `Description` *(required for explicit-args mode; absent for auto-derive mode)* — the original problem the diff is supposed to address
- `Suggested fix direction` *(required for explicit-args mode; absent for auto-derive mode)* — how the diff was meant to be shaped
- `Target file` *(required for explicit-args mode; absent for auto-derive mode)* — one relative path (single-file scope; multi-file diffs are out of scope in this mode)
- `Base ref` *(optional, default `HEAD`)* — git ref to diff against (both modes)
- `Max iterations` *(optional, default `3`)* — upper bound on the refinement loop (both modes; auto-derive applies the same upper bound to each per-skill loop)

### Mode determination

A field counts as **provided** iff the caller supplied a non-empty, non-whitespace value. Empty string and whitespace-only count as **absent**.

- **All three of** `Description` / `Suggested fix direction` / `Target file` provided → **explicit-args mode** (run `## Workflow` § Step 1 – Step 5 unchanged).
- **All three absent** → **auto-derive mode** (run `## Auto-derive mode` instead).
- **1 or 2 of the three provided** (incomplete framing) → return early with the explicit-args mode Step 1 schema (does **not** enter auto-derive mode — `incomplete args` is an explicit-args bug signal, surfaced loudly rather than silently falling back):

  ```json
  {"status": "skipped", "reason": "incomplete args", "iterations_used": 0, "applied_edits_count": 0, "unresolved_gaps": [], "reverted_paths": [], "objective_met": "unknown"}
  ```

The caller must **not** stage changes while this skill is running. The skill reads the working tree vs `Base ref`; staged content would mix into the diff and corrupt the verdict.

## Auto-derive mode

Triggered when `Description`, `Suggested fix direction`, and `Target file` are **all** absent (see `## Invocation contract` § Mode determination). The mode infers intent from the diff itself and verifies on a per-skill basis, so the empirical check still runs when the caller (e.g. `dev-workflow` `hooks.on_complete`) has no per-Finding framing to pass through. The per-iteration loop semantics defined in `## Workflow` § Step 3 are reused verbatim per skill, with the auto-derive-specific overrides spelled out in `A2 § Per-iter loop semantics`.

### A1. Collect diff and group by skill

1. Run `git diff <Base ref>` (`Base ref` defaults to `HEAD`).
2. If the diff is empty, return early with the auto-derive aggregate shape (matches A3 schema):

   ```json
   {"mode": "auto-derive", "status": "skipped", "reason": "empty diff", "iterations_used_total": 0, "applied_edits_count_total": 0, "non_skill_files": [], "per_skill": {}}
   ```

   Auto-derive treats an empty diff as `skipped` rather than `conflict` (the explicit-args mode policy) — without caller framing, an empty diff is informational, not a bug signal.
3. Compute `affected_files` from the diff (`git diff --name-only <Base ref>`).
4. Group each file by skill prefix:
   - `skills/<name>/...` → skill `<name>`
   - `.claude/skills/<name>/...` → skill `<name>`
   - paths outside both prefixes → `non_skill_files` bucket (not dispatched; reported in the aggregate verdict)
   - **Dedup**: if the same skill `<name>` appears under both prefixes (a file under `skills/<name>/...` and another under `.claude/skills/<name>/...`), merge both into the same skill key so symlink-resolved or dev-test-symlink layouts do not register the same skill twice. `files` keeps every distinct path string that appeared in the diff (both prefixes if they were both listed). `primary_file` selection (A2 § Primary file selection) prefers the `skills/<name>/...` form when both are present.
5. If `skill_groups` is empty (only non-skill files were changed), return early with `status=skipped`, `reason="no skill files in diff"`. The aggregate verdict still records the skipped paths so the caller can see what was bypassed:

   ```json
   {"mode": "auto-derive", "status": "skipped", "reason": "no skill files in diff", "iterations_used_total": 0, "applied_edits_count_total": 0, "non_skill_files": ["<path>", "..."], "per_skill": {}}
   ```

### A2. Per-skill dispatch

Auto-derive pre-registers `N skills × Max iterations` task rows total, namespaced by `<skill>`. For each `(skill, files)` pair in `skill_groups`:

1. **Pre-register iteration tasks** — `TaskCreate` one task per iteration named `<skill> iter 1` … `<skill> iter <Max iterations>` following the same protocol as `## Workflow` § Step 3 pre-registration (mark `in_progress` via `TaskUpdate` before dispatch, `completed` after parse + apply, append `— skipped: <reason>` on early exit).
2. **Primary file selection** — pick `primary_file` = `<skill-dir>/SKILL.md` if it appears in `files`; otherwise the first entry in `files` sorted lexicographically. This choice only labels the per-skill verdict's `primary_file` field for human-readable anchoring and influences the dispatch payload's framing — it does **not** affect per-skill verdict semantics (`status` / `applied_edits_count` / `objective_met` etc.), which are emitted for the whole `files` set.
3. **Per-iter snapshot** — on iter 1, `Read` the full current contents of every entry in `files` (per-iter snapshot) and run `git diff <Base ref> -- <files>` to capture the per-skill diff. The A1 (1) full-tree diff is used **only for skill-prefix splitting in A1** and is not forwarded to the dispatch payload; the `--- DIFF ---` payload carries the per-skill diff only, keeping main-thread context bounded. On `i ≥ 2`, only re-`Read` the subset of `files` whose path appeared in a successfully-applied `suggested_edits` entry during iter `i-1` (untouched files keep their iter-1 snapshot — wasted work otherwise, same convention as `publicity-review` § Step 2 (a)) and re-run `git diff <Base ref> -- <files>` so the per-skill diff reflects edits that landed in prior iterations.
4. **Dispatch payload assembly** — invoke the `Agent` tool to dispatch a fresh executor. Assemble the dispatch prompt from the four sections below, each framed with a clear `--- LABEL ---` fence (same convention as `## Workflow` § Step 3 (a) Dispatch bias-free executor and sibling Pattern A skills `publicity-review` / `skill-review`):

   - `--- DIFF ---`: the per-skill multi-file diff captured in `A2 § Per-iter snapshot`
   - `--- AFFECTED FILES ---`: each entry in `files` as a `### <path>` sub-heading followed by that file's full current contents (publicity-review pattern)
   - `--- INFERENCE PROMPT ---`: the body of `references/auto-derive-prompt.md` § Executor prompt injected verbatim, with `<skill-name>` substituted by the current skill's name. The reference holds the two-phase Phase 1 INFER INTENT (1–2 sentences) + Phase 2 VERIFY (scenarios + checklist) prompt
   - `--- RESPONSE FORMAT ---`: `references/auto-derive-prompt.md` § Response format injected verbatim — output schema (including `inferred_intent` and per-entry `file` for `suggested_edits`) plus the "1–3 lines of surrounding context" convention. Single canonical home for the response format; do not duplicate the schema body in this SKILL.md

   **Agent unavailable fallback**: detect availability and fall back per the canonical write-up in `rules-review` SKILL.md `§ 5. Review` (the "Detecting Agent availability" / "Fallback when Agent is unavailable" paragraphs). Auto-derive specialization: when falling back, walk the executor prompt inline-sequentially against each per-skill group in the main thread and emit the same A3 aggregate JSON defined below so callers' parse path is identical.

5. **Schema differences from explicit-args mode** (the response format adds two extensions over `## Workflow` § Step 3 (a)):

   - **`inferred_intent`**: a new top-level required field (string, 1–2 sentences). Its value is captured from iter 1 only — see `A2 § Per-iter loop semantics` (6.1) below.
   - **`suggested_edits` per-entry shape**: `{file, old_string, new_string, rationale}` — `file` is required and must equal one of the paths in `files`. This is the publicity-review / skill-review multi-file pattern (different from explicit-args mode's `{old_string, new_string, rationale}`, which targets the single `Target file`).

6. **Per-iter loop semantics** — reuse `## Workflow` § Step 3 (b) Parse & apply and § Step 3 (c) safety rails verbatim, with these auto-derive-specific overrides:

   - **(b) sub-case 2 schema violation** is extended: in addition to the existing checks, require non-empty string `inferred_intent` at the top level **and** non-empty string `file` on every `suggested_edits` entry. Per-entry shape failures here prevent a malformed entry from crashing a downstream `Edit` call.
   - **(b) sub-case 5 apply** edits the file named by `suggested_edits[i].file` (not the explicit-args `Target file`). **Before each `Edit`**, verify `suggested_edits[i].file ∈ F` (the per-skill `files` set); if not, record the path in an `out_of_scope` list and skip the entry without calling `Edit` (no working-tree write occurs, so no revert is needed for that entry — see (c) Scope rail). `old_string` mismatches on in-scope entries remain the no-op fallback. `applied_edits_count` increments only for `Edit` calls that succeeded; record the `file` path of each successfully-applied edit into the per-iter set `D` consumed by (c) Frontmatter rail.
   - **(c) Scope rail (per-skill, judgment annotation)**: each per-skill dispatch is an independent executor, and (b) sub-case 5's pre-check already prevents writes outside `F`. After the apply phase, if `out_of_scope` is non-empty, the executor attempted to write paths the dispatch payload did not authorize — emit the per-skill verdict with `status: "conflict"`, `reason: "scope violation"`, `reverted_paths: out_of_scope`. No `git checkout HEAD --` runs because no offending write actually landed (the pre-check skipped them); `reverted_paths` reports the rejected paths for caller-visibility. Other per-skill loops continue. This design avoids the multi-skill collateral-damage failure mode where a global `git checkout HEAD -- <sibling-path>` would wipe a sibling skill's already-landed edits.
   - **(c) Frontmatter rail**: applies per edited file in **this iter's apply phase** (the set `D` defined in (b) sub-case 5 — paths whose `Edit` call returned success this iter). For each such file with a `---`-delimited YAML frontmatter block, re-`Read` and parse it; on parse failure, run `git checkout HEAD -- <file>` and emit the per-skill verdict with `status: "conflict"`, `reason: "frontmatter broken"`, `reverted_paths=[<file>]`. Since `D ⊆ F` by construction (pre-check filtering), the `git checkout` only ever touches paths in this skill's `files` — no sibling-skill collateral damage. Files without frontmatter skip this rail (same as explicit-args mode).
   - **(6.1) `inferred_intent` persistence**: capture `inferred_intent` from the **iter 1 verdict** into main-thread context and treat that value as fixed for the rest of the per-skill loop. iter 2+ verdicts may return a different `inferred_intent` because the executor reruns Phase 1 each dispatch — do **not** overwrite the iter-1 value. Auditability requires the per-skill verdict to report a single, stable intent across the loop; iter-2+ drift would make the verdict misleading to a human reader.

     `inferred_intent` write rules at per-skill loop exit (closed list, covers every termination path so the field is unambiguous regardless of `status`):

     - iter 1 produced no parseable verdict (sub-case 1 / 2 / dispatch error) and the per-skill loop terminates immediately with `status=skipped` → write `inferred_intent: null` (there is no iter-1 value to fix).
     - All other termination paths — `converged` at any iter, `conflict` from (c) Scope rail or Frontmatter rail at any iter, `skipped (divergent gaps)` at iter ≥ 2, max-iter `unresolved` — write the iter-1 captured value into `inferred_intent` regardless of `status`. The captured value is fixed at iter 1 and survives subsequent iter outcomes.

     Divergence comparison (sub-case 4) uses the existing `(remaining_gaps, regressions)` multiset pair only; `inferred_intent` is excluded since the main thread fixes it.

7. **Per-skill verdict** — assembled in main-thread context at per-skill loop exit (no JSON is emitted yet; A3 aggregates and emits a single block at the end):

   ```text
   {
     "primary_file": "<path>",
     "files": ["<path>", ...],
     "inferred_intent": "<1-2 sentences>",     // iter-1 value, fixed per (6.1)
     "status": "converged|unresolved|skipped|conflict",
     "iterations_used": N,
     "objective_met": "yes|partial|no|unknown",
     "applied_edits_count": N,
     "unresolved_gaps": ["..."],
     "reverted_paths": ["..."],
     "reason": "..." or null
   }
   ```

   - Field semantics for `unresolved_gaps` / `reverted_paths` / `reason` mirror `## Workflow` § Step 5 — Emit structured summary (the explicit-args contract). Per-skill `status` is the 4-value enum (`converged|unresolved|skipped|conflict`); the new `partial` value lives only at the top level (see A3).
   - **Per-skill `skipped` paths**: (b) sub-case 1 verdict missing/malformed, (b) sub-case 2 schema violation, (b) sub-case 4 divergent gaps, and dispatch error (the `Agent` call itself errored / timed out / returned empty). These mirror the explicit-args mode's `skipped` paths. Because each skill is an independent executor, a `skipped` per-skill verdict alongside other `converged` per-skill verdicts is the typical case that triggers the top-level `partial` rule in A3.

### A3. Aggregate per-skill verdicts

After every per-skill loop has finished, emit a single fenced JSON block at the end of the invocation matching this schema (this replaces `## Workflow` § Step 5 — Emit structured summary for the auto-derive mode code path):

```json
{
  "mode": "auto-derive",
  "status": "converged|partial|unresolved|skipped|conflict",
  "iterations_used_total": 0,
  "applied_edits_count_total": 0,
  "non_skill_files": ["<path>", "..."],
  "per_skill": {
    "<skill-name>": { "...per-skill verdict object from `A2 § Per-skill verdict`..." }
  },
  "reason": null
}
```

`iterations_used_total` and `applied_edits_count_total` are sums across every per-skill verdict.

**Top-level `status` rule** (precedence, first-match-wins):

1. any per-skill `status == conflict` → `conflict`
2. else any per-skill `status == unresolved` → `unresolved`
3. else every per-skill `status == converged` → `converged`
4. else every per-skill `status == skipped` → `skipped`
5. else (a mix of `converged` and `skipped` only — neither `conflict` nor `unresolved`) → `partial`

`partial` is **auto-derive-only** and is never emitted by the explicit-args mode contract — existing callers wired to the explicit-args 4-value enum (e.g. `dev-workflow-triage`'s (d) verdict parser) will not see it.

**Top-level `reason` rule**:

- `status: "skipped"` from A1 early-return: enum string (`"empty diff"` or `"no skill files in diff"`).
- `status: "skipped"` from A3 aggregation (every per-skill is `skipped`): JSON `null` at the top level. Each skill's individual `reason` survives in its per-skill verdict object — callers should `inspect per_skill[<skill>].reason` for the underlying cause.
- All other top-level statuses (`converged` / `partial` / `unresolved` / `conflict`): JSON `null`.

This split prevents a same-status / different-`reason`-shape ambiguity for the `skipped` case (A1 carries an enum string; A3 carries `null` and pushes per-skill `reason` deeper into the verdict).

## Workflow

### Step 1 — Extract context

1. Parse the five fields from the invocation text. If `Target file` is missing or empty, return early:
   ```json
   {"status": "skipped", "reason": "missing target", "iterations_used": 0, "applied_edits_count": 0, "unresolved_gaps": [], "reverted_paths": [], "objective_met": "unknown"}
   ```
2. Run `git diff <Base ref> -- <Target file>`. This captures working-tree-vs-base; no staging is assumed.
3. If the diff is empty, return early with `status=conflict` and `reason="empty diff"`. An empty diff means the caller's Edit did not actually change the file, which is a bug signal the parent should surface as a conflict, not a warning.

### Step 2 — Derive evaluation scenarios (inline, no dispatch)

Generate 1–2 evaluation scenarios plus a requirements checklist from the caller's Finding framing before the executor loop begins. This runs in the skill's main thread — `Read` only, no Agent dispatch.

1. `Read` the first 100 lines of `Target file` to pick up the frontmatter, `description`, and opening intent prose.
2. Produce **at least one** "median" scenario — the typical use case the `Description` and `Suggested fix direction` imply. If those two fields are too thin to anchor a median, fall back to the target file's own frontmatter `description`: "does the diff achieve what this file declares as its purpose?" This fallback always yields a scenario.
3. Add **one** "edge" scenario only if the Finding text explicitly names a boundary condition, a failure mode, or an input class the median does not cover. Otherwise stop at one scenario — do not pad.
4. For each scenario, write a requirements checklist of 3–7 items. Frame each item as an **intent / behavior / property** the diff must achieve, not as a reference to specific text fragments or line numbers — edits may legitimately rewrite a referenced fragment mid-run and invalidate text-anchored checks. When the Finding inherently involves a specific fragment (e.g. a rename), frame the item around the resulting behavior (e.g. "no occurrences of the old name remain in the specified scope") rather than requiring an exact substring match. **At least one item must be tagged `[critical]`** (minimum success bar; prevents vacuous-pass verdicts).
5. Hold the scenarios and checklists in main-thread context for the duration of the run. **Do not regenerate mid-run** — cross-iteration reproducibility depends on fixing them once.

### Step 3 — Iteration loop (i = 1 .. Max iterations)

**Pre-register iteration tasks** — before entering the loop, `TaskCreate` one task per iteration named `iteration 1`, `iteration 2`, ..., `iteration <Max iterations>`. Mark `in_progress` (via `TaskUpdate`) before each dispatch, `completed` after parse+apply (for a `converged` verdict, "apply" is a no-op — mark `completed` immediately after parsing the verdict). On early convergence (verdict matches the converged rule) or safety-rail triggered exit (`skipped` / `conflict`), mark remaining iteration tasks `completed` with note matching the exit reason (e.g. `skipped: converged at iter 2`). The "note" lives in the task's `description` field (the `content` field under the `TodoWrite` fallback) — append as `— <reason>`. Where the Task tools are unavailable (e.g. the VSCode extension, or Claude Code before v2.1.142), use the equivalent `TodoWrite` operations instead — the status values and pre-register semantics are identical; `allowed-tools` grants both. Pre-registration is load-bearing: without it, the executor-driven loop tends to stop after the first iteration that looks acceptable, even when gaps remain that further iterations could close.

#### (a) Dispatch bias-free executor

At the start of every iteration, `Read` the full current contents of `Target file` so the snapshot reflects prior edits. On `i ≥ 2`, also re-run `git diff <Base ref> -- <Target file>` so the diff reflects edits that landed in prior iterations.

Invoke the `Agent` tool to dispatch a fresh executor. Assemble the dispatch prompt from the four sections below, each framed with a clear section label (e.g. `--- TARGET FILE ---`, `--- UNIFIED DIFF ---`, `--- SCENARIOS ---`, `--- EXECUTOR PROMPT ---`) so the executor can parse each payload unambiguously. Use the same order as the labels above:

- **TARGET FILE**: `Target file`'s full current contents (verbatim)
- **UNIFIED DIFF**: the unified diff
- **SCENARIOS**: the scenarios and requirements checklists from Step 2 — Derive evaluation scenarios (verbatim, identical across all iterations)
- **EXECUTOR PROMPT**: the executor prompt and JSON schema below (verbatim)

**Executor prompt (include verbatim in the executor prompt):**

> You are a fresh executor of the target file. You have **not** seen the original Finding framing — only the scenarios and checklist below. **Actually execute each scenario** against the target as written; do not merely read and judge. Produce scenario artifacts in your response body, then report unclear points, discretionary fills, and retries in natural language.
>
> Judge whether the diff resolves the problem the scenarios encode AND follows the scenarios' intent. Check for regressions — changes that break behavior the original file relied on. Return `objective_met: "yes"` only if there are **no remaining gaps AND no regressions**. Otherwise return `"partial"` (direction is right but gaps remain) or `"no"` (diff does not address the objective).
>
> **Gate reachability rule (required)**: when your final verdict is `objective_met: "yes"` AND `regressions: []`, you **must** return `suggested_edits: []`. Do not emit speculative or nice-to-have edits on a pass — record any such observations in `remaining_gaps` instead. `suggested_edits` is the convergence signal and must be empty when you are declaring the work done.
>
> If a scenario requires running a script or command, `cd` into a scratch/temp directory outside the skill tree under test **before** invoking it (or otherwise redirect its output there) — prefer the session scratchpad or the system temp directory. Never let output artifacts (e.g. captured stdout/stderr) land inside the directory being verified.

**Response format (include verbatim in the executor prompt):**

> Write your reasoning and scenario execution in natural language, then end your response with a single fenced JSON block matching this schema:
>
> ````
> ```json
> {
>   "objective_met": "yes|partial|no",
>   "remaining_gaps": ["<short phrase>"],
>   "regressions": ["<short phrase>"],
>   "suggested_edits": [
>     {"old_string": "<unique snippet>", "new_string": "<replacement>", "rationale": "<why>"}
>   ],
>   "confidence": "high|medium|low"
> }
> ```
> ````
>
> `old_string` must match exactly one location in the current file. Include **1–3 lines of surrounding context** so the snippet is unique — short one-liners collide and cause the Edit to fail.

#### (b) Parse & apply — evaluate in this order, first match wins

1. **Verdict missing or malformed** — no fenced JSON block found, or JSON parse fails → return `status=skipped`, `reason="verdict parse failure"`.
2. **Schema violation** — `objective_met` is not one of `yes|partial|no`, or required keys are missing → return `status=skipped`, `reason="verdict schema violation"`.
3. **Converged** — `objective_met == "yes"` AND `regressions` is empty → exit loop with `status=converged` and proceed directly to Step 5 — Emit structured summary. If `suggested_edits` is nonempty, discard the edits (do not apply them) — the gate-reachability rule told the executor to return `[]` on a pass verdict, so nonempty edits on a pass are treated as ignored observations consistent with that contract. Safety rails (c) do not run (no edit was applied this iteration).
4. **Divergence** — only when `i >= 2`: if both `remaining_gaps` AND `regressions` contain the same elements as the previous iteration's values (compare as multisets — sort each array textually before comparison so a reordered-but-identical report still counts as divergence), the loop is not making progress → return `status=skipped`, `reason="divergent gaps"`. (Skip on `i = 1`. Comparing the `(remaining_gaps, regressions)` pair catches an executor that reports regressions-only with empty `suggested_edits`, which would otherwise loop on empty-equal `remaining_gaps` alone.)
5. **Otherwise** — apply `suggested_edits` in order:
   - Re-Read the target file before each Edit so `old_string` matches current contents.
   - If an `old_string` is not found, skip that edit and continue with the next. This is expected when the executor returned multiple edits from a single snapshot and a later edit overlaps a region an earlier edit already rewrote — the skip is a no-op fallback, not an error.
   - After the edits (applied or skipped), run the safety rails in (c), then continue to iteration `i + 1`.

#### (c) Per-iteration safety rails — run only if at least one edit was applied

- **Frontmatter integrity** — Re-Read the file. If the file begins with a `---`-delimited YAML frontmatter block, parse it; if parsing fails:
  ```
  git checkout HEAD -- <Target file>
  ```
  Return `status=conflict`, `reason="frontmatter broken"`, `reverted_paths=[<Target file>]`. If the file has no frontmatter block at all (e.g., a plain source file), skip this rail — there is nothing to corrupt.
- **Scope** — Run `git diff --name-only`. If any returned path is not `<Target file>`:
  ```
  git checkout HEAD -- <each offending path>
  ```
  Return `status=conflict`, `reason="scope violation"`, `reverted_paths=[<each offending path>]`.

### Step 4 — Max iterations reached without convergence

Set `status=unresolved`, `unresolved_gaps = <last remaining_gaps>`. `applied_edits_count` reflects edits that actually landed (not skipped).

### Step 5 — Emit structured summary

Emit a single fenced JSON block at the end of the response, matching the schema for the mode that ran:

- **explicit-args mode**: emit the schema below (4-value `status` enum). Existing callers (e.g. `dev-workflow-triage`'s (d) verdict parser) consume this contract.
- **auto-derive mode**: emit the aggregate schema defined in `## Auto-derive mode` § A3 (5-value `status` enum including `partial`, plus the per-skill nesting).

The explicit-args mode schema:

```json
{
  "status": "converged|unresolved|skipped|conflict",
  "iterations_used": N,
  "objective_met": "yes|partial|no|unknown",
  "applied_edits_count": N,
  "unresolved_gaps": ["..."],
  "reverted_paths": ["..."],
  "reason": "verdict parse failure|verdict schema violation|divergent gaps|frontmatter broken|scope violation|missing target|incomplete args|empty diff|dispatch error|null"
}
```

The `|null` token at the end of the `reason` enum means JSON `null` (not the string `"null"`).

Field semantics by status:

- `reason`: JSON `null` for `converged` and `unresolved`; the matching enumerated string otherwise.
- `objective_met`: the last verdict's value for `converged` (always `"yes"`), `unresolved`, and `skipped (divergent gaps)`. `"unknown"` for all other skipped/conflict paths — this also covers the degenerate case where no verdict was ever received (e.g. `Max iterations ≤ 0`, or every dispatch failed).
- `unresolved_gaps`: the last verdict's `remaining_gaps` for `unresolved` and `skipped (divergent gaps)`; `[]` otherwise (including the no-verdict degenerate cases above).
- `applied_edits_count`: count of `suggested_edits` whose `Edit` call succeeded. Edits skipped because their `old_string` did not match do not count. Applies to all statuses.
- `iterations_used`: the number of iterations whose executor dispatch returned a verdict, whether or not edits landed — **including the iteration whose verdict triggered `converged`** (which applies no edits itself). Early returns from Step 1 (`missing target`, `empty diff`) and from `## Invocation contract` § Mode determination (`incomplete args`) count as `0`.

## Dispatch failure

If the `Agent` tool call itself errors, times out, or returns an empty response, return `status=skipped`, `reason="dispatch error"`. Do not re-read the file yourself as a fallback — self-review reintroduces the bias this skill exists to avoid.

## No structure-only mode

This skill does not have a static-review fallback. Callers that want best-practices checking (prose quality, naming, description rigor) should chain `Skill(skill-review)` after `verify-diff`; the two skills are complementary, not substitutes.

## Scope check boundary

`verify-diff` runs its scope check per iteration (inside (c)) to catch leaks the moment they appear. A caller running its own final scope check per Finding provides a last-resort backstop — two independent gates, different granularities. In auto-derive mode the scope check is per-skill — the baseline is the per-skill `files` set rather than a single `Target file` — see `## Auto-derive mode` `A2 § Per-iter loop semantics` (c).

## Sub-skill caller directive

When invoked as a sub-skill (i.e. via `Skill(verify-diff)` from an orchestrator), the fenced JSON verdict block this skill emits is the **structured return value** of the skill's procedure — it is **not** a deliverable to the user, and emitting it does **not** terminate the orchestrator's turn. The same agent that ran this skill must immediately issue the next tool call dictated by the orchestrator's flow (see `dev-workflow-triage` SKILL.md `§ No-Stall Principle`; orchestrators that surface a per-callee guidance bullet — e.g. `dev-workflow-triage`'s `**Pre-invocation reminder**` — name the specific next action there). Do not insert a prose summary, an acknowledgment, or a "shall I proceed?" sentence between the JSON verdict and the next tool call. The JSON verdict block and the next tool call MUST be emitted in the same assistant turn. Closing the turn after emitting the JSON block — even with no prose between them — is the same violation as inserting prose. Only one fenced JSON block — the verdict block — appears in the response, so callers can locate it unambiguously. The skill's own procedure is over; the orchestrator's procedure continues without pause.

## Stop hook structural conflict (caller-side note)

This skill operates on an uncommitted working tree throughout: main thread `Edit` applies executor-suggested edits, the Step 3 iteration loop dispatches further `Agent` executors, and the caller is the one that eventually decides whether to commit. On Claude Code on the Web the auto-installed `~/.claude/stop-hook-git-check.sh` fires on every Stop event between dispatches and feeds back `Please commit and push…`. **Treat each fire as a spurious fire** — record it, ignore the prose, and keep going until Step 5 emits the structured summary. Do **not** commit from inside this skill (and `allowed-tools` omits `git commit` so an attempt would fail anyway); commit policy lives with the caller (e.g. `dev-workflow-triage`'s per-Finding flow). See `dev-workflow-triage` SKILL.md `§ Stop hook structural conflict` for the canonical write-up.

In auto-derive mode the per-skill loop dispatches an `Agent` for each skill (and again per iteration), so the multiplier on hook-fire count is `N skills × iter` rather than the single dispatch loop of explicit-args mode. Same disposition (record and continue), but the higher fire count is normal for this mode.

## Outer review loop interaction (caller-side note)

When invoked from `dev-workflow-triage`'s § Apply accepted Findings (D) per-Finding review subagent's outer loop at outer iter ≥ 2, the cumulative diff against `Base ref = HEAD` may include sibling-review polish edits (from `Skill(skill-review)` / `Skill(publicity-review)`) unrelated to the original Finding's `Description` / `Suggested fix direction`. The bias-free executor may judge those polish lines as "regression" and emit `unresolved` more frequently than it would in iter 1 — this is the expected disposition under outer-loop wrapping, not a `verify-diff` bug.

Caller-side handling: under the (D) design `verify-diff` is invoked via `Skill(verify-diff)` from inside the dispatched per-Finding subagent, not directly from the orchestrator main thread. The subagent treats `unresolved` as a non-fatal warning and proceeds to skill-review; the outer loop's early-exit may absorb the noise once all three callees stop applying edits. The rationale for `Base ref = HEAD` across outer iters: no commit happens until (g), so each callee sees the cumulative working-tree state including prior outer iter's edits. See `dev-workflow-triage` SKILL.md `§ Apply accepted Findings` (D) for the canonical (D) write-up.

## Related

- `prompt-tuning` — iterative empirical evaluation of a whole prompt against multi-scenario requirement checklists. Shares the anti-self-review philosophy (dispatch a fresh executor; never self-review) but operates at prompt-quality granularity, while `verify-diff` operates on a single diff with a single objective. `verify-diff` automates prompt-tuning's human-in-the-loop by having the executor emit `suggested_edits` from its unclear-points report. The auto-derive mode reuses the same intent-inference pattern from prompt-tuning and re-specializes it for diff-verification granularity.
