{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "lumpConfig.schema.json",
  "title": "Lump JSON Configuration",
  "description": "JSON configuration schema for a Lump — a long-running automated coding campaign managed by Lumpcode. This schema defines the JSON-serializable subset of the full Lump configuration (LumpJsonConfig). Function-valued fields from the JS config are either excluded entirely or replaced by file paths pointing to modules that export the corresponding function.",
  "type": "object",
  "additionalProperties": true,
  "allOf": [
    {
      "oneOf": [
        { "required": ["contextListJson"], "not": { "anyOf": [{ "required": ["getContextListFn"] }, { "required": ["contextMatchFn"] }] } },
        { "required": ["getContextListFn"], "not": { "anyOf": [{ "required": ["contextListJson"] }, { "required": ["contextMatchFn"] }] } },
        { "required": ["contextMatchFn"], "not": { "anyOf": [{ "required": ["contextListJson"] }, { "required": ["getContextListFn"] }] } }
      ],
      "description": "Exactly one context source must be provided: 'contextListJson' (a static json or path to a json file), 'getContextListFn' (a file path to a dynamic context-fetching function), or 'contextMatchFn' (a file path to a MatchFn that builds contexts by scanning codebase files)."
    },
    {
      "oneOf": [
        { "required": ["prompt"], "not": { "required": ["steps"] } },
        { "required": ["steps"], "not": { "required": ["prompt"] } }
      ],
      "description": "Exactly one prompt definition must be provided: either 'prompt' (a single prompt item) or 'steps' (an ordered list of prompt steps)."
    },
    {
      "not": {
        "allOf": [
          { "required": ["postSetupWorkspaceFn"] },
          { "required": ["postSetupWorkspaceCommand"] }
        ]
      },
      "description": "postSetupWorkspaceFn and postSetupWorkspaceCommand are mutually exclusive."
    },
    {
      "not": {
        "allOf": [
          { "required": ["postTeardownWorkspaceFn"] },
          { "required": ["postTeardownWorkspaceCommand"] }
        ]
      },
      "description": "postTeardownWorkspaceFn and postTeardownWorkspaceCommand are mutually exclusive."
    }
  ],
  "properties": {
    "baseBranch": {
      "type": "string",
      "description": "Git integration line for execution: pre-flight, markers, finished checks, and worktrees. Must be an exact branch name (not a glob). When omitted, defaults to the concrete effective discovery branch. In JS/TS configs, may also be a BaseBranchFn or FilePath that receives { effectiveDiscoveryBranch, contexts } (pre-status raw list) and returns an exact branch string.",
      "examples": ["main", "develop", "ver/0.0.9"]
    },
    "discoveryBranch": {
      "type": "string",
      "description": "Singular discovery rule: exact branch or git ls-remote glob (e.g. feature/*). Mutually exclusive with discoveryBranches. Optional; omit both to use the exact primary from local.json. Dedicated: each rule must be allowlisted against configured (unexpanded) primaryBranches. Pattern-only rules require --discoveryBranch <concrete> for manual run/plan/status. Ignored for scheduling in shared mode.",
      "examples": ["main", "ver/0.0.9", "feature/*"]
    },
    "discoveryBranches": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "string",
        "minLength": 1
      },
      "description": "Discovery rules (exact and/or git globs). Mutually exclusive with discoveryBranch. Lump is eligible on a concrete scan/flag branch when any rule matches. Flagless CLI uses the first exact rule; pattern-only requires --discoveryBranch.",
      "examples": [["dev", "feature/*"], ["main"]]
    },
    "branchFn": {
      "type": "string",
      "description": "File path to a module that default-exports a BranchFn — a function that determines the branch name for a given set of contexts. In the JS config this can be the function itself; in JSON it must be a file path to the module.",
      "examples": ["./lump/branchFn.ts", "./lump/branchFn.js"]
    },
    "command": {
      "type": "string",
      "description": "Default command for all prompt items (inherited when a step omits 'command'). Either a registered command tag (e.g. 'copilot', 'cursor', 'claude-code', 'opencode', 'codex') resolved via '.lumpcode/commands/<name>.ts|.js', global commands, then presets — or a lump-relative file path ending in '.ts' or '.js' with no whitespace that exports a CommandModule ('command', optional 'setup'/'teardown'). Missing file-path modules fail at config load. Agent flags belong in the module's CommandFn, not in this string.",
      "examples": ["copilot", "cursor", "claude-code", "opencode", "codex", "./agents/custom.ts"]
    },
    "contextListJson": {
      "oneOf": [
        {
          "type": "string",
          "description": "File path to a JSON file containing a { [variableName]: pathTemplate } mapping."
        },
        {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "Inline mapping from variable name to path template. Each value is a path template with {PLACEHOLDER} captures (e.g. 'src/{NAME}.ts'); each key is the variable name exposed to the prompt as {KEY} / @{KEY}."
        }
      ],
      "description": "Static context source. The engine scans the tree and, for every path that matches a template, builds a context whose variables map keys (e.g. 'FILE') to the matched path. Path templates support {PLACEHOLDER} captures and $modifier{PLACEHOLDER} (e.g. $upperFirst{NAME}) for naming-convention checks. Prefer paths without a leading './'. Mutually exclusive with getContextListFn / contextMatchFn.",
      "examples": [
        "./contextList.json",
        {
            "FILE": "src/{NAME}.ts"
        },
        {
            "FOLDER": "src/components/{COMPONENT_NAME}/",
            "INDEX": "src/components/{COMPONENT_NAME}/index.ts",
            "TYPES": "src/components/{COMPONENT_NAME}/{COMPONENT_NAME}.types.ts",
            "COMPONENT": "src/components/{COMPONENT_NAME}/$upperFirst{COMPONENT_NAME}.tsx"
        }
      ]
    },
    "contextMatchFn": {
      "type": "string",
      "description": "File path to a module exporting a MatchFn — a function that builds contexts by scanning codebase files. For each codeBasePath, the function returns an object { contextName, filePathVariableName, moreContextVariables?, contextOptions? } or null if the file doesn't match. Multiple matches with the same contextName merge into one context (variables accumulate; later matches override duplicate keys). This is an alternative to 'contextListJson' and 'getContextListFn' for dynamically grouping files into contexts based on custom matching logic.",
      "examples": ["./lump/contextMatchFn.ts"]
    },
    "contextOptionsFn": {
      "type": "string",
      "description": "File path to a module exporting a function that provides additional options or configuration for each context. In the JS config this can be the function itself; in JSON it must be a file path.",
      "examples": ["./lump/contextOptionsFn.ts"]
    },
    "disabled": {
      "oneOf": [
        {
          "type": "boolean",
          "description": "When true, the background daemon skips this lump; manual 'lumpcode run' reports a skipped run."
        },
        {
          "type": "string",
          "description": "File path to a module whose default export is a zero-argument function returning boolean (or Promise<boolean>). Evaluated on each run/tick to decide whether the lump is disabled."
        }
      ],
      "description": "Disable this lump without removing its configuration. Either a boolean, or a file path to a module default-exporting a zero-argument function returning boolean (sync or async) evaluated on each run/tick. In config.js/config.ts the function can also be inline.",
      "default": false,
      "examples": [true, "./isDisabled.ts"]
    },
    "getContextListFn": {
      "type": "string",
      "description": "File path to a module exporting a GetContextListFn — a function that dynamically retrieves the list of contexts to process. In the JS config this can be the function itself; in JSON it must be a file path. Use contextListJson instead if your context list is static.",
      "examples": ["./lump/getContextListFn.ts"]
    },
    "maximumNumberOfConcurrentBranches": {
      "type": "integer",
      "minimum": 1,
      "description": "Maximum number of context branches that can be processed concurrently. Limits parallelism to avoid overwhelming the system or hitting rate limits."
    },
    "numberOfContextsPerBranch": {
      "type": "integer",
      "minimum": 1,
      "description": "Number of contexts to group into a single branch. Defaults to 1, meaning each context gets its own branch.",
      "default": 1
    },
    "lumpVariables": {
      "type": "object",
      "additionalProperties": true,
      "description": "An arbitrary key-value object of variables passed through to prompt functions, setup/teardown hooks, and other lifecycle functions. Use this to parameterize your lump without changing code.",
      "examples": [
        { "language": "TypeScript", "style": "functional" }
      ]
    },
    "verbose": {
      "type": "boolean",
      "description": "When true, enables verbose logging output during lump execution for debugging purposes.",
      "default": false
    },
    "setupFn": {
      "type": "string",
      "description": "File path to a module exporting a SetupFn — a per-context lifecycle hook that runs once before the prompt loop for each context. Its returned { contextRunState } seeds the mutable per-context state bag. In the JS config this can be the function itself; in JSON it must be a file path.",
      "examples": ["./lump/setupFn.ts"]
    },
    "teardownFn": {
      "type": "string",
      "description": "File path to a module exporting a TeardownFn — a per-context lifecycle hook that runs after all prompt items for a context, receiving the accumulated contextRunState. In the JS config this can be the function itself; in JSON it must be a file path.",
      "examples": ["./lump/teardownFn.ts"]
    },
    "postSetupWorkspaceFn": {
      "type": "string",
      "description": "File path to a module exporting a PostSetupWorkspaceFn. Runs after generated checkout/worktree setup, with the branch workspace already prepared. Return { command } for a shell fragment executed in that workspace. Mutually exclusive with postSetupWorkspaceCommand. Not invoked by lump-plan. Do not put git mutations here (not covered by gitCommonDirLock).",
      "examples": ["./postSetupWorkspace.ts"]
    },
    "postSetupWorkspaceCommand": {
      "type": "string",
      "description": "Shell fragment run in the branch workspace after generated git setup (for example npm ci). Mutually exclusive with postSetupWorkspaceFn. Not run by lump-plan.",
      "examples": ["npm ci", "npm i"]
    },
    "postTeardownWorkspaceFn": {
      "type": "string",
      "description": "File path to a module exporting a PostTeardownWorkspaceFn. Runs before generated workspace teardown, while the branch workspace still exists. Return { command } for a shell fragment executed in that workspace. Mutually exclusive with postTeardownWorkspaceCommand. Not invoked by lump-plan.",
      "examples": ["./postTeardownWorkspace.ts"]
    },
    "postTeardownWorkspaceCommand": {
      "type": "string",
      "description": "Shell fragment run in the branch workspace before generated git teardown. Mutually exclusive with postTeardownWorkspaceFn. Not run by lump-plan.",
      "examples": ["rm -rf node_modules/.cache"]
    },
    "keepHistory": {
      "type": "boolean",
      "description": "When true, append one YAML entry per prompt step (prompt text, agent output, context, step index) to '.lumpcode/lumps/<lumpName>/history/<contextName>.yaml'. History files are gitignored by project-setup; intended for local debugging and inspection.",
      "default": false
    },
    "registerCommands": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "An array of command names to pre-register before processing prompt items. Required when prompt items are defined dynamically (via functions) and reference commands by name, since dynamic prompt items cannot trigger command registration at build time. Each name is resolved to a command module file (e.g. 'cursor-agent' resolves to 'commands/cursor-agent.js').",
      "examples": [["claude-code"], ["cursor-agent", "aider"]]
    },
    "prompt": {
      "$ref": "#/$defs/LumpJsonConfigStep",
      "description": "A single prompt item defining the prompt template or function, the command to run, and optional lifecycle hooks. Use this shorthand for simple lumps that only require one prompt step. For multi-step lumps, use 'steps' instead."
    },
    "steps": {
      "type": "array",
      "items": {
        "oneOf": [
          {
            "$ref": "#/$defs/LumpJsonConfigStep"
          },
          {
            "type": "string",
            "description": "Shorthand for 'promptTemplate': inline text with {VAR} substitution, or a lump-relative template file when the entire string has no whitespace and ends with '.md', '.txt', '.template', or '.prompt' (file must exist under '.lumpcode/lumps/<lumpName>/')."
          }
        ]
      },
      "description": "An ordered list of prompt items executed sequentially for each context. Each element can be a full LumpJsonConfigStep object or a string shorthand (same rules as 'promptTemplate' file vs inline text)."
    }
  },
  "$defs": {
    "LumpJsonConfigStep": {
      "type": ["object", "string"],
      "additionalProperties": false,
      "description": "A single step in a lump's execution pipeline. Defines an optional prompt, which command to execute, and optional post-processing. Prompt fields are optional; when omitted the command receives an empty prompt string. The top-level 'command' is used when this step does not set 'command'.",
      "oneOf": [
        { "type": "string", "description": "Shorthand for promptTemplate (inline text or lump-relative template file — see promptTemplate)." },
        {
          "type": "object",
          "not": { "required": ["promptTemplate", "promptFn"] }
        }
      ],
      "properties": {
        "promptTemplate": {
          "type": "string",
          "description": "Prompt template text for this step. Inline string with {VAR} and @{VAR} substitution, OR a lump-relative file path when the entire value has no whitespace and ends with '.md', '.txt', '.template', or '.prompt' (resolved from '.lumpcode/lumps/<lumpName>/'; read once at config load; missing file fails). Mutually exclusive with 'promptFn'. When omitted, the command receives an empty prompt string.",
          "examples": [
            "Refactor the following file to use modern syntax",
            "Tighten the prop types in @{COMPONENT} and update @{TEST}.",
            "./prompts/refactor.md"
          ]
        },
        "promptFn": {
          "type": "string",
          "description": "File path to a module exporting a PromptFn — a function that dynamically generates prompt text at runtime. The function signature is:\n  (params: { context, stepIndex, contextRunState, lumpVariables, stepVariables? }) => string | Promise<string>\nUse this instead of 'promptTemplate' when the prompt needs to vary based on context, run state, or other dynamic data.",
          "examples": ["./lump/promptFn.ts", "./lump/promptFn.js"]
        },
        "command": {
          "type": "string",
          "description": "Command tag (e.g. 'copilot', 'cursor', 'claude-code', 'opencode', 'codex') resolved via '.lumpcode/commands/<name>.ts|.js', global commands, then presets — OR a lump-relative '.ts' or '.js' file path with no whitespace exporting a CommandModule. Inherits top-level 'command' when omitted. Agent flags belong in the module's CommandFn, not here.",
          "examples": ["copilot", "cursor", "claude-code", "opencode", "codex", "./agents/custom.ts"]
        },
        "postCommandExecFn": {
          "type": "string",
          "description": "File path to a module exporting a PostCommandExecFn — a function called after the command finishes executing. The function receives:\n  { commandResult, commandSucceeded, context, prompt, stepIndex, contextRunState, lumpVariables, stepVariables?, projectRoot }\nUse this for validation, logging, cleanup, or other side effects after each prompt/command cycle.",
          "examples": ["./lump/postCommandExecFn.ts"]
        },
        "stepVariables": {
          "type": "object",
          "additionalProperties": true,
          "description": "An arbitrary key-value object of variables specific to this prompt item. Passed to the promptFn alongside the global lumpVariables, enabling per-step parameterization without modifying the top-level lumpVariables.",
          "examples": [
            { "maxRetries": 3, "style": "concise" }
          ]
        },
        "timeoutMillis": {
          "type": "integer",
          "minimum": 1,
          "description": "Maximum time in milliseconds to wait for the agent command to complete. On expiry, Lumpcode terminates the command process tree (SIGTERM, then SIGKILL after a grace period) and the step fails. Defaults to 30 minutes (1800000) when omitted.",
          "examples": [60000, 300000]
        },
        "continueOnError": {
          "type": "boolean",
          "description": "When true, a failing agent command (non-zero exit) on this step does not abort the context: 'postCommandExecFn' still runs (with commandSucceeded: false) and execution continues with the next step. Defaults to false — the context stops on the first failing step.",
          "default": false
        }
      }
    }
  }
}
