{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://harnery.com/schemas/config.schema.json",
  "title": "harnery configuration",
  "description": "Settings file format for harnery. Lives at ~/.config/harnery/config.jsonc (user-global) or <project-root>/.harnery/config.jsonc (project). Project values override user values.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "URL of this schema. Included so editors provide autocomplete + validation."
    },
    "project_name": {
      "type": "string",
      "description": "Project identity surfaced to peers and in log lines. Defaults to the basename of the monorepo root."
    },
    "binName": {
      "type": "string",
      "default": "harn",
      "description": "Host CLI bin name used in agent-facing strings (council prompts, end-of-turn nudges, command help). The coord binaries + web UI run as harnery and read this back since they can't see a consumer CLI's name. Stamped by `harn init` for a consumer. Resolution: HARNERY_BIN env -> this field -> 'harn'."
    },
    "hooksSetupHint": {
      "type": "string",
      "description": "Host-specific command that (re)installs the project's git hooks (e.g. 'scripts/setup-hooks.sh'). Surfaced verbatim in the SessionStart 'commit guard not wired' nudge. harnery doesn't own git-hook installation (each host wires its own pre-commit to invoke `agent-coord verdict`) and the path/command is host-specific, so the host declares it here. Unset -> a generic, host-agnostic hint."
    },
    "agents": {
      "type": "object",
      "additionalProperties": false,
      "description": "Host-owned agent ritual policy. Harnery capabilities stay available when a policy is disabled; automatic prompts and Stop remediation follow these switches.",
      "properties": {
        "requireGitFinalization": {
          "type": "boolean",
          "default": false,
          "description": "Require `agents status --end-turn` instead of plain `agents status` at the end of tool-using turns. The guarded call verifies that session-owned paths are committed and their repositories are pushed. Env override: HARNERY_AGENTS_REQUIRE_GIT_FINALIZATION=1|0."
        },
        "finalizationRoots": {
          "type": "array",
          "default": [],
          "description": "Project-owned authority for guarded writes outside the coordination root. Each exact root declares one end-turn disposition: `git` requires the path to be a Git repository root and applies dirty/remote checks; `output` explicitly accepts intentional non-Git output. Paths may be absolute or relative to the coordination root. The coordination root is implicitly `git` when it is a Git repository. User-global config cannot grant these roots.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["path", "disposition"],
            "properties": {
              "path": {
                "type": "string",
                "minLength": 1
              },
              "disposition": {
                "type": "string",
                "enum": ["git", "output"]
              }
            }
          }
        }
      }
    },
    "coord": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "freshness_seconds": {
          "type": "integer",
          "minimum": 30,
          "default": 600,
          "description": "How stale a peer heartbeat can get before the sweeper prunes it. Env override: HARNERY_AGENT_COORD_FRESHNESS."
        },
        "finalization": {
          "type": "object",
          "additionalProperties": false,
          "description": "Grace periods for converging independent lifecycle observations on the canonical V3 session finalizer.",
          "properties": {
            "archive_grace_seconds": {
              "type": "integer",
              "minimum": 1,
              "maximum": 86400,
              "default": 600
            },
            "idle_observe_seconds": {
              "type": "integer",
              "minimum": 60,
              "maximum": 2592000,
              "default": 259200
            },
            "idle_finalize_seconds": {
              "type": "integer",
              "minimum": 60,
              "maximum": 7776000,
              "default": 604800
            },
            "cascade_grace_seconds": {
              "type": "integer",
              "minimum": 1,
              "maximum": 86400,
              "default": 3600
            },
            "reconcile_interval_seconds": {
              "type": "integer",
              "minimum": 1,
              "maximum": 86400,
              "default": 900
            }
          }
        },
        "run_quality": {
          "type": "object",
          "additionalProperties": false,
          "required": ["mode"],
          "description": "Report-only run-quality observation. Invalid objects disable evaluation as one unit.",
          "properties": {
            "mode": { "type": "string", "enum": ["off", "shadow", "report"], "default": "off" },
            "evaluation_interval_seconds": {
              "type": "integer",
              "minimum": 5,
              "maximum": 3600,
              "default": 30
            },
            "snapshot_ttl_seconds": {
              "type": "integer",
              "minimum": 10,
              "maximum": 86400,
              "default": 120
            },
            "max_tail_bytes": {
              "type": "integer",
              "minimum": 65536,
              "maximum": 67108864,
              "default": 2097152
            },
            "evaluation_timeout_seconds": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 30
            },
            "lock_stale_seconds": {
              "type": "integer",
              "minimum": 5,
              "maximum": 3600,
              "default": 60
            },
            "supervised_roots_per_sweep": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 8
            },
            "thresholds": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "repeated_tool_calls": {
                  "type": "integer",
                  "minimum": 2,
                  "maximum": 10000,
                  "default": 8
                },
                "consecutive_failures": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 10000,
                  "default": 5
                },
                "context_growth_per_minute": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 10000000,
                  "default": 60000
                },
                "compaction_grace_seconds": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 86400,
                  "default": 300
                },
                "no_progress_evaluations": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 10000,
                  "default": 2
                }
              }
            }
          }
        }
      }
    },
    "logs": {
      "type": "object",
      "additionalProperties": false,
      "description": "Structured-log settings. Retention overrides change only approved byte and age scalars; roots, providers, privacy, durability, writers, and record limits stay source-owned.",
      "properties": {
        "storage": {
          "type": "object",
          "additionalProperties": false,
          "description": "Storage budgets for shared operational and debug log segments. Project values override user-global values at the same path; exact-family values are more specific than class values.",
          "properties": {
            "classes": {
              "type": "object",
              "additionalProperties": false,
              "description": "Defaults applied to every shared log family in the named storage class.",
              "properties": {
                "operational-log": { "$ref": "#/$defs/logStorageOverride" },
                "debug-log": { "$ref": "#/$defs/logStorageOverride" }
              }
            },
            "families": {
              "type": "object",
              "description": "Overrides for exact registered shared-log family IDs.",
              "propertyNames": {
                "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$"
              },
              "additionalProperties": { "$ref": "#/$defs/logStorageOverride" }
            }
          }
        }
      }
    },
    "artifacts": {
      "type": "object",
      "additionalProperties": false,
      "description": "Defaults for repository-local working artifacts under .harnery/artifacts/.",
      "properties": {
        "default_retention_days": {
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 3650,
          "default": 3,
          "description": "Retention for `harn artifacts create` when --days is absent. Env override: HARNERY_ARTIFACT_RETENTION_DAYS."
        }
      }
    },
    "web": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "port": {
          "type": "integer",
          "minimum": 1024,
          "maximum": 65535,
          "default": 4276,
          "description": "Port that `harn web up` binds to."
        },
        "bind": {
          "type": "string",
          "default": "127.0.0.1",
          "description": "Bind address. Default localhost-only. Override with --bind 0.0.0.0 + gated IP allowlist to expose."
        }
      }
    },
    "files": {
      "type": "object",
      "additionalProperties": false,
      "description": "Universal file viewer (web dashboard) path-serving policy. The shipped secret denylist is a baked-in, non-removable floor in code; these knobs are ADDITIVE ONLY (they extend the floor, never replace or shrink it).",
      "properties": {
        "deny_globs": {
          "type": "array",
          "items": { "type": "string" },
          "default": [],
          "description": "Extra deny patterns on top of the shipped floor. Must be `**/`-anchored; supported shapes are `**/SEG` and `**/SEG/**` where SEG may use `*` and one `{a,b}` alternation. Matched case-insensitively against the canonical repo-relative path. Operator-added denies are treated as hard tier (never overridable). An entry that fails to compile disables the file viewer entirely (fail-closed) until fixed."
        },
        "allow_overrides": {
          "type": "array",
          "items": { "type": "string" },
          "default": [],
          "description": "Rescue globs consulted ONLY when a deny came from a soft-tier floor pattern (the deliberately over-broad nets like `**/.env.*` or `**/*secret*.json`). Hard-tier matches (credentials, key/cert files, `.ssh`/`.aws`/`.gnupg`, SA-key/oauth/token JSON, `.git`) are never overridable; an override that names a hard family is loudly dropped at config load."
        }
      }
    },
    "tools": {
      "type": "object",
      "additionalProperties": false,
      "description": "Managed-tool provisioning consent.",
      "properties": {
        "ripgrep": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "autoInstall": {
              "type": "boolean",
              "default": false,
              "description": "Allow `grep` to download the pinned, checksum-verified ripgrep into the harnery tools dir on first miss. Env override: HARNERY_TOOLS_AUTOINSTALL=1|0."
            }
          }
        }
      }
    },
    "workflow": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "subscriptionOnly": {
          "type": "boolean",
          "default": false,
          "description": "Pin `workflow run` to subscription billing: API-key vars are scrubbed from every child env. Stamped true by `harn init` when the key is absent. Env override: HARNERY_WORKFLOW_SUBSCRIPTION_ONLY=1|0."
        }
      }
    },
    "skills": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "exclude": {
          "type": "array",
          "items": { "type": "string" },
          "default": [],
          "description": "Shipped Harnery skills this repo suppresses. Installed skill IDs are harn-decide, harn-council, and harn-end."
        }
      }
    },
    "hooks": {
      "type": "object",
      "additionalProperties": false,
      "description": "Optional project-owned hook extensions. User-global values cannot enable project executables.",
      "properties": {
        "promptContext": {
          "type": "object",
          "additionalProperties": false,
          "required": ["enabled"],
          "description": "Run scripts/hooks/harness/extensions/prompt-context through the normalized prompt hook. Disabled by default and project config only.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false
            },
            "timeoutMs": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120000,
              "default": 15000
            },
            "maxOutputBytes": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4194304,
              "default": 65536
            }
          }
        }
      }
    },
    "instructions": {
      "type": "object",
      "additionalProperties": false,
      "description": "Agent-facing instructions harnery manages in AGENTS.md.",
      "properties": {
        "hostAddendumFile": {
          "type": "string",
          "description": "Repo-relative path to a markdown file holding this project's own coordination policy. `harn init` splices its contents into a second managed region in AGENTS.md, `init --check` reports drift against it, and `deinit` (or removing this key) takes the region back out. Harnery never parses the content. Must resolve inside the project and be non-empty."
        },
        "promptReminder": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500,
          "pattern": "^[^\\r\\n]*\\S[^\\r\\n]*$",
          "description": "One project-owned line emitted on every supported prompt for adapters that lack native reminder behavior. Project config only; user-global values are ignored. The text is not copied into coordination state."
        }
      }
    },
    "presence": {
      "type": "object",
      "additionalProperties": false,
      "description": "Cross-machine session presence (decision record 0016).",
      "properties": {
        "enabled": {
          "type": "boolean",
          "default": true,
          "description": "Presence over git refs (refs/harnery/presence/<machine> on origin). On by default when an origin remote exists; fail-silent everywhere. Env override: HARNERY_PRESENCE=1|0."
        },
        "relay": {
          "type": "string",
          "description": "Optional live-socket upgrade: a wss:// relay URL (public reference: wss://relay.harnery.com; self-host via `harn relay serve` or relay/worker/). Hooks keep a per-machine daemon connected; git refs stay on as the floor. Env override: HARNERY_PRESENCE_RELAY=<url|0>."
        }
      }
    },
    "backup": {
      "type": "object",
      "additionalProperties": false,
      "description": "`harn backup` (restic) defaults. Env overrides: HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE. Set backup.schedule to run a non-blocking, freshness-gated snapshot from SessionStart.",
      "properties": {
        "repo": {
          "type": "string",
          "description": "restic repository path or URL. Default: ~/.cache/harnery/restic-repo. Common remote form: 'rclone:gdrive:harnery-backup/<project>'."
        },
        "password_file": {
          "type": "string",
          "description": "File holding the restic repo password. Relative paths resolve from the coordination root. Default: ~/.config/harnery/restic-password (autogenerated on `harn backup init`)."
        },
        "include": {
          "type": "array",
          "default": [],
          "items": { "type": "string", "minLength": 1 },
          "description": "Extra .harnery-relative paths to add to the catalog-derived default snapshot set."
        },
        "exclude": {
          "type": "array",
          "default": [],
          "items": { "type": "string", "minLength": 1 },
          "description": "Catalog family IDs or .harnery-relative paths to remove from the snapshot set."
        },
        "max_bytes": {
          "type": "integer",
          "minimum": 1,
          "default": 52428800,
          "description": "Maximum logical bytes selected by snapshot before --allow-large is required."
        },
        "schedule": {
          "type": "object",
          "additionalProperties": false,
          "required": ["if_stale"],
          "description": "Optional SessionStart snapshot schedule. Absence disables scheduled snapshots.",
          "properties": {
            "if_stale": {
              "type": "string",
              "pattern": "^[1-9][0-9]*(ms|s|m|h|d)$",
              "description": "Run only when this host's newest snapshot is older than the duration, such as 24h."
            },
            "tags": {
              "type": "array",
              "default": [],
              "items": { "type": "string", "minLength": 1 }
            }
          }
        },
        "keep_daily": {
          "type": "integer",
          "minimum": 0,
          "default": 7,
          "description": "Daily snapshots to keep on `harn backup prune` (overridable per run with --keep-daily)."
        },
        "keep_weekly": {
          "type": "integer",
          "minimum": 0,
          "default": 4,
          "description": "Weekly snapshots to keep on `harn backup prune` (overridable with --keep-weekly)."
        },
        "keep_monthly": {
          "type": "integer",
          "minimum": 0,
          "default": 6,
          "description": "Monthly snapshots to keep on `harn backup prune` (overridable with --keep-monthly)."
        }
      }
    },
    "sync": {
      "type": "object",
      "additionalProperties": false,
      "description": "`harn sync` (rclone) defaults for the curated cross-machine subset (identities/, journal/archived/, councils/). Env overrides: HARNERY_SYNC_REMOTE, HARNERY_SYNC_PREFIX. `harn sync init` also records these in ~/.config/harnery/sync.json (a lower-precedence fallback than this section).",
      "properties": {
        "remote": {
          "type": "string",
          "description": "rclone remote name (e.g. 'gdrive'). Set up the remote with `rclone config`; harnery doesn't wrap the OAuth flow."
        },
        "prefix": {
          "type": "string",
          "default": "harnery",
          "description": "Subpath under the rclone remote root that holds the synced subset."
        }
      }
    }
  },
  "$defs": {
    "logStorageOverride": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "max_bytes": {
          "type": "integer",
          "minimum": 10485760,
          "maximum": 1099511627776,
          "description": "Logical storage budget in bytes. This is an asynchronous retention budget, not a producer write quota."
        },
        "max_age_days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 3650,
          "description": "Age in whole days after which an immutable sealed segment is eligible for retention."
        }
      }
    }
  }
}
