{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://airlock.lunareng.com/schema/autonomy.schema.json",
  "title": "AUTONOMY.yaml",
  "description": "Per-repository autonomy policy: repos/<repo>.yaml in the policy repo, mirrored as AUTONOMY.yaml in the repository. Levels: L1 human leads, L2 AI collaborates, L3 AI leads, L4 self-directed. There is no L0.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "repo",
    "default",
    "actions"
  ],
  "properties": {
    "schema": {
      "type": "integer",
      "const": 1
    },
    "repo": {
      "type": "string",
      "pattern": "^[A-Za-z0-9-]+/[a-z0-9.-]+$"
    },
    "default": {
      "$ref": "#/$defs/level",
      "description": "Level for any path or action not matched below."
    },
    "actions": {
      "type": "object",
      "description": "Level per action class. Missing classes take `default`. The effective level of a change is the strictest of every action class and path it touches.",
      "additionalProperties": false,
      "properties": {
        "read": {
          "$ref": "#/$defs/level"
        },
        "write_code": {
          "$ref": "#/$defs/level"
        },
        "merge": {
          "$ref": "#/$defs/level"
        },
        "deploy": {
          "$ref": "#/$defs/level"
        },
        "send": {
          "$ref": "#/$defs/level",
          "description": "Email, SMS, push, publish to a public channel."
        },
        "spend": {
          "$ref": "#/$defs/level",
          "description": "Any paid service or one-off cost over `budget.spend_usd`."
        },
        "delete_data": {
          "$ref": "#/$defs/level"
        },
        "touch_ehi": {
          "$ref": "#/$defs/level"
        },
        "change_policy": {
          "$ref": "#/$defs/level",
          "description": "Edits to this file. Loosening is always at least L2; tightening auto-merges (see `policy.tighten`)."
        }
      }
    },
    "paths": {
      "type": "array",
      "description": "Carve-outs by path. A path can only set a level stricter than `default`.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "glob",
          "level"
        ],
        "properties": {
          "glob": {
            "type": "string"
          },
          "level": {
            "$ref": "#/$defs/level"
          },
          "why": {
            "type": "string"
          }
        }
      }
    },
    "policy": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "tighten": {
          "type": "string",
          "enum": [
            "auto_merge",
            "review"
          ],
          "default": "auto_merge",
          "description": "What happens to a PR that only makes levels stricter. Decided 2026-10-19: auto_merge once CI proves every change is a tightening."
        },
        "loosen": {
          "type": "string",
          "enum": [
            "review"
          ],
          "default": "review",
          "description": "A PR that loosens any level always waits for the human owner. Not configurable."
        }
      }
    },
    "budget": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "prs_in_flight": {
          "type": "integer",
          "minimum": 1,
          "default": 2
        },
        "prs_per_week": {
          "type": "integer",
          "minimum": 1,
          "default": 5
        },
        "spend_usd": {
          "type": "number",
          "minimum": 0,
          "default": 100
        },
        "tokens_per_week": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "judge": {
      "type": "object",
      "description": "Required for L4. A number the agent cannot write to. CI computes it; the agent reads it.",
      "additionalProperties": false,
      "required": [
        "metric",
        "source",
        "pass"
      ],
      "properties": {
        "metric": {
          "type": "string"
        },
        "source": {
          "type": "string",
          "description": "Where the number comes from: a CI job, a sim report, a telemetry query."
        },
        "pass": {
          "type": "string",
          "description": "Threshold expression, e.g. '>= 0.99' or '<= 1000ms'."
        },
        "check": {
          "description": "Required for L4 (scripts/validate.py). One check, or a list when the judge has several sources; it ran only if every one of them ran.",
          "oneOf": [
            {
              "$ref": "#/$defs/judge_check"
            },
            {
              "type": "array",
              "minItems": 1,
              "items": {
                "$ref": "#/$defs/judge_check"
              }
            }
          ]
        }
      }
    },
    "goals": {
      "type": "string",
      "description": "Path to the goals file an L4 agent may take direction from. Required for L4."
    },
    "signals": {
      "type": "array",
      "description": "Extra signal sources beyond the charter defaults (canary, tool-request issues, failing tests, logged friction, usage data).",
      "items": {
        "type": "string"
      }
    },
    "digest": {
      "type": "string",
      "enum": [
        "friday",
        "monthly",
        "none"
      ],
      "description": "Where merged changes are reported. L3 defaults to friday, L4 to monthly."
    },
    "ratchet": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "promoted_on": {
          "type": "string",
          "format": "date"
        },
        "demoted_on": {
          "type": "string",
          "format": "date"
        },
        "evidence": {
          "type": "string",
          "description": "Link to the catch log or incident that justified the last change."
        }
      }
    },
    "owner_role": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*$",
      "description": "The agent role that works this repo: the router starts it with a tool-request filed here as the signal (2026-10-21). For a repo created by proposal (charter, 2026-10-20), the role that proposed it."
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "default": {
            "const": "L4"
          }
        }
      },
      "then": {
        "required": [
          "judge",
          "goals"
        ]
      }
    }
  ],
  "$defs": {
    "level": {
      "type": "string",
      "enum": [
        "L1",
        "L2",
        "L3",
        "L4"
      ]
    },
    "judge_check": {
      "type": "object",
      "description": "Where the ratchet looks for the judge's last run. A CI judge names its workflow file; a report judge names the label of the issue its report is commented on. The ratchet only reads when it last ran, never its number.",
      "additionalProperties": false,
      "required": [
        "repo",
        "max_age_days"
      ],
      "oneOf": [
        {
          "required": [
            "workflow",
            "branch"
          ],
          "not": {
            "required": [
              "issue_label"
            ]
          }
        },
        {
          "required": [
            "issue_label"
          ],
          "not": {
            "anyOf": [
              {
                "required": [
                  "workflow"
                ]
              },
              {
                "required": [
                  "branch"
                ]
              }
            ]
          }
        }
      ],
      "properties": {
        "repo": {
          "type": "string",
          "pattern": "^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$"
        },
        "workflow": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._-]+\\.ya?ml$",
          "description": "A CI judge: the workflow file in the repo's .github/workflows. It ran if a run on `branch` completed (success or failure; skipped and cancelled don't count) within max_age_days. A file that doesn't exist yet has never run."
        },
        "branch": {
          "type": "string",
          "enum": [
            "main"
          ]
        },
        "issue_label": {
          "type": "string",
          "description": "A report judge: the label of the open issue the report is commented on (the sims' daily `sim-coverage` comment). It ran if the account that opened that issue commented within max_age_days."
        },
        "max_age_days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 7,
          "description": "Older than this, the judge has not run: one `judge-missing` catch. At 14 days the ratchet's tightening PR drops the repo or role from L4 to L3 (decided 2026-10-25)."
        }
      }
    }
  }
}
