{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://flow-next.dev/schema/flow-config.schema.json",
  "title": "flow-next .flow/config.json",
  "description": "Project configuration for flow-next, stored at .flow/config.json. All keys are optional: flowctl merges these defaults over the file at read time, so an absent key reads as its documented default. Generated by scripts/gen_flow_config_schema.py - do not hand-edit the schema.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "description": "URL of the JSON Schema this config validates against, stamped by /flow-next:setup. Inert to flowctl (just a string); editors are the consumer.",
      "type": "string"
    },
    "memory": {
      "description": "Memory system settings (.flow/memory/ categorized learnings).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "description": "Enable the memory system: skills capture and search categorized learnings under .flow/memory/.",
          "type": "boolean",
          "default": true
        }
      }
    },
    "planSync": {
      "description": "Plan-sync settings (downstream task-spec reconciliation after each completed task).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "description": "Enable plan-sync after task completion.",
          "type": "boolean",
          "default": true
        },
        "crossSpec": {
          "description": "Cross-spec plan-sync: scan other open specs for stale references after each task (opt-in; increases sync time).",
          "type": "boolean",
          "default": false
        }
      }
    },
    "review": {
      "description": "Review subsystem settings.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "backend": {
          "description": "Default review backend (rp, codex, copilot, cursor, host, none) or spec form backend[:model[:effort]], e.g. codex:gpt-5.4:high. cursor folds effort into the model name (no :effort rung); rp, host, and none are bare-only. copilot accepts no none/minimal effort. If unset, review commands require --review or FLOW_REVIEW_BACKEND.",
          "anyOf": [
            {
              "type": "null"
            },
            {
              "enum": [
                "codex",
                "copilot",
                "cursor",
                "host",
                "none",
                "rp"
              ]
            },
            {
              "type": "string",
              "pattern": "^(codex|copilot|cursor|host|none|rp):{1,2}$"
            },
            {
              "type": "string",
              "pattern": "^codex:[^:\\s]*(:(high|low|medium|minimal|none|xhigh)?)?$"
            },
            {
              "type": "string",
              "pattern": "^copilot:[^:\\s]*(:(high|low|medium|xhigh)?)?$"
            },
            {
              "type": "string",
              "pattern": "^(cursor):[^:\\s]*:?$"
            }
          ],
          "default": null
        }
      }
    },
    "scouts": {
      "description": "Planning-scout settings.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "github": {
          "description": "Enable github-scout during planning (requires the gh CLI).",
          "type": "boolean",
          "default": false
        }
      }
    },
    "tracker": {
      "description": "Tracker-sync bridge settings (Linear / GitHub / GitLab / Jira). See docs/tracker-sync.md.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "version": {
          "description": "Tracker config schema version.",
          "type": "integer",
          "default": 1
        },
        "enabled": {
          "description": "Enable the tracker-sync bridge. The bridge is active iff raw tracker.enabled == true OR raw tracker.type is one of linear/github/gitlab/jira.",
          "type": "boolean",
          "default": false
        },
        "type": {
          "description": "Tracker backend: linear, github, gitlab, or jira.",
          "enum": [
            "linear",
            "github",
            "gitlab",
            "jira",
            null
          ],
          "default": null
        },
        "provenance": {
          "description": "Free-form provenance written by the discovery ceremony on confirmation (who/when/signals).",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "perEvent": {
          "description": "Per-lifecycle-event sync operations. Schema default off for every event, so a bare enabled=true fires no lifecycle sync; the /flow-next:tracker-sync discovery ceremony activates events opt-out when you hook up the bridge.",
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "capture": {
              "description": "Sync op fired when a spec is captured: off | pull | push | reconcile | comment.",
              "enum": [
                "off",
                "pull",
                "push",
                "reconcile",
                "comment"
              ],
              "default": "off"
            },
            "interview": {
              "description": "Sync op fired after an interview updates a spec: off | pull | push | reconcile | comment.",
              "enum": [
                "off",
                "pull",
                "push",
                "reconcile",
                "comment"
              ],
              "default": "off"
            },
            "plan": {
              "description": "Sync op fired after planning: off | pull | push | reconcile | comment.",
              "enum": [
                "off",
                "pull",
                "push",
                "reconcile",
                "comment"
              ],
              "default": "off"
            },
            "work": {
              "description": "Work-lifecycle sync events.",
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "firstClaim": {
                  "description": "Sync op fired on a task's first claim: off | pull | push | reconcile | comment.",
                  "enum": [
                    "off",
                    "pull",
                    "push",
                    "reconcile",
                    "comment"
                  ],
                  "default": "off"
                },
                "done": {
                  "description": "Sync op fired when a task completes: off | pull | push | reconcile | comment.",
                  "enum": [
                    "off",
                    "pull",
                    "push",
                    "reconcile",
                    "comment"
                  ],
                  "default": "off"
                }
              }
            },
            "makePr": {
              "description": "Sync op fired when make-pr opens a PR: off | pull | push | reconcile | comment. The PR link + In Review push is unconditional whenever the bridge is active.",
              "enum": [
                "off",
                "pull",
                "push",
                "reconcile",
                "comment"
              ],
              "default": "off"
            },
            "resolvePr": {
              "description": "Sync op fired after resolve-pr: off | pull | push | reconcile | comment.",
              "enum": [
                "off",
                "pull",
                "push",
                "reconcile",
                "comment"
              ],
              "default": "off"
            },
            "completionReview": {
              "description": "Sync op fired after the spec completion review: off | pull | push | reconcile | comment. The ceremony seeds comment (verdict + R-ID coverage; never terminal Done).",
              "enum": [
                "off",
                "pull",
                "push",
                "reconcile",
                "comment"
              ],
              "default": "off"
            },
            "qa": {
              "description": "Post the /flow-next:qa ship verdict as a tracker comment: off | comment only. comment is the only sensible verb for a verdict; the QA skill treats any non-off value as comment. Not switched on by the ceremony's default-on set - QA-specific opt-in.",
              "enum": [
                "off",
                "comment"
              ],
              "default": "off"
            },
            "land": {
              "description": "Land-lifecycle sync events.",
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "merged": {
                  "description": "Post-merge touchpoint for /flow-next:land. Active-by-default whenever the bridge is active (a real merge is the only event that legitimately projects terminal Done); this leaf only tunes the optional verdict comment, never the merge-gated status write.",
                  "enum": [
                    "off",
                    "pull",
                    "push",
                    "reconcile",
                    "comment"
                  ],
                  "default": "off"
                }
              }
            }
          }
        },
        "perTracker": {
          "description": "Per-tracker linkage written by the discovery ceremony on confirmation. Open: tracker types add their own keys here.",
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "teamId": {
              "description": "Linear team id.",
              "type": [
                "string",
                "null"
              ],
              "default": null
            },
            "projectId": {
              "description": "Linear project id.",
              "type": [
                "string",
                "null"
              ],
              "default": null
            },
            "labelMap": {
              "description": "Label linkage map (tracker-specific shape).",
              "type": "object",
              "additionalProperties": true,
              "default": {}
            },
            "priorityMap": {
              "description": "Priority linkage map (tracker-specific shape).",
              "type": "object",
              "additionalProperties": true,
              "default": {}
            },
            "repo": {
              "description": "GitHub repo as owner/name, written by the discovery ceremony (machine-written; not part of the seeded defaults).",
              "type": [
                "string",
                "null"
              ]
            },
            "project": {
              "description": "GitLab group/subgroup/project path (URL-encoded once for the API, never double-encoded).",
              "type": [
                "string",
                "null"
              ],
              "default": null
            },
            "host": {
              "description": "Self-managed GitLab base URL. null resolves from glab config / CI_SERVER_URL; gitlab.com is never assumed.",
              "type": [
                "string",
                "null"
              ],
              "default": null
            },
            "baseUrl": {
              "description": "Jira site base URL (Cloud or DC/Server). The JIRA_BASE_URL env var overrides it at runtime.",
              "type": [
                "string",
                "null"
              ],
              "default": null
            },
            "projectKey": {
              "description": "Jira project key (the JQL / listOpenIssues scope).",
              "type": [
                "string",
                "null"
              ],
              "default": null
            },
            "authScheme": {
              "description": "Jira auth shape decided once at the discovery ceremony: cloud-basic (Cloud HTTP-basic email:API_TOKEN) or bearer-pat (DC/Server bearer PAT). Credentials still read from env each run, never stored here.",
              "enum": [
                "cloud-basic",
                "bearer-pat",
                null
              ],
              "default": null
            },
            "apiVersion": {
              "description": "Jira REST API version. null until the resolver pins 2; migration converges a legacy 3 to 2.",
              "type": [
                "integer",
                "null"
              ],
              "default": null
            },
            "owner": {
              "description": "GitHub repository owner (discovery-fingerprint input; dynamic per.get read via _FINGERPRINT_KEYS).",
              "type": [
                "string",
                "null"
              ]
            },
            "issueType": {
              "description": "Jira issue type (name or id) for created issues; a configured value that does not resolve against the live project is an error.",
              "type": [
                "string",
                "integer",
                "null"
              ]
            },
            "blocksLinkType": {
              "description": "GitLab link type used for blocks relations (e.g. blocks); probe and mutation use the same resolved name.",
              "type": [
                "string",
                "null"
              ]
            },
            "preferredTransport": {
              "description": "Linear transport preference (mcp routes through the MCP continuation; anything else uses HTTP).",
              "type": [
                "string",
                "null"
              ]
            },
            "transport": {
              "description": "Legacy alias for preferredTransport (read second).",
              "type": [
                "string",
                "null"
              ]
            },
            "sslVerify": {
              "description": "Verify TLS certificates against Jira. false is an explicit opt-out for a self-hosted internal-CA / self-signed cert (JIRA_SSL_VERIFY env overrides).",
              "type": "boolean",
              "default": true
            },
            "statusMap": {
              "description": "Legacy normalized-status to Jira status map ({name}/{id}; id preferred - names are project-renamable). Live entries migrate into tracker.resolved.destination.statusIds; dead entries are dropped with a warning.",
              "type": "object",
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "id": {
                    "type": [
                      "string",
                      "integer"
                    ]
                  }
                },
                "additionalProperties": true
              },
              "default": {}
            }
          }
        },
        "staleAfterHours": {
          "description": "Staleness threshold (hours) consumed by sync list-stale.",
          "type": "integer",
          "default": 24
        },
        "conflictTiebreak": {
          "description": "Status who-wins tiebreak: flow-wins | tracker-wins | always-ask. Strict enum: invalid CLI writes are rejected; malformed persisted values fail before status work. In Ralph mode always-ask resolves to queue, not prompt.",
          "enum": [
            "always-ask",
            "flow-wins",
            "tracker-wins"
          ],
          "default": "always-ask"
        },
        "readyState": {
          "description": "Readiness projection: the tracker workflow state meaning ready for work (a Linear state name, Jira status name, or a GitHub/GitLab label). When set, pull-side sync projects it onto the local spec ready flag - one-way, tracker is authoritative. null = projection off.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "specIds": {
          "description": "Id scheme for new specs when a tracker bridge is active: flow (native fn-N) or tracker (tracker-keyed KEY-N-slug / synthetic gh-N / gl-N). Strict enum on write; malformed on-disk values fail closed to flow. Not materialized at init so setup can detect never-asked via a raw null read.",
          "enum": [
            "flow",
            "tracker"
          ],
          "default": "flow"
        },
        "transport": {
          "description": "HTTP transport tuning for tracker calls (dict-read in flowctl_tracker).",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": true,
          "properties": {
            "timeoutS": {
              "description": "Per-request timeout in seconds (0-600).",
              "type": [
                "number",
                "null"
              ]
            },
            "maxRetries": {
              "description": "Retry attempts per call.",
              "type": [
                "integer",
                "null"
              ]
            },
            "backoffCapS": {
              "description": "Backoff cap in seconds.",
              "type": [
                "number",
                "null"
              ]
            },
            "concurrency": {
              "description": "Max concurrent tracker calls.",
              "type": [
                "integer",
                "null"
              ]
            }
          }
        },
        "resolved": {
          "description": "Persisted runtime facts written by flowctl tracker resolve (destination + capability cache). Machine-written, atomic, partially-absent-by-design during migration - never hand-edit; refresh with flowctl tracker resolve.",
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "destination": {
              "description": "Resolved destination facts. Fields vary by tracker type (GitHub owner/repo; GitLab projectId/projectPath/host/namespaceId; Linear teamId/teamKey/stateIds/labelIds; Jira baseUrl/projectKey/projectId/issueTypeId/apiVersion/style/statusIds), so no per-provider fields are required here.",
              "type": "object",
              "additionalProperties": true,
              "properties": {
                "statusIds": {
                  "description": "Normalized status slots (todo, in_progress, done; optional provider slots) mapped to provider status ids.",
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "stateIds": {
                  "description": "Normalized state slots mapped to provider state ids (Linear).",
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              }
            },
            "capabilities": {
              "description": "Provider capability booleans (e.g. attachments, blockedBy, subIssues, deleteIssue) from the provider's static table.",
              "type": "object",
              "properties": {
                "_source": {
                  "type": "object",
                  "description": "Machine-written capability provenance (which probe/endpoint established each flag); GitLab's resolver persists it alongside the boolean capability keys."
                }
              },
              "additionalProperties": {
                "type": "boolean"
              }
            },
            "scopeResolvedAt": {
              "description": "Per-scope resolution timestamps, keyed by exactly the four canonical scopes. Scope timestamps are independent: a destination refresh cannot make capabilities or status ids look fresh.",
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "destination": {
                  "type": "string",
                  "description": "ISO timestamp of the last successful destination resolution."
                },
                "destination.statusIds": {
                  "type": "string",
                  "description": "ISO timestamp of the last successful destination.statusIds resolution."
                },
                "destination.stateIds": {
                  "type": "string",
                  "description": "ISO timestamp of the last successful destination.stateIds resolution."
                },
                "capabilities": {
                  "type": "string",
                  "description": "ISO timestamp of the last successful capabilities resolution."
                }
              }
            },
            "resolvedAt": {
              "description": "Non-null only when all required destination fields, required normalized slots, and capability booleans are present.",
              "type": [
                "string",
                "null"
              ]
            }
          }
        }
      }
    },
    "work": {
      "description": "/flow-next:work implementation-delegation settings (fn-55).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "delegate": {
          "description": "Opt-in delegation of implementation to a local codex exec. Only the literal string codex activates (any other value, including bool true, is OFF). Resolution: arg token delegate:codex / delegate:local > this config > hard default OFF.",
          "enum": [
            "codex",
            false
          ],
          "default": false
        },
        "delegateModel": {
          "description": "On-disk override for the model passed to codex exec (-m) for delegated implementation. When raw-set on disk it wins over models.roles.delegate.codex; skills resolve via flowctl models resolve delegate. Requires codex CLI >= 0.144 for the default.",
          "type": "string",
          "default": "gpt-5.6-terra"
        },
        "delegateEffort": {
          "description": "Reasoning-effort floor (none | low | medium | high | xhigh), emitted as -c model_reasoning_effort= on the delegated codex exec. Per-run risk escalation floors against this. The enum includes none, not minimal.",
          "enum": [
            "none",
            "low",
            "medium",
            "high",
            "xhigh"
          ],
          "default": "medium"
        },
        "delegateSandbox": {
          "description": "Codex sandbox mode (yolo | full-auto), persisted by the one-time consent gate. yolo has a wider blast radius - the gate surfaces this before first use.",
          "enum": [
            "yolo",
            "full-auto"
          ],
          "default": "yolo"
        },
        "delegateConsent": {
          "description": "One-time-consent flag written by the host consent gate after the user opts in. Headless/Ralph requires this pre-set to true (no live prompt path).",
          "type": "boolean",
          "default": false
        },
        "delegateDecision": {
          "description": "Per-task delegation decision (auto | ask). auto delegates every eligible task; ask prompts per-task in interactive mode (treated as auto in headless only when delegateConsent=true).",
          "enum": [
            "auto",
            "ask"
          ],
          "default": "auto"
        }
      }
    },
    "land": {
      "description": "/flow-next:land babysit-loop settings (fn-60).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "release": {
          "description": "Run the post-merge release-follow step (the project's own release docs; no-ops when none are discovered). false = stop at merge.",
          "type": "boolean",
          "default": true
        },
        "patienceMinutes": {
          "description": "Reviewer patience window in minutes, anchored to the LAST push (a land-authored CI-fix push restarts it).",
          "type": "integer",
          "default": 30
        },
        "reviewSignal": {
          "description": "Merge review-signal: silence (automated review present + zero unresolved threads + window elapsed), approve (formal reviewDecision == APPROVED), or a GitHub login (that reviewer's latest review must be clean).",
          "type": "string",
          "default": "silence"
        },
        "automatedReviewers": {
          "description": "CSV allowlist of reviewer logins land counts as automated, supplementing the [bot]-suffix rule. Empty = suffix rule only.",
          "type": "string",
          "default": ""
        },
        "reviewTrigger": {
          "description": "One-shot comment land posts to summon a reviewer bot on a draft PR with zero automated reviews (e.g. @codex review - bots don't auto-review drafts). Empty = never post.",
          "type": "string",
          "default": ""
        },
        "ciFixBudget": {
          "description": "CI-fix attempts per PR before land durably labels it flow-next:needs-human and skips it on later ticks.",
          "type": "integer",
          "default": 3
        },
        "cleanReviewCommentPattern": {
          "description": "ERE for the silence-signal clean-review COMMENT path: a bot's no-findings issue comment naming the current head SHA also satisfies the gate. Requires BOTH the clean phrase AND the Reviewed commit marker. null/missing falls back to the built-in default; set to an empty string to disable the comment scan (the only real off-switch).",
          "type": [
            "string",
            "null"
          ],
          "default": "(Didn'?t find any( major)? issues|No( major)? issues found).*Reviewed commit"
        }
      }
    },
    "makePr": {
      "description": "/flow-next:make-pr export settings.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "derivedPaths": {
          "description": "Optional derived-file classification rules for the make-pr export: bucket names (dualCopy, mirror, state) mapped to arrays of rules ({path|prefix, source}). A configured value fully replaces flow-next's built-in default shapes; never required and not part of the seeded defaults.",
          "type": "object",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "artifacts": {
      "description": "HTML artifact mode settings (fn-62).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "html": {
          "description": "HTML render-lens settings.",
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enabled": {
              "description": "Enable optional HTML artifacts: participating skills emit self-contained lenses at .flow/artifacts/<spec-id>/. OFF by default - markdown stays the sole source of truth.",
              "type": "boolean",
              "default": false
            }
          }
        }
      }
    },
    "pipeline": {
      "description": "Pilot pipeline stage gates.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "qa": {
          "description": "Optional QA pipeline stage (fn-72). String-enum, NOT a bool: only the literal on activates; any other value, including bool true, is OFF. With it on, pilot inserts one live /flow-next:qa pass at the all-tasks-done juncture before make-pr.",
          "enum": [
            "off",
            "on"
          ],
          "default": "off"
        }
      }
    },
    "pilot": {
      "description": "/flow-next:pilot settings.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "autonomy": {
          "description": "Pilot backlog mode (fn-68). Scalar string-enum (ready | backlog), NOT a bool. ready = select only already-ready specs. Only the literal backlog widens selection to the whole open backlog; any other value stays ready. Backlog mode never authors a spec, never sets ready, never merges.",
          "enum": [
            "ready",
            "backlog"
          ],
          "default": "ready"
        },
        "gateClasses": {
          "description": "Backlog-mode force-gate: class names (e.g. risky, prod-config) that force surfacing before action - a matching item is parked with a question instead of advanced full-auto. Empty = full-auto for every workable item.",
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ],
          "default": []
        }
      }
    },
    "models": {
      "description": "Model-pin role map (fn-115). flowctl stores + validates + does staleness math only; /flow-next:setup probes and refreshes pins.",
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "properties": {
        "roles": {
          "description": "Role-map pins: models.roles.<role>.<backend> = model or model:effort. Roles name semantic jobs (fastJudge, review, delegate, scoutFast, scoutIntelligent), not call sites; backends are codex, copilot, cursor. Schema-validated on config set. Empty = registry baselines.",
          "type": [
            "object",
            "null"
          ],
          "patternProperties": {
            "^(fastJudge|review|delegate|scoutFast|scoutIntelligent)$": {
              "type": [
                "object",
                "null"
              ],
              "description": "Per-backend pins for one semantic role.",
              "patternProperties": {
                "^(codex|copilot|cursor)$": {
                  "anyOf": [
                    {
                      "type": "null"
                    },
                    {
                      "enum": [
                        ""
                      ]
                    },
                    {
                      "type": "string",
                      "pattern": "^[^:\\s]+(:[^:\\s]+)?$"
                    }
                  ],
                  "description": "Pin value: model or model:effort."
                }
              },
              "additionalProperties": false
            }
          },
          "additionalProperties": false,
          "default": {}
        },
        "verifiedAt": {
          "description": "ISO date stamped by /flow-next:setup after the model-pin refresh ceremony. Older than ~90 days prints one nudge line (never blocks); absent = no nudge.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "verifiedWith": {
          "description": "Optional free-form record of CLI versions probed during the refresh ceremony.",
          "default": null
        }
      }
    }
  }
}
