# Spur project configuration example
# Seeded by `spur init` as ~/.config/spur/config.yaml on first run
# (never overwritten).
# Copy / adapt into a project as `.spur/config.yaml`.

$schema: "@gobing-ai/spur/schemas/spur-config.schema.json"
# Config schema version (string). Current recommended: "1.2".
# "1" remains accepted; there is no hard migrator yet. Prefer "1.1" for
# new projects so editors and operators know the file targets the ADR-033
# + planning shape.
version: "1.2"
name: my-project

# ── Portable bootstrap (consumed by @gobing-ai/ts-infra
# runNodeApplication) ──
# Shared across spur and (future) spur-server. Keys map 1:1 to ts-infra's
# LoggingOptions / TelemetryOptions / database / SchedulerOptions.
# Not validated by spurConfigSchema (bootstrap is owned by runNodeApplication).
bootstrap:
  logging:
    enabled: true
    level: info # debug | info | warn | error
    console: false
    json: false
    file: true
    filePath: .spur/logs/spur.log # relative to project root
  telemetry:
    enabled: false # OFF by default for CLI (per-invocation latency)
    serviceName: spur
    environment: development
    # endpoint: http://localhost:4318   # set => Node OTel exporter activates
  database:
    enabled: true
    driver: bun-sqlite
    url: .spur/spur.db # ${DATABASE_URL} interpolation supported
  scheduler:
    enabled: false # CLI is run-once; no scheduler
    # Periodic executions live here and nowhere else (task 0750). A tick
    # enqueues `scheduler.custom`, which the server's job worker runs through
    # /bin/sh -c in the project root. Use the repo-local CLI, never a global
    # `spur` that may be a stale bundle (AGENTS.md history-validation rule).
    jobs:
      # Incremental refresh, waking hours only (07:00-23:50; the cron hour range
      # keeps 00:00-06:59 quiet). `history daily` = import + analyze; analyze is
      # what refreshes the Board History rollups (token usage included) - import
      # alone never does. Checkpoint resume + the rollup-freshness short-circuit
      # keep a no-op tick cheap (I81).
      - name: history-refresh
        cron: "*/10 7-23 * * *"
        command: spur --no-logo history daily

      # 05:00 daily: full-corpus import (checkpoint resume self-heals gaps),
      # then the twelve-section history-anatomy diagnostic report headless. On
      # a cache hit the workflow skips the token-expensive model stages and
      # republishes provenance (ADR-079), so a ready report costs no tokens.
      - name: history-daily-report
        cron: "3 5 * * *"
        command: >-
          spur --no-logo history import --source all &&
          spur --no-logo workflow run config/workflows/history-anatomy.yaml --quiet

# ── Spur app section (validated by spurConfigSchema in
# @gobing-ai/spur-config) ──
# Server runtime (port/host/openBrowser) is env-backed via PORT/HOST — not a
# project-config key. See design §2.2.
agent:
  # `default` is the role used for `--agent auto` when the caller declares
  # nothing (task 0542 R2 — the value domain moved from executor names to
  # roles). A role id selects its tier's cheapest eligible executor; a
  # configured executor name still resolves during the transition with a
  # one-time warning (shim agent-default-executor); any other value fails
  # naming both accepted sets.
  default: coder

  # Role → tier map (SSOT: DEFAULT_AGENT_ROLES in packages/config, task 0572 /
  # ADR-061; plugins/sp/references/roles.md is a parity-gated projection).
  # Resolution: role → tier → cheapest USABLE executor whose tier >= the role's
  # tier, climbing the ladder across gaps. Declare at least one usable executor
  # at or above each rung or that role fails loudly at dispatch:
  #   scribe=cheap · coder=standard · reviewer=capable-1 · planner=capable-2
  # Stage floors (stage-registry model_policy): changelog=cheap;
  # implement/test/wrap/review/refine/brainstorm=standard;
  # verify/dogfood=capable-1; plan=capable-2. A stage's fallback ladder
  # (auth / resource-exhaustion / gate-fail) only advances when a higher rung
  # has a usable executor — an empty rung turns a quota failure into a hang.
  #
  # Optional per-project override (0572): re-tier / re-stage a KNOWN role only —
  # the vocabulary is closed; a config naming any other key fails at config load
  # naming the accepted four. Per-field merge over the defaults (restate only
  # what you override; a role you omit keeps its default wholesale):
  # roles:
  #   reviewer:
  #     tier: capable-2        # e.g. pin review work one rung higher
  #   coder:
  #     stages: [implement]    # e.g. stop folding test/wrap into coder

  # Canonical coding-agent ids (ts-ai-runner DISPLAY_ORDER, 0.4.8+):
  #   claude, codex, gemini, pi, omp, opencode, antigravity-cli,
  #   openclaw, hermes, grok
  # Grok auth: non-empty XAI_API_KEY and/or ~/.grok/auth.json (no CLI
  # auth-status verb).

  # Named executor profiles (ADR-033 / 0343). Each pairs a canonical agent with
  # an explicit model override and an explicit capability tier.
  # `model` is pinned on every entry: omitting it defers to the agent CLI's own
  # default, which drifts with CLI upgrades and account state — pin what your
  # CLI would resolve today (`grok models`, omp config.yml modelRoles.default,
  # ~/.codex/config.toml, ~/.claude.json). The ids below are working examples,
  # not vendor defaults — re-pin for your accounts.
  #
  # Pairing philosophy (operator guidance, 2026-08): a `native` pair — a
  # vendor's model served by that vendor's own CLI (gpt-5.6-* via codex,
  # claude-*-5 via claude, grok-4.6 via grok, gemini-3.x via agy) —
  # consistently outperforms the same model routed through a third-party CLI,
  # and belongs at the capable rungs. `portable` models (glm-5.x,
  # deepseek-v4-*, …) are provider-agnostic and best carried by omp or pi at
  # the cheap/standard rungs. The ladder below mixes both classes on purpose;
  # measure pairings with the history plane before promoting
  # a portable model into a capable rung.
  # Live tiers: cheap | standard | capable-1 | capable-2 | capable-3
  # (1=low quality within capable, 3=high). DECLARE tier — inference never
  # invents capable-2/3. Stage routing starts on the cheapest executor whose
  # tier >= min_tier; same exact tier ties break by array order.
  # Legacy bare `capable` is accepted at runtime as synonym for capable-1.
  executors:
    # cheap rung is optional: with none declared, scribe-role work starts one
    # rung up at the cheapest standard executor. Add one when transcript-heavy
    # mechanical commands (changelog/gitmsg/handover) get noisy on cost.
    # - name: minimax
    #   agent: omp
    #   model: minimax/MiniMax-M3
    #   tier: cheap
    # A disabled profile stays visible to `spur agent doctor` but never routes
    # (no role, team, stage, or explicit selection). Flip back to enabled by
    # removing the line or setting disabled: false.
    # Quota refresh is external: schedule `spur agent usage` (cron/launchd) to
    # capture provider usage and record quota-owned availability observations.
    # spur serve never runs it (B6 0892; docs/design/session-pinned-dispatch.md §3.4).
    # A bare `true` is operator-owned (only humans write booleans). Automatic
    # writers (quota/probe checks) emit the ownership object instead; an
    # operator-owned disable is never auto-re-enabled (B6 0890).
    # - name: retired-profile
    #   agent: omp
    #   disabled: true
    # - name: quota-limited
    #   agent: omp
    #   disabled: { owner: quota, since: 2026-02-14T09:30:00.000Z, reason: agent.quota.exhausted quota-limited }
    - name: pi-dsv4-flash-volc
      agent: pi
    #   tier: standard
      executionCapabilities:
        version: 1
        axes:
          fsRead:
            state: available
            provenance: native-known
          fsWrite:
            state: available
            provenance: native-known
          processSpawn:
            state: available
            provenance: native-known
    # - name: omp-zai
    #   agent: omp
    #   model: zai/glm-5.2
    #   tier: standard
    - name: pi
      agent: pi
      model: "k3"
      tier: capable-1
    - name: grok
      agent: grok
      model: grok-4.6
      tier: capable-2
    - name: claude
      agent: claude
      model: k3
      tier: capable-3
    - name: codex
      agent: codex
      model: gpt-5.6
      tier: capable-3

  # `default-by-phase` is a REMOVED (0452). Prefer
  # executor `tier` + stage-registry model_policy. When present, a matching
  # phase mapping is still authoritative (fail-fast; no fall back to default)
  # and shadows stage routing for that phase.
  # default-by-phase:
  #   dev-run: omp
  #   dev-review: claude

  # A project's agent roster is a **fleet** declared in the project's own
  # `.spur/config.yaml` under `agent.fleet` — never in this global file, and never
  # in the retired `<projectPath>/.spur/fleet.json` (ADR-116 carrier moved by task
  # 0858). Materialized and autostarted at `spur serve` start only when
  # `agent.fleet.enabled` is true; preview with `spur projects list --fleet`.
  # Shape (project layer; a member declares role or executor):
  #   agent:
  #     fleet:
  #       enabled: true
  #       strategy: gtd            # rest | gtd (default rest)
  #       orchestrator: planner-1
  #       members:
  #         - { role: planner, purpose: orchestrator }
  #         - { role: coder, executor: omp }
  #         - { role: reviewer, enabled: false }
  # `enabled: false` keeps the member's derived `<role>-<n>` id index, so later
  # members never silently reallocate ids. The retired per-team config key is no
  # longer read — a leftover block fails the load and names `agent.fleet` as the
  # replacement — a leftover `fleet.json` fails the load too, and the roster is
  # keyed on the worktree path now, not on a config-global team name.

rules:
  paths:
    - .spur/rules/**/*.yaml

workflows:
  paths:
    - .spur/workflows/

redaction:
  enabled: false

# ── Release (spur builder bump-ver / drop-tags) ──
# Optional. Release plumbing knobs for `spur builder bump-ver` / `drop-tags`;
# every value defaults as shown, so an absent section keeps default behavior.
# builder:
#   bump-ver:
#     tagVersionSeparator: "-v"
#     publishWorkflow: publish.yml
#     releaseCommitType: chore
#     releaseCommitScope: release
#     ghRunListLimit: 5

# ── Planning (design §2.1 / §9) ──
tasks:
  active: docs/tasks
  folders:
    docs/tasks:
      baseCounter: 0
      label: Core
  # severity:
  #   L3.plan-format: off   # error | warning | off (known finding codes only)

features:
  dir: docs/features
