{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/triage-output.schema.json",
  "version": "3.3.0",
  "title": "Multi-Agent Pipeline  -  Phase 4 triage output",
  "description": "Contract for the Opus triage agent's JSON output in Phase 4 Step 3. Triage consumes merged reviewer findings and splits them into accepted/deferred/rejected. Only `accepted` blocking/important items trigger Phase 3 rework. v3.1.0 adds the optional `consensus` block so triage can surface reviewer-agreement risk (false consensus among same-base-model reviewers) instead of silently merging. v3.2.0 adds the optional per-finding `verification` block written by Phase 4 Step 3.7 (verify-by-test): the empirical repro-test outcome for accepted blocking findings. v3.3.0 carries ruleId + criteriaSource through triage so a finding that cites a stable rule ID keeps that citation into Phase 6 and Phase 7, and the lesson loop can key durable learnings by rule.",
  "type": "object",
  "additionalProperties": false,
  "required": ["accepted", "deferred", "rejected", "approved"],
  "properties": {
    "accepted": {
      "type": "array",
      "description": "Findings that are real, in-scope, and must be actioned in this iteration.",
      "items": {
        "$ref": "#/$defs/acceptedFinding"
      }
    },
    "deferred": {
      "type": "array",
      "description": "Findings that are real but out of current task scope. Surfaced in Phase 7 report; not actioned.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["finding", "reason"],
        "properties": {
          "finding": {
            "$ref": "#/$defs/rawFinding"
          },
          "reason": {
            "type": "string",
            "minLength": 4,
            "description": "Why deferred  -  e.g. 'out of scope: touches payment module, task is auth-only'."
          }
        }
      }
    },
    "rejected": {
      "type": "array",
      "description": "Findings that are false-positives, duplicates, style-only nitpicks, or already correct. Not actioned; kept for audit.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["finding", "reason"],
        "properties": {
          "finding": {
            "$ref": "#/$defs/rawFinding"
          },
          "reason": {
            "type": "string",
            "minLength": 4,
            "description": "Why rejected  -  e.g. 'false positive: the null check the reviewer wants already happens in the caller'."
          }
        }
      }
    },
    "approved": {
      "type": "boolean",
      "description": "True if no accepted BLOCKING items remain. The pipeline uses this to gate Phase 5 / Phase 6 entry."
    },
    "consensus": {
      "$ref": "#/$defs/consensus"
    }
  },
  "if": {
    "properties": {
      "accepted": {
        "contains": {
          "type": "object",
          "properties": {
            "severity": {
              "const": "blocking"
            }
          },
          "required": ["severity"]
        }
      }
    }
  },
  "then": {
    "properties": {
      "approved": {
        "const": false
      }
    }
  },
  "$defs": {
    "severity": {
      "type": "string",
      "enum": ["blocking", "important", "suggestion"]
    },
    "reviewer": {
      "type": "string",
      "enum": ["fable", "opus", "sonnet", "gpt"],
      "description": "Which reviewer produced the raw finding. Claude Code Reviewer 1 is fable (opus when fallback engages); Copilot CLI adds gpt. Haiku was removed in v2.1.0."
    },
    "consensus": {
      "type": "object",
      "additionalProperties": false,
      "description": "Optional v3.1.0 reviewer-agreement metadata. Unanimous agreement among reviewers that share a base model is NOT proof on judgment calls (correlated failure); triage marks those `unverified` so the user can confirm rather than the pipeline treating agreement as a pass.",
      "required": ["reviewerCount", "verdict"],
      "properties": {
        "reviewerCount": {
          "type": "integer",
          "minimum": 1,
          "description": "Reviewers dispatched this iteration (Claude Code = 2, Copilot CLI = 3, Codex CLI = 3)."
        },
        "verdict": {
          "type": "string",
          "enum": ["unanimous-pass", "unanimous-block", "split", "unverified"],
          "description": "unanimous-pass/block = clear-cut agreement; split = reviewers disagreed and triage resolved it; unverified = reviewers agreed on a judgment call but the agreement may be correlated, so it is surfaced to the user rather than trusted."
        },
        "disagreements": {
          "type": "array",
          "description": "Findings where reviewers split (existence or severity). Surfaced verbatim in the Phase 7 report and at the Step 4 user checkpoint.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["file", "issue", "note"],
            "properties": {
              "file": {
                "type": "string",
                "minLength": 1
              },
              "line": {
                "type": "integer",
                "minimum": 0
              },
              "issue": {
                "type": "string",
                "minLength": 4
              },
              "note": {
                "type": "string",
                "minLength": 4,
                "description": "How the reviewers split  -  e.g. 'Opus blocking, Sonnet approved'."
              }
            }
          }
        }
      }
    },
    "verification": {
      "type": "object",
      "additionalProperties": false,
      "description": "v3.2.0 verify-by-test outcome (Phase 4 Step 3.7, opt-in via prefs.global.verifyByTest). confirmed = repro test failed as the finding predicts (finding stands, test kept as the Phase 3 RED test); not-reproduced = repro test passed under evidence-gate (finding downgraded to deferred); inconclusive = compile error / timeout / not unit-testable (judgment verdict stands).",
      "required": ["result"],
      "properties": {
        "result": {
          "type": "string",
          "enum": ["confirmed", "not-reproduced", "inconclusive"]
        },
        "testRef": {
          "type": "string",
          "minLength": 1,
          "description": "Single-test reference, e.g. 'AuthTests/LoginTests/testExpiredTokenRejected' or 'tests/test_auth.py::test_expired_token'."
        },
        "evidencePath": {
          "type": "string",
          "minLength": 1,
          "description": "Path to the test-run log verified by evidence-gate.mjs, e.g. '.pipeline/verify-1.test.log'."
        },
        "note": {
          "type": "string"
        }
      },
      "if": {
        "properties": {
          "result": {
            "enum": ["confirmed", "not-reproduced"]
          }
        }
      },
      "then": {
        "required": ["result", "testRef", "evidencePath"]
      }
    },
    "rawFinding": {
      "type": "object",
      "additionalProperties": false,
      "required": ["severity", "file", "line", "issue"],
      "properties": {
        "severity": {
          "$ref": "#/$defs/severity"
        },
        "file": {
          "type": "string",
          "minLength": 1
        },
        "line": {
          "type": "integer",
          "minimum": 0
        },
        "issue": {
          "type": "string",
          "minLength": 4
        },
        "fix": {
          "type": "string"
        },
        "reviewer": {
          "$ref": "#/$defs/reviewer"
        },
        "verification": {
          "$ref": "#/$defs/verification"
        },
        "ruleId": {
          "type": "string",
          "minLength": 1,
          "description": "Stable ID of the cited rule (e.g. SEC-01), preserved from the reviewer output. A finding citing a registry rule carries evidence an opinion does not, so triage must not reject it as a matter of taste  -  it can still be deferred or rejected as out of scope for THIS task."
        },
        "criteriaSource": {
          "type": "string",
          "minLength": 1,
          "description": "Which criteria source produced the rule: a registry name, a module-guide path, or a deterministic gate such as 'exception-marker-audit'."
        }
      }
    },
    "acceptedFinding": {
      "allOf": [
        {
          "$ref": "#/$defs/rawFinding"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["severity", "file", "line", "issue", "reviewer", "fix"],
          "properties": {
            "severity": {
              "$ref": "#/$defs/severity"
            },
            "file": {
              "type": "string",
              "minLength": 1
            },
            "line": {
              "type": "integer",
              "minimum": 0
            },
            "issue": {
              "type": "string",
              "minLength": 4
            },
            "reviewer": {
              "$ref": "#/$defs/reviewer"
            },
            "fix": {
              "type": "string",
              "minLength": 4,
              "description": "Concrete change the dev agent must make. Required for accepted items so Phase 3 re-entry has actionable direction."
            },
            "verification": {
              "$ref": "#/$defs/verification"
            },
            "ruleId": {
              "type": "string",
              "minLength": 1,
              "description": "Stable ID of the cited rule (e.g. SEC-01), preserved from the reviewer output. A finding citing a registry rule carries evidence an opinion does not, so triage must not reject it as a matter of taste  -  it can still be deferred or rejected as out of scope for THIS task."
            },
            "criteriaSource": {
              "type": "string",
              "minLength": 1,
              "description": "Which criteria source produced the rule: a registry name, a module-guide path, or a deterministic gate such as 'exception-marker-audit'."
            }
          }
        }
      ]
    }
  }
}
