{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Cauce engine configuration",
  "type": "object",
  "required": [
    "project",
    "mode",
    "workspaceRoots",
    "runner"
  ],
  "properties": {
    "$schema": {
      "type": "string",
      "minLength": 1
    },
    "cauceVersion": {
      "type": "string",
      "minLength": 1
    },
    "project": {
      "type": "string",
      "minLength": 1
    },
    "mode": {
      "enum": [
        "embedded",
        "sidecar",
        "toolkit"
      ]
    },
    "workspaceRoots": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "name",
          "path"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "path": {
            "type": "string",
            "minLength": 1
          },
          "verify": {
            "type": "string",
            "description": "Comando que corre la puerta de esta raíz —pruebas, lint, typecheck, build—, tal como se invoca desde ella. Sin él, quien verifica tiene que descubrirlo leyendo el repositorio en cada tarea, y eso es trabajo de modelo repetido para siempre sobre una respuesta que no cambia."
          },
          "scope": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "Qué rutas lee la puerta de esta raíz, relativas a ella, con * ? y **. Sirve para que un archivo sucio que el gate no va a abrir no fuerce la copia del índice. Sin él, cualquier diferencia entre árbol e índice la fuerza, que es el comportamiento de siempre."
          }
        },
        "additionalProperties": false
      }
    },
    "comments": {
      "type": "object",
      "description": "Activa la pasada de comentarios de R11: antes de un commit, el guard comments lista los comentarios que agrega y lo frena una vez; entra repitiéndolo con CAUCE_COMMENTS_REVIEWED=<token>. `{}` activa sólo el listado. Sin esta clave el guard no corre.",
      "properties": {
        "language": {
          "enum": ["es", "en"],
          "description": "El idioma de los comentarios. Un comentario agregado en el otro frena el commit."
        },
        "inlineMax": {
          "type": "integer",
          "minimum": 1,
          "description": "Cuántas líneas puede tener un comentario dentro de una unidad. Uno más largo frena el commit; lo que encabeza una unidad o el archivo no tiene tope."
        }
      },
      "additionalProperties": false
    },
    "deployCommands": {
      "type": "array",
      "description": "Los comandos con que este proyecto despliega —\"make deploy\", \"./scripts/release.sh\"—, tal como se escriben al principio de un comando. El guard destructive los frena como frena terraform apply o kubectl apply, y se aprueban igual que un push: una persona, en el diálogo de Claude Code, nombrándolos en el chat, o en .ops-approval.",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "timeZone": {
      "type": "string",
      "minLength": 1,
      "description": "El huso con que el proyecto fecha lo que escribe —la entrada de done/, un reclamo, un vencimiento—, en forma IANA: \"America/Bogota\". Sin declararlo se fecha en UTC, y lo que se cierra a la noche al oeste de Greenwich queda con la fecha del día siguiente. Es del proyecto y no de la máquina, para que todo el equipo y el CI fechen igual."
    },
    "boundedCommands": {
      "type": "array",
      "description": "Los comandos con que este proyecto corre algo dentro de un entorno con tope de recursos —\"scripts/run-in-container.sh\", \"docker compose exec\"—, tal como se escriben al principio de un comando. El guard de workers no opina sobre un jest o un vitest lanzado con uno de ellos: ahí el runner no puede saturar la máquina, que es lo único que ese guard cuida. El programa se compara por su nombre, así que vale llamarlo con cualquier ruta.",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "writableOutsideRoots": {
      "type": "array",
      "description": "Rutas que el proyecto declara escribibles sin ser raíces de código —la memoria del runner, un scratchpad, un directorio de salida—. Sólo levantan el límite de workspace-boundary: no entran a scan ni al inventario de credenciales, que recorren workspaceRoots. `~` se expande a la casa del usuario y el resto se resuelve contra la raíz de ops. `check` las muestra resueltas en cada corrida, porque una exención que no se ve es un límite que ya no existe.",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "runner": {
      "type": "object",
      "required": [
        "maxTaskHours",
        "humanCheckpointBetweenMilestones",
        "commitPerTask",
        "allowPush"
      ],
      "properties": {
        "maxTaskHours": {
          "type": "number",
          "exclusiveMinimum": 0
        },
        "humanCheckpointBetweenMilestones": {
          "type": "boolean"
        },
        "commitPerTask": {
          "type": "boolean"
        },
        "allowPush": {
          "type": "boolean"
        },
        "commitToLiveBranch": {
          "type": "boolean",
          "description": "Si el recorrido puede commitear en una rama viva —main, master o la rama por defecto del remoto—. Apagado o ausente, corta una rama para la tarea y commitea ahí."
        },
        "allowAiSignature": {
          "type": "boolean",
          "description": "Si lo que el proyecto publica puede llevar una firma de IA —un Co-Authored-By de un asistente, «Generated with…»—. Apagado o ausente, el guard frena el commit o el texto de PR que la traiga, que es lo que pide R8. Lo enciende una persona cuando la empresa quiere dejar constancia de lo que hizo un agente."
        },
        "gateTimeoutMinutes": {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "Cuánto puede durar cada gate de verify antes de cortarlo, con todo lo que lanzó. Por defecto 10."
        },
        "pushToLiveBranches": {
          "type": "array",
          "description": "Las ramas vivas —main, master y la rama por defecto de cada remoto— en las que se puede publicar. Sin nombrarla acá, ni allowPush ni una orden en el chat llegan a una rama viva. Nombres exactos, sin patrones.",
          "items": {
            "type": "string",
            "pattern": "^[^\\s*?\\[]+$"
          },
          "uniqueItems": true
        }
      },
      "additionalProperties": false
    },
    "migrations": {
      "type": "object",
      "description": "Qué cuenta como migración para el guard `migrations`. Sin esto sólo juzga `.sql`, así que en un proyecto TypeORM, Prisma, Django, Rails o Alembic el guard no mira nada.",
      "additionalProperties": false,
      "properties": {
        "extensions": {
          "type": "array",
          "description": "Extensiones, sin el punto y en minúscula. Por defecto [\"sql\"].",
          "items": {
            "type": "string",
            "pattern": "^[a-z0-9]+$"
          },
          "minItems": 1
        },
        "paths": {
          "type": "array",
          "description": "Carpetas donde viven las migraciones, relativas y sin «.» ni «..»: el guard las reconoce en cualquier lugar de la ruta, así que \"alembic/versions\" también alcanza a \"api/alembic/versions\". Declararlas reemplaza el default [\"migrations\", \"migration\", \"migrate\"], y `check` avisa la que no alcanza a ningún archivo.",
          "items": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$"
          },
          "minItems": 1
        }
      }
    },
    "inbox": {
      "type": "object",
      "description": "El aviso de tamaño del INBOX. `check` advierte —nunca falla— cuando `planning/INBOX.md` pasa de este número de líneas: el INBOX es de la persona, y lo que el aviso pide es recorrerlo, no dejar de escribir.",
      "additionalProperties": false,
      "properties": {
        "warnLines": {
          "type": "integer",
          "minimum": 1,
          "description": "Líneas a partir de las cuales `check` avisa. Por defecto 300."
        }
      }
    },
    "lines": {
      "type": "object",
      "description": "Las líneas de trabajo y los repositorios que trabaja cada una, nombrados desde la carpeta de sesión. Cada repositorio de una línea tiene su worktree en `lines/<línea>/<repo>/`, en la rama `line/<línea>`. Lo escribe `ops line`; sin líneas, la instancia trabaja como siempre.",
      "propertyNames": {
        "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
      },
      "additionalProperties": {
        "type": "array",
        "minItems": 1,
        "uniqueItems": true,
        "items": {
          "type": "string",
          "minLength": 1
        }
      }
    },
    "guards": {
      "type": "object",
      "description": "Los guards propios del proyecto, por evento, con rutas relativas a la raíz ops. El motor los corre después de los suyos en cualquier runner —Claude, Codex, Gemini y Antigravity—, así que no se registran también en la configuración de cada uno: correrían dos veces, y `automation doctor` lo avisa. Cada guard recibe por stdin el JSON de la llamada, y `OPS_ROOT` con la raíz ops; sale en 0 para dejar pasar, y lo que escriba en stderr le llega a la sesión como aviso, y en 2 para frenar con lo que escribe en stderr. Cualquier otra salida, que no esté o que no se pueda ejecutar también frena, y lo mismo si los de un evento tardan más de 20 s entre todos.",
      "properties": {
        "pre-shell": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "pre-files": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "pre-read": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "pre-mcp": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "stop": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "additionalProperties": false
    }
  },
  "additionalProperties": false
}
