Metadata-Version: 2.4
Name: brindle
Version: 0.3.17
Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
Project-URL: Homepage, https://pawdelta.com/brindle/
Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
Project-URL: Changelog, https://pawdelta.com/brindle/changelog
Project-URL: Support, https://pawdelta.com/brindle/docs/faq
Author: Hannah Lerner
License-Expression: LicenseRef-brindle-1.0
License-File: LICENSE
License-File: SCHEDULE-A
Keywords: agents,ai,claude-code,codex,git,mcp,orchestration,tmux,worktree
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.11
Requires-Dist: cryptography>=42
Requires-Dist: mcp<3,>=2.2
Requires-Dist: packaging>=24
Requires-Dist: typer>=0.12
Description-Content-Type: text/markdown

# brindle

*[pawdelta.com/brindle](https://pawdelta.com/brindle/) · Published on PyPI as `brindle`; the command is `brindle`. This project is
unrelated to the Brindle desktop app at brindle.dev.*

A supervisor for your coding agents that won't call work done until it's proven.
brindle runs Claude Code, Codex and open-weight models side by side, each in its own
git worktree. A supervisor agent splits up the work and hands it out. Each branch
merges only after a reviewer approves
that exact commit and your checks pass. A goal is done only when its check commands
exit 0.

```sh
curl -fsSL pawdelta.com/brindle/install | sh
brindle demo        # watch it finish a practice repo in a few minutes
```

![brindle demo: a supervisor splits a goal between two workers, each branch is reviewed and merged, and both milestones turn green once their checks pass](https://pawdelta.com/brindle/brindle-demo.gif?v=0.0.2)

*`brindle demo`, recorded on brindle 0.0.2 (sped up 4×): two workers in parallel, each branch reviewed and merged, both milestones verified by their check commands.*

- **Done means a command passed.** A goal is split into milestones, each with a
  check command that brindle runs itself. A milestone is verified when its check exits
  0, re-run after later merges to catch regressions, never when a model says so.
- **Reviewed before it merges.** A reviewer approves the exact sha, then
  pre-commit and your `checks` run. Codex reviews Claude's work (or a local model
  does) when available. Nothing reaches your default branch on its own.
- **Parallel agents, no collisions.** Every worker gets its own worktree and
  branch, cut from a freshly fetched base, with its own block of ports and
  optional per-worktree databases (brindle Pro).
- **Delegation built in.** Agents get a `brindle` MCP server: `assign` work to
  parallel workers with the files they'll touch and what they depend on, `handoff`
  and wait, `send_message`, then review and merge without spending the supervisor's turns.
- **Walk away.** Autopilot keeps going until every milestone is verified, pauses
  at your usage limit and resumes after the reset, and asks you only when it
  needs a decision. Quit and `brindle continue` later; workers resume too.
- **Any model, including local ones.** Claude Code, Codex, Google Antigravity, or
  brindle's own agent loop over Ollama or any OpenAI- or Anthropic-style endpoint.
- **Reliable status.** brindle knows whether each agent is working, idle, or
  waiting for your approval from the agent's own lifecycle hooks, not by
  scraping the terminal.

## Install

```sh
curl -fsSL pawdelta.com/brindle/install | sh
```

The script ([scripts/install.sh](scripts/install.sh)) installs uv and tmux if
they're missing, then brindle, then runs `brindle doctor`. Run it again to upgrade.
Session starts check PyPI at most once a day and show available updates in the status
bar and `brindle doctor`; set `"auto_update": true` in `~/.brindle/config.json` to
upgrade in the background (restart to use it). Checks skip local and editable
installs and can be disabled with `BRINDLE_NO_UPDATE_CHECK`.
To do it by hand:

```sh
brew install tmux                             # Debian/Ubuntu: sudo apt install tmux
uv tool install brindle
uv tool install --editable ~/Projects/brindle   # or from a local checkout
```

brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).

`brindle --version` prints the installed version; the tmux status bar of every brindle
session shows it too (`brindle 0.3.17`). A session started before an upgrade keeps
running the old code, and shows the old number, until you restart it.

## Quick start

```sh
cd ~/code/myapp
brindle init      # once per repo
brindle
```

`brindle init` reads your lockfiles and manifests (uv, Poetry, npm/pnpm/yarn/bun,
Cargo, Go, Bundler, a Makefile) and writes `.brindle/config.json` with the
`setup` a new worktree needs, the `checks` that must pass before a branch merges,
and the git-ignored env files to `copy` into each worktree. It shows what it found
before writing, never touches an existing config, and finishes with the same checks
as `brindle doctor`. Commit the config so your team gets it too.

New to brindle? `brindle demo` runs it on a tiny practice repo (under `~/.brindle/demo/`)
with two failing test files and a two-milestone goal: in a few minutes you watch two
workers fix them in parallel, each branch get reviewed and merged, and both milestones
turn green only once their test command passes. `brindle demo --local` keeps the workers
and reviewers on a local model (Ollama).

`brindle doctor` checks that everything brindle needs is there (tmux, the agent CLIs,
a writable home) and, in a repo, what's set up for it, and says what to do about
anything missing. Bare `brindle` runs the essential ones (tmux, the chat's CLI)
before it launches anything.

That's it. `brindle` opens a supervisor chat (Claude Code) in your repo, with a
narrow sidebar on the left showing every agent: who's working, who's idle, and
who's waiting for your approval. Tell the supervisor what you want. It splits the
work between workers, each on its own branch, then reviews and merges their
branches. It starts in under a second.

**The sidebar follows you.** There's one sidebar pane per session root, not
one per window: switch to any other brindle window or session (⏎ in the
sidebar, `brindle attach`, prefix-L back, clicking a pane) and it relocates
there too, always beside whatever you're looking at, never spawning a second
dashboard. Scroll it
with the mouse wheel, PageUp/PageDown, or Home/End when there's more than fits;
moving the ↑↓ selection scrolls to keep it in view.

The follow behavior uses the session-scoped `session-window-changed` and
`client-session-changed` tmux hooks. To turn it off for an existing session,
run `tmux set-hook -u -t <session> session-window-changed` and
`tmux set-hook -u -t <session> client-session-changed`; `-t <session>` matches
the scope brindle uses when installing them. Starting a new brindle session installs
the hooks again. Without them, the sidebar stays in its current window when you
switch windows or sessions; you can still move it manually or use `brindle attach`
to choose a brindle window.

**Sidebar keys.** `?` shows them in the sidebar too. ◆ marks an agent that needs
you: stuck on a prompt, a supervisor with a question, or (without autopilot) a
worker whose branch is waiting for your review. Under autopilot, a reported worker shows
a dim ◇ instead, because the supervisor reviews it.

**Copying chat text.** Drag with the mouse in the chat: the selection stays inside
that pane and is copied to the system clipboard (`pbcopy`, `wl-copy` or `xclip`;
`brindle doctor` shows which); a double or triple click copies the word or line.
tmux keeps the mouse in the chat even though Claude Code asks to track it, so a
click only focuses the pane (and can't park Claude Code's focus in a dialog where
Esc stops working); the wheel still scrolls the chat. If Claude Code's Settings
dialog (`/usage`, `/config`) ever ignores Esc, its filter box has the focus: press
Esc up to three times (clear, leave the box, close), or `↓` then Esc. Don't reach
for Ctrl-C twice, which quits Claude Code and pauses the session.

**Hiding the sidebar.** `h` in the sidebar hides it without quitting brindle
(the chat zooms; `Ctrl-b S` brings it back), and `Ctrl-b z` zooms the chat by hand.
If the sidebar got left behind in another tmux session, `Ctrl-b S` in a window
without one pulls it here, as does `brindle sidebar` (it restarts a sidebar that was closed).
To keep the dashboard below the chat instead, set `"sidebar": "bottom"` in
`.brindle/config.json`.

| Key | |
|---|---|
| `↑↓ j k` | move |
| `PgUp/Dn` | page |
| `Home/End` | top / bottom |
| `⏎ a` | open the agent |
| `p` | peek at its screen |
| `x` | close (2× if busy) |
| `n` | next needing you |
| `Spc Tab` | fold group |
| `/` | filter, Esc clears |
| `r` | refresh |
| `?` | this help |
| `q` | quit |

When an agent uses Claude Code's own Agent tool, its built-in subagents (Explore,
Plan, ...) show up nested underneath it in the sidebar too, e.g. `↳ Explore ·
running 1m`, so you can see what it's fanned out to without leaving brindle.

**Closing and coming back.** When you quit the supervisor's chat, or press `q` twice
in the sidebar (or `x` twice on your own supervisor), the brindle window closes cleanly
and you're back at your prompt. The whole session is paused: its
workers stop too, and everything is kept (branches, worktrees,
queued messages, and each agent's Claude conversation). `brindle continue` (or
`brindle -c`) picks up the most recent paused session and lists the others by id
(`brindle continue <id>`). Plain `brindle` always starts fresh. If a session is still running in that folder, it asks first: open that one, start the new one in its own worktree (branch `brindle/session-N`, cut from what you have checked out, so they run at once without sharing files), or pause it and start fresh. There's no limit: each further `brindle` there can start another session in its own worktree (`brindle/session-2`, `-3`, ...). Without a terminal to ask in, it pauses the old one. `brindle sessions` lists
what's paused. brindle keeps the newest 3 paused sessions per repo for up to 7 days;
cleanup never merges anything or deletes an unmerged branch, and worktrees with uncommitted
changes are kept.

**Not in a git repo?** `brindle` still works. It starts a *scratch session*: a
fresh git repo under `~/.brindle/scratch/`, and nothing is created in the folder you ran
it from. When the work belongs in a real repository, run `brindle transfer ~/path/to/repo`
(or ask the supervisor). The commits land on a new branch there, ready to review and
merge. Running `brindle` in a repo also offers to bring in any scratch work that hasn't
been moved yet.

Or give a supervisor its own workspace:

```sh
brindle new fix-login -p "Fix the login redirect bug; add a test"
brindle ls                                    # workspaces, agents, ahead/behind
brindle diff fix-login --stat
brindle pr fix-login                          # push + gh pr create
brindle rm fix-login                          # deletes the branch only if it's merged (-K keeps it)
```

## Recommended use

brindle pays off when work splits into pieces that can proceed in parallel, or
runs long enough that you want it reviewed and merged without babysitting. For a
one-line fix, plain `claude` is quicker; the supervisor will also just do small
things itself instead of starting workers.

1. **Start in the repo, on a clean base.** Commit or stash first: workers branch
   from the supervisor's committed work, not your uncommitted edits.
2. **Give it a goal with checks.** For anything bigger than one sitting, tell the
   supervisor what "done" means in commands (a test file, `npm test`, a build), or
   write `.brindle/goals.md` yourself. Milestones with real checks are what let
   autopilot keep going without you and stop when the work is actually done.
3. **Set `checks` in `.brindle/config.json`** (usually your full test suite) so no
   branch merges red, and a `setup` if new worktrees need `npm install` or similar.
4. **Watch the sidebar, not every window.** It flags agents waiting on you (◆);
   `n` jumps to the next one, ⏎ opens it, `p` peeks, `?` lists every key. Workers that need a
   decision surface through the supervisor's `need_user`.
5. **Step away freely.** Quit the chat to pause everything; `brindle continue`
   resumes the session, workers included. `brindle history` shows what ran, what
   merged and what it cost.
6. **Clean up.** Finished workers close on their own after `stale_after` minutes,
   and `brindle` sweeps leftover processes at start. `brindle close --exited` and
   `brindle prune` do it on demand. Branches are never deleted for you.

For a large task: write the milestones first (each with a check), keep each worker's
task to one area of the code with its own test command, and let the supervisor run up
to `max_agents` workers at once. Use a cheaper profile (see [Cheap workers](#cheap-workers))
for mechanical edits and a Codex reviewer for a second model's opinion.

## Autopilot

`brindle` starts the supervisor with autopilot on. Tell it what we're building,
or put the goal in `.brindle/goals.md`, and it works like a project manager:

1. **Goal and milestones.** The goal is split into milestones, and each one has
   a check command that brindle runs itself. A milestone is done only when its
   check exits 0, so progress is verified, not just claimed. A milestone may also name a
   `profile`: the worker profile to use for its tasks, so a small milestone can
   run on a cheaper profile. `assign` and `handoff` called without an
   `agent_profile` use the first unverified milestone's profile, else the repo's
   `default_agent`.
2. **Workers in parallel.** The supervisor splits each milestone into tasks
   and starts workers on their own branches, up to `max_agents` at once.
   Claude workers run the task as a Claude Code `/goal` with a finish line, so
   they keep going until it's met. `assign`/`handoff` take `files` (the
   paths/globs a task expects to touch, checked for overlap against other
   active workers' declared and actually-changed files; refused by default,
   a warning with `"overlap": "warn"`) and `depends_on` (earlier tasks, by agent id or branch, that must
   merge first): a task with unmet dependencies is queued instead of started,
   and starts automatically, cut from the updated base, once
   `merge_workspace` resolves them. `list_tasks` shows what's queued.
   `assign`/`handoff` also take `plan_first` (default: the `plan_first` config
   key): the worker reads the code, then calls `submit_plan` with a short plan
   and waits. The plan reaches the supervisor as a message; `approve_plan`
   approves it, or (`approved=false`, with feedback) sends it back for a
   revision. For Claude workers brindle's PreToolUse hook refuses Edit, Write
   and NotebookEdit until the plan is approved; other CLIs are only told to
   wait. Autopilot doesn't count a worker waiting on approval as stalled, and
   reminds the supervisor about plans awaiting a decision.
3. **Gated merges.** A branch merges only when everything is committed, a
   reviewer agent has approved that exact commit, your pre-commit hooks pass,
   and your `checks` pass. brindle runs these itself before `merge_workspace`,
   and caches a clean commit's passing result so it isn't re-run for every
   review and merge attempt at the same sha. `request_review` starts the
   reviewer immediately and runs `checks` in the background, delivering a
   pass/fail summary (output only for failures) as a message once they
   finish, instead of asking the reviewer to run the whole suite itself.
   `request_review` picks the reviewer profile itself unless you pass one: an
   explicit `profile` argument, else `review_profile` in the repo config,
   else `reviewer` (the repo's `reviewer` setting). brindle never picks a
   different model on purpose. With hosted learning on, the learner may
   choose `reviewer-codex` or `reviewer-local` instead when they can run here
   and its evidence says they review this kind of work better.
4. **It keeps going.** If the supervisor stops while milestones are still
   unverified and no worker is running, brindle tells it to continue. It stops
   when every check passes, when it needs a decision from you, after three
   reminders with no progress, or when your Claude usage nears its limit.

The sidebar shows the goal, each milestone (✓ verified, ✗ failing, ○ not
checked yet), and anything that needs you.

```markdown
<!-- .brindle/goals.md -->
# Settings page

## Settings API
check: uv run pytest tests/test_settings_api.py -q

## Settings UI
check: npm test -- settings
profile: developer-cheap
```

A session that loaded its goal from `goals.md` writes each milestone's status
back to that file after every check, as a line under the milestone, for
example `status: passed at abc1234 (2026-09-29)` (`passed`, `failed` or
`pending`, a short commit sha and a date). The rest of the file is left byte
for byte as you wrote it. Since `goals.md` may be committed, it stays free of
session data: no agent or session ids, check output, notes, usage or
questions ever go in. A status line is information only: a new session
starts every milestone as pending and re-runs the checks, never trusting it.
A session stops writing when it's handed over, paused, or has autopilot off,
or once its goal was replaced from the chat. From a linked worktree, it writes
the `goals.md` it loaded (the main checkout's), never another session's. A
write that fails never fails the check.

`brindle autopilot` shows progress, `brindle autopilot check` runs the checks
now, and `brindle autopilot off` (or `on`) hands the wheel back (or takes it
again). `brindle --no-autopilot`, or `"autopilot": false` in the repo config,
starts without it.

When your Claude usage reaches `usage_limit`, autopilot pauses for usage: its
running Claude workers stop (worktrees, branches, queued messages and sessions
are kept) and the sidebar says "paused for usage until <time>". Once the usage
window resets, brindle restarts those workers on its own, sets autopilot running
again and tells the supervisor what it resumed. Speaking to the supervisor
doesn't end the pause.

To track your Claude usage, brindle gives the agents it launches a status line.
It records the usage percentage Claude Code reports, then prints whatever
your own status line prints, so what you see doesn't change.

## Commands

| | |
|---|---|
| `brindle` | a fresh supervisor chat here, dashboard alongside |
| `brindle supervisor` | the same, with `--branch`/`--worktree` to run it in another worktree |
| `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
| `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
| `brindle new BRANCH [-b BASE] [-p PROMPT] [--no-agent]` | worktree + branch + a supervisor in it |
| `brindle continue [ID]` / `brindle -c` | resume a paused session (default: the most recent) |
| `brindle sessions` / `brindle prune` | list paused sessions / apply retention rules, remove merged or missing worktrees, close stale stopped agents whose work is merged or gone, and remove leftover tmux sessions |
| `brindle start [-p PROMPT] [--no-watch] [--no-autopilot] [-b BRANCH] [-w PATH]` | the same, with options; `-b`/`-w` run it in that branch's worktree (created if needed, or the one you already made), which gets the repo's `.brindle` config |
| `brindle handover --to BRANCH\|PATH [-n NOTE]` | hand the session to a new supervisor there: goal and milestones, workers, queued tasks and your note move across; the old one is paused |
| `brindle autopilot [on\|off\|check]` | the goal's progress; turn autopilot on or off; run the checks now |
| `brindle audit-decision FINDING [-r RATIONALE]` | record acceptance of an audit finding with rationale so subsequent audits respect it |
| `brindle delegation [conservative\|balanced\|fast]` | how readily the supervisor delegates: fewest tokens, the default, or quickest |
| `brindle transfer [REPO] [--from SESSION] [-b BRANCH]` | move a scratch session's work into a real repo |
| `brindle ls [--all]` | workspaces and agents |
| `brindle repo add PATH [--name ALIAS] / rm ALIAS / ls` | attach other local repos to the current session so workers can go there (brindle Pro; see "Several repos in one session") |
| `brindle keys set/unset/list [NAME]` | store model API keys (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) once in the OS keychain (or a 0600 file); every agent pane gets the ones its provider reads, unless you already export it. `set` reads the value from a hidden prompt or stdin, `list` shows names and the last 4 characters. `CLAUDE_CODE_OAUTH_TOKEN` (a Claude subscription token) is never stored |
| `brindle history [--limit N] [--kind K] [--all]` | durable log of worker results, reviews, merges and milestone checks |
| `brindle cost [--days N] [--all]` | dollar spend at list prices over the last 30 days, by model |
| `brindle cost report [--days N] [--all]` | Pro: spend by day, profile and goal, cost per merged branch, review pass rate per worker profile |
| `brindle history --share [--session ID]` | a few lines about this session to paste into Slack or a post: goal, milestones verified, workers, merges, reviews (and how many by a different model), parallel speedup, tokens |
| `brindle permissions list / check / suggestions / accept / allow / deny / forget / reset` | the rules brindle answers workers' permission requests with, and what it suggests from your approvals (see "Permission policy") |
| `brindle permissions install-codex-hook [--yes]` / `brindle permissions sync-agy` | trust brindle's Codex permission hook now (brindle does it itself when needed); copy your rules into Antigravity's settings (see "Permission policy") |
| `brindle cost request --usd N --reason "..." [--goal TEXT]` | Enterprise (cost centers): ask an admin to approve spend over your budget. A budget refusal points here. Once approved, this month's limit (or the named goal's) goes up by the approved amount, for the month it was approved in |
| `brindle cost requests` | Enterprise: your approval requests and their status; checks pending ones with the server (the cull pass also does, every few minutes). With cost centers, spend events also carry the repo (`owner/name`) so the org can total spend per center |
| `brindle cost estimate [--tasks N] [--weight W] [--profile P] [--reviewer R]` | what the current goal (or N tasks) is likely to cost, as a range from this repo's history (median to p80 of past worker and review runs), plus a cheaper alternative with a different reviewer; says "low confidence" when there's too little history. Also added to `set_goal` and `assign` replies (brindle Pro) |
| `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
| `brindle learning seed` | Pro: sends this repo's finished routing history to the hosted learner once, so it starts with what this machine has seen. Only the usual coarse record fields leave (kind/size one-hots, counters, HMAC keys), never task text or paths. A second run sends nothing new; decisions already sent live are skipped |
| `brindle account [login\|logout\|status\|upgrade\|portal\|seats\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
| `brindle login` | the same as `brindle account login`; when your org's policy has an `agent_setup` block it also signs you in to the Claude and Codex route the company chose (AWS SSO profile, `gcloud`, `az`, `claude auth login`, `codex login`), stores the Claude Code environment for brindle's panes in `~/.brindle/config.json`, and checks the pinned models. `brindle doctor --fix` does the same |
| `brindle audit verify\|export\|pubkey\|ship\|prune` | the local tamper-evident audit log, and shipping it to your SIEM (brindle Enterprise; see "Audit log" below) |
| `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
| `brindle sidebar` | bring this session's sidebar into the tmux session you're in (also `Ctrl-b S` in a window without one); restarts it if it was closed |
| `brindle attach / cd / open [WS]` | tmux session (at the agent waiting on you, else the busiest or newest) / path / editor |
| `brindle status / diff [--stat] [WS]` | compared with the base branch (committed + uncommitted) |
| `brindle sync [--merge] [WS]` | rebase (or merge) the latest base into the branch |
| `brindle commit / push / pr [WS]` | commit everything (`-m MSG`), push with upstream, open a PR |
| `brindle merge [--squash] [WS]` | merge into the base locally |
| `brindle rm WS [-f] [-D]` | remove the worktree; `-D` deletes the branch too, only if merged unless `-f` |
| `brindle doctor` | check that brindle has what it needs (tmux, the agent CLIs, native profiles' model endpoints, a writable home) and, in a repo, its config, checks and code map |
| `brindle close AGENT` / `brindle close --exited` | hide an agent (or every stopped one) from the dashboard, stopping it if it's running; its worktree and branch stay |
| `brindle send AGENT MSG` | message an agent; waits in its inbox until it's idle |
| `brindle agent kill/peek/profiles` | manage agents (only a supervisor starts them) |
| `brindle profile new/lint/show` | write a profile file, check every profile (extends chains, rule packs, patterns), or print one as brindle resolves it |
| `brindle rules suggest/accept/reject` | (brindle Pro) rules suggested from review findings that keep recurring; accept one into `.brindle/rules/learned.md`, or reject it for good |
| `brindle agent turns AGENT` / `brindle agent rewind AGENT --to N [--profile P] [--note "..."]` | a worker's worktree is snapshotted after every turn that changed files; rewind puts it back as it was after turn N and starts a fresh session there, briefed on the task, turns 1..N and your note (any provider, Codex included). Supervisors have the `agent_turns` and `rewind_agent` tools |
| `brindle setup [WS]` | re-run the repo's setup commands in a workspace |
| `brindle mcp` | the MCP server agents talk to (launched for them; you don't run it) |

With no `WS` argument, commands act on the workspace you're in.

### Agent tools

Agents launched by brindle get these MCP tools. You don't call them yourself, but
knowing them helps when you tell the supervisor how to work.

| Tool | Used by | |
|---|---|---|
| `assign` / `handoff` / `wait_for_worker` | supervisor | start a worker (return now / wait for its result / keep waiting); `dry_run=true` on `assign`/`handoff` starts nothing and reports the profile, what each `files` glob matches, overlaps and whether it would queue |
| `send_message` | any agent | message another agent; delivered when it's idle |
| `read_messages` | supervisor | read the messages agents and brindle sent you and mark them read; with `message_delivery` `"pull"` (the default) you get a one-line "N new messages" notice instead of each message |
| `list_agents` / `list_tasks` / `list_agent_profiles` | supervisor | who's running, what's queued, which profiles exist; `list_tasks(full=true)` also shows each task's brief and `done_when` |
| `list_repos` | supervisor | the repos attached to the session (brindle Pro); `repo=<alias>` on `assign`/`handoff` puts a worker there |
| `cancel_task` | supervisor | cancel a queued task (and its dependents) to re-plan |
| `requeue` | supervisor | queue a cancelled task again with the same brief, files and profile; `depends_on` replaces its dependencies |
| `workspace_diff` | supervisor | a worker branch's changes against its base |
| `request_review` / `submit_review` | supervisor / reviewer | start a reviewer on a branch / record its verdict |
| `merge_workspace` / `remove_workspace` | supervisor | merge through the gates / delete the worktree |
| `report_result` | worker | finish a task and hand back the result |
| `submit_plan` | worker | a `plan_first` worker proposes its plan and waits for approval before editing |
| `approve_plan` | supervisor | approve a worker's plan, or send it back with feedback (`approved=false`) |
| `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
| `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
| `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
| `need_user` | supervisor | stop autopilot and ask you a question |
| `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
| `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |

## Token usage and history

Every Claude Code agent's token usage (input, cached, output, model) is read
straight from its own transcript JSONL under `~/.claude/projects/`, summed
incrementally so it's cheap to check often. It shows up:

- in the sidebar and `brindle ls`, next to each agent (e.g. `191k tok`)
- appended to the result a worker or reviewer forwards to its supervisor
  (e.g. `tokens: 182k in (160k cached, 20k written) · 9k out · sonnet`)
- in `brindle history`, per row, with a total across the rows shown. Each row
  holds only what its agent used since that agent's previous row, so the
  total never double counts

`brindle history` is an append-only log of what happened: a worker's report, a
reviewer's verdict, a successful merge, and a milestone check (reports and
merges carry tokens). Unlike `brindle ls`, it survives session pruning (`brindle prune`), so
it's the place to look for what an agent did after its session is gone. It's
capped at 5000 rows per repo, oldest dropped first. Recording usage or
history never blocks a report, merge or check: a failure there is logged and
skipped.

Codex agents' usage comes from Codex's own session rollouts under
`~/.codex/sessions/`; agents on other providers (Antigravity) have none, and
count as unknown.

### Spend in dollars

`brindle cost` prices the last 30 days of history at each model's list price
(per million tokens: input, output, cache write, cache read), copied from
Anthropic's and OpenAI's pricing pages and dated; brindle warns when they're
more than 90 days old. Local models cost $0, and usage on a model known to be
free reads "Free" (its tokens still count; a mix reads "$1.20 (+40k tokens
free)"). A model brindle has no price for is shown as unpriced tokens, never
guessed. Add or correct prices under
`pricing` in `~/.brindle/config.json` or `.brindle/config.json`:

```json
"pricing": {"my-model": {"input": 1, "output": 5, "cache_write": 1.25, "cache_read": 0.1}}
```

These are list prices: a subscription or a discount makes the real bill
lower. `brindle cost report` (Pro) breaks spend down by day, profile and goal,
with the cost per merged branch and each worker profile's review pass rate,
and a savings section: the cache-hit rate, what routing to cheaper profiles
saved against running the same tokens on the most expensive profile in your
routing, and the spend budgets avoided.
Forwarded results add the dollar figure to their token line, e.g.
`tokens: 182k in (160k cached, 20k written) · 9k out · sonnet · ~$0.18`.

### Budgets (Pro)

```json
"budget": {"task_usd": 5, "goal_usd": 40, "month_usd": 200, "stop": false}
```

Set in `~/.brindle/config.json`, `.brindle/config.json` or `config.local.json`
(merged per key, the local file last). When a task routed by `weight` would
take the task, its goal or the month over a limit (a task's cost is estimated
from this repo's finished tasks on that profile, else a typical token mix at
list prices), brindle skips that candidate and uses the next cheaper one. If
none fits it refuses the assign, says why and, in autopilot, asks you. A
profile you name yourself is never second-guessed. Running workers are
reported to the supervisor at 80% of `task_usd` and again at 100%; set
`"stop": true` to close a worker at 100% instead of only warning. Without the
Pro `cost` feature budgets do nothing.

## Provider quota

brindle keeps one place (`~/.brindle/quota.json`) that knows how close each
provider is to its subscription limit, and shows a note per provider that has
data (e.g. `Codex at 82% of its weekly limit, resets Thu 9:00am`) in `assign`
replies, `get_progress`, the sidebar's autopilot block and `brindle doctor`.
It only uses what the CLIs write locally, and never reads a CLI's login
token or auth files or calls a provider's servers:

- **Claude**: the status line data Claude Code gives brindle.
- **Codex**: the last `rate_limits` event in its newest session rollout under
  `~/.codex/sessions` (windows are told apart by their length: 5-hour,
  weekly, monthly), refreshed on each turn and when asked.
- **Antigravity**: no numbers; when it reports a limit error the provider counts
  as unavailable for `limit_cooldown_minutes` (default 300).
- **Native**: full headroom while the local model server answers, none while it doesn't.

## Repo config: `.brindle/config.json`

A linked git worktree you made yourself doesn't have the git-ignored parts of
`.brindle`; brindle finds the config through the main worktree
(`git rev-parse --git-common-dir`), so there's nothing to symlink.

```json
{
  "setup": ["pnpm install", "cp \"$BRINDLE_ROOT_PATH/.env.local\" ."],
  "teardown": ["docker compose down"],
  "copy": [".env", "apps/*/.env"],
  "base_branch": "main",
  "branch_prefix": "",
  "default_agent": "developer",
  "fetch": true,
  "checks": ["uv run pytest -q"],
  "max_agents": 4,
  "pool_size": 1
}
```

Autopilot, merge gates and cleanup:

| Key | Default | |
|---|---|---|
| `autopilot` | `true` | start the supervisor with autopilot on |
| `auto_update` | `false` | run `uv tool upgrade brindle` in the background at session start when PyPI has a newer version; restart to use it. Can also be set in `~/.brindle/config.json` for every repo |
| `checks` | `[]` | commands that must pass in a worker's branch before it merges |
| `review` | only under autopilot | require a reviewer's approval before merging |
| `reviewer` | `"reviewer"` | the agent profile that reviews (a Codex profile gives a second model's view) |
| `review_profile` | none | force `request_review`'s profile, skipping its automatic cross-model pick (see below) |
| `pre_commit` | `true` | run [pre-commit](https://pre-commit.com) over the branch, if the repo uses it |
| `max_agents` | `4` | workers running at once per session (`0`: no cap) |
| `check_timeout` | `900` | seconds each check may take |
| `check_concurrency` | `2` | check runs at once on this machine, across branches; the rest wait their turn (`0`: no cap). A run far past its last duration is reported to the supervisor |
| `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` before handing it to you; `true` turns it on for a cloud account, `false` off |
| `usage_limit` | `90` | autopilot stops pushing on at this % of your Claude usage limit |
| `limit_cooldown_minutes` | `300` for Antigravity | how long a provider that hit its limit counts as unavailable |
| `graphify` | if the graph is there | point agents at the repo's [graphify](https://github.com/safishamsi/graphify) code map (`false` turns it off) |
| `stale_after` | `30` | minutes before a worker that reported and sat idle is closed, or `brindle prune` closes a stale paused/exited agent whose workspace is merged or gone (`0`: never) |
| `pipeline` | `true` | brindle reviews and merges reported branches itself; the supervisor gets one message per branch |
| `review_rounds` | `2` | fix-and-re-review rounds the pipeline runs before handing findings to the supervisor |
| `goal_audit` | `true` | once every milestone passes, an adversarial reviewer checks the work against the original goal before the goal counts as reached |
| `merge_into` | none | branch that worker branches are cut from and merge into, whatever branch the supervisor is on |
| `auto_merge_default_branch` | `false` | let the pipeline merge into the repo's default branch (origin HEAD, else `main`/`master`) on its own; by default it sends a "needs you" message instead, and you run `merge_workspace` yourself (manual merges are never gated) |
| `permission_policy` | `"off"` | `"on"`: brindle answers Claude Code workers' permission prompts from its rules (see "Permission policy" below); off, the prompts behave as they always did |
| `agy_approvals` | `"prompt"` | `"brindle"`: with the policy on, Antigravity workers run without approval prompts and brindle's hook allows or denies every call (see "Antigravity" under "Permission policy") |
| `rules` | `[]` | standing rules for the supervisor, e.g. `["Fix review findings without asking", "Validate options before offering them"]`; added to every supervisor's brief. Rules in `~/.brindle/config.json`, the repo's config and `config.local.json` are all kept, so a team's rules and your own add up |
| `plan_first` | `false` | workers propose a plan (`submit_plan`) and wait for `approve_plan` before editing |
| `overlap` | `"block"` | a task whose `files` overlap a running task's is refused (`"warn"` starts it with a warning) |
| `protected_paths` | `[]` | paths or globs (`"migrations/"`, `"*.lock"`) whose merge conflicts brindle never hands to a worker to resolve: the branch waits for you instead (see "Conflict-aware merging") |
| `pool_size` | `1` if `setup` is set, else `0` | pre-built worktrees (checked out, files copied, setup run) kept ready so a new worker doesn't wait on `setup`; `0` disables it |
| `add_dirs` | `[]` | directories outside the worktree that Claude Code agents may use (`--add-dir`; full tool access, see "Directories outside the workspace") |
| `local_models` | `true` | When a native profile points at Ollama on this machine and it isn't running, `brindle` starts `ollama serve` in the background (with the context length the profiles need) and loads their models, then stops it when the last session closes. `false`: brindle uses a server that's already running and never starts or preloads one |
| `sidebar` | `"left"` | where the dashboard sits in each window: `"left"` of the chat, or `"bottom"` (full-width rows under it) |
| `delegation` | `"balanced"` | how readily the supervisor hands work to workers. `"conservative"` does most work in its own chat (fewest tokens), `"fast"` splits any multi-part request across parallel workers straight away (quickest, most tokens). `brindle delegation fast` saves it for every repo and session (in `~/.brindle/config.json`; `--repo` for this repo only) and tells a running supervisor |
| `delete_merged_branches` | `true` | removing a worktree (after a merge, `brindle rm`, `brindle prune`, session cleanup) also deletes its branch once every commit is in its base, so finished branches don't pile up. An unmerged branch is always kept; `false` keeps them all. If GitHub keeps merged PR branches, the first `brindle pr` in a repo offers to turn on its automatic deletion with your `gh` login (repo admins only) |
| `pr_footer` | `true` | `brindle pr` ends the PR description with one line: "🌲 Built in parallel and verified with brindle" (a link). `false` leaves it out. Never added to commit messages |
| `message_delivery` | `"pull"` | how agent and brindle messages reach an interactive supervisor: `"pull"` keeps them unread and delivers one notice ("brindle (16:25:03): 2 new messages (from 9f742c5c, pipeline). Call read_messages."; the time keeps Claude Code from dropping a repeat; the sidebar shows an unread count), `"push"` delivers each message's text. Messages you send (`brindle send`, typing) and messages to workers are always pushed |
| `learning` | `"auto"` | hosted learning (brindle Pro, via the API; see below): `"auto"` uses it when your plan includes it and nothing otherwise; `"cloud"`; `"off"`. Any other value means off |
| `learning_candidates` | `[]` | the profile names the hosted learner may pick from |
| `learned_rules_repeats` | `3` | learned rules (brindle Pro): how many tasks of one profile's work must get the same review finding before a rule is suggested |
| `learned_rules_profile` | none | the profile that groups review findings for learned rules (a `native` or `claude` one); unset, a local model that answers, else Claude's cheapest model |
| `plugins` | `{}` | which installed plugin to use per group, e.g. `{"events": "<name>", "policy": "off"}`; unset, a group uses the only plugin installed in it, except `events`, which uses every installed one (several names: `"pro, audit"`; see "Plugins" below) |
| `routing` | see below | for each task weight (`light`, `medium`, `heavy`), the profiles `assign`/`handoff` try in order |
| `services` | `[]` | per-worktree Docker services (brindle Pro; see "Per-worktree services" below) |

### Per-worktree services

With brindle Pro, each worktree can get its own database or cache, so parallel agents never share one. List them in `.brindle/config.json`:

```json
{"services": [
  {"name": "db", "preset": "postgres"},
  {"name": "cache", "preset": "redis"},
  {"name": "search", "image": "opensearchproject/opensearch:2", "port": 9200,
   "env": {"SEARCH_URL": "http://127.0.0.1:{port}"}}
]}
```

Each entry takes `name`, an optional `preset` (`postgres`, `redis` or `mongo`, which fill in the image, container `port` and default env such as `DATABASE_URL=postgres://postgres:brindle@127.0.0.1:{port}/app`), `image`, `port` (the container's port) and `env`. In `env` templates, `{port}` is the host port, `{name}` the service name and `{workspace}` the workspace name.

When a workspace is created (including from the pool), brindle starts one container per service, named `brindle-<repo>-<workspace>-<service>`, bound to `127.0.0.1` on a port from the worktree's own block (`BRINDLE_PORT_BASE` + 1 + the service's index, leaving `BRINDLE_PORT_BASE` to your app, so at most 9 services). The rendered env and `BRINDLE_SVC_<NAME>_PORT` reach agents and `setup` commands. `brindle rm` (and the idle cull) remove every container labelled with the workspace, even if the config changed since. Running `up` again replaces the containers.

`brindle services [ls|up|down] [workspace]` lists, starts or stops them by hand. Without brindle Pro, brindle prints a one-line notice and starts nothing; if Docker isn't installed it warns and carries on. `brindle doctor` reports Docker when services are configured.

### Routing by weight

The supervisor sizes a task and passes `weight` (`"light"`, `"medium"`,
`"heavy"` or `"advanced"`, with `"reasoning"` and `"complex"` accepted as aliases) to
`assign`/`handoff`; brindle picks an available profile for that tier.
Light is small, well-specified, mechanical work (docs, renames, simple tests);
medium is a normal feature or bugfix in one area; heavy is architecture design,
broad cross-cutting refactors, or subtle bugs; advanced is complex, long-running
system-design and implementation tasks. The `routing`
config maps each tier to profile names: the first is the tier's baseline, the
rest are candidates hosted learning may pick instead (defaults shown; set one
tier and the others keep theirs):

```json
{
  "routing": {
    "light":    ["developer-light", "developer", "developer-codex", "developer-local"],
    "medium":   ["developer", "developer-codex", "developer-heavy"],
    "heavy":    ["developer-heavy", "developer-gemini", "developer", "developer-codex"],
    "advanced": ["developer-fable", "developer-heavy", "developer-gemini"]
  }
}
```

No tier prefers another provider for its own sake: all-Claude, all-Codex or
all-Gemini may be what works best for you, and that's for the learner to find out.

Routing merges per tier in this order: built-in defaults, `~/.brindle/config.json`
(every repo), the repo's `.brindle/config.json` (shared with the team), then
`.brindle/config.local.json` (your override for that repo). Each tier you set
replaces that tier's list; tiers you leave out keep the list from the layer below.
For example, put this in `~/.brindle/config.json` to use Codex for light tasks
in every repo, or in a repo's `.brindle/config.local.json` to do so only there:

```json
{"routing": {"light": ["developer-codex", "developer"]}}
```

Run `brindle doctor` in the repo to see the effective list and source file for
each tier. Unknown profile names are skipped; doctor and the routing reply
report the missing name and the file that supplied it.

`developer-light` runs on Claude Haiku at low effort; `developer` on your Claude
Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Opus
at high effort; `developer-gemini` on Google Antigravity (Gemini) with high effort
(Tier 3); `developer-fable` on Claude Fable at high effort (Tier 4 advanced).
A profile is skipped when its CLI isn't installed (`claude`, `codex`, `agy`), the
local model server isn't answering, or its provider is at your `usage_limit`. If
hosted learning is on it chooses among the profiles left; otherwise the first
wins. When every candidate is out, the repo's `default_agent` runs. The reply says
what was picked and why, e.g. `weight medium -> developer (Codex at 93%, skipped developer-codex)`.
An `agent_profile` you pass, or a milestone's `profile`, always wins over weight.

Provisioning and routing also check explicit models against supported read-only
catalogs, with the managed model, stored keys and the worker's filtered pane
environment. A model absent from a complete authoritative catalog is rejected
before creating a worker worktree or process. Partial catalogs, aliases,
discovery errors and Codex's bundled catalogs remain unverified; being listed
does not prove generation access, billing or quota. Catalog discovery is bounded
to five seconds for standalone checks; foreground routing/provisioning limits
discovery to one second, leaving room in the three-second launch budget for
provisioning and bounded helper cleanup. A timeout remains unverified and
launch proceeds with the real-task health follow-up. Catalogs are cached
briefly by environment and local account/config
revision where available. Routing and provisioning reuse equivalent catalog
context across worktrees, excluding launch bookkeeping while retaining project
settings, credentials and account selectors. CLI catalogs remain advisory;
unobservable external/keychain login changes may remain cached for 30 seconds.
Custom CLI binaries retain separate cache entries per working directory.
Claude, Bedrock, Vertex and Foundry have no catalog
probe here; no Claude CLI or synthetic generation request is used for these checks.

The real first worker task is sent once. A detached helper inspects its current
pane five seconds after delivery, so readiness delays do not use up that window.
Fresh interactive Antigravity chats without a task still receive their profile,
tool instructions and any autopilot context once after readiness; worker
instructions accompany the actual task in the same delivery.
For argv tasks, the timer starts at prompt submission or observed CLI readiness,
after trust/startup handling; readiness timeout does not establish delivery or
release queued work. Paused workers, replaced launches and reassigned panes are
excluded from the observation.
Explicit fresh authentication, access, model and capacity errors, or an early
exit, produce a safe supervisor message with a next step. A live process with
no clear result is recorded as pending for diagnostics, not verified model
access; it does not send a supervisor notification. Refusals use
the existing one-hour cooldown, scoped to the effective model/environment and
local login revision. Changing the account, model or credentials invalidates
that refusal; CLI/keychain account changes without an observable local revision
may require waiting for the cooldown or clearing `model_refusals.json`.
This is a single observation, not an entitlement test: unfamiliar error layouts,
later failures and slow requests can remain pending. It does not scan
historical transcripts, and excludes task text, previous pane lines and older
marked replies, fenced examples and ordinary assistant/tool explanations. A
Codex follow-up retains the complete submitted argv prompt (including profile
instructions) and distinguishes assistant/tool responses from the idle composer.
Quoted prompts and error-looking markers inside assistant/tool output remain
ambiguous and pending; markers from another provider do not establish failure.
A separate detached check covers headless and argv startup exits before task
delivery. Existing readiness and watch/cull handling continues afterward.

**Hosted learning (brindle Pro).** With a plan that includes it, brindle learns
which of your `learning_candidates` profiles suits which kind of task, and picks
one when `assign` gets no profile and no milestone names one (the reply says
`learning picked it` and why). A profile you or a milestone name always wins.
Learning is per person on Pro and per organization on Team, runs only on
PawDelta's servers, and your data stays private (see "brindle Pro and Team").
`"learning": "auto"` (the default) uses it when your plan includes it,
`"cloud"` forces it and `"off"` turns it off. If the API is unreachable, work
carries on unaffected.

**Plugins.** Three entry-point groups let a package extend
brindle through (`brindle/plugins.py` loads them; each interface is in the module
named):

| group | interface | what brindle does with it |
|---|---|---|
| `brindle.events` | `brindle/events.py` | is told when a task starts (`assign`/`handoff`), a reviewer decides, the supervisor is asked to step in, a branch merges or a worktree is removed: the repo, the worker's id, branch, profile, provider and model, who caused it, and when. Never a diff, a prompt or the task text |
| `brindle.policy` | `brindle/policy.py` | may refuse a delegation or a merge with a reason; `assign`/`handoff` then reply "Not started: ..." and `merge_workspace` (and the pipeline) "Not merged: ..." |
| `brindle.account` | `brindle/account.py` | handles `brindle account ...` |

An entry point's object is a factory `make(repo_root)` returning the plugin
(or `None`), called once per repo per process. The policy and account groups
select themselves: when exactly one plugin is installed in a group it's used;
with several, or to turn one off, set `plugins` in the config. The events
group fans out: every installed events plugin hears every event, unless
`plugins.events` names the ones to use (`"audit"`, `"pro, audit"`, or `"off"`).
With no plugin, every delegation and merge is allowed and nothing is reported.
A plugin that's missing, broken or raises is logged and ignored, never failing
what brindle was doing. brindle's own Pro, Team and Enterprise plugins (`pro` in
the events, policy and account groups, `audit` in events;
`src/brindle/pro`) are always installed and do nothing until you log in to a
plan that includes them.

### Permission policy

With `"permission_policy": "on"` (repo config or `~/.brindle/config.json`), when
a Claude Code worker (Codex and Antigravity: see below) is about to show a permission prompt, its
`PermissionRequest` hook hands brindle the structured request (the tool and its
command, path or URL; brindle never reads the screen) and brindle answers
**allow**, **deny**, or **ask**, which leaves the prompt to you as usual. Any
error is ask.

The starting rules:

- **allow** reading a file git tracks in the worktree or repo root (exact
  path, symlinks resolved and kept inside the root; never `.git/`, ignored or
  untracked files);
- **allow** a Bash command that is exactly one of the repo's `checks`, and
  `git status` / `git diff` / `git log` / `git show`, but only as a single
  simple command: anything with `;`, `|`, `&`, `$`, backticks, quotes,
  redirections or globs is never auto-allowed;
- **deny** `git push`, git force flags, and reading `~/.ssh`, `~/.aws`,
  `~/.gnupg` or `.env*`;
- **ask** for everything else.

A deny beats any allow; nothing else is ranked. Add your own with
`brindle permissions allow|deny KIND MATCH` (kinds: `read`, `write`, `edit`,
`bash`, `fetch`, `mcp`, `other`; the match is exact unless `--prefix` or
`--glob`); they're kept in `~/.brindle/permissions.json`. A repo's
`.brindle/permissions.json` (`{"rules": [...]}`, same format) can only add
denies. `brindle permissions list` shows every rule and where it came from,
`forget ID` removes one of yours, `reset` goes back to the starting rules.
Profiles can add narrower, profile-local denies with a JSON array in frontmatter,
for example `permission_denies: ["read glob ~/.private*", "bash prefix deploy"]`.
Built-in profiles ship with no profile-specific denies by default. These denies
apply to Claude Code, Codex, and Antigravity requests. Run
`brindle permissions check --profile reviewer` to inspect the effective rules by
provider and see warnings for rules Antigravity cannot express. It also lists
what Antigravity's own `settings.json` holds right now, marking which entries
are brindle's mirror and which are yours. A profile's denies are enforced on
Antigravity by brindle's hook only: that file is shared by every agy agent, so
brindle mirrors only the rules that apply to all of them. The check only reads
policy and never edits provider settings.

brindle learns from what you approve, but never on its own: when a request it
left to you is approved (the tool ran), it counts that exact command or path.
Once you've approved the same one twice, `brindle permissions suggestions` lists
it, and `brindle permissions accept ID` makes it a rule. A prompt the turn ended
on without the tool running counts for nothing. Every decision is in
`brindle history --kind permission`, and when a worker sits on a prompt, its
supervisor is told exactly which request is pending (only you can answer it).

**Codex.** Codex runs a hook only once it's trusted, so with
`permission_policy` on, brindle trusts its own hook the first time a Codex worker
needs it (and says so at `brindle start`). It adds one entry,
`[hooks.state."/<session-flags>/config.toml:permission_request:0:0"]
trusted_hash = "sha256:..."`, to Codex's `config.toml` (`~/.codex`, or
`$CODEX_HOME`), written by Codex itself. brindle passes the hook on the command
line of the Codex workers it starts (only while `permission_policy` is on), with
the same command for every worker, so that one trust covers every worktree, and
it trusts it again after a reinstall moves brindle. Nothing else in `~/.codex`
changes. If you remove the entry, brindle leaves it removed (Codex workers then
prompt as usual, and `brindle doctor` says so); `brindle permissions
install-codex-hook --yes` puts it back.
Codex patches are checked file by file: allowed only if every file is. Codex
doesn't say which tool run followed which prompt, so brindle doesn't learn from
Codex approvals. To undo, delete that entry (or untrust it in Codex's `/hooks`).

Codex also uses the profile's `allowed_tools` shell entries: `Bash(just lint)`
allows that exact command, and `Bash(task test:*)` allows it with arguments.
The same simple-command restriction applies, and default, user, repo and
profile denies still win. Other `allowed_tools` entries aren't translated.
An unrecognized patch header prevents automatic approval without discarding
denies for files brindle can identify. Session/status bookkeeping failures
don't discard the hook's decision; failures to evaluate the policy leave
Codex's normal approval flow in place.

Scope and platform limits: this is an **approval-request hook**, not a gate
on every tool call. Codex commands already permitted by its sandbox or own
rules never reach it, so brindle cannot enforce denies on those calls. It
doesn't change Codex's sandbox or approval mode, and cannot make a `never`
approval mode interactive. Disabled hooks, managed-only hook policies, older
Codex versions without `PermissionRequest`, and nonlocal execution can prevent
the hook from running. Keep Codex's own restrictions for tools that must always
be blocked. See the [OpenAI hook protocol](https://learn.chatgpt.com/docs/hooks).

The integration targets local macOS/Linux Codex CLI processes (POSIX shell
quoting); native Windows hook commands aren't supported. The local app-server
protocol was checked on macOS with Codex CLI **0.162.1**: newline-delimited
`initialize`, `initialized`, and `hooks/list` returned the session-flags key
above, `eventName: "permissionRequest"`, `source: "sessionFlags"`,
`currentHash`, `enabled: true`, and `trustStatus: "trusted"` for the installed
brindle hook. This read-only probe made no model calls or trust edits; hook
dispatch/allow/deny behavior is covered by the permission tests. Linux runtime
and native Windows behavior were not exercised by that probe.
`codex features list` also reported `hooks stable true` without a user
`features.hooks` setting, both on 0.162.1 and the bundled 0.158.0-alpha.2.1.
A session override of `features.hooks=false` reported false. No feature-enable
flag is needed on these versions, and brindle leaves explicit disablement alone.

**Antigravity.** agy's hook can deny but its "allow" is ignored (an agy bug),
and it has no "no opinion" answer. So the hook denies what the policy denies,
answers "allow" for a file inside the workspace (which agy reads and writes
without asking anyway) and "ask" for everything else, and brindle copies the
rules agy can express exactly into your `~/.gemini/antigravity-cli/settings.json`
`permissions` (allows to `allow`: `git status/diff/log/show` as exact commands
and your own exact rules, but not a repo's checks, since agy's settings apply in
every project; denies to `deny`). It adds only entries that
weren't there, remembers which ones in `~/.brindle/permissions.json`, never
touches anything else, and keeps your original file once as
`settings.json.brindle-backup`. It syncs when an agy worker starts and after
`brindle permissions allow/deny/accept/forget/reset`; `brindle permissions sync-agy`
does it by hand. To undo, turn `permission_policy` off and run `sync-agy`: brindle
removes exactly its own entries. brindle doesn't learn from agy approvals (its
hook runs for every tool call, so a tool running doesn't mean you approved it).

With `"agy_approvals": "brindle"` (and the policy on), brindle's hook is the
only gate instead: agy workers start with `--dangerously-skip-permissions`,
under which agy still honors a hook's deny, and the hook runs on every tool,
subagents' calls included. It allows what a rule allows (a repo's checks
included), the shell commands in the profile's `allowed_tools` (what a Claude
Code worker on it runs without asking), files inside the workspace given as one
absolute path (writes under `.agents/`, `.git/` and
`.brindle/` only by a rule, since those hold the hook, git's hooks and the
repo's checks), brindle's own tools and agy's tools that act only on the
conversation, and denies everything else, telling the agent to
ask its supervisor; you allow it with `brindle permissions allow`. Any failure
in the hook is a deny, and a tool brindle doesn't know is denied. Nobody can
approve a prompt in the agent's pane in this mode. The default, `"prompt"`,
leaves agy asking as described above.

## brindle Pro and Team

brindle is complete on its own. brindle Pro adds hosted learning (which profile
to use for which kind of task; per person on Pro, per organization on Team,
and kept private) and brindle Team adds org policies (allowed providers
and models, human review before merges) and an audit feed of what brindle did.
Plans and prices: https://pawdelta.com/brindle#pricing.

Run `brindle account` to see every paid feature, which ones your plan includes,
how to use them, and the next step to get the rest:

| Feature | Plan | Use it |
|---|---|---|
| hosted learning | Pro | on by itself; `brindle learning` |
| per-worktree services | Pro | `"services"` in `.brindle/config.json` |
| several repos in one session | Pro | `brindle repo add <path>` |
| settings sync across machines | Pro | `brindle account sync` |
| Brindle-CI build fixing (the fix workflow) | Pro | `brindle ci init` |
| learned rules from repeated review findings | Pro | `brindle rules suggest` |
| cost (estimates, budgets, reports) | Pro | `brindle cost report` |
| guardrails (write scope, read scope, env allowlist per profile) | Pro | `write_scope`, `read_scope`, `env_allow` in a profile |
| org policies + audit feed | Team | `brindle account org policy` |
| Brindle-CI (fixes builds, issues into verified pull requests, validation) | Team | `brindle ci init` |
| org profile and rule-pack library, pinned so a repo can't loosen it | Team | `brindle account org profiles` |
| org budgets and protected paths | Team | `budget`, `protected_paths` in your org policy (`brindle account org policy`) |
| per-member budgets, pause, and admin messages (sidebar "Messages", the bottom bar, `brindle org messages`) | Team | `brindle org messages [--keep]` lists and marks read the notices your admins sent you; a pause refuses new workers and stops running ones |
| audit log, air-gap | Enterprise | `brindle audit verify`, `"airgap": true` |
| audit export | Enterprise | `"audit_export"` in `~/.brindle/config.json`, `brindle audit ship` |
| managed models | Enterprise | `agent_setup` routes with `enforce`, and `deny_personal_keys`, in your org policy (see "Managed models" below) |
| wider CI, cost centers, managed rollout | Enterprise | `brindle ci init --host gitlab`, `brindle cost request`, and `min_version`, required packs and the kill switch in your org policy; admins can also shut down or throttle a member, role or org remotely (it arrives within about a minute) |

`brindle ci init` checks your plan before it changes anything: it refuses
without Brindle-CI on the org you're using, and on Pro it sets up only the fix
workflow.

```sh
brindle account            # paid features: what you have, how to use them, how to get the rest
brindle login              # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
brindle account login      # opens your browser to sign in (--device: enter a code instead, e.g. over SSH); brindle checks the plan offline from then on
brindle account status     # your plan, features, hosted learning on or off, when the entitlement expires
brindle account savings    # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
brindle account upgrade    # opens the checkout for brindle Pro (and prints its URL)
brindle account upgrade --enterprise [--trial] --seats N --org ORG   # brindle Enterprise for a team org; --trial: free trial, at most 10 seats
brindle account portal     # opens the billing portal (invoices, payment method, cancellation); --org ORG for a team org
brindle account seats N    # set a Team or Enterprise org's seat count (billing admin); --org ORG; Stripe prorates the change
brindle account org list   # the orgs you belong to; `org use <id>` switches, `org policy` shows the current one
brindle account org policy # the org's policy, its per-role overrides, and the policy that applies to you
brindle account org profiles                                  # the org's shared profiles and rule packs, and which are pinned
brindle account org profiles push <file> [--pack] [--pinned] [--name NAME]   # publish a profile (--pack: a rule pack) to the org (admin)
brindle account org profiles rm <name> [--pack]               # take one out of the org's library (admin)
brindle account org member policy-role <member> <role|none>   # give a member a policy role (admin; Enterprise)
brindle account org company [link <org_id> | unlink]          # link orgs you own into one company; learning is pooled only within it
```

**Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
Claude and Codex are still signed in (`claude auth status` or the cloud route's
identity, re-checked only when its credentials change; `codex login status`;
Antigravity can't be checked). If one is signed out, its running agents pause
with their worktrees, commits and conversations kept, and the supervisor and
your session are told; a timeout or missing CLI never pauses, it warns once.
A wrong company identity pauses them only when the org policy enforces it
(Enterprise); otherwise it warns once. While it enforces, Claude agents run
on exactly the route brindle checked: a profile's env lines can't change the
backend, account, key or proxy, and other cloud and Claude credential variables
(`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
environment or `brindle keys` are left out of their panes, so those agents
can't use your own AWS, Google Cloud or Azure access in their shell either.
Claude Code's own settings files can still set variables inside it; to rule
that out too, use Claude Code's managed settings. After `brindle login`,
`brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
into their conversations, but only as the account they started under: if
someone else is signed in (a personal login, say) they stay paused until the
original account is back. Your own chat session is never paused, only the
workers. A cloud sign-in is re-checked only when its credential files or
variables change, so a token that expires silently isn't noticed until then.
`brindle doctor` lists agents paused for sign-in.

**Sessions stay with their identity.** Each Claude agent records the identity it
launched under (Claude org, AWS account and role, GCP project, Azure
subscription; ids only, never tokens). While a different identity is signed in,
that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
it, message or hand off to it, start a new agent in its worktree (including
naming its branch for a new worker), or `peek` at it, with "This session
belongs to <org/account>; sign in as it to continue (`brindle login`)".
`brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
`locked: <org/account>` without its conversation (usage, last activity, turn
summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
session under another identity. Agents with no recorded identity (started before
this, or without a company setup) are unaffected, and so is everything while the
current identity can't be told (signed out, CLI missing).

This is a guard inside brindle, not a sandbox: on one OS user account the
worktrees, transcripts and credential files stay readable outside brindle (a
shell, another tool). Real separation needs a separate OS user or your
company's device management. Codex and Antigravity can't be told apart by
account (brindle only sees "signed in"), so their sessions are paused on
sign-out but can't be locked to one account; isolation applies to Claude routes
(subscription, Bedrock, Vertex, Foundry).

### Several repos in one session (brindle Pro)

A session can work across several repos. Attach another local repo you own
to the running session and the supervisor can put workers there: each gets a
worktree in that repo, that repo's own checks and review, and merges into that
repo's branch. A task may depend on one in another repo, a milestone check can
run in another repo (`check@<alias>: <command>` in `goals.md`, or `repo` in
`set_goal`), and the sidebar and `brindle ls` group workers by repo.

```sh
brindle repo add ../pawdelta-web --name web   # attach a repo to the current session (alias: web)
brindle repo ls                               # the repos attached to this session
brindle repo rm web                           # detach it (its worktrees and branches stay)
```

Setting up a team takes no sign-up form:

```sh
brindle account org create "Acme Eng"                    # a team org you own
brindle account upgrade --team --seats 5 --org org_...   # check out brindle Team for it
brindle account org invite dev@acme.com --org org_...    # prints a one-time `org join` code (--admin for admins)
brindle account org join cpi_...                         # your teammate, logged in with that email
brindle account logout     # revoke this device's session and forget its credentials
```

Credentials live in your keychain (macOS), the Secret Service (Linux) or a
0600 file under `~/.brindle/pro`. What leaves the machine, and only with a
plan that includes it: for learning, a coarse feature vector of each task
(a kind such as bugfix or docs, a size bucket), the declared weight, profile
names and their relative cost, and outcome numbers (approved, merged, checks
passed, review rounds, tokens, time); for the Team audit feed, the event
kind, the profile, provider and model names, and who caused it. Repos,
agents and branches are named only by keyed hashes (HMACs under your org's
key) that the server can't reverse. Never the task text, prompts, diffs,
file names, paths or branch names. See `src/brindle/pro/learning.py` and
`src/brindle/pro/team_events.py` for the exact payloads.

### Managed models (brindle Enterprise)

An org policy can pick the route its workers run on and keep people's own
model keys out of their panes. All of this is ignored without the Enterprise
entitlement.

* The routes live in the policy's `agent_setup` block (the same one `brindle
  login` applies, see "Using your company's access"), one per agent. With
  `enforce` on, an agent whose route points at a cloud or an endpoint of the
  org's is managed: Claude Code on `bedrock` (`aws.region`; the SSO group
  `sso_start_url`, `sso_region`, `account_id` and `role_name` is optional, all
  four or none, and without it your own AWS sign-in applies), `vertex`
  (`gcp.project`, `gcp.region`), `foundry` (`azure.resource` with its
  `subscription_id`, or `azure.base_url`, the endpoint) or `gateway`
  (`gateway.base_url`), with `model_ids` (the ids the route serves) and
  `default_model` (what panes start on, `ANTHROPIC_MODEL`); Codex on `azure`
  or `gateway` with `codex.base_url` (`OPENAI_BASE_URL`). The route's
  variables are the top layer of every pane's environment, a profile that
  points somewhere else is refused with a message saying why, and the
  identity is checked at launch. With `exclusive` on as well, an agent the
  block gives no route at all is refused (Claude Code or Codex with no entry,
  and always native and Antigravity); without it such an agent runs
  unmanaged (use `allowed_providers` to keep it out). Without `enforce` the
  same block is sign-in help only.
* `provider_config` is the older, org-wide form of the same thing: `provider`
  (`bedrock`, `vertex`, `azure` or `openai-compatible`), plus `region`,
  `model_ids` and `endpoint` as the provider needs them (for `vertex`,
  `project`; an older policy that put the project id in `endpoint` still
  works). The server now converts it into `agent_setup` routes (with
  `exclusive` on) and keeps deriving it for older clients; this client reads
  it whenever `agent_setup` has no `exclusive` field (a server older than
  routing in `agent_setup`) or routes nothing. An old
  `provider_config` and its `agent_setup` form give panes the same variables.
* `deny_personal_keys` (in `agent_setup`, or the policy's top-level field):
  the personal model keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...) are
  taken out of every agent pane, and a profile that sets one is refused.
* An org-owned key: with `provider` `anthropic` (or `openai-compatible`), set
  `key_env` to the name of a variable that holds the org's key (not a personal
  one like `ANTHROPIC_API_KEY`), or `key_helper` to a command, as a list of
  arguments, that prints the key (for example `["vault", "kv", "get",
  "-field=key", "secret/anthropic"]`). brindle runs the helper on the person's
  machine, without a shell and with a timeout, each time a worker starts, and
  gives the key to that worker as `ANTHROPIC_API_KEY` (`OPENAI_API_KEY` for
  Codex). It passes `deny_personal_keys`; a personal key doesn't. The key never
  goes through brindle's servers. When the variable is missing or the helper
  fails, the worker doesn't start.

Two things to know when you turn on `deny_personal_keys`:

* With an `openai-compatible` or `azure` provider, `OPENAI_API_KEY` is
  stripped like the rest, so give the profile a non-personal key to use: set
  `api_key_env` to the name of a variable that holds the org's key (not
  `OPENAI_API_KEY`).
* It can't block a Claude subscription or keychain login by itself: Claude
  Code signs in on its own, outside the variables brindle removes. Only an
  enforced route that takes Claude Code to Bedrock, Vertex or Foundry keeps
  it off that login. Always pair `deny_personal_keys` with one.

### Air-gapped mode (brindle Enterprise)

For machines that must not talk to the internet at all, air-gap mode turns
brindle into a local-only tool: nothing is sent to the brindle Pro backend, no
telemetry of any kind leaves the machine, and delegation only reaches models
that run on this machine or your private network.

```json
{"airgap": true}
```

in `.brindle/config.json` (or `.brindle/config.local.json`) turns it on for a
repo; `BRINDLE_AIRGAP=1` turns it on for a process. Either is enough, and
neither can turn the other off. Once a process has loaded an air-gapped
repo's config it stays air-gapped for every repo it serves until it exits
(fail safe: a dashboard or MCP server spanning repos never leaks for one of
them). With it on:

* **No outbound traffic.** Every brindle Pro request (login, entitlement
  refresh, key fetches, hosted learning, the team policy, the audit feed) is
  refused before it reaches the network. Learning is off (nothing is learned
  locally), audit events are not recorded, and the entitlement comes from an
  offline license that is never refreshed.
* **Local models only.** No agent with a hosted provider (`claude`, `codex`,
  `antigravity`, ...) is launched: not a worker, not a reviewer, not a
  subagent, and not the chat itself. A profile runs only with the native
  provider on a loopback or private-network `base_url` (`localhost`,
  `127.0.0.1`, `::1`, `10.x`, `172.16-31.x`, `192.168.x`), or a native
  profile marked `local: true` (for an endpoint named by a hostname brindle
  can't check offline; the flag is ignored on hosted providers). Point
  `default_agent`, `routing` and `reviewer` at such profiles; see "The
  native provider" below. In particular, `brindle` itself won't start unless
  `default_agent` is a local profile: the supervisor is an agent like any
  other, and a hosted one would send your repo to its service. The native
  provider also refuses a request to an endpoint that isn't local, as a
  second line of defence.
* **An offline license.** brindle Enterprise issues a signed license file.
  `brindle account license install <file>` verifies it against the keys pinned
  in brindle (no network) and stores it under `~/.brindle/pro`; `brindle account
  license status` shows it. Air-gap mode is a feature of that license: with
  one that doesn't include it, brindle still blocks everything (fail safe) and
  `brindle doctor` and `brindle account status` say the plan doesn't include it.
* **An offline team policy.** With a Team license, the org policy is read
  from `.brindle/policy.json` in the repo instead of being fetched: the same
  JSON `brindle account org policy` shows, e.g. `{"org_id": "org_...",
  "version": 3, "policy": {"allowed_providers": ["native"], "allowed_models":
  null, "require_human_review": true, "max_parallel_workers": 4}}`. Without
  the file, delegations and merges are refused until it is there.

`brindle doctor` shows whether air-gap mode is on and licensed, which configured
profiles it refuses, and whether the offline license and policy are in place.
### Audit log (brindle Enterprise)

With a plan that includes `audit`, brindle keeps a local, tamper-evident record
of every action an agent took: each delegation (`assign`/`handoff`), review
verdict, escalation, merge and worktree removal, and every delegation or merge
the repo's policy refused (with the reason). Nothing leaves the machine; this
is the `audit` events plugin (`src/brindle/pro/audit_chain.py`) running next to
the Team feed, and without the entitlement it writes nothing, not even a key.

Each repo's log is an append-only JSONL file, `~/.brindle/audit/<repo>-<hash>.jsonl`
(0600, in a 0700 directory). Every record carries `seq`, `ts` (UTC), the full
local event (kind, agent, branch, profile, provider, model, actor, workspace,
repo, time, and the outcome: `approved`, `merged`, `reason`), `prev_hash` (the
SHA-256 of the previous record's canonical JSON; 64 zeros for the first),
`hash` (the SHA-256 of this record's seq, ts, event and prev_hash in canonical
JSON) and `sig`, an Ed25519 signature over `hash` by a per-install key kept in
`~/.brindle/audit/signing.key` (0600, created on first use). A head file next to
the log remembers the last seq, so records removed from the end are caught too.

```sh
brindle audit verify [--repo PATH]                 # recompute the chain and check every signature;
                                                 # prints the first broken seq and exits 1 if any
brindle audit export [--since ISO] [--format jsonl|csv]   # the records, for your SIEM or a spreadsheet
brindle audit pubkey                               # this install's public key (hex), to verify elsewhere
```

`verify` reports what went wrong at the first record that doesn't check out:
one altered in place (hash or signature), one removed, inserted or reordered
(seq and prev_hash), or a truncated tail. Verifying and exporting never need
the entitlement, so a log keeps its value after a plan lapses.

#### Audit export (brindle Enterprise)

With a plan that includes `audit_export`, brindle ships the audit records to your
own logging stack and can prune old ones locally. Configure it under
`audit_export` in `~/.brindle/config.json`; secrets are never in the config, each
`*_env` names an environment variable that is read when sending:

```json
{"audit_export": {
  "retention_days": 90,
  "sinks": [
    {"type": "webhook", "url": "https://siem.example.com/brindle", "secret_env": "BRINDLE_WEBHOOK_SECRET"},
    {"type": "splunk",  "url": "https://splunk.example.com:8088", "token_env": "SPLUNK_HEC_TOKEN", "index": "audit"},
    {"type": "datadog", "site": "datadoghq.com", "api_key_env": "DD_API_KEY", "tags": ["env:prod"]},
    {"type": "s3",      "bucket": "acme-audit", "prefix": "brindle/", "region": "eu-west-1"}]}}
```

- `webhook`: an HTTPS `POST` of `{"version", "source", "sent_at", "records"}` with
  `X-Brindle-Signature: sha256=<HMAC-SHA256 of the raw body under the secret>`.
- `splunk`: the HTTP Event Collector (`Authorization: Splunk <token>`); the path
  defaults to `/services/collector/event`; `index`, `source`, `sourcetype` are optional.
- `datadog`: the Logs intake for your `site`, one log per record.
- `s3`: one JSONL object per batch under `<prefix>YYYY/MM/DD/`. Needs `boto3`
  (not installed with brindle; without it the sink reports it isn't configured).
  Credentials are boto3's usual chain, or `access_key_env` / `secret_key_env`.

Two sources are shipped: every record of the local audit chain, as written
(`seq`, `ts`, the event, `prev_hash`, `hash`, `sig`, plus the log's name, so the
receiver can re-check the chain against `brindle audit pubkey`), and the Team
feed's events (the same HMAC refs the Team backend gets, never raw names, each
with its content `hash` and the `chain_hash` of the matching chain record). Each
sink has its own cursor, so a slow or failing sink never holds the others back.
Records go out in batches, in order, at least once; a failed send is retried with
exponential backoff from what is on disk, so it survives offline periods and
restarts. A sink that can't be set up (an unset variable, a non-HTTPS URL) is
reported and skipped. Nothing is sent without the entitlement or in air-gap mode.

`retention_days` prunes local audit records older than that, but only once every
sink has received them. Pruning replaces the dropped records with one signed
checkpoint (the last dropped seq and its hash), so `brindle audit verify` still
passes; the newest record is always kept.

```sh
brindle audit ship [--force]                       # send now; exit 1 if a sink failed
brindle audit prune [--repo PATH] [--days N]       # prune now (default: retention_days)
```

### Brindle-CI

Brindle-CI (brindle Team; build fixing alone is on Pro) brings brindle into your CI: it fixes broken builds,
turns issues into pull requests whose checks it has verified, and reviews pull
requests with evidence. It runs on your own CI with your own model keys; the
hosted control plane decides what each run does and what it reports. The
workflows call these commands:

- `brindle ci init`: set a repository up in one go (the GitHub App, the secrets,
  the workflows as a pull request, then a doctor check). Run it inside a
  checkout of the repository. For Claude it asks whether to use an API key
  (the `ANTHROPIC_API_KEY` secret) or Anthropic
  [workload identity federation](https://platform.claude.com/docs/en/manage-claude/wif-reference)
  (`--credential key|federation` answers without asking). It also creates the
  `brindle` issue label (label an issue with it to hand it to brindle CI), and
  commits the workflows on a `brindle/ci-setup` branch, then puts you back on
  the branch you started on and deletes the local setup branch, so `git pull`
  after merging the setup pull request is clean (commit or stash your changes
  first: it refuses to start with uncommitted work). With your own cloud
  (`--credential bedrock|vertex|foundry`, keyless GitHub OIDC) it can read the
  IDs from the deployment template's outputs instead of asking:
  `--from-stack NAME [--region R]` (AWS CloudFormation), `--from-terraform DIR`
  (Google) or `--from-deployment NAME -g RG` (Azure). They are defaults for
  `--cloud-var`, which still overrides them, and every ID is format-checked. A
  missing `aws`, `terraform` or `az` CLI or output is an error that names the
  `--cloud-var` to pass instead. Vertex AI and Microsoft Foundry are in
  **preview** (not yet verified end to end): `init` refuses them unless you pass
  `--preview` or set `BRINDLE_CI_PREVIEW_CLOUDS=1`, and `doctor` marks them
  `(preview)`. Bedrock is generally available.
- `brindle ci doctor`: which provider CLIs and credential names a runner has, and
  which providers CI may use on this repository. `--models` also makes one tiny
  call per pinned model and says why any is refused (a quota, a form not filled in).
- `brindle ci start`: start a run for an issue, a goal text or a dispatched run,
  or a validation of a pull request.
- `brindle ci run`: run what was started: the supervisor and the result upload,
  or the pull request's checks and reviews and the evidence upload.
- `brindle ci report`: tell the service how the workflow's jobs ended.

Credential names are reported, never values. CI uses API keys, cloud sign-ins
(Bedrock, Vertex, Foundry) or identity federation only: a provider signed in
only with a subscription (`CLAUDE_CODE_OAUTH_TOKEN`, a Codex ChatGPT login) is
not used, on personal and organization repositories alike. Add an API key
secret instead.

Fix builds only fix the default branch's *required* status checks, and a new
repository has none. When the default branch requires none, `brindle ci init`
warns and, for each job in your workflows, asks whether to make it required
(`--required-check NAME` picks one without asking, `--no-required-check` only
warns; run without a terminal, init only makes a check required when
`--required-check` names it). It adds the check to the branch's existing protection, or protects the
branch with just that check. GitHub only allows branch protection on a private
repository with a paid plan; when it refuses, init says how to require the
check yourself (Settings > Branches, "Require status checks to pass").

An Anthropic API key is either a workspace key (scoped to one workspace) or an
organization-level key (not scoped to a workspace). An organization-level key
needs the workspace in every request, so after the key `brindle ci init` asks for
its workspace ID (`wrkspc_...`; leave it blank for a workspace key, or pass
`--workspace-id`, default `$ANTHROPIC_WORKSPACE_ID`) and stores it as the Actions
variable `ANTHROPIC_WORKSPACE_ID`. The workflow then sets
`ANTHROPIC_CUSTOM_HEADERS="anthropic-workspace-id: <id>"`, which reaches every
Claude Code process the run starts.

With identity federation the repository stores no Anthropic key: the workflow
exchanges GitHub's OIDC token once for a short-lived Anthropic token, as a
Claude Console service account (billed as API, so it is allowed on
organization repositories), and gives the job `ANTHROPIC_AUTH_TOKEN`. `brindle
ci init` stores the rule, organization, service account and optional workspace
IDs as the Actions variables `ANTHROPIC_FEDERATION_RULE_ID`,
`ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID` and
`ANTHROPIC_WORKSPACE_ID`, from `--rule-id`, `--organization-id`,
`--service-account-id` and `--workspace-id` or else asked (defaulting to those
environment variables). Without a terminal on stdin, init asks nothing and
takes the options and the environment, so it can run unattended; a missing or
malformed ID (rule `fdrl_...`, organization a UUID, service account
`svac_...`, workspace `wrkspc_...`) stops it before it creates the CI token.
The ID options imply `--credential federation` and can't be combined with
`--credential key`. Create the federation rule in the Claude Console with
subject prefix `repo:<owner>/<name>:*`, the condition

```text
claims.repository == "<owner>/<name>" && claims.workflow_ref.startsWith("<owner>/<name>/.github/workflows/brindle-ci-")
```

audience `https://api.anthropic.com`, and a token lifetime of at least 600
seconds. The condition lets only brindle's workflows mint tokens. On pull
requests the pull request's own copy of the validate workflow runs, so give
write access only to people you trust (forks never get a token). Leave the `ANTHROPIC_API_KEY` secret
unset: a key takes precedence (brindle drops an empty one before Claude Code
starts).

A federated token lives at most about ten minutes, however long the rule
allows: Anthropic caps it at twice what remains of GitHub's OIDC token. So
the run job doesn't rely on the workflow's one exchange: `brindle ci run`
fetches a fresh OIDC token and exchanges it again at 70% of each token's
life, and lends the current token to the agents through a proxy on the
runner's loopback interface. The agents get `ANTHROPIC_BASE_URL` pointing at
it and a random per-run secret as `ANTHROPIC_AUTH_TOKEN`; the Anthropic token
itself stays in the `brindle ci run` process, and the GitHub token endpoint
(`ACTIONS_ID_TOKEN_REQUEST_URL` and its token) is scrubbed before any agent or
check starts. If the first exchange fails, the run goes on with the
workflow's token as before; `BRINDLE_CI_FEDERATION_REFRESH=0` in the job's
environment turns the refresh off.

**Closing and cleaning up.** Press `x` on an agent in the sidebar (twice for one
that's still running) or run `brindle close <id>` to stop it and hide it. Stopping means
every process of the agent, not just its window: Claude Code can host a session in its
background daemon, where it would otherwise keep running. brindle also cleans up on its
own, from the sidebar every minute and whenever `brindle` starts: it stops anything left
running for agents that are paused, closed or whose window is gone, and closes workers
that reported and have been idle for `stale_after` minutes. Closing never touches a
worktree or branch, so unmerged work stays reviewable and mergeable. A worktree whose
branch is already merged into its base and whose agents are all finished drops out of
the sidebar; `brindle prune` then removes it (the branch stays, and a worktree with
uncommitted changes is kept and listed). `prune` also kills brindle tmux sessions that
hold only idle shells and no running agent, stops leftover brindle tmux servers, and
removes stale locks and empty worktree folders.

**The pipeline.** The slow part of delegating isn't the workers, it's the supervisor's
turns between the stages: report, review, merge, remove, each waiting on a model turn
that carries the whole session's context. So brindle runs those stages itself. When a
worker reports, brindle starts the review at once and runs the checks in the
background; when the reviewer approves and the checks pass, brindle merges the branch,
removes the worktree, and sends the supervisor one message with the worker's report
and the review. Findings go straight back to the worker to fix (`review_rounds`
times) before they reach the supervisor, and anything the pipeline can't settle (a
conflict, a failing check, no reviewer) arrives as "needs you" with the details.
`"pipeline": false` restores the manual flow. Supervisors are also told to keep task
briefs short: writing a long brief holds up every worker waiting on it.

**Conflict-aware merging.** Declared `files` are a guess; what a branch actually
changed is the truth. So at the end of each worker turn brindle compares the files
each running branch has changed (committed or not) and tells the supervisor as soon
as two branches touch the same file, once per pair, while there's still time to have
one wait for the other. When several approved branches are ready at once, brindle
merges the one whose hunks overlap the others' least first, then syncs each of the
rest onto the new tip (merging the base into it, so its approval carries over) and
merges it in turn; a conflict then belongs to one branch, not to the pile. That
branch goes back to its own worker with the conflicting files to resolve, or to a
light worker started in its worktree if the original has finished; its next report
starts a fresh review, and the checks run again on the resolved commit before it
merges. Conflicts in `protected_paths` (migrations, lockfiles, generated code) are
never handed to a worker: the supervisor gets them as "needs you".

**The completion audit.** A milestone's check can pass without the milestone being
done: the test asserts too little, or checks the wrong thing. So when every milestone
passes, autopilot doesn't call the goal reached yet. A read-only reviewer audits everything since the goal was set
against the goal and each milestone, looking for what's missing, stubbed or weakly
tested. Only its approval of that commit finishes the goal. If it finds gaps, the
supervisor gets them as a message and keeps working until a fresh audit approves.
`"goal_audit": false` turns it off.

**Spending fewer tokens.** Workers run only the tests that cover their change while
they work. The full suite runs once: as the repo's `checks` before a branch merges,
or, with no `checks`, by the worker just before it commits. brindle runs the checks
the moment a worker reports and caches the result by commit, so the review and the
merge gate reuse it instead of each running the suite; `check_milestone` runs in
the background and delivers its result as a message, so nothing waits on a long
suite. An approved review carries over when brindle merges the base branch into a
branch cleanly before merging it (the checks still run on the merged result), so a
branch that only fell behind isn't reviewed twice. Workers and reviewers get
brindle's tools loaded up front (`tool_search: false` on their profiles; a chat keeps
Claude Code's on-demand loading, since it may carry your own MCP servers), so they
don't spend a round trip finding `report_result` at the end. And the supervisor is
told to size work first: a change it can make in a few minutes it makes itself,
since a worker plus its review costs about ten times as much. If the repo has a graphify
knowledge graph (`graphify-out/graph.json`, built with `/graphify`) and `graphify` is
installed, brindle tells the supervisor and every worker to find code with
`graphify query` before grepping or reading whole files. Those commands are
pre-approved, and brindle refreshes the graph's code (`graphify update`, no LLM) in
the background after each merge.

When `pool_size` is greater than `0`, a claimed worktree keeps the path and
port block it was built with -- it's never moved, and its port block is fixed
before `setup` ever runs. That means `setup` (and anything it writes) must
not depend on the workspace's branch name or assume it's running at
`<worktrees_dir>/<repo>/<branch>`; use `$BRINDLE_WORKSPACE_PATH` and
`$BRINDLE_BRANCH` instead of hardcoding either.

A pool entry's `setup` runs under a placeholder identity (a `brindle-pool/*`
branch and a `pool-*` name) before any workspace claims it, but its
`teardown` can run later against the real workspace's branch and name -- or,
if the entry is discarded unclaimed, against that same placeholder identity.
Only `$BRINDLE_WORKSPACE_PATH` and `$BRINDLE_PORT_BASE` are guaranteed to be the
same value in both runs; `setup` must not write anything `teardown` needs to
find by branch or workspace name/id.

`.brindle/config.local.json` is gitignored and overrides keys for you only. For
`setup`/`teardown` it can also give `{"before": [...], "after": [...]}` to run
commands around the team's list. `~/.brindle/config.json` holds your own defaults
for every repo (any key except the command lists, like `delegation` or
`sidebar`); a repo's two files override it. With brindle Pro or higher, those
preferences (`delegation`, `sidebar`, `message_delivery`, `pr_footer`,
`delete_merged_branches`, `max_agents`, `autopilot`, `plan_first`, `stale_after`,
`usage_limit`, `review_rounds`) sync across your machines (last change wins; nothing
else in the file leaves the machine); `brindle account sync` syncs now.

Setup, teardown, and agents all see these variables: `BRINDLE_ROOT_PATH`,
`BRINDLE_WORKSPACE_PATH`, `BRINDLE_WORKSPACE_NAME`, `BRINDLE_WORKSPACE_ID`,
`BRINDLE_BRANCH`, `BRINDLE_BASE_BRANCH`, and `BRINDLE_PORT_BASE`. Each workspace gets
ten ports, from `BRINDLE_PORT_BASE` to `BRINDLE_PORT_BASE+9`, so parallel dev
servers don't collide. Agents also get `BRINDLE_AGENT_ID`.

## Agent profiles

Markdown files with frontmatter. brindle looks in `.brindle/agents/`, then
`~/.brindle/agents/`, then your org's library (brindle Team, below), then its
built-ins (`supervisor`, `developer`, `reviewer`,
`reviewer-codex`, `developer-local`, `reviewer-local`, `subagent`):

```markdown
---
name: frontend
description: React/TypeScript specialist
provider: claude          # claude | codex | antigravity | native | shell | subagent
model: sonnet             # optional
permission_mode: acceptEdits   # optional, Claude Code only
---
You are a frontend engineer...
```

**Permissions.** The built-in `developer` profile runs in Claude Code's auto mode
(`permission_mode: auto`): a classifier approves ordinary actions and prompts only
for risky ones, while the profile's `allowed_tools` still apply. Every worker is
also told to run commands plainly from its own worktree, never to `cd` into or read
the main checkout, and to write files with its tools rather than shell heredocs;
those were the commands that stalled on prompts most. Workers run with Claude Code's normal permission prompts. When a
worker is waiting on one, `brindle ls` shows it as `waiting`, and you attach to
approve it; if it's still waiting after 90 seconds, its supervisor gets a message
saying so (once); if the profile asked for auto mode, the message says so too, since a
prompt then means Claude Code switched auto mode off for that session. brindle marks each worktree it starts Claude Code in as trusted,
so a worker never stops on the first-run "trust this folder?" dialog. The
built-in `reviewer` runs with `dontAsk`: anything outside its `allowed_tools` is
refused rather than waiting for an answer. The built-in `developer` profile edits files without asking
(`acceptEdits`) and has an `allowed_tools` list covering git inspect/commit and
common test/build commands: `pytest`, `uv run`, `npm/pnpm/yarn test|run`,
`cargo`, `go`, `make`, `swift`, `xcodebuild`. It can't push or run arbitrary
commands. Note that `npm run`, `make`, and `uv run` execute whatever the repo
defines, so only point workers at repos you trust. Override the list in
`.brindle/agents/developer.md`. One thing Claude Code's rule matching does not
say up front: a `Write(path)` allow rule is not consulted by file permission
checks, only `Edit(path)` rules are, and an `Edit` rule covers every file-editing
tool. So write `Edit(docs/**)`, not `Write(docs/**)`, to let a worker create and
change files under a directory without prompts.

### Which provider can do what

| Provider | Supervisor (`brindle --provider …`) | Worker / reviewer |
|---|---|---|
| `claude` (Claude Code) | yes (the default) | yes |
| `codex` | yes | yes |
| `antigravity` (`agy`) | yes | yes |
| `native` (local models) | no: its loop has only the worker tools (report, message, diff), not `assign` or `handoff` | yes |
| `subagent` | no: it runs inside a supervisor's own Agent tool | yes, through `assign`/`handoff` |
| `shell` | only as a stand-in, for testing brindle itself | a plain shell, for dev servers and testing brindle |

brindle only offers a profile whose CLI is installed and signed in. A profile on a
CLI that isn't signed in is left out of the supervisor's profile list and of
routing by weight, and naming it directly stops with how to sign in
(`claude auth login`, `codex login`, running `agy`) instead of opening the CLI's
login screen. `brindle doctor` shows, for each installed CLI, how it's signed in
and whether a quota limit is in effect. Sign-in is its own login, an environment
key (by name, never its value) set in your shell or in a profile's
`env.NAME: value` lines, signed out, or unknown when the status check gives no
answer. Keys set in the environment (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
`GEMINI_API_KEY` and the like) count as signed in; see the table below for what
each CLI takes. A worker that no hook reports on (Codex)
and that shows nothing new for 10 minutes without reporting, for example because
it's signed in without a plan that includes it, is reported to its supervisor.

#### Key login

| Provider | Key login (environment variables) |
|---|---|
| `claude` | yes: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or Bedrock/Vertex/Foundry (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`) |
| `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY`. With Codex signed in with ChatGPT (checked on Codex CLI 0.162.0): interactive `codex`, which brindle runs in agent panes, ignores both keys and uses the ChatGPT plan, and only `codex login --with-api-key` switches it to a key; headless `codex exec` (Brindle-CI) bills `CODEX_API_KEY` over the login and ignores `OPENAI_API_KEY`. `brindle doctor` and `brindle keys set` say so, and `auth: api_key` on an interactive Codex profile is refused under a ChatGPT login (run `codex login --with-api-key`, or use `auth: subscription`) |
| `antigravity` | `GEMINI_API_KEY`, only with `"modelProvider": "gemini"` in `agy`'s `settings.json` (see [Google Antigravity](#google-antigravity)); otherwise `agy`'s own browser sign-in |

Workers that run unattended should use key login where the CLI supports it.
A personal login is one interactive session with one quota shared by every
worker: when it expires or its quota runs out, every worker on it stalls until
someone signs in again in a browser. A key gives workers their own limits and
needs no browser step. Put the key in your environment, or in a profile's
`env.NAME: value` lines in `~/.brindle/agents` (never in the repo), and
`brindle doctor` names the variable each CLI gets, from your shell or from
which profiles.

A profile chooses between the two with `auth`:

| `auth:` | What the worker signs in with |
|---|---|
| `auto` (default) | A key from your environment or `brindle keys` if there is one, else the CLI's own login. |
| `subscription` | Always the CLI's own login: brindle strips that provider's API keys from the pane. |
| `api_key` | Only a key: the worker refuses to start without one. |

Your org's `deny_personal_keys` (Enterprise) still applies on top of `auth`.

#### Running unattended on API keys

When running long, unattended autopilot sessions with API keys:

- **Quota, token and cost measurements per provider**:
  - Autopilot's automatic usage pause (`usage_limit: 90`) relies on the percentage numbers emitted by Claude Code's status line under subscription plans. Under API-key billing (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`), the CLIs report no subscription percentage, so subscription usage pause thresholds do not trigger; manage spend via provider account budget limits or brindle budget policies.
  - Token and cost tracking: Token counts and cost estimates are captured from Claude and Codex transcripts. Antigravity (`agy`) currently has **no token/cost measurements**, so budget policies and cost tracking cannot monitor Antigravity spend; enforce usage caps directly in Google AI Studio or your Google Cloud project billing settings.
- **Runtime auth failure handling and provider differences**:
  - **Claude and Antigravity**: Runtime authentication failures (such as 401/403, revoked API keys, or credit/billing errors) are intercepted via StopFailure and exit hooks. Brindle marks the worker as failed, alerts the supervisor, invalidates cached sign-in checks, and pauses autopilot with a `Credentials need attention` notice rather than cycling idle nudges.
  - **Codex (unsupported runtime auth detection)**: Codex CLI turn completion (`agent-turn-complete`) does not classify auth errors at runtime. If an OpenAI API key expires or is rejected, Codex workers will not automatically transition to `failed` or trigger an autopilot credential pause. When running Codex workers unattended, monitor their progress via `brindle watch` or check tmux panes; if an auth failure occurs, manually restart or rotate keys.
- **Supervisor credential refresh and key rotation**:
  - **Recovering failed/blocked workers**: When an agent fails on an expired key or autopilot pauses with a credential error, export the fresh key (or update `brindle keys set`), then run `brindle continue`. This unpauses the session and relaunches paused/failed agents with the new credentials. The replacement credentials are automatically synced to the session credential store, dynamically refreshed by the supervisor's launcher on all subsequent worker delegations (`assign`/`handoff`).
  - **Proactive rotation in live sessions**: Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
  - **Precedence**: Newly stored keys (`brindle keys set`) or session credentials updated during recovery take effect dynamically for all new panes, overriding stale launcher environments. For profile-level overrides, an explicit profile key (`env.NAME`) takes precedence over default provider credentials.
- **Antigravity permissions prerequisite**: Antigravity (`agy`) does not support approving shell commands solely via external hooks. With `GEMINI_API_KEY`, unattended agy workers will still stall on approval prompts unless permissions are pre-populated in `settings.json` (see Permissions) or run with `agy_approvals: "brindle"` under `permission_policy: "on"` (`--dangerously-skip-permissions`).

### Cheap workers

By default a Claude Code worker loads everything your own `claude` does: your
plugins, MCP servers and `~/.claude/CLAUDE.md`. That context is re-read on every
turn and can be most of a worker's input tokens. These optional fields (Claude
Code only, all off by default) trim it:

| Field | Passes | Effect |
|---|---|---|
| `strict_mcp: true` | `--strict-mcp-config` | Only brindle's MCP server loads; your other MCP servers don't. |
| `setting_sources: project,local` | `--setting-sources` | Skips your user settings (`~/.claude`: plugins, hooks, `CLAUDE.md`). brindle's own hooks come through `--settings`, which is always applied. |
| `effort: low` | `--effort` | `low`, `medium`, `high`, `xhigh` or `max`. |
| `headless: true` | `claude -p` | No interactive TUI; each turn is one `claude -p` run (see below). |

```markdown
---
name: cheap
description: Small, well-specified edits at low cost
provider: claude
model: sonnet
effort: low
strict_mcp: true
setting_sources: project,local   # no user plugins or ~/.claude/CLAUDE.md
headless: true
permission_mode: acceptEdits
allowed_tools: Bash(git add:*), Bash(git commit:*), Bash(git status:*), Bash(git diff:*), Bash(uv run:*), Bash(pytest:*)
---
You are a developer agent running under brindle. Implement the task, run the
tests, commit, and report.
```

**Headless workers** still run in a tmux window, where a small brindle runner
starts `claude -p` for the task, then `claude -p --resume <session>` for each
message sent to the worker while it's idle. Messages sent mid-turn are handed
over when the turn ends, as for any Claude worker, and so is the reminder to call
`report_result`. The window shows each turn's prompt and final answer (`brindle
agent peek`). If `claude` exits with an error, the worker stops, and `handoff`
reports the error. Differences from an interactive worker:

- Nothing can answer a permission prompt, so any tool not covered by
  `permission_mode` and `allowed_tools` is refused rather than waiting for you.
  A headless worker never shows as `waiting`.
- You can't attach and type into it. Talk to it with `brindle send` or `send_message`.
- A `done_when` finish line is added to the task, without `/goal` (an interactive command).

Blank values and anything after ` #` are ignored, so frontmatter can carry comments.
`env.NAME: value` lines set environment variables for the agent's process (see
"Open-weight models" for what that's for). A list field takes `a, b`, `[a, b]`,
or a block list (`- item` lines under the key), which is the form for items that
contain commas.

### Building on a profile: `extends` and rule packs

A profile can start from another one instead of copying it:

```markdown
---
name: backend-auditor
description: Reviews a branch as a backend security audit
extends: reviewer
rules: security/backend
---
Audit the change the way a security reviewer would...
```

`extends` names one parent, found by the same lookup order (the repo's
`.brindle/agents/`, then `~/.brindle/agents/`, then the built-ins). The child's
fields override the parent's, field by field; a field the child leaves out is
the parent's. The child's prompt is appended to the parent's, so the child adds
to the role rather than restating it. Chains work (`a` extends `b` extends
`developer`); a cycle is an error that names the loop. `brindle profile show
<name>` prints the result.

`rules` names **rule packs**: markdown files that give an agent a standing set
of rules, and brindle a few it can check itself. A pack lives in
`.brindle/rules/<name>.md` (the repo's), `~/.brindle/rules/<name>.md` (yours),
or ships with brindle, in that order of precedence; `security/backend` is
`security/backend.md`. A child's packs are added after its parent's.

```markdown
---
name: security/backend
description: Backend code handles untrusted input, secrets and the shell carefully
deny_deps: [pickle]              # names that may not be imported or added to a manifest
require_tests_for: [src/**/*.py] # a change here must come with a test file in the diff
deny_patterns:                   # regexes no added line may match
  - shell=True
  - \beval\(
---
Treat every input that crosses a trust boundary as hostile until validated...
```

The pack's text joins the agent's prompt, for every provider, with the
mechanical rules spelled out. Those three are checked against the branch's
diff (what it commits on top of its base) in the same step that runs the repo's
`checks`: the reviewer's check summary lists each pack as `PASS`/`FAIL` with
the offending `file:line`, and `merge_workspace` refuses the branch while a
pack fails, with the violations to send back to the worker, like a failed
check. `deny_deps` looks at added `import`/`from`/`require` lines and at
dependency manifests (`pyproject.toml`, `requirements*.txt`, `package.json`,
`go.mod`, `Cargo.toml` and the like); `require_tests_for` is satisfied by any
added or changed test file (`tests/`, `test_*`, `*_test.*`, `*.test.*`,
`*.spec.*`, `__tests__/`). A pack with only prose is a prompt and nothing to
check. A pack a profile names but that doesn't exist stops the launch, so a
rule never silently stops applying. These are pattern checks on the diff's
text: they catch the honest mistake and the obvious shortcut and name the
line, and a dependency loaded through a name built at run time, or a test
that tests nothing, is the reviewer's to catch. What they read can't be
hidden: the diff is taken with git's attributes, drivers and prefixes
overridden, and a worker whose profile no longer loads fails the gate rather
than passing with no rules.

Built-in packs: `security/backend` (no shell-outs from input, no `eval`, no
unsafe deserializing, no secrets in code), `tests/only` (change tests, not the
code under test: every diff must touch a test file), `style/minimal-diff` (the
smallest diff that does the job). Built-in profiles that use them:
`backend-auditor` (`reviewer` plus `security/backend`) and `qa` (`developer`
plus `tests/only`).

`brindle profile new <name> --extends developer --rules security/backend` writes
the file; `brindle profile lint` checks every profile it can see (a missing
parent, a cycle, an unknown pack, a pattern that doesn't compile, an unknown
provider) and exits 1 if any fails.

### Guardrails: write scope, read scope, env allowlist (brindle Pro)

```markdown
---
name: api-dev
extends: developer
write_scope: [src/api/**, tests/api/**]
read_scope: [src/**, tests/**, docs/**]
env_allow: [NPM_TOKEN, AWS_*]
---
```

Globs are relative to the worktree; a trailing `/` or a bare directory name
means everything under it, `*` crosses `/`, and case counts.

- **`write_scope`** is enforced on the branch: the same step that checks rule
  packs lists every file the branch changes (deletions, renames and mode
  changes included) and fails the review and merge gates on any outside the
  scope. On top of that, best effort: Claude Code's PreToolUse hook denies an
  Edit or Write outside it, the native runner's file tools refuse one, and the
  worker is told its scope in its prompt. A shell command can still write
  anywhere, and Codex and agy get only the prompt, so the diff check is what
  holds. An empty `write_scope: []` means no limit, not "deny everything".
- **`read_scope`** is best effort only. The native runner's Read refuses a
  file outside it and Glob and Grep leave such files out; Claude Code's hook
  denies a Read outside it, and a Glob or Grep unless the directory searched
  is wholly in scope (a scope of `src/api/**` can search `src/api/`, not
  `src/`). A shell command can read anything, other providers only get the
  prompt, and a read leaves no diff to check afterwards.
- **`env_allow`** (names or globs): the worker's process starts with only
  these variables, brindle's own (`BRINDLE_*`), the basics a process needs
  (`PATH`, `HOME`, `LANG`, `LC_*`, `TERM`, ...), the profile's own `env.NAME`
  lines and the provider's sign-in (Claude Code's keys and cloud backends,
  Codex's keys, a native profile's `api_key_env`). brindle's own secrets (the
  Pro token, GitHub's tokens) never reach a worker, listed or not. Anything
  else is dropped, so a tool that needs `GIT_*`, the proxy variables
  (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) or `NO_COLOR` must have them listed.

Without the `guardrails` feature these keys are ignored, with one warning per
profile, and the profile runs as if they weren't there.

### Learned rules (brindle Pro)

When reviewers keep asking for the same change, brindle suggests a rule for
it. Each review that requests changes is a finding, filed under the repo and
the profile of the worker whose branch it was. After a review like that,
brindle groups the new findings with one small model call (the repo's
`learned_rules_profile`, else a local model that answers, else Claude's
cheapest model; its tokens show up in `brindle history` like any other).
Once the same finding has come up in `learned_rules_repeats` tasks (3 by
default), the supervisor's `get_progress` mentions the suggestion and
`brindle rules suggest` lists it. Nothing changes until you decide:

```sh
brindle rules suggest            # group new findings, then list the suggestions (--no-refresh: just list)
brindle rules accept 3f9c2a1b    # add it to .brindle/rules/learned.md
brindle rules reject 3f9c2a1b    # never suggest it again
```

`.brindle/rules/learned.md` is an ordinary rule pack that every profile in the
repo picks up: each accepted rule is a line of its prose, and when the finding
is a mechanical one (a pattern no added line may match, a dependency not to
add, paths whose changes need a test), it lands in the pack's `deny_patterns`,
`deny_deps` or `require_tests_for` and is checked against every branch's diff.
Commit it so the rule is reviewed and shared; edit or delete lines as with any
pack. Rejections are remembered on this machine: findings that fall under a
rejected rule keep it rejected.

### The org library (brindle Team)

With a Team plan, an org's admins can share profiles and rule packs with every
member. An admin publishes a file with `brindle account org profiles push
reviewer.md` (a rule pack: `push --pack security.md`, named like
`security/backend` with `--name`), and `brindle account org profiles rm <name>`
removes one. `brindle account org profiles` lists what the org shares.

Members get them in the lookup order repo, user, org, built-in (for profiles
and rule packs alike), so a repo or user file of the same name still wins.
`push --pinned` changes that for an item: a pinned org profile or pack is used
over any repo or user file of that name, which is ignored with a warning, so a
repo can't loosen what the org requires.

The library is signed by the backend and verified before use. A copy is kept
in `~/.brindle/pro/profiles-<org>/` and used while the backend can't be
reached (for a week past its signature); with no usable copy brindle uses
only what it has locally, and says so. Air-gap mode never fetches.

### Directories outside the workspace

A worktree is the agent's world, which is the point. Anything shared between
workspaces is outside it — a build cache instead of one per worker, a checked-out
reference repo, a folder of profiles kept beside the repo — and an agent that needs
it stops for a permission nobody can grant: a headless worker is refused, an
interactive one waits for a human who may not be watching.

`add_dirs` names those directories. It belongs in `.brindle/config.json`, because a
shared cache is a property of the repository rather than of a role, and every
profile the repo launches needs the same list:

```json
{ "add_dirs": ["/srv/cargo-cache", "vendor/reference"] }
```

A profile may add to that list for a role that needs more, and never removes from
it:

```markdown
---
name: developer
provider: claude
add_dirs: /srv/extra
---
```

Both are passed as `--add-dir`, once per entry. Four things to know:

- **It is full tool access, not read access.** Claude Code's own help says
  "directories to allow tool access to": edits and Bash reach them too, so a worker
  in `acceptEdits` or `auto` can write into a directory you thought of as reference
  material, and four parallel workers can write into a shared cache with no prompt.
- **`CLAUDE.md` in those directories is loaded**, which is worth knowing before you
  add a directory that has one.
- **Relative entries resolve against the repo root**, not the worktree the agent
  runs in, and a leading `~` means your home directory. That holds in a profile in
  `~/.brindle/agents` too, so `~/refs` there is the same directory in every repo,
  while a relative entry there resolves against whichever repo the agent runs in.
- **A directory that does not exist is reported**, because Claude Code ignores a
  missing `--add-dir` silently, which would be the failure this field exists to
  prevent. `brindle doctor` checks the list; a launch from the terminal says so on
  stderr; `handoff`, `assign` and a queued task's start put it in what they tell
  the supervisor.

### Subagent workers

The built-in `subagent` profile (`provider: subagent`) gives a Claude Code
supervisor brindle's worktree and branch handling for work done by its **own**
subagent (its Agent tool), with no separate `claude` process. `handoff` or
`assign` with it creates the workspace and returns immediately. The reply
contains the worktree path, the branch, the agent id and a ready-made prompt
for the Agent tool. That prompt tells the subagent to work only in that
directory, commit there and end with a summary. The supervisor then calls
`complete_subagent(agent_id, result)`. The worker shows as working until then
and done after. `workspace_diff`, `request_review`, `merge_workspace` and
`remove_workspace` work as usual. brindle can't message a subagent, and
`send_message` says so. Worktrees live under `~/.brindle/worktrees/`, outside the
supervisor's own directory. Unless the supervisor runs with permission to edit
there (`--add-dir ~/.brindle/worktrees`, or `additionalDirectories` in Claude
Code settings), the subagent's edits ask for approval.

## Open-weight models

brindle can run workers and reviewers on free, open-weight models (Qwen3-Coder,
GLM, DeepSeek, Kimi, gpt-oss, ...) served locally by Ollama, LM Studio or
llama.cpp, or by a hosted API. There are two ways in.

### The native provider

`provider: native` runs brindle's own agent loop instead of a third-party CLI:
brindle talks to the model's chat endpoint directly, runs its tool calls (Read,
Write, Edit, Glob, Grep, Bash, plus brindle's `report_result`, `send_message`,
`submit_review` and `workspace_diff`), and reports the worker's status itself.
No hooks, no screen scraping, and messages sent to the worker arrive between
its model calls. It works with any OpenAI-compatible chat-completions endpoint
or Anthropic Messages endpoint.

```markdown
---
name: developer-local
provider: native
api: openai                        # openai (chat completions) | anthropic (messages)
base_url: http://localhost:11434/v1
model: qwen3-coder:30b
context_tokens: 32k                # the model's window, less room for its reply
api_key_env: OPENROUTER_API_KEY    # optional: the variable holding the key
permission_mode: acceptEdits
allowed_tools: Bash(git add:*), Bash(git commit:*), Bash(uv run:*), Bash(pytest:*)
---
You are a developer agent running under brindle...
```

The built-in `developer-local` and `reviewer-local` profiles are set up for
Ollama with `qwen3-coder:30b` (19 GB; runs on a 32 GB machine). To use them:

```
brew install ollama            # or https://ollama.com/download
ollama pull qwen3-coder:30b
brindle doctor                   # "model qwen3-coder:30b ... is available"
```

Run `ollama serve` yourself, or let brindle do it, with no setup. A 30B model holds
about 20 GB of GPU memory, so set `"local_models": false` if you don't want brindle
to start or preload one. When `brindle` (or `brindle continue`)
starts and a native profile points at Ollama on this machine that isn't answering,
brindle starts it in the background with `OLLAMA_CONTEXT_LENGTH` set to the largest
`context_tokens` any profile asks of it plus room for the reply (40960 for the
built-ins), then loads each profile's model so the first task doesn't wait on the
read from disk. Its output goes to `~/.brindle/ollama.log`. Only Ollama on a
loopback address is started; a remote endpoint is yours to run. A server you
started yourself (with whatever settings) is left alone.

A server brindle started is stopped again, along with the model it holds in memory,
when the last brindle session that uses it ends: its chat is closed or paused, or
its tmux goes away (the next cleanup sweep catches that). Starting a new session
in the same checkout keeps it running for the new one.

Then a supervisor can `assign` a task to `developer-local`, or the repo config
can make the free model the reviewer: `"review_profile": "reviewer-local"`.
Other endpoints, same fields:

| Backend | `api` | `base_url` | Notes |
|---|---|---|---|
| Ollama (local) | openai | `http://localhost:11434/v1` | free; `ollama pull <model>` first |
| LM Studio | openai | `http://localhost:1234/v1` | free; load the model in the app |
| llama.cpp `llama-server` | openai | `http://127.0.0.1:8080/v1` | free; start with `--jinja` for tool calls |
| OpenRouter | openai | `https://openrouter.ai/api/v1` | `:free` models; `api_key_env: OPENROUTER_API_KEY` |
| Z.ai GLM | anthropic | `https://api.z.ai/api/anthropic` | GLM-4.7-Flash is free; `api_key_env: ZAI_API_KEY` |
| DeepSeek | anthropic | `https://api.deepseek.com/anthropic` | paid; `api_key_env: DEEPSEEK_API_KEY` |
| Anthropic | anthropic | `https://api.anthropic.com` | `api_key_env: ANTHROPIC_API_KEY` |

What to expect: a 30B-class local model does well on small, well-specified
tasks (the kind brindle hands out: one change, the test named up front) and on
reviews of modest diffs, and less well on long multi-step work. The native
loop keeps it on rails: exact-match edits that fail loudly, one command at a
time, a reminder to report when a turn ends without one, and old context
folded into a summary when the window fills. Tool-calling quality varies by
model; if a model keeps mis-forming tool calls, try another (`qwen3-coder`,
`gpt-oss:20b` and `glm-4.7-flash` all support tools in Ollama). Set
`OLLAMA_CONTEXT_LENGTH` to at least `context_tokens` plus reply room, or Ollama
silently truncates the conversation. Native workers are always headless (no
TUI to attach to): `brindle agent peek` shows each turn's prompt, tool calls and
answer, and `brindle send` talks to them.

### Claude Code on another backend

Claude Code itself can be pointed at any Anthropic-compatible endpoint. A
profile's `env.NAME: value` lines set that up, and everything else about the
worker (hooks, permissions, the MCP tools) stays the same:

```markdown
---
name: developer-glm
provider: claude
model: glm-4.7-flash
env.ANTHROPIC_BASE_URL: https://api.z.ai/api/anthropic
env.ANTHROPIC_AUTH_TOKEN: ${ZAI_API_KEY}     # brindle doesn't expand this: put the key itself here, or in ~/.brindle/agents
env.ANTHROPIC_API_KEY:
---
```

Ollama (0.14+) serves the Anthropic API too: `env.ANTHROPIC_BASE_URL:
http://localhost:11434` with `env.ANTHROPIC_AUTH_TOKEN: ollama`. Anthropic
documents this route as unsupported for non-Claude models, and each vendor
documents its own quirks (no prompt caching on most, smaller context windows),
so prefer the native provider for open-weight models and keep this route for
Claude itself behind a gateway.

To run workers on your own Bedrock, Vertex, Codex or Antigravity account, see
[Running workers on your own cloud](#running-workers-on-your-own-cloud) below.

Codex agents report status through Codex's `notify` hook (brindle passes
`-c notify=[...]` at launch, leaving your own Codex config alone): a completed turn
marks the agent idle and delivers any queued message. Codex has no Stop hook, so for
an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
going, with the same limit on reminders as Claude Code.

### Running workers on your own cloud

Any brindle user, free tier included, can run a worker's model on their own cloud
account. A profile's `env.NAME: value` lines are set in the agent's environment
before brindle's own variables, so the profile decides which backend the worker
uses. Profiles live in `.brindle/agents/` in the repo, or in your user profiles
folder; `brindle profile new` writes one.

```markdown
---
name: developer-bedrock
provider: claude
model: us.anthropic.claude-haiku-4-5-20251001-v1:0   # a Bedrock model id your account can use
env.CLAUDE_CODE_USE_BEDROCK: 1
env.AWS_REGION: us-east-1
---
```

Pass `developer-bedrock` as the worker profile (`agent_profile` in `assign`), or
list it under `routing` in `.brindle/config.json`.

- **Claude Code on Bedrock.** Set `env.CLAUDE_CODE_USE_BEDROCK: 1` and
  `env.AWS_REGION`. The worker uses the AWS credentials on this machine (an
  `AWS_PROFILE` from `~/.aws/config`, SSO or environment keys), so sign in to AWS
  first. See [Claude Code on Amazon Bedrock](https://code.claude.com/docs/en/amazon-bedrock).
- **Claude Code on Vertex AI.** Set `env.CLAUDE_CODE_USE_VERTEX: 1`,
  `env.CLOUD_ML_REGION` and `env.ANTHROPIC_VERTEX_PROJECT_ID`. The worker
  authenticates with Application Default Credentials, so run
  `gcloud auth application-default login` once. See
  [Claude Code on Google Vertex AI](https://code.claude.com/docs/en/google-vertex-ai).
- **Codex on Bedrock.** Codex picks its provider in `config.toml` in its home
  folder (`$CODEX_HOME`, default `~/.codex`). Point that file at the
  `amazon-bedrock` provider, as described in
  [Codex advanced configuration](https://learn.chatgpt.com/docs/config-file/config-advanced).
  To give one profile its own Codex home, add `env.CODEX_HOME: /path/to/home`
  to it. Each Codex home keeps its own sign-in (`auth.json`).
- **Google Antigravity.** `agy` can run on a Gemini API key: set
  `"modelProvider": "gemini"` in `~/.gemini/antigravity-cli/settings.json`, and
  export `GEMINI_API_KEY` in your shell, or put it in a profile in your user
  profiles folder; never in a profile under `.brindle/agents/`, which is committed. For a
  Gemini-compatible gateway, set `env.GOOGLE_GEMINI_BASE_URL` to its URL. As of
  1.3.4, `agy` ignores Vertex AI credentials, so use a Gemini API key or gateway.
  The full setup is in [Google Antigravity](#google-antigravity) below.

If your org runs brindle for a team, its admins can set a route for everyone in
the org's Routing settings on the brindle dashboard; an enforced org route replaces
a profile's `env.` lines, so a profile can't move a worker off the company's route. See
[Using your company's access](#using-your-companys-access).

### Using your company's access

Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
Foundry, a Team or Enterprise plan) and Codex, without handling keys:

- **Your org sets it up (brindle Team).** When your org's policy has an
  `agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
  in to the route the company chose and `brindle doctor` shows which identity
  each agent uses. It always wins over anything below.
- **You set it up yourself (any plan).** When your org has no `agent_setup`,
  `brindle login` looks, read-only, at what this machine is already signed in
  to: Claude Code's managed settings (if your IT configures Claude Code,
  brindle just uses it), `claude auth status`, AWS SSO profiles in
  `~/.aws/config`, the active `gcloud` project, the active `az` subscription
  and `codex login status`. It never reads tokens or credential files. It
  proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
  the default is no, and with no terminal it is always no. A yes is saved as
  `"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
  policy's (`enforce` is never applied to it), and `brindle doctor` labels it
  as self-serve and says who is billed.

You must be authorized to use any company credentials you connect: brindle only
signs in with the access you already have, it doesn't grant any.

## Google Antigravity

brindle runs Google Antigravity's terminal agent, `agy`, as well as Claude Code and
Codex. Install it and sign in once:

```sh
curl -fsSL https://antigravity.google/cli/install.sh | bash
agy        # sign in with your Google account, then quit
```

To run unattended workers without a browser login, `agy` (1.1.13 or newer) can use
a Gemini API key instead: put `"modelProvider": "gemini"` in
`~/.gemini/antigravity-cli/settings.json` and set `GEMINI_API_KEY`, in your
environment or in a profile's `env.GEMINI_API_KEY: ...` line in your user
profiles folder (never in `.brindle/agents/`, which is committed). The key alone does nothing: without `modelProvider`, `agy` still asks
you to sign in. `agy` takes no other key or token from the environment; its
Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
chosen on its sign-in screen.

Then run the whole session on it with `brindle --provider antigravity`, or mix models:
give a profile `provider: antigravity` (for example a `gemini-reviewer` for a second
model's review) and the supervisor can hand it tasks.

`agy` has no command-line options for hooks, MCP servers or instructions, so brindle
adds three files to the checkout's `.agents/` folder: `mcp_config.json` (brindle's tools),
`hooks.json` (status, messages, autopilot) and `rules/brindle.md`. They're listed in
`.git/info/exclude`, so they never show up in `git status`. brindle adds to these files if
you already have them, and won't change one that's committed. Each agent's first
message is a short warm-up with its instructions, because `agy` connects MCP servers
only once a conversation has started.

**Permissions.** `agy` doesn't let hooks approve shell commands, so an Antigravity
agent asks before running anything your own `agy` settings don't already allow, and
the sidebar shows it as needing you. To let agents run tests and commit without asking,
add rules to `~/.gemini/antigravity-cli/settings.json`, for example:

```json
{ "permissions": { "allow": ["command(uv run pytest)", "command(git status)",
                               "command(git diff)", "command(git add)", "command(git commit)"] } }
```

## How it works

- **Look:** brindle's tmux sessions get their own dark purple theme and mouse
  scrolling. Your own tmux setup and other sessions are untouched (apart from
  tmux's `focus-events`, which Claude Code asks for).
- **State** lives in `~/.brindle/brindle.db` (SQLite, WAL mode). The CLI, the hooks,
  and every agent's MCP server share it. Worktrees live in
  `~/.brindle/worktrees/<repo>/<branch>`, and the base branch is recorded in git
  config as `branch.<b>.brindle-base`.
- **Messages arrive through Claude Code's own inbox.** Every Claude Code session
  listens on a socket for messages from other sessions, and tells its hooks where
  it is; brindle's SessionStart hook records it, and from then on messages go there
  instead of being typed into the pane. An idle agent starts a new turn with the
  message; a busy one gets it between tool calls. Typing into the pane is the
  fallback when there's no socket (Codex, Antigravity, older Claude Code). A
  `crossSessionInbound` of `hold` or `refuse` in your Claude settings would hold or
  drop them.
- **Agent status comes from hooks, not screen-scraping.** Guessing an agent's
  state by pattern-matching terminal output breaks whenever a CLI redesigns its
  interface. brindle launches Claude Code with `--settings` hooks
  (`SessionStart`, `UserPromptSubmit`, `Stop`, `StopFailure`, `Notification`) that call
  `brindle _hook <event>`. The `Stop` hook also delivers queued messages: it
  returns `{"decision": "block", "reason": <message>}`, so Claude continues with
  the message as its next instruction and nothing is typed into a busy terminal.
  A message queued for an *idle* agent is typed in instead, but only once brindle
  checks the screen and finds a clear chat input: not text you're still typing,
  and not Claude Code's background-session launcher (which would otherwise
  start a whole new session). Otherwise it stays queued for the next chance.
- **Results are explicit.** Workers call the `report_result` MCP tool instead of
  having their output parsed from the screen. If a worker stops without
  reporting, the Stop hook reminds it once.
- **Worker isolation:** `handoff`/`assign` with `isolate=true` (the default)
  create a worktree whose branch starts from the *supervisor's* current branch,
  so workers build on the supervisor's committed work. `merge_workspace`
  brings a worker's branch back.

## Development

```sh
uv sync
uv run pytest
```

### CI

GitHub runs the tests on every pull request and every push to `main`: one Ubuntu
and one macOS job, on Python 3.12. A change that only touches docs runs nothing.

You can also run the same tests locally, on a clean export of the commit:

```sh
scripts/ci_local.sh                   # Python 3.12 here and on Linux (Docker), tests in parallel
scripts/ci_local.sh --full            # 3.11, 3.12 and 3.13, here and on Linux
```

The pre-push hook that ran these before every push is off for now
(`git config core.hooksPath .githooks` turns it back on).

### Releasing

1. Bump `version` in `pyproject.toml`, commit, and push.
2. Write the release notes, run `scripts/release_footer.py NOTES.md` to add the docs link,
   and create a GitHub release tagged `v<version>` (e.g.
   `gh release create v0.1.1 --notes-file NOTES.md`).
3. The Publish workflow tests, builds, and uploads to PyPI via Trusted Publishing.


## License

brindle is source-available, not open source: you may install and use it under the
[brindle License 1.0](LICENSE), which does not allow changing or redistributing it,
using it for the competing services listed in [SCHEDULE-A](SCHEDULE-A), or getting
around paid-feature checks.
