{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://github.com/gobing-ai/spur/schemas/spur-config.schema.json",
    "title": "Spur Project Configuration",
    "description": "Schema for .spur/config.yaml \u2014 the single project configuration surface consumed by spur and (future) spur-server.",
    "type": "object",
    "properties": {
        "$schema": {
            "type": "string",
            "description": "Reference to this schema for IDE validation. spur resolves this at runtime via loadStructuredConfig."
        },
        "version": {
            "type": "string",
            "description": "Config schema version (string, not integer). Current recommended: '1.2' (config-1.2 layered global/project merge, ADR-033 executor tiers + planning blocks). '1' remains accepted for backward compatibility."
        },
        "name": {
            "type": "string",
            "description": "Project name written by spur init."
        },
        "bootstrap": {
            "type": "object",
            "description": "Portable bootstrap block consumed by @gobing-ai/ts-infra runNodeApplication. Shared across spur and spur-server.",
            "properties": {
                "logging": {
                    "type": "object",
                    "properties": {
                        "enabled": {
                            "type": "boolean",
                            "description": "Enable structured logging via LogTape."
                        },
                        "level": {
                            "type": "string",
                            "enum": ["trace", "debug", "info", "warn", "error", "fatal"],
                            "description": "Minimum log level."
                        },
                        "console": {
                            "type": "boolean",
                            "description": "Emit log records to console (stderr)."
                        },
                        "json": {
                            "type": "boolean",
                            "description": "Format log records as JSON Lines. false = text format."
                        },
                        "file": {
                            "type": "boolean",
                            "description": "Write log records to a file sink. Uses filePath when set, otherwise disabled."
                        },
                        "filePath": {
                            "type": "string",
                            "description": "Path to the log file. Relative to project root. Default: .spur/logs/spur.log."
                        }
                    }
                },
                "telemetry": {
                    "type": "object",
                    "properties": {
                        "enabled": {
                            "type": "boolean",
                            "description": "Enable OpenTelemetry tracing/metrics export."
                        },
                        "serviceName": {
                            "type": "string",
                            "description": "Service name reported to OTel collectors."
                        },
                        "environment": {
                            "type": "string",
                            "description": "Deployment environment label."
                        }
                    }
                },
                "database": {
                    "type": "object",
                    "properties": {
                        "enabled": {
                            "type": "boolean",
                            "description": "Enable the SQLite database adapter."
                        },
                        "driver": {
                            "type": "string",
                            "enum": ["bun-sqlite"],
                            "description": "Database driver. Currently only bun-sqlite."
                        },
                        "url": {
                            "type": "string",
                            "description": "Database file path. Supports ${DATABASE_URL} interpolation. Default: .spur/spur.db."
                        }
                    }
                },
                "scheduler": {
                    "type": "object",
                    "properties": {
                        "enabled": {
                            "type": "boolean",
                            "description": "Enable the scheduled-task runner. OFF by default for CLI (run-once)."
                        },
                        "jobs": {
                            "type": "array",
                            "description": "Declarative scheduled commands (task 0734). Each entry registers one scheduler tick that enqueues a `scheduler.custom` queue job running `command` through `/bin/sh -c`. Normalized and validated by @gobing-ai/ts-infra runNodeApplication before the server starts.",
                            "items": {
                                "type": "object",
                                "required": ["name", "command"],
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "minLength": 1,
                                        "description": "Job name, unique across the list after trimming. Reported as the `scheduler.custom:<name>` display name on scheduler.job.executed."
                                    },
                                    "command": {
                                        "type": "string",
                                        "minLength": 1,
                                        "description": "Shell command run by `/bin/sh -c` with the project root as cwd. Trusted operator input \u2014 never logged."
                                    },
                                    "intervalMinutes": {
                                        "type": "integer",
                                        "minimum": 1,
                                        "maximum": 35791,
                                        "description": "Fixed interval in minutes. Mutually exclusive with `cron`. The upper bound is the Node timer ceiling (2147483647 ms)."
                                    },
                                    "cron": {
                                        "type": "string",
                                        "minLength": 1,
                                        "description": "Five-field cron expression (minute hour day-of-month month day-of-week) evaluated in local time. Mutually exclusive with `intervalMinutes`."
                                    }
                                },
                                "oneOf": [
                                    { "required": ["intervalMinutes"], "not": { "required": ["cron"] } },
                                    { "required": ["cron"], "not": { "required": ["intervalMinutes"] } }
                                ]
                            }
                        }
                    }
                }
            }
        },
        "agent": {
            "type": "object",
            "properties": {
                "default": {
                    "type": "string",
                    "description": "Default role for `--agent auto` when nothing is declared (0542 R2). A role id (scribe, coder, reviewer, planner) selects its tier's cheapest eligible executor; a configured executor name still resolves during the transition with a one-time warning; any other value is rejected naming both accepted sets."
                },
                "executors": {
                    "type": "array",
                    "description": "Named executor profiles: each pairs a canonical agent with an optional model override. Referenced by name from `agent.default` and stage model_policy (default-by-phase removed 0452).",
                    "items": {
                        "type": "object",
                        "required": ["name", "agent"],
                        "properties": {
                            "name": {
                                "type": "string",
                                "minLength": 1,
                                "description": "Executor selector name (unique across the list)."
                            },
                            "agent": {
                                "type": "string",
                                "minLength": 1,
                                "description": "Canonical coding agent (claude, codex, gemini, pi, opencode, antigravity-cli, openclaw, hermes, omp)."
                            },
                            "model": {
                                "type": "string",
                                "minLength": 1,
                                "description": "Opaque model override passed straight to the agent (e.g. zai//glm-5.2). Applied only when the user passes no explicit --model."
                            },
                            "tier": {
                                "type": "string",
                                "enum": ["cheap", "standard", "capable-1", "capable-2", "capable-3"],
                                "description": "Capability tier for stage-registry adaptive model routing (ADR-033, 0343). Live values: cheap | standard | capable-1 | capable-2 | capable-3 (1=low output quality, 3=high within the capable band). A stage starts on the cheapest eligible executor meeting its model_policy min_tier. Bare legacy `capable` is accepted at runtime (zod preprocess \u2192 capable-1) during the deprecation window but is not part of this editor enum."
                            },
                            "disabled": {
                                "description": "Routing kill-switch (111; ownership widened 0890): a disabled profile never serves a role, team, stage, or explicit selection, and doctor inventories it without probing. Omitted = enabled. Bare `true` is operator-owned (only humans write booleans); automatic writers emit the object form carrying owner (operator|quota|probe), since (RFC 3339) and reason.",
                                "anyOf": [
                                    { "type": "boolean" },
                                    {
                                        "type": "object",
                                        "additionalProperties": false,
                                        "required": ["owner", "since", "reason"],
                                        "properties": {
                                            "owner": { "enum": ["operator", "quota", "probe"] },
                                            "since": { "type": "string", "format": "date-time", "minLength": 1 },
                                            "reason": { "type": "string", "minLength": 1 }
                                        }
                                    }
                                ]
                            }
                        }
                    }
                },
                "fleet": {
                    "type": "object",
                    "description": "The project's agent fleet (G65 / 0858): the single declaration of composition, the on/off switch, and the dispatch strategy. PROJECT LAYER ONLY — a global-layer agent.fleet fails the load. Replaces the retired .spur/fleet.json (0835) and agent.team (0857). Mirrors @gobing-ai/spur-config AgentFleetSchema (Zod is SSOT).",
                    "properties": {
                        "enabled": {
                            "type": "boolean",
                            "default": false,
                            "description": "The single fleet switch. `spur serve` materializes and autostarts the roster only when true; an absent or disabled fleet starts nothing."
                        },
                        "strategy": {
                            "type": "string",
                            "enum": ["rest", "gtd"],
                            "default": "rest",
                            "description": "Dispatch strategy, reconciled into project_strategy at serve start (0859). One name is added by a typed code change, not by config."
                        },
                        "orchestrator": {
                            "type": "string",
                            "minLength": 1,
                            "description": "memberLocalId of the one planner-role member carrying purpose: orchestrator. Absent = no orchestrator declared (never inferred)."
                        },
                        "members": {
                            "type": "array",
                            "default": [],
                            "description": "The roster. Each member declares a role or an executor. `enabled: false` keeps the member's derived <role>-<n> id index but does not materialize it.",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "id": {
                                        "type": "string",
                                        "minLength": 1,
                                        "description": "Explicit stable id; wins over the derived <role>-<n> form."
                                    },
                                    "role": {
                                        "type": "string",
                                        "enum": ["scribe", "coder", "reviewer", "planner"],
                                        "description": "Layer-1 role id (closed vocabulary)."
                                    },
                                    "executor": {
                                        "type": "string",
                                        "minLength": 1,
                                        "description": "Executor profile name or a raw canonical agent type."
                                    },
                                    "purpose": {
                                        "type": "string",
                                        "description": "Free-form member purpose; `orchestrator` is the value the orchestrator pointer binds to."
                                    },
                                    "enabled": {
                                        "type": "boolean",
                                        "description": "Default true. False preserves the member's derived id index without materializing it."
                                    }
                                },
                                "anyOf": [{ "required": ["role"] }, { "required": ["executor"] }]
                            }
                        }
                    }
                },
                "roles": {
                    "type": "object",
                    "description": "Per-role tier/stage overrides over DEFAULT_AGENT_ROLES (0572 / ADR-061). Keys are the closed role vocabulary (scribe, coder, reviewer, planner); a present field replaces the default for that role, an omitted field keeps it. Mirrors @gobing-ai/spur-config AgentRoleConfigSchema (Zod is SSOT).",
                    "additionalProperties": {
                        "type": "object",
                        "properties": {
                            "tier": {
                                "type": "string",
                                "enum": ["cheap", "standard", "capable-1", "capable-2", "capable-3"],
                                "description": "Replacement capability tier for the role. Must not sit below the highest min_tier among the role's folded stages (enforced at the CLI boundary)."
                            },
                            "stages": {
                                "type": "array",
                                "minItems": 1,
                                "items": { "type": "string", "minLength": 1 },
                                "description": "Replacement folded-stage list for the role. Ids must come from the canonical stage registry (validated at the CLI boundary). Omit the field to keep the default — an empty array is rejected."
                            }
                        }
                    }
                }
            }
        },
        "rules": {
            "type": "object",
            "properties": {
                "paths": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "description": "Glob paths to rule files."
                }
            }
        },
        "workflows": {
            "type": "object",
            "properties": {
                "paths": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "description": "Glob paths to workflow directories."
                }
            }
        },
        "redaction": {
            "type": "object",
            "properties": {
                "enabled": {
                    "type": "boolean",
                    "description": "Enable sensitive-data redaction in CLI output."
                }
            }
        },
        "history": {
            "type": "object",
            "description": "History import/analyze configuration (feature E3). Opt-in operation-triggered refresh.",
            "properties": {
                "refresh": {
                    "type": "object",
                    "properties": {
                        "on_completion": {
                            "type": "boolean",
                            "description": "Enqueue a coalesced history refresh when a task or pipeline run completes. Default false (opt-in; hidden automation is out of contract)."
                        },
                        "debounce_ms": {
                            "type": "integer",
                            "minimum": 0,
                            "description": "Coalescing window in milliseconds. Completions inside the window join one pending refresh. Default 60000."
                        }
                    }
                }
            }
        },
        "builder": {
            "type": "object",
            "description": "Builder noun configuration. Currently hosts the release plumbing knobs for `spur builder bump-ver` / `drop-tags`; every field defaults to the built-in release convention, so an absent builder section keeps default behavior.",
            "properties": {
                "bump-ver": {
                    "type": "object",
                    "description": "Release knobs for bump-ver/drop-tags. Defaults: -v separator, publish.yml workflow, chore(release) commit, gh run list limit 5.",
                    "properties": {
                        "aggregatePackage": {
                            "type": "string",
                            "minLength": 1,
                            "description": "Package whose own release tag is the publish trigger for the aggregate (--all) path, as an unscoped package id (e.g. \"spur\" for @gobing-ai/spur). Default: the package whose full name equals the workspace root manifest name."
                        },
                        "tagVersionSeparator": {
                            "type": "string",
                            "minLength": 1,
                            "description": "Release tag format: <package><separator><version>. Default \"-v\"."
                        },
                        "publishWorkflow": {
                            "type": "string",
                            "minLength": 1,
                            "description": "GitHub Actions workflow file the pushed release tag triggers. Default \"publish.yml\"."
                        },
                        "releaseCommitType": {
                            "type": "string",
                            "minLength": 1,
                            "description": "Conventional commit type for the release commit. Default \"chore\"."
                        },
                        "releaseCommitScope": {
                            "type": "string",
                            "minLength": 1,
                            "description": "Conventional commit scope for the release commit. Default \"release\"."
                        },
                        "ghRunListLimit": {
                            "type": "integer",
                            "minimum": 1,
                            "description": "gh run list --limit used to point at the triggered publish run. Default 5."
                        },
                        "versionCarriers": {
                            "type": "array",
                            "description": "Extra version carriers beyond workspace package.json files. Default: none. Types are registered in apps/cli/src/version-carriers.ts (registerCarrierType), which validates instances at release time; registering a new type extends this description in the same commit.",
                            "items": {
                                "type": "object",
                                "description": "One carrier instance. Required field: type (string). Built-ins: plugin-manifest { paths: [..] } — repo-wide manifests synced to the release version (e.g. per-platform plugin.json mirrors); ts-literal { file?, identifier? } — version constant in a package source file (defaults src/config.ts / binaryVersion).",
                                "properties": {
                                    "type": { "type": "string", "minLength": 1 }
                                },
                                "required": ["type"]
                            }
                        }
                    }
                }
            }
        },
        "tasks": {
            "type": "object",
            "description": "Task-folder registration (design \u00a79). Absorbs the legacy docs/.tasks/config.json folders + baseCounter concepts. Mirrors @gobing-ai/spur-config tasksConfigSchema (Zod is SSOT).",
            "properties": {
                "folders": {
                    "type": "object",
                    "description": "Map of task-folder path \u2192 folder config.",
                    "additionalProperties": {
                        "type": "object",
                        "properties": {
                            "baseCounter": {
                                "type": "integer",
                                "minimum": 0,
                                "description": "WBS base counter for new tasks in this folder. Default 0."
                            },
                            "label": {
                                "type": "string",
                                "description": "Human-readable folder label."
                            }
                        }
                    }
                },
                "active": {
                    "type": "string",
                    "description": "Default folder for `spur task create`. Default: docs/tasks."
                },
                "severity": {
                    "type": "object",
                    "description": "Rule severity overrides map by code (error | warning | off).",
                    "additionalProperties": {
                        "type": "string",
                        "enum": ["error", "warning", "off"]
                    }
                }
            }
        },
        "features": {
            "type": "object",
            "description": "Feature directory location (design \u00a79). Mirrors @gobing-ai/spur-config featuresConfigSchema (Zod is SSOT).",
            "properties": {
                "dir": {
                    "type": "string",
                    "description": "Feature files directory. Default: docs/features."
                }
            }
        }
    }
}
