// Every key of ~/.pi/agent/extensions/pi-automode-classifier/config.json, set to its default.
// The config file is plain JSON: drop these comments, and keep only the keys you change. A key
// you leave out keeps its default. A config with any error is ignored as a whole.
{
  // Starting state of each session. `/automode-classifier on|off` changes it for that session.
  "enabled": true,
  // Append every decision to ~/.local/state/pi-automode-classifier/decisions.jsonl.
  "log": true,

  // Your own patterns over one simple command, `*` matches anything, for example
  // "terraform destroy *" or "gh pr view *". They replace the built-in verdict, except a
  // built-in deny and the ask for this extension's own files.
  "rules": {
    "deny": [],
    "ask": [],
    "classify": [],
    "allow": []
  },
  // Extra directories where file changes are allowed, like the working directory, for example
  // "~/.cache/my-builds".
  "allowedPaths": [],
  // More files or directories that count as this extension's own: changes to them always ask.
  "guardedPaths": [],

  // A confirm prompt nobody answers is treated as a Block after this long, so an unattended
  // session does not stop there forever. 0 waits forever.
  "promptTimeoutMs": 60000,

  // When there is no UI to show a prompt (print mode, most subagents).
  "withoutUi": {
    // A call that needs a prompt: "block" or "allow". Root, credential reads, downloaded code and
    // this extension's own files are blocked either way, unless one of your rules matches.
    "onAsk": "block",
    // Added to `rules` when there is no UI, for example "cargo test *" or "git push *". A deny
    // here wins over an allow in `rules`.
    "rules": {
      "allow": [],
      "deny": []
    }
  },

  // Who judges the calls the rules leave open, in the order they run:
  // - ["llm"]: the chat model.
  // - ["command"] or ["classifier"]: a classifier scores each call from 0 to 1. A score at or
  //   above askAbove asks you, a lower score runs.
  // - ["command", "llm"]: the classifier scores first. Below allowBelow the call runs, at or
  //   above askAbove it asks you, and in between the chat model decides.
  // "llm" must be last. []: every such call asks you. Not set: ["command"] if command.argv is
  // set, ["classifier"] if the file has a `classifier` block, ["llm"] otherwise.
  "decider": ["llm"],

  // A chat model, in Claude Code auto mode style. It judges with the user's messages and the
  // session's tool calls, and also gets the asks, except root, credential reads, downloaded code,
  // your ask rules, and changes to this extension's or Pi's own files.
  "llm": {
    // Pi provider and model id, for example "llamastash-z13" and "flash-next-128k". Both
    // empty: the session's own model.
    "provider": "",
    "model": "",
    // Name in the log and in `/automode-classifier status`. Empty: the model id.
    "name": "",
    // Override the Pi catalog entry. 0 keeps the catalog value.
    "contextWindow": 0,
    // Reply budget of the reasoning stage, in tokens.
    "maxTokens": 2048,
    // off, minimal, low, medium, high, xhigh or max. "off": neither stage thinks. Stage 1 never
    // thinks.
    "thinkingLevel": "low",
    // One throwaway call at session start so the 25k-token prompt is cached before the first
    // decision. Worth it on a local model; on a model you pay per token it costs a call per session.
    "warmCache": false,
    // A quick stage 1 that errs toward blocking, and reasoning only for what it flags.
    "twoStage": true,
    "stage1MaxTokens": 128,
    // Per stage, so a call that needs both stages can take twice this.
    "timeoutMs": 60000,
    // No answer (down, timeout, no verdict): "block", or "ask" with the confirm prompt.
    "onNoAnswer": "block",
    // A block: "block" denies and tells the agent to find a safer way, "ask" shows the prompt.
    "onBlock": "block",
    // After this many denials in a row, or in the session, you are asked instead.
    "maxConsecutiveDenials": 3,
    "maxTotalDenials": 20,
    // What the decider reads. Assistant text and tool output are left out, as in Claude Code.
    "transcript": {
      "maxEntries": 60,
      "maxChars": 24000,
      "toolInputChars": 1200,
      "userMessageChars": 4000,
      "includeAssistantText": false,
      "includeToolResults": false
    },
    // Rule lists of the decider's prompt, as text. "$defaults" stands for Claude Code's list, so
    // ["$defaults", "Restarting the shared LlamaStash daemon"] adds one rule after it.
    "prompt": {
      "environment": ["$defaults"],
      "hardDeny": ["$defaults"],
      "softDeny": ["$defaults"],
      "allow": ["$defaults"]
    }
  },

  // A decision model that scores each command. It never blocks: a risk at or above `askAbove`
  // gets a prompt.
  "classifier": {
    // Local server (Kev, Laya), used when `provider` is empty.
    "endpoint": "http://127.0.0.1:11438",
    // Started when nothing listens on `endpoint`, for example
    // ["systemctl", "--user", "start", "pi-automode-classifier.service"]. Empty: start nothing.
    "startCommand": [],
    // A model through Pi instead, for example "typesafe" with model "jev-1.13". A provider Pi lists
    // no such classifier for, such as LlamaStash serving LANCET, gets the request at
    // <its baseUrl>/systemone, with its own API key.
    "provider": "",
    // Local server: part of the model name it must report. With `provider`: the model id in Pi.
    "model": "kev",
    "timeoutMs": 5000,
    // 0 asks for every classified call.
    "askAbove": 0.2,
    // Only used when another decider comes after this one: a score below this runs, and a score
    // from this up to askAbove goes to the next decider.
    "allowBelow": 0.1,
    // The question the model answers, and what its yes and no mean.
    "question": "Is this command risky?",
    "risky": "Deletes data, touches files outside the project, changes the system, uses credentials, or sends data to a remote server",
    "safe": "Routine development work that only reads, builds, tests or edits files inside the project directory",
    // 2 to 10 levels, least risky first, to ask for a score instead of yes/no. Empty: yes/no.
    "levels": [],
    // More questions, by id. Each adds about one model call. A value is a bare question string,
    // or {"question", "yes", "no", "askAbove"}.
    "extraQuestions": {},
    // How much of the session the model is told about, in characters: the user's own message and
    // the calls before this one, as the chat decider sees them. 0: the command alone.
    "contextChars": 0,
    // "state" sends that text as its own `context` key beside the command, which a free-form
    // server such as Jev reads. "command" folds it into the command text, which is the only way a
    // server that reads just the command can see it. Either way it is untrusted content.
    "contextIn": "state"
  },

  // A classifier that runs as a process, like LANCET's classify.py. It reads a JSON line per
  // command on stdin and answers with a `score` from 0 to 1.
  "command": {
    // argv, for example ["~/.local/share/lancet/venv/bin/python", "classify.py"].
    "argv": [],
    "timeoutMs": 5000,
    "askAbove": 0.2,
    // Only used when another decider comes after this one: a score below this runs, and a score
    // from this up to askAbove goes to the next decider.
    "allowBelow": 0.1
  }
}
