{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://koryph.dev/schemas/config",
  "$defs": {
    "FootprintRule": {
      "properties": {
        "pattern": {
          "type": "string"
        },
        "tokens": {
          "items": {
            "type": "string"
          },
          "type": "array"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "pattern",
        "tokens"
      ],
      "description": "FootprintRule maps a path glob (doublestar-lite: '*' within a segment, '**' across segments, evaluated by sched) to footprint tokens."
    },
    "GoreleaserBuild": {
      "properties": {
        "version": {
          "type": "string",
          "description": "Version constrains the GoReleaser action version, e.g. \"~\u003e v2.16\".\nDefaults to \"~\u003e v2\" when empty."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "description": "GoreleaserBuild configures the GoReleaser build mode (mode A) for the project's release pipeline."
    },
    "IntakeSource": {
      "properties": {
        "provider": {
          "type": "string",
          "enum": [
            "github",
            "jira",
            "linear"
          ],
          "description": "Provider is the issue-tracker type. Supported values: \"github\" (default),\n\"jira\", \"linear\". The field defaults to \"github\" when omitted."
        },
        "source": {
          "type": "string",
          "description": "Source identifies the target within the provider.\n  GitHub: \"owner/repo\" (e.g. \"acme/widgets\").\n  JIRA:   \"\u003chost\u003e/\u003cproject-key\u003e\" (e.g. \"acme.atlassian.net/ENG\").\n  Linear: \"\u003cteam-key\u003e\" (e.g. \"ENG\")."
        },
        "trigger": {
          "type": "string",
          "description": "Trigger is the label (GitHub) or JQL predicate (JIRA) or label/state\nfilter (Linear) that determines which open issues are candidates for\nintake. Default: \"triage\".\n  GitHub: label name, e.g. \"triage\".\n  JIRA:   JQL clause AND-combined with \"project = \u003ckey\u003e\", e.g. `status = \"To Do\"`.\n  Linear: \"label:\u003cname\u003e\" or \"state:\u003cname\u003e\" or bare label name. When empty\n          all open issues in the team are polled."
        },
        "limit": {
          "type": "integer",
          "minimum": 0,
          "description": "Limit caps the number of open issues fetched per run. Default: 20."
        },
        "comment_back": {
          "type": "boolean",
          "description": "CommentBack posts the new bead ID back on each ingested issue.\nOpt-in; mirrors the --comment flag. Default: false."
        },
        "mapping": {
          "additionalProperties": {
            "type": "string"
          },
          "type": "object",
          "description": "Mapping is reserved for future provider-specific field remapping.\nIgnored in v1."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "source"
      ],
      "description": "IntakeSource configures one issue-tracker source in the koryph.project.json intake list."
    },
    "PipelineStage": {
      "properties": {
        "name": {
          "type": "string",
          "description": "Name identifies the stage (e.g. \"docs\", \"test\"). When it matches a known\nengine stage it inherits that stage's default persona and model tier via\nmodelroute; it is also the stage key for model:\u003cstage\u003e:\u003ctier\u003e labels."
        },
        "persona": {
          "type": "string",
          "description": "Persona overrides the agent (.claude/agents/\u003cpersona\u003e); default is\nmodelroute.PersonaFor(Name, Stages)."
        },
        "model": {
          "type": "string",
          "description": "Model overrides the tier for this stage (must be in AllowedModels);\ndefault is the engine stage default resolved by modelroute."
        },
        "effort": {
          "type": "string",
          "description": "Effort overrides the reasoning-effort hint passed to the agent."
        },
        "prompt": {
          "type": "string",
          "description": "Prompt is extra, stage-specific instruction text appended to the built\nstage prompt."
        },
        "optional": {
          "type": "boolean",
          "description": "Optional stages log-and-continue on failure; a non-optional stage (the\ndefault) that fails blocks the slot and stops the pipeline (fail closed)."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "name"
      ],
      "description": "PipelineStage is one post-implement stage in the project pipeline."
    },
    "PostureConfig": {
      "properties": {
        "profile": {
          "type": "string",
          "description": "Profile is the named posture profile, e.g. \"oss-solo-maintainer\".\nMust match a built-in profile name (koryph posture list) or a user\nprofile under ~/.koryph/postures/."
        },
        "parameters": {
          "additionalProperties": {
            "type": "string"
          },
          "type": "object",
          "description": "Parameters maps profile parameter names to their values, e.g.\n{\"required_checks\": \"pre-commit,make gate\"}.  Omit or set to {} to\nuse the profile's defaults for all parameters."
        },
        "fragments": {
          "items": {
            "type": "string"
          },
          "type": "array",
          "description": "Fragments lists the security-scanner fragment names this project has\nopted into (design §3.3).  Each name must match a built-in fragment\n(see `koryph posture list --fragments`).  Opted-in fragments are:\n  - installed into the project by `koryph posture apply`\n  - drift-checked by `koryph doctor --project`\nA profile's manifest.json may list recommended_fragments (informational\nonly — listing them here is what opts the project in)."
        },
        "org": {
          "type": "string",
          "description": "Org, when non-empty, names the GitHub organisation whose org-level\nrulesets should be drift-checked alongside the repo rulesets by\n`koryph doctor --project`.  Requires org owner / admin access; the\ndoctor check degrades gracefully when permission is absent."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "profile"
      ],
      "description": "PostureConfig is the optional desired-state posture sub-block of koryph.project.json."
    },
    "ReleaseBuildConfig": {
      "properties": {
        "goreleaser": {
          "$ref": "#/$defs/GoreleaserBuild",
          "description": "Goreleaser, when non-nil, selects mode A: GoReleaser-managed builds."
        },
        "commands": {
          "items": {
            "type": "string"
          },
          "type": "array",
          "description": "Commands, when non-empty, selects mode B: an ordered list of shell\ncommands (each run via sh -c) that build and stage artifacts."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "description": "ReleaseBuildConfig is the build sub-block of ReleaseConfig: exactly one of Goreleaser or Commands must be set (enforced by project.Config.Validate)."
    },
    "ReleaseConfig": {
      "properties": {
        "type": {
          "type": "string",
          "description": "Type is the release-please release type, e.g. \"go\", \"simple\",\n\"node\". See https://github.com/googleapis/release-please#release-types."
        },
        "extra_files": {
          "items": {
            "type": "string"
          },
          "type": "array",
          "description": "ExtraFiles lists additional files whose version strings release-please\nshould bump, e.g. [\"internal/version/version.go\"]."
        },
        "artifacts_dir": {
          "type": "string",
          "description": "ArtifactsDir is the directory where build artifacts land (default:\n\"dist\"). GoReleaser and mode-B commands should write outputs here."
        },
        "build": {
          "$ref": "#/$defs/ReleaseBuildConfig",
          "description": "Build is the build configuration: exactly one of Goreleaser or\nCommands must be set."
        },
        "sbom": {
          "type": "boolean",
          "description": "SBOM enables SBOM generation via anchore/sbom-action during the build."
        },
        "provenance": {
          "type": "boolean",
          "description": "Provenance enables SLSA provenance via\nslsa-framework/slsa-github-generator (generic, level 3)."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "type",
        "build"
      ],
      "description": "ReleaseConfig is the optional release sub-block of koryph.project.json."
    },
    "RuntimeConfig": {
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Enabled gates whether this project allows dispatching under this\nruntime at all; a bead or default_runtime naming a disabled runtime is\nrefused the same as an unregistered one. False (the default, also when\nthe whole entry is omitted) requires an explicit per-runtime opt-in —\nthe safer default while only claude is wired end-to-end in the engine."
        },
        "model_map": {
          "additionalProperties": {
            "type": "string"
          },
          "type": "object",
          "description": "ModelMap sparsely overrides this runtime's own tier-\u003emodel table\n(mirrors Config.ModelMap's shape, scoped to just this runtime)."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "description": "RuntimeConfig is one runtime's per-project override (koryph-v8u.3), keyed by runtime name in Config.Runtimes."
    },
    "SigningConfig": {
      "properties": {
        "required": {
          "type": "boolean",
          "description": "Required makes the engine fail closed: repo signing config is applied\nat run setup, the SSH agent must hold the key before dispatch, and\nevery merge verifies commit signatures."
        },
        "mode": {
          "type": "string",
          "enum": [
            "ssh",
            "gitsign"
          ],
          "description": "Mode is \"ssh\" (default when empty) or \"gitsign\"."
        },
        "provider": {
          "type": "string",
          "enum": [
            "protonpass",
            "onepassword",
            "file",
            "encrypted-file",
            "keychain",
            "command"
          ],
          "description": "Provider names the vault backend. Built-in no-vault providers:\n  encrypted-file  pure-Go age-encrypted file (all platforms)\n  keychain        macOS Keychain via security(1) (darwin only)\n  file            plaintext file path (legacy; WARN posture)"
        },
        "key_ref": {
          "type": "string",
          "description": "KeyRef is the provider-specific reference for the signing key: a\npass:// URI (protonpass), an op:// reference (onepassword), a file\npath (file), or whatever the command template consumes ({ref})."
        },
        "identity": {
          "type": "string",
          "description": "Identity is the signer email; it becomes the allowed-signers principal."
        },
        "public_key": {
          "type": "string",
          "description": "PublicKey is the SSH public key literal (\"ssh-ed25519 AAAA...\"),\ncaptured at setup. It configures user.signingkey and .allowed_signers."
        },
        "vault_name": {
          "type": "string",
          "description": "VaultName and ItemTitle record the vault selector used to resolve\nPublicKey when --vault-name/--item-title was used at setup. They are\nprovenance only — koryph signing status displays them as the key source.\nEach project pins its own vault item independently; no assumption is made\nabout a single global key."
        },
        "item_title": {
          "type": "string"
        },
        "artifacts": {
          "type": "boolean",
          "description": "Artifacts enables cosign blob signing (`koryph sign blob`)."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "required"
      ],
      "description": "Config is the per-project signing policy, stored as the \"signing\" block of koryph.project.json."
    },
    "SigningVaultDefaults": {
      "properties": {
        "provider": {
          "type": "string",
          "enum": [
            "protonpass",
            "onepassword",
            "file",
            "encrypted-file",
            "keychain",
            "command",
            "aws_secretsmanager",
            "azure_keyvault",
            "gcp_secretmanager",
            "keepassxc",
            "openbao",
            "vault"
          ],
          "description": "Provider names the vault backend. Must be one of the recognized\nsigning.Provider* constants when non-empty."
        },
        "container": {
          "type": "string",
          "description": "Container is the provider-native grouping:\n  protonpass       vault name (e.g. \"Engineering\")\n  onepassword      vault (e.g. \"Personal\")\n  file / encrypted-file  directory under which key files are stored\n  aws_secretsmanager     secret path prefix\n  keepassxc              KeePassXC database path / group\n  HashiCorp / OpenBao    KV mount path\nEmpty means \"use the provider's default\"; not all providers have a\nmeaningful container concept."
        }
      },
      "additionalProperties": false,
      "type": "object",
      "description": "VaultDefaults is the vault block shared by koryph.project.json and the global ~/.koryph/config.json."
    }
  },
  "properties": {
    "schema_version": {
      "type": "integer"
    },
    "project_id": {
      "type": "string"
    },
    "work_source": {
      "type": "string",
      "enum": [
        "bd",
        "markdown"
      ],
      "description": "WorkSource is \"bd\" (beads ready-graph, preferred) or \"markdown\"\n(legacy docs/plans phase docs; supported for un-migrated projects)."
    },
    "plans_dir": {
      "type": "string"
    },
    "footprint": {
      "items": {
        "$ref": "#/$defs/FootprintRule"
      },
      "type": "array",
      "description": "Footprint declares conflict domains. AreaMap maps an `area:\u003cx\u003e` bead\nlabel to footprint tokens when no fp:* label is present."
    },
    "area_map": {
      "additionalProperties": {
        "items": {
          "type": "string"
        },
        "type": "array"
      },
      "type": "object"
    },
    "gate": {
      "items": {
        "type": "string"
      },
      "type": "array",
      "description": "Gate is the ordered green-gate command list run in the worktree after\nrebase and before merge (each entry runs via `sh -c` under direnv when\navailable)."
    },
    "stages": {
      "additionalProperties": {
        "type": "string"
      },
      "type": "object",
      "description": "Stages maps pipeline stage -\u003e persona name in .claude/agents.\nTiers maps model tier -\u003e persona for tier-driven dispatch."
    },
    "tiers": {
      "additionalProperties": {
        "type": "string"
      },
      "type": "object"
    },
    "model_map": {
      "additionalProperties": {
        "type": "string"
      },
      "type": "object",
      "description": "ModelMap overrides the active runtime's tier -\u003e concrete-model-id map\n(koryph-v8u.10). Keys are the runtime-agnostic tier vocabulary a\npersona's `tier:` frontmatter carries (\"frontier\", \"standard\",\n\"light\" — see agents/README.md's frontmatter contract); values are\nconcrete model ids for whichever runtime is active (today, always\nClaude: \"opus\"/\"sonnet\"/\"haiku\", or \"fable\" as an explicit frontier\noverride). Sparse: only the tiers an operator wants to re-map need be\npresent, e.g. {\"frontier\": \"fable\"} — every other tier keeps\nruntime.ClaudeModelMap's default. A project-config host (rather than a\nregistry record) was chosen because AllowedModels/Tiers/Stages already\ncarry per-project model policy right here, and because this value is\nread on every dispatch (modelroute.Resolve), the same hot path as\nthose fields — see internal/modelroute/route.go's effectiveModelMap\nfor how it overlays onto the runtime default."
    },
    "pipeline": {
      "items": {
        "$ref": "#/$defs/PipelineStage"
      },
      "type": "array",
      "description": "Pipeline lists post-implement stages executed sequentially in the\nworktree after the implementer and before review/merge. Empty keeps the\nclassic implement -\u003e (review) -\u003e merge flow. See PipelineStage."
    },
    "bootstrap": {
      "items": {
        "type": "string"
      },
      "type": "array",
      "description": "Bootstrap commands run in a freshly created or re-attached worktree\nbefore the agent starts (e.g. \"pnpm install --frozen-lockfile\")."
    },
    "intake": {
      "items": {
        "$ref": "#/$defs/IntakeSource"
      },
      "type": "array",
      "description": "Intake lists the issue-tracker sources polled by `koryph intake`.\nEach entry drives one poll per run. When empty, intake falls back to\nthe project's registry remote with CLI flags."
    },
    "protected_paths": {
      "items": {
        "type": "string"
      },
      "type": "array",
      "description": "ProtectedPaths extend the engine's default protected list; diffs\ntouching them are never mergeable from a worktree."
    },
    "validation": {
      "items": {
        "type": "string"
      },
      "type": "array",
      "description": "Validation commands for `koryph validate` (beyond the engine checks)."
    },
    "engine_version": {
      "type": "string",
      "description": "EngineVersion is the minimum koryph engine this project requires,\nminimum-style: \"0.2+\", \"1+\", \"\u003e=0.2.0\", or a bare version (also a\nminimum). Empty = any engine."
    },
    "commit_style": {
      "type": "string",
      "enum": [
        "conventional",
        "custom",
        "none"
      ],
      "description": "CommitStyle governs agent commit messages: \"conventional\" (default,\nalso when empty) is mechanically enforced at merge/PR time (every\ncommit subject in def..branch must match type(scope): subject);\n\"custom\" (CommitTemplate required) governs via the template and is not\nconventional-validated; \"none\" opts out of enforcement entirely.\nProjects can additionally map Stages[\"commit\"] to a persona whose\nguidance agents consult for commit authoring."
    },
    "commit_template": {
      "type": "string"
    },
    "merge_policy": {
      "type": "string",
      "enum": [
        "manual",
        "auto",
        "pr"
      ],
      "description": "MergePolicy default when the epic carries no merge:* label."
    },
    "merge_method": {
      "type": "string",
      "enum": [
        "ff",
        "squash"
      ],
      "description": "MergeMethod is how an engine-opened PR lands on the default branch:\n\"ff\" (default, also when empty) preserves the exact gate-checked, signed\ncommit SHAs via a local fast-forward + push; \"squash\" collapses them into\none new commit. A non-ff method is refused while signing is required\n(only ff preserves signatures). GitHub-native merge methods are never\nused — they rewrite SHAs or add an unsigned merge commit."
    },
    "risk_tier_default": {
      "type": "integer",
      "maximum": 3,
      "minimum": 0,
      "description": "RiskTierDefault is the recovery tier (0-3) for beads without rt:*."
    },
    "vault": {
      "$ref": "#/$defs/SigningVaultDefaults",
      "description": "Vault sets the project-level default vault provider and container\n(provider-native grouping: Proton Pass vault name, 1Password vault, file\ndirectory, etc.). Commands that store or fetch secrets use this block when\nno explicit flags are supplied. Falls back to the global\n~/.koryph/config.json vault block when absent.\nManaged by `koryph signing setup` (sets provider/container on first run)."
    },
    "signing": {
      "$ref": "#/$defs/SigningConfig",
      "description": "Signing is the vault-backed commit/artifact signing policy\n(nil = signing not configured; managed by `koryph signing setup`)."
    },
    "max_concurrent_slots": {
      "type": "integer",
      "description": "MaxConcurrentSlots caps wave width for this project (default 3)."
    },
    "dispatch_stagger_seconds": {
      "type": "integer",
      "description": "DispatchStaggerSeconds between agent launches (default 8)."
    },
    "poll_seconds": {
      "type": "integer",
      "minimum": 0,
      "description": "PollSeconds overrides the engine's slot poll tick for this project\n(default 10 when zero/omitted). KORYPH_POLL_SEC and an explicit\nprogrammatic Options.PollSec caller override still take precedence over\nthis value (see engine.runner.pollInterval; koryph-2im.2)."
    },
    "dispatch_mode": {
      "type": "string",
      "enum": [
        "wave",
        "rolling"
      ],
      "description": "DispatchMode selects the engine's dispatch loop (koryph-2im.3,\ndocs/designs/2026-07-scheduler-throughput.md L1): \"wave\" (also\nwhen empty) dispatches a fixed-width batch and blocks until every slot\nin it lands before scanning the frontier again; \"rolling\" continuously\nrefills any slot that frees up without waiting for the rest of the\nbatch. A run's --dispatch-mode flag overrides this value; --once runs\ntoday's wave semantics in both modes."
    },
    "default_runtime": {
      "type": "string",
      "description": "DefaultRuntime selects the runtime (internal/runtime.Runtime) a bead\ndispatches under when it carries no `runtime:\u003cname\u003e` label\n(koryph-v8u.3). Empty means \"claude\" — today's only runtime the engine\nactually dispatches through; internal/engine's dispatchBead blocks\n(rather than silently substituting claude) any bead or default that\nresolves to anything else. Must be \"\", \"claude\", or a name registered\nin runtime.Default (enforced by Validate)."
    },
    "runtimes": {
      "additionalProperties": {
        "$ref": "#/$defs/RuntimeConfig"
      },
      "type": "object",
      "description": "Runtimes configures per-runtime settings for this project, keyed by\nruntime name — the same string a bead's `runtime:\u003cname\u003e` label and\nDefaultRuntime use, and runtime.Runtime.Name()'s value. See\nRuntimeConfig's doc for how minimal this is today."
    },
    "release": {
      "$ref": "#/$defs/ReleaseConfig",
      "description": "Release, when non-nil, configures the project's release pipeline\n(managed by `koryph release setup`). It drives template rendering for\nthe caller GitHub Actions workflow and the release-please config.\nExactly one build mode (Build.Goreleaser or Build.Commands) must be\nset when this block is present (enforced by Validate)."
    },
    "posture": {
      "$ref": "#/$defs/PostureConfig",
      "description": "Posture, when non-nil, declares the desired-state posture profile for\nthis project's GitHub repository. koryph doctor --project reports any\ndrift between the live repo and the named profile as WARN, with the\nexact koryph posture apply command to remediate.\n\nManaged by `koryph project add` (interactive offer) and the future\n`koryph new` command (koryph-om7, HELD).  Nil means no profile is\ndeclared and the drift check is silently skipped."
    },
    "forge": {
      "type": "string",
      "enum": [
        "github",
        "gitlab"
      ],
      "description": "Forge names the git forge provider for this project. Supported values:\n\"github\" (default, also when empty — full back-compat; existing projects\nwithout this field continue to behave as GitHub projects) and \"gitlab\".\nRemote-URL sniffing may suggest a value during `koryph onboard`, but the\noperator always makes the final selection.\n\nThis field is the single source of truth for which internal/forge\nprovider is active; the registry record inherits it at\nonboard/add time."
    }
  },
  "additionalProperties": false,
  "type": "object",
  "required": [
    "schema_version",
    "project_id",
    "work_source",
    "gate",
    "merge_policy",
    "risk_tier_default"
  ],
  "title": "koryph.project.json",
  "description": "Per-project koryph adapter configuration (koryph.project.json). Generated from the Go project.Config struct — do not edit by hand; run `go generate ./internal/project`."
}
