{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "little-loops://fsm-loop-schema.json",
  "title": "FSM Loop Definition",
  "description": "Schema for little-loops FSM loop definitions (.loops/*.yaml)",
  "type": "object",
  "required": ["name", "initial", "states"],
  "properties": {
    "name": {
      "type": "string",
      "description": "Unique loop identifier"
    },
    "initial": {
      "type": "string",
      "description": "Starting state name (must exist in states)"
    },
    "states": {
      "type": "object",
      "description": "State definitions",
      "additionalProperties": {
        "$ref": "#/definitions/stateConfig"
      },
      "minProperties": 1
    },
    "context": {
      "type": "object",
      "description": "User-defined shared variables",
      "additionalProperties": true
    },
    "scope": {
      "type": "array",
      "description": "Paths this loop operates on (for concurrency control)",
      "items": {
        "type": "string"
      }
    },
    "max_iterations": {
      "type": "integer",
      "description": "Safety limit for loop iterations",
      "default": 50,
      "minimum": 1
    },
    "backoff": {
      "type": "number",
      "description": "Seconds between iterations",
      "minimum": 0
    },
    "timeout": {
      "type": "integer",
      "description": "Max total runtime in seconds (loop-level)",
      "minimum": 1
    },
    "maintain": {
      "type": "boolean",
      "description": "If true, restart after completion",
      "default": false
    },
    "llm": {
      "$ref": "#/definitions/llmConfig"
    },
    "input_key": {
      "type": "string",
      "description": "Context key populated by the positional input arg at runtime (default: 'input')",
      "default": "input"
    },
    "on_handoff": {
      "type": "string",
      "description": "Behavior when handoff signal detected",
      "enum": ["pause", "spawn", "terminate"],
      "default": "pause"
    },
    "default_timeout": {
      "type": "integer",
      "description": "Default timeout for individual states in seconds",
      "minimum": 1
    },
    "config": {
      "type": "object",
      "description": "Per-loop ll-config overrides (applied for the duration of this loop's execution)",
      "properties": {
        "handoff_threshold": {
          "type": "integer",
          "description": "Override for LL_HANDOFF_THRESHOLD (context monitor handoff percentage)",
          "minimum": 1,
          "maximum": 100
        },
        "commands": {
          "type": "object",
          "description": "Override for commands config",
          "properties": {
            "confidence_gate": {
              "type": "object",
              "description": "Override for confidence gate thresholds",
              "properties": {
                "readiness_threshold": {
                  "type": "integer",
                  "description": "Override for readiness score gate threshold (1-100)",
                  "minimum": 1,
                  "maximum": 100
                },
                "outcome_threshold": {
                  "type": "integer",
                  "description": "Override for outcome confidence gate threshold (1-100)",
                  "minimum": 1,
                  "maximum": 100
                }
              },
              "additionalProperties": false
            }
          },
          "additionalProperties": false
        },
        "automation": {
          "type": "object",
          "description": "Override for automation config",
          "properties": {
            "max_continuations": {
              "type": "integer",
              "description": "Override for maximum number of session continuations",
              "minimum": 1
            }
          },
          "additionalProperties": false
        },
        "continuation": {
          "type": "object",
          "description": "Override for continuation config",
          "properties": {
            "max_continuations": {
              "type": "integer",
              "description": "Override for maximum number of session continuations",
              "minimum": 1
            }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    }
  },
  "additionalProperties": false,
  "definitions": {
    "stateConfig": {
      "type": "object",
      "description": "Configuration for a single FSM state",
      "properties": {
        "action": {
          "type": "string",
          "description": "Command to execute (shell or slash command)"
        },
        "action_type": {
          "type": "string",
          "description": "How to execute the action: prompt (Claude CLI), slash_command (Claude CLI), shell (bash), or mcp_tool (direct MCP tool call). If omitted, uses heuristic: / prefix = slash_command, else = shell.",
          "enum": ["prompt", "slash_command", "shell", "mcp_tool"]
        },
        "params": {
          "type": "object",
          "description": "MCP tool arguments (only used with action_type: mcp_tool). Supports ${variable} interpolation in string values.",
          "additionalProperties": true
        },
        "evaluate": {
          "$ref": "#/definitions/evaluateConfig"
        },
        "route": {
          "type": "object",
          "description": "Full routing table (verdict -> state mapping)",
          "additionalProperties": {
            "type": "string"
          }
        },
        "on_yes": {
          "type": "string",
          "description": "Shorthand for yes verdict routing"
        },
        "on_no": {
          "type": "string",
          "description": "Shorthand for no verdict routing"
        },
        "on_success": {
          "type": "string",
          "description": "Deprecated: use on_yes instead"
        },
        "on_failure": {
          "type": "string",
          "description": "Deprecated: use on_no instead"
        },
        "on_error": {
          "type": "string",
          "description": "Shorthand for error verdict routing"
        },
        "next": {
          "type": "string",
          "description": "Unconditional transition (no evaluation)"
        },
        "terminal": {
          "type": "boolean",
          "description": "If true, this is an end state",
          "default": false
        },
        "capture": {
          "type": "string",
          "description": "Variable name to store action output"
        },
        "timeout": {
          "type": "integer",
          "description": "Action-level timeout in seconds",
          "minimum": 1
        },
        "on_maintain": {
          "type": "string",
          "description": "State to transition to when maintain=true and loop completes"
        },
        "on_partial": {
          "type": "string",
          "description": "Shorthand for partial verdict routing"
        },
        "on_blocked": {
          "type": "string",
          "description": "Shorthand for blocked verdict routing"
        },
        "max_retries": {
          "type": "integer",
          "minimum": 1,
          "description": "Max consecutive re-entries before transitioning to on_retry_exhausted. A value of N allows N retries after the initial execution (N+1 total entries). Requires on_retry_exhausted."
        },
        "on_retry_exhausted": {
          "type": "string",
          "description": "State to transition to when max_retries consecutive re-entries are exceeded. Required when max_retries is set."
        },
        "loop": {
          "type": "string",
          "description": "Name of a loop YAML to execute as a sub-FSM. Mutually exclusive with action."
        },
        "context_passthrough": {
          "type": "boolean",
          "default": false,
          "description": "Pass parent context variables to child loop and merge child captures back into parent context."
        }
      },
      "additionalProperties": false
    },
    "evaluateConfig": {
      "type": "object",
      "description": "Evaluator configuration for action result interpretation",
      "required": ["type"],
      "properties": {
        "type": {
          "type": "string",
          "description": "Evaluator type",
          "enum": [
            "exit_code",
            "output_numeric",
            "output_json",
            "output_contains",
            "convergence",
            "diff_stall",
            "llm_structured",
            "mcp_result"
          ]
        },
        "operator": {
          "type": "string",
          "description": "Comparison operator",
          "enum": ["eq", "ne", "lt", "le", "gt", "ge"]
        },
        "target": {
          "description": "Target value for comparison",
          "oneOf": [
            { "type": "integer" },
            { "type": "number" },
            { "type": "string" }
          ]
        },
        "tolerance": {
          "type": "number",
          "description": "Acceptable distance from target (for convergence)",
          "minimum": 0
        },
        "pattern": {
          "type": "string",
          "description": "Pattern string for output_contains"
        },
        "negate": {
          "type": "boolean",
          "description": "If true, invert the match result (output_contains)",
          "default": false
        },
        "path": {
          "type": "string",
          "description": "JSON path for output_json (jq-style)"
        },
        "prompt": {
          "type": "string",
          "description": "Custom prompt for llm_structured"
        },
        "schema": {
          "type": "object",
          "description": "Custom JSON schema for llm_structured response"
        },
        "min_confidence": {
          "type": "number",
          "description": "Minimum confidence threshold for llm_structured",
          "default": 0.5,
          "minimum": 0,
          "maximum": 1
        },
        "uncertain_suffix": {
          "type": "boolean",
          "description": "If true, append _uncertain to low-confidence verdicts",
          "default": false
        },
        "source": {
          "type": "string",
          "description": "Override default source (current action output)"
        },
        "previous": {
          "type": "string",
          "description": "Previous value reference for convergence"
        },
        "direction": {
          "type": "string",
          "description": "Optimization direction for convergence",
          "enum": ["minimize", "maximize"],
          "default": "minimize"
        }
      },
      "additionalProperties": false,
      "allOf": [
        {
          "if": {
            "properties": { "type": { "const": "output_numeric" } }
          },
          "then": {
            "required": ["operator", "target"]
          }
        },
        {
          "if": {
            "properties": { "type": { "const": "output_json" } }
          },
          "then": {
            "required": ["path", "operator", "target"]
          }
        },
        {
          "if": {
            "properties": { "type": { "const": "output_contains" } }
          },
          "then": {
            "required": ["pattern"]
          }
        },
        {
          "if": {
            "properties": { "type": { "const": "convergence" } }
          },
          "then": {
            "required": ["target"]
          }
        }
      ]
    },
    "llmConfig": {
      "type": "object",
      "description": "LLM evaluation configuration",
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "If false, disable LLM evaluation entirely",
          "default": true
        },
        "model": {
          "type": "string",
          "description": "Model identifier for LLM calls (default: DEFAULT_LLM_MODEL from schema.py)",
          "default": "claude-sonnet-4-20250514"
        },
        "max_tokens": {
          "type": "integer",
          "description": "Maximum tokens for evaluation response",
          "default": 256,
          "minimum": 1
        },
        "timeout": {
          "type": "integer",
          "description": "Timeout for LLM calls in seconds",
          "default": 30,
          "minimum": 1
        }
      },
      "additionalProperties": false
    }
  }
}
