{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "llmlint configuration",
  "description": "Configuration for llmlint, an LLM-as-judge linter for code-quality checks deterministic linters can't express. Docs: https://github.com/nickderobertis/llmlint",
  "type": "object",
  "properties": {
    "version": {
      "description": "The config's published version (`1`, `1.1`, or `1.1.1`). Set this when\nthe config is consumed as a plugin: a consumer pins a desired version\nwith an `@` suffix on the plugin URL, validated against this value.",
      "anyOf": [
        {
          "$ref": "#/$defs/Version"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "prompt_template": {
      "description": "Master minijinja prompt template, rendered with `rules` (each with name +\ndescription) and `files` (the target paths). Overrides the built-in one.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "files": {
      "description": "Default include/exclude globs selecting target files when none are passed\non the CLI.",
      "$ref": "#/$defs/FileFilter",
      "default": {
        "include": [],
        "exclude": []
      }
    },
    "oneharness": {
      "description": "Defaults for how llmlint invokes the oneharness subprocess.",
      "$ref": "#/$defs/OneharnessCfg",
      "default": {
        "config": [],
        "bin": null,
        "model": null,
        "timeout": null,
        "schema_max_retries": null
      }
    },
    "rationales": {
      "description": "Whether judges must justify each verdict with a short `rationale`\n(default `true`). Rationales aid auditability, debugging, and reliability\n(the judge reasons before concluding) but cost extra output tokens on\nevery request. A per-rule `rationale` overrides this default. The\n`--rationales`/`--no-rationales` CLI flags override the config.",
      "type": [
        "boolean",
        "null"
      ]
    },
    "diff_base": {
      "description": "Default base the `--diff` git backend compares target files against when\n`--diff-base` is not passed. Any git revision — a branch, tag, commit, or\n`A..B`/`A...B` range — e.g. `main` to make a quality gate review whatever\nthe current branch changed versus the default branch. Unset keeps the\nbuilt-in `HEAD` (working-tree) base; the `--diff-base` flag overrides it.",
      "type": [
        "string",
        "null"
      ]
    },
    "plugins": {
      "description": "Plugins (shared rule sets) merged in, one entry each: a local file path\nor a URL (`http(s)://`, `file://`), the URL optionally pinned with an\n`@version` suffix. Named `plugins` (not `include`) to avoid confusion\nwith `files.include`. Resolution lives in [`crate::io::plugins`].",
      "type": "array",
      "items": {
        "type": "string"
      },
      "default": []
    },
    "agents": {
      "description": "Named agents that group rules and share harness/model/batch config. A\nrule with no `agent` uses the `default` agent.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/Agent"
      },
      "default": {}
    },
    "rules": {
      "description": "The lint rules. Each is a positive invariant judged true/false about the\ntarget files (holds=true passes; holds=false is a violation).",
      "type": "array",
      "items": {
        "$ref": "#/$defs/Rule"
      },
      "default": []
    }
  },
  "$defs": {
    "Version": {
      "type": [
        "integer",
        "number",
        "string"
      ]
    },
    "FileFilter": {
      "description": "Include/exclude glob set used to select target files.",
      "type": "object",
      "properties": {
        "include": {
          "description": "Globs selecting files to lint.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": []
        },
        "exclude": {
          "description": "Globs subtracted from the included set.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": []
        }
      },
      "additionalProperties": false
    },
    "OneharnessCfg": {
      "description": "Passthrough/defaults for how llmlint invokes `oneharness`.",
      "type": "object",
      "properties": {
        "config": {
          "description": "oneharness config file(s) to forward via `--config` (single-file today;\nextras are warned and dropped).",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": []
        },
        "bin": {
          "description": "Override the oneharness binary path.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "model": {
          "description": "Default model for every judge (an agent's `model` overrides it).",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "timeout": {
          "description": "Per-judge timeout in seconds (default 120).",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "minimum": 1,
          "default": null
        },
        "schema_max_retries": {
          "description": "Schema-validation re-prompt budget passed to oneharness `--schema-max-retries`.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint32",
          "minimum": 0,
          "default": null
        }
      },
      "additionalProperties": false
    },
    "Agent": {
      "description": "A group of rules sharing reviewer context and harness/model/batch config.",
      "type": "object",
      "properties": {
        "harness": {
          "description": "Harness id from `oneharness list`. When unset, llmlint omits `--harness`\nand oneharness selects its own configured default harness.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "model": {
          "description": "Model override for this agent's judges.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "batch_size": {
          "description": "Max rules per judge run (default 20).",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 1,
          "default": null
        },
        "prompt_template": {
          "description": "Extra prompt text appended to the master template before rendering.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        }
      },
      "additionalProperties": false
    },
    "Rule": {
      "description": "A single lint rule: a statement judged true/false about the target files.",
      "type": "object",
      "properties": {
        "name": {
          "description": "Terse snake_case identifier: an ASCII letter followed by letters, digits,\nor underscores. Used as a JSON Schema key for the judge's verdict.",
          "type": "string",
          "pattern": "^[A-Za-z][A-Za-z0-9_]*$"
        },
        "description": {
          "description": "The invariant the judge evaluates. State clearly what is true (passes)\nand what is false (a violation). Required for a normal rule (an empty one\nis rejected); an `override` rule may omit it to inherit the base rule's\ntext, so the schema leaves it optional.",
          "type": "string",
          "minLength": 1,
          "default": ""
        },
        "override": {
          "description": "Override a same-named rule contributed by a plugin: inherit all of the\nbase rule's fields, replacing only the ones set here. Without this, a\nduplicate rule name is an error. Set it on the consuming (nearer-root)\nconfig; the override is resolved into the base when the config loads.",
          "type": "boolean"
        },
        "agent": {
          "description": "Name of the agent (under `agents`) this rule runs on. Defaults to the\n`default` agent.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "judges": {
          "description": "Independent judges to run; the majority verdict wins (default 1). Must be\nodd so the vote can't tie.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint32",
          "minimum": 1,
          "default": null
        },
        "files": {
          "description": "Override the target files for this rule.",
          "anyOf": [
            {
              "$ref": "#/$defs/FileFilter"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "rationale": {
          "description": "Whether the judge must justify this rule's verdict with a `rationale`.\nOverrides the session-wide `rationales` default for this one rule; unset\ninherits it.",
          "type": [
            "boolean",
            "null"
          ],
          "default": null
        },
        "relevance": {
          "description": "When this rule should be evaluated. `true` (the default) always\nevaluates; `false` never does (the rule is reported not relevant without\na judge call); a string is a condition the judge decides about the change\nfirst, reporting the rule \"not relevant\" when it does not hold. Lets a\nrule scope itself to applicable changes instead of every `description`\nneeding its own \"or not applicable\" escape hatch.",
          "anyOf": [
            {
              "$ref": "#/$defs/Relevance"
            },
            {
              "type": "null"
            }
          ]
        },
        "require_line_attribution": {
          "description": "Whether every violation of this rule must cite a concrete `file` and\n`line`. Off by default — some findings (e.g. a cross-cutting\narchitectural drift) genuinely can't be pinned to one source line, so a\nviolation may omit its location. Set `true` for a rule whose violations\nmust always be localizable: the generated schema then marks each\nviolation's `file`/`line` **required**, so oneharness re-prompts the judge\nto localize *every* violation in one batched turn (no per-violation back\nand forth), and the default template asks for it up front. A violation\nthat still arrives without a file+line is a hard error rather than a\nsilently-imprecise report. Inherited/overridable like the other per-rule\nfields.",
          "type": [
            "boolean",
            "null"
          ]
        }
      },
      "additionalProperties": false,
      "required": [
        "name"
      ]
    },
    "Relevance": {
      "description": "When a rule should be evaluated. Mirrors the `description`/verdict split: a\nboolean is resolved deterministically by llmlint, a string is a\nnatural-language condition the judge decides about the change first.",
      "anyOf": [
        {
          "description": "`true` (the default): always evaluate — the judge may not opt out.\n`false`: never evaluate — the rule is statically not applicable and is\nreported as not relevant without calling a judge.",
          "type": "boolean"
        },
        {
          "description": "A natural-language condition describing when the rule applies. The judge\ndecides whether it holds for the change *before* evaluating the verdict,\nand reports the rule \"not relevant\" (with no verdict) when it does not.",
          "type": "string"
        }
      ]
    }
  },
  "$id": "https://raw.githubusercontent.com/nickderobertis/llmlint/main/assets/llmlint.schema.json"
}
